深入理解C——const与volatile

理解编译器级别的类型限定符,const与volatile的实质原理。

深入理解C——const与volatile

1 const

1.1 概要

constconstant 的简记形式,作为C语言的关键字,实现的是类型限定符的作用。

类型限定符可以与任何类型说明符一起使用。可以对const对象进行初始化,但在初始化之后不能进行赋值。……const用于声明可以存放在只读存储器中的对象,并可能提高优化的可能性。……除了诊断显式尝试修改const对象的情况外,编译器可能会忽略这些限定符。

(Kernighan&Ritchie 《The C Programming Language(2nd Edition)》)

const用于实现常量变量,通过编译器检查诊断的方式,避免对const变量的二次赋值。

1.2 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 定义const变量a */
int const a=10; // 写法1
const int a=10; // 写法2

/* 指针p指向空间为const,即*p或a为const */
int const *p=&a; // 写法1
const int *p=&a; // 写法2

/* 指针变量p为const变量,即指针本身不能被修改 */
int *const p=&a;

/* 指针变量p为const变量,所指变量a也为const变量 */
int const* const p=&a;

1.3 实质

const实现的不是真正意义上的常量!

const机制是编译器检查实现的,不是运行时保护,运行时无const约束。

const变量仅仅是编译时的“伪常量”。

具体说来:

1
2
3
4
5
6
int const a=10;
int *p = (int *)&a;

// a=100; 编译报错!直接赋值无法通过编译器检查,编译器会保护const变量被赋值修改。
*p = 100; // 赋值成功!通过指针,在运行时对该地址存储的int变量进行赋值。

  • 此例中,通过取const变量的地址并强制类型转换为(int *)非const的int型指针,此地址赋值给int指针变量p,通过指针p,即绕过了编译器的const常量约束检查,成功地在运行时实现对const的修改。
  • 真正的只读常量,编译后存储在目标文件的.ro.data区。

1.4 目标文件的存储安排

之所以称const变量为“伪常量”,不仅是因为其根据的是编译器的编译时诊断检查来实现常量效果,而且const变量的存储区域和普通变量别无二致。全局变量存储在.data段或记录在.bss段,局部变量则在程序的临时栈中管理。

编译过后生成的目标文件中,包含以下几个与变量相关的存储段:

  • .text
    • 代码段
    • 存储代码以及立即数等数据,只读常量。
  • .ro.data
    • 只读(readonly)段 - 数据区
    • 真正的常量,如:字符串常量,存储在目标文件的.ro.data 区,此区域是真正的只读常量
  • .data
    • 数据段
    • 存储已初始化为且值为非0的全局变量。
  • .bss
    • BSS段(Block Started by Symbol
    • 记录未初始化的全局变量。此段的变量将被默认初始化为0。此部分仅仅是占位符,数据不占用实际的磁盘空间。

2 volatile

volatile声明易变常量,即告知编译器该变量可能被代码以外的因素改变,如:中断服务例程(ISR),多线程中的异线程修改,或硬件修改等。

volatile用于强制某个实现屏蔽可能的优化。例如,对于具有内存映像输入/输出的机器,指向设备寄存器的指针可以声明为指向volatile的指针,目的是防止编译器通过指针删除明显多于的引用。

(Kernighan&Ritchie 《The C Programming Language(2nd Edition)》)

  • 编译器遇到volatile变量,将不会对其做编译优化,以避免出现问题。