第29章 高级指针应用:指针与二维数组、函数指针数组、复杂指针声明解析、回调函数实战

指针这东西,说简单也简单,说复杂能复杂到让你怀疑人生。我做了十几年C语言开发,见过太多人在指针上栽跟头。尤其是高级指针应用,很多人一看到二维数组指针、函数指针数组就头大。今天咱们就把这些硬骨头啃下来。

29.1 指针与二维数组:别被表象迷惑

二维数组在内存里其实是一维存储的。这个事实,我当年学的时候花了很长时间才真正接受。你想想看,int a[3][4] 在内存中就是12个int连续排列,没有所谓的“行”和“列”。

那指针怎么玩?看这个:

int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4] = a;  // 指向包含4个int的数组的指针
int *q = &a[0][0]; // 指向第一个元素的指针

这里 p 和 q 有什么区别?

  • p+1 跳过了4个int(一行)
  • q+1 只跳过了1个int

说白了,p 是“行指针”,q 是“元素指针”。我在项目中处理图像数据时,经常用行指针来遍历像素行,效率比元素指针高不少。

核心理解: a[i][j] 等价于 *(*(a+i)+j)。先取第i行的地址,再偏移j个元素。

29.2 函数指针数组:把函数当数据用

函数指针数组,说白了就是一个数组,里面存的是函数地址。这个技巧在实现状态机、命令分发时特别好用。

我曾经写过一个网络协议解析器,不同消息类型对应不同处理函数。用函数指针数组,代码干净得像教科书:

#include <stdio.h>

void handle_login(void) { printf("处理登录\n"); }
void handle_logout(void) { printf("处理登出\n"); }
void handle_data(void) { printf("处理数据\n"); }

int main() {
    // 函数指针数组
    void (*handlers[3])(void) = {
        handle_login,
        handle_logout,
        handle_data
    };
    
    int msg_type = 1; // 假设收到登出消息
    if (msg_type >= 0 && msg_type < 3) {
        handlers[msg_type](); // 直接调用
    }
    return 0;
}

嗯,这里要注意:函数指针数组的类型声明有点绕。void (*handlers[3])(void) 怎么读?

  • handlers[3] 是一个数组
  • (*handlers[3]) 数组元素是指针
  • (*handlers[3])(void) 指针指向一个函数,参数是void
  • void (*handlers[3])(void) 函数返回void

我建议你从右往左读,或者用typedef简化:

typedef void (*HandlerFunc)(void);
HandlerFunc handlers[3] = {handle_login, handle_logout, handle_data};

这样是不是清爽多了?

29.3 复杂指针声明解析:右左法则

复杂指针声明是C语言的“劝退”利器。什么“返回函数指针的函数的指针”之类的,看着就头疼。但我有一套方法——右左法则。

右左法则: 从标识符开始,先向右看,再向左看,遇到括号改变方向。

举个例子:

void (*signal(int, void (*)(int)))(int);

别慌,一步步来:

  1. 找到标识符 signal
  2. 向右看,遇到 (int, void (*)(int)),说明 signal 是一个函数,参数是int和一个函数指针
  3. 向左看,遇到 *,说明 signal 返回一个指针
  4. 再向右看,遇到 (int),说明这个指针指向一个函数,参数是int
  5. 再向左看,遇到 void,说明这个函数返回void

所以 signal 是:一个函数,参数是int和函数指针,返回一个指向函数的指针,该函数参数是int,返回void。

我的小技巧: 遇到复杂声明,先用typedef拆解。比如上面的signal,可以拆成:

typedef void (*Handler)(int);
Handler (*signal(int, Handler))(int);

虽然还是有点绕,但至少能看懂了。

29.4 回调函数实战:让代码活起来

回调函数,说白了就是把一个函数的地址传给另一个函数,让它在合适的时候调用你。这是实现“控制反转”的经典手法。

我在写一个排序工具时,需要支持多种排序规则。用回调函数,调用者想怎么排就怎么排:

#include <stdio.h>

// 比较函数类型
typedef int (*CompareFunc)(int, int);

int ascending(int a, int b) { return a - b; }
int descending(int a, int b) { return b - a; }

// 冒泡排序,接受比较函数作为回调
void bubble_sort(int arr[], int n, CompareFunc cmp) {
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-i-1; j++) {
            if (cmp(arr[j], arr[j+1]) > 0) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
    int n = sizeof(arr)/sizeof(arr[0]);
    
    bubble_sort(arr, n, ascending);
    // 现在arr是升序
    
    bubble_sort(arr, n, descending);
    // 现在arr是降序
    
    return 0;
}

避坑指南: 我曾经在回调函数中不小心修改了全局状态,导致多线程环境下数据错乱。回调函数一定要设计成“无副作用”的,或者明确文档说明其行为。

29.5 知识体系总览

下面这张图,是我整理的高级指针应用知识结构。你可以把它当作学习路线图:

高级指针应用 指针与二维数组 函数指针数组 复杂指针声明解析 回调函数实战 行指针 vs 元素指针 内存布局理解 状态机实现 命令分发 右左法则 typedef拆解 控制反转 无副作用设计

29.6 实战经验总结

说了这么多,最后分享几个我个人的经验:

  • 能用typedef就别手写复杂声明。 代码是给人看的,不是用来炫技的。
  • 函数指针数组的长度最好用枚举常量控制。 我见过有人硬编码数字,后来加功能时忘了改数组大小,直接越界。
  • 回调函数的参数尽量简单。 我曾经传了一个复杂的结构体指针,结果回调里各种解引用,代码又臭又长。
  • 二维数组传参时,第二维长度必须明确。 这是C语言的硬性要求,别想着偷懒。

一句话总结: 高级指针应用的核心,就是理解“指针的类型决定了它如何解释内存”。把这个想通了,什么二维数组指针、函数指针数组,都是纸老虎。


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