引言
在C语言中,static、const和volatile是三个非常重要的关键字。它们虽然简单,但理解不透彻很容易用错。本文将用最简洁的方式,带你彻底搞懂这三个关键字的用法和区别。
一、static关键字
1.1 static的作用
static关键字有三个核心作用:
-
隐藏:修饰全局变量/函数时,限制作用域在当前文件
-
持久:修饰局部变量时,延长生命周期到整个程序运行期
-
默认初始化:静态变量自动初始化为0
1.2 static修饰局部变量
作用:变量在函数退出后值不丢失,下次进入函数时继续使用
#include <stdio.h>
void count() {
static int num = 0; // 只初始化一次
num++;
printf("%d ", num);
}
int main() {
count(); // 输出 1
count(); // 输出 2
count(); // 输出 3
return 0;
}
普通局部变量 vs static局部变量:
-
普通局部变量:存储在栈区,函数结束即销毁
-
static局部变量:存储在静态数据区,程序结束才销毁
1.3 static修饰全局变量
作用:变量只能在当前文件中访问,其他文件不可见
// file1.c
static int a = 10; // 只在file1.c有效
void func1() {
a = 20; // 可以访问
}
// file2.c
extern int a; // 错误!找不到a
void func2() {
a = 30; // 编译错误
}
1.4 static修饰函数
作用:函数只能在当前文件中调用,其他文件不可见
// file1.c
static void helper() { // 私有函数
printf("helper\n");
}
void public_func() { // 公开函数
helper(); // 可以调用
}
// file2.c
extern void public_func(); // 可以
extern void helper(); // 错误!找不到
二、const关键字
2.1 const的作用
const用于定义只读变量,一旦初始化就不能修改。编译器会检查所有试图修改const变量的操作并报错。
2.2 const修饰基本变量
const int a = 10; // a不能被修改
// a = 20; // 错误!
int b = 10;
const int* p = &b; // 指向的内容不能改
2.3 const与指针(两种核心概念)
常量指针与指针常量是两个完全不同的概念,区分它们的关键看const在*的哪一侧:
概念一:常量指针(pointer to const)
定义:指向常量的指针,指针指向的内容不可修改
const int* p; // const在*左边,指向的内容只读
int const* p; // 同上,两种写法等价
int a = 10, b = 20;
const int* p = &a;
// *p = 100; // 错误!不能通过p修改a的值
p = &b; // 正确!p本身可以指向其他地方
printf("%d", *p); // 可以读取值
概念二:指针常量(const pointer)
定义:指针本身是常量,一旦指向某个变量就不能再指向其他地方
int* const p = &a; // const在*右边,指针本身只读
*p = 100; // 正确!可以通过p修改a的值
// p = &b; // 错误!p不能再指向其他地方
概念三:双重const
定义:指针本身和指向的内容都不可修改
const int* const p = &a; // 两边都有const
// *p = 100; // 错误!内容只读
// p = &b; // 错误!指针本身只读
口诀:号前面找const,指向内容不让动;号后面找const,指针本身不变更
2.4 const修饰函数参数
作用:保护传入的参数不被函数修改
void print(const char* str) {
// str[0] = 'A'; // 错误!不能修改
printf("%s", str);
}
int main() {
char s[] = "hello";
print(s); // 可以传入普通变量
print("world"); // 也可以传入常量
return 0;
}
2.5 const修饰全局变量
const int MAX = 100; // 全局常量
const char* MSG = "OK"; // 字符串常量
int main() {
// MAX = 200; // 错误!
return 0;
}
三、volatile关键字
3.1 volatile的作用
volatile告诉编译器:这个变量的值可能会被意想不到地改变,每次使用都必须从内存中重新读取,禁止编译器优化到寄存器中。
3.2 变量存储位置与优化问题
正常情况下:
-
变量可以存储在CPU寄存器或内存中
-
编译器为了效率,会将频繁访问的变量加载到寄存器中
-
后续操作直接读写寄存器,不再访问内存
这样做的问题:
-
如果变量在寄存器中的值没有写回内存,其他途径修改了内存中的值,就会产生不一致
-
程序感知不到变量的变化,导致逻辑错误
3.3 不使用volatile的后果
// 示例:等待中断修改flag
int flag = 0; // 普通变量
void wait_for_flag() {
while(flag == 0) {
// 等待flag变为1
}
}
编译器可能做的优化:
-
第一次读取flag到寄存器(假设值为0)
-
判断寄存器值是否为0,成立则进入循环
-
后续循环不再读取内存,直接判断寄存器
-
即使中断修改了内存中的flag,寄存器值仍是0
-
程序陷入死循环,永远看不到flag的变化
3.4 使用volatile后的变化
volatile int flag = 0; // 告诉编译器不要优化
void wait_for_flag() {
while(flag == 0) {
// 每次循环都从内存读取flag的最新值
}
}
编译器行为:
-
不会将flag加载到寄存器中缓存
-
每次访问flag都直接从内存地址读取
-
能及时感知到中断或其他线程对flag的修改
-
虽然速度稍慢,但保证了正确性
3.5 volatile与寄存器/内存的关系
| 对比项 | 普通变量 | volatile变量 |
|---|---|---|
| 存储位置 | 可能缓存在寄存器 | 始终在内存中操作 |
| 访问频率 | 频繁访问用寄存器 | 每次都用内存 |
| 读取时机 | 首次读取后可能不再读 | 每次都重新读取 |
| 性能 | 高 | 较低(内存访问慢于寄存器) |
| 正确性 | 可能感知不到变化 | 总能感知变化 |
3.6 什么时候用volatile
三种典型场景
1.被中断程序改变的变量
volatile int interrupt_flag = 0;
void interrupt_handler() {
interrupt_flag = 1; // 中断中修改
}
void main_loop() {
while(!interrupt_flag) {
// 等待中断
}
}
2.多线程共享的变量
volatile int thread_running = 1;
void thread_func() {
while(thread_running) {
// 线程工作
}
}
// 主线程可以修改thread_running来停止子线程
C语言标准本身并不直接支持多线程(这是由操作系统或第三方库提供的),但在多线程程序中,使用volatile可以确保一个线程对共享变量的修改能够立即对其他线程可见。然而,volatile并不保证操作的原子性,也不提供互斥访问,因此在多线程程序中通常需要与适当的同步机制(如互斥锁,信号量)结合使用。
volatile在多线程中的真实作用:
-
能做的:保证变量的可见性,防止编译器优化
-
不能做的:保证操作的原子性,防止指令重排
-
适用范围:简单的标志变量(一个线程只写,一个线程只读)
多线程编程的正确姿势:
-
简单标志 → volatile(谨慎使用)
-
计数器 → 原子操作
-
复杂数据 → 互斥锁
-
既要性能又要安全 → 读写锁、无锁数据结构
3.硬件寄存器
#define GPIO_STATUS ((volatile uint32_t*)0x40020000)
// 硬件可能随时改变这个寄存器的值
uint32_t status = *GPIO_STATUS; // 每次都要重新读取
四、三关键字对比
4.1 核心区别
| 关键字 | 核心作用 | 影响对象 | 使用场景 |
|---|---|---|---|
| static | 隐藏/持久 | 作用域、生命周期 | 计数器、私有函数 |
| const | 只读保护 | 变量的可写性 | 常量、参数保护 |
| volatile | 防优化 | 变量的访问方式 | 中断、多线程、硬件 |
4.2 修饰对象对比
| 修饰对象 | static | const | volatile |
|---|---|---|---|
| 局部变量 | 值持久化 | 只读 | 防优化(每次读内存) |
| 全局变量 | 文件私有 | 只读 | 防优化(每次读内存) |
| 函数 | 文件私有 | 返回值只读 | 不支持 |
| 指针 | 不支持 | 指针/内容只读 | 指向的内容防优化 |
| 参数 | 不支持 | 参数只读 | 参数防优化 |
五、一句话总结
-
static:让局部变量"长生不老",让全局变量和函数"隐姓埋名"
-
const:给变量贴上"只读"标签,并区分是"指向的内容不能动"还是"指针本身不能动"
-
volatile:告诉编译器"别把变量放到寄存器缓存,每次都去内存读取最新值"
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_46716828/article/details/159240994



