我有下面的代码。

#include <iostream>

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

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

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

输出是58

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


当前回答

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

其他回答

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

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

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

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

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

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/

在典型的编译器实现中,您可以将代码视为“打印出内存块的值,该值带有曾经被a占用的地址”。此外,如果您将一个新的函数调用添加到一个约束局部整型的函数中,则a的值(或a所指向的内存地址)很有可能发生变化。这是因为堆栈将被包含不同数据的新帧覆盖。

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

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

返回临时作品的地址,但由于临时作品在函数的末尾被销毁,访问它们的结果将是未定义的。

因此,您没有修改a,而是修改了a曾经所在的内存位置。这种差异与崩溃和不崩溃之间的差异非常相似。

永远不要通过访问无效内存来抛出c++异常。您只是给出了一个关于引用任意内存位置的一般概念的示例。我也可以这样做:

unsigned int q = 123456;

*(double*)(q) = 1.2;

在这里,我简单地将123456作为double类型的地址,并对其进行写入。任何事情都可能发生:

Q实际上可能是double的有效地址,例如double p;Q = &p; q可能指向已分配内存中的某个地方,我只是在那里覆盖了8个字节。 Q指向分配的内存之外,操作系统的内存管理器向我的程序发送了一个分割错误信号,导致运行时终止它。 你中了彩票。

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

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

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

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