函数

$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
2
3
4
5
6
7
//方式1
void foo();
func_pointer = foo;

//方式2
void foo();
func_pointer = &foo;

从汇编层面了解函数调用过程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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
          +------------+   |
| arg 2 | \
+------------+ >- previous function's stack frame
| arg 1 | /
+------------+ |
| ret %eip | /
+============+
| saved %ebp | \
%ebp-> +------------+ |
| | |
| local | \
| variables, | >- current function's stack frame
| etc. | /
| | |
| | |
%esp-> +------------+ /

其中:

  • 通过移动esp,可以控制栈的生长与收缩
  • ebp指向前一个函数保存的ebp

函数调用

函数调用时会保存ebp,然后将当前的esp保存在ebp中:

1
2
pushl %ebp
movl %esp, %ebp

函数返回

函数返回时会复原esp和ebp

1
2
movl %ebp, %esp
popl %ebp

一个完整的例子

  • C code

    1
    2
    3
    int 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
2
3
4
_g:
movl 4(%esp), %eax
addl $3, %eax
ret

实际上,_f也可以被压缩,压缩为:

1
# TODO

参考文献

0%