在函数外存取局部变量的一个比喻
在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 ,请勿用于任何商业用途)
《在函数外存取局部变量的一个比喻》的相关评论
难道只是为了一个比喻吗……好吧……不懂的人多多去实验或反汇编来看看好了
Linus说C++恐怖是和C比,但是这里的例子……
长大了还怕火的大概都是小时候遇到过火灾的?
我想请问一下,最后一个例子中为什么函数a中的数组会影响到b中的数组。我在gdb中调试时,函数b的进入地址比函数a的进入地址提前了8个字节,这是为什么?在我的理解中,函数a与函数b应该进入函数的地址保持一致,只是里面保存的内容可能会产生变化。在此题中b应该输出0~9。我想请问一下,为什么会产生现在的效果?原因是什么?
volatile关键字我在《C++ Primer》一书中查到的解释是,“关键字volatile是给编译器的指示,支出对这样的对象不应该执行优化”。但是此题中,我将volatile关键字去掉,编译后结果仍然与原题一致。
我把第二个例子改了一下,去掉了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里面的局部变量在内存中的布局。
@o0慢节奏0o 我也有这种感觉,对编译器优化不是很明白,刚才查了一下volatile关键字,好像在嵌入式方面应用比较多,好像是保证获取到的始终是变量的实时数据。但是在这里为什么要用volatile还是不明白。
同意,Linus 指責的是 C++ 裏面包含了太多特性以至於難以精通但一堆非master在用吧。
這個論據放在這裏有點莫名其妙。
最后那个例子,不是说a影响b的数组,而是b的数组因为没有初始化,所以值就是当时堆栈的内容,而这个堆栈的内容和指针,在不同的编译器,不同的编译选项时都可能不一样。
因此只能说,b函数里的数组的值是不确定的
不懂为什么要加volatile, 有没有大大能解释一下?
我想Linus说的更多的是在于,不合格的程序员在滥用语言。仅此用来类比一下这个示例。虽然好像C/C++可以这样用,但并不代表这样用是对的。这些示例看上去如此之恐怖。不是吗?
另外,C++里的局部变量的内存管理比C的可能会更乱。
不加的话,开优化后编译器可以认为数组没用而去掉初始化部分
@chenxin1_5
编译时加个优化选项-O3试试?
@chenxin1_5
不加volatile的话, 是不是如果优化级别高了的话会直接把a()里面的语句直接优化掉? 也就是a()里面的东西根本不执行.
@DJ a函数里面对array赋值操作之后再没有用过,在某些编译器优化中会被认为是无意义的赋值操作,所以会被优化掉
个人感觉这里的例子举得并不是很恰当,这些不是C/C++语言本身导致的特性,只是由于函数调用的堆栈实现方式导致的,甚至对于不同的编译行为结果也是不一样的。严格来说这些行为导致的结果是未定义的……
我觉得大家应该重点看一下那个比喻。 @zhtlancer 同样也是。
不懂…
请问输出结果为 “乱码 乱码 0 1 2 3 4 5 6 7″算是达到效果么?
对于我的GNU GCC 编译器来说,貌似用不用volatile 效果都一样。
你用 -O2 效果应该会一样了。
囧,漏了个“不”字。。。。
有了这个比喻之后就不容易忘记了
Linus只说过C++不好。他一直支持C的
除非你不得不用C++去开发,否则真的没什么必要用这个东西,因为他的成本实在是太高了。
优化大概是这个意思,当编译器编译的时候会帮你改一些代码,比如说下面的代码。
static int nInt = 0;
while(nInt == 0)
;
编译器就会优化为
static int nInt = 0;
while(true)
;
所以说 要是nInt这个值要变化了,while循环也会继续。但是你要申明加上关键字volatile时候就不会做相应的替代。
所以陈皓大哥给的列子,应给这样给:
1、首先不加volatile 关键字
2、加关键字volatile
为什么大家加不加都一样呢,是两个函数a()和b()的调用顺序问题。大家再仔细想想。
为什么会出现这样的问题呢,就像stackoverflow里一大哥说的那样,由于c++没有对内存做相应的gc。所以说编译器默认申明数据就直接指向之前申明内存的地址了。其实并不是说c++有多大问题,只要咱们了解这些机制,做到对变量的初始化和指针的释放,问题还是不大的。
@SummerTown
so…难怪我看不出那个volatile有啥用
输出结果是
134519320 1 0 1 2 3 4 5 6 7
请问一下这两个数134519320 1是怎么挖来的内存?
使用vc 2008 express进行了测试,貌似和volatile 关系不大,
如果使用 /O1选项,加上volatile,两个数组的内容是一样的,输出0~9
不加volatile,两个数组的内容是不一样的
如果使用/O2或/Ox选项,加不加volatile,都是输出随机数字:42235094223529429765512450444297515429765542458483016041112450564245987
如果使用/Od选项,加不加volatile结果都一样,输出0~9
这只能说明人比C++更难管理。怪C++,没啥事;怪人,事情大了。
对于没有初始化就访问的内容,有啥必要去研究。这本来就不在c++标准范围内,和c++有啥关系。
你非的不按规则办事,怪哪个?
唉,又是未定义行为……
这是我们编译原理考试的最后一道题,内容差不多
这个问题搬Linus出来背书很无厘头欸,这个问题对于C或者C++都是同样存在的,毋宁说是C++从C那里继承来的。
我想每一个合格的C/C++程序员都不会写出上面那样的代码,C/C++给了你那样的自由不代表你得那么去做,故意去犯错然后怪罪语言,对于这样的程序员,我推荐一个classless,stateless,而且freeless的语言,叫做CCCP。
关于未初始化值,ISO C/C++里都是indeterminate value,虽然这个概念在ISO C++中没有被明确定义而ISO C中有定义,不过可以确定未初始化对象的使用是严格受限的,乱用(C中在未确定值之前使用,以及C++中的涉及class type convertion的non-POD/非以void*或unsigned char*的间接访问之类)的结果会导致undefined behavior。
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.
这跟 C++ 有毛关系啊,Linus 也是写 C 的啊。我明白那句话是想突出“不称职的程序员”,但文中并没有突出这个重点。
看到楼主的文章真是蛋疼的紧。
这就是C/C++的不安全的地方,也正是Linus说的,C++是一门恐怖的语言是因为有很多不合格的程序员在使用它。就像你看到小孩子玩火一样的恐怖。
Linus说的,C++是一门恐怖的语言是因为有很多不合格的程序员在使用它,
问题是Linus支持C啊,C也有这个问题。
我得要回复多少遍才有人看得懂啊——Linux说的那句话,重点不是C++,重点是不合格的程序员。大家读不懂Linux的话吗?
我觉得应该是操作系统开始时给函数a中的数组分配地址比如X,当a函数退出时地址被系统回收,当遇到函数b时,系统重新为函数b中的数组分配地址,而此时分配的地址恰好是X,而地址X已经有一个在函数a中留下了并且初始化的数组,所以这样函数a才能影响函数b,如果将函数a和函数b颠倒,就不会出现这中情况了!
@dshe
防止编译器对程序进行优化
发现用VC++6.0编译,没起到作用
@陈皓
不知能否写一篇关于 gcc 优化选项的文章
不同阵营里的支持者,总会互相攻击。但是存在就有存在的道理,C至今仍然高居编程语言排行榜第二名。关键是要在我们的项目里易用,快速,安全。
函数不都是 有自己的堆么,想不通
这个就是C++不可靠的依据吗?
可笑了:)
其实函数中的局部变量都占用的是栈的空间,所以a函数用的时候会影响到b所谓的变量是可能的
但是b函数使用时又会重新初始化那些变量,所以是没有影响的
@hlp_127
书中写的是回收,但是实际过程中栈内存应该是不会回收的,如果其他地方需要这段内存的话则直接用。