第六章:指针与函数——当指针遇上函数,才是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];
}
知识体系总览
下面这张图,是我梳理的本章知识结构。你可以把它当作一个思维导图来看。
这张图把四个核心知识点串在了一起。你可以看到,函数指针是基础,回调函数和转移表是它的应用,而返回指针的函数则是一个容易踩坑的独立话题。
好了,这一章的内容就到这里。记住:函数指针不是用来炫技的,它是用来写出更灵活、更可维护的代码的。我在实际项目中,几乎每个模块都会用到它——从驱动层到应用层,无处不在。
公众号:蓝海资料掘金营,微信 deep3321