C 语言提供四个类型限定符:const、volatile、restrict 和 atomic。它们影响变量的存储方式、访问权限和编译器优化行为。本文详细介绍每个限定符的原理与使用场景。
const - 只读限定
const 告诉编译器该变量的值不可被修改,帮助捕获意外的修改并生成更优化的代码:
/* 基本用法 */
const int MAX = 100;
int const ARR[10] = {1, 2, 3}; /* const 放在类型后也可以 */
/* 指针相关的 const 位置非常重要 */
const int *p1; /* p1 指向 const int,不能通过 p1 修改值 */
int * const p2; /* p2 是 const 指针,指针本身不可变 */
const int * const p3; /* 指针和值都不可变 */
/* 更好的写法:在 * 两侧都加 const */
int const *p4; /* 等价于 const int *p4,语义更清晰 */
const 不是常量
const 变量只是要求编译器阻止直接修改,它仍然占用内存存储。与#define 宏不同,const 有地址,可以通过指针绕过限制。
const 与函数参数
在函数参数中使用 const 可以表达接口的不变性:
/* 表示函数不会修改字符串内容 */
size_t my_strlen(const char *s) {
size_t len = 0;
while (s[len] != '\0') len++;
return len;
}
/* 表示函数不会修改数组内容 */
int sum_array(const int *arr, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += arr[i];
return total;
}
/* 传递大型结构时使用 const 避免复制 */
void print_point(const Point *pt) {
printf("(%d, %d)\n", pt->x, pt->y);
}
volatile - 防止优化
volatile 告诉编译器该变量可能被意外修改,每次访问必须从内存读取:
/* 硬件寄存器 */
volatile uint32_t *reg = (volatile uint32_t *)0x40021000;
*reg = 0x01; /* 写入寄存器 */
while (!(*reg & 0x01)); /* 等待标志位 */
/* 多线程共享变量 */
volatile bool flag = false;
/* 线程 1 */
void *thread1(void *arg) {
/* 等待 flag 变为 true */
while (!flag) { /* 必须每次检查内存 */
sched_yield();
}
/* 处理数据 */
}
/* 注意:volatile 不保证原子性,需要原子操作 */
volatile 的误用
volatile 不能替代多线程同步机制。它只阻止编译器优化访问,但不能阻止 CPU 指令重排或硬件缓存。在多线程场景下应使用 atomic、mutex 等同步原语。
restrict - 指针别名优化
restrict 告诉编译器指定指针是访问对象的唯一方式,允许更激进的优化:
/* 没有 restrict,编译器必须考虑指针别名 */
void process(int *a, int *b, size_t n) {
for (size_t i = 0; i < n; i++) {
a[i] += b[i]; /* a 和 b 可能指向同一内存 */
}
}
/* 使用 restrict,编译器可以优化 */
void process(int *restrict a, int *restrict b, size_t n) {
for (size_t i = 0; i < n; i++) {
a[i] += b[i]; /* 可以将循环向量化 */
}
}
/* memcpy 使用 restrict 确保不重叠 */
void *memcpy(void *restrict dest,
const void *restrict src,
size_t n);
/* 违反 restrict 的行为是未定义的 */
int arr[10] = {0};
process(arr, arr + 1, 9); /* 未定义行为 */
_Atomic - 原子操作
C11 引入的 _Atomic 类型限定符提供原子操作,避免数据竞争:
/* 基本原子类型 */
_Atomic int counter = 0;
_Atomic bool ready = false;
/* 原子增加(等效于 mutex+操作)*/
atomic_fetch_add(&counter, 1);
/* 原子比较交换 */
int expected = counter;
while (!atomic_compare_exchange_weak(&counter, &expected, expected + 1)) {
/* counter 被其他线程修改,重新尝试 */
}
/* 线程安全的标志位检查 */
void wait_until_ready(_Atomic bool *flag) {
while (!atomic_load(flag)) {
sched_yield();
}
}
/* 原子标志位(更高效的自旋锁)*/
_Atomic flag lock = ATOMIC_FLAG_INIT;
void lock_acquire() {
while (atomic_flag_test_and_set(&lock)) {
sched_yield();
}
}
void lock_release() {
atomic_flag_clear(&lock);
}
原子操作的选择
对于简单计数器、标志位等,优先使用原子类型。对于复杂操作(如链表操作),仍需要锁(mutex)保护。原子操作不是万能的,需要谨慎设计。
限定符组合使用
多个限定符可以组合使用,表达更精确的语义:
/* 只读且不会意外修改的值(适合硬件寄存器)*/
static const volatile uint32_t CLOCK_FREQ =
(volatile uint32_t *)0x40021000;
/* 多线程共享的只读数据 */
const _Atomic int CONFIG_VALUE;
/* 函数参数:不会修改且无别名(用于优化)*/
void compute(const int *restrict input,
int *restrict output,
size_t n);
/* volatile 和 atomic 的区别 */
volatile _Atomic int status; /* 原子且每次读取最新值 */
总结
- const:声明只读变量,帮助编译器优化和捕获意外修改
- volatile:防止编译器优化每次从内存读取,适合硬件寄存器
- restrict:声明指针无别名,允许更激进的优化
- _Atomic:提供原子操作,避免多线程数据竞争
正确使用类型限定符可以同时提升程序正确性和性能。