我有下面的代码。

#include <iostream>

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

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

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

输出是58

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


当前回答

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

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

让我举一个真实的例子:

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

其他回答

你在编译程序时启用了优化器吗?foo()函数非常简单,可能已经在结果代码中被内联或替换。

但是我同意Mark B的观点,结果行为是不确定的。

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

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

一周后,你回到酒店,没有入住,用偷来的钥匙偷偷溜进你的旧房间,看了看抽屉。你的书还在那儿。惊人的!

这怎么可能呢?如果你还没有租到酒店房间,难道你就找不到房间抽屉里的东西吗?

显然,这种情况在现实世界中是可以发生的。当你被禁止进入房间时,没有什么神秘的力量会让你的书消失。也没有一种神秘的力量阻止你用偷来的钥匙进入一个房间。

The hotel management is not required to remove your book. You didn't make a contract with them that said that if you leave stuff behind, they'll shred it for you. If you illegally re-enter your room with a stolen key to get it back, the hotel security staff is not required to catch you sneaking in. You didn't make a contract with them that said "if I try to sneak back into my room later, you are required to stop me." Rather, you signed a contract with them that said "I promise not to sneak back into my room later", a contract which you broke.

在这种情况下,任何事情都可能发生。书可以在那里,你很幸运。别人的书可能在那儿,而你的可能在旅馆的火炉里。你进来的时候可能就有人在那里,把你的书撕成碎片。酒店本可以把桌子和书全部移走,用一个衣柜取而代之。整个酒店可能会被拆除,取而代之的是一个足球场,而你会在你偷偷摸摸的时候死于爆炸。

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

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

更新

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

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

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

第一种是要有某种“长寿命”的存储区域,其中存储中每个字节的“寿命”——也就是说,它与某个程序变量有效关联的时间——不能轻易提前预测。编译器生成对“堆管理器”的调用,该管理器知道如何在需要时动态分配存储空间,并在不再需要时回收存储空间。

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

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

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

这就像酒店决定只按顺序出租房间,在房间号比你高的人都退房之前,你不能退房。

So let's think about the stack. In many operating systems you get one stack per thread and the stack is allocated to be a certain fixed size. When you call a method, stuff is pushed onto the stack. If you then pass a pointer to the stack back out of your method, as the original poster does here, that's just a pointer to the middle of some entirely valid million-byte memory block. In our analogy, you check out of the hotel; when you do, you just checked out of the highest-numbered occupied room. If no one else checks in after you, and you go back to your room illegally, all your stuff is guaranteed to still be there in this particular hotel.

我们使用堆栈作为临时商店,因为它们真的很便宜和简单。c++的实现不需要使用堆栈来存储局部变量;它可以使用堆。它不会,因为那样会使程序变慢。

c++的实现不需要保持你留在堆栈上的垃圾不动,以便你以后可以非法地回来取它;编译器生成的代码将您刚刚腾出的“房间”中的所有东西归零是完全合法的。不会,因为那样会很贵。

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

相反,实现允许您犯错误并逃脱惩罚。大多数时候是这样。直到有一天,真正可怕的事情发生了,这个过程爆炸了。

这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次。更糟糕的是,当内存在发生损坏后数十亿纳秒被检测到损坏时,问题往往才会浮出水面,这时很难找出是谁弄乱了内存。

More memory-safe languages solve this problem by restricting your power. In "normal" C# there simply is no way to take the address of a local and return it or store it for later. You can take the address of a local, but the language is cleverly designed so that it is impossible to use it after the lifetime of the local ends. In order to take the address of a local and pass it back, you have to put the compiler in a special "unsafe" mode, and put the word "unsafe" in your program, to call attention to the fact that you are probably doing something dangerous that could be breaking the rules.

欲进一步阅读:

如果c#允许返回引用呢?巧合的是,这也是今天这篇博文的主题: https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/ 为什么我们要使用堆栈来管理内存?c#中的值类型总是存储在堆栈中吗?虚拟内存是如何工作的?还有更多关于c#内存管理器如何工作的主题。其中许多文章也与c++程序员密切相关: https://ericlippert.com/tag/memory-management/

如果使用::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); 
}

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

您遇到的问题是为什么程序在引用非法内存时不发出错误信号。这是因为c++标准在非法内存和合法内存之间没有明确的界限。引用弹出堆栈中的内容有时会导致错误,有时不会。视情况而定。不要指望这种行为。假设在编程时它总是会导致错误,但在调试时它永远不会发出错误信号。

What you're doing here is simply reading and writing to memory that used to be the address of a. Now that you're outside of foo, it's just a pointer to some random memory area. It just so happens that in your example, that memory area does exist and nothing else is using it at the moment. You don't break anything by continuing to use it, and nothing else has overwritten it yet. Therefore, the 5 is still there. In a real program, that memory would be re-used almost immediately and you'd break something by doing this (though the symptoms may not appear until much later!)

当您从foo返回时,您告诉操作系统您不再使用该内存,并且可以将其重新分配给其他内存。如果你很幸运,它从来没有被重新分配,而且操作系统也没有发现你再次使用它,那么你就可以摆脱这个谎言。很有可能你最终会以那个地址结尾。

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

简而言之:这通常不会起作用,但有时会偶然起作用。