【C语言-变量】C语言中的变量——程序里那个"会变的小盒子"

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 241
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

变量的概念听着简单,但你要真把它理解透了,涉及的东西可不少:变量在内存里怎么存的?局部变量和全局变量有啥区别?static关键字到底干了啥?变量的生命周期和作用域是啥意思?这些东西不光考试常考,你写代码的时候天天都得跟它们打交道。

今天咱们把变量从头到尾捋一遍,不光是"int a = 10"这么一句话,咱们把变量的定义、初始化、存储类别、作用域、生命周期全部拆开揉碎了讲。你跟着代码一步一步走,把每个变量在内存里啥时候出生、啥时候死亡、在哪儿能访问到它,全部整明白。


一、变量是啥?就是内存里的"带名字的格子"

先回到最根本的问题:变量到底是什么?

从硬件的角度来说,变量就是内存里的一块空间。你定义一个变量,编译器就在内存里给你划出一块地盘,这块地盘的大小由变量的类型决定(int通常是4个字节,char是1个字节,double是8个字节)。这块地盘有个地址,但是人记不住地址这种数字,所以给它起了个名字——这就是变量名。

从程序员的角度来说,变量就是一个可以存放数据、并且可以随时修改数据的容器。

打个比方:变量就像你家里的储物柜。柜子有编号(地址),你给它贴了个标签(变量名),里面可以放东西(值),而且你可以随时把里面的东西换成别的(赋值)。

#include <stdio.h>

int main() {
    int age;           // 定义一个变量,在内存里划出4个字节
    age = 25;          // 往这块内存里存入25
    printf("%d\n", age);   // 输出25

    age = 26;          // 修改成26,覆盖原来的值
    printf("%d\n", age);   // 输出26

    return 0;
}

代码拆解:

  • int age; 定义变量,分配内存,但里面的值是不确定的(垃圾数据)。

  • age = 25; 把25存进去,这叫"赋值"。

  • age = 26; 把新的值存进去,覆盖旧的,原来的25就没了。

你看,变量的值"可以变",这就是它叫"变"量的原因。


二、变量的定义和初始化——出生的时候给不给它塞点东西

定义变量的同时直接给它一个初始值,这叫"初始化"。初始化不是必须的,但强烈建议你每次都初始化,不然变量里的值是随机的,很容易出Bug。

#include <stdio.h>

int main() {
    // 定义但没初始化,里面的值是垃圾数据
    int a;
    printf("%d\n", a);   // 输出一个随机数,比如 4200132

    // 定义并初始化
    int b = 10;
    printf("%d\n", b);   // 输出10

    // 多个变量一行定义并初始化
    int c = 20, d = 30, e = 40;

    // 数组的初始化
    int arr[5] = {1, 2, 3, 4, 5};
    int arr2[5] = {0};   // 全部初始化为0

    return 0;
}

重点: 局部变量(在函数里面定义的)如果不初始化,里面的值是"不确定的",可能是什么都有。全局变量和静态变量如果不初始化,默认会被初始化为0。

这个"局部变量不初始化就是垃圾值"的特性,是很多Bug的来源。你定义一个变量忘了赋初值,拿过来就用,结果有时候对有时候错,特别难排查。所以养成习惯:定义变量的同时就给它一个值。


三、变量的存储类别——决定变量在内存的哪个区域待着

C语言里变量的"存储类别"决定了变量放在内存的哪个区域、什么时候出生、什么时候死亡。总共有四种:auto、static、register、extern。

1. auto——自动变量,默认就是它

你在函数里面定义的变量,默认就是auto类型的。auto的意思是"自动分配、自动释放"——函数执行到变量定义的地方,自动分配内存;函数执行完,自动释放内存。

#include <stdio.h>

void func() {
    auto int a = 10;   // auto可以省略,int a = 10; 就是这个意思
    int b = 20;        // 不写auto,默认也是auto
    printf("%d %d\n", a, b);
}   // 函数结束,a和b的内存自动释放

int main() {
    func();
    // 这里访问不到a和b,因为它们已经释放了
    return 0;
}

auto其实没什么好讲的,因为你平时写的局部变量全是它。关键是你要知道:它存在的"作用域"是它所在的那个花括号里面,出了花括号就没了。

2. static——静态变量,活得特别长

static修饰的变量不会因为函数结束而被释放,它会一直存活到程序结束。而且static变量只会被初始化一次。

#include <stdio.h>

void count_calls() {
    static int count = 0;   // 只初始化一次
    count++;
    printf("函数被调用了%d次\n", count);
}

int main() {
    count_calls();   // 输出1
    count_calls();   // 输出2
    count_calls();   // 输出3
    return 0;
}

追踪一下执行过程:

第一次调用count_calls:static int count = 0;执行,count被初始化为0,然后count++变成1,输出"1"。

第二次调用:程序不会再执行static int count = 0;这行了(因为它只初始化一次),直接执行count++,count从1变成2,输出"2"。

第三次调用:同理,count从2变成3,输出"3"。

看到了吗?static让变量"记住"了上一次的值,不会因为函数退出就消失。

static修改变量还有另一个效果:限制变量的作用域。在函数外面定义的static全局变量,只能在这个文件里使用,其他文件访问不到它。这个后面讲全局变量的时候再说。

3. register——寄存器变量,存放在CPU寄存器里

register关键字建议编译器把变量放在CPU的寄存器里,而不是内存里。寄存器的访问速度比内存快得多,所以频繁使用的变量可以用register来优化速度。

#include <stdio.h>

int main() {
    register int i;
    for (i = 0; i < 1000000; i++) {
        // 循环里频繁使用i,放到寄存器里能提高速度
    }
    return 0;
}

 注意: register只是一个"建议",编译器不一定采纳。而且register变量不能取地址(因为它在寄存器里,没有内存地址)。

register int a = 10;
int *p = &a;   // 错误,不能取register变量的地址

现代的编译器优化已经非常好了,很多时候不用你手动写register,编译器自己会决定把哪些变量放到寄存器里。所以现在register用得越来越少了,你知道有这回事就行。

4. extern——声明外部变量

extern的作用是告诉编译器:"这个变量在其他文件里定义了,你先别给我分配内存,链接的时候去别的文件找。"

这玩意儿主要用于多文件编程。

假设你有两个文件:

 file1.c:

#include <stdio.h>

int global = 100;   // 定义一个全局变量

void func() {
    printf("global = %d\n", global);
}

file2.c:

#include <stdio.h>

extern int global;   // 声明:global在别的文件里定义了

int main() {
    printf("%d\n", global);   // 可以访问到file1.c里的global
    return 0;
}

extern的作用就是"跨文件共享变量"。你只要记住:定义变量的时候不加extern,声明(告诉编译器有这个变量)的时候加extern。


四、局部变量 vs 全局变量——谁在哪儿能用

局部变量:在函数或者代码块(花括号里面)定义的变量。只能在定义它的那个花括号里使用。

全局变量:在函数外面定义的变量。从定义的位置开始,到文件末尾都能用。

#include <stdio.h>

int globalVar = 100;   // 全局变量

void func1() {
    printf("%d\n", globalVar);   // 可以访问全局变量
    int local = 10;              // 局部变量
    printf("%d\n", local);
}

void func2() {
    printf("%d\n", globalVar);   // 也可以访问全局变量
    // printf("%d\n", local);    // 错误!访问不到func1里的local
}

int main() {
    printf("%d\n", globalVar);   // 可以访问全局变量
    // printf("%d\n", local);    // 错误!访问不到func1里的local
    return 0;
}

全局变量的"坑":名字冲突

如果你在一个局部作用域里定义了跟全局变量同名的变量,局部变量会"遮盖"全局变量。

#include <stdio.h>

int value = 100;   // 全局变量

int main() {
    int value = 200;   // 局部变量,跟全局变量同名
    printf("%d\n", value);   // 输出200,局部变量覆盖了全局变量

    // 如果想访问全局变量,可以用::(但C语言不支持,C++才有)
    // 在C语言里,一旦被覆盖,在局部作用域里就访问不到全局的了

    return 0;
}

尽量避免用全局变量,因为:

  • 全局变量在任何地方都能被修改,你很难追踪是谁改的。

  • 多文件编程的时候,全局变量容易引起命名冲突。

  • 局部变量的逻辑更清晰,封装性更好。


五、变量的作用域和生命周期

这俩词经常一起出现,但意思不一样。

  • 作用域:变量在代码的"哪个区域"能访问到。

  • 生命周期:变量在内存里"存活多长时间"。

 
变量类型 作用域 生命周期
局部变量(auto) 定义它的那个函数/代码块 执行到定义处开始,函数结束结束
静态局部变量(static) 定义它的那个函数/代码块 程序开始到程序结束
全局变量 整个文件(从定义处开始) 程序开始到程序结束
静态全局变量(static) 定义它的那个文件 程序开始到程序结束
形式参数(函数参数) 函数内部 函数调用期间

画个图帮你理解:

程序开始运行
  |
  +-- 全局变量、静态变量出生(分配内存,初始化为0)
  |
  +-- main函数开始
  |     |
  |     +-- 局部变量出生(分配内存,不自动初始化)
  |     |
  |     +-- 调用其他函数
  |     |     |
  |     |     +-- 被调用函数的局部变量出生
  |     |     |
  |     |     +-- 函数返回,被调用函数的局部变量死亡
  |     |
  |     +-- main结束,main的局部变量死亡
  |
  +-- 程序结束,全局变量、静态变量死亡

用代码验证一下:

#include <stdio.h>

int global = 10;              // 全局变量:程序开始就活着,程序结束才死

void func() {
    static int s = 0;         // 静态局部变量:程序开始就活着,程序结束才死
    int local = 0;            // 普通局部变量:函数每次调用才出生,函数结束就死

    s++;
    local++;
    printf("s=%d, local=%d\n", s, local);
}

int main() {
    func();   // s=1, local=1
    func();   // s=2, local=1  (local又变成1了,因为是新出生的)
    func();   // s=3, local=1

    // printf("%d\n", local);   // 错误,local在func外面访问不到
    // printf("%d\n", s);       // 错误,s虽然还活着,但只能在func里访问

    return 0;
}

看到没?s因为加了static,虽然作用域只在func里,但它的值一直在累加。local每次函数调用都是新分配,所以每次都从0开始。


六、几个容易踩的坑

坑1:用未初始化的局部变量

int a;
printf("%d", a);   // 输出垃圾值,可能是个随机数

解决:定义的时候就给个初值。

坑2:函数返回局部变量的地址

int* func() {
    int a = 10;
    return &a;   // 错误!函数结束a就死了,返回的地址无效
}

如果你返回了一个局部变量的地址,函数结束后这块内存可能被别的数据覆盖了,你拿到的地址指向的是一块"别人的地盘",访问它会出问题。

坑3:全局变量和局部变量同名

虽然语法允许,但容易混淆。尽量避免。

坑4:静态变量初始化的时机

静态变量只初始化一次,而且必须在编译时就能确定的值(常量表达式)。

static int a = 10;        // 正确
static int b = 10 + 20;   // 正确,编译时能算出来
int x = 30;
static int c = x;         // 错误,x不是常量,编译时不知道值

坑5:extern和定义的区分

// 在file1.c里
int a = 10;   // 定义

// 在file2.c里
extern int a;   // 声明,告诉编译器a在别处

你可能会想:如果我在file2.c里写int a = 20;会怎样?那就是重新定义了一个新的全局变量a,跟file1.c的a不是同一个,链接的时候会报"重复定义"错误。

坑6:静态全局变量限制文件内使用

// file1.c
static int hidden = 50;   // 静态全局变量,只在file1.c里能用

// file2.c
extern int hidden;   // 这行会报错,因为file1.c里的hidden是静态的,外面看不到

七、总结:变量到底该怎么用?

说了这么多,咱们来点实用的建议:

  • 能用局部变量就别用全局变量。局部变量的作用域小,不容易互相影响。

  • 定义局部变量的时候一定初始化。防止垃圾值导致的Bug。

  • 全局变量如果要跨文件使用,在一个文件里定义,在其他文件里用extern声明。

  • 如果某个变量只想在一个文件里用,加static,防止别的文件乱访问。

  • 函数里的静态局部变量适合用来统计函数被调用的次数、或者缓存一些中间结果。

练到什么程度算过关?

你能清楚地说出:局部变量在栈上分配,函数结束就释放;全局变量和静态变量在静态存储区,程序结束才释放。能解释static修饰局部变量和全局变量分别有什么效果。能在多文件编程里用extern正确引用变量。写代码的时候有意识地区分变量的作用域和生命周期。

把这些整明白了,变量这关你就真正过了。打开编译器,把上面每种变量的例子都写一遍,尤其是static和extern的部分,多文件编译你得亲手试一次才会真正理解。