C 语言存储类型

内容简介

存储类型说明符用于描述对象或函数的存储期、作用域和链接属性。C 语言常见相关关键字包括 autostaticexternregistervolatile 严格说不是存储类型说明符,而是类型限定符,但它经常和全局变量、寄存器、中断、并发可见性一起讨论,所以放在本篇补充。

核心问题:

  • 变量活多久:自动存储期、静态存储期、动态存储期。
  • 名字在哪里可见:块作用域、文件作用域。
  • 跨文件是否同一个实体:外部链接、内部链接、无链接。
  • 编译器能不能缓存访问结果:volatile 影响优化假设。

用法

auto

局部变量默认就是 auto,表示自动存储期。现代 C 代码几乎不会显式写它。

void func(void)
{
    auto int value = 10;  // 合法但不推荐,等价于 int value = 10
}

value 在进入代码块时创建,离开代码块时生命周期结束。

static 局部变量

int next_id(void)
{
    static int id = 0;
    id++;
    return id;
}

局部 static 的名字只在函数块内可见,但对象拥有静态存储期:只初始化一次,函数返回后仍然保留值。

static 文件作用域变量和函数

static int counter = 0;
 
static void helper(void)
{
    counter++;
}

文件作用域的 static 表示内部链接:这个名字只在当前 .c 文件内可见,适合隐藏模块内部状态和辅助函数。

extern 声明外部实体

extern 表示“这个名字的定义在别处,这里只是声明”。

// config.h
#ifndef CONFIG_H
#define CONFIG_H
 
extern int global_config;
 
#endif
// config.c
#include "config.h"
 
int global_config = 100;
// main.c
#include "config.h"
 
int main(void)
{
    return global_config;
}

函数声明默认具有外部链接,通常不需要显式写 extern

int add(int a, int b);      // 常见写法
extern int add(int a, int b); // 也合法,但少见

register

register 是给编译器的历史性建议,表示希望变量放在寄存器中。现代编译器通常不需要这个提示。

void func(void)
{
    register int i = 0;
    /* 不能对 register 变量取地址 */
}

volatile

volatile 告诉编译器:这个对象的值可能被当前代码之外的因素改变,每次访问都要按源代码语义真的读写。

常见场景:

  • 内存映射硬件寄存器。
  • 中断服务程序和主循环共享的简单标志。
  • 信号处理函数修改的 sig_atomic_t 标志。
volatile unsigned int * const UART_STATUS = (volatile unsigned int*)0x40000000u;
 
while ((*UART_STATUS & 1u) == 0u) {
    /* 等待硬件状态变化 */
}

中断标志示意:

static volatile int g_ready = 0;
 
void interrupt_handler(void)
{
    g_ready = 1;
}
 
int main(void)
{
    while (!g_ready) {
        /* wait */
    }
    return 0;
}

volatile 不等于线程安全,不提供原子性、互斥、内存顺序,也不能修复数据竞争。

最佳代码实践

  • 能限制在当前 .c 文件内的全局变量和辅助函数,用 static 限制可见性。
  • 跨文件共享变量时,声明放在 .h,唯一的定义放在一个 .c
  • 少用可写全局变量;需要共享状态时优先封装访问函数。
  • extern 放声明,不要在头文件里定义普通全局变量。
  • volatile 只用于确实会被“当前执行流之外”改变的对象,不要把它当并发同步工具。
  • C++ 的 auto 是类型推导,和 C 的 auto 存储类型不是一回事。

错误用法

// a.h
int global_count = 0;  // 错误:头文件中定义全局变量,多个 .c include 后会重复定义

正确做法:

// a.h
extern int global_count;
 
// a.c
int global_count = 0;
extern int value = 10;  // 错误倾向:带初始化就是定义,不是普通 extern 声明
volatile int done = 0;
 
/* 错误倾向:多线程用 done 当同步变量。volatile 不保证原子性和内存顺序。 */
static int global_state;  // 错误倾向:滥用全局状态,导致模块难测试、难复用

注意事项

  • 文件作用域对象如果没有显式初始化,会被零初始化。
  • 局部自动变量如果没有初始化,读取它是未定义行为。
  • const 描述只读语义,不等同于存储类型;文件作用域 const 在 C 和 C++ 中链接规则也不完全一样。
  • volatile const 常见于只读硬件寄存器:程序不能写,但硬件可能改变它。
  • 动态存储期来自 malloc/free,不由这些存储类型关键字直接控制。

相关:C语言知识地图C语言系统栈与过程调用CandCppconstCandCpp底层知识

学习路径