4. 变量与常量:变量的定义、初始化与赋值,常量的定义(#define, const),作用域与生命周期
好,咱们今天聊点实在的。变量和常量,说白了就是程序里存数据的两个基本容器。你想想看,写代码不就是处理数据吗?数据放哪儿、怎么放、放多久,这些搞不清楚,后面写再多代码也是空中楼阁。
我个人习惯,学一个新语言,先看它怎么定义变量。C语言的变量定义,其实很直白,但细节里藏着不少坑。我当年刚入行时,就因为没搞懂作用域,把一个全局变量改得乱七八糟,查了两天bug……嗯,从那以后我再也不敢轻视这些基础概念了。
4.1 变量的定义
变量定义,就是告诉编译器:我要用一块内存,给它起个名字,用来存某种类型的数据。格式很简单:
类型 变量名;
比如:
int count;
float temperature;
char grade;
这里要注意几点:
- 变量名:只能由字母、数字、下划线组成,不能以数字开头。别用关键字,比如
int、return这些。 - 命名风格:我建议用有意义的英文单词,比如
student_count而不是sc。你想想看,三个月后你自己回来看代码,看到sc是什么感觉? - 类型:决定了变量能存什么数据、占多大内存。C语言是静态类型语言,类型一旦确定,就不能变了。
4.2 变量的初始化与赋值
初始化和赋值,这两个概念容易混淆。我简单解释一下:
- 初始化:在定义变量的同时给一个初始值。
- 赋值:定义之后,再给变量设一个新值。
看代码:
int a = 10; // 初始化
a = 20; // 赋值
初始化有多种方式:
int x = 5; // 直接初始化
int y = {5}; // 花括号初始化(C99起支持)
int z = {0}; // 初始化为0
int arr[3] = {1,2,3}; // 数组初始化
我曾经在项目里见过一个bug:有人定义了一个局部变量没初始化,直接拿来用。结果每次运行结果都不一样,因为局部变量在栈上,初始值是不确定的。嗯,这种问题最难查,因为它不是每次都复现。
4.3 常量的定义
常量,就是程序中不可修改的值。C语言里定义常量有两种主流方式:#define 和 const。
4.3.1 #define 宏定义
#define 是预处理器指令,在编译之前就把文本替换掉。说白了,它就是个文本替换工具。
#define PI 3.14159
#define MAX_SIZE 1024
#define ERROR_CODE -1
使用 #define 时要注意:
- 没有类型检查,就是纯文本替换。
- 结尾不要加分号,否则会替换进去。
- 宏名通常全大写,这是约定俗成的风格。
我记得有一次,同事写了个 #define SQUARE(x) x*x,然后调用 SQUARE(a+1),结果展开成 a+1*a+1,完全不是他想要的效果。所以带参数的宏,一定要加括号:
#define SQUARE(x) ((x)*(x))
4.3.2 const 关键字
const 是C语言的关键字,用来声明一个变量是只读的。它比 #define 更安全,因为有类型检查。
const int MAX_USERS = 100;
const float GRAVITY = 9.8;
const char* MESSAGE = "Hello";
const 和 #define 的区别,我整理了一个表格:
| 特性 | #define | const |
|---|---|---|
| 处理阶段 | 预处理(编译前) | 编译时 |
| 类型检查 | 无 | 有 |
| 占用内存 | 不占(直接替换) | 占用内存 |
| 作用域 | 全局(从定义到文件尾) | 遵循变量作用域规则 |
| 调试 | 不方便(替换后看不到原名) | 方便(符号表中有名字) |
我个人更推荐用 const,除非你需要定义一些编译时常量(比如数组大小),那时候 #define 更合适。
4.4 作用域与生命周期
这两个概念,我当年学的时候总觉得差不多,其实完全不同。作用域是“在哪儿能访问”,生命周期是“在内存里活多久”。
4.4.1 作用域
C语言的作用域分几种:
- 块作用域:在
{}内定义的变量,只能在这个块里用。 - 文件作用域:在所有函数外面定义的变量,从定义点到文件尾都能用。
- 函数作用域:只有
goto标签有这个作用域,其他变量没有。 - 函数原型作用域:函数声明中的参数名,只在声明中有效。
看个例子:
int global_var = 10; // 文件作用域
void func() {
int local_var = 20; // 块作用域
if (1) {
int block_var = 30; // 块作用域,只在if内有效
printf("%d", block_var); // 可以
}
// printf("%d", block_var); // 错误!超出作用域
}
我曾经在项目里看到有人把局部变量和全局变量起了同一个名字,结果局部变量“遮蔽”了全局变量,导致全局变量在函数里无法访问。这种问题,说白了就是命名不规范造成的。
4.4.2 生命周期
生命周期决定了变量什么时候创建、什么时候销毁。C语言里有三种存储类别:
| 存储类别 | 生命周期 | 作用域 | 示例 |
|---|---|---|---|
| 自动(auto) | 进入块时创建,退出块时销毁 | 块作用域 | 局部变量默认就是auto |
| 静态(static) | 程序启动时创建,程序结束时销毁 | 块作用域或文件作用域 | static int count = 0; |
| 寄存器(register) | 同自动变量,但建议放在寄存器中 | 块作用域 | register int i; |
这里有个经典例子:
void counter() {
static int count = 0; // 静态局部变量
count++;
printf("调用次数: %d\n", count);
}
int main() {
counter(); // 输出1
counter(); // 输出2
counter(); // 输出3
return 0;
}
为什么每次调用 counter(),count 的值会累加?因为 static 让它的生命周期变成了整个程序运行期间,但作用域仍然是块作用域——只能在 counter() 函数里访问它。
- 作用域:编译时的概念,决定“能不能访问”。
- 生命周期:运行时的概念,决定“还活着没”。
- 全局变量:作用域大,生命周期长,但容易造成耦合。
- 局部变量:作用域小,生命周期短,但更安全。
4.5 知识体系图
下面这张图,把变量和常量的核心知识点串起来了。你仔细看看,能帮你理清思路。
4.6 避坑指南
最后,我把自己踩过的坑总结一下,你遇到了能少走弯路:
- 未初始化局部变量:我曾经调试过一个串口通信程序,数据老是乱码。查了两天,发现是一个局部变量没初始化,里面是随机值。从那以后,我定义变量必初始化。
- 宏定义不加括号:前面说过的
SQUARE(x)例子,就是血的教训。带参数的宏,参数和整个表达式都要加括号。 - 全局变量滥用:全局变量虽然方便,但会让代码耦合严重。我建议能用局部变量就用局部变量,实在不行再用全局。
- const 和指针的搭配:
const int *p和int * const p含义不同,前者是指向常量的指针,后者是常量指针。这个容易搞混,我建议你多写几个例子试试。
#define 定义(比如寄存器地址),把业务逻辑相关的常量用 const 定义。这样既方便调试,又保证了类型安全。
好了,变量和常量这部分就聊到这儿。你把这些概念理清楚,后面学指针、数组、函数的时候,就会轻松很多。记住一句话:变量是内存的别名,常量是值的别名。搞懂这个,C语言的内存管理你就入门了。
公众号:蓝海资料掘金营,微信deep3321