酒店房间和 C++ 局部变量的作用域

  • A+
所属分类:C/C++/C#

  问题:Can a local variable’s memory be accessed outside its scope? 有一段局部变量的内存,可以从其范围之外访问它么?

  如下代码:

int *foo()
{
    int a = 5;
    return &a;
}
 
int main()
{
    int *p = foo();
    cout << *p;
    *p = 8;
    cout << *p;
}

  这样的代码可以正常执行,而且没有任何运行时的异常!

  输出是 5 8

  这是怎么回事?难道局部变量在函数外也可以被访问吗?

  来自微软资深软件工程师 Eric Lippert 的最佳答案(3200+赞):

  你在酒店里租了一间房。你把一本书放进了桌子的第一个抽屉里,然后就去睡觉了。当你第二天早上醒来时,你假装忘记去还钥匙了。你偷了房间的钥匙!

  一周之后,你回到了酒店,但没有入住,你用偷来的钥匙溜进了你上次入住的房间,并查看了那个抽屉。你的书还在那里。是不是很令人吃惊!

  这是怎么回事呢?难道一个酒店房间的抽屉不是应该无法被一个没有入住这个房间的人看到吗?

  好吧,明显的是,这种情况在真实世界中当然会发生。在你不入住这个房间的时候,这里面没有任何神秘的力量把你的书弄消失掉,也没有魔法能够阻止你用偷来的钥匙进入房间。

  酒店的管理规章里没有要求拿走你的书。你也没有跟他们说如果你落下了一本书,他们可以帮你撕毁它。如果你用偷来的钥匙非法进入了你上次的房间,并且没有被酒店的安保系统发现。你也没有跟他们说如果你之后尝试溜进房间,他们应该阻止你。不过事实上,你确实签了一份协议规定你保证不会偷偷溜回房间。只不过你打破了协议。

  在这种情况下任何事情都有可能发生。如果你运气好的话,那本书可能还在那里。其他人的书也可能在那个抽屉里而你的书则被丢进了酒店的火炉里。也可能当你溜进去的时候正好有个人在把你的书撕成碎片。酒店可能把那张桌子连带你的书都移走了,而把一个衣柜放在那里。这家酒店也可能正好要被拆除,换成一个足球场,在你溜来溜去的时候。你可能会在一场爆破中死去。

  当你离开酒店而偷了房间的钥匙的时候,你不知道将会发生什么。你放弃了去生活在一个可靠的,安全的世界里,因为你选择去打破系统的规则。

  C++不是一门安全的语言。你可以非常轻松就打破这个系统的规则。如果你尝试去做一些非法并且愚蠢的事情,比如你回到那个你已经不入住的房间,并想要去查看那张也许已经不存在的桌子。C++不会阻止你的。比C++更加安全的语言通过限制你的能力来解决这个问题,比如通过更加严格的控制房间钥匙。

  【更新】:

  我的老天。这个答案获得了这么多的关注。(我不知道为什么,我只是觉得这样比喻比较有趣, 不过管他呢。)

  我认为在经过了更加技术性的思考之后更新一下这个答案是必要的。

  编译器的工作是生成代码来管理这个程序数据拥有的内存。有很多方式来生成管理内存的代码,但是这么多年来有两个基本的技术是必须要知道的。

  第一个是拥有一片长期存在的区域,这片存储区域里的每一个字节,他们的生命周期比较长。生命周期的意思就是它们能够被程序访问的时期。这类内存没办法提前进行预估。编译器生成一种叫堆管理器的代码,它知道如何在需要的时候动态的分配内存,当内存不再被需要的时候释放掉他们。

  第二个是拥有一片短期存在的区域,这片存储区域里的每一个字节都可以提前进行预估。而且比较特殊的是,这片区域的生命周期遵循一种嵌套模式。也就是说,在这片区域中拥有最长生命周期的变量,它所分配的内存地址被它之后分配的那些生命周期较短的变量所重用。

  局部变量就是第二种情况。当调用一个函数时,它的局部变量便被生成了。当这个函数调用另外一个函数时,新函数的局部变量也被生成了。这些变量会在第一个函数的局部变量之前被释放掉。这些局部变量的内存地址的开始和结束可以提前被计算出来。

  因为这个原因,局部变量经常被分配到栈数据结构里,因为一个栈的特点是第一个入栈的元素将会最后一个出栈。

  这就好像酒店决定只能按照顺序进行房间的出租。你没办法离开,除非你之前所有房间号比你大的人都走了。

  所以,让我们来想一下栈的操作过程。在很多操作系统中,每一个线程都有一个栈,并且栈的大小是一个可变的确定大小。当你调用一个函数的时候,相关的内容被压入栈内。当你把一个这个栈的指针传出这个函数时,就像上面的提问者所干的一样。那个指针只是指向全部有效的数百万个字节内存块的中间。在我们的类比中,当你离开酒店的时候,你只是离开了当前被占用的数字最大的房间。如果没有人在你之后入住,你又非法地回到了这个房间。你所有的东西肯定都还在这个酒店的房间里。

  我们用栈作为临时存储因为它们非常廉价并且容易实现。C++的实现没有规定一定要用栈来存储局部变量,你可以使用堆来存储它们,不过没有人这么干,因为那样做会使得程序变得很慢。

  C++也没有规定在你离开栈之后需要清掉栈里的内容,所以你可以在之后非法地回到栈里找到你之前的内容。当然编译器如果生成代码,一旦你不再使用了就把栈里的所有内容都清零,这是完全合法的。不需要再解释为什么了,因为这样做代价非常高。

  C++没有规定要确保当栈变小时,之前有效的内存地址依然有效。C++的实现也允许告诉操作系统“我们已经不再需要栈的个内存页了。除非我说,否则当有任何人要访问这个之前有效的栈的内存页的时候抛出一个异常并结束程序”。再次,一般的实现也没有这么做,因为这么说使程序变慢而且没有必要。

  相反,大多数时候,一般的C++实现允许你犯错然后避免它。直到有一天,一些真正非常令人恐怖的错误出现了然后把整个程序弄崩溃了。

  这样做是有问题的。C++里有如此多的规则而又如此轻易就可以打破它们。我自己就有好多次这样的经历。更糟的是,这种问题往往是表面的,当你发现内存地址冲突了之后去检查内存,却发现它们在很长时间内又是正确的。所以你很难知道到底是哪个地方出错了。

  那些内存安全的语言通过限制你的能力来解决这个问题。在规范的C#里,没有任何办法去获取一个局部变量的内存地址,然后返回它或者是存储它等以后再用。你可以获取一个局部变量的内存地址,但是语言被很好的设计了,你不可能在局部变量生命周期之后还能够使用它。为了取得局部变量的内存地址并把它返回,你必须要把编译器设置为一个特殊的不安全的模式,并且在你的程序里写上“unsafe”关键字。这可以帮助提醒你,你正在做一些不安全的可能会打破规则的事情。

  更进一步阅读:当C#返回引用时做了些什么?

  http://blogs.msdn.com/b/ericlippert/archive/2011/06/23/ref-returns-and-ref-locals.aspx

  为什么我们用栈来管理内存?C#里值的类型是否一直存储在栈里?虚拟内存是如何工作的?以及更多的关于C#内存管理是如何工作的。这里许多文章都对C++程序员有帮助。

  http://blogs.msdn.com/b/ericlippert/archive/tags/memory+management/

  原文链接: StackOverflow   翻译: 伯乐在线 - 菜鸟浮出水

QQ群: WEB开发者官方总群(196171896) 验证消息:Admin10000
  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin