4. 变量与常量:变量的定义、初始化与赋值,常量的定义(#define, const),作用域与生命周期

好,咱们今天聊点实在的。变量和常量,说白了就是程序里存数据的两个基本容器。你想想看,写代码不就是处理数据吗?数据放哪儿、怎么放、放多久,这些搞不清楚,后面写再多代码也是空中楼阁。

我个人习惯,学一个新语言,先看它怎么定义变量。C语言的变量定义,其实很直白,但细节里藏着不少坑。我当年刚入行时,就因为没搞懂作用域,把一个全局变量改得乱七八糟,查了两天bug……嗯,从那以后我再也不敢轻视这些基础概念了。

4.1 变量的定义

变量定义,就是告诉编译器:我要用一块内存,给它起个名字,用来存某种类型的数据。格式很简单:

类型 变量名;

比如:

int count;
float temperature;
char grade;

这里要注意几点:

  • 变量名:只能由字母、数字、下划线组成,不能以数字开头。别用关键字,比如 intreturn 这些。
  • 命名风格:我建议用有意义的英文单词,比如 student_count 而不是 sc。你想想看,三个月后你自己回来看代码,看到 sc 是什么感觉?
  • 类型:决定了变量能存什么数据、占多大内存。C语言是静态类型语言,类型一旦确定,就不能变了。
小技巧:我习惯在定义变量时就初始化,哪怕暂时用不到也赋个0。这样可以避免很多“野指针”和“未初始化”的坑。

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:有人定义了一个局部变量没初始化,直接拿来用。结果每次运行结果都不一样,因为局部变量在栈上,初始值是不确定的。嗯,这种问题最难查,因为它不是每次都复现。

警告:局部变量不会自动初始化!全局变量和静态变量会自动初始化为0。这个区别一定要记住。

4.3 常量的定义

常量,就是程序中不可修改的值。C语言里定义常量有两种主流方式:#defineconst

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 知识体系图

下面这张图,把变量和常量的核心知识点串起来了。你仔细看看,能帮你理清思路。

变量与常量知识体系 变量与常量 变量 常量 定义与初始化 赋值操作 作用域 块作用域 文件作用域 函数作用域 自动 静态 #define 宏 const 常量 枚举常量 核心:类型决定内存,作用域决定可见,生命周期决定存活

4.6 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了能少走弯路:

  • 未初始化局部变量:我曾经调试过一个串口通信程序,数据老是乱码。查了两天,发现是一个局部变量没初始化,里面是随机值。从那以后,我定义变量必初始化。
  • 宏定义不加括号:前面说过的 SQUARE(x) 例子,就是血的教训。带参数的宏,参数和整个表达式都要加括号。
  • 全局变量滥用:全局变量虽然方便,但会让代码耦合严重。我建议能用局部变量就用局部变量,实在不行再用全局。
  • const 和指针的搭配const int *pint * const p 含义不同,前者是指向常量的指针,后者是常量指针。这个容易搞混,我建议你多写几个例子试试。
我的习惯:写嵌入式程序时,我会把硬件相关的常量用 #define 定义(比如寄存器地址),把业务逻辑相关的常量用 const 定义。这样既方便调试,又保证了类型安全。

好了,变量和常量这部分就聊到这儿。你把这些概念理清楚,后面学指针、数组、函数的时候,就会轻松很多。记住一句话:变量是内存的别名,常量是值的别名。搞懂这个,C语言的内存管理你就入门了。


公众号:蓝海资料掘金营,微信deep3321