C语言的变量,分为全局变量、局部变量、静态全局变量(作用域在文件内、生命周期到进程结束)、静态局部变量(作用域在函数内、生命周期到进程结束)。
通常所说的局部变量,是在函数内定义的、不用static关键字修饰、分配在栈上的变量,其作用域在函数内,生命周期在函数调用的过程内,随着函数调用结束和栈的变化而被销毁。
因此,严禁返回局部变量的指针!!!
因为随着函数调用的结束和栈的变化,返回主调函数后,被调函数的局部变量已经被销毁了,其指针变成了野指针!!!
野指针的值是不确定的,结果也是不确定的,导致的BUG也远比空指针更危险,更难查。
x86 32位C程序的ABI(应用程序二进制接口),是通过栈传递调用参数,通过EAX寄存器返回调用结果。ARM 32位的C程序是前4个参数通过r0~r3传递,多于4个的参数通过栈传递,通过r0寄存器返回调用结果。64位的ABI没详细研究过,暂且不提。
以x86 32位为例,函数调用过程是这样的:
1,主调函数把参数压栈
2,主调函数使用call指令调用被调函数
3,被调函数保存ebp寄存器
4,被调函数把esp寄存器(指向栈顶)保存到ebp寄存器
5,被调函数更改esp寄存器,分配局部变量的内存!重点!
6,被调函数用"ebp寄存器加偏移"访问调用参数,并执行计算过程
7,被调函数用ebp寄存器回写esp寄存器,从而清理栈帧!
该步之后,局部变量的内存已经被清理,局部变量失效!
8,被调函数恢复第3步保存的ebp寄存器的值
9,返回主调函数,调用过程结束。
函数刚分配完局部变量后的栈帧结构(x86 32位,内存地址从上往下依次减小):
调用参数 -----ebp+8
返回地址 -----ebp+4
ebp寄存器原值 ---- ebp指向这里
局部变量 ------------ esp指向这里
汇编代码大概如下:
f:被调函数
pushl %ebp #保存ebp原值
movl %esp,%ebp #保存esp到ebp
subl $4,%esp #分配4字节的局部变量,假设就1个32位的局部变量
movl 8(%ebp), %eax #获取第1个调用参数
...... #计算过程,其结束时要把返回值写入eax寄存器
movl %ebp,%esp #恢复esp寄存器
popl %ebp #恢复ebp寄存器,此时esp指向返回地址
ret #弹出返回地址,返回主调函数
main:主调函数
......
movl $1,%eax #准备参数,假设就1个参数
pushl %eax #参数压栈
call f #调用函数f
addl $4,%esp #清理之前压栈的调用参数,此时eax存储被调函数的返回值
......
下图的代码在linux+gcc下是可以的,宏S直接在main函数内展开,其内部定义的变量s也是main函数的局部变量,可直接在main函数内打印其值,不属于"返回局部变量的指针"。
虽然能用函数的地方尽量不用宏,尤其是C也支持了inline之后,但在某些地方宏仍然是不能被替代的。
下图的用法,在某些需要把出错码转化为出错信息并打印时,可以用到。