第六章:指针与函数——当指针遇上函数,才是C语言的真正开始

说实话,很多C语言学习者学到指针和数组时,觉得已经够难了。但我要告诉你,指针与函数结合,才是C语言真正的灵魂所在。我当年在做一个嵌入式通信协议栈时,就是因为搞懂了函数指针,才把代码量从两千行压缩到了八百行。今天,我们就来聊聊这个话题。

6.1 函数指针的声明与使用

函数指针,说白了就是指向函数的指针。就像数组名是数组首地址一样,函数名也是函数的入口地址。

核心概念:函数指针存储的是函数的起始地址,通过它可以间接调用函数。

6.1.1 声明一个函数指针

声明函数指针的语法,说实话有点绕。我个人的记忆方法是:先写一个普通函数声明,然后把函数名换成 (*指针名)

// 普通函数声明
int add(int a, int b);

// 函数指针声明
int (*pFunc)(int, int);

// 解释:pFunc 是一个指针,指向一个返回 int、接受两个 int 参数的函数

嗯,这里要注意:括号不能少。如果写成 int *pFunc(int, int),那就变成了一个返回 int* 的函数声明了——完全两码事。

6.1.2 赋值与调用

#include <stdio.h>

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

int main() {
    int (*pFunc)(int, int);  // 声明函数指针
    pFunc = add;             // 赋值:函数名就是地址
    // pFunc = &add;        // 这样写也行,但没必要

    int result = pFunc(3, 5);   // 通过指针调用
    // int result = (*pFunc)(3, 5); // 这样写也行

    printf("结果:%d\n", result);
    return 0;
}

我在项目中遇到过一个问题:有人把函数指针赋值写成 pFunc = add(),结果编译报错。记住,add 是地址,add() 是调用返回值——千万别搞混。

6.2 回调函数机制

回调函数,是函数指针最经典的应用场景。你想想看,如果你写了一个排序函数,但排序的比较规则不确定——这时候就可以用回调函数,把比较逻辑交给调用者决定。

我的经验:回调函数的核心思想是「你调用我,我调用你传进来的函数」。我在做传感器驱动时,就用回调函数来处理不同传感器的数据解析逻辑,上层代码完全不用改。

6.2.1 标准库中的回调:qsort

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

// 比较函数:升序
int compareAsc(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}

// 比较函数:降序
int compareDesc(const void *a, const void *b) {
    return (*(int*)b - *(int*)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 传入回调函数
    qsort(arr, n, sizeof(int), compareAsc);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

你看,qsort 不知道你要怎么排序,它只管排序算法本身。比较逻辑通过回调函数传进去——这就是解耦。

6.2.2 自己实现回调

#include <stdio.h>

// 定义回调函数类型
typedef void (*EventCallback)(int eventCode, void *userData);

// 注册回调的结构体
typedef struct {
    EventCallback callback;
    void *userData;
} EventHandler;

EventHandler handler = {NULL, NULL};

void registerCallback(EventCallback cb, void *data) {
    handler.callback = cb;
    handler.userData = data;
}

void triggerEvent(int code) {
    if (handler.callback != NULL) {
        handler.callback(code, handler.userData);
    }
}

// 用户定义的回调
void myEventHandler(int code, void *data) {
    printf("事件 %d 发生,用户数据:%s\n", code, (char*)data);
}

int main() {
    registerCallback(myEventHandler, "传感器模块");
    triggerEvent(0x01);
    return 0;
}

避坑指南:我曾经在回调函数里直接修改了全局变量,导致多线程环境下数据竞争。回调函数里尽量不要依赖全局状态,用 userData 传上下文更安全。

6.3 函数指针数组(转移表)

函数指针数组,也叫转移表。说白了就是一个数组,里面每个元素都是函数指针。这在实现状态机、命令解析时特别好用。

6.3.1 基本用法

#include <stdio.h>

void func0() { printf("执行操作 0\n"); }
void func1() { printf("执行操作 1\n"); }
void func2() { printf("执行操作 2\n"); }

int main() {
    // 声明函数指针数组
    void (*funcTable[3])() = {func0, func1, func2};

    int cmd;
    printf("输入命令 (0-2):");
    scanf("%d", &cmd);

    if (cmd >= 0 && cmd <= 2) {
        funcTable[cmd]();  // 通过索引调用
    } else {
        printf("无效命令\n");
    }
    return 0;
}

你想想看,如果没有函数指针数组,你得写一个 switch-case,每个 case 调用一个函数。如果命令有几十个,那个 switch 会写得你怀疑人生。用转移表,代码量直接减半。

6.3.2 实际项目中的应用

我在做一个串口命令解析器时,就用到了转移表。每个命令对应一个处理函数,命令码就是数组下标。

#include <stdio.h>

// 命令处理函数
void cmdLedOn()    { printf("LED 打开\n"); }
void cmdLedOff()   { printf("LED 关闭\n"); }
void cmdReadTemp() { printf("读取温度\n"); }
void cmdReadHum()  { printf("读取湿度\n"); }
void cmdError()    { printf("未知命令\n"); }

// 转移表
void (*cmdTable[256])() = {NULL};

void initCmdTable() {
    cmdTable[0x01] = cmdLedOn;
    cmdTable[0x02] = cmdLedOff;
    cmdTable[0x10] = cmdReadTemp;
    cmdTable[0x11] = cmdReadHum;
    // 其他命令默认指向 cmdError
    for (int i = 0; i < 256; i++) {
        if (cmdTable[i] == NULL) {
            cmdTable[i] = cmdError;
        }
    }
}

void processCommand(unsigned char cmd) {
    cmdTable[cmd]();
}

int main() {
    initCmdTable();
    processCommand(0x01);  // LED 打开
    processCommand(0xFF);  // 未知命令
    return 0;
}

我的习惯:转移表配合枚举类型使用,可读性会更好。比如 enum {CMD_LED_ON = 0x01, CMD_LED_OFF = 0x02, ...},这样代码一看就懂。

6.4 返回指针的函数

返回指针的函数,就是返回值是一个指针。这本身不难,但容易踩坑——最常见的问题就是返回了局部变量的地址。

6.4.1 正确用法:返回动态分配的内存

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

char* createString(const char *str) {
    char *p = (char*)malloc(strlen(str) + 1);
    if (p != NULL) {
        strcpy(p, str);
    }
    return p;
}

int main() {
    char *s = createString("Hello, Pointer!");
    if (s != NULL) {
        printf("%s\n", s);
        free(s);  // 记得释放
    }
    return 0;
}

6.4.2 错误用法:返回局部变量地址

// ❌ 错误示例
int* badFunction() {
    int localVar = 100;
    return &localVar;  // localVar 在函数返回后就被销毁了
}

int main() {
    int *p = badFunction();
    printf("%d\n", *p);  // 未定义行为!可能崩溃,可能打印垃圾值
    return 0;
}

我曾经踩过的坑:在一个嵌入式项目中,我返回了一个局部数组的地址,结果程序运行一段时间后随机崩溃。调试了两天才发现是野指针问题。从那以后,我每次写返回指针的函数,都会先问自己:这个指针指向的内存,函数返回后还在不在?

6.4.3 返回静态变量地址

另一种安全的方式是返回静态变量的地址,但要注意线程安全问题。

char* getMonthName(int month) {
    static char *names[] = {
        "Invalid", "January", "February", "March", "April",
        "May", "June", "July", "August", "September",
        "October", "November", "December"
    };
    if (month >= 1 && month <= 12) {
        return names[month];
    }
    return names[0];
}

知识体系总览

下面这张图,是我梳理的本章知识结构。你可以把它当作一个思维导图来看。

指针与函数 函数指针 声明:int (*p)(int) 赋值:p = funcName 调用:p() 或 (*p)() 回调函数 作为参数传入 解耦调用者与实现 典型:qsort、事件驱动 函数指针数组 转移表(Jump Table) 替代 switch-case 状态机、命令解析 返回指针的函数 ✅ 返回动态内存 ❌ 返回局部变量

这张图把四个核心知识点串在了一起。你可以看到,函数指针是基础,回调函数和转移表是它的应用,而返回指针的函数则是一个容易踩坑的独立话题。

好了,这一章的内容就到这里。记住:函数指针不是用来炫技的,它是用来写出更灵活、更可维护的代码的。我在实际项目中,几乎每个模块都会用到它——从驱动层到应用层,无处不在。


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