在函数外存取局部变量的一个比喻

在函数外存取局部变量的一个比喻

在StackOverflow上一这样一个关于C/C++的问题,问问题的人给了一个代码如下:

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

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

你可以编译并运行这个代码(编译时会有一个Warning),结果是:5 8。看上去你可以存取一个函数内的局部变量。但这和我们理解的不一样——函数内的变量在函数退出时就被释放了,不应该在外部还可以被引用。当然,对于C/C++熟悉的人都知道其实并不是真正的释放,你依然还可以通过内存地址去进行操作,这是C/C++的内存管理的不安全性——指针可以用来乱指。

这个问题的解答是比较简单的,但是这个问题有一个答案中的比喻非常精彩。这个比喻是这样的——

你在某个酒店订了一个房,你入住的时候,你放了一本书在这个酒店的抽屉里,但是你走的时候,你忘了这本书。而且,你还没有把这个房间的钥匙还回去。于是,你在未来某个时候,偷偷地回来,打开这个房间的门,你看到了你的书还在里间。当然,还还可以放回别的书。因为,这个酒店管理不会在你走的时候把你留下的书清走,而且,这个酒店的管理的安保措施不是那么严格,因为他信任每一个客人都会遵守管理条例。

在这种情况下,如果你幸运的话,书还会在那里,也可能你的书已经没了。也有可能当你回去的时候,有一个人在那里正在撕你的书,或者酒店把那个抽屉都挪走并变成衣柜,或是整个酒店正在被拆除以改成了一个足球场,而你偷偷摸摸进到施工现场的时候被炸死。

真是很精彩的比喻。这就是C/C++的不安全的地方,也正是Linus说的,C++是一门恐怖的语言是因为有很多不合格的程序员在使用它。就像你看到小孩子玩火一样的恐怖。

关于这个事,还有一个比较经典的示例如下—— 函数a的初始化会影响函数b的数组。注意函数a中的 volatile 关键字。

#include <iostream>
using namespace std; 
void a()
{
    volatile int array[10];
    for (int i = 0; i < 10; i++)
        array[i] = i;
}

void b()
{
    int array[10];
    for (int i = 0; i < 10; i++)
        cout << array[i];
}

int main()
{
    a();
    b();
}

真是可爱的C/C++。


关注CoolShell微信公众账号和微信小程序

(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——
好烂啊有点差凑合看看还不错很精彩 (12 人打了分,平均分: 5.00 )
Loading...

在函数外存取局部变量的一个比喻》的相关评论

  1. Linus说C++恐怖是和C比,但是这里的例子……
    长大了还怕火的大概都是小时候遇到过火灾的?

  2. 我想请问一下,最后一个例子中为什么函数a中的数组会影响到b中的数组。我在gdb中调试时,函数b的进入地址比函数a的进入地址提前了8个字节,这是为什么?在我的理解中,函数a与函数b应该进入函数的地址保持一致,只是里面保存的内容可能会产生变化。在此题中b应该输出0~9。我想请问一下,为什么会产生现在的效果?原因是什么?
    volatile关键字我在《C++ Primer》一书中查到的解释是,“关键字volatile是给编译器的指示,支出对这样的对象不应该执行优化”。但是此题中,我将volatile关键字去掉,编译后结果仍然与原题一致。

  3. 我把第二个例子改了一下,去掉了volatile,结果是类似的

    #include
    void a()
    {
    //volatile int array[10];
    int array[4];
    int b, c;
    for (int i = 0; i < 4; i++)
    array[i] = i;
    b = 11;
    c = 12;
    }

    void b()
    {
    int array2[6];
    for (int i = 0; i < 6; i++)
    printf("%d\n", array2[i]);
    }

    int main()
    {
    a();
    b();
    return 0;
    }

    在gcc-4.0 gcc-4.2 下编译运行的结果一致。不同优化级别的编译结果相差很大。
    根据反汇编代码来看,这些局部变量是“共享”使用的。我认为这和C/C++语言本身特性无关,而是与具体实现有关,如果学习过编译原理,应该比较容易理解为什么会这样实现。C/C++标准里面没有规定怎样安排不同scope里面的局部变量在内存中的布局。

  4. @o0慢节奏0o 我也有这种感觉,对编译器优化不是很明白,刚才查了一下volatile关键字,好像在嵌入式方面应用比较多,好像是保证获取到的始终是变量的实时数据。但是在这里为什么要用volatile还是不明白。

  5. resty :
    Linus说C++恐怖是和C比,但是这里的例子……
    长大了还怕火的大概都是小时候遇到过火灾的?

    同意,Linus 指責的是 C++ 裏面包含了太多特性以至於難以精通但一堆非master在用吧。
    這個論據放在這裏有點莫名其妙。

  6. 最后那个例子,不是说a影响b的数组,而是b的数组因为没有初始化,所以值就是当时堆栈的内容,而这个堆栈的内容和指针,在不同的编译器,不同的编译选项时都可能不一样。
    因此只能说,b函数里的数组的值是不确定的

  7. Ivan :

    resty :
    Linus说C++恐怖是和C比,但是这里的例子……
    长大了还怕火的大概都是小时候遇到过火灾的?

    同意,Linus 指責的是 C++ 裏面包含了太多特性以至於難以精通但一堆非master在用吧。
    這個論據放在這裏有點莫名其妙。

    我想Linus说的更多的是在于,不合格的程序员在滥用语言。仅此用来类比一下这个示例。虽然好像C/C++可以这样用,但并不代表这样用是对的。这些示例看上去如此之恐怖。不是吗?

    另外,C++里的局部变量的内存管理比C的可能会更乱。

  8. chenxin1_5 :
    不懂为什么要加volatile, 有没有大大能解释一下?

    不加的话,开优化后编译器可以认为数组没用而去掉初始化部分

  9. 个人感觉这里的例子举得并不是很恰当,这些不是C/C++语言本身导致的特性,只是由于函数调用的堆栈实现方式导致的,甚至对于不同的编译行为结果也是不一样的。严格来说这些行为导致的结果是未定义的……

  10. 请问输出结果为 “乱码 乱码 0 1 2 3 4 5 6 7″算是达到效果么?
    对于我的GNU GCC 编译器来说,貌似用不用volatile 效果都一样。

  11. Walkerinwind :
    请问输出结果为 “乱码 乱码 0 1 2 3 4 5 6 7″算是达到效果么?
    对于我的GNU GCC 编译器来说,貌似用不用volatile 效果都一样。

    你用 -O2 效果应该会一样了。

  12. 依云 :

    Walkerinwind :
    请问输出结果为 “乱码 乱码 0 1 2 3 4 5 6 7″算是达到效果么?
    对于我的GNU GCC 编译器来说,貌似用不用volatile 效果都一样。

    你用 -O2 效果应该会一样了。

    囧,漏了个“不”字。。。。

  13. 除非你不得不用C++去开发,否则真的没什么必要用这个东西,因为他的成本实在是太高了。

  14. 优化大概是这个意思,当编译器编译的时候会帮你改一些代码,比如说下面的代码。
    static int nInt = 0;
    while(nInt == 0)
    ;
    编译器就会优化为
    static int nInt = 0;
    while(true)
    ;
    所以说 要是nInt这个值要变化了,while循环也会继续。但是你要申明加上关键字volatile时候就不会做相应的替代。

    所以陈皓大哥给的列子,应给这样给:
    1、首先不加volatile 关键字
    2、加关键字volatile
    为什么大家加不加都一样呢,是两个函数a()和b()的调用顺序问题。大家再仔细想想。

  15. 为什么会出现这样的问题呢,就像stackoverflow里一大哥说的那样,由于c++没有对内存做相应的gc。所以说编译器默认申明数据就直接指向之前申明内存的地址了。其实并不是说c++有多大问题,只要咱们了解这些机制,做到对变量的初始化和指针的释放,问题还是不大的。

  16. 使用vc 2008 express进行了测试,貌似和volatile 关系不大,
    如果使用 /O1选项,加上volatile,两个数组的内容是一样的,输出0~9
    不加volatile,两个数组的内容是不一样的
    如果使用/O2或/Ox选项,加不加volatile,都是输出随机数字:42235094223529429765512450444297515429765542458483016041112450564245987

    如果使用/Od选项,加不加volatile结果都一样,输出0~9

  17. 对于没有初始化就访问的内容,有啥必要去研究。这本来就不在c++标准范围内,和c++有啥关系。

  18. 这个问题搬Linus出来背书很无厘头欸,这个问题对于C或者C++都是同样存在的,毋宁说是C++从C那里继承来的。

    我想每一个合格的C/C++程序员都不会写出上面那样的代码,C/C++给了你那样的自由不代表你得那么去做,故意去犯错然后怪罪语言,对于这样的程序员,我推荐一个classless,stateless,而且freeless的语言,叫做CCCP。

  19. 关于未初始化值,ISO C/C++里都是indeterminate value,虽然这个概念在ISO C++中没有被明确定义而ISO C中有定义,不过可以确定未初始化对象的使用是严格受限的,乱用(C中在未确定值之前使用,以及C++中的涉及class type convertion的non-POD/非以void*或unsigned char*的间接访问之类)的结果会导致undefined behavior。

  20. For kernel programming, C is usually preferred over C++ since the latter brings a lot of uncertainty derived from compiler. That’s why Linus is strongly against C++ in Linux Kernel. However, for application programming, OO does have some edge over C language.

  21. 这跟 C++ 有毛关系啊,Linus 也是写 C 的啊。我明白那句话是想突出“不称职的程序员”,但文中并没有突出这个重点。

  22. 看到楼主的文章真是蛋疼的紧。
    这就是C/C++的不安全的地方,也正是Linus说的,C++是一门恐怖的语言是因为有很多不合格的程序员在使用它。就像你看到小孩子玩火一样的恐怖。
    Linus说的,C++是一门恐怖的语言是因为有很多不合格的程序员在使用它,
    问题是Linus支持C啊,C也有这个问题。

    1. 我得要回复多少遍才有人看得懂啊——Linux说的那句话,重点不是C++,重点是不合格的程序员。大家读不懂Linux的话吗?

  23. 我觉得应该是操作系统开始时给函数a中的数组分配地址比如X,当a函数退出时地址被系统回收,当遇到函数b时,系统重新为函数b中的数组分配地址,而此时分配的地址恰好是X,而地址X已经有一个在函数a中留下了并且初始化的数组,所以这样函数a才能影响函数b,如果将函数a和函数b颠倒,就不会出现这中情况了!

  24. 不同阵营里的支持者,总会互相攻击。但是存在就有存在的道理,C至今仍然高居编程语言排行榜第二名。关键是要在我们的项目里易用,快速,安全。

  25. 这个就是C++不可靠的依据吗?
    可笑了:)
    其实函数中的局部变量都占用的是栈的空间,所以a函数用的时候会影响到b所谓的变量是可能的
    但是b函数使用时又会重新初始化那些变量,所以是没有影响的

  26. @hlp_127
    书中写的是回收,但是实际过程中栈内存应该是不会回收的,如果其他地方需要这段内存的话则直接用。

发表评论

电子邮件地址不会被公开。 必填项已用*标注