4. 指针与函数:函数参数传递
函数和指针,这两个东西放在一起,是很多C语言学习者的分水岭。我个人觉得,搞懂了这一章,你才算真正开始用C语言写工程代码了。咱们今天不聊虚的,直接看代码,看实际项目中怎么用。
4.1 值传递与地址传递——你真的会传参吗?
先问个问题:你写一个函数,想修改传入的变量,为什么有时候改不了?
说白了,C语言默认的参数传递方式是值传递。什么意思?就是函数拿到的是实参的副本,不是它本人。
#include <stdio.h>
void swap_wrong(int a, int b) {
int temp = a;
a = b;
b = temp;
printf("函数内部:a=%d, b=%d\n", a, b);
}
int main() {
int x = 10, y = 20;
printf("调用前:x=%d, y=%d\n", x, y);
swap_wrong(x, y);
printf("调用后:x=%d, y=%d\n", x, y);
return 0;
}
运行结果:
调用前:x=10, y=20
函数内部:a=20, b=10
调用后:x=10, y=20
看到了吧?函数内部确实交换了,但main函数里的x和y纹丝不动。这就是值传递的“坑”——你改的是副本,原件完好无损。
核心结论:值传递时,函数形参是实参的拷贝。修改形参不影响实参。
那怎么才能真的改掉?用指针。把变量的地址传进去,函数通过地址直接操作内存。
void swap_correct(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
printf("调用前:x=%d, y=%d\n", x, y);
swap_correct(&x, &y);
printf("调用后:x=%d, y=%d\n", x, y);
return 0;
}
运行结果:
调用前:x=10, y=20
调用后:x=20, y=10
这才是我们想要的交换。你想想看,地址传递的本质是什么?是传地址的值。没错,地址本身也是值,但通过这个地址,函数可以访问到实参所在的内存空间。
我的经验:在嵌入式开发中,我经常用地址传递来修改硬件寄存器。比如操作GPIO的输出寄存器,直接传地址进去,函数里用指针修改。效率高,而且意图明确。
4.2 指针作为函数参数——不只是为了修改
指针作为参数,不只是为了“改值”。它还有两个重要用途:减少拷贝和输出多个结果。
4.2.1 减少拷贝——传大结构体时特别有用
假设你有一个结构体,里面几百个字节。如果值传递,每次调用函数都要把整个结构体压栈。浪费栈空间,还慢。
typedef struct {
int id;
char name[64];
float scores[100];
} Student;
// 值传递——拷贝整个结构体
void print_student_by_value(Student s) {
printf("ID: %d, Name: %s\n", s.id, s.name);
}
// 指针传递——只传4个字节(32位系统)
void print_student_by_ptr(const Student *s) {
printf("ID: %d, Name: %s\n", s->id, s->name);
}
注意第二个函数里的const关键字。我建议你养成这个习惯:如果函数不需要修改指针指向的内容,就加const。这既是文档,也是保护。
4.2.2 输出多个结果——一个函数返回多个值
C语言的return只能返回一个值。那要返回多个结果怎么办?用指针参数“带出来”。
// 计算一个数的商和余数
int divide(int dividend, int divisor, int *quotient, int *remainder) {
if (divisor == 0) {
return -1; // 错误码
}
*quotient = dividend / divisor;
*remainder = dividend % divisor;
return 0; // 成功
}
int main() {
int q, r;
if (divide(17, 5, &q, &r) == 0) {
printf("商=%d, 余数=%d\n", q, r);
}
return 0;
}
我曾经踩过的坑:在写一个通信协议解析函数时,我忘了检查指针参数是否为NULL。结果传入了一个空指针,函数直接解引用,程序崩溃。从那以后,我所有接收指针参数的函数,第一件事就是判空。
4.3 函数指针——把函数当成变量用
函数指针,说白了就是指向函数的指针。函数本身也有地址,存在代码区。我们可以把这个地址存起来,通过它来调用函数。
4.3.1 定义与调用
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
// 定义一个函数指针
int (*func_ptr)(int, int);
// 让指针指向add函数
func_ptr = add; // 或者 &add,效果一样
// 通过函数指针调用
int result = func_ptr(3, 4); // 或者 (*func_ptr)(3, 4)
printf("结果:%d\n", result);
return 0;
}
函数指针的类型声明看起来有点怪:int (*func_ptr)(int, int)。我教你一个记忆方法:先写一个普通函数声明int func(int, int),然后把函数名换成(*指针名)。搞定。
4.3.2 函数指针数组——查表法调用
在嵌入式菜单系统或协议处理中,我经常用函数指针数组。比如一个计算器:
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide_op(int a, int b) { return b ? a / b : 0; }
int main() {
// 函数指针数组
int (*ops[4])(int, int) = {add, sub, mul, divide_op};
char op_chars[] = {'+', '-', '*', '/'};
int a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("%d %c %d = %d\n", a, op_chars[i], b, ops[i](a, b));
}
return 0;
}
这种写法比用switch-case优雅多了。增加新运算时,只需要加一个函数,然后在数组里注册一下。代码的扩展性一下子就上来了。
4.4 回调函数机制——让代码活起来
回调函数,本质上就是把函数指针作为参数传给另一个函数。被调用的函数在合适的时机,通过这个指针去调用你传进来的函数。
为什么要这么绕?因为这样可以实现“控制反转”——你写一个通用框架,让使用者通过回调来定制行为。
4.4.1 一个简单的回调例子
#include <stdio.h>
// 回调函数类型
typedef void (*callback_t)(int);
// 一个通用的事件处理器
void process_events(int data, callback_t cb) {
printf("处理数据:%d\n", data);
if (cb) {
cb(data); // 调用回调
}
}
// 用户自定义的处理函数
void my_handler(int value) {
printf("我的处理:%d 的平方是 %d\n", value, value * value);
}
int main() {
process_events(5, my_handler);
process_events(10, NULL); // 不传回调,也能工作
return 0;
}
4.4.2 嵌入式中的实际应用——定时器回调
我在做物联网终端时,经常用定时器回调。比如:
typedef void (*timer_callback_t)(void *arg);
typedef struct {
uint32_t interval_ms;
timer_callback_t callback;
void *arg;
uint8_t is_running;
} Timer;
void timer_start(Timer *t) {
t->is_running = 1;
// 启动硬件定时器...
// 定时时间到后,自动调用 t->callback(t->arg)
}
// 用户定义的回调
void led_blink(void *arg) {
int *led_pin = (int *)arg;
// 翻转LED
toggle_gpio(*led_pin);
}
int main() {
int led_pin = 13;
Timer led_timer = {
.interval_ms = 500,
.callback = led_blink,
.arg = &led_pin,
.is_running = 0
};
timer_start(&led_timer);
// 主循环做其他事...
return 0;
}
回调机制的核心价值:分离“什么时候做”和“做什么”。框架负责时机,用户负责行为。这是很多嵌入式RTOS和中间件的设计基础。
4.5 本章知识体系
下面这张图,把本章的核心逻辑串起来了。建议你多看几遍,理解每个概念之间的关系。
4.6 避坑指南
最后,分享几个我实际工作中踩过的坑,希望能帮你少走弯路。
- 函数指针类型不匹配:我曾经把一个返回
int的函数指针赋值给返回void的指针变量,编译没报错,运行时栈乱了。一定要用typedef明确类型。 - 回调中的上下文丢失:在RTOS中注册定时器回调时,忘了传
void *arg参数。结果回调函数里拿不到需要的变量。解决方案:总是给回调传一个上下文指针。 - 函数指针数组越界:用查表法时,索引值超出了数组范围。我后来都会加一个边界检查,或者用枚举做索引,避免硬编码数字。
- const指针的误用:
const int *p和int * const p是两回事。前者不能改指向的值,后者不能改指向的地址。搞混了,编译不过。
我的建议:刚开始用函数指针时,先写一个简单的回调例子跑通。比如用qsort的排序回调。跑通了,再往复杂了用。一口吃不成胖子,指针这东西,得慢慢品。
好了,这一章的内容就到这里。指针和函数的结合,是C语言从“能用”到“会用”的关键一步。多写、多练、多调试,慢慢你就能体会到它的威力了。