C语言变量分类、作用域、链接属性与存储期笔记
- C语言进阶
- 1天前
- 69热度
- 0评论
先看整体
变量总表
| 写法 / 对象 | 定义位置 | 作用域 | 存储期 | 链接属性 | 常见用途 |
|---|---|---|---|---|---|
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 个问题
├─ 哪里能用? -> 看作用域
├─ 活多久? -> 看生命周期
└─ 能否跨文件访问? -> 看链接属性
一、先分清:分类和属性不是一回事
最容易混的地方就在这里。
- 全局 / 局部 说的是变量怎么分类
- 作用域 / 生命周期 / 链接属性 说的是变量有哪些属性
这两个层次不分开,后面看 static 和 extern 就很容易绕进去。
1. 按作用域分类
局部变量
局部变量定义在函数内部或者代码块内部,只能在当前代码块里使用。不初始化时,它的值是不确定的。
void fun(void) {
int x = 10;
}
用途:保存当前函数里的临时数据,不让外部逻辑随意碰到它。
全局变量
全局变量定义在所有函数外部,从定义处开始,到当前文件结束都可见。没有显式初始化时,默认值是 0。
int g_count = 0;
用途:让多个函数共享同一份数据。
static,那它仍然是全局变量,但只能在当前文件里使用。
2. 按存储类别分类
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. 再看三个核心属性
前面说"全局 / 局部"是在讲分类,这里说的"作用域、存储期、链接属性"是在讲名字或对象本身的属性。后面分析 static 和 extern,判断依据也主要落在这三个点上。
作用域
作用域回答的是:这个名字在哪里能用。
- 局部变量、形参是块作用域
- 全局变量是文件作用域
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; // 无链接
}
二、static 和 extern 到底在改什么
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 主要让名字收起来
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. 声明和定义
前面把 static 和 extern 分开以后,这里再看声明和定义就会顺很多,因为 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. 一组真正实用的跨文件例子
下面这个例子把头文件、extern、static、变量和函数放到一起,基本能把跨文件使用讲清楚。
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 全局变量修改的是作用域属性。
**不严谨,通常应判为错。** 更准确地说,它主要修改的是链接属性,让名字从外部链接变成内部链接;它仍然具有文件作用域。