C语⾔⼦函数return(局部变量局部指针栈)机制笔记
0引
C/C++中,函数内部的⼀切变量(函数内部局部变量,形参 )都是在其被调⽤时才被分配内存单元。⼦函数运⾏结束时,所有局部变量的内存单元会被系统释放。形参和函数内部的局部变量的⽣命期和作⽤域都是在函数内部( static变量的⽣命期除外)。
在C中,函数被调⽤时的传参⽅式有两种形式:传值和传址。
传址的好处:
(1)能在函数内部通过实参地址间接地改变实参的值。
(2)当所传实参内容⽐较庞⼤时,传址只是复制了整个实参的地址过去,指针依据同⼀个地址访问实参变量。⽽传值就会将实参内容整个拷贝过去,形参会跟实参占⼀样⼤的内存,栈空间是有限的。当然了,在弱⼩的程序中,传址的这个优点不会被体现出来。
在函数中,可以随意的返回⼀个局部变量。但如果返回⼀个局部变量的地址(指针 ),编译器就会给出警告(编译器也不可能那么完美能够彻底的检查出段错误)。在函数内部返局部指针这的确是⼀个危险的操作。鄙⼈的笔记先将⽤return返回值(指针为地址值)的机制搞清楚后再分析⼀下。
1函数内部返回局部变量过程
1.1结论
Linux等的C语⾔中return返回值的机制为:将返回值存⼊eax寄存器中,然后系统再将eax中的值赋给变量(i)。
(1)编写⼀个简单的C源程序
在linux 下敲⼀个简单的函数调⽤的程序:
c语言return的用法和搭配
Figure1:C中函数调⽤的简单例⼦
涉及到局部变量存储问题时先查了2个概念:
堆栈:堆栈其实是两种数据结构。
堆:由程序员分配和释放。如在C/C++中程序员使⽤malloc/new分配堆空间,使⽤free/delete释放所申请的堆空间。特点:释放内存块顺序随意。
栈:栈是由系统⾃动分配和回收的内存。如⼀个⼦函数被调⽤时,系统会将函数内的局部变量的内存单元分配到栈上,当函数执⾏完毕时系统⾃动释放所分
配的栈地址单元。特点:释放栈内存顺序为后进先出。
(2)分析⼦函数调⽤的过程
【1】当程序执⾏到第8⾏调⽤⼦函数child_fun,程序转到到child_fun⼦函数⼊⼝地址处。
【2】程序进⼊child_fun⼦函数(即此⼦函数开始运⾏ ),执⾏到”return 1;”时,系统将返回的1存⼊寄存器eax中,然后经‘}’标志后函数运⾏完毕。若⼦函数中有形参和局部变量,则在函数开始运⾏时,系统⾃动为局部变量分配栈空间,待函数运⾏完毕时系统⾃动释放在栈中为局部变量分配的内存单元中的数据。
【3】child_fun⼦函数执⾏完毕,函数返回到调⽤⼦函数的地⽅即第8⾏处继续执⾏,将保存在寄存器eax中的值即1赋给变量i。
1.2汇编验证
如何验证所总结的return机制呢?
(1)汇编C源⽂件
在linux字符界⾯下,将以上提到的那段C语⾔程序编译成与之对应的汇编代码:gcc –S  var_return_in_fun.c
得到var_return_in_fun.s⽂件,打开⽂件查看汇编代码:vi var_return_in_fun.s:
Figure2:C语⾔对应的汇编代码
编译C语⾔源⽂件时可不为gcc添加加-O2优化参数,不然在汇编代码中会看不到⼦函数调⽤的call指令。
(2)分析汇编代码
当初学习RAM汇编指令的时候没有清晰的动过⼿,对于这段汇编代码也是只认识push、move之系列英语单词,但是不会可以学习⼀下:【1】由于不同的CPU的汇编格式不⼀样,故⾸先了解⼀下当前操作系统使⽤的什么汇编格式。⽐如windows下采⽤的Intel的汇编格
式,linux采取的是AT&T汇编格式。
【2】收索⼀下AT&T汇编指令,浏览⼀下。明⽩⼀些基本指令的含义和编写格式后,只抓这个汇编代码的关键部分进⾏跟踪:
[1]在main函数中,调⽤⼦函数child_fun之前的汇编代码就不⽤看了,是依函数地址,初始化栈、代码段之类的含义。从13⾏的”call child_fun”开始,程序就从红线箭头标识的⽅向跳到⼦函数child_fun处开始执⾏。
[2]27⾏前的代码就不⽤看了,也根据将⼦函数地址初始化栈之类的。请我对照C语⾔源代码,第27⾏的
代码”move1  &1, %eax”的含义是将常数1装⼊寄存器eax中。常数1对应C语⾔源代码中return后⾯的常数1。28⾏出栈,29⾏从⼦函数child_fun处返回到14⾏处。
[3]14⾏代码”movel        %eax,  -8(%ebp)”的含义是将寄存器eax的值载⼊”-8(%ebp)”所寻址之处,⽽且这个地址就是变了i的地址( 12⾏
及16⾏对应语句之间的内容是main函数中的内容,12⾏之前是初始化代码,16⾏后是恢复未初始状态的代码,可单独写程序验证)。并且main函数中的返回值也是保存在寄存器eax中的[见图2汇编代码 ]。
2函数内部返回局部指针过程
在最开始的未明⽩return机制前可能还是要纳闷:在⼦函数中返回⼀个局部变量,等⼦函数运⾏结束时,此局部变量会被释放掉。当在⼦函数中返回⼀个指针时,等⼦函数运⾏结束时,此地址中的值会被释放掉。有点不出其中被释放的差别。根据返回局部变量的经验,
可以这么分析:在执⾏return语句时,⾸先将return后⾯的地址值返回存⼊到⽐如eax寄存器中,然后系统再将eax中的地址值给接收函数返回地址的指针变量。这看起来都没什么问题,但问题在于两个⽅⾯:
[1]接收函数返回地址值的指针变量要访问此地址中的内容。
[2]⼦函数运⾏结束后,⼀切有关于局部变量的内存都已经释放回收。那么在⽤这个地址来操作就很危险:根本没有这个地址或者是地址中没有内容[没有内容是对的 ]。
但真的是像分析的这样么?(是的)。只有写程序来验证了。
2.1返回局部指针也没出错问题的情况
有的程序就能够将局部变量的地址放回回来,甚⾄在编译时警告都没有。例如以下程序例⼦:
Figure3:返回局部地址
Figure4:编译运⾏
这个令⼈吃惊的结果不禁让⼈怀疑⾃⼰最开始对栈内存释放的理解。这个例⼦最起码验证了在⼦函数执⾏完毕后,原存在栈中的内容是没有被释放掉的。那么栈由系统⾃动分配和回收到底是怎么个情况呢?再整个不能输出正确结果的例⼦。
2.2栈内容被释放掉的例⼦
代码:
Figure5:栈内容被释放的例⼦
Figure6:图5的执⾏结果
分析:
根据程序代码和执⾏结果可见正如标题那个样⼦:栈内存还在,只是栈内存中的值被释放掉了。它不在被程序所占⽤。
因为在⼦函数执⾏完毕时毕竟还是将栈内存(即局部变量的地址)返回到了⽗函数中。但是内存中的值已经被释放掉了。但是为什么第⼀个值依旧没有被释放掉呢?是正确的呢?可能是⾸地址所以⼀直都会给其它程序留个好印象吧。
所以最后的结论是:⼦函数中的局部地址是能够被return到⽗函数中去的。只是在⽗函数中⽤这个地址去访问内容时,此地址中的内容已经被系统清除掉。这是很危险的操作:在⽗函数中⽤此地址访问其内容时,有可能刚被释放掉的这块栈内存⼜被系统分配另外的局部变量了,⽽此时你所访问的结果只是会导致程序结果不正确⽽已;但如果此地址中的内容还是不定状态,访问得到的值跟Figure 6⼀般。
3内存分配释放/回收的含义
栈的分配和释放可以这样⼦理解:栈内存块在计算机中不可能会移动,它的地址已经被固定。系统分不分配它,它就在那⾥。当为局部变量分配栈内存时,系统就将局部变量存⼊到栈的某个内存块中;当⼦函数运⾏结束局部变量应当被释放时,系统再将这些存⼊局部变量的栈内存中的数据清除掉,恢复原来没有被初始化的状态。
4总结
(1)return
不管是返回指针还是返回值,return将return之后的值存到eax寄存器中,回到⽗函数再将返回的值赋给变量。
(2)局部地址
在函数内返回⼀个指针会出错的原因:⼦函数运⾏完毕时,存局部变量的所有栈地址的内容已经被释放。若在⽗函数中再访问这些地址中的内容时,因为这些地址的内容已经被释放,所访问到的值可能是乱的、不定的。
(3)分配/释放内存
分配内存,就是将某变量存⼊到某块内存中的⼀个地址中;释放内存,就是将此内存中的内容清除掉,恢复内存未被初始化的状态。
此次笔记记录完毕。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。