volatile与const在C语言中如何协同作用?

99ANYc3cd6
预计阅读时长 16 分钟
位置: 首页 C语言 正文

核心概念速览

关键字 核心作用 目的 编译器行为
const “只读” 保证变量的值在程序运行期间不被意外修改。 优化:编译器会假设 const 变量的值不会改变,从而进行更激进的优化,比如将值缓存到寄存器中。
volatile “易变” 告诉编译器,变量的值可能被程序之外的因素(硬件、其他线程)改变。 禁止优化:编译器每次使用 volatile 变量时,都必须从内存中重新读取,而不是使用寄存器中的缓存值。

const 关键字详解

const 的全称是 "constant",意为“常量”,它用来声明一个“只读”变量。

c语言 volatile const
(图片来源网络,侵删)

基本用法

最简单的用法是声明一个普通常量:

const int MAX_SIZE = 100;
// MAX_SIZE = 200; // 错误!不能修改 const 变量的值。

const 与指针

const 与指针结合使用时,情况会变得稍微复杂,关键在于 const 修饰的是谁。

a. 指向常量的指针

int a = 10;
int b = 20;
const int *ptr = &a; // 或者 int const *ptr = &a;
*ptr = 30;           // 错误!不能通过 ptr 修改它指向的值。
ptr = &b;            // 正确!可以改变指针的指向,让它指向另一个变量。
  • 含义ptr 是一个指针,它指向的值是“只读”的,你不能通过 ptr 来修改这个值。
  • 重点const 修饰的是 *ptr(指针指向的值),而不是 ptr(指针本身)。

b. 常量指针

c语言 volatile const
(图片来源网络,侵删)
int a = 10;
int b = 20;
int *const ptr = &a;
*ptr = 30;           // 正确!可以通过 ptr 修改它指向的值。
ptr = &b;            // 错误!不能改变指针的指向。
  • 含义ptr 本身是一个“常量”指针,它初始化后就不能再指向其他地址了。
  • 重点const 修饰的是 ptr(指针本身)。

c. 指向常量的常量指针

int a = 10;
int b = 20;
const int *const ptr = &a;
*ptr = 30;           // 错误!不能修改指向的值。
ptr = &b;            // 错误!不能改变指针的指向。
  • 含义ptr 是一个“常量”指针,并且它指向的值也是“只读”的,两者都不能改变。

const 的主要用途

  • 定义真正的常量:如 const double PI = 3.14159;
  • 函数参数:防止函数意外修改传入的参数。
    void print_string(const char *str) {
        // str[0] = 'H'; // 错误!防止函数修改字符串内容
        printf("%s\n", str);
    }
  • 函数返回值:返回一个指向只读数据的指针,防止调用者修改。
    const char* get_status_message() {
        static const char msg[] = "Operation successful.";
        return msg;
    }
  • 全局/静态常量:避免使用 #define 宏,因为 const 变量有类型检查,作用域也更可控。

volatile 关键字详解

volatile 的全称是 "volatile",意为“不稳定的、易变的”,它用于告诉编译器,不要对它修饰的变量做任何“假设”,因为它的值可能在任何时候、以任何未知的方式被改变。

为什么需要 volatile

编译器为了优化性能,会做一些“合理”的假设。

int flag = 0;
// ... 其他代码 ...
while (flag == 0) {
    // do something
}

编译器可能会认为 flag 在循环中不会被改变,于是它可能会将 flag 的值(0)加载到 CPU 寄存器中,然后一直检查寄存器里的值,而不是去内存中读取,如果另一个线程或硬件中断将 flag 的值改为了 1,这个循环将永远不会结束,因为它永远看不到内存中的变化。

c语言 volatile const
(图片来源网络,侵删)

volatile 的作用就是打破这种“合理”的假设。

volatile 的典型应用场景

a. 硬件寄存器

在嵌入式系统或驱动开发中,我们直接操作硬件寄存器,这些寄存器的值由硬件本身改变,而不是由 C 代码改变。

// 假设 0x12345678 是一个状态寄存器的地址
#define STATUS_REGISTER (*(volatile unsigned int *)0x12345678)
// 轮询等待某个事件发生
while ((STATUS_REGISTER & 0x01) == 0) {
    // 循环体内什么都不做
    // 编译器不能优化掉这个循环,也不能将 STATUS_REGISTER 的值缓存
}
  • 为什么必须用 volatile 因为硬件可能会在任何时候改变 STATUS_REGISTER 的值,没有 volatile,编译器可能会认为循环条件 STATUS_REGISTER & 0x01 永远为 0,从而将整个循环优化成一个空操作或死循环。

b. 中断服务程序

全局变量可能被主循环使用,同时被中断服务程序修改。

int g_flag = 0;
void main_loop() {
    while (1) {
        if (g_flag) {
            // 处理中断事件
            g_flag = 0;
        }
    }
}
void ISR() { // Interrupt Service Routine
    g_flag = 1; // 中断发生时设置标志
}

g_flag 没有 volatile 修饰,main_loop 中的 if (g_flag) 可能会被编译器优化为只读取一次,导致无法响应后续的中断。

正确写法:

volatile int g_flag = 0;

c. 多线程环境

在多线程编程中,一个线程共享的变量,如果被另一个线程修改,那么这个变量就应该声明为 volatile,以防止编译器优化导致一个线程看不到另一个线程的修改。

// 线程1
volatile int shared_data = 0;
// 线程1
while (shared_data == 0) {
    // 等待线程2修改 shared_data
    // 没有 volatile,编译器可能优化成死循环
}
// 线程2
shared_data = 1; // 修改共享数据

volatile 的局限性

volatile 不能保证原子性,它只保证“每次都从内存读取”,但不保证读取-修改-写回这个操作是原子的。

volatile int counter = 0;
// 线程A
counter++;
// 线程B
counter++;

即使 countervolatile 的,counter++ 操作(读取-加1-写回)在多线程下仍然是不安全的,如果两个线程同时读取到 0,然后都加 1 再写回,最终结果会是 1 而不是 2,在这种情况下,你需要使用互斥锁原子操作 来保证线程安全。


constvolatile 的结合使用

这是非常重要的一个用法,一个变量可以同时是 constvolatile

const volatile int *p_reg = (const volatile int *)0x12345678;
  • volatile:告诉编译器,这个变量的值可能被硬件(程序之外)改变,所以每次使用都必须从内存重新读取,不要缓存。
  • const:告诉编译器,这个变量的值不能被当前程序(C 代码)修改,你不能通过指针 p_reg 来改变这个地址的值。

典型场景:一个只读的状态寄存器,它的值由硬件自动更新,但你的程序只能读取它,不能写入。

  • 硬件:可以改变这个寄存器的值(所以是 volatile)。
  • 你的程序:不能改变这个寄存器的值(所以是 const)。

总结与对比

特性 const volatile
中文含义 只读的 易变的
核心目的 保护数据不被代码修改 保护代码不被数据误导
告诉编译器 “这个变量我不会改,你可以放心地基于这个假设进行优化。” “这个变量可能会被我不知道的东西(硬件/中断/其他线程)改变,每次用都得去内存里看看,别用你手里的缓存。”
主要用途 定义常量、函数参数、函数返回值 硬件寄存器、全局标志位、多线程共享变量
优化影响 允许编译器进行更激进的优化(如缓存值)。 禁止编译器进行某些优化(如缓存值、移除看似“无用”的代码)。
线程安全 不提供任何线程安全保障。 不提供原子性,因此不保证线程安全。
结合使用 const volatile:表示一个“只读”但“易变”的变量,如硬件只读状态寄存器。
  • const 是为了让代码更安全、清晰
  • volatile 是为了让程序能正确地与外部世界(硬件/多线程)交互
-- 展开阅读全文 --
头像
dede sql标签如何实现分页?
« 上一篇 12-02
C语言16进制1002是什么意思?
下一篇 » 12-02

相关文章

取消
微信二维码
支付宝二维码

目录[+]