参数

C语言函数调用中参数分为形参(formal parameter)和实参(actual parameter)

  • 形参:定义函数时候使用的参数,用来接收调用该函数时传入的参数
  • 实参:调用者调用时传递给函数的参数

传参

C中主要有三种传参方式值传递地址传递引用传递,其中引用传递再C++中引入

值传递

传递的为实参的拷贝,二者值相同,但在内存中地址不同

地址传递

传递的为实参地址的拷贝,一定要清楚:本质依旧是值传递,只不过实参是地址

引用传递

传递的为实参地址,与地址传递区分,引用传递的实参就是变量本身,非其地址,但在函数中的操作都针对原始实参

对比Java

C中值传递和地址传递可对应Java中的基本数据类型传参与对象引用传参,Java本身按值传参:

  • 对于int,float等基本类型传递值即可
  • 每个对象引用都是一个指针/地址,指向堆上同一个对象,传递引用即传递地址

示例代码

  1. #include <stdio.h>
  2. int first;
  3. void callee(int value_first, int* addr_first, int& refe_first) {
  4. value_first = 2;
  5. *addr_first = 2;
  6. refe_first = 3;
  7. }
  8. void main() {
  9. first = 1;
  10. callee(first, &first, first);
  11. }

分析
image.png

  • 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
Ch5 函数调用与返回 - 图2

示例代码

  1. #include <stdio.h>
  2. int first;
  3. int callee(int value_first, int* addr_first) {
  4. value_first = 2;
  5. *addr_first = 2;
  6. return value_first;
  7. }
  8. void main() {
  9. first = 1;
  10. callee(first, &first);
  11. }

函数call过程

当发生函数调用时,编译器和硬件(寄存器)会进行下列动作:

  1. 将参数入栈
  2. 返回地址入栈
  3. 进入callee,旧的帧指针入栈保存(push ebp)
  4. 让帧指针等于当前栈顶指针(mov ebp,esp),成为新帧指针
  5. 帧指针偏移一定数值,预留用于保存局部变量的地址空间(sub ebp xxxxh)

部分反汇编结果
参考链接: 汇编语言OFFSET运算符**

  1. //-------------1. 函数调用部分----------
  2. callee(first, &first);
  3. push offset first (020A17Ch) //全局变量first位于数据段偏移020A17Ch处,&first入栈
  4. mov eax,dword ptr [first (020A17Ch)] //取&first处数据放入eax
  5. push eax //eax入栈
  6. //对应步骤1,&first和first先后入栈
  7. call callee (02013BBh) //call地址02013BBh处指令,当前地址入栈,
  8. 对应步骤2,进入子函数
  9. add esp,8
  10. }
  11. //-------------2. 子函数部分----------
  12. void callee(int value_first, int* addr_first) {
  13. push ebp //对应步骤3,保存上一栈帧
  14. mov ebp,esp //对应步骤4,移动当前帧到栈顶,为子函数开辟新帧
  15. sub esp,0C0h //对应步骤5,预留局部变量地址空间
  16. push ebx
  17. push esi
  18. push edi //保存原来的寄存器值
  19. lea edi,[ebp-0C0h]
  20. mov ecx,30h
  21. mov eax,0CCCCCCCCh
  22. rep stos dword ptr es:[edi] //初始化辅助段,见Ch3汇编分析

函数ret过程

函数返回时

  1. 如果有返回值,先保存返回值到eax
  2. esp add,释放预留给局部变量的空间
  3. 令帧指针ebp等于栈指针esp,从栈中弹出上一个帧指针
  4. 从栈中弹出返回地址,使用eip保存
  5. 回到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,栈指针递增,释放被参数占用空间 }

  1. ---
  2. <a name="V1UY4"></a>
  3. ### 选择题知识点
  4. 1. 活动记录什么时候产生:①程序开始执行(可理解为main()的栈帧)②调用子函数
  5. 1. 全局变量,静态变量,函数的地址在编译时被compiler确定,但函数内局部变量地址无法被compiler获得
  6. 1. 当执行函数callee()后,帧指针的值是①caller()帧的topcallee()帧的底部,可以对比活动记录图示
  7. 1. 递归函数,深入n次即产生n个活动记录,递归返回时最终要从栈中弹出n个活动记录,如下当调用factorial(4),最终弹出4个活动记录
  8. ```c
  9. int factorial(int n) {
  10. if (n == 1) return n;
  11. return n * factorial(n - 1);
  12. }