$f(x) = x$
函数指针
本节将对函数指针的使用进行讲解,在C++中我们应当尽量使用虚函数或多态,回避函数指针。当然,虚函数的底层实现依然是通过函数指针实现的。
函数指针常用场景
作为其他函数的参数
作为回调函数
回调机制在GUI中大量使用,当特定事件发生时,回调函数会被调用,格式如下:
1 | void create_button( int x, int y, const char *text, function callback_func ); |
常见回调函数
定时器回调
1 | setTimeout(void (*func)(), delay); //在时间到后执行func |
回调机制的实现
以Javascript为例,其回调机制实现如下:函数栈和WEB API分别运行于独立环境中,当调用回调API,例如setTimeout时,该API会进入WEB API中。当定时器到达时间后,会将回调函数放置入回调队列,如果函数栈为空或者允许抢占当前正在执行的函数,那么将回调函数再放入函数栈中进行执行。
graph LR node[函数栈] node1[WEB API] node2[回调队列] node1-->node2 node2-->node
回调机制的缺陷
回调机制由于有中断作用,因此增加了程序的复杂度,同时也会导致我们无法使用程序提供的异常处理机制
函数指针基本语法
指针声明
1 | void (*foo) (int); //返回值,指针,参数列表 |
初始化
初始化有两种方式,如下,从下面的代码我们可以看出,函数名实际就是函数指针。
1 | //方式1 |
从汇编层面了解函数调用过程1
call命令及其原理
在汇编语言中,函数调用是通过call命令实现的,这其中涉及到了栈的生长和收缩,在x86中,栈向下生长,一个函数调用例子如下:
Example instruction | What it does |
---|---|
pushl %eax |
subl $4, %esp movl %eax, (%esp) |
popl %eax |
movl (%esp), %eax addl $4, %esp |
call 0x12345 |
pushl %eip movl $0x12345, %eip |
ret |
popl %eip |
栈使用规则
而编译器GCC决定了栈是如何被使用的:
- 在一个函数的入口(在
call
语句后的一条语句)- %eip指向函数的第一条语句
- %esp+4指向第一个参数
- %esp指向返回地址
- 在ret指令后
- %eip包含返回地址
- %esp指向调用者压入栈中保存的参数
- 被调用的函数可能会有垃圾参数
- %eax(以及%edx,如果是64位)保存返回值(如果函数为void,保存垃圾值)
- %ebp, %ebx, %esi, %edi 在
call
命令后必须有内容
总结:
- %eax, %ecx, %edx 是保存调用者参数的寄存器
- %ebp, %ebx, %esi, %edi 是保存被调用者参数的寄存器
栈分配规则
每一个函数都有一段由%ebp, %esp标识的独立的栈空间,一般这些空间是连续的
1 | +------------+ | |
其中:
- 通过移动esp,可以控制栈的生长与收缩
- ebp指向前一个函数保存的ebp
函数调用
函数调用时会保存ebp,然后将当前的esp保存在ebp中:
1 | pushl %ebp |
函数返回
函数返回时会复原esp和ebp
1 | movl %ebp, %esp |
一个完整的例子
C code
1
2
3int main(void) { return f(8)+1; }
int f(int x) { return g(x); }
int g(int x) { return x+3; }assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40_main:
prologue
pushl %ebp
movl %esp, %ebp
body
pushl $8
call _f
addl $1, %eax
epilogue
movl %ebp, %esp
popl %ebp
ret
_f:
prologue
pushl %ebp
movl %esp, %ebp
body
pushl 8(%esp)
call _g
epilogue
movl %ebp, %esp
popl %ebp
ret
_g:
prologue
pushl %ebp
movl %esp, %ebp
save %ebx
pushl %ebx
body
movl 8(%ebp), %ebx
addl $3, %ebx
movl %ebx, %eax
restore %ebx
popl %ebx
epilogue
movl %ebp, %esp
popl %ebp
ret
根据_g
的运算过程,其可以被压缩为:
1 | _g: |
实际上,_f
也可以被压缩,压缩为:
1 | # TODO |