4、指针与函数:函数参数传递(值传递与地址传递)、指针作为函数参数、函数返回指针
函数和指针的关系,说白了就是C语言的灵魂所在。我刚开始学的时候,总觉得函数就是函数,指针是指针,两者各玩各的。直到我在一个嵌入式项目里,被一个参数传递的bug折磨了整整两天……嗯,从那以后我才真正理解了,指针和函数配合起来,能干多少大事。
4.1 值传递 vs 地址传递——你真的会传参吗?
先问个问题:你写了一个swap函数,想交换两个变量的值,为什么调用完发现没变?
很多人刚学C语言时都踩过这个坑。我当年也不例外。
// 值传递——交换失败
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap(x, y);
printf("x = %d, y = %d\n", x, y); // 还是10, 20
return 0;
}
为什么会这样?因为C语言默认是值传递。你传进去的是x和y的副本,函数里改的是副本,原件纹丝不动。说白了,函数拿到的是复印件,你撕了复印件,原件还好好的。
核心区别:
- 值传递:形参是实参的拷贝,函数内修改不影响实参
- 地址传递:形参是指针,函数内通过指针修改实参指向的内存
那怎么才能真的交换?用指针!
// 地址传递——交换成功
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swap(&x, &y);
printf("x = %d, y = %d\n", x, y); // 20, 10
return 0;
}
你看,传的是地址,函数通过地址直接操作原始内存。这就像你把保险柜的钥匙给了别人,别人当然能打开你的保险柜拿东西。
我的习惯:如果函数需要修改传入的变量,一律用指针。如果只是读取数据,用值传递更安全,不会意外改坏数据。
4.2 指针作为函数参数——不只是为了改值
指针作为参数,可不只是为了修改实参。它还有两个更重要的用途:减少拷贝开销和实现输出参数。
4.2.1 减少拷贝——传大结构体时你就懂了
我记得有一次处理一个几百KB的结构体,用值传递传进去,每次调用都要拷贝一份,程序慢得像蜗牛。改成指针传递后,只传一个地址(8字节),速度飞起。
typedef struct {
char name[256];
int scores[1000];
// ... 很多字段
} Student;
// 值传递——每次拷贝几百KB
void printStudent(Student s) {
printf("Name: %s\n", s.name);
}
// 指针传递——只传8字节
void printStudentPtr(const Student *s) {
printf("Name: %s\n", s->name);
}
注意:用指针传参时,如果不想让函数修改数据,记得加const修饰。这是好习惯,能避免很多意外bug。我曾经因为忘了加const,在一个回调函数里把全局配置表改乱了,排查了三个小时……
4.2.2 输出参数——让函数“返回”多个值
函数只能有一个返回值,但有时候你需要返回多个结果。怎么办?用指针参数!
// 计算一个数的商和余数
void divide(int dividend, int divisor, int *quotient, int *remainder) {
*quotient = dividend / divisor;
*remainder = dividend % divisor;
}
int main() {
int q, r;
divide(17, 5, &q, &r);
printf("商 = %d, 余数 = %d\n", q, r); // 商 = 3, 余数 = 2
return 0;
}
你想想看,如果没有指针,你要么用全局变量(不推荐),要么把结果塞进结构体返回。指针参数是最优雅的方式。
4.3 函数返回指针——小心!别返回局部变量的地址
函数返回指针,这个功能很强大,但坑也最多。我见过太多新手(甚至老手)在这里翻车。
4.3.1 正确用法:返回动态分配的内存或静态变量
// 正确:返回动态分配的内存
int* createArray(int size) {
int *arr = (int*)malloc(size * sizeof(int));
if (arr == NULL) {
return NULL; // 分配失败
}
// 初始化...
return arr;
}
// 正确:返回静态变量
char* getMessage() {
static char msg[] = "Hello, Pointer!";
return msg;
}
4.3.2 错误用法:返回局部变量的地址
// 错误!返回局部变量的地址
int* getLocal() {
int local = 42;
return &local; // local在函数结束后就销毁了
}
int main() {
int *p = getLocal();
printf("%d\n", *p); // 未定义行为!可能打印42,也可能打印垃圾值
return 0;
}
我曾经犯过的错:在一个网络协议栈的项目里,我返回了一个局部缓冲区指针,结果在高并发下程序随机崩溃。查了两天才发现是野指针问题。从那以后,我给自己定了个规矩:永远不要返回局部变量的地址。
4.4 知识体系总览
下面这张图,是我梳理的本章核心逻辑。你看一遍,应该就能把零散的知识点串起来了。
4.5 实战避坑指南
最后,分享几个我这些年积累的经验,希望能帮你少走弯路。
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 修改实参 | 传指针 | 传值(改不了) |
| 传大结构体 | 传const指针 | 传值(拷贝慢) |
| 返回多个值 | 指针参数作为输出 | 全局变量(不安全) |
| 返回字符串 | 返回静态变量或动态内存 | 返回局部数组 |
| 函数回调 | 函数指针参数 | 硬编码(不灵活) |
我的一个小技巧:写函数时,先想清楚这个参数是「输入」还是「输出」。输入用值传递或const指针,输出用非const指针。这样代码一看就懂,不容易出错。
嗯,指针和函数这块,说白了就是「怎么把数据安全高效地传给函数,以及怎么从函数里把结果拿回来」。理解了值传递和地址传递的本质,你就能写出更健壮、更高效的代码。下次写函数时,多想想:这个参数,我到底该传值还是传地址?