C 变量声明与作用域
变量是程序里用来装数据的"盒子"。C 是一门静态强类型语言,每个变量都必须有明确的类型,编译器在编译期检查类型是否匹配。本篇带你搞懂 C 变量的声明、初始化、作用域、生命周期,以及一个让新手困惑的关键字:static。
一、变量声明与初始化
C 里声明变量的基本格式是 类型 变量名;,也可以同时赋初值:
#include <stdio.h>
int main(void) {
int age; // 声明(未初始化,值不确定!)
int score = 95; // 声明并初始化
int a = 1, b = 2, c = 3; // 同时声明多个
age = 20; // 之后赋值
printf("age=%d score=%d\n", age, score);
// C99 起支持指定初始化(数组/结构体常用)
// 也支持在 for 循环里声明(C99)
for (int i = 0; i < 3; i++) {
printf("%d ", i); // 0 1 2
}
printf("\n");
return 0;
}几个要点:
int age;:只声明,不初始化。int score = 95;:声明的同时初始化。int a = 1, b = 2;:一行声明多个,用逗号分隔。- C99 起,可以在
for循环里直接声明循环变量(旧 C89 不行,必须声明在循环外)。
二、未初始化变量的陷阱
这是 C 区别于 Java/Go 的一个重要陷阱:局部变量如果声明后不初始化,它的值是内存里的随机垃圾,读取它是未定义行为(Undefined Behavior)——可能崩溃、可能输出乱七八糟的数,而且每次运行结果都不一样。
#include <stdio.h>
int main(void) {
int a; // 未初始化:值是内存里的随机垃圾!
printf("a = %d\n", a); // 每次运行结果都不一样,这是未定义行为
// 全局/静态变量会自动初始化为 0(下文讲)
// 局部变量不会!必须手动初始化
int b = 0; // 好习惯:声明时就初始化
printf("b = %d\n", b); // b = 0
return 0;
}因此养成好习惯:声明变量时就立即初始化(哪怕初始化为 0),能避免 80% 的诡异 bug。
三、局部变量 vs 全局变量
按定义位置不同,C 变量分为两大类:
- 局部变量:在函数(或代码块)内部定义。只在所属函数/块内有效,函数返回后销毁。不会自动初始化。
- 全局变量:在所有函数之外定义。从定义处到文件结尾都可见,自动初始化为 0。整个程序运行期都存在。
#include <stdio.h>
int global_x = 100; // 全局变量:所有函数都能访问
void func(void) {
int local_a = 10; // 局部变量:只在 func 内有效
printf("global_x=%d, local_a=%d\n", global_x, local_a);
}
int main(void) {
int local_a = 999; // 这个 local_a 和 func 里的不是同一个
func(); // 打印 global_x=100, local_a=10
printf("main local_a=%d\n", local_a); // 999
{
int block_var = 50; // 块级变量:出了这对花括号就消失
printf("block=%d\n", block_var);
}
// printf("%d", block_var); // 报错:block_var 在这里不可见
return 0;
}四、作用域规则
作用域指的是变量"在哪些代码范围内能被访问"。C 的作用域由花括号决定,分三层:
- 文件作用域(全局):在所有函数外定义,从定义处到文件结尾可见。
- 函数作用域:只有标签(goto 用的)是函数作用域,变量不是。
- 块作用域:在
{}内定义的变量,出了这对花括号就不可见。包括if、for、while的花括号。
规则:内层可以访问外层变量,外层不能访问内层。如果内外层同名,内层会遮蔽(shadow)外层变量:
#include <stdio.h>
int x = 1; // 外层 x
int main(void) {
int x = 2; // 内层 x 遮蔽了外层 x
printf("x = %d\n", x); // 2(用的是内层)
{
int x = 3; // 更内层的 x
printf("x = %d\n", x); // 3
}
printf("x = %d\n", x); // 2(回到内层)
// C 没有 :: 全局解析符(C++ 才有)
// 一旦被遮蔽,本作用域内无法访问外层同名变量
return 0;
}新手要尽量避免重名,免得 debug 到怀疑人生。注意 C 没有 C++ 的 :: 全局解析符,一旦被遮蔽就访问不到外层同名变量。
五、存储类(auto / static / extern / register)
存储类决定变量的生命周期、可见性、存储位置。C 有四个存储类关键字:
#include <stdio.h>
// 1. auto:函数内局部变量默认就是 auto,几乎从不显式写
auto int n = 10; // 等价于 int n = 10;(实际很少写 auto)
// 2. static:静态变量,生命周期是整个程序运行期
static int count = 0; // 文件级 static:只在本文件可见(内部链接)
void counter(void) {
static int n = 0; // 函数内 static:只初始化一次,值会保留
n++;
printf("调用次数: %d\n", n);
}
// 3. extern:声明"在别的文件定义过的"全局变量
// extern int shared; // 告诉编译器 shared 在别处定义
// 4. register:建议编译器把变量放进 CPU 寄存器(提速)
// 现代编译器会自动优化,几乎不需要手动写
register int loop_i;
int main(void) {
counter(); // 调用次数: 1
counter(); // 调用次数: 2
counter(); // 调用次数: 3
return 0;
}详细解释:
- auto:局部变量默认就是 auto(自动存储,函数返回时销毁)。实际编程几乎从不显式写 auto。
- static:有两种含义——(1) 函数内的 static 变量:只初始化一次,值在函数调用间保留(如计数器);(2) 文件级的 static 变量/函数:只在本 .c 文件内可见,其他文件访问不到(实现"私有")。
- extern:引用在别处(其他文件)定义的全局变量,实现跨文件共享。常用于多文件项目的头文件。
- register:建议编译器把变量放进 CPU 寄存器提速。现代编译器会自动优化,几乎不需要手动写,而且寄存器变量不能取地址
&。
六、const 修饰符
const 表示"只读"——变量声明后不能再修改。它让编译器帮你检查"不该被改的值被误改",是一种安全机制。
#include <stdio.h>
// const 修饰的是"只读",并非真正的常量
const double PI = 3.14159; // 运行期不能改 PI
// PI = 3; // 编译错误
int main(void) {
const int max = 100;
// max = 200; // 编译错误:不能修改 const 变量
// const 必须在声明时初始化
// const int x; // 错误:未初始化
// const 修饰指针(后续指针篇细讲)
int n = 10;
const int *p1 = &n; // 指向 const int 的指针:不能通过 *p1 改值
int * const p2 = &n; // const 指针:不能改 p2 指向哪里
return 0;
}注意 C 的 const 并不像 C++ 那样是真正的常量——它本质是"只读变量",不能用作数组长度(const int n = 10; int arr[n]; 在 C89 是不行的,C99 的变长数组可以)。要定义真正的编译期常量,用 #define 或 enum(后续预处理器篇细讲)。
七、生命周期总结
- 自动存储期:普通局部变量,函数进入时创建,返回时销毁。
- 静态存储期:全局变量、static 变量,程序启动时创建,程序结束时销毁。
- 动态存储期:malloc 分配的堆内存,由你手动控制(后续内存篇细讲)。
小结
记住这几条就够用了:声明时立即初始化(避免垃圾值);函数内是局部变量、函数外是全局变量;作用域跟花括号走,内层同名会遮蔽外层;static 让变量"长寿"或"私有";const 是"只读"不是真常量。下一篇我们看 C 的数据类型——这决定了变量能装什么样的数据。
← 上一篇 C 基础语法
下一篇 C 数据类型 →