当我阅读《The C programming language, second edition》 SECTION A8.2 (page 211) 时看到下面一段话,谈论的是 const 与 volatile 的对象与编译器优化相关的问题。

The const and volatile properties are new with the ANSI Standart. The purpose of const is to announce objects that may be placed in read-only memory, and perhaps to increase opportunities for optimization. The purpose of volatile is to force an implementation to suppress optimization that could otherwise occur. For example, for a machine with memory-mapped input/output, a pointer to a device register might be declared as a pointer to volatile, in order to prevent the compiler from removing apparently redundant references through the pointer. Except that it should diagnose explicit attempts to change const objects, a compiler compiler may ignore these qulifiers.

以下是翻译的中文版本:

constvolatile 是 ANSI 标准中的新特性。const 的作用是声明可能放置在只读内存中的对象,也可以增加优化的机会。volatile 的作用是强制实现禁止可能发生的优化。例如,在具有内存映射输入/输出的机器上,指向设备寄存器的指针可以声明为 volatile,以防止编译器移除通过该指针进行的看似多余的引用。除了应该诊断显式尝试更改 const 对象之外,编译器可能会忽略这些修饰符。

const 和 volatile 的解释

根据 ANSI C 标准(C89/C90、C99、C11)以及 C++ 标准,constvolatile 是两个关键字,用于修饰变量的类型。

  1. constconst 用于声明一个常量,即其值在初始化后不能被修改。在 ANSI C 标准中,使用 const 关键字来声明常量的语法如下:

    1
    
    const <type> <identifier> = <value>;
    

    例如:

    1
    
    const int MAX_VALUE = 100;
    

    const 可以用于修饰任何数据类型,包括基本数据类型、结构体、指针等。对于指针来说,const 可以用于修饰指针本身,也可以用于修饰指针所指向的对象。

  2. volatilevolatile 用于声明一个变量是易变的,即其值可能在未知的时间点被外部因素修改。在 ANSI C 标准中,volatile 用于修饰那些可以被意外修改的变量,例如硬件寄存器、中断服务子例程中使用的变量等。volatile 告诉编译器不要对被修饰的变量进行优化,以确保每次访问都是直接从内存读取或写入。volatile 的语法如下:

    1
    
    volatile <type> <identifier>;
    

    例如:

    1
    
    volatile int status;
    

    volatile 主要用于修饰全局变量、指针、静态变量以及被多个任务或中断共享的变量。

总结区别:

  • const 用于声明常量,其值在初始化后不能被修改。
  • volatile 用于声明易变的变量,其值可能在未知的时间点被外部因素修改。
  • const 用于修饰常量对象,而 volatile 用于修饰易变的对象。
  • const 关注的是对象的值是否可变,而 volatile 关注的是对象的访问是否具有可观察的副作用,如与硬件的交互。
  • const 用于类型安全和编译时优化,而 volatile 用于确保对变量的读写操作不会被优化掉。

需要注意的是,constvolatile 可以同时修饰一个变量,例如 const volatile int x; 表示一个既是常量又是易变的整数。

举例

下面是 constvolatile 的区别以及使用示例:

  1. const 关键字的作用:

    • 声明一个常量,即不可修改的值。
    • 提示编译器进行优化,因为编译器知道该变量的值不会改变。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    
    const int MAX_VALUE = 100;
    const float PI = 3.1415;
    
    int main() {
        // MAX_VALUE 和 PI 的值在初始化后不能修改
        // 编译器可以进行常量折叠等优化
        return 0;
    }
    
  2. volatile 关键字的作用:

    • 告知编译器变量的值可能会在未经过编译器读取或写入的情况下被改变。
    • 防止编译器进行针对该变量的优化,以确保读取和写入操作的准确性。

    示例:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    volatile int* device_register = (volatile int*)0x1234;
    
    void readFromDevice() {
        int value = *device_register;  // 从设备寄存器读取值
        // ...
    }
    
    void writeToDevice(int value) {
        *device_register = value;  // 写入值到设备寄存器
        // ...
    }
    

在上述示例中,device_register 是一个指向设备寄存器的指针,并被声明为 volatile。这是因为设备寄存器的值可能会被外部设备修改,而编译器不应该对这些读写操作进行优化,以确保读取和写入的准确性。通过将指针声明为 volatile,编译器将确保对寄存器的读取和写入操作不会被优化或省略,从而保证与外部设备的正确交互。

编译器有时会忽略 const 和 volatile

上面一段话中最后一句:

Except that it should diagnose explicit attempts to change const objects, a compiler compiler may ignore these qulifiers.

这句话的意思是,编译器应该检测并报告对 const 对象进行显式修改的尝试,但是编译器也可以忽略这些修饰符。

在 ANSI 标准中,const 修饰符用于声明常量,即不可修改的值。然而,有时程序员可能会试图通过某种方式来修改一个被声明为 const 的对象。根据标准,编译器应该检测到这样的尝试并发出警告或错误信息。

但是,标准也指出编译器可以忽略 constvolatile 修饰符。这意味着编译器可以选择不执行对于这些修饰符的检查,也不发出相应的警告或错误信息。这种情况下,编译器可能会对被声明为 const 的对象进行修改,或者对被声明为 volatile 的对象进行优化。

总结起来,根据 ANSI 标准,编译器应该检测显式修改 const 对象的尝试,但是编译器也可以选择忽略这些修饰符,从而允许对 const 对象进行修改或对 volatile 对象进行优化。

为什么建议将指向 device register 的指针声明为 volatile?

将指针声明为volatile是为了告诉编译器该指针所指向的内存地址是可能发生变化的,因此编译器在优化代码时不应该对该指针的读写进行优化。

当一个指针指向一个设备寄存器时,这个寄存器通常是与外部设备交互的接口,其内容可以在任何时间被外部设备修改。这种情况下,如果不将指针声明为volatile,编译器可能会对读取和写入该指针所指向的寄存器进行优化,例如将多次读取合并为一次,或者将多次写入合并为一次。这样的优化可能会导致与外部设备的交互出现问题,因为外部设备的操作没有被及时反映在内存中。

通过将指针声明为volatile,编译器将确保每次对该指针所指向的寄存器的读写都是实际进行的,而不会进行任何优化。这样可以保证与外部设备的交互是准确的,并且能够及时地处理设备状态的变化。

需要注意的是,将指针声明为volatile只能确保对指针所指向的内存地址的读写是可见的,但无法保证对指针本身的操作是原子的。如果需要保证原子性,还需要使用适当的同步机制,如互斥锁或原子操作。