病例:不理解局部变量超出作用域之后的行为

病人:医生,局部变量超出作用域之后会发生什么事?我为此头疼了很久。 中医:哦,它们不能被访问了,消亡了。你的病不会这么简单吧,到底什么问题,详细描述一下。 病人:我想知道的是,指针所指的局部变量,超出作用域之后,那个指针的行为。比如这段程序 #include "stdio.h" int main(int argc, char* argv[]) { int i=10; int *piToTest=&i; printf("%d/n",*piToTest); { int iGone=20; piToTest=&iGone; // <---咔咔 } printf("%d/n",*piToTest); // <---啊啊 return 0; } Figure 1. 运行结果是打出“20”,正是*piToTest生前的值,难道iGone其实没有消亡,永远活在我们心中? 中医:嗯,你要这么说的话,iGone“生前”究竟活在什么地方呢?用术语来说,其storage在何处? 病人:哦哦,不知道。 中医:这就对了,你的问题的本质正是局部变量的storage问题,所以你会头疼。现在这个问题先搁置一下,你也不知道函数传递参数的机制对么? 病人:对。 中医:我们得从这里着手。每次函数调用,都要有一块内存存放其参数(不妨假设所有函数都有一些参数), 而编译时刻无从知道某个函数究竟会被(递归)调用几次,要为他留多少份参数的空间,对不对? 病人:。。。 对,即使是有一个main函数,也不妨碍他调用自己n次m层。放参数的地方,必定是一个很“动态”的地方。 中医:这个很动态的地方,术语叫堆栈(简称“栈”)。其运作机制和数据结构中的堆栈一样,都是先进后出所有操作都在栈顶发生,不过这里的堆栈是由CPU和OS来实现的。 函数调用的时候,①把其参数push进堆栈,②把返回地址push进堆栈,③跳转到函数入口地址。 void func1(int a,int b); ... func1(10,20); ... ┌--------------------┐ │返回地址 │ ├--------------------┤ │左面的参数 -- 10 │ ├--------------------┤ │最右面的参数 -- 20 │ ├--------------------┤ │之前的堆栈 │ Figure 2. 进入func1时刻的堆栈情况(假定堆栈向上生长) 参数自右向左入栈,最后是返回地址 现在,你说说函数返回的时候,应该发生什么事。 病人:我猜是调用的逆序列吧。(1)取得返回地址跳转回去,(2)堆栈恢复成“之前的堆栈” 中医:很好,请记住(1)是callee做的事,而(2)是caller的责任。看完病之后,你再想一下printf之类不定个数参数函数的机制来理解这样做的必要性。不过现在,我们得整理一下函数调用的过程。 ①caller 把其参数push进堆栈 ②caller 把返回地址push进堆栈 ③跳转到函数入口地址。 ④callee的函数体被执行 ⑤callee取得返回地址跳转回去 ⑥caller把堆栈恢复成“之前的堆栈” Figure 3. 函数调用的过程 病人:嗯,这些我理解了。这和我的病根“局部变量的storage问题”之间的关系是--? 中医:真的理解了么?这里面还隐含了一个前提,callee必定可以取得返回地址。 病人:这还用推,不就在栈顶么? 中医:嘿嘿,你这句话也隐含了一个前提,函数体内,除了调用函数这样的“堆栈平衡的操作”,堆栈不生长,不然返回地址不会在栈顶让你唾手可得。 病人:难道不是? 。。。 啊! 莫非函数体内把局部变量给放进了堆栈?局部变量的作用域和函数的参数非常接近,他的storage也应该是堆栈吧! 中医:能悟出这点,强。我们来把图3中的函数体再细化一下。 ④.1 把局部变量放进堆栈(函数内有多少局部变量编译器很清楚) ④.2 函数代码 ④.3 把堆栈恢复成“进入函数时的堆栈”(姑且认为是.1的逆操作) Figure 4. 函数体细化 ┌--------------------┐ │ │ │各局部变量 │ │ │ ├--------------------┤ │返回地址 │ ├--------------------┤ │左面的参数 -- 10 │ ├--------------------┤ │最右面的参数 -- 20 │ ├--------------------┤ │之前的堆栈 │ Figure 5. 局部变量给放进了堆栈(函数内有多少局部变量编译器很清楚) 这样,返回时的确可以在栈顶取到返回地址。顺便,你说说函数代码中如何定位某个具体参数? 病人:这下总可以根据栈顶了吧!编译器为每一个局部变量定下一个该函数内栈顶的偏移量, 函数代码中就根据当时的栈顶和那个偏移量来确定每个变量。 等等! 我的病,我想想。。。 ┌--------------------┐ │iGone=20 │ │piToTest │ │i=10 │ ├--------------------┤ │返回地址 │ ├--------------------┤ │左面的参数 -- 10 │ ├--------------------┤ │最右面的参数 -- 20 │ ├--------------------┤ │之前的堆栈 │ Figure 6. 运行到“咔咔”时,堆栈的概貌。 由于C++编译器,只对类和结构产生析构函数, 在简单类型变量消亡时不对它们做清理操作(如果一个int也要配上析构函数,那C++还能有效率么), 所以iGone消亡后,他的空间还在那里,又没有人去用那个堆栈位置, 那么“啊啊”处打出iGone的前身也就不奇怪了。 中医:推理正确! 病人:看来这病看似轻微,其实根子很深哩! 中医:现在好了么? 病人:大部分好了,如果你带我看看编译器产生的汇编码就全好了。 中医:嘿嘿,否决。 第一,看汇编码有点西医化,我们中医讲究推理(玩笑^^); 第二,我们研究的是C++,不是特定CPU特定OS下,特定编译器的行为; 第三,由于try...catch,栈中动态分配内存这些东西的存在, 真正的函数调用中,堆栈结构比图5复杂,确定每个变量的确靠偏移量,但不是到栈顶的偏移量。 所以,你现在看汇编码,容易把自己搞混。今天先到这里,能理解这些,以后的也快了。 病人:啊!!! 还没有到底,我的头比刚才更疼了,你这是治病还是传病?啊... 中医:你不是也想当中医么? 病人:那是。 中医:久病成医。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值