一、常量与变量:程序世界的“不变”与“变”

各位同学,欢迎来到《C语言数据类型与运算符精讲》的第一章。今天咱们聊聊程序里最基础的两个概念——常量和变量。

说白了,程序就是处理数据的。有些数据是固定的,比如圆周率π;有些数据是变化的,比如用户的输入。C语言里,前者叫常量,后者叫变量。这个区分,我当年刚学的时候觉得很简单,但后来在项目里踩过不少坑,才发现里面门道挺多。

1.1 常量:那些“雷打不动”的值

常量,就是程序运行过程中值不会改变的量。C语言里常量有三种形式:字面常量、const常量、#define宏常量。咱们一个一个说。

1.1.1 字面常量

字面常量是最直接的——你直接在代码里写出来的值。比如:

int a = 10;          // 10 就是整型字面常量
float b = 3.14;      // 3.14 是浮点型字面常量
char c = 'A';        // 'A' 是字符型字面常量
char *s = "hello";   // "hello" 是字符串字面常量

这些值写死了,没法改。你总不能把 10 改成 20 吧?那叫改代码,不是改常量。

小提示: 字符串字面量在C语言里是存储在只读数据区的。我曾经试过用指针去修改它,结果程序直接崩溃。嗯,血的教训。

1.1.2 const常量

const是C语言的关键字,用来声明一个“只读”的变量。注意我的措辞——它本质上还是变量,只是你不能通过赋值去修改它。

const int MAX_SIZE = 1024;
// MAX_SIZE = 2048;  // 编译报错!不能修改

我个人习惯用const来定义那些“逻辑上不该变”的值。比如缓冲区大小、配置参数等。为什么不用#define?别急,下面就说。

注意: const常量在C语言里并不是真正的常量。你不能用它来定义数组大小(至少在C89标准里不行)。比如 const int n = 5; int arr[n]; 在C89里是编译不过的。但在C99之后可以了。这个细节,我当年面试时被问过,印象深刻。

1.1.3 #define宏常量

#define是预处理器指令,它在编译之前就把代码里的宏名替换成对应的值。说白了,就是文本替换。

#define PI 3.1415926
#define BUFFER_SIZE 256

int main() {
    double area = PI * r * r;
    char buffer[BUFFER_SIZE];
    return 0;
}

宏常量没有类型检查,就是纯粹的替换。这既是优点也是缺点。优点是灵活,缺点是不安全。比如:

#define MUL(a, b) a * b
int result = MUL(2 + 3, 4);  // 实际展开为 2 + 3 * 4 = 14,不是20!

这种坑,我踩过不止一次。所以现在写宏,我习惯把参数都加上括号:#define MUL(a, b) ((a) * (b))

常量类型 特点 适用场景
字面常量 直接写在代码里,简单直观 临时值、一次性使用的数值
const常量 有类型检查,只读变量 需要类型安全的常量,如函数参数
#define宏常量 无类型检查,文本替换 数组大小、条件编译、简单宏函数

1.2 变量:程序里的“活水”

变量,就是值可以改变的量。每个变量都有三个属性:名字、类型、值。定义变量时,你得告诉编译器这三样东西。

1.2.1 变量的定义与初始化

定义变量很简单:

int count;          // 定义,未初始化
count = 0;          // 赋值

int total = 100;    // 定义并初始化
char grade = 'A';   // 字符变量
double pi = 3.14;   // 浮点变量

这里有个重要区别:初始化是在定义时给值,赋值是定义之后给值。未初始化的局部变量,它的值是随机的——也就是所谓的“垃圾值”。

重要: 我见过太多新手因为忘记初始化变量,导致程序出现莫名其妙的bug。比如:
int sum;
for(int i = 0; i < 10; i++) {
    sum += i;  // sum初始值未知!
}
这种错误很难排查,因为每次运行结果可能都不一样。我的建议是:定义变量时就初始化,养成习惯。

1.2.2 变量的作用域

作用域,说白了就是变量在代码里“能管到”的范围。C语言里主要有三种作用域:

  • 全局作用域:定义在函数外面的变量,整个文件都能访问。
  • 局部作用域:定义在函数或代码块内部的变量,只能在该块内访问。
  • 文件作用域:用static修饰的全局变量,只在本文件内可见。
#include <stdio.h>

int global_var = 10;  // 全局变量,整个程序可见

void func() {
    int local_var = 20;  // 局部变量,只在func内可见
    static int static_var = 0;  // 静态局部变量,生命周期贯穿整个程序
    static_var++;
    printf("static_var = %d\n", static_var);
}

int main() {
    func();  // 输出 static_var = 1
    func();  // 输出 static_var = 2
    // printf("%d", local_var);  // 编译报错!local_var不在作用域内
    return 0;
}

你看,static局部变量虽然作用域只在函数内,但它的生命周期是整个程序运行期间。每次调用func,static_var的值都会保留。这个特性,我在写状态机的时候经常用。

1.2.3 变量的生命周期

生命周期,就是变量从创建到销毁的这段时间。和内存分配方式密切相关:

存储类型 生命周期 存储位置
全局变量 程序启动到结束 数据段(.data或.bss)
static局部变量 程序启动到结束 数据段
普通局部变量 进入函数到退出函数
动态分配变量 malloc到free

这里有个容易混淆的点:作用域和生命周期不是一回事。全局变量的作用域是整个文件,生命周期是整个程序;static局部变量的作用域是函数内部,但生命周期也是整个程序。你想想看,是不是这个理?

避坑指南: 我曾经在项目里犯过一个错误——在函数里返回了一个局部变量的地址。调用者拿到指针后去访问,结果数据已经被覆盖了。这就是典型的“生命周期结束,但还在使用”的问题。记住:永远不要返回局部变量的地址!

1.3 知识体系总览

说了这么多,咱们用一张图来总结本章的核心内容:

常量与变量知识体系 常量(值不可变) 字面常量 const常量 #define宏常量 变量(值可变) 定义与初始化 作用域 生命周期 核心:理解“不变”与“变”,掌握内存管理 作用域:变量在代码中可见的范围 生命周期:变量从创建到销毁的时间段 两者不一定相同(如static局部变量)

这张图把本章的核心内容串起来了。左边是常量,右边是变量,下面是作用域和生命周期这两个关键概念。你把它记在脑子里,后面学指针、数组、函数的时候,会少走很多弯路。

好了,第一章就到这里。记住:常量是程序的“骨架”,变量是程序的“血肉”。把这两个概念吃透,后面的路就好走了。


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