const与指针:一场关于“只读”的博弈

说实话,我刚学C语言那会儿,看到const int*int* constconst int* const这三种写法,脑袋是懵的。明明都是const和指针,怎么组合起来意思完全不一样?后来在项目里踩了几次坑,才真正搞明白——这玩意儿,说白了就是“谁不能改”的问题。

今天咱们就把这个知识点彻底掰扯清楚。我保证,看完这一章,你以后再也不会搞混它们。

一、先理解const的本质

const是C语言里的一个类型修饰符,它的意思是“只读”。但注意,const不是说这个值永远不变,而是说你不能通过这个变量名去修改它。至于它本身是不是真的不变,那得看它存在哪里——比如const变量如果存在ROM里,那确实改不了;如果存在RAM里,其实还是能通过指针偷偷改的,不过那是未定义行为,咱们不推荐这么干。

我个人习惯把const理解成一种“承诺”:你告诉编译器,我保证不通过这个标识符去修改数据。编译器信了你,就会帮你检查,一旦你试图修改,它就报错。

核心原则:const修饰的是它右边紧跟着的那个东西。

记住这句话,后面三种组合你就都能推导出来了。

二、三种const指针的对比

咱们直接上代码,边看边讲。

1. const int* —— 指向常量的指针

const int* p;   // 或者写成 int const* p,意思一样
int a = 10;
int b = 20;

p = &a;         // 可以:指针本身可以改
// *p = 30;     // 错误:不能通过p修改a的值
a = 30;         // 可以:a本身不是const,可以直接改
p = &b;         // 可以:指针可以指向别处

这种写法,const修饰的是int,也就是指针指向的那个整数。所以你不能通过p去修改它指向的值,但是p本身可以指向别的地方。

我在项目中遇到过一种场景:有个函数要读取一个传感器数据缓冲区,但绝对不能修改它。这时候我就会用const int*作为参数类型,告诉调用者:“你放心,我只读不改。”

2. int* const —— 常量指针

int* const p = &a;  // 必须初始化
int a = 10;
int b = 20;

*p = 30;            // 可以:可以通过p修改a的值
// p = &b;          // 错误:p本身是const,不能指向别处

这里const修饰的是p本身,也就是指针变量。所以p一旦初始化,就不能再指向其他地址,但是你可以通过p去修改它指向的值。

嗯,这里要注意:int* const必须在定义时初始化,因为后面不能再赋值了。这有点像数组名——数组名其实就是一个常量指针,你不能让它指向别处。

3. const int* const —— 双重const

const int* const p = &a;  // 必须初始化
int a = 10;
int b = 20;

// *p = 30;    // 错误:不能通过p修改a的值
// p = &b;     // 错误:p本身也不能改

这个就很好理解了:指针本身不能改,指向的值也不能通过它改。两个const,一个管指针,一个管数据,各司其职。

说实话,这种写法在实际项目里用得不多,但一旦用了,通常是为了保护一些全局配置数据,既不让指针乱飘,也不让数据被意外修改。

三、一张表看懂区别

写法 指针本身可改? 指向的值可改? 常见叫法
const int* ✅ 可以 ❌ 不可以 指向常量的指针
int* const ❌ 不可以 ✅ 可以 常量指针
const int* const ❌ 不可以 ❌ 不可以 双重const指针

你想想看,其实只要记住“const修饰它右边的东西”这个原则,这三种情况完全可以自己推导出来,根本不用死记硬背。

四、const在函数参数中的应用

这部分才是实际开发中最常用的。我敢说,你去看任何一个成熟的开源项目,函数参数里const的出现频率都高得吓人。

4.1 保护输入参数

void print_data(const int* data, int len) {
    for (int i = 0; i < len; i++) {
        printf("%d ", data[i]);
        // data[i] = 0;  // 编译错误,不能修改
    }
}

为什么要加const?说白了就是契约。你告诉调用者:“我保证不修改你的数据。”调用者看到const,就知道这个函数是安全的,不会偷偷改他的东西。

我曾经在一个嵌入式项目里遇到过这样的bug:一个同事写的函数没有加const,结果内部不小心修改了传入的缓冲区数据,导致上层逻辑全乱了。从那以后,我给自己定了个规矩:只要函数不修改指针指向的数据,就一定要加const。这不仅是好习惯,更是团队协作的基本素养。

4.2 const与字符串常量

void process_string(const char* str) {
    // 处理字符串,但不修改它
}

字符串常量(比如"hello")的类型是const char*,如果你用char*去接收,编译器会给你一个警告。所以处理字符串时,参数类型写成const char*是最规范的。

4.3 const指针作为输出参数

void get_version(const char** version) {
    static const char* ver = "v2.1.0";
    *version = ver;  // 修改指针指向,但不修改字符串内容
}

这种写法稍微绕一点,但原理一样:const char**表示指向“指向常量的指针”的指针。你通过它可以让外部的指针指向一个常量字符串,但不能通过它去修改那个字符串。

五、避坑指南

我曾经踩过的坑:

  • const int* 和 int* 混用:你不能把int*赋值给const int*?其实可以,反过来不行。因为从“可修改”到“只读”是安全的,但从“只读”到“可修改”是危险的。
  • const修饰的是类型还是变量:记住,const永远修饰它右边的东西。比如int const *pconst int *p完全等价,都是指向常量的指针。
  • 函数参数用const指针传递大结构体:如果你传一个结构体,用const struct big_struct*比传值效率高得多,还能保证不被修改。

我的个人建议:

写函数时,先把参数都写成const,然后编译器会告诉你哪里需要去掉const。这样你就能确保每个参数都经过了深思熟虑——到底要不要修改它?

六、知识体系图

下面这张图帮你理清const与指针的完整关系,建议保存下来多看几遍。

const与指针知识体系 const 与 指针 const int* int* const const int* const 指向的值不可改 指针本身可以改 常用于保护输入参数 指针本身不可改 指向的值可以改 常用于硬件寄存器映射 两者都不可改 必须初始化 常用于全局配置数据 函数参数保护 硬件地址映射 全局只读数据 核心原则:const修饰它右边紧跟着的东西

七、总结

const与指针的组合,说白了就是“谁不能改”的问题。你只要记住:

  • const int*:数据是const,指针不是
  • int* const:指针是const,数据不是
  • const int* const:都是const

在函数参数中多用const,能让你的代码更安全、更清晰。我个人觉得,const用得好不好,是区分C语言新手和老手的一个重要标志。别嫌麻烦,养成习惯就好了。

好了,这一章的内容就到这里。记住,const不是限制,而是保护——保护你的数据不被意外修改,保护你的代码意图更加明确。


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