我有以下的代码。
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
而且代码只是运行而没有运行时异常!
输出为58
怎么可能?一个局部变量的内存不是在其函数外部无法访问吗?
我有以下的代码。
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
而且代码只是运行而没有运行时异常!
输出为58
怎么可能?一个局部变量的内存不是在其函数外部无法访问吗?
怎么可能?局部变量的内存不是在函数外部无法访问吗?
你租了一间酒店房间,在床头柜的顶层抽屉里放了一本书,然后去睡觉了。第二天早上你退房了,但“忘记”归还钥匙。你偷了钥匙!
一周后,你回到了酒店,没有办理入住手续,用偷来的钥匙溜进了你以前的房间,看了看抽屉里。你的书还在那里,惊人!
这怎么可能?如果你没有租房间,酒店房间抽屉里的内容不是无法访问吗?
显然,这种情况在现实中可以轻易发生。没有神秘的力量会导致你离开房间后书本消失。也没有神秘的力量阻止你用偷来的钥匙进入房间。
酒店管理方并不必须清理你的书。你没有与他们签订合同,说如果你遗留物品,他们会为你销毁它。如果你用偷来的钥匙非法进入房间取回物品,酒店保安不必须抓住你溜进去。你没有与他们签订合同,说“如果我试图以后潜回房间,你必须阻止我。”相反,你与他们签订了一份合同,承诺“我保证不会在以后偷偷回到我的房间”,而你违约了。
在这种情况下,任何事情都可能发生。书可能在那里——你很幸运。别人的书可能在那里,而你的书可能已经被酒店的炉子烧掉了。当你进来的时候,有人可能正在那里,把你的书撕成碎片。酒店可能已经把桌子和书全部拿走了,并用衣柜代替了它们。整个酒店可能就要被拆除,换成一个足球场,而你正在偷偷摸摸地四处溜达时,会死于爆炸。你不知道会发生什么;当你从旅馆退房并偷了一把钥匙以后,你放弃了生活在一个可预测、安全的世界的权利,因为你选择打破系统的规则。
C++不是一种安全的语言。它会欣然让你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你没有权限进入的房间并翻找可能已经不存在的抽屉,C++不会阻止你。比C++更安全的语言通过限制你的权力来解决这个问题——例如更严格地控制钥匙。
天哪,这个答案引起了很多关注。(我不知道为什么——我认为它只是一个“有趣”的比喻,但无论如何。)
我想更新一下这个问题,加入一些更多的技术思考。
编译器的工作是生成管理程序所操作的数据存储的代码。有很多不同的生成内存管理代码的方式,但随着时间的推移,两种基本技术已经得到了巩固。
第一种方法是拥有某种“长期存储”区域,其中存储中每个字节的“寿命”,即其与某个程序变量有效关联的时间段,不能事先轻易预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储空间,并在不再需要时回收它。我们使用堆栈作为临时存储,因为它们非常便宜和易于使用。C++的实现不需要使用堆栈来存储本地变量;它可以使用堆。但实际上并没有这样做,因为那会使程序变慢。
C++的实现不需要保留您在堆栈上留下的垃圾,以便您以后非法返回获取它;编译器生成代码将"房间"中的所有内容都归零是完全合法的。但它不这样做,因为那样会很昂贵。
C++的实现不需要确保当堆栈逻辑缩小时,以前有效的地址仍然映射到内存中。该实现允许告诉操作系统“我们现在不再使用此堆栈页面。除非我另有说明,否则发出一个异常,如果任何人触摸以前有效的堆栈页面,则销毁进程”。同样,实现实际上并不这样做,因为这是缓慢和不必要的。
相反,实现允许您犯错误并逃脱惩罚。大多数时候。直到某天出现真正可怕的问题,进程才会崩溃。
这是一个问题。有许多规则,很容易意外地违反它们。我肯定已经犯了很多次。更糟糕的是,当检测到内存损坏时,问题通常只会在十亿纳秒之后浮出水面,此时很难弄清楚是谁搞砸了它。a
地址的内存。现在你已经离开了foo
,它只是一个指向某个随机内存区域的指针。恰好在你的例子中,那个内存区域确实存在,并且此时没有其他东西在使用它。5
仍然存在。在真正的程序中,该内存将几乎立即被重用,这样做会破坏程序(尽管症状可能要到很久之后才会出现!)。foo
返回时,你告诉操作系统你不再使用那块内存,它可以被重新分配给其他东西。如果你很幸运,它永远不会被重新分配,并且操作系统不会发现你再次使用它,那么你就可以逃脱这个谎言。但很有可能你最终会覆盖掉那个地址上的其他内容。foo
。通常它会警告你这种情况。C假设你知道自己在做什么,从技术上讲,你没有违反作用域(a
本身在foo
外没有引用),只是违反了内存访问规则,这只会触发一个警告而不是错误。因为存储空间尚未被覆盖。不要指望这种行为。
对于所有答案的补充:
如果你做了以下这样的事情:
#include <stdio.h>
#include <stdlib.h>
int * foo(){
int a = 5;
return &a;
}
void boo(){
int a = 7;
}
int main(){
int * p = foo();
boo();
printf("%d\n", *p);
}
输出可能是:7
这是因为在从 foo() 返回后,堆栈被释放并由 boo() 重新使用。
如果您反汇编可执行文件,将会清楚地看到它。
foo()
,退出,然后进入 boo()
。Foo()
和 Boo()
都在相同位置的堆栈指针处进入。然而,这不是应该依赖的行为。其他“东西”(如中断或操作系统)可以在调用 boo()
和 foo()
之间使用堆栈,修改其内容... - Russ Schultz在 C++ 中,你可以访问任何地址,但这并不意味着你应该这样做。你正在访问的地址已经无效了。它能够工作是因为在 foo 返回后没有其他东西破坏了内存,但在许多情况下它可能会崩溃。尝试使用 Valgrind 分析你的程序,甚至只是编译优化,看看...
访问无效内存时,并不会抛出 C++ 异常。你只是举了一个引用任意内存位置的一般性例子。我也可以像这样做:
unsigned int q = 123456;
*(double*)(q) = 1.2;
我这里简单地将123456视为double的地址并将其写入。可能会发生以下任何一种情况:
q
实际上可能是一个有效的double地址,例如:double p; q = &p;
。q
可能指向已分配内存中的某个位置,我只是覆盖了其中8个字节。q
指向已分配内存之外,操作系统的内存管理器将向我的程序发送一个分段故障信号,导致运行时终止。你设置的方式使返回的地址在内存中的一个有效区域中更为合理,因为它可能只是栈中稍微往下一点,但它仍然是一个无效的位置,你不能以确定的方式访问它。
在正常程序执行期间,没有人会自动为您检查像那样的内存地址的语义有效性。但是,内存调试器(例如Valgrind)将乐意执行此操作,因此您应该通过它运行您的程序并观察错误。
你编译程序时启用了优化器吗?foo()
函数非常简单,可能已被内联或替换为生成的代码。
但我同意Mark B的观点,结果的行为是未定义的。
5
就会被改变... - Tomas你的问题与作用域无关。在你展示的代码中,函数main
看不到函数foo
中的变量名,所以你不能在foo
外直接使用this名字访问a
。
你遇到的问题是为什么程序在引用非法内存时没有发出错误信号。这是因为C++标准并没有明确规定非法内存和合法内存之间的很清晰的界限。有时从弹出的堆栈中引用某些东西会导致错误,有时则不会。这取决于环境。不要依赖这种行为。当你编程时,应该假设它总是会产生错误,但当你进行调试时,则应该假设它永远不会发出错误信号。
这是因为自从放置了变量a之后,栈还没有被改变(但现在还没有)。在再次访问a
之前,调用一些其他函数(这些函数也在调用其他函数),你可能就不那么幸运了... ;-)