局部变量的内存可以在其范围之外访问吗?

我有以下代码。

#include <iostream>
int * foo(){int a = 5;return &a;}
int main(){int* p = foo();std::cout << *p;*p = 8;std::cout << *p;}

代码正在运行,没有运行时异常!

输出为58

295371 次浏览

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

是的,如果您尝试取消引用该内存地址,您将有未定义的行为。

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}

您是否在启用优化器的情况下编译程序?foo()函数非常简单,可能已在生成的代码中内联或替换。

但我同意Mark B的观点,即结果行为是未定义的。

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

你永远不会通过访问无效内存来抛出C++异常。你只是给出了一个引用任意内存位置的一般想法的例子。我可以这样做:

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

在这里,我只是将123456视为双精度对象的地址并向其写入。

  1. q实际上可能是双精度的有效地址,例如double p; q = &p;
  2. q可能指向分配内存中的某个地方,我只是覆盖了那里的8个字节。
  3. q指向分配的内存之外,操作系统的内存管理器向我的程序发送分段故障信号,导致运行时终止它。
  4. 你中了彩票。

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

在正常程序执行期间,没有人会自动检查内存地址的语义有效性。然而,像valgrind这样的内存调试器会很乐意这样做,所以你应该通过它运行你的程序并目睹错误。

在典型的编译器实现中,你可以将代码视为“打印出地址为曾经是的内存块的值”。此外,如果你向包含本地int的函数添加新函数调用,a的值(或a用来指向的内存地址)很有可能发生变化。发生这种情况是因为堆栈将被包含不同数据的新帧覆盖。

然而,这是未定义行为,你不应该依赖它来工作!

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

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

一周后,你回到旅馆,没有登记入住,拿着偷来的钥匙溜进你的旧房间,看看抽屉里,你的书还在那里。

怎么会这样?如果你没有租住房间,酒店房间抽屉里的东西不是无法进入吗?

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

酒店管理人员不会把你的书拿走。你没有和他们签订合同,说如果你把东西留下,他们会为你撕碎。如果你用偷来的钥匙非法重新进入你的房间把它拿回来,酒店保安人员不会抓住你偷偷溜进来。你没有和他们签订合同,说“如果我以后试图溜进我的房间,你必须阻止我。”相反,你和他们签订了一份合同,说“我保证以后不溜进我的房间”,一份合同。

在这种情况下什么事都可能发生。书可能在那里——你很幸运。别人的书可能在那里,你的书可能在酒店的炉子里。当你进来的时候,有人可能就在那里,把你的书撕成碎片。酒店本可以把桌子和书完全移走,换成一个衣柜。整个酒店可能即将被拆除,取而代之的是一个足球场,当你偷偷摸摸的时候,你会在爆炸中死去。

你不知道会发生什么;当你退房并偷了一把钥匙以供以后非法使用时,你放弃了生活在一个可预测的安全世界的权利,因为0号选择了打破系统的规则。

C++不是安全的语言.它会很高兴地允许你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你没有被授权进入的房间,翻找一张可能已经不在那里的桌子,C++不会阻止你。比C++更安全的语言通过限制你的权力来解决这个问题--例如,对钥匙有更严格的控制。

更新

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

我认为用更多的技术思想来更新这一点可能是密切相关的。

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

首先是拥有某种“长寿命”存储区域,其中存储中每个字节的“生命周期”-即它与某个程序变量有效关联的时间段-无法轻松提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储并在不再需要时回收它。

第二种方法是有一个“短命”存储区域,每个字节的生命周期是众所周知的。在这里,生命周期遵循一个“嵌套”模式。这些短命变量中寿命最长的将在任何其他短命变量之前分配,并将最后被释放。短命变量将在寿命最长的变量之后分配,并在它们之前被释放。这些短命变量的生命周期被“嵌套”在寿命较长的变量的生命周期内。

局部变量遵循后一种模式;当一个方法被输入时,它的局部变量是活的。当该方法调用另一个方法时,新方法的局部变量是活的。在第一个方法的局部变量死之前,它们就死了。与局部变量相关的存储寿命的开始和结束的相对顺序可以提前计算出来。

出于这个原因,局部变量通常作为“堆栈”数据结构上的存储而生成,因为堆栈具有这样的属性,即第一个推到它上面的东西将是最后一个弹出的东西。

这就像酒店决定只按顺序出租房间,你不能退房,直到房间号比你高的每个人都退房。

所以让我们想想堆栈。在许多操作系统中,每个线程有一个堆栈,并且堆栈被分配为一定的固定大小。当你调用一个方法时,东西会被压到堆栈上。如果你然后从你的方法中传回一个指向堆栈的指针,就像最初的海报在这里所做的那样,那只是一个指向完全有效的百万字节内存块中间的指针。在我们的类比中,你退房了酒店;当你退房时,你只是从编号最高的房间退房。如果你之后没有其他人签入,并且你非法回到你的房间,你所有的东西都保证仍然存在在这个特别的酒店

我们将堆栈用于临时存储,因为它们非常便宜和容易。C++的实现不需要使用堆栈来存储本地存储;它可以使用堆。但事实并非如此,因为这会使程序变慢。

C++的实现不需要保留堆栈上的垃圾,以便以后可以非法返回;编译器生成代码将您刚刚腾出的“房间”中的所有内容返回为零是完全合法的。这不是因为这会很昂贵。

不需要C++的实现来确保当堆栈逻辑收缩时,过去有效的地址仍然映射到内存中。允许实现告诉操作系统“我们现在已经使用了堆栈的这个页面。除非我另有说明,否则发出一个异常,如果有人触摸以前有效的堆栈页面,该异常将破坏进程”。同样,实现实际上并没有这样做,因为它很慢且不必要。

相反,实现让你犯错误并逃脱惩罚。大多数时候。直到有一天,真正可怕的事情出错了,流程爆炸了。

这是有问题的。有很多规则,很容易被意外打破。我肯定有很多次。更糟糕的是,问题通常只有在内存被检测到损坏时才会出现,在损坏发生数十亿纳秒后,很难弄清楚是谁把它搞砸了。

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

进一步阅读:

你的问题与范围无关。在你展示的代码中,函数main没有看到函数foo中的名称,所以你不能直接访问foo中的a,而这个名称在foo之外。

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

你在这里做的只是对用来a地址的内存进行读写。现在你在foo之外,它只是指向某个随机内存区域的指针。碰巧在你的例子中,那个内存区域确实存在,目前没有其他东西在使用它。继续使用它不会破坏任何东西,而且还没有其他东西覆盖它。因此,5还在那里。在一个真正的程序中,那个内存几乎会立即被重新使用,你这样做会破坏一些东西(尽管症状可能要到很久以后才会出现!)

当你从foo返回时,你告诉操作系统你不再使用该内存,它可以被重新分配给其他东西。如果你很幸运,它永远不会被重新分配,操作系统也不会发现你再次使用它,那么你就逃脱了谎言。很有可能你最终会写在任何其他以该地址结束的东西上。

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

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

它之所以有效,是因为自从a被放在那里以来,堆栈还没有被改变。在再次访问a之前调用一些其他函数(也调用其他函数),你可能不会再那么幸运了…;-)

如果您使用printf而不是cout,具有正确(?)控制台输出的内容可能会发生显着变化::。您可以在以下代码中使用调试器(在x86、32位、MSVisual Studio上测试):

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);}

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

返回临时地址可以工作,但由于临时对象在函数结束时被销毁,访问它们的结果将是未定义的。

所以你没有修改a,而是修改了a曾经所在的内存位置。这种区别与崩溃和不崩溃的区别非常相似。

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

你正在做的事情被描述为未定义行为。结果无法预测。

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

正如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”,但您的结果可能会有所不同(真的!)。您的指针正在破坏其他不相关的局部变量。

对所有答案的一点补充:

如果你这样做:

#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()重用。如果您反汇编可执行文件,您将清楚地看到它。

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

警告:返回局部变量'a'的地址

这是C++的力量。您应该关心内存。使用-Werror标志,此警告将成为错误,现在您必须调试它。

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

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

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

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

假设一个人把钱藏在一个地方,并告诉你这个地方。过了一段时间,那个告诉你钱藏在哪里的人死了。但你仍然可以接触到隐藏的钱。

您的代码非常危险。您正在创建一个局部变量(在函数结束后被视为已销毁),并在该变量被销毁后返回该变量的内存地址。

这意味着内存地址可能有效或无效,您的代码将容易受到可能的内存地址问题(例如分段错误)的影响。

这意味着你正在做一件非常糟糕的事情,因为你正在将内存地址传递给一个指针,这是完全不可信的。

考虑这个例子,并测试它:

int * foo(){int *x = new int;*x = 5;return x;}
int main(){int* p = foo();std::cout << *p << "\n"; //better to put a new-line in the output, IMO*p = 8;std::cout << *p;delete p;return 0;}

与您的示例不同,使用此示例您是:

  • 将int的内存分配到本地函数中
  • 当函数过期时,该内存地址仍然有效(它不会被任何人删除)
  • 内存地址是可信的(该内存块不被认为是空闲的,因此在删除它之前不会被覆盖)
  • 内存地址不使用时应删除。(见程序末尾的删除)