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 与函数 修饰函数参数 传值参数 指针参数 修饰返回值 基本类型 指针类型 常量正确性 隐式转换规则 调用链传递 核心原则:该加 const 的地方必须加,不该加的地方别乱加 常见应用场景 结构体只读访问 字符串常量保护 内部数据封装 函数指针类型匹配

八、总结与建议

const 与函数的结合,说白了就是一件事:用编译器帮你保证数据不被意外修改。我做了这么多年嵌入式开发,可以负责任地说:const 用得好的代码,bug 率明显更低。

给你几个实用建议:

  1. 输入参数指针一律加 const,除非你明确要修改它
  2. 返回内部数据地址时加 const,防止外部篡改
  3. 函数指针类型声明要匹配 const,否则编译不过
  4. 结构体传指针不加 const 要写注释,说明为什么需要修改
  5. 不要用强制类型转换去掉 const,那是掩耳盗铃

最后提醒一句:const 是给编译器看的,不是给运行时的。它不会让代码跑得更快,但能让代码更安全。别嫌麻烦,该加就加。


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