指针(二):指针数组与数组指针、多级指针、函数指针、回调函数、void指针

大家好,欢迎来到指针的第二讲。上一章我们聊了指针的基础,说白了就是「指针存地址」这个核心概念。但实际项目中,指针从来不会单独出现——它总是和数组、函数、结构体混在一起用。今天这一讲,我们就把指针的「进阶玩法」一次性讲透。

我个人习惯把指针分为三个层次:第一层是基础指针(指向变量),第二层是指针与数组的纠缠(指针数组、数组指针),第三层是指针指向函数(函数指针、回调)。今天我们就从第二层开始。

核心一句话:指针本身也是一种数据类型,它可以指向数组、指向指针、甚至指向函数。理解这一点,后面的所有内容都是顺理成章的。

指针进阶知识体系 指针(二) 指针数组 char *arr[5] 数组指针 int (*p)[5] 多级指针 int **pp 函数指针 int (*f)(int) 回调函数 qsort 应用 void 指针 通用指针

一、指针数组 vs 数组指针——别被名字绕晕

这两个概念,我敢说80%的C语言初学者都搞混过。我自己当年也栽过跟头。其实区分方法很简单:看最后两个字

  • 指针数组:最后是「数组」,所以它是一个数组,里面存的是指针。
  • 数组指针:最后是「指针」,所以它是一个指针,指向一个数组。

你想想看,中文的修饰语是从前往后读的,「指针数组」就是「指针的数组」,「数组指针」就是「数组的指针」。嗯,就这么简单。

1.1 指针数组

最常见的场景是什么?处理多个字符串。比如你要存一组名字,用二维数组当然可以,但每个名字长度不一样,浪费空间。指针数组就派上用场了:

#include <stdio.h>

int main() {
    // 指针数组:每个元素都是 char* 类型
    char *names[] = {"Alice", "Bob", "Charlie", "David"};
    
    for (int i = 0; i < 4; i++) {
        printf("names[%d] = %s\n", i, names[i]);
    }
    return 0;
}

这里 names 是一个数组,每个元素是一个 char* 指针,指向字符串常量。我在项目中处理配置文件时,经常用这种结构来存储键值对列表,非常灵活。

1.2 数组指针

数组指针指向的是整个数组,而不是数组的第一个元素。看声明:

int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;  // 数组指针,指向长度为5的int数组

注意括号!int (*p)[5]int *p[5] 完全是两码事。没有括号的话,int *p[5] 就是指针数组了。

我的记忆口诀:「括号优先,指针优先;无括号,数组优先。」——看到 *p 被括号包着,它就是指针;否则先结合成数组。

数组指针在二维数组传参时特别有用。我曾经写过一个图像处理函数,需要传入一个固定宽度的像素矩阵,用数组指针做参数,代码清晰很多:

void process_image(int (*pixels)[1024], int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 1024; j++) {
            pixels[i][j] = pixels[i][j] > 128 ? 255 : 0;
        }
    }
}

二、多级指针——指针的指针

多级指针,说白了就是「存地址的地址」。最常见的二级指针,在需要修改指针本身的值时使用。

举个例子,你写了一个函数要分配内存,并且要把分配到的地址传回给调用者。这时候一级指针不够用,得用二级指针:

void alloc_memory(int **pp, int size) {
    *pp = (int *)malloc(size * sizeof(int));
    if (*pp == NULL) {
        printf("内存分配失败!\n");
    }
}

int main() {
    int *p = NULL;
    alloc_memory(&p, 10);  // 传指针的地址
    // 现在 p 指向了堆上分配的内存
    free(p);
    return 0;
}

我曾经在调试一个链表库时,发现插入函数总是无效。查了半天,发现参数传的是 Node * 而不是 Node **——函数内部修改了指针,但外部根本不知道。嗯,这就是典型的「值传递陷阱」。

注意:三级指针、四级指针虽然语法上合法,但实际项目中极少使用。如果你发现需要三级指针,多半是设计有问题。我见过有人写 char ****p,那代码基本没法维护。

三、函数指针——把函数当变量用

函数指针,就是把函数的入口地址存起来。C语言里,函数名本身就是地址,所以取函数指针不需要 & 符号。

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main() {
    // 声明一个函数指针
    int (*op)(int, int);
    
    op = add;   // 指向 add 函数
    printf("add: %d\n", op(10, 5));
    
    op = sub;   // 指向 sub 函数
    printf("sub: %d\n", op(10, 5));
    
    return 0;
}

函数指针的声明语法有点绕:返回类型 (*指针名)(参数列表)。记住括号不能少,少了就变成返回指针的函数了。

我在做嵌入式驱动开发时,经常用函数指针数组来实现状态机。每个状态对应一个处理函数,状态切换时直接查表调用,比 switch-case 优雅得多。

四、回调函数——把函数当参数传

回调函数,说白了就是「你写一个函数,我帮你调用」。标准库里的 qsort 就是最经典的例子:

#include <stdio.h>
#include <stdlib.h>

// 比较函数,由调用者提供
int compare_int(const void *a, const void *b) {
    return *(int *)a - *(int *)b;
}

int main() {
    int arr[] = {5, 2, 9, 1, 5, 6};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    qsort(arr, n, sizeof(int), compare_int);
    
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

你想想看,qsort 不知道你要排序整数还是浮点数,也不知道你是升序还是降序。它把「比较」这个动作抽象出来,让你自己实现。这就是回调的精髓——把控制权交还给调用者

实战经验:我在写一个事件驱动框架时,把所有事件处理函数注册到一个回调表中。事件发生时,框架遍历回调表依次调用。这样新增功能时,只需要注册一个新回调,不用改框架代码。这就是「开闭原则」在C语言中的体现。

五、void 指针——万能指针

void * 可以指向任何类型的数据。但它有一个限制:不能直接解引用。因为编译器不知道它指向的是什么类型,也就不知道要取几个字节。

int a = 100;
void *p = &a;

// printf("%d\n", *p);  // 错误!不能解引用 void 指针
printf("%d\n", *(int *)p);  // 正确,先转成 int*

void * 最常见的用途就是写通用函数。比如上面 qsort 的比较函数参数就是 const void *,这样任何类型的数据都能传进来。

我自己写内存池时,也大量使用 void *。分配函数返回 void *,使用者自己决定怎么解释这块内存。这种设计让接口非常通用。

小技巧:void * 做参数或返回值时,一定要在文档里写清楚「实际类型是什么」。否则别人看你的代码,就像看天书一样。我曾经接手过一个项目,函数返回 void *,但文档里根本没写是什么类型……那感觉,就像拆盲盒。

六、避坑指南——我踩过的那些坑

讲到这里,我把这些年遇到的指针相关的问题总结一下,希望能帮你少走弯路。

  • 数组名不是指针:数组名是常量,不能做 arr++ 操作。但数组名可以隐式转换成指针。这个区别很微妙,但很重要。
  • 函数指针的类型匹配:声明函数指针时,返回类型和参数列表必须完全一致。我曾经因为参数类型不匹配(比如 intlong),导致回调函数拿到错误的值,查了一下午。
  • void * 的算术运算:标准C不允许对 void * 做加减法,因为不知道步长。GCC 扩展允许,但为了可移植性,建议先转成 char * 再运算。
  • 多级指针的初始化:二级指针在使用前,一定要确保它指向的地址是有效的。否则 *pp = xxx 会写到一个非法地址,直接段错误。

好了,指针的第二讲就到这里。这些概念看起来多,但核心就一句话:指针也是一种变量,它存的是地址。至于这个地址指向什么——数组、函数、还是另一个指针——都由你说了算。


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