19、指针与复杂声明:右左法则解析复杂声明、函数指针与数组指针的声明、typedef简化

说实话,C语言里最让人头疼的,就是那些看着像天书一样的复杂声明。我记得刚入行那会儿,看到 void (*signal(int, void (*)(int)))(int) 这种声明,直接就想把键盘摔了。后来带我的老工程师跟我说了一句话:「别怕,用右左法则,一层层剥开它。」

嗯,今天我就把这个方法教给你。顺便聊聊函数指针、数组指针,还有怎么用 typedef 让代码变得清爽。

19.1 右左法则:拆解复杂声明的利器

什么叫右左法则?说白了就是:先往右看,再往左看,遇到括号就调转方向。具体步骤是这样的:

  1. 找到最左边的标识符(变量名或函数名)
  2. 先向右看,直到遇到右括号 ) 或分号 ;
  3. 再向左看,直到遇到左括号 (
  4. 遇到括号就跳出当前层,继续重复「右-左」的规则

我习惯把这个过程叫做「剥洋葱」。每剥一层,你就知道这个标识符是什么类型。

核心口诀:标识符先右后左,括号改变方向,逐层解析。

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)

这个经典声明,咱们用右左法则一层层剥:

  1. 找到最左边的标识符 signal
  2. 向右看:遇到 (int, void (*)(int)) —— signal 是一个函数,接受两个参数
  3. 第一个参数是 int,第二个参数是 void (*)(int)(一个函数指针)
  4. 向左看:遇到 * —— signal 返回一个指针?不对,被括号挡住了
  5. 跳出括号,向右看:遇到 (int) —— 哦,返回的是一个函数,接受 int 参数
  6. 向左看:遇到 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_ptrint (*)[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 知识体系结构图

下面这张图总结了本章的核心知识点和它们之间的关系:

指针与复杂声明知识体系 复杂声明 右左法则 先右后左,括号调转 逐层剥洋葱 函数指针 声明:int (*fp)(int, int) 用途:回调、状态机、驱动抽象 数组指针 声明:int (*p)[5] 区别:指针数组 int *p[5] typedef 简化

19.5 总结与避坑

好了,咱们把这一章的内容串一下:

  • 右左法则是解析复杂声明的万能钥匙。遇到看不懂的声明,别慌,从标识符开始,先右后左,一层层剥。
  • 函数指针数组指针是两种完全不同的东西。函数指针指向函数,数组指针指向整个数组。注意括号的位置决定了类型。
  • typedef 能让复杂声明变得可读。我建议在项目里多用 typedef,尤其是函数指针类型,能减少很多 bug。

我曾经踩过的坑:

  • int *p[5](指针数组)当成 int (*p)[5](数组指针)用,导致内存访问越界。
  • 在函数指针赋值时忘了取地址符 &,虽然大多数编译器能通过,但语义不清晰。
  • 用 typedef 定义函数指针时,把类型名放错了位置。记住:typedef 返回类型 (*新类型名)(参数列表)

说实话,复杂声明在面试题里出现得多,实际项目中很少手写这么复杂的。但能看懂它们,说明你对 C 语言的类型系统有了深刻理解。下次遇到 void (*signal(int, void (*)(int)))(int),你应该能笑着说:「哦,这不就是个信号处理函数的声明嘛。」


公众号:蓝海资料掘金营,微信deep3321