1.5.4 延迟绑定
延迟绑定(Lazy Binding)是动态链接器优化,将外部函数地址的解析推迟到第一次调用时。通过PLT(过程链接表)和GOT(全局偏移表)实现,本质上是函数指针的数组,在调用时动态更新。
在动态链接的程序中,延迟绑定是一种优化技术:外部函数(如 printf、malloc)的实际地址不是在程序启动时解析,而是在第一次被调用时才由动态链接器确定。
这通过 PLT(Procedure Linkage Table) 和 GOT(Global Offset Table) 配合实现,本质上就是函数指针表的运行时动态更新------GOT 中保存的是真正的函数地址(初始指向 PLT 中的一段解析代码),第一次调用时动态链接器会填充正确的地址,之后直接通过 GOT 跳转。
1.5.4.1 例子
C++
// lazy.c
#include <stdio.h>
int main() {
printf("第一次调用 printf\n");
printf("第二次调用 printf\n");
return 0;
}

1.5.4.2 分析




在动态链接的程序中,外部函数(如 puts)的地址并不在程序启动时就确定,而是在第一次调用时由动态链接器解析并填充到 GOT 表中。
-
PLT(过程链接表):存放一小段代码,负责从 GOT 中读取函数地址并跳转。
-
GOT(全局偏移表):存放实际函数地址的数组,初始时指向 PLT 中的解析器,第一次调用后更新为真实地址。
Assembly
puts@plt:
adrp x16, GOT_page ; 获取 GOT 页基址
ldr x17, [x16, #offset] ; 从 GOT 条目加载地址到 x17
add x16, x16, #something
br x17 ; 间接跳转到该地址
Assembly
编译:gcc -g -O0 -o lazy lazy.c(默认延迟绑定)
禁用 ASLR(可选,便于地址重现):
bash
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Assembly
gdb ./lazy
break main # 断点1:停在 main 入口
break *0x0000aaaaaaaa063c # 断点2:停在 puts@plt 中的 br x17 指令
注:0xaaaaaaaa063c 是 br x17 的地址,可以通过 disas puts@plt 查看

Assembly
##程序会执行到第一次 printf(实际是 puts)的 PLT 桩,并在 br x17 指令处停下:
Breakpoint 2, 0x0000aaaaaaaa063c in puts@plt ()

Assembly
info register x16 # 查看 GOT 页基址(此时已经过 add 指令)
info register x17 # 应该是一个解析器地址(如 0xaaaaaaaa05d0)
x/gx $x16 # 查看 GOT 条目内容,应与 x17 相同
##预期输出:
x17 0xaaaaaaaa05d0x/gx $x16 -> 0xaaaaaaaa05d0
单步执行 br x17,进入动态链接器
Assembly
stepi
程序会跳转到 0xaaaaaaaa05d0(动态链接器内部,无源码)。这证实了间接跳转。
在真正的 puts 函数设置断点,继续执行
Assembly
break puts
continue
动态链接器完成后会调用 puts,程序停在 puts 断点:
C
Breakpoint 3, __GI__IO_puts (str=0xaaaaaaaa07a0 "第一次调用 printf") ...
结论:第一次调用完成,且 GOT 已被更新。
从 puts 返回,继续执行到第二次调用
C
finish # 从 puts 返回到 maincontinue # 继续执行,遇到第二次 printf 的 puts@plt
程序再次停在 br x17 断点(第二次调用)。
观察第二次调用时的状态(解析后)
Assembly
info register x17 # 应为 puts 的真实地址
x/gx $x16 # GOT 条目内容也应相同
##预期输出:
x17 0xffff7e61af0x/gx
$x16 -> 0xffff7e61af0
继续执行,程序正常结束
Assembly
continue
输出 第二次调用 printf,程序退出。
关键点讲解
| 步骤 | 观察内容 | 技术意义 |
|---|---|---|
| 第一次 br x17 前 | x17 为解析器地址(如 0xaaaaaaaa05d0) | GOT 未初始化,指向 PLT 内的解析桩 |
| stepi 后 | 跳转到解析器地址 | 间接跳转实现回调(框架调用用户代码) |
| break puts 命中 | 动态链接器找到符号并调用 | 延迟绑定的符号解析完成 |
| 第二次 br x17 前 | x17 为真实函数地址(如 0xffff7e61af0) | GOT 已被更新,后续直接调用 |
| x/gx $x16 | GOT 内容与 x17 一致 | 验证 GOT 就是函数指针表 |
1.5.4.3 三种跳转指令对比
| bl | Branch with Link | 直接跳转(相对地址偏移) | ✅ 是(pc+4 存入 x30) | 调用普通函数(编译时地址已知) |
|---|---|---|---|---|
| blr | Branch with Link to Register | 间接跳转(从寄存器取地址) | ✅ 是(pc+4 存入 x30) | 调用函数指针、回调、虚函数 |
| br | Branch to Register | 间接跳转(从寄存器取地址) | ❌ 否 | 用于实现 switch 跳转表、ret(br x30)、长跳转 |
- bl (直接调用)
-
目标地址由 PC 相对偏移 编码在指令中,编译时确定。
-
适合调用静态链接的函数或动态库中的已知 PLT 桩。
-
示例:bl printf 或 bl 0x1234。
- blr (间接调用 + 保存返回地址)
-
从寄存器(如 x0-x30)中读取目标地址。
-
同时将下一条指令的地址写入 x30(链接寄存器)。
-
适合所有函数指针调用、C++ 虚函数、回调函数等运行时才能确定地址的场景。
-
示例:blr x1 或 blr x16。
- br (纯间接跳转)
-
从寄存器中读取目标地址,不保存返回地址。
-
常用于:
-
函数返回:ret 实际是 br x30 的别名。
-
状态机跳转表:从表中加载地址到 x16 然后 br x16。
-
实现 goto 或尾调用优化。
-
-
示例:br x8、ret(即 br x30)。
1.5.5 设计模式
设计模式的核心思想之一是"将变化的部分与不变的部分分离"。在 C 语言中,函数指针是实现这一目标的有力工具,它允许我们将行为作为参数传递给框架,或在不同对象间共享相同的接口。
1.5.5.1 策略模式(Strategy Pattern)
将不同的算法 / 逻辑封装成独立函数,通过函数指针动态选择执行。
解决:大量 if-else、代码耦合、无法动态切换逻辑
应用:计算器、协议解析、设备驱动、状态机
1.5.5.1.1 例子
C++
// strategy.c
#include <stdio.h>
// 策略函数类型:两个整数,返回整数
typedef int (*strategy_t)(int, int);
// 具体策略:加法
int add(int a, int b) { return a + b; }
// 具体策略:减法
int sub(int a, int b) { return a - b; }
// 具体策略:乘法
int mul(int a, int b) { return a * b; }
// 上下文:使用策略的客户
int execute_strategy(strategy_t strat, int x, int y) {
return strat(x, y); // 通过函数指针调用具体算法
}
int main() {
int x = 10, y = 5;
printf("10 + 5 = %d\n", execute_strategy(add, x, y));
printf("10 - 5 = %d\n", execute_strategy(sub, x, y));
printf("10 * 5 = %d\n", execute_strategy(mul, x, y));
// 运行时动态切换策略
strategy_t current = add;
printf("current: 10 + 5 = %d\n", execute_strategy(current, x, y));
current = mul;
printf("switch to mul: 10 * 5 = %d\n", execute_strategy(current, x, y));
return 0;
}

分析





Java
(gdb) p execute_strategy
$3 = {int (strategy_t, int, int)} 0xaaaaaaaa07b8 <execute_strategy>
(gdb) p add
$4 = {int (int, int)} 0xaaaaaaaa0758 <add>
(gdb) p sub
$5 = {int (int, int)} 0xaaaaaaaa0778 <sub>
(gdb) p mul
$6 = {int (int, int)} 0xaaaaaaaa0798 <mul>
execute_strategy (strat=0xaaaaaaaa0758 <add>, x=10, y=5) at strategy.c:18
execute_strategy (strat=0xaaaaaaaa0758 <add>, x=10, y=5) at strategy.c:18
函数指针strat指向add,将add函数首地址传入execute_strategy函数,return strat(x, y)相当于return add(x,y);
Assembly
//add函数首地址
(gdb) p add
$2 = {int (int, int)} 0xaaaaaaaa0758 <add>
//execute_strategy函数首地址
Dump of assembler code for function execute_strategy:
0x0000aaaaaaaa07b8 <+0>: stp x29, x30, [sp, #-32]!
//execute_strategy(add, x, y)->跳转到add函数首地址
0x0000aaaaaaaa07d8 <+32>: blr x2
//此时x2中的值,是add函数的首地址
(gdb) info register x2
x2 0xaaaaaaaa0758 187649984431960
1.5.5.2 命令模式(Command Pattern)
将请求封装为一个对象(在 C 语言中为函数指针 + 参数),从而允许将请求参数化、排队、记录日志、支持撤销等。
1.5.5.2.1 例子
C++
// command.c
#include <stdio.h>
#include <stdlib.h>
// 命令结构体:包含函数指针和参数数据
typedef struct command {
void (*execute)(void*); // 执行函数
void* data; // 命令携带的数据
} command_t;
// 具体命令:打印整数
void print_int(void* d) { //void* 参数接收的是"任意类型对象的地址"
int* p = (int*)d; //*将 void* 转回 int**
printf("print_int: %d\n", *p);//*解引用,得到int值*
}
// 具体命令:打印字符串
void print_str(void* d) { //void* 参数接收的是"任意类型对象的地址"
char* s = (char*)d; //*将 void* 转回 char**
printf("print_str: %s\n", s);//*解引用,得到char值*
}
// 命令队列(简化)
#define MAX_CMDS 10
command_t cmd_queue[MAX_CMDS];
int cmd_count = 0;
void add_command(command_t cmd) {
if (cmd_count < MAX_CMDS) cmd_queue[cmd_count++] = cmd;
}
void execute_all_commands() {
for (int i = 0; i < cmd_count; i++) {
cmd_queue[i].execute(cmd_queue[i].data); // 间接调用
}
}
int main() {
int a = 42, b = 100;
char* msg = "Hello Command!";
// 构造命令并加入队列
add_command((command_t){print_int, &a});
add_command((command_t){print_str, msg});
add_command((command_t){print_int, &b});
// 批量执行
execute_all_commands();
return 0;
}

1.5.5.2.2 分析


Java
(gdb) p print_int
$1 = {void (void *)} 0xaaaaaaaa0858 <print_int>
(gdb) p print_str
$2 = {void (void *)} 0xaaaaaaaa0890 <print_str>
(gdb) p add_command
$3 = {void (command_t)} 0xaaaaaaaa08c0 <add_command>
(gdb) p execute_all_commands
$4 = {void ()} 0xaaaaaaaa0920 <execute_all_commands>
函数print_int,print_str,add_command,execute_all_commands
在0xaaaaaaaa0000~0xaaaaaaaa1000 权限r-xp(可读、可执行)代码段。

初始化变量。
disas main:


disas add_command:

disas execute_all_commands:

Assembly
add_command((command_t){print_int, &a});
add_command((command_t){print_str, msg});
add_command((command_t){print_int, &b});
C
正确使用** void* **的通用模式
通常我们约定:传入的 void* 数据必须与函数内部转换的指针类型实际匹配。例如:
print_int 应该只接收 int* 转换来的 void*。
print_str 应该只接收 char* 转换来的 void*。
这种匹配靠程序员保证,编译器无法检查。
//错误操作:
double pi = 3.14;
print_int(&pi); // 将 double* 转为 void*,内部转为 int* 再解引用 → 危险!
//
在实际编程中(如命令模式、回调函数),通过 void* 携带数据是非常常见的技巧,
但必须严格遵守类型契约:哪个函数用哪种数据类型,就应该只传入该类型的地址。如果混用,后果自负。
Assembly
//main 函数调用add_command
0x0000aaaaaaaa09dc <+68>: adrp x2, 0xaaaaaaaa0000
0x0000aaaaaaaa09e0 <+72>: add x0, x2, #0x858
0x0000aaaaaaaa09e4 <+76>: add x2, sp, #0x8
0x0000aaaaaaaa09e8 <+80>: mov x1, x2
0x0000aaaaaaaa09ec <+84>: bl 0xaaaaaaaa08c0 <add_command>
0x0000aaaaaaaa09f0 <+88>: adrp x0, 0xaaaaaaaa0000
0x0000aaaaaaaa09f4 <+92>: add x20, x0, #0x890
0x0000aaaaaaaa09f8 <+96>: ldr x21, [sp, #16]
0x0000aaaaaaaa09fc <+100>: mov x0, x20
0x0000aaaaaaaa0a00 <+104>: mov x1, x21
0x0000aaaaaaaa0a04 <+108>: bl 0xaaaaaaaa08c0 <add_command>
0x0000aaaaaaaa0a08 <+112>: adrp x0, 0xaaaaaaaa0000
0x0000aaaaaaaa0a0c <+116>: add x22, x0, #0x858
0x0000aaaaaaaa0a10 <+120>: add x0, sp, #0xc
0x0000aaaaaaaa0a14 <+124>: mov x23, x0
0x0000aaaaaaaa0a18 <+128>: mov x0, x22
0x0000aaaaaaaa0a1c <+132>: mov x1, x23
0x0000aaaaaaaa0a20 <+136>: bl 0xaaaaaaaa08c0 <add_command>
//----------------------------------------------------------------------------
$1 = {void (void *)} 0xaaaaaaaa0858 <print_int>
$2 = {void (void *)} 0xaaaaaaaa0890 <print_str>
$3 = {void (command_t)} 0xaaaaaaaa08c0 <add_command>
Assembly
void add_command(command_t cmd) {
if (cmd_count < MAX_CMDS) cmd_queue[cmd_count++] = cmd;
}
#0xaaaaaaaa08c0 <add_command>入口地址
0x0000aaaaaaaa08c0 <+0>: sub sp, sp, #0x10
0x0000aaaaaaaa08c4 <+4>: stp x0, x1, [sp]
0x0000aaaaaaaa08c8 <+8>: adrp x0, 0xaaaaaaac0000
0x0000aaaaaaaa08cc <+12>: add x0, x0, #0xb8
0x0000aaaaaaaa08d0 <+16>: ldr w0, [x0]
#如果 cmd_count > 9,
0x0000aaaaaaaa08d4 <+20>: cmp w0, #0x9
#则跳转到 +84(即函数末尾的 nop; ret),跳过添加操作。
#对应 C 语句 if (cmd_count < MAX_CMDS) 的否定条件。
0x0000aaaaaaaa08d8 <+24>: b.gt 0xaaaaaaaa0914 <add_command+84>
0x0000aaaaaaaa08dc <+28>: adrp x0, 0xaaaaaaac0000
0x0000aaaaaaaa08e0 <+32>: add x0, x0, #0xb8
0x0000aaaaaaaa08e4 <+36>: ldr w0, [x0]
0x0000aaaaaaaa08e8 <+40>: add w2, w0, #0x1
0x0000aaaaaaaa08ec <+44>: adrp x1, 0xaaaaaaac0000
0x0000aaaaaaaa08f0 <+48>: add x1, x1, #0xb8
0x0000aaaaaaaa08f4 <+52>: str w2, [x1]
0x0000aaaaaaaa08f8 <+56>: adrp x1, 0xaaaaaaac0000
0x0000aaaaaaaa08fc <+60>: add x1, x1, #0x18
0x0000aaaaaaaa0900 <+64>: sxtw x0, w0
0x0000aaaaaaaa0904 <+68>: lsl x0, x0, #4
0x0000aaaaaaaa0908 <+72>: add x2, x1, x0
0x0000aaaaaaaa090c <+76>: ldp x0, x1, [sp]
0x0000aaaaaaaa0910 <+80>: stp x0, x1, [x2]
0x0000aaaaaaaa0914 <+84>: nop
0x0000aaaaaaaa0918 <+88>: add sp, sp, #0x10
0x0000aaaaaaaa091c <+92>: ret



Plain
(gdb) p cmd_queue
$15 = {{execute = 0xaaaaaaaa0858 <print_int>, data = 0xffffffffeca8},
{execute = 0xaaaaaaaa0890 <print_str>, data = 0xaaaaaaaa0aa8},
{execute = 0xaaaaaaaa0858 <print_int>, data = 0xffffffffecac},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0},
{execute = 0x0, data = 0x0}}
//将print_int,print_str,print_int三个任务添加到对列。

C
void add_command(command_t cmd) {
if (cmd_count < MAX_CMDS) cmd_queue[cmd_count++] = cmd;
}
//cmd_count=2,第三个任务
(gdb) p print_int
$16 = {void (void *)} 0xaaaaaaaa0858 <print_int>
(gdb) p cmd
$17 = {execute = 0xaaaaaaaa0858 <print_int>, data = 0xffffffffecac}
(gdb) p cmd.execute
$18 = (void (*)(void *)) 0xaaaaaaaa0858 <print_int>
(gdb) p cmd.data
$19 = (void *) 0xffffffffecac
(gdb) p cmd_count
$20 = 3
//--------------------------------------------------------------
add_command((command_t){print_int, &a}); 展开是:
command_t cmd = {print_int, &a};
或:
command_t cmd;
cmd.execute = print_int;
cmd.data = &a;
add_command(cmd);
//-------------------------------------------------------------
command_t cmd -->任务
//任务的数据结构
typedef struct command {
void (*execute)(void*); // 执行函数 --> md_queue[2].execute -->print_int
void* data; // 命令携带的数据 --> cmd_queue[2].data --> &b
} command_t;
add_command((command_t){print_int, &b});
add_command(command_t cmd) --> add 第三个任务 cmd_queue[2]-->{print_int, &b}
//执行第三个任务
execute_all_commands --> cmd_queue[2].execute(cmd_queue[2].data);
//第三个任务的数据结构
md_queue[2].execute -->print_int
cmd_queue[2].data --> &b
//第三个任务
print_int(&b)

1.5.5.2.3 命令模式和策略模式的区别
| 角度 | 策略模式 (Strategy) | 命令模式 (Command) |
|---|---|---|
| 目的 | 封装算法,使其可以互换 | 封装请求(动作 + 参数),使其可存储、排队、撤销 |
| 关注点 | "如何做" (How) | "做什么" (What) + "何时做" (When) |
| 调用关系 | 上下文主动调用策略 | 调用者(Invoker)触发命令,命令执行者(Receiver)完成实际工作 |
| 参数传递 | 通常通过函数参数传递(如 execute_strategy(strat, x, y)) | 参数与命令对象绑定(如 {print_int, &a}),调用时无需额外传参 |
| 是否支持撤销 | 一般不涉及 | 常常支持(存储状态,提供 undo()) |
| 典型例子 | 排序算法、压缩算法、税费计算 | GUI 按钮点击、任务队列、事务回滚 |
- 策略模式:函数指针 + 参数(数据由调用方提供)
C
typedef int (*strategy_t)(int, int);
--> execute_strategy(add, x, y)
- 命令模式:函数指针 + 数据指针(数据与函数捆绑成对象)
C
typedef struct command {
void (*execute)(void*); // 执行函数 --> md_queue[i].execute -->print_int
void* data; // 命令携带的数据 --> cmd_queue[i].data --> &b
} command_t;
add_command((command_t){print_int, &a});
cmd_queuei.execute(cmd_queuei.data);
状态模式(State Pattern)
状态模式允许对象在内部状态改变时改变其行为。在 C 语言中,状态模式通常通过函数指针表(状态表)实现:每个状态对应一组操作函数,状态切换时只需改变指向当前状态函数表的指针。
例子
功能说明
-
门有三种状态:关闭(CLOSED)、打开(OPEN)、锁定(LOCKED)。
-
支持三个操作:open、close、lock、unlock。
-
不同状态下对相同操作的反应不同。例如:
-
关闭状态下 open 会切换到打开状态;lock 会切换到锁定状态。
-
锁定状态下 open / close 无效;unlock 会切换到关闭状态。
-
打开状态下 close 切换到关闭状态;lock 无效(或提示先关闭)。
-
C++
// state_door_simple.c
#include <stdio.h>
typedef struct Door Door;
// 操作函数指针类型
typedef void (*op_func_t)(Door*);
// 状态结构体:包含该状态下的四个操作函数
typedef struct DoorState {
op_func_t open;
op_func_t close;
op_func_t lock;
op_func_t unlock;
const char* name;
} DoorState;
// 门结构体:持有当前状态
struct Door {
const DoorState* state;
};
// 前向声明状态实例
extern const DoorState STATE_CLOSED;
extern const DoorState STATE_OPEN;
extern const DoorState STATE_LOCKED;
// 状态函数实现
static void open_closed(Door* d) {
printf("当前状态: %s, 执行 open -> 切换到 OPEN\n", d->state->name);
d->state = &STATE_OPEN;
}
static void close_closed(Door* d) {
printf("当前状态: %s, 执行 close -> 无变化 (已关闭)\n", d->state->name);
}
static void lock_closed(Door* d) {
printf("当前状态: %s, 执行 lock -> 切换到 LOCKED\n", d->state->name);
d->state = &STATE_LOCKED;
}
static void unlock_closed(Door* d) {
printf("当前状态: %s, 执行 unlock -> 无变化 (未锁定)\n", d->state->name);
}
static void open_open(Door* d) {
printf("当前状态: %s, 执行 open -> 无变化 (已打开)\n", d->state->name);
}
static void close_open(Door* d) {
printf("当前状态: %s, 执行 close -> 切换到 CLOSED\n", d->state->name);
d->state = &STATE_CLOSED;
}
static void lock_open(Door* d) {
printf("当前状态: %s, 执行 lock -> 无效 (请先关闭)\n", d->state->name);
}
static void unlock_open(Door* d) {
printf("当前状态: %s, 执行 unlock -> 无变化 (未锁定)\n", d->state->name);
}
static void open_locked(Door* d) {
printf("当前状态: %s, 执行 open -> 无效 (已锁定)\n", d->state->name);
}
static void close_locked(Door* d) {
printf("当前状态: %s, 执行 close -> 无效 (已锁定)\n", d->state->name);
}
static void lock_locked(Door* d) {
printf("当前状态: %s, 执行 lock -> 无变化 (已锁定)\n", d->state->name);
}
static void unlock_locked(Door* d) {
printf("当前状态: %s, 执行 unlock -> 切换到 CLOSED\n", d->state->name);
d->state = &STATE_CLOSED;
}
// 定义状态表实例
const DoorState STATE_CLOSED = {
.open = open_closed,
.close = close_closed,
.lock = lock_closed,
.unlock = unlock_closed,
.name = "CLOSED"
};
const DoorState STATE_OPEN = {
.open = open_open,
.close = close_open,
.lock = lock_open,
.unlock = unlock_open,
.name = "OPEN"
};
const DoorState STATE_LOCKED = {
.open = open_locked,
.close = close_locked,
.lock = lock_locked,
.unlock = unlock_locked,
.name = "LOCKED"
};
// 门操作封装(通过当前状态调用)
void door_open(Door* d) { d->state->open(d); }
void door_close(Door* d) { d->state->close(d); }
void door_lock(Door* d) { d->state->lock(d); }
void door_unlock(Door* d) { d->state->unlock(d); }
int main() {
Door d;
d.state = &STATE_CLOSED;
printf("=== 门状态机演示 ===\n");
door_open(&d); // CLOSED -> OPEN
door_open(&d); // OPEN 中再打开: 无效
door_close(&d); // OPEN -> CLOSED
door_lock(&d); // CLOSED -> LOCKED
door_open(&d); // LOCKED: 无效
door_unlock(&d); // LOCKED -> CLOSED
door_open(&d); // CLOSED -> OPEN
door_close(&d); // OPEN -> CLOSED
return 0;
}

1.5.5.3.2 分析:





C
//0xaaaaaaaa0000~0xaaaaaaaa2000 权限:r-xp 代码段
0xaaaaaaaa0000 0xaaaaaaaa2000 0x2000 0x0 r-xp /home/parallels/my/ADT/function/state
(gdb) p door_open
$1 = {void (Door *)} 0xaaaaaaaa0c88 <door_open>
(gdb) p door_close
$2 = {void (Door *)} 0xaaaaaaaa0cb4 <door_close>
(gdb) p door_lock
$3 = {void (Door *)} 0xaaaaaaaa0ce0 <door_lock>
(gdb) p door_unlock
$4 = {void (Door *)} 0xaaaaaaaa0d0c <door_unlock>
(gdb) p STATE_LOCKED
$5 = {open = 0xaaaaaaaa0ba8 <open_locked>, close = 0xaaaaaaaa0bdc <close_locked>,
lock = 0xaaaaaaaa0c10 <lock_locked>, unlock = 0xaaaaaaaa0c44 <unlock_locked>,
name = 0xaaaaaaaa1090 "LOCKED"}
(gdb) p STATE_OPEN
$6 = {open = 0xaaaaaaaa0ac8 <open_open>, close = 0xaaaaaaaa0afc <close_open>,
lock = 0xaaaaaaaa0b40 <lock_open>, unlock = 0xaaaaaaaa0b74 <unlock_open>,
name = 0xaaaaaaaa1088 "OPEN"}
(gdb) p STATE_CLOSED
$7 = {open = 0xaaaaaaaa09d8 <open_closed>, close = 0xaaaaaaaa0a1c <close_closed>,
lock = 0xaaaaaaaa0a50 <lock_closed>, unlock = 0xaaaaaaaa0a94 <unlock_closed>,
name = 0xaaaaaaaa1080 "CLOSED"}
(gdb) p open_closed
$8 = {void (Door *)} 0xaaaaaaaa09d8 <open_closed>
(gdb) p close_closed
$9 = {void (Door *)} 0xaaaaaaaa0a1c <close_closed>
(gdb) p open_open
$16 = {void (Door *)} 0xaaaaaaaa0ac8 <open_open>
(gdb) p close_open
$17 = {void (Door *)} 0xaaaaaaaa0afc <close_open>
C++
typedef struct Door Door; // ① 前向声明,使得后续可以只用 Door 代替 struct Door
typedef void (*op_func_t)(Door*); // ② 定义函数指针类型:接收 Door*,无返回值
/*
这里 op_func_t 是一个函数指针类型,它接受一个 Door* 类型的参数。如果在此之前没有 Door 类型的任何声明,
编译器会报错:"未知类型名 Door"。
通过前向声明 typedef struct Door Door;,编译器知道 Door 是一个结构体类型(虽然还不完整),
所以可以合法地使用 Door* 作为参数类型。后续再给出 struct Door 的完整定义:
在完整定义出现之前,不能定义 Door 类型的变量(因为编译器不知道大小),也不能访问其成员。
但可以定义指向 Door 的指针,因为指针大小固定。
可以声明以 Door 为参数或返回值的函数(但函数体内仍不能访问成员)
*/
typedef struct DoorState {
op_func_t open; // ③ 成员都是函数指针
op_func_t close;
op_func_t lock;
op_func_t unlock;
const char* name;
} DoorState;
struct Door {
const DoorState* state; // ④ 门对象持有一个指向状态结构体的指针
};
//在 C 语言中,extern 关键字用于声明一个变量或函数是在别处定义的(通常在其他源文件或本文件后面)。
//对于全局变量,默认情况下(不带 static)已经有外部链接属性,但加 extern 可以显式声明而不定义。
extern const DoorState STATE_CLOSED; // ⑤ 声明外部的全局常量状态实例
extern const DoorState STATE_OPEN;
extern const DoorState STATE_LOCKED;
//这几行是声明,告诉编译器:"STATE_CLOSED 等变量是 const DoorState 类型,
//它们在别处定义(可能在同一个文件后面或其他源文件中),链接时请找到它们。"
//实际定义出现在后面:
const DoorState STATE_CLOSED = { ... };
const DoorState STATE_OPEN = { ... };
const DoorState STATE_LOCKED = { ... };
C
**const**
初始化时,只要是希望不被修改的值,都需要被定义成 const 类型,防止程序运行时被修改。
原因:
安全性:状态表的内容(函数指针和状态名)应当是 只读的,因为在程序生命周期中不会改变。
如果某个代码错误地试图修改 STATE_CLOSED.open,加 const 后编译器会报错。
内存优化:const 全局变量通常被放置于只读数据段(.rodata),与可读写的 .data 或 .bss 段分离。
多个门对象可以共享同一份状态表,避免不必要的内存拷贝。
清晰表达意图:任何阅读代码的人看到 const 就知道这个变量是常量,不会在运行时被修改,有助于理解设计。
哪些数据应该使用const?
全局或静态的配置数据(如状态表、虚函数表、查找表)。
字符串常量(const char*)。
任何不希望被意外修改的数据。
哪些数据不应该用 const?
必须修改的变量(如门对象的 state 指针,
它是运行时变化的,所以struct Door中的state是指向const DoorState的指针,但指针本身不是const)。
**extern**
如果直接写 const DoorState STATE_CLOSED; 而没有 extern,编译器会认为这是一个定义(分配内存),
可能导致重复定义错误(当多个文件包含同一头文件时)。
extern 告诉编译器:"这只是声明,不是定义",避免多重定义。
对于 const 全局变量,C 语言默认具有内部链接(除非显式使用 extern),
所以在头文件中使用 extern const 是常见做法,使得多个源文件可以共享同一个只读常量。
总结:
extern:声明变量存在,不分配内存,链接时解析。
const:限定变量只读,不可修改。
组合 extern const 用于声明一个全局只读变量,定义在其他地方。
**typedef**
struct Door:声明一个名为 Door 的结构体标签
(不完全类型,此时编译器只知道这个类型存在,但不知道其包含哪些成员)。
typedef:将 struct Door 定义为一个新的类型别名 Door(注意这里 Door 既是标签名也是类型名,
但通常标签和类型名可以相同)。
整体效果:定义了一个类型 Door,它代表 struct Door,并且 struct Door 是不完全类型。
typedef struct Door Door; // 前向声明 + 类型别名
struct Door; // 只声明结构体标签(不定义)
如果只用 struct Door; 而没有 typedef,那么使用类型时必须写 struct Door,
而 Door 不是类型名。为了让 Door 作为类型名,typedef 是必要的。
总结:
typedef struct Door Door; 是前向声明,使得 Door 成为一个不完全类型,可以用于指针或函数声明。
它解决了类型相互引用(如 op_func_t 需要 Door*,而 Door 的成员又用了 op_func_t)的循环依赖问题。
完整定义在后面给出,此时类型变为完整。
//------------
extern const 允许将状态表声明与定义分离,使得多个文件可以共享同一个常量状态表,且避免重复定义。
前向声明 typedef struct Door Door; 让函数指针类型 op_func_t 可以顺利使用 Door* 参数,
同时门结构体的定义又可以包含 DoorState*,形成了合理的依赖关系。这正是相互递归数据结构的典型处理方式。
C
/*
定义状态表中的函数指针,定义"在当前状态下,收到某个操作时应该做什么":
.open = open_closed;
表示:当门处于关闭状态时,执行"开门"操作,应该调用 open_closed 函数。
.close = close_closed;
表示:当门处于关闭状态时,执行"关门"操作,应该调用 close_closed 函数
*/
const DoorState STATE_LOCKED = {
.open = open_locked, *// 将 **open 函数指针**赋值为 **open_locked*
.close = close_locked, *// 将 **close 函数指针**赋值为 **close_locked*
.lock = lock_locked,
.unlock = unlock_locked,
.name = "LOCKED"
};
/*
这是 C99 标准的指定初始化器(designated initializer) 语法。
在初始化结构体时,使用 .成员名 = 值 的形式可以显式指定要初始化的成员,而不必按顺序。
其他未指定的成员会被自动初始化为 0(对于指针就是 NULL)。
*/
//等价于:
const DoorState STATE_LOCKED = { open_locked, close_locked, lock_locked,
unlock_locked, "LOCKED" };
/*
.成员名 的好处:
可读性好,清楚知道每个值赋给哪个成员。
不依赖成员声明的顺序,便于维护。
可以只初始化部分成员,其余自动清零。
*/

-
Door 是门对象结构体,内部仅有一个 state 指针,指向当前所处的 DoorState 常量。
-
DoorState 是状态表类型,包含四个函数指针(open、close、lock、unlock)和一个状态名称字符串。
-
op_func_t 是函数指针类型,定义为 void (*)(Door*),所有状态函数都符合此类型。
-
STATE_CLOSED、STATE_OPEN、STATE_LOCKED 是三个全局常量,属于 DoorState 类型,分别初始化为对应状态下的具体函数(如 open_closed 等)。这些函数在图中用 状态函数 类示意。
-
Door d 是 main 函数中的局部变量 d,其 state 最初指向 &STATE_CLOSED,运行时可能随操作改变指向(如切换到 &STATE_OPEN)。
-
所有状态函数(如 open_closed、close_open 等)均定义在源文件中,通过函数指针被状态表引用。
C
static void open_closed(Door* d) {
printf("当前状态: %s, 执行 open -> 切换到 OPEN\n", d->state->name);
d->state = &STATE_OPEN;
}
void door_open(Door* d) { d->state->open(d); }
//-----main-----
Door d;//在栈上创建一个门对象 d,此时里面的 state 指针还未初始化(垃圾值)。
d.state = &STATE_CLOSED;//将 d 的当前状态设置为 "关闭(CLOSED)"
//STATE_CLOSED 是一个全局常量状态表,
//记录了在关闭状态下各个操作(open/close/lock/unlock)应该执行的函数。
door_open(&d); // CLOSED -> OPEN
//调用 door_open 函数,它内部执行 d->state->open(d)。
//因为 d.state 指向 STATE_CLOSED,所以 d->state->open 就是 open_closed 函数
//(见上面定义)。open_closed 函数的作用是:打印"当前状态: CLOSED,
//执行 open -> 切换到 OPEN",然后将门的当前状态改为 &STATE_OPEN(即打开状态)。
//这个过程是:初始关闭状态 -> 执行开门操作 -> 状态变为打开状态。
//-----------------
/*
当调用 door_open(&d) 时,实际上执行的是 d->state->open(d),
即调用当前状态(STATE_CLOSED)的 open 函数指针,也就是 open_closed。
状态切换:d->state = &STATE_OPEN;
*/
door_open(&d); // OPEN 中再打开: 无效
/*
当再次调用 door_open(&d) 时,实际上执行的是 d->state->open(d),
即调用当前状态(STATE_OPEN)的 open 函数指针,也就是 open_open。
状态不切换
*/
其他代码逻辑同上。
1.5.5.3.3 状态模式的应用场景
函数指针在状态模式中的核心作用是将状态与行为解耦,并通过查表(函数指针数组或结构体) 实现运行时动态切换。
传统状态机通常用 switch(状态) 处理不同事件,当状态和事件增多时代码臃肿且难以维护。用函数指针状态表可以:
C++
// 状态表:状态 × 事件 → 处理函数
typedef void (*event_handler_t)(void* ctx);
event_handler_t state_table[STATE_COUNT][EVENT_COUNT] = { ... };
// 调用
state_table[current_state][event](ctx);
优点:新增状态只需添加一行表项和对应处理函数,无需修改分发逻辑。避免冗长的 switch-case / if-else 分支
C++
// 状态表:状态 × 事件 → 处理函数
typedef void (*event_handler_t)(void* ctx);
event_handler_t state_table[STATE_COUNT][EVENT_COUNT] = { ... };
// 调用
state_table[current_state][event](ctx);
如:TCP 协议状态机:CLOSED、LISTEN、SYN_SENT、ESTABLISHED 等状态,每个状态下收到不同报文(SYN、ACK、RST 等)执行不同动作。用函数指针表实现高效、确定性响应。
蜂窝:搜网、注网、建立连接、断开等。
1.5.5.3.4 手机蜂窝网络状态机的一个简化示例
搜网、注网、建立连接、断开。

C++
// cellular_state.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 前向声明
typedef struct CellularContext CellularContext;
// 事件类型
typedef enum {
EV_SEARCH_START, // 启动搜网
EV_NETWORK_FOUND, // 找到网络
EV_REGISTER_COMPLETE, // 注册成功
EV_REGISTER_FAIL, // 注册失败
EV_CONNECT_REQ, // 请求连接
EV_CONNECT_SUCCESS, // 连接建立成功
EV_CONNECT_FAIL, // 连接失败
EV_DISCONNECT, // 主动断开
EV_LOST_NETWORK, // 网络丢失
EV_TIMEOUT // 超时
} Event;
// 状态机上下文,保存当前状态和一些数据
struct CellularContext {
int (*state_handler)(CellularContext* ctx, Event ev);
const char* state_name;
int retry_count; // 搜网重试次数
bool has_network; // 是否找到网络
};
// 状态处理函数声明
static int handle_searching(CellularContext* ctx, Event ev);
static int handle_registered(CellularContext* ctx, Event ev);
static int handle_connected(CellularContext* ctx, Event ev);
static int handle_idle(CellularContext* ctx, Event ev);
// 定义状态表(函数指针数组)------ 每个状态对应一个处理函数
typedef int (*state_func_t)(CellularContext*, Event);
static state_func_t state_table[] = {
[0] = handle_idle,
[1] = handle_searching,
[2] = handle_registered,
[3] = handle_connected
};
// 状态枚举(与表索引对应)
enum State {
ST_IDLE = 0,
ST_SEARCHING,
ST_REGISTERED,
ST_CONNECTED
};
static const char* state_names[] = {
"IDLE", "SEARCHING", "REGISTERED", "CONNECTED"
};
// 切换状态
static void change_state(CellularContext* ctx, enum State new_state) {
ctx->state_handler = state_table[new_state];
ctx->state_name = state_names[new_state];
printf("[状态转换] 进入 %s\n", ctx->state_name);
}
// 事件分发入口
static void dispatch(CellularContext* ctx, Event ev) {
printf("事件: %d, 当前状态: %s\n", ev, ctx->state_name);
int next_state = ctx->state_handler(ctx, ev);
if (next_state >= 0) {
change_state(ctx, (enum State)next_state);
}
}
// ---------- 各状态处理函数 ----------
static int handle_searching(CellularContext* ctx, Event ev) {
switch (ev) {
case EV_NETWORK_FOUND:
printf("搜到网络,尝试注册...\n");
// 模拟注册过程,此处直接触发注册完成(实际可能异步)
dispatch(ctx, EV_REGISTER_COMPLETE);
return -1; // 暂不切换状态,等待事件处理完
case EV_REGISTER_COMPLETE:
printf("注网成功!\n");
return ST_REGISTERED;
case EV_TIMEOUT:
ctx->retry_count++;
if (ctx->retry_count < 3) {
printf("搜网超时,重试第 %d 次\n", ctx->retry_count);
return -1; // 保持在 SEARCHING,重新搜网
} else {
printf("搜网失败,进入 IDLE\n");
return ST_IDLE;
}
default:
printf("SEARCHING 状态下忽略事件 %d\n", ev);
return -1;
}
}
static int handle_registered(CellularContext* ctx, Event ev) {
switch (ev) {
case EV_CONNECT_REQ:
printf("请求建立连接...\n");
// 模拟连接建立(直接成功或失败)
dispatch(ctx, EV_CONNECT_SUCCESS);
return -1;
case EV_CONNECT_SUCCESS:
printf("连接建立成功!\n");
return ST_CONNECTED;
case EV_CONNECT_FAIL:
printf("连接建立失败\n");
return -1; // 仍保持在 REGISTERED
case EV_LOST_NETWORK:
printf("网络丢失,重新搜网\n");
return ST_SEARCHING;
case EV_DISCONNECT:
printf("主动去注册,进入 IDLE\n");
return ST_IDLE;
default:
return -1;
}
}
static int handle_connected(CellularContext* ctx, Event ev) {
switch (ev) {
case EV_DISCONNECT:
printf("断开连接\n");
return ST_REGISTERED;
case EV_LOST_NETWORK:
printf("连接中网络丢失,重新搜网\n");
return ST_SEARCHING;
default:
printf("CONNECTED 状态下忽略事件 %d\n", ev);
return -1;
}
}
static int handle_idle(CellularContext* ctx, Event ev) {
switch (ev) {
case EV_SEARCH_START:
printf("启动搜网\n");
ctx->retry_count = 0;
return ST_SEARCHING;
default:
printf("IDLE 状态下忽略事件 %d\n", ev);
return -1;
}
}
// 主函数演示
int main() {
CellularContext ctx = {
.state_handler = state_table[ST_IDLE],
.state_name = "IDLE",
.retry_count = 0,
.has_network = false
};
// 模拟一个完整的流程
printf("\n=== 启动手机 ===\n");
dispatch(&ctx, EV_SEARCH_START); // 开始搜网
dispatch(&ctx, EV_NETWORK_FOUND); // 找到网络 -> 触发注册完成
dispatch(&ctx, EV_CONNECT_REQ); // 请求连接 -> 连接成功
dispatch(&ctx, EV_DISCONNECT); // 断开连接 -> 回到 REGISTERED
dispatch(&ctx, EV_LOST_NETWORK); // 网络丢失 -> 重新搜网
dispatch(&ctx, EV_TIMEOUT); // 搜网超时重试
dispatch(&ctx, EV_TIMEOUT); // 再次超时,进入IDLE
return 0;
}

分析:
状态与事件枚举定义:
C
状态枚举 (State):
ST_IDLE = 0
ST_SEARCHING = 1
ST_REGISTERED = 2
ST_CONNECTED = 3
事件枚举 (Event):
EV_SEARCH_START = 0
EV_NETWORK_FOUND = 1
EV_REGISTER_COMPLETE = 2
EV_REGISTER_FAIL = 3
EV_CONNECT_REQ = 4
EV_CONNECT_SUCCESS = 5
EV_CONNECT_FAIL = 6
EV_DISCONNECT = 7
EV_LOST_NETWORK = 8
EV_TIMEOUT = 9
每个(状态,事件)对应的处理函数行为(及返回值/副作用):
状态 ST_IDLE
| 事件 | 处理行为 | 返回新状态 |
|---|---|---|
| EV_SEARCH_START | 打印"启动搜网",重置重试计数 | ST_SEARCHING |
| 其他所有事件 | 打印"忽略事件" | 无变化(返回 -1 或原状态) |
状态 ST_SEARCHING
| 事件 | 处理行为 | 返回新状态 |
|---|---|---|
| EV_NETWORK_FOUND | 打印"搜到网络,尝试注册...";递归调用dispatch 发送 EV_REGISTER_COMPLETE | 暂不返回(实际由内部事件驱动) |
| EV_REGISTER_COMPLETE | 打印"注网成功!" | ST_REGISTERED |
| EV_TIMEOUT | 重试计数自增;若 <3 则打印重试次并停留在本状态;否则打印"搜网失败" | 重试中返回 -1;超限返回 ST_IDLE |
| 其他事件 | 打印"忽略事件" | -1 |
状态 ST_REGISTERED
| 事件 | 处理行为 | 返回新状态 |
|---|---|---|
| EV_CONNECT_REQ | 打印"请求建立连接...";递归调用 EV_CONNECT_SUCCESS | 暂不返回 |
| EV_CONNECT_SUCCESS | 打印"连接建立成功!" | ST_CONNECTED |
| EV_CONNECT_FAIL | 打印"连接建立失败" | -1 |
| EV_LOST_NETWORK | 打印"网络丢失,重新搜网" | ST_SEARCHING |
| EV_DISCONNECT | 打印"主动去注册,进入 IDLE" | ST_IDLE |
| 其他事件 | 忽略 | -1 |
状态 ST_CONNECTED
| 事件 | 处理行为 | 返回新状态 |
|---|---|---|
| EV_DISCONNECT | 打印"断开连接" | ST_REGISTERED |
| EV_LOST_NETWORK | 打印"连接中网络丢失,重新搜网" | ST_SEARCHING |
| 其他事件 | 忽略 | -1 |
每个 handle_xxx 函数内部执行对应的行为,并可能调用 change_state 来切换上下文中的状态(或返回新状态值由分发器统一处理)。代码中,状态机采用的是每个状态一个处理函数的一维查表方式(state_tablestate 指向该状态的处理函数,函数内部再通过 switch(event) 分派具体行为)。一维表更紧凑,适合事件数量不多的情况。每一个 (状态,事件) 组合都可以对应到原 handle_xxx 函数中的一个 case 分支。
数据结构关系图:

-
CellularContext 是状态机的上下文,保存当前状态处理函数指针(state_handler)、状态名称字符串、重试计数器等。
-
state_func_t 是函数指针类型,定义为 int (*)(CellularContext*, Event),所有状态处理函数都符合此类型。
-
state_table 是一个静态数组,使用指定初始化器将 State 枚举值映射到对应的状态处理函数。例如 ST_SEARCHING = handle_searching。
-
dispatch 函数接收事件,调用当前上下文的 state_handler,并根据返回值(新状态索引)调用 change_state。
-
change_state 根据新状态索引从 state_table 中取出新的处理函数,更新到 CellularContext 的 state_handler 和 state_name。
-
各个 handle_xxx 函数实现了每个状态下的具体行为,内部通过 dispatch 可以触发"内部事件"(例如注册成功后立即触发连接请求),并返回下一个状态索引或 -1(表示不转换)。
GDB debug:

Java
(gdb) p ctx
$2 = {state_handler = 0xaaaaaaaa0c74 <handle_idle>, state_name = 0xaaaaaaaa0dc0 "IDLE",
retry_count = 0, has_network = false}
(gdb) p state_table
$3 = {0xaaaaaaaa0c74 <handle_idle>, 0xaaaaaaaa0a28 <handle_searching>,
0xaaaaaaaa0b14 <handle_registered>, 0xaaaaaaaa0c04 <handle_connected>}
(gdb) p handle_idle
$5 = {int (CellularContext *, Event)} 0xaaaaaaaa0c74 <handle_idle>
(gdb) p handle_searching
$6 = {int (CellularContext *, Event)} 0xaaaaaaaa0a28 <handle_searching>
(gdb) p handle_registered
$7 = {int (CellularContext *, Event)} 0xaaaaaaaa0b14 <handle_registered>
(gdb) p handle_connected
$8 = {int (CellularContext *, Event)} 0xaaaaaaaa0c04 <handle_connected>

C
//main
printf("\n=== 启动手机 ===\n");
(gdb) p ctx
$2 = {state_handler = 0xaaaaaaaa0c74 <handle_idle>, state_name = 0xaaaaaaaa0dc0 "IDLE",
retry_count = 0, has_network = false}
dispatch(&ctx, EV_SEARCH_START); // 开始搜网



C++
// dispatch 事件分发入口
static void dispatch(CellularContext* ctx, Event ev) {
printf("事件: %d, 当前状态: %s\n", ev, ctx->state_name);
/*
(gdb) p *ctx
$5 = {state_handler = 0xaaaaaaaa0c74 <handle_idle>, state_name = 0xaaaaaaaa0dc0 "IDLE",
retry_count = 0, has_network = false}
*/
int next_state = ctx->state_handler(ctx, ev);
/*
(gdb) p next_state
$13 = 1
*/
if (next_state >= 0) {
change_state(ctx, (enum State)next_state);
}
}
C++
//handle_idle
static int handle_idle(CellularContext* ctx, Event ev) {
/*
(gdb) s
handle_idle (ctx=0xffffffffecb0, ev=EV_SEARCH_START) at reg_state.c:139
*/
switch (ev) {
case EV_SEARCH_START:
printf("启动搜网\n");
ctx->retry_count = 0;
return ST_SEARCHING;
default:
printf("IDLE 状态下忽略事件 %d\n", ev);
return -1;
}
}
C++
// change_state 切换状态
static void change_state(CellularContext* ctx, enum State new_state) {
/*
(gdb) p *ctx
$14 = {state_handler = 0xaaaaaaaa0c74 <handle_idle>, state_name = 0xaaaaaaaa0dc0 "IDLE",
retry_count = 0, has_network = false}
*/
ctx->state_handler = state_table[new_state];
/*
(gdb) p *ctx
$15 = {state_handler = 0xaaaaaaaa0a28 <handle_searching>,
state_name = 0xaaaaaaaa0dc0 "IDLE", retry_count = 0, has_network = false}
*/
ctx->state_name = state_names[new_state];
/*
(gdb) p *ctx
$16 = {state_handler = 0xaaaaaaaa0a28 <handle_searching>,
state_name = 0xaaaaaaaa0dc8 "SEARCHING", retry_count = 0, has_network = false}
*/
printf("[状态转换] 进入 %s\n", ctx->state_name);
}
C
状态表 + 函数指针驱动状态机的核心架构:
上下文保存当前状态函数,
事件分发时通过函数指针间接调用,
状态切换则重新绑定函数指针。