0引
C/C++中,函数内部的一切变量(函数内部局部变量,形参 )都是在其被调用时才被分配内存单元。子函数运行结束时,所有局部变量的内存单元会被系统释放。形参和函数内部的局部变量的生命期和作用域都是在函数内部( static变量的生命期除外)。
在C中,函数被调用时的传参方式有两种形式:传值和传址。
传址的好处:
(1)能在函数内部通过实参地址间接地改变实参的值。
(2)当所传实参内容比较庞大时,传址只是复制了整个实参的地址过去,指针依据同一个地址访问实参变量。而传值就会将实参内容整个拷贝过去,形参会跟实参占一样大的内存,栈空间是有限的。当然了,在弱小的程序中,传址的这个优点不会被体现出来。
在函数中,可以随意的返回一个局部变量。但如果返回一个局部变量的地址(指针 ),编译器就会给出警告(编译器也不可能那么完美能够彻底的检查出段错误)。在函数内部返局部指针这的确是一个危险的操作。鄙人的笔记先将用return返回值(指针为地址值)的机制搞清楚后再分析一下。
1函数内部返回局部变量过程
1.1结论
Linux等的C语言中return返回值的机制为:将返回值存入eax寄存器中,然后系统再将eax中的值赋给变量(i)。
(1)编写一个简单的C源程序
在linux 下敲一个简单的函数调用的程序:
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汇编代码 ]。 |
其实,在C程序中,函数的返回值不一定就是被存到eax寄存器中,还得看返回值的类型,《professiona Assembly Language》一书中有这么一段话:
Likewise, the C style defines a common method for returning values to the main program, using the EAX register for 32-bit results (such as short integers), the EDX:EAX register pair for 64-bit integer values, and the FPU ST(0) register for floating-point values. |
可以看到,不同位的函数的返回值被存的位置。
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)分配/释放内存
分配内存,就是将某变量存入到某块内存中的一个地址中;释放内存,就是将此内存中的内容清除掉,恢复内存未被初始化的状态。
在子函数中,未释放的堆地址可被函数返回给父函数使用,但栈地址不可被子函数返回给父函数使用。
C Note Over。