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;
}

几个要点:

二、未初始化变量的陷阱

这是 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 变量分为两大类:

#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 的作用域由花括号决定,分三层:

规则:内层可以访问外层变量,外层不能访问内层。如果内外层同名,内层会遮蔽(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;
}

详细解释:

六、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 让变量"长寿"或"私有";const 是"只读"不是真常量。下一篇我们看 C 的数据类型——这决定了变量能装什么样的数据。

← 上一篇 C 基础语法

下一篇 C 数据类型

✈️💬