c++ - 本地变量的内存可以在其范围外部访问?

  显示原文与译文双语对照的内容

我有以下代码。


int * foo()
{
 int a = 5;
 return &a;
}

int main()
{
 int* p = foo();
 cout <<*p;
 *p = 8;
 cout <<*p;
}

代码运行时没有运行时异常 !

输出是 5 8

怎么可能不是局部变量的内存在它的函数之外不可访问?

时间:

怎么可能不是局部变量的内存在它的函数之外不可访问?

租用酒店房间。 你把书放在床头柜的顶部抽屉,然后睡觉。 你第二天早上退房,但"忘了"把你的钥匙还给我。 你偷钥匙 !

一周后,你回到旅馆,不要入住,用偷来的钥匙潜入你的旧房间,然后在抽屉里查找。 你的书还在那里。 令人惊讶 !

? 如果你没有租用房间,则旅馆房间抽屉的内容不可访问?

显然,这个场景可以在真实世界中发生没有问题。 当你不再被授权进入房间时,没有神秘的力量会让你的书消失。 也没有一个神秘的力量可以阻止你进入一个带有被盗钥匙的房间。

在酒店管理不需要 以删除你的书。 你没有跟他们签合同,这意味着如果你留下了东西,他们会把你撕碎。 同时,如果你给非法re-enter你的房间提供能量钥匙要让它回来,酒店的安检人员是不能要求 抓住 sneaking. 你没有把它们做成一个合同,也就是说"如果我以后想偷偷回我的房间,你需要阻止我。""我保证以后不回我的房间"说,与他们相反,你签了合同,一个约定,该约定你中断了。

在这种情况下中一切皆有可能 。 这本书可以在那里,你很幸运。 其他人的书可以在那里,你可以在酒店的炉子里。 当你进来的时候有人会在那里,把你的书撕成碎片。 旅馆可以把桌子和书完全删除,用衣柜替换掉。 在一次爆炸中,而你是偷渡来around,整个酒店可能是刚要被撕开记下并替换为一个足球体育场,你等会要模具。

你不知道将会发生什么是事情;当你已经从酒店和偷了一把钥匙来非法使用了以后,你就放弃了权利处于可以预测的,安全的环境,因为你选择系统的去打破规则。

C++ 不是安全语言 。 它会愉快地让你打破系统的规则。 如果你想做一些非法和愚蠢的事情,比如回到一个房间,你没有被授权进入一个可能不再存在的办公桌,C++ 不会阻止你。 比 C++ 更安全的语言是通过对密钥进行更严格的控制来解决这个问题的,例如。

更新

Holy,这个答案引起了很多关注。 ( 我不知道为什么--认为它只是一个"趣味"小的类比,但不管怎样)

我认为用一些技术想法来更新这一点可能有关系。

编译器正在生成代码,这些代码管理由该程序操作的数据的存储。 生成管理内存的代码有很多种不同的方法,但是随着时间的推移,两种基本技术已经根深蒂固。

第一种方法是有某种"长时间居住"存储中的每个字节的存储区,用于保存该"使用寿命"--即,当它是合式公式的时间段并加以若干程序变量提前--不易被预测。 编译器生成对"堆管理器"的调用,它知道当需要时如何动态分配存储并在不再需要时回收它。

第二个是拥有某种"短寿命"存储区域,存储中每个字节的生命周期都是已知的,特别是存储的生命周期遵循"嵌套"模式。 也就是说,短期变量的longest-lived分配严格重叠了shorter-lived变量的分配。

局部变量遵循后一种模式;当输入一个方法时,它的局部变量就会出现。 当该方法调用另一个方法时,方法变量的新局部会出现。 在方法变量的第一个局部消失之前,它们将死掉。 局部变量相关的存储生命周期的开始和结束的相对顺序可以提前计算出来。

出于这个原因,局部变量通常作为存储在"堆栈"数据结构中生成,因为堆栈具有第一个推送到它的属性将是最后一个事件。

这就像酒店决定按顺序出租房间,你不能在每个房间都有房间号的情况下检查出房间。

我们来考虑一下堆栈。 在许多操作系统中,每个线程都有一个堆栈,堆栈被分配为一定的固定大小。 当你调用一个方法时,东西被压入堆栈。 然后如果你传递一个指向堆栈的指针回从你的方法,因为原来的海报在这里做的,那只是一个指向中间的一些完全有效million-byte内存块。 在我们的类比中,你从酒店退房;当你做的时候,你只是检查了highest-numbered占用的房间。 如果在你,你回到你的房间中没有其他人会检查非法,你所有的东西都是能确保继续保持但在这个特定的酒店 。

我们为临时商店使用堆栈,因为它们非常便宜和容易。 不需要 C++的实现来使用堆栈来存储局部变量;它可以使用堆。 它不存在,因为这会使程序变慢。

在"聊天室"刚才 vacated, C++ 不是必需的,以保持你的垃圾存放在堆栈上不动的实现,以便你可以回来,以后为它非法;它为编译器生成代码是完全合法的固定使用的转动的恢复为0 这不是因为再次,这将是昂贵的。

C++的实现不是必需的,以确保当堆栈逻辑收缩时,曾经有效的地址仍然映射到内存。 实现被允许告诉操作系统"我们已经使用了这个堆栈的页面"。 除非我说否则,如果有人碰到previously-valid堆栈页面,发出一个破坏进程的异常。 同样,实现并没有真正做到这一点,因为它是缓慢和不必要的。

相反,实现让你犯错并摆脱它。 大多数情况下,直到某一天发生了真正糟糕的事情,进程会爆炸。

这是有问题的。有很多规则,而且很容易被破坏。 我当然有很多时间。 和更糟糕的是,该问题通常只有表面检测,以被破坏数十亿纳秒之后内存损坏时会发生,当它很难找出是谁乱了起来。

更多的memory-safe语言通过限制你的能力来解决这个问题。 在"普通"C# 中,没有办法获取本地的地址,返回它或者存储它以备以后使用。 你可以获取本地的地址,但语言设计巧妙,因此在本地结束后无法使用它。 为了获取本地的地址并将它的传递回去,你必须将编译器置于一个特殊的"不安全"模式中,和将单词"不安全"放在你的程序中,以便引起你的注意。

要进一步阅读:

你在这里做的只是读写内存 用于a的地址。 现在你已经超出了 foo,它只是一个指向一些随机内存区域的指针。 在你的例子中,内存区域确实存在,而且目前没有其他东西正在使用它。 你不能通过继续使用它来破坏它,也没有其他东西覆盖它。 因此,5 仍然存在。 在实际的程序中,该内存便会立即re-used几乎和你只有破坏了某些事物通过这样做( 虽然可能不会出现,直到很久以后的症状) !

当你从 foo 返回时,你告诉操作系统你不再使用那个内存,它可以被重新分配给其他的。 如果你很幸运,而且它从来没有被重新分配,并且操作系统不会再用它,那么你就会摆脱谎言。 你可能会在任何结束于那个地址的地方结束。

现在,如果你想知道编译器为什么不抱怨,可能是因为 foo 被优化消除了。 它通常会警告你这种事情。 ( 没有对 a 引用本身 foo 之外,) 假设你知道你正在做什么,在技术上你没有违反这里的范围,只有内存访问规则,它只触发一个警告而不是一个错误。

简而言之:这通常不会奏效,但有时会有机会。

因为存储空间还不是 stomped 。 不要指望那个行为。

在 C++,你 访问任何地址,但它并不意味着你可以应该 。 你正在访问的地址不再有效。 它 因为别的事情扰乱了工作内存返回foo之后的,但在许多情况下它可能会崩溃。 尝试使用 Valgrind 来分析你的程序,或者只是编译它优化,然后查看。

你从不通过访问无效的内存来抛出 C++ 异常。 你只是给出了引用任意内存位置的一般概念的示例。 我可以这样做:


unsigned int q = 123456;

*(double*)(q) = 1.2;

这里我简单地把 123456当作一个double的地址,并写入它。 任何事情都可能发生: 1 ) q 实际上实际上是一个 double,比如的有效地址 double p; q = &p; 2 ) q 可能指向已经分配内存内的某个地方,而我只覆盖 8字节。 3 ) q 点超出分配的内存,系统管理器的操作内存向我的程序发送一个分段故障信号,导致运行时终止它。 4 ) 你赢得了彩票。

你设置它的方式是更合理的,返回的地址指向一个有效的内存区域,因为它可能只是堆栈中的一个很小的部分,但它仍然是一个不能以确定性方式访问的无效位置。

在正常的程序执行过程中,没有人会自动检查内存地址的语义有效性。 但是,像 valgrind 这样的内存调试器会很高兴做到这一点,所以你应该通过它运行你的程序并见证错误。

对所有答案的一点补充:

如果你这样做:


#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("%dn",*p);
}

输出可能是: 7

这是因为在从 foo() 返回之后堆栈被释放,然后被 boo() 重用。 如果你deassemble的可执行文件,你会看到它很清楚。

是否使用optimiser启用了程序编译?

foo() 函数非常简单,可能已经在结果代码中被内联/替换。

但我用标记为aggree的表示结果行为未定义。

你的问题与的范围无关。 在代码中使用这里 名称外 foo 你显现,则函数 main 没有看到这些名字在函数foo中,因此你无法访问 a的大小

你所遇到的问题是程序在引用非法内存时不发出信号。 这是因为 C++ 标准在非法内存和合法内存之间没有明确的界限。 在弹出的堆栈中引用某些内容有时会导致错误,有时不会导致。 这取决于,不要指望这个行为。 假设它总是会在你运行时产生错误,但假设在调试时不会出现错误。

在典型的编译器实现中,你可以将代码看作"打印内存块的值,地址是曾经被a 占用的"。 另外,如果你添加新的函数调用到一个函数,该函数在本地 int constains a的值是个很好的机会,( 或者 a 用来指向的内存地址) 更改。 这是因为堆栈将被一个包含不同数据的新框架覆盖。

但在它来work,上,这属于未定义行为和你不应该完全 !

它之所以工作是因为堆栈没有被改变,因为它被放在了那里。 叫几个其他函数( 它也在调用其他函数) a 访问之前,你将再次很可能再不会如此幸运了。 ;- )

...