C语言变量分类、作用域、链接属性与存储期笔记

先看整体

变量总表

写法 / 对象 定义位置 作用域 存储期 链接属性 常见用途
int a;(局部) 函数内 块作用域 自动存储期 无链接 保存函数内部临时数据
static int a;(局部) 函数内 块作用域 静态存储期 无链接 多次调用之间保留值
int a;(全局) 函数外 文件作用域 静态存储期 外部链接 多个函数 / 文件共享数据
static int a;(全局) 函数外 文件作用域 静态存储期 内部链接 只在当前文件内使用
extern int a; 其他文件或头文件中 取决于原定义 不创建新对象 表示引用外部名字 声明"变量在别处定义"
register int i; 函数内 块作用域 自动存储期 无链接 建议编译器优先放寄存器

函数补充表

写法 / 对象 可见性 链接属性 常见用途
void fun(void); / void fun(void) {} 可被其他文件看到 外部链接(默认) 对外提供功能
static void fun(void); 仅当前 .c 文件可见 内部链接 隐藏模块内部辅助函数
extern void fun(void); 声明函数在别处定义 外部链接 跨文件声明函数
⚠️ 最容易混淆的地方static 局部变量和 static 全局变量虽然都写作 static,但它们解决的不是同一个问题。
变量 / 函数名字
├─ 一、先看分类
│  ├─ 按作用域分
│  │  ├─ 局部变量
│  │  └─ 全局变量
│  └─ 按存储类别分
│     ├─ auto
│     ├─ register
│     ├─ static
│     └─ extern
│
├─ 二、再看属性
│  ├─ 作用域:名字在哪里能用
│  ├─ 生命周期:对象活多久
│  └─ 链接属性:能不能跨文件找到
│
├─ 三、重点关键字
│  ├─ static
│  │  ├─ 修饰局部变量 -> 保留值
│  │  ├─ 修饰全局变量 -> 变内部链接
│  │  └─ 修饰函数     -> 只限当前文件
│  └─ extern
│     ├─ 声明外部变量
│     └─ 声明外部函数
│
└─ 四、最后判断 3 个问题
   ├─ 哪里能用?       -> 看作用域
   ├─ 活多久?         -> 看生命周期
   └─ 能否跨文件访问? -> 看链接属性

一、先分清:分类和属性不是一回事

最容易混的地方就在这里。

  • 全局 / 局部 说的是变量怎么分类
  • 作用域 / 生命周期 / 链接属性 说的是变量有哪些属性

这两个层次不分开,后面看 staticextern 就很容易绕进去。

1. 按作用域分类

ℹ️ 补充:更严谨地说,C 里常一起讨论的是作用域、存储期和链接这几个概念。不过落实到普通变量,最常见、也最重要的还是块作用域文件作用域

局部变量

局部变量定义在函数内部或者代码块内部,只能在当前代码块里使用。不初始化时,它的值是不确定的。

void fun(void) {
    int x = 10;
}

用途:保存当前函数里的临时数据,不让外部逻辑随意碰到它。

全局变量

全局变量定义在所有函数外部,从定义处开始,到当前文件结束都可见。没有显式初始化时,默认值是 0

int g_count = 0;

用途:让多个函数共享同一份数据。

⚠️ 注意全局变量不等于一定能跨文件访问。如果这个全局变量带了 static,那它仍然是全局变量,但只能在当前文件里使用。

2. 按存储类别分类

ℹ️ 补充:作用域和链接描述的是名字在程序中的可见范围,存储期描述的是对象的生存时间。按 C 标准,对象的存储期包括自动存储期、静态存储期、线程存储期和分配存储期。这篇笔记主要讨论普通变量以及 static / extern,重点放在最常见的自动存储期静态存储期

auto

auto 是默认局部变量,进入代码块时创建,离开代码块时销毁。

void fun(void) {
    auto int x = 1;
}

实际开发里几乎不会特地把 auto 写出来,省略就可以。

register

register 建议编译器把变量放到寄存器里,目的是让高频访问更快一些,但不能对这种变量取地址。

void fun(void) {
    register int i;
}

这只是建议,不是强制要求。现代编译器自己的优化能力已经很强,register 的实际存在感并不高。

static

static 的作用可以先粗略记成两种:

  • 让对象"保留下来"
  • 让名字"收起来"

前者常见于局部变量,后者常见于全局变量和函数。后面会单独展开。

extern

extern 的核心意思:告诉编译器"这个名字在别处"

extern int g_count;
extern void inc(void);

最常见的用途是跨文件共享变量或者声明函数。

3. 再看三个核心属性

前面说"全局 / 局部"是在讲分类,这里说的"作用域、存储期、链接属性"是在讲名字或对象本身的属性。后面分析 staticextern,判断依据也主要落在这三个点上。

作用域

作用域回答的是:这个名字在哪里能用。

  • 局部变量、形参是块作用域
  • 全局变量是文件作用域
int g = 1;      // 文件作用域

void fun(int p)
{
    int x = 0;  // x、p 都是块作用域
}

存储期

存储期回答的是:这个对象从什么时候开始存在,到什么时候结束。 日常讲解里很多资料会把它说成"生命周期",这样理解没问题;更准确的说法还是"存储期"。

  • 普通局部变量是自动存储期
  • 全局变量和 static 变量是静态存储期
void fun(void) {
    int a = 1;         // 每次进入函数重新创建
    static int b = 1;  // 程序运行期间一直存在
}

链接属性

链接属性回答的是:这个名字能不能被别的文件找到。

  • 普通全局变量、普通函数:外部链接
  • static 全局变量、static 函数:内部链接
  • 局部变量、形参:无链接
int g = 0;            // 外部链接
static int s = 0;     // 内部链接

void fun(int p) {
    int x = 0;        // 无链接
}
ℹ️ 补充链接属性不只适用于变量,也适用于函数

二、staticextern 到底在改什么

1. static

static 是 C 语言里最容易让人误会的关键字之一,因为它出现在不同位置时,作用并不一样。

前面已经把"作用域、存储期、链接属性"这三个判断维度拆开了,所以这里直接顺着这个思路看:static 放在不同位置,改动的重点也不同。

static 局部变量

static 用在局部变量上时,主要改的是存储期

void fun(void) {
    static int count = 0;
    count++;
}

这里的 count 仍然只能在 fun 里访问,所以它不是全局变量;但它不会随着函数执行结束而销毁,而是会一直保留到程序结束。

适合处理"函数多次调用之间需要记住上一次状态"的场景。和全局变量相比,好处是封装性更强,只有当前函数能访问它。

static 全局变量

static 用在全局变量上时,真正改变的重点不是"作用域",而是链接属性

static int cache = 0;

普通全局变量默认是外部链接,其他文件可以通过 extern 引用它;一旦加上 static,它就变成内部链接,只能在当前文件中使用。

所以,static 全局变量更像"文件私有变量"

static 函数

static 也可以修饰函数。

static void helper(void) {
}

static 全局变量的思路一样:让名字只留在当前 .c 文件中。适合放模块内部的辅助函数,不希望被外部直接调用。

💡 一句话总结 static

修饰局部变量时,static 主要让值保留下来

修饰全局变量或函数时,static 主要让名字收起来

2. extern

extern 的方向和 static 正好相反。它不是把名字收起来,而是在告诉编译器:这个名字定义在别处。

extern 本身并不改变对象的存储期;它更像是在补一个声明,告诉编译器这个名字应该去别处找。

从编译流程来看,extern 先在编译阶段告诉编译器"先别报错,这个名字别处有";到了链接阶段,链接器再去把它们真正对应起来。

extern 声明变量

extern int g_count;

表示变量 g_count 不在当前文件定义,而是在别处定义。常常写在头文件或者其他 .c 文件里,用来实现多个文件共享同一变量。

extern 声明函数

extern void inc(void);

表示函数 inc 在别处定义。因为普通函数默认就是外部链接,所以函数声明前面的 extern 经常被省略:

void inc(void);

大多数场景下,这两种写法效果一致。

一个特别容易考的点

extern int a;       // 声明,不是定义
extern int a = 10;  // 定义,因为带了初始化

这是很多人第一次看到时最容易误会的地方。


三、声明、定义和跨文件使用

1. 声明和定义

前面把 staticextern 分开以后,这里再看声明和定义就会顺很多,因为 extern 本来就经常和"声明但不定义"一起出现。

变量的声明与定义

int g;          // 定义(文件作用域下教材里通常视为定义)
extern int g;   // 声明
extern int g2;  // 声明
int g2 = 10;    // 定义
  • 声明:告诉编译器"有这个名字"
  • 定义:真正创建对象,分配存储空间

函数的声明与定义

void show(void);      // 声明
void show(void) { }   // 定义

函数声明只是告诉编译器这个函数存在,函数定义才提供函数体。

2. 为什么 extern int a = 10; 容易让人误解

extern int a;       // 声明
extern int a = 10;  // 定义

问题在于:它表面上带了 extern,看起来像"在引用别处的名字",但实际上它已经在当前文件里把变量 a 定义出来,并且初始化成 10 了。

所以,这句代码不是在初始化别的文件里的变量 a,而是在当前文件里定义变量 a

更直白地说:

  • 语法上,这种写法没有问题
  • 实际开发中,这种写法意义不大
  • 它唯一的含义是:这里在定义一个变量,并且还想强调它具有外部链接

但普通全局变量默认就是外部链接,所以直接写:

int a = 10;

就够了。

💡 判断技巧:以后看到 extern,先别急着下结论。先问一句:这里是在声明一个外部名字,还是已经在定义对象?

3. 一组真正实用的跨文件例子

下面这个例子把头文件、externstatic、变量和函数放到一起,基本能把跨文件使用讲清楚。

counter.h

#ifndef COUNTER_H
#define COUNTER_H

extern int g_count;
void inc(void);
void print_count(void);

#endif

counter.c

#include <stdio.h>
#include "counter.h"

int g_count = 0;   // 定义:外部链接变量

static void log_change(void) {   // 文件私有函数
    printf("count = %d\n", g_count);
}

void inc(void) {
    g_count++;
    log_change();
}

void print_count(void) {
    printf("%d\n", g_count);
}

main.c

#include "counter.h"

int main(void) {
    inc();
    print_count();
    return 0;
}

这组代码里有三层意思:

  • g_count 定义在 counter.c,通过头文件里的 extern int g_count; 暴露给外部文件
  • inc()print_count() 是普通函数,默认外部链接,可以给别的文件调用
  • log_change() 带了 static,只在 counter.c 内部可见,用来隐藏模块内部细节
💡 实际开发中的组织方式

.h 放声明,.c 放定义

对外暴露的接口放头文件里

不希望外部碰到的细节,用 static 收在当前文件内

四、几个高频误区

1. 全局变量不一定能跨文件

static int g = 10;

它是全局变量,但不是外部链接变量,所以别的文件拿不到它。

2. static 局部变量不是全局变量

void fun(void) {
    static int x = 0;
}

这里的 x 只是生命周期变长了,作用域并没有扩大,依然只能在 fun 内部用。

3. extern 通常是声明,但不是绝对永远如此

extern int a;       // 通常只是声明
extern int a = 10;  // 这里就是定义

4. 普通函数默认就是外部链接

extern void show(void);
void show(void);

很多时候效果一样,所以函数声明里经常不写 extern

5. register 不等于一定更快

它只是对编译器的建议。现代编译器通常会自己做更合理的优化。

6. 文件作用域不等于从文件第一行开始都能直接用

全局变量在文件中的可见范围,通常是从声明或定义出现的位置开始,到文件结束为止。

printf("%d", g); // 如果前面还没声明 g,就不能直接用
int g = 10;

📋 速记

  • 这个名字哪里能用? → 看作用域
  • 这个对象的存储期是什么? → 看生命周期
  • 这个名字能不能被别的文件找到? → 看链接属性

常见问题

Q:为什么普通全局变量可以被别的文件用 extern 引用?

因为普通全局变量默认具有外部链接。外部链接意味着这个名字可以被其他源文件看到,所以别的文件可以用 extern 声明它。

Q:static 局部变量和普通局部变量的区别是什么?

两者的作用域都还是局部,都只能在当前函数或代码块中使用。区别在生命周期:普通局部变量是自动存储期,函数结束后销毁;static 局部变量是静态存储期,程序运行期间一直存在。

Q:static 局部变量和全局变量相比,有什么优点?

它既能保留上一次调用的值,又不会像全局变量那样被其他函数直接访问或修改,所以封装性更好

Q:static 全局变量和普通全局变量的核心区别是什么?

核心区别是链接属性:普通全局变量是外部链接,可以跨文件访问;static 全局变量是内部链接,只能在当前文件访问。

Q:static 能不能修饰函数?修饰后表示什么?

能。static 修饰函数后,函数变成内部链接,只能在当前 .c 文件中使用,适合做模块内部辅助函数。

Q:extern 能不能修饰函数?为什么很多时候看不到它?

能。因为普通函数默认就是外部链接,所以函数声明前面的 extern 往往可以省略。

Q:extern int a;extern int a = 10; 有什么区别?

extern int a; 通常是声明,表示 a 在别处定义;extern int a = 10; 是定义,因为它带了初始化。

Q:extern int a = 10; 的真正含义是什么?

它不是"初始化别的文件中的变量 a",而是在当前文件里定义变量 a,并把它初始化为 10。只是因为写法上带了 extern,所以很容易让人误以为它只是声明。

Q:局部变量为什么没有链接属性?

因为局部变量的名字只在自己的代码块里有效,别的文件或别的作用域都不能通过名字去引用它,所以它属于无链接

Q:"全局变量"和"能跨文件访问"是不是一回事?

不是。全局变量只说明它定义在函数外部;能不能跨文件访问还要看它的链接属性。普通全局变量可以,static 全局变量不可以。

Q:如果一个名字要跨文件使用,至少要满足什么条件?

通常要满足两点:第一,这个名字本身具有外部链接;第二,使用它的文件里要有对应的声明,通常通过头文件提供。

Q:为什么说 static 是"保留下来 / 收起来",而 extern 是"名字在别处"?

static 修饰局部变量时,是把值保留下来;修饰全局变量或函数时,是把名字收起来,限制在当前文件内。extern 的核心不是创建对象,而是告诉编译器:这个名字在别处已经有了。

Q:判断:static 局部变量是全局变量。

**错。** 它只是生命周期变成静态存储期,但作用域仍然是局部。

Q:判断:普通函数默认没有链接属性。

**错。** 普通函数默认具有外部链接。

Q:判断:static 全局变量修改的是作用域属性。

**不严谨,通常应判为错。** 更准确地说,它主要修改的是链接属性,让名字从外部链接变成内部链接;它仍然具有文件作用域。