怎么会这样?局部变量的内存不是在其函数之外无法访问吗?
你租了一个旅馆房间。你把一本书放在床头柜最上面的抽屉里,然后去睡觉。您第二天早上退房,但“忘记”归还您的钥匙。你偷了钥匙!
一周后,你回到酒店,不办理入住手续,用偷来的钥匙潜入旧房间,然后在抽屉里翻找。你的书还在。惊人!
这个怎么可能?如果您没有租过房间,酒店房间抽屉里的东西是不是无法访问?
好吧,显然这种情况可以在现实世界中发生,没问题。当您不再被授权进入房间时,没有神秘的力量会导致您的书消失。也没有神秘的力量阻止你带着偷来的钥匙进入房间。
酒店管理人员无需移除您的图书。您没有与他们签订合同,说如果您留下东西,他们会为您撕碎。如果您使用偷来的钥匙非法重新进入您的房间以取回它,酒店保安人员不需要抓住您偷偷溜进来。您没有与他们签订合同,上面写着“如果我试图偷偷溜回我的房间”以后有房间,你必须阻止我。” 相反,您与他们签订了一份合同,上面写着“我保证以后不会偷偷溜回我的房间”,这是您违反的合同。
在这种情况下,任何事情都可能发生。这本书可以在那里——你很幸运。别人的书可能在那里,你的书可能在酒店的炉子里。当你进来时,有人可能会在那里,把你的书撕成碎片。酒店本可以完全拆除桌子并预订,并用衣柜取而代之。整个酒店可能即将被拆除,取而代之的是一个足球场,而你会在偷偷摸摸的时候死于爆炸。
你不知道会发生什么;当您退房并偷走钥匙以供以后非法使用时,您就放弃了生活在可预测的安全世界中的权利,因为您选择了违反系统规则。
C++ 不是一种安全的语言。它会很高兴地让你打破系统的规则。如果你试图做一些非法和愚蠢的事情,比如回到一个你无权进入的房间并在一张可能不再存在的桌子上翻找,C++ 不会阻止你。比 C++ 更安全的语言通过限制你的权力来解决这个问题——例如,通过对键进行更严格的控制。
更新
天哪,这个答案引起了很多关注。(我不知道为什么——我认为这只是一个“有趣”的小类比,但无论如何。)
我认为用更多的技术思想来更新这一点可能是密切相关的。
编译器的业务是生成代码,该代码管理由该程序操作的数据的存储。有很多不同的方法可以生成代码来管理内存,但随着时间的推移,两种基本技术已经变得根深蒂固。
第一个是有某种“长寿命”的存储区域,其中存储中每个字节的“寿命”——即它与某个程序变量有效关联的时间段——不能轻易地提前预测的时间。编译器生成对“堆管理器”的调用,该“堆管理器”知道如何在需要时动态分配存储,并在不再需要时回收它。
第二种方法是有一个“短期”存储区域,其中每个字节的生命周期是众所周知的。在这里,生命周期遵循“嵌套”模式。这些短期变量中寿命最长的将在任何其他短期变量之前分配,并将最后释放。寿命较短的变量将在寿命最长的变量之后分配,并在它们之前被释放。这些寿命较短的变量的寿命“嵌套”在寿命较长的变量的寿命中。
局部变量遵循后一种模式;当一个方法被输入时,它的局部变量就会活跃起来。当该方法调用另一个方法时,新方法的局部变量就会活跃起来。在第一个方法的局部变量死亡之前,它们就会死亡。可以提前计算出与局部变量相关的存储生命周期的开始和结束的相对顺序。
出于这个原因,局部变量通常作为存储在“堆栈”数据结构上而生成,因为堆栈具有这样的属性:第一个压入它的东西将是最后一个弹出的东西。
这就好比酒店决定只按顺序出租房间,等到房间号比你大的人都退房后才能退房。
所以让我们考虑一下堆栈。在许多操作系统中,每个线程都有一个堆栈,堆栈被分配为某个固定大小。当你调用一个方法时,东西会被压入堆栈。如果你然后将一个指向堆栈的指针从你的方法中传回,就像原始海报在这里所做的那样,那只是一个指向一些完全有效的百万字节内存块中间的指针。在我们的类比中,您从酒店退房;当你这样做时,你只是从最高编号的房间里签了出来。如果没有其他人在您之后入住,并且您非法返回房间,则可以保证您的所有东西仍然在这家特定的酒店中。
我们将堆栈用于临时商店,因为它们非常便宜且容易。C++ 的实现不需要使用堆栈来存储局部变量;它可以使用堆。它没有,因为这会使程序变慢。
C++ 的实现不需要让您留在堆栈上的垃圾保持原样,以便您以后可以非法返回;编译器生成将您刚刚腾出的“房间”中的所有内容归零的代码是完全合法的。这不是因为再次,那会很昂贵。
不需要 C++ 的实现来确保当堆栈逻辑收缩时,曾经有效的地址仍然映射到内存中。该实现被允许告诉操作系统“我们现在已经完成了使用这个堆栈页面。除非我另有说明,否则如果有人触摸先前有效的堆栈页面,则发出一个破坏进程的异常”。同样,实现实际上并没有这样做,因为它很慢而且没有必要。
相反,实现可以让你犯错并侥幸逃脱。大多数时候。直到有一天,真正可怕的事情出错了,这个过程爆炸了。
这是有问题的。有很多规则,很容易不小心打破它们。我当然有很多次。更糟糕的是,问题通常只有在内存损坏发生数十亿纳秒后检测到内存损坏时才会出现,此时很难弄清楚是谁搞砸了它。
更多的内存安全语言通过限制你的能力来解决这个问题。在“普通”C# 中,根本没有办法获取本地地址并将其返回或存储以备后用。您可以获取本地地址,但该语言设计巧妙,因此在本地生命周期结束后无法使用它。为了获取本地地址并将其传回,您必须将编译器置于特殊的“不安全”模式,并将“不安全”一词放入程序中,以提醒您注意您可能正在做的事实可能违反规则的危险事物。
进一步阅读: