const与函数:const修饰参数与返回值,常量正确性
const 这个关键字,在 C 语言里经常被新手忽略。说实话,我早年做项目时也吃过它的亏。有一次维护一个通信协议栈,函数参数传进来一个结构体指针,结果被内部函数偷偷改了。查了两天 bug,最后发现是 const 没加。从那以后,我对 const 的态度就认真起来了。
今天咱们就聊聊 const 在函数中的正确用法。说白了,就是让编译器帮你盯着那些不该被改的数据。
一、const 修饰函数参数
函数参数分两种:传值和传指针。const 在这两种场景下的意义完全不同。
1. 传值参数加 const
先看个例子:
void print_value(const int x) {
// x = 10; // 编译错误!不能修改 const 参数
printf("%d\n", x);
}
我个人觉得,传值参数加 const 意义不大。因为传进来的本来就是副本,你改不改都不影响调用者。但有些团队规范要求这么做,主要是为了函数内部逻辑清晰——告诉读者:这个参数在函数里是只读的。
2. 传指针参数加 const
这才是重点。指针参数加 const 有两种写法:
// 写法一:指针指向的内容不可修改
void process_data(const int *p) {
// *p = 10; // 错误!不能修改指向的内容
int val = *p; // 可以读取
}
// 写法二:指针本身不可修改
void store_ptr(int * const p) {
int x = 10;
// p = &x; // 错误!不能修改指针本身
*p = 20; // 可以修改指向的内容
}
// 写法三:两者都不可修改
void read_only(const int * const p) {
// 既不能改指针,也不能改内容
}
我在项目中见过最多的场景是:函数需要读取一个大型结构体,但绝对不能修改它。这时候用 const struct big_struct * 就非常合适。你想想看,如果不加 const,调用者就得担心自己的数据被篡改,还得自己备份一份,多麻烦。
核心原则:如果函数只读不写指针指向的数据,就加上 const。这是接口契约的一部分。
二、const 修饰函数返回值
返回值加 const 的情况相对少一些,但用对了能避免一些隐蔽的错误。
1. 返回基本类型
const int get_value(void) {
return 42;
}
int main() {
const int a = get_value(); // 可以
int b = get_value(); // 也可以,赋值时会拷贝
}
说实话,返回基本类型加 const 基本没用。因为返回值是右值,你没法对它赋值。比如 get_value() = 10; 本来就不合法,加不加 const 都一样。
2. 返回指针类型
这个就有意思了:
// 返回指向常量的指针
const char *get_message(void) {
return "Hello, world!";
}
// 调用者不能通过返回值修改字符串
int main() {
const char *msg = get_message();
// msg[0] = 'h'; // 错误!不能修改 const 数据
}
嗯,这里要注意:如果函数返回的是内部静态数据的地址,一定要加 const。否则调用者就能绕过你的封装,直接修改内部数据。我曾经在一个日志模块里犯过这个错——返回了内部缓冲区的指针,结果被某个线程改了,日志全乱套了。
避坑指南:返回 const 指针时,调用者如果试图用非 const 指针接收,编译器会报 warning。别忽略它!
三、常量正确性(const-correctness)
这个概念听起来高大上,说白了就是:整个代码中,该加 const 的地方都加上,不该加的地方别乱加。
为什么要强调这个?因为 C 语言的 const 传递是有规则的:
- 非 const 指针可以隐式转换为 const 指针(安全)
- const 指针不能隐式转换为非 const 指针(不安全)
看个例子就明白了:
void read_only(const int *p) { }
void read_write(int *p) { }
int main() {
int a = 10;
const int b = 20;
read_only(&a); // 没问题,int* → const int*
read_only(&b); // 没问题,const int* → const int*
read_write(&a); // 没问题,int* → int*
// read_write(&b); // 错误!const int* 不能传给 int*
}
你想想看,如果允许 const 指针传给非 const 参数,那函数就能修改本不该被修改的数据,const 就形同虚设了。
四、const 在函数链中的传递
实际项目中,函数往往是层层调用的。const 必须贯穿整个调用链:
// 底层函数
static int calc_internal(const int *data, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}
// 中间层函数
int process_buffer(const int *buf, int size) {
// 可以安全地调用底层函数
return calc_internal(buf, size);
}
// 顶层函数
void handle_data(int *data, int n) {
// 非 const 可以传给 const
int result = process_buffer(data, n);
printf("Result: %d\n", result);
}
如果中间某个函数忘了加 const,整个链条就断了。编译器会报错,或者你得用强制类型转换——那基本等于放弃了 const 保护。
我的习惯:写新函数时,先想清楚参数是输入还是输出。输入参数一律加 const,输出参数不加。这样接口意图一目了然。
五、const 与结构体参数
结构体传参时,const 的作用更明显:
typedef struct {
int id;
char name[32];
float score;
} Student;
// 传结构体指针,加 const
void print_student(const Student *s) {
printf("ID: %d, Name: %s, Score: %.1f\n",
s->id, s->name, s->score);
// s->id = 100; // 错误!不能修改
}
// 传结构体本身(拷贝),加不加 const 都行
void print_student_copy(Student s) {
s.id = 100; // 可以修改,但只影响副本
printf("ID: %d\n", s.id);
}
我个人强烈建议:结构体参数一律传指针,并且输入参数加 const。传结构体本身效率低,而且容易造成栈溢出。你想想看,一个 1KB 的结构体,每次调用都拷贝一份,多浪费。
六、const 与函数指针
函数指针的 const 用法比较特殊:
// 声明一个函数指针,指向的函数参数带 const
int (*func_ptr)(const int *data, int len);
// 实际函数
int sum_array(const int *arr, int size) {
int total = 0;
for (int i = 0; i < size; i++) {
total += arr[i];
}
return total;
}
int main() {
func_ptr = sum_array; // 类型匹配
int arr[] = {1, 2, 3};
int result = func_ptr(arr, 3);
printf("%d\n", result);
}
这里要注意:函数指针的类型声明必须和被指向的函数完全一致,包括 const 修饰。我曾经因为函数指针的 const 不匹配,折腾了半天才找到问题。
七、知识体系图
下面这张图总结了 const 在函数中的核心用法:
八、总结与建议
const 与函数的结合,说白了就是一件事:用编译器帮你保证数据不被意外修改。我做了这么多年嵌入式开发,可以负责任地说:const 用得好的代码,bug 率明显更低。
给你几个实用建议:
- 输入参数指针一律加 const,除非你明确要修改它
- 返回内部数据地址时加 const,防止外部篡改
- 函数指针类型声明要匹配 const,否则编译不过
- 结构体传指针不加 const 要写注释,说明为什么需要修改
- 不要用强制类型转换去掉 const,那是掩耳盗铃
最后提醒一句:const 是给编译器看的,不是给运行时的。它不会让代码跑得更快,但能让代码更安全。别嫌麻烦,该加就加。
公众号:蓝海资料掘金营,微信deep3321