参数
C语言函数调用中参数分为形参(formal parameter)和实参(actual parameter)
- 形参:定义函数时候使用的参数,用来接收调用该函数时传入的参数
- 实参:调用者调用时传递给函数的参数
传参
C中主要有三种传参方式值传递地址传递引用传递,其中引用传递再C++中引入
值传递
地址传递
传递的为实参地址的拷贝,一定要清楚:本质依旧是值传递,只不过实参是地址
引用传递
传递的为实参地址,与地址传递区分,引用传递的实参就是变量本身,非其地址,但在函数中的操作都针对原始实参
对比Java
C中值传递和地址传递可对应Java中的基本数据类型传参与对象引用传参,Java本身按值传参:
- 对于int,float等基本类型传递值即可
- 每个对象引用都是一个指针/地址,指向堆上同一个对象,传递引用即传递地址
示例代码
#include <stdio.h>
int first;
void callee(int value_first, int* addr_first, int& refe_first) {
value_first = 2;
*addr_first = 2;
refe_first = 3;
}
void main() {
first = 1;
callee(first, &first, first);
}
分析
- value_first=first但是&value_first≠&first =>值传递
- addr_first=&first且&addr_first单独存在 =>地址传递,且说明该地址本身作为拷贝又存储在别的区域(int**)
- refe_first=first且&refe_first=&first => 引用传递
活动记录
定义: 在Ch3中了解过栈帧,即活动记录(Activation Record) C语言作为面向过程的语言,当每个函数被调用时,都会产生一个过程记录,就是程序执行过程中函数”运行时栈”上的内容变化,一个函数被调用,反映在栈上的与之相关的内容被称为一帧,其中包含了参数、返回地址、旧ebp值、局部变量以及esp和ebp |
|
---|---|
示例代码
#include <stdio.h>
int first;
int callee(int value_first, int* addr_first) {
value_first = 2;
*addr_first = 2;
return value_first;
}
void main() {
first = 1;
callee(first, &first);
}
函数call过程
当发生函数调用时,编译器和硬件(寄存器)会进行下列动作:
- 将参数入栈
- 返回地址入栈
- 进入callee,旧的帧指针入栈保存(push ebp)
- 让帧指针等于当前栈顶指针(mov ebp,esp),成为新帧指针
- 帧指针偏移一定数值,预留用于保存局部变量的地址空间(sub ebp xxxxh)
部分反汇编结果
参考链接: 汇编语言OFFSET运算符**
//-------------1. 函数调用部分----------
callee(first, &first);
push offset first (020A17Ch) //全局变量first位于数据段偏移020A17Ch处,&first入栈
mov eax,dword ptr [first (020A17Ch)] //取&first处数据放入eax
push eax //eax入栈
//对应步骤1,&first和first先后入栈
call callee (02013BBh) //call地址02013BBh处指令,当前地址入栈,
对应步骤2,进入子函数
add esp,8
}
//-------------2. 子函数部分----------
void callee(int value_first, int* addr_first) {
push ebp //对应步骤3,保存上一栈帧
mov ebp,esp //对应步骤4,移动当前帧到栈顶,为子函数开辟新帧
sub esp,0C0h //对应步骤5,预留局部变量地址空间
push ebx
push esi
push edi //保存原来的寄存器值
lea edi,[ebp-0C0h]
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi] //初始化辅助段,见Ch3汇编分析
函数ret过程
函数返回时
- 如果有返回值,先保存返回值到eax
- esp add,释放预留给局部变量的空间
- 令帧指针ebp等于栈指针esp,从栈中弹出上一个帧指针
- 从栈中弹出返回地址,使用eip保存
- 回到caller,esp add,释放参数占据的空间
```powershell
//——————3.从callee return—————
return value_first;
mov eax,dword ptr [value_first] //步骤1,返回值保存在eax,mov中对数值操作数加[]还是数值
}
pop edi
pop esi
pop ebx //弹出保存的寄存器,恢复之前的状态 add esp,0C0h //步骤2,esp加上之前的偏移,释放预留给局部变量的空间 cmp ebp,esp
call __RTC_CheckEsp (0241212h) //debugger相关,可忽略 mov esp,ebp //步骤3,令帧指针等于栈指针 pop ebp //步骤3,弹出旧的帧指针给ebp ret //步骤4,栈中弹出返回地址,EIP接收
//——————4.回到caller—————
call callee (02413C0h)
add esp,8 //步骤5,栈指针递增,释放被参数占用空间
}
---
<a name="V1UY4"></a>
### 选择题知识点
1. 活动记录什么时候产生:①程序开始执行(可理解为main()的栈帧)②调用子函数
1. 全局变量,静态变量,函数的地址在编译时被compiler确定,但函数内局部变量地址无法被compiler获得
1. 当执行函数callee()后,帧指针的值是①caller()帧的top②callee()帧的底部,可以对比活动记录图示
1. 递归函数,深入n次即产生n个活动记录,递归返回时最终要从栈中弹出n个活动记录,如下当调用factorial(4),最终弹出4个活动记录
```c
int factorial(int n) {
if (n == 1) return n;
return n * factorial(n - 1);
}