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

我有以下代码

#包括<iostream>
int*foo()
{
INTA=5;
返回&a;
}
int main()
{
int*p=foo();
标准::cout<<*p;
*p=8;
标准::cout<<*p;
}

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

输出为58

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

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

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

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

这怎么可能?如果您没有租过房间,酒店房间抽屉里的东西是不是就无法进入?

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

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

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

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

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

更新

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

我想用一些技术上的想法来更新这一点可能是密切相关的

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

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

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

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

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

就像酒店决定只按顺序出租房间一样,在所有房间号高于你的人都退房之前,你不能退房

让我们考虑一下堆栈。在许多操作系统中,每个线程都有一个堆栈,并且该堆栈被分配为特定的固定大小。当您调用一个方法时,会将内容推送到堆栈上。如果然后从方法中传递一个指向堆栈的指针,就像原始海报在这里所做的那样,那只是指向某个完全有效的百万字节内存块中间的指针。在我们的类比中,你从酒店退房;当你这样做的时候,你只是从人数最多的房间退房。如果没有其他人在你之后办理入住手续,而你又非法回到房间,那么你所有的东西肯定都会留在这家酒店

我们使用堆叠的临时商店,因为它们非常便宜和容易。不需要使用C++来实现本地存储的堆栈;它可以使用堆。没有,因为那样会使程序变慢

不需要将C++的实现留下给堆栈上未被保留的垃圾,以便以后非法返回。编译器生成的代码将您刚刚腾出的“房间”中的所有内容都归零是完全合法的。这并不是因为再次强调,这将是昂贵的

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

相反,实现可以让您犯错误,并且可以逍遥法外。大多数时候。直到有一天,真正可怕的事情发生了,整个过程爆发了

这是有问题的。有很多规则,很容易意外地打破它们。我当然有很多次。更糟糕的是,当内存在崩溃发生几十亿纳秒后被检测到被破坏时,问题往往才会浮出水面,当时很难找出是谁把它搞砸了

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

进一步阅读:

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

    Ref returns and ref locals

  • 为什么我们要使用堆栈来管理内存?C#中的值类型是否始终存储在堆栈上?虚拟内存是如何工作的?还有更多关于C#内存管理器工作原理的主题。这些文章中的许多也与C++程序员有密切关系:

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

发表评论