病人:医生,局部变量超出作用域之后会发生什么事?我为此头疼了很久。
中医:哦,它们不能被访问了,消亡了。你的病不会这么简单吧,到底什么问题,详细描述一下。
病人:我想知道的是,指针所指的局部变量,超出作用域之后,那个指针的行为。比如这段程序
#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复杂,确定每个变量的确靠偏移量,但不是到栈顶的偏移量。
所以,你现在看汇编码,容易把自己搞混。今天先到这里,能理解这些,以后的也快了。
病人:啊!!! 还没有到底,我的头比刚才更疼了,你这是治病还是传病?啊...
中医:你不是也想当中医么?
病人:那是。
中医:久病成医。
病例:不理解局部变量超出作用域之后的行为
最新推荐文章于 2025-07-26 22:39:17 发布