40、const与volatile:C语言中的“冰与火”

说实话,const和volatile这两个关键字,单独拎出来都不算难。但把它们放在一起,很多同学就开始犯迷糊了。我当年刚学C语言时,也觉得这俩东西就像冰与火——一个说“你别动我”,一个说“我随时会变”。

嗯,今天咱们就把这对“冤家”彻底讲明白。

一、const修饰指针:到底谁不能变?

const修饰指针,说白了就三种情况。我习惯用一个口诀来记:“左定值,右定向,左右都定”

咱们直接看代码:

int a = 10, b = 20;

// 情况1:const在*左边 —— 指针指向的值不能改
const int *p1 = &a;
*p1 = 30;  // ❌ 错误!值不能改
p1 = &b;   // ✅ 可以,指针本身可以改

// 情况2:const在*右边 —— 指针本身不能改
int * const p2 = &a;
*p2 = 30;  // ✅ 可以,值能改
p2 = &b;   // ❌ 错误!指针不能改

// 情况3:两边都有const —— 值和指针都不能改
const int * const p3 = &a;
*p3 = 30;  // ❌ 错误!
p3 = &b;   // ❌ 错误!

我在项目中遇到过最坑的一次:有个同事把const写在了*右边,结果想改指针指向时死活编译不过。他盯着代码看了半小时,最后发现是const位置写反了。你想想看,这种低级错误要是上了生产环境,排查起来得多痛苦?

我的小技巧: 读const声明时,从右往左读。比如 const int *p 读作“p是一个指针,指向const int”。这样永远不会搞混。

二、volatile的作用:别让编译器“自作聪明”

volatile,说白了就是告诉编译器:“这个变量,你别给我优化,它随时可能变”

编译器有时候挺“自作聪明”的。它觉得某个变量没被修改,就把值缓存到寄存器里。但有些场景下,变量确实会“莫名其妙”地变——比如硬件寄存器、多线程共享变量、信号处理函数中的变量。

看个典型例子:

// 假设这是一个硬件状态寄存器地址
#define STATUS_REG (*(volatile unsigned int *)0x40001000)

void wait_for_flag() {
    while ((STATUS_REG & 0x01) == 0) {
        // 等待硬件置位
    }
}

如果不加volatile,编译器可能会把STATUS_REG的值读到寄存器里,然后一直判断那个寄存器值——硬件改了内存,但编译器不知道,程序就死循环了。我曾经在调试一个嵌入式设备时遇到过这问题,折腾了两天才发现是漏了volatile。

注意: volatile不能保证原子性!它只是防止编译器优化,不解决多线程的竞态问题。多线程同步还是得靠锁或原子操作。

三、volatile的典型使用场景

我总结了一下,volatile主要用在三个地方:

  • 硬件寄存器映射:比如上面那个状态寄存器
  • 中断服务程序中修改的变量:主程序和中断都可能改它
  • 多线程共享的全局标志:一个线程写,另一个线程读

举个例子:

volatile int g_flag = 0;

// 中断服务程序
void ISR_Handler() {
    g_flag = 1;  // 中断里修改
}

// 主循环
void main_loop() {
    while (g_flag == 0) {
        // 等待中断
    }
    // 处理事件
}

这里如果不加volatile,编译器可能把g_flag优化成永远为0,主循环就永远跳不出来了。

四、const与volatile的组合:看似矛盾,实则互补

const和volatile能同时修饰一个变量吗?能!而且很常见。

你想想看:一个硬件状态寄存器,它的值由硬件决定,程序不能改——这是const。但它的值随时可能变化——这是volatile。所以:

// 只读硬件寄存器
const volatile uint32_t *p_hw_reg = (const volatile uint32_t *)0x40001000;

uint32_t val = *p_hw_reg;  // ✅ 可以读
*p_hw_reg = 0x01;          // ❌ 错误!const禁止写

再比如,一个只读的全局时间戳,由RTC硬件更新:

const volatile time_t g_rtc_time;  // 程序不能改,但硬件会改

我在做物联网设备时,就经常用这种组合来定义硬件寄存器映射。既保证了程序不会误写,又防止了编译器优化导致读不到最新值。

核心要点: const和volatile不冲突。const管的是“程序能不能改”,volatile管的是“编译器要不要优化”。它们从不同维度约束变量,完全可以共存。

五、知识体系总览

下面这张图,把const和volatile的核心知识点串起来了:

const与volatile知识体系 const 修饰指针 const int *p 值不能改,指针能改 int * const p 指针不能改,值能改 const int * const p 值和指针都不能改 volatile 使用场景 硬件寄存器 防止编译器优化 中断共享变量 主程序和中断都访问 多线程标志 一个写,多个读 const + volatile 组合 程序不能改,但值会变(如硬件只读寄存器)

六、避坑指南

最后,分享几个我踩过的坑:

  • const变量被意外修改:通过指针强制类型转换可以绕过const检查,但后果自负。我曾经见过有人这么干,结果程序跑飞了。
  • volatile变量被过度使用:不是所有变量都需要volatile。滥用会导致代码膨胀、性能下降。只在确实需要的地方加。
  • const和volatile的顺序const volatile intvolatile const int 是一样的,但建议统一写法,保持代码风格一致。
一句话总结: const是“程序别动我”,volatile是“编译器别优化我”。搞懂这两个,面试时再遇到const/volatile的题,你就能从容应对了。

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