我有下面的代码。

#include <iostream>

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

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

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

输出是58

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


当前回答

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

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

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

其他回答

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

It's 'Dirty' way of using memory addresses. When you return an address (pointer) you don't know whether it belongs to local scope of a function. It's just an address. Now that you invoked the 'foo' function, that address (memory location) of 'a' was already allocated there in the (safely, for now at least) addressable memory of your application (process). After the 'foo' function returned, the address of 'a' can be considered 'dirty' but it's there, not cleaned up, nor disturbed/modified by expressions in other part of program (in this specific case at least). A C/C++ compiler doesn't stop you from such 'dirty' access (might warn you though, if you care). You can safely use (update) any memory location that is in the data segment of your program instance (process) unless you protect the address by some means.

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

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

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

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

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

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

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

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曾经所在的内存位置。这种差异与崩溃和不崩溃之间的差异非常相似。