局部variables的内存是否可以在其范围外访问?

我有以下代码。

int * foo() { int a = 5; return &a; } int main() { int* p = foo(); cout << *p; *p = 8; cout << *p; } 

而代码只是运行没有运行时exception!

产量是58

怎么会这样? 是不是一个局部variables的内存不能访问其function?

怎么会这样? 是不是一个局部variables的内存不能访问其function?

你租一个旅馆房间。 你把一本书放在床头柜的最上面的抽屉里,然后去睡觉。 你第二天早上退房,但“忘记”给你的钥匙。 你偷了钥匙!

一个星期后,你回到旅馆,不要登记入住,用偷来的钥匙偷偷摸进你的旧房间,然后看着抽屉。 你的书还在那里。 惊人!

怎么可能? 如果您没有租用房间,那么酒店房间抽屉里的内容是不是不可用?

那么,很明显,这种情况可能发生在现实世界中没有问题。 没有神秘的力量,当你不再被授权进入房间时,你的书会消失。 也没有一种神秘的力量可以阻止你进入一个被盗钥匙的房间。

酒店pipe理部门不需要删除您的图书。 你没有和他们签订合同,说如果你把东西留在后面,他们会为你撕碎。 如果你用偷来的钥匙非法重新进入你的房间把钥匙还给你,那么酒店保安人员就没有必要让你偷偷溜进去。你没有与他们签订一份合同,说“如果我试图潜入我的房间过后,你必须阻止我。“ 相反,你和他们签了一个合同,说“我保证以后不要偷偷回到我的房间里”,这是你打破的合同。

在这种情况下, 什么都可能发生 这本书可以在那里 – 你很幸运。 别人的书可以在那里,你的可以在酒店的炉子里。 当你进来的时候,有人会在那里,把你的书撕成碎片。 酒店本来可以把桌子全部拿走,然后把它换成衣柜。 整个酒店可能即将被拆除,换成一个足球场,而你正在偷偷摸摸地死在爆炸中。

你不知道会发生什么, 当您离开酒店偷走了一把钥匙后被非法使用时,您放弃了在一个可预测,安全的世界中生活的权利,因为select违反了系统的规则。

C ++不是一种安全的语言 。 它会高兴地让你打破系统的规则。 如果你试图做一些非法和愚蠢的行为,比如回到一个房间,那么你不能通过一个可能不在那里的桌子翻找,C ++不会阻止你。 比C ++更安全的语言通过限制你的能力来解决这个问题 – 例如,通过对密钥进行更严格的控制。

UPDATE

圣善,这个答案引起了很多的关注。 (我不知道为什么 – 我认为这只是一个“有趣”的小类比,但无论如何)。

我认为用更多的技术思想来进行更新可能是有密切关系的。

编译器负责生成用于pipe理由该程序操作的数据的存储的代码。 生成代码来pipe理内存有很多不同的方式,但随着时间的推移,两种基本的技术已经成为根深蒂固的。

首先是要有某种“长寿命”的存储区域,其中存储器中每个字节的“生命周期” – 也就是与某个程序variables有效关联的时间段 – 不能容易地预测到的时间。 编译器将调用生成为“堆pipe理器”,知道如何在需要时dynamic分配存储,并在不再需要时将其回收。

第二种是拥有某种“短暂”存储区,其中存储器中每个字节的生命周期是众所周知的,特别是存储的生存期遵循“嵌套”模式。 也就是说,寿命最长的短期variables的分配严格地与其后的短期variables的分配重叠。

局部variables遵循后一种模式; 当一个方法被input时,它的局部variables就会生效。 当该方法调用另一个方法时,新方法的局部variables就会生效。 在第一个方法的局部variables死亡之前,它们将会死亡。 可以提前计算与局部variables相关的存储的生命周期的开始和结束的相对顺序。

因为这个原因,局部variables通常是作为“堆栈”数据结构中的存储来生成的,因为堆栈的属性是第一件事就是popup最后一个东西。

这就像酒店决定只按顺序租用房间一样,只有房间号码高于你的人才能退房。

所以我们来考虑一下堆栈。 在许多操作系统中,每个线程会得到一个堆栈,并且堆栈被分配为一定的固定大小。 当你调用一个方法时,东西被压入堆栈。 如果你把一个指向堆栈的指针传回你的方法,就像原来的海报一样,这只是指向一个完全有效的百万字节内存块的中间指针。 在我们的比喻中,你退房的酒店; 当你这样做的时候,你只是从被占用的最高的房间里检查出来。 如果没有其他人在您之后登记入住 ,并且您非法返回您的房间,您所有的东西都保证在这个特定的酒店里

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

C ++的实现不需要把你留在堆栈上的垃圾保持原样,以便以后可以非法使用; 编译器生成的代码将您刚刚腾出的“房间”中的所有内容都归零,这是完全合法的。 这不是因为再次,这将是昂贵的。

C ++的实现不需要确保当堆栈逻辑缩小时,过去有效的地址仍然映射到内存中。 这个实现允许告诉操作系统“我们已经完成了使用这个页面的堆栈,直到我不这么说的时候,如果有人触摸到以前有效的堆栈页面,就会发出一个破坏进程的exception。 同样,实现实际上不这样做,因为它是缓慢和不必要的。

相反,实施可以让你犯错误,并摆脱它。 大多数时候。 直到有一天真正糟糕的事情出现了问题,这个过程爆炸了。

这是有问题的。 有很多的规则,很容易打破他们的意外。 我当然有很多次。 更糟糕的是,这个问题通常只会在腐败发生后数十亿纳秒的时间内发现内存时才会出现,当时很难弄清楚是谁搞砸了。

更多内存安全的语言通过限制你的能力来解决这个问题。 在“正常的”C#中,根本无法取得本地地址,并将其返回或存储以备后用。 你可以把一个地方的地址,但语言是巧妙的devise,以便在本地生命周期结束后不可能使用它。 为了获取本地地址并将其传回,必须将编译器置于特殊的“不安全”模式, 并将 “不安全”一词放在程序中,以引起注意,有些可能违反规定的危险。

进一步阅读:

你在这里做的只是读和写的内存, 曾经是一个地址。 现在你在foo之外,它只是一个随机存储区的指针。 在你的例子中,恰好如此,这个内存区域确实存在,目前没有别的东西在使用它。 你不会因为继续使用而破坏任何东西,也没有别的东西会覆盖它。 所以5还在那里。 在一个真正的程序中,这个记忆几乎可以立即被重复使用,并且这样做会破坏某些东西(尽pipe这些症状可能在很晚之后才会出现)。

当你从foo返回时,你告诉操作系统你不再使用这个内存,它可以被重新分配给别的东西。 如果你幸运的话,它永远不会被重新分配,而且操作系统不会让你再次使用它,那么你就会撒谎。 尽pipe你最终可能会写下这个地址的任何结果。

现在如果你想知道为什么编译器不抱怨,可能是因为foo被优化淘汰了。 它通常会警告你这种事情。 C假设你知道自己在做什么,从技术上说,你没有违反范围(没有提到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()重用。 如果你反汇编可执行文件,你会看清楚。

在C ++中,你可以访问任何地址,但这并不意味着你应该 。 您正在访问的地址不再有效。 它的工作原理是因为在foo返回之后,没有其他内容搅乱了内存,但在许多情况下它可能会崩溃。 尝试用Valgrind分析你的程序,或者甚至只是编译优化,看看…

您从来不会通过访问无效的内存来抛出C ++exception。 你只是给出了一个引用任意内存位置的一般思路的例子。 我可以这样做:

 unsigned int q = 123456; *(double*)(q) = 1.2; 

在这里,我简单地把123456作为一个double的地址,然后写入它。 任何事情都可能发生:

  1. q实际上可能是一个双倍的有效地址,例如double p; q = &p; double p; q = &p;
  2. q可能指向内部分配的内存,我只是在那里覆盖8个字节。
  3. q分配给外部分配的内存,操作系统的内存pipe理器向我的程序发送分段错误信号,导致运行时终止它。
  4. 你赢了彩票。

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

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

您是否启用了优化器来编译程序?

foo()函数非常简单,可能在代码中被内联/replace。

但是我同意马克B的意见,得出的结果是不确定的。

你的问题与范围无关。 在你显示的代码中,函数main看不到函数foo的名字,所以你不能直接用foo以外的这个名字访问foo

您遇到的问题是为什么程序在引用非法内存时不会报错。 这是因为C ++标准没有规定非法内存和合法内存之间的非常明确的界限。 在popup的堆栈中引用某些东西有时会导致错误,有时不会。 这取决于。 不要指望这种行为。 假设在编程时它总是会导致错误,但是假设它在debugging时不会发出错误信号。

你只是返回一个内存地址,这是允许的,但可能是一个错误。

是的,如果你试图解引用那个内存地址,你将会有未定义的行为。

 int * ref () { int tmp = 100; return &tmp; } int main () { int * a = ref(); //Up until this point there is defined results //You can even print the address returned // but yes probably a bug cout << *a << endl;//Undefined results } 

这是两天前讨论过的经典的未定义行为 – 在网站上search一下。 简而言之,你是幸运的,但任何事情都可能发生,你的代码对内存进行无效访问。

这个行为是不确定的,正如Alex指出的那样 – 事实上,大多数编译器会警告不要这样做,因为这是一个简单的方法来获取崩溃。

对于你可能得到的那种恐怖行为的例子,试试这个例子:

 int *a() { int x = 5; return &x; } void b( int *c ) { int y = 29; *c = 123; cout << "y=" << y << endl; } int main() { b( a() ); return 0; } 

这打印出“y = 123”,但你的结果可能会有所不同(真的!)。 你的指针正在破坏其他不相关的局部variables。

你实际上调用了未定义的行为。

返回临时作品的地址,但临时作业在函数结束时被销毁,访问它们的结果将不确定。

所以你没有修改a而是修改a曾经的内存位置。 这种差异非常类似于崩溃和不崩溃之间的差异。

在典型的编译器实现中,您可以将代码想象为“ 用过去由a占据的地址输出内存块的值”。 另外,如果将一个新的函数调用添加到一个执行一个本地int的函数中,则a (或者a用来指向的内存地址)的值改变a可能性很大。 发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。

但是,这是不确定的行为,你不应该依靠它的工作!

它的工作原理是堆栈没有被改变(还),因为放在那里。 再次访问之前调用一些其他函数(也调用其他函数),你可能不再那么幸运了; 😉

它可以,因为a是在其作用域( foo函数)的生命周期中临时分配的variables。 从foo返回后,内存是空闲的,可以被覆盖。

你在做什么被描述为未定义的行为 。 结果无法预测。

注意所有的警告。 不仅要解决错误。
GCC显示此警告

警告:返回局部variables“a”的地址

这是C ++的力量。 你应该关心记忆。 随着-Werror标志,这个警告成为一个错误,现在你必须debugging它。

正确的(?)控制台输出的东西可以改变,如果你使用:: printf而不是cout。 您可以在下面的代码中使用debugging器(在x86,32位,MSVisual Studio中testing):

 char* foo() { char buf[10]; ::strcpy(buf, "TEST”); return buf; } int main() { char* s = foo(); //place breakpoint & check 's' varialbe here ::printf("%s\n", s); } 

从一个函数返回后,所有的标识符都被销毁,而不是将值保存在一个内存位置,我们不能find没有标识符的值。但是该位置仍然包含前一个函数存储的值。

所以,这里函数foo()返回一个地址和a地址后被销毁。 您可以通过返回的地址访问修改的值。

让我以一个真实世界的例子:

假设一个人在一个位置隐藏钱,并告诉你的位置。 过了一段时间,那个告诉你钱的位置的人死了。 但是你仍然可以获得隐藏的资金。

这是使用内存地址的“肮脏”的方式。 当你返回一个地址(指针)时,你不知道它是否属于某个函数的局部范围。 这只是一个地址。 现在你调用了'foo'函数,'a'的那个地址(内存位置)已经被分配到了你的应用程序(进程)的(安全的,至less现在)至less可寻址的内存中。 在'foo'函数返回后,'a'的地址可以被认为是'dirty',但是它在那里,没有被清理,也没有被程序的其他部分(至less在这个具体情况)中的expression式所干扰/修改。 AC / C ++编译器不会阻止你从这种“脏”访问(如果你在意的话,可能会警告你)。 除非通过某种方式保护地址,否则可以安全地使用(更新)程序实例(进程)数据段中的任何内存位置。

这绝对是一个计时问题! p指针指向的对象是“预定的”,如果超出foo的范围则被销毁。 然而,这个操作并不是立即发生,而是在一些CPU周期后。 不pipe这是不确定的行为,还是C ++实际上在后台做了一些预先清理的东西,我不知道。

如果您在调用foocout语句之间插入对您的操作系统的sleep函数的调用,在解引用指针之前使程序等待一秒钟左右,您将注意到数据在读取之前已经消失! 看看我的例子:

 #include <iostream> #include <unistd.h> using namespace std; class myClass { public: myClass() : i{5} { cout << "myClass ctor" << endl; } ~myClass() { cout << "myClass dtor" << endl; } int i; }; myClass* foo() { myClass a; return &a; } int main() { bool doSleep{false}; auto p = foo(); if (doSleep) sleep(1); cout << p->i << endl; p->i = 8; cout << p->i << endl; } 

(请注意,我使用了unistd.hsleep函数,它只出现在类Unix系统上,所以如果你在Windows系统上,你需要用Sleep(1000)Windows.hreplaceWindows.h 。)

我用一个类replace了你的int ,所以我可以看到什么时候析构函数被调用。

这个代码的输出如下:

 myClass ctor myClass dtor 5 8 

但是,如果将doSleep更改为true

 myClass ctor myClass dtor 0 8 

正如你所看到的,应该销毁的对象实际上是被销毁的,但是我想有一些预清理的指令必须在一个对象(或者一个variables)被销毁之前执行,所以直到完成之后,数据仍然可以访问很短的时间(但是当然不能保证,所以请不要编写依赖于此的代码)。

这很奇怪,因为析构函数在退出范围时立即被调用,但是实际的销毁会稍微延迟。

我从来没有真正阅读过指定这种行为的官方ISO C ++标准的一部分,但很可能这个标准只承诺你的数据一旦被超出范围就会被销毁,但是它并没有提到什么这在任何其他指令执行之前立即发生。 如果是这样的话,这种行为就完全没有问题,人们只是误解了这个标准。

或者另一个原因可能是厚颜无耻的编译器,不正确地遵循标准。 实际上,这不是编译器为了获得额外性能而采用一点标准符合性的唯一情况!

无论这个原因是什么,显然数据被破坏,而不是立即。

Interesting Posts