19、指针与复杂声明:右左法则解析复杂声明、函数指针与数组指针的声明、typedef简化
说实话,C语言里最让人头疼的,就是那些看着像天书一样的复杂声明。我记得刚入行那会儿,看到 void (*signal(int, void (*)(int)))(int) 这种声明,直接就想把键盘摔了。后来带我的老工程师跟我说了一句话:「别怕,用右左法则,一层层剥开它。」
嗯,今天我就把这个方法教给你。顺便聊聊函数指针、数组指针,还有怎么用 typedef 让代码变得清爽。
19.1 右左法则:拆解复杂声明的利器
什么叫右左法则?说白了就是:先往右看,再往左看,遇到括号就调转方向。具体步骤是这样的:
- 找到最左边的标识符(变量名或函数名)
- 先向右看,直到遇到右括号
)或分号; - 再向左看,直到遇到左括号
( - 遇到括号就跳出当前层,继续重复「右-左」的规则
我习惯把这个过程叫做「剥洋葱」。每剥一层,你就知道这个标识符是什么类型。
核心口诀:标识符先右后左,括号改变方向,逐层解析。
19.1.1 实战拆解:int *p[5]
咱们先来个简单的。看这个声明:
int *p[5];
按右左法则走一遍:
- 找到标识符
p - 向右看:遇到
[5]—— 哦,p 是一个数组,有5个元素 - 向左看:遇到
*—— 数组的元素是指针 - 再向左看:遇到
int—— 指针指向 int
所以 p 是:一个包含5个 int 指针的数组。我在项目中经常用这种结构来管理多个动态分配的缓冲区,每个指针指向一块独立的内存。
19.1.2 实战拆解:int (*p)[5]
注意,这里多了个括号:
int (*p)[5];
拆解过程:
- 找到标识符
p - 向右看:遇到
)—— 被括号挡住了,调转方向 - 向左看:遇到
*—— p 是一个指针 - 跳出括号,向右看:遇到
[5]—— 指针指向一个数组,有5个元素 - 向左看:遇到
int—— 数组的元素是 int
所以 p 是:一个指向包含5个 int 元素的数组的指针。这就是数组指针。
避坑指南:我曾经在写一个图像处理算法时,把 int *p[5] 和 int (*p)[5] 搞混了。结果数组指针当成指针数组用,内存越界,查了整整一个下午的 bug。记住:括号决定了是指针还是数组。
19.1.3 实战拆解:void (*signal(int, void (*)(int)))(int)
这个经典声明,咱们用右左法则一层层剥:
- 找到最左边的标识符
signal - 向右看:遇到
(int, void (*)(int))—— signal 是一个函数,接受两个参数 - 第一个参数是
int,第二个参数是void (*)(int)(一个函数指针) - 向左看:遇到
*—— signal 返回一个指针?不对,被括号挡住了 - 跳出括号,向右看:遇到
(int)—— 哦,返回的是一个函数,接受 int 参数 - 向左看:遇到
void—— 这个函数返回 void
所以 signal 是:一个函数,接受 int 和函数指针,返回一个函数指针(指向一个接受 int 返回 void 的函数)。
说实话,这种声明在实际项目中很少手写。但如果你在阅读系统库代码或者信号处理相关代码时,就会遇到。我建议你至少能看懂它。
19.2 函数指针与数组指针的声明
这两个概念经常被混淆。咱们分开讲清楚。
19.2.1 函数指针
函数指针,说白了就是指向函数的指针。声明方式:
// 声明一个函数指针,指向返回 int、接受两个 int 参数的函数
int (*func_ptr)(int, int);
// 赋值
int add(int a, int b) { return a + b; }
func_ptr = add;
// 调用
int result = func_ptr(3, 4); // 或者 (*func_ptr)(3, 4)
我在项目中常用函数指针来实现回调机制。比如在嵌入式系统中,按键中断触发时,通过函数指针调用不同的处理函数,代码非常灵活。
函数指针的典型用途:
- 回调函数(如 qsort 的比较函数)
- 状态机中的状态处理函数
- 驱动层的接口抽象
19.2.2 数组指针
数组指针指向的是整个数组,而不是数组的第一个元素。声明方式:
// 声明一个数组指针,指向包含5个 int 的数组
int (*arr_ptr)[5];
// 赋值
int arr[5] = {1, 2, 3, 4, 5};
arr_ptr = &arr; // 注意取地址符
// 访问元素
(*arr_ptr)[0] = 10; // 等价于 arr[0] = 10
你想想看,为什么不能用 arr_ptr = arr?因为 arr 退化为指向第一个元素的指针(类型是 int*),而 arr_ptr 是 int (*)[5],类型不匹配。
注意:数组指针和指针数组是两个完全不同的东西。数组指针是指向数组的指针,指针数组是存放指针的数组。我曾经在面试中问过这个问题,能答对的人不到一半。
19.3 typedef 简化复杂声明
复杂声明写起来费劲,读起来更费劲。用 typedef 可以给它们起个简单的别名。我个人习惯在项目里大量使用 typedef,尤其是涉及函数指针的时候。
19.3.1 简化函数指针
// 原始声明
void (*signal(int, void (*)(int)))(int);
// 用 typedef 简化
typedef void (*sighandler_t)(int);
sighandler_t signal(int, sighandler_t);
是不是清爽多了?sighandler_t 就是一个函数指针类型,指向接受 int 返回 void 的函数。
19.3.2 简化数组指针
// 原始声明
int (*arr_ptr)[5];
// 用 typedef 简化
typedef int (*arr5_ptr_t)[5];
arr5_ptr_t p = &arr;
19.3.3 简化复杂结构体指针
在嵌入式驱动开发中,我经常这样用:
typedef struct {
uint32_t base_addr;
uint32_t irq_num;
void (*init)(void);
void (*deinit)(void);
} uart_device_t;
typedef uart_device_t* uart_handle_t;
这样在函数参数中直接写 uart_handle_t,比写 struct uart_device* 简洁多了。
我的习惯:用 typedef 给函数指针类型加 _t 后缀,给结构体类型加 _t 后缀,一眼就能看出是类型别名。但注意,有些编码规范禁止用 _t 后缀(因为 POSIX 标准保留了),你可以用 _type 或 _handler 代替。
19.4 知识体系结构图
下面这张图总结了本章的核心知识点和它们之间的关系:
19.5 总结与避坑
好了,咱们把这一章的内容串一下:
- 右左法则是解析复杂声明的万能钥匙。遇到看不懂的声明,别慌,从标识符开始,先右后左,一层层剥。
- 函数指针和数组指针是两种完全不同的东西。函数指针指向函数,数组指针指向整个数组。注意括号的位置决定了类型。
- typedef 能让复杂声明变得可读。我建议在项目里多用 typedef,尤其是函数指针类型,能减少很多 bug。
我曾经踩过的坑:
- 把
int *p[5](指针数组)当成int (*p)[5](数组指针)用,导致内存访问越界。 - 在函数指针赋值时忘了取地址符
&,虽然大多数编译器能通过,但语义不清晰。 - 用 typedef 定义函数指针时,把类型名放错了位置。记住:
typedef 返回类型 (*新类型名)(参数列表)。
说实话,复杂声明在面试题里出现得多,实际项目中很少手写这么复杂的。但能看懂它们,说明你对 C 语言的类型系统有了深刻理解。下次遇到 void (*signal(int, void (*)(int)))(int),你应该能笑着说:「哦,这不就是个信号处理函数的声明嘛。」
公众号:蓝海资料掘金营,微信deep3321