变量的概念听着简单,但你要真把它理解透了,涉及的东西可不少:变量在内存里怎么存的?局部变量和全局变量有啥区别?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的部分,多文件编译你得亲手试一次才会真正理解。