提问者:小点点

局部变量的内存可以在其作用域之外访问吗?


我有以下代码。

#include <iostream>

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

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

而且代码只是在运行,没有运行时异常!

输出为58

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


共3个答案

匿名用户

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

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

一周后,你回到酒店,不办理入住手续,带着偷来的钥匙潜入你的旧房间,在抽屉里找。 你的书还在。 令人惊讶!

怎么会这样? 如果你还没有租住酒店房间,那么房间抽屉里的东西是不是就无法取用了呢?

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

酒店管理人员不需要移走您的书。 你没有和他们签订合同,说如果你留下东西,他们会给你撕碎。 如果你带着偷来的钥匙非法重新进入房间取回,酒店的保安人员不需要抓住你偷偷溜进来。 你没有和他们订立合同,说“如果我稍后试图溜回我的房间,你必须阻止我。” 相反,你和他们签了一份合同,上面写着“我保证以后不会再溜回我的房间”,但你却违反了这份合同。

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

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

C++不是一种安全的语言。 它会让你愉快地打破系统的规则。 如果你试图做一些违法和愚蠢的事情,比如回到一个你无权进入的房间,在一张可能已经不在那里的桌子里翻找东西,C++是不会阻止你的。 比C++更安全的语言通过限制您的能力来解决这个问题--例如,通过对键进行更严格的控制。

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

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

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

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

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

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

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

这就好比酒店决定只按顺序出租房间,等到房号比你高的人都退房了,你才能退房。

让我们考虑一下堆栈。 在许多操作系统中,每个线程都有一个堆栈,堆栈被分配为一定的固定大小。 当您调用一个方法时,stuff会被推送到堆栈上。 如果您将一个指向堆栈的指针从方法中传递回来,就像这里的原始海报所做的那样,那只是一个指向某个完全有效的百万字节内存块中间的指针。 在我们的类比中,你从酒店退房; 当你这样做的时候,你刚从最多的房间退了出来。 如果没有人跟在你后面登记,而你又非法回到房间,你所有的东西都保证会留在这家酒店里。

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

C++的实现不需要将您留在堆栈上的垃圾原封不动地留在堆栈上,这样您就可以在以后非法地返回来取回它; 编译器生成代码,将您刚刚腾出的“房间”中的所有内容归零,这是完全合法的。 它不是因为,再一次,那将是昂贵的。

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

相反,实现会让您犯错误并逃脱惩罚。 大部分时间。 直到有一天真正糟糕的事情出了问题,整个过程彻底崩溃。

这是有问题的。 规则很多,一不小心就很容易打破。 我当然有很多次。 更糟糕的是,问题往往只在内存损坏数十亿纳秒后才浮出水面,此时很难找出是谁搞砸了它。

更多的内存安全语言通过限制您的权力来解决这个问题。 在“普通”C#中,根本没有办法获取本地地址并返回它或存储它以备以后使用。 你可以取一个本地的地址,但是这种语言设计得很巧妙,使得在本地的生存期结束后不可能再使用它。 为了获取本地地址并将其传回,您必须将编译器置于特殊的“不安全”模式,并在程序中添加“不安全”一词,以提醒注意您可能正在做一些可能违反规则的危险行为。

欲进一步阅读:

>

  • 如果C#确实允许返回引用呢? 巧合的是,这就是今天这篇博文的主题:

    https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/

    为什么我们要使用堆栈来管理内存? C#中的值类型总是存储在堆栈上吗? 虚拟内存是如何工作的? 以及更多关于C#内存管理器如何工作的主题。 其中许多文章也与C++程序员密切相关:

    https://ericlippert.com/tag/memory-management/

  • 匿名用户

    这里所做的只是对过去a地址的内存进行读写操作。 现在您已经脱离了foo,它只是指向某个随机内存区域的指针。 恰好在您的示例中,该内存区域确实存在,并且目前没有其他内存区域在使用它。 你不会因为继续使用而破坏任何东西,而且还没有其他东西覆盖它。 因此,5仍然存在。 在一个真实的程序中,内存几乎会立即被重新使用,这样做会破坏某些东西(尽管症状可能要很久以后才会出现!)

    当您从foo返回时,您告诉OS您不再使用该内存,可以将其重新分配给其他内存。 如果你很幸运,它从来没有被重新分配,操作系统也不会发现你再次使用它,那么你就可以逃脱这个谎言。 不过,你可能会写完其他的东西,最后写上那个地址。

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

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

    匿名用户

    因为存储空间还没被踩到。 别指望那种行为。

    相关问题