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