10 C 语言常量详解:#define 宏定义、const 关键字、#define 与 const 区别、预处理陷阱、魔法数字
1 C 语言常量基础
1.1 常量的概念与意义
在 C 语言中,常量(Constant)是指在程序运行期间,其值固定不变、不可修改的数据项。
这与变量形成了鲜明对比:变量的值允许在程序运行时被改变;而常量的值一旦被定义,在整个程序运行过程中都不能被再次修改。
这个 “固定不变” 的特性在编程和现实逻辑中非常常见。 为了直观理解,我们可以看几个例子:
- 固定定义:例如一周固定有 7 天,这个数值在任何逻辑中都不应被改变。
- 协议标准:例如 HTTP 状态码中,200 固定表示 “请求成功”,404 固定表示 “未找到资源”,这些代码具有标准含义,必须保持一致。
- 数学常数:例如圆周率 π(约 3.14159),它在数学计算中是一个公认的固定值。
- 配置限制:例如网络协议中规定数据包最大长度(MTU)为 1500 字节,在处理数据包时,这个 1500 就是一个不应被随意修改的限制值。
可以看到,这些值在逻辑上都具有 “恒定不变” 的特性。 在编程中,如果一个数值也具有这种特性(即不应该或不允许在程序运行时被修改),就强烈建议将其定义为常量。
这样做的好处非常明确:
- 提升可读性与可维护性:使用有意义的常量名(如 MAX_MTU)代替无意义的 “魔法数字”(如 1500),使代码更易理解。当需要修改该值时(例如协议升级,MTU 变为 9000),只需修改常量的定义处即可。
- 保证数据稳定性:常量的值在程序运行时将受到保护,防止在代码的其他地方被意外篡改,确保了数据的稳定性。
- 提升代码可靠性:防止因本应不变的数据被错误修改而导致的程序 Bug,从根本上提升了软件的整体可靠性。
📚 扩展:什么是 “魔法数字”(Magic Number)?
“魔法数字” 是一个常见的编程术语,指的是在代码中直接使用的、但含义不明确的数值(即 “硬编码” 的数字)。
例如,在计算价格时写:total_price = item_price * 1.08;
在这个例子中,1.08 就是一个典型的 “魔法数字”。 这种写法会带来显而易见的坏处:
- 可读性差:其他人(或未来的你)很难理解 1.08 到底代表什么(比如:是 8% 的税率?还是 8% 的服务费?)。
- 可维护性差:如果这个 1.08(假设是税率)在 10 个不同的地方都用了,当税率调整时,你需要手动修改 10 次,非常麻烦且容易出错。
为了解决这些问题, 我们就应该给这个数字起一个有意义的名字(例如 TAX_RATE),并用这个名字来代替它。在 C 语言中,这种有名字且不变的值,就是我们所说的常量。
1.2 常量的分类
在 C 语言中,常量根据其表现形式可以分为两类:字面量常量(Literal Constant)和符号常量(Symbolic Constant)。
字面量常量
- 定义:字面量常量(或称 “字面值”)是指直接写在代码中的固定值,它们本身就代表了其自身的数值,无需额外定义。
- 特点:值固定、不可修改,在编译时被编译器直接处理。
- 常见类型:
- 整型常量:如 100、-5,默认类型为 int。
- 浮点型常量:如 3.14、2.0e5,默认类型为 double。
- 字符常量:如 'A'、'\n'。在 C 语言中其类型为 int(表示该字符的 ASCII 值)。
- 字符串常量:如 "Hello"。其类型为 “char 类型的数组”。
- 示例:
// 1. 整型字面量常量
int http_status = 200; // 200 (HTTP 成功状态) 是整型常量
int max_mtu = 1500; // 1500 (MTU 限制) 也是整型常量
// 2. 浮点型字面量常量
double pi = 3.14159; // 3.14159 (圆周率) 是浮点型常量
// 3. 字符字面量常量
char grade = 'A'; // 'A' 是字符常量
// 4. 字符串字面量常量
printf("Hello, World!"); // "Hello, World!" 是字符串字面量常量
字面量常量用起来很方便,但它有一个明显的缺点:它本身就是 “魔法数字”。
正如我们在 1.1 节中讨论的,如果 1500 这个值在程序中多处使用(例如代表 “最大 MTU 长度”),当需要将其修改为 9000 时,维护起来将非常困难。
为了解决字面量常量(魔法数字)带来的可读性和可维护性问题,我们就需要用到第二类常量:符号常量。
符号常量
- 定义:符号常量是指使用一个有意义的标识符(名称)来代表的常量。它必须在使用前进行定义。
- 常见定义形式:
- 使用 #define 预处理指令定义的宏常量。
- 使用 const 关键字修饰的 “只读变量”(常变量)。
- 使用 enum 关键字定义的枚举常量。
- 在本篇教程中,我们将重点讲解前两种方式(#define 和 const),枚举类型(enum)将在后续的章节中详细介绍。
2 常量的定义方式
2.1 使用 #define 定义符号常量
基本概念与语法格式
在 C 语言中,#define 是一种预处理指令。它允许我们定义一个 “符号常量”,这本质上是一个名称与固定值之间的映射关系。
它的基本语法格式如下:
#define 宏名 替换文本
- 宏名:即符号常量的名称。按照编程惯例,宏名通常使用全大写字母并用下划线分隔单词(如 PI、MAX_MTU),以便与普通变量名(如 myVariable)清晰地区分,提升代码可读性。
- 替换文本:指宏名将被替换成的具体内容,可以是数值、表达式、字符串等。
- 示例:
// 宏名通常使用全大写字母并用下划线分隔单词
#define PI 3.14159
#define MAX_MTU 1500
#define VERSION "1.0.0"
使用这种方式,可以将 “魔法数字”(如 1500)替换为有意义的名称(如 MAX_MTU),从而显著提升代码的可读性与可维护性。
使用规范
在使用 #define 时,需要注意以下几点:
- #define 语句不以分号结尾。预处理指令不是 C 语句。如果误加了分号(如 #define PI 3.14159;),那么这个分号(;)也会被当作 “替换文本” 的一部分被替换到代码中,常常导致编译错误。
- 建议将 #define 定义写在函数外部(通常在文件的开头或头文件中)。这能确保其作用域覆盖整个文件(或所有包含该头文件的文件),符合 “全局常量” 的定位。
- #define 定义的符号常量不占用存储空间(即不占内存)。它在预处理阶段完成替换,这一点与后文将要介绍的 const 关键字不同,const 定义的常量(常变量)是会占用内存空间的。
案例演示
下面我们通过一个完整的案例来演示 #define 的用法。
首先,请在本地计算机上创建一个名为 Chapter3_constant 的文件夹,并使用 VS Code 打开该文件夹。然后,在此文件夹中新建一个名为 01define.c 的 C 源文件,作为本次案例演示的代码文件,如下所示:

接着,在 01define.c 文件中输入以下示例代码:
#include <stdio.h>
// 使用 #define 预处理指令定义符号常量 PI
// 在预编译阶段,程序中所有出现的 PI 都会被替换为 3.14
// 注意:宏定义没有类型检查,也不占用内存,仅做文本替换
#define PI 3.14 // 注意:宏定义后面不需要加分号
int main(void)
{
double area; // 用于存储圆的面积,double 是浮点数类型
double r = 1.2; // 初始半径设为 1.2
// 计算面积:面积 = π × r²
area = PI * r * r;
// 输出结果:printf 默认输出小数点后 6 位
// 如果需要保留到小数点后两位,则需要使用 %.2f 格式化输出
printf("圆的面积是(保留到小数点后两位): %.2f\n", area);
// 将半径扩大为原来的 2 倍(1.2 → 2.4)
r = 2.4;
// 重新计算面积(此时面积应为原来的 4 倍)
area = PI * r * r;
printf("圆的面积是(保留到小数点后两位): %.2f\n", area);
return 0;
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

处理时机:预处理阶段
#define 的处理时机发生在 C 语言编译的第一个阶段 —— 预处理阶段。
这一步操作称为宏替换(macro substitution),其本质是纯粹的文本替换。
预处理器会逐行扫描源文件,查找所有已定义的 “宏名”(如 PI),并将其原封不动地替换为对应的 “替换文本”(如 3.14)。预处理器本身不涉及任何 C 语言的语法分析或类型检查(因为这是后续编译器的工作)。
我们可以通过 gcc 命令来亲眼验证这一点:
gcc -E .\01define.c -o .\01define.i
这条命令告诉 gcc 编译器在预处理(-E)阶段结束后就停止,并将结果输出(-o)到 01define.i 文件中。
打开这个新生成的 01define.i 文件,你会看到代码中所有的 PI 确实都已经被替换成了 3.14,如下所示:

注意事项:纯文本替换陷阱
理解了 #define 只是 “纯粹的文本替换” 至关重要,因为这个机制如果使用不当,会因运算符优先级导致严重的逻辑错误。
例如,看下面这个例子:
#include <stdio.h>
#define PI 3 + 2
int main(void)
{
// 预处理后,本行代码会变为: int i = 3 + 2 * 2;
int i = PI * 2;
printf("i = %d\n", i); // 输出:i = 7 而不是 i = 10
return 0;
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

上面这个程序的最终输出结果是 i = 7 而不是 i = 10,原因就在于 #define 是纯文本替换:
- 在预处理阶段,PI 被原封不动地替换为 3 + 2。
- 因此,表达式 PI * 2 实际上变为了 3 + 2 * 2。
- 在编译阶段,由于乘法运算符(*)的优先级高于加法运算符(+),表达式将按 3 + (2 * 2) 的顺序进行计算,最终结果为 7。
为避免此类问题,应在宏定义中使用括号来确保表达式的整体性:#define PI (3 + 2)。这样,PI * 2 就会被正确展开为 (3 + 2) * 2,计算结果为 10,符合预期。
2.2 使用 const 定义符号常量
基本概念与语法格式
在 2.1 节中,我们学习了 #define。#define 作为一种纯文本替换,它虽然简单,但也带来了缺乏类型检查和易引发优先级错误(如 3 + 2 陷阱)的缺点。
为了解决这些问题,C89(ANSI C)标准引入了一个新的关键字:const。
const(constant 的缩写)用于声明一个 “只读变量”(或称 “常变量”)。它具有以下关键特性:
- 具有明确类型:const 定义的常量是有数据类型的(如 const int),编译器会在编译阶段对其进行类型检查,这大大提升了代码的安全性。
- 必须初始化:const 变量在声明时必须被初始化。
- 值不可更改:初始化之后,其值在程序运行期间不可被修改。
因此,在现代 C 语言编程中,推荐优先使用 const 来定义常量,而不是 #define。
使用 const 定义常量的基本格式如下,请注意它是一条 C 语句,末尾需要分号:
const 数据类型 常量名 = 常量值; // 末尾需要分号
示例:
const double PI = 3.14159;
const int MAX_MTU = 1500;
// 在程序中,PI 和 MAX_MTU 都是只读变量,其值在初始化后不可修改
使用规范:不可变性
const 的核心规则是 “不可变”,这体现在两个方面(这两条都是 C 语言的强制语法规定):
1. 必须在声明时初始化:const 变量的值是 “只读” 的,你只有一次(即声明时)给它赋值的机会。如果声明时没有初始化,它后续将永远无法被赋值。如下所示:

2. 初始化后不能被修改:任何在初始化之后试图修改 const 变量值的行为,都会被编译器阻止。如下所示:

案例演示
接下来,我们在 Chapter3_constant 文件夹中创建一个名为 04const.c 的源文件,并编写如下代码:
#include <stdio.h>
// 使用 const 关键字定义常量 PI
// 1. 它有类型 (double)
// 2. 它是一条语句,需要分号
const double PI = 3.14;
int main(void)
{
double area; // 声明变量 area 用于存储圆的面积
double r = 1.2; // 声明并初始化变量 r 为圆的半径
// 编译器在编译时,知道 PI 是一个 double 类型的常量
area = PI * r * r;
// 使用 printf 函数输出圆的面积,保留两位小数
printf("圆的面积是(保留到小数点后两位) : %.2f\n", area);
// (下一节我们会看到,如果尝试修改 PI,编译器会报错)
return 0;
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

处理时机:编译阶段
const 常量是在编译阶段进行处理的,而 #define 是在更早的预处理阶段。
我们可以用 2.1 节中相同的 gcc -E 命令来验证这一点:
gcc -E .\04const.c -o .\04const.i
执行该指令后,打开生成的 04const.i 文件。你会发现,const double PI = 3.14; 这行代码原封不动地保留在文件中,代码中的 PI 也没有被替换,如下所示:

这有力地证明了:预处理器根本不关心 const,const 是交由后续的编译器来处理的 C 语言语法。
对比优势:表达式求值
还记得 2.1 节中 #define PI 3 + 2 导致的陷阱吗?我们来看看 const 如何完美解决这个问题。
#include <stdio.h>
#define DEFINE_VAL 3 + 2 // 预处理器:纯文本替换
const int CONST_VAL = 3 + 2; // 编译器:先求值,再赋值
int main(void)
{
// 预处理后:int i = 3 + 2 * 2; (结果为 7)
int i = DEFINE_VAL * 2;
// 编译时:CONST_VAL 已经是 5
// 相当于: int j = 5 * 2; (结果为 10)
int j = CONST_VAL * 2;
printf("define value: %d\n", i); // 输出: define value: 7
printf("const value: %d\n", j); // 输出: const value: 10
return 0;
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

原因分析:
- #define:预处理器把 DEFINE_VAL * 2 替换为 3 + 2 * 2,编译器根据优先级计算得到 7。
- const:编译器在处理 const int CONST_VAL = 3 + 2; 这条语句时,会先计算表达式 3 + 2,得到结果 5,然后将 5 作为 CONST_VAL 的值。后续代码 CONST_VAL * 2 自然就是 5 * 2,得到 10。
结论:const 具有类型安全、在编译期处理、能正确处理表达式,是比 #define 更安全、更现代的常量定义方式。
3 #define 与 const 的区别
在 1.2 节中,我们将常量分为了 “字面量常量” 和 “符号常量”。
而在第 2 节中,我们学习了定义符号常量的两种主要方法:#define 和 const。
一个初学者可能会问:既然两种都能用,我该用哪个?
答案是:在 C 语言中,定义常量时应优先使用 const。
为了理解为什么 const 是更优、更现代的选择,我们必须深入理解它们在 C 语言机制上根本不同的 “恩怨情仇”。
3.1 核心区别:处理时机
#define 和 const 最根本的区别在于它们在程序 “编译过程” 中的处理时机完全不同。
- #define(宏常量):在 “预处理” 阶段处理。
- 预处理器是一个在编译器 “开工” 之前运行的工具。
- 它的工作非常简单粗暴:纯文本替换。
- 正如我们在 2.1 节用 gcc -E 验证的,预处理器会把代码中所有的 PI 替换为 3.14。当替换完成后,PI 这个名字本身就消失了。
- 后续的编译器根本不知道 PI 曾经存在过,它只看到了 3.14。
- const(常变量):在 “编译” 阶段处理。
- 编译器 “开工” 时,它会完整地看到 const double PI = 3.14; 这条 C 语句。
- 编译器会理解:哦!这是一个 C 语言的 const 关键字,用户定义了一个 double 类型的 “只读变量”,它的名字叫 PI,它的值是 3.14。
- PI 这个名字会一直存在于编译过程中,并被存入 “符号表”(Symbol Table)中。
结论:#define 是编译前的 “全局查找替换”,而 const 是编译中的 “语法规则处理”。
3.2 区别二:类型安全
由 “处理时机” 的不同,直接引出了两者在 “安全性” 上的巨大差异。
- #define:没有类型,不安全。
- 预处理器只管 “替换”,它根本不懂 C 语言的 “数据类型”。
- area = PI * r * r; 被替换为 area = 3.14 * r * r;
- 如果你写错了 #define PI "Hello"(一个字符串),预处理器也会忠实地替换,代码会变成 area = "Hello" * r * r;,直到编译器看到这行代码时,才会报出一个莫名其妙的 “字符串不能做乘法” 的错误。
- const:有明确类型,安全。
- const double PI = 3.14; 这行代码明确告诉了编译器:PI 是一个 double 类型。
- 如果你试图写 const double PI = "Hello";,编译器在编译阶段就会立刻报错,告诉你类型不匹配。
- 这种在编译期就能发现错误的能力,就是 “类型安全”(Type Safety)。
结论:const 提供了强大的类型检查,能帮你提前发现错误,而 #define 不行。
3.3 区别三:内存占用与作用域
- #define:不占内存,没有作用域。
- 因为在预处理后 PI 这个名字就没了(只剩下 3.14),所以 PI 本身不占用内存。
- #define 也没有 C 语言 “作用域” 的概念。它从被定义的那一行开始,到文件结束(或被 #undef)都有效。
- const:占用内存,遵守作用域。
- const 定义的是一个 “只读变量”。是变量,就要占用内存空间。
- 因为它是 C 语言的变量,所以它完全遵守 C 语言的作用域规则。例如,定义在函数内部的 const 变量,只有在该函数内部才有效,这提供了更精细的控制。
结论:const 占用内存(有地址),但换来了更灵活、更安全的 “作用域” 控制。
3.4 区别四:表达式求值
这是我们在 2.2 节演示过的 “陷阱” 和 “优势” 的最直接对比。
- #define:仅替换,不求值。
- #define VAL 3 + 2
- result = VAL * 2;
- 预处理替换为 result = 3 + 2 * 2;
- 编译器计算为 3 + 4 = 7。(错误!)
- const:先求值,再赋值。
- const int VAL = 3 + 2;
- 编译器在处理这行代码时,会先计算 3 + 2,得到 5。
- 然后将 5 存入 VAL 这个只读变量中。
- result = VAL * 2;
- 编译器计算为 5 * 2 = 10。(正确!)
3.5 总结:#define 与 const 对比表
为了方便你复习,我将以上所有区别总结为下表:
| 对比项 | #define 宏常量 | const 常变量(只读变量) |
|---|---|---|
| 处理时机 | 预处理阶段 | 编译阶段 |
| 本质 | 纯粹的文本替换 | 有类型的只读变量 |
| 类型安全 | 无(不进行类型检查) | 有(编译器进行类型检查) |
| 内存占用 | 不占用(仅在代码中展开) | 占用(有自己的内存地址) |
| 语法 | 无需分号,无需类型 | 需要分号,需要指定类型 |
| 作用域 | 文件作用域(或到 #undef) | C 语言的块作用域(随定义位置) |
| 表达式求值 | 不求值(易引发优先级错误) | 先求值(再赋值给常量,安全) |
| 调试 | 不友好(符号表中没有宏名) | 友好(符号表中有变量名) |
4 编程练习与巩固
4.1 使用 #define 计算总工资
编写一个 C 程序,实现以下功能:
- 使用 #define 定义一个整型常量 HOURLY_WAGE(时薪),并赋值(例如 25);
- 提示用户从键盘输入一个整数 hours_worked(工作小时数);
- 计算总工资 total_pay(HOURLY_WAGE * hours_worked);
- 清晰地输出时薪、工作小时数和最终的总工资。
通过本练习,你将巩固 #define 的基本用法,并理解它在定义固定费率(如时薪)以提高代码可维护性时的作用。
#include <stdio.h> // 引入标准输入输出库
// 假设一个固定的时薪(Hourly Wage)
// 使用 #define 定义这个 “配置项”
// 如果以后时薪调整了(比如从 25 涨到 30),
// 只需要修改这一行代码即可。
#define HOURLY_WAGE 25
int main(void)
{
int hours_worked; // 声明一个整型变量,用于存储工作小时
int total_pay; // 声明一个整型变量,用于存储总工资
// 1. 提示用户输入
printf("请输入您的工作小时数 (整数): ");
scanf("%d", &hours_worked); // 读取用户输入的整数
// 2. 使用 #define 的常量进行计算
// 预处理器会把这行代码替换为:
// total_pay = 25 * hours_worked;
total_pay = HOURLY_WAGE * hours_worked;
// 3. 输出结果
printf("--- 工资计算结果 ---\n");
printf("您的时薪是: %d 元/小时\n", HOURLY_WAGE);
printf("您工作了: %d 小时\n", hours_worked);
printf("您的总工资是: %d 元\n", total_pay);
return 0; // 程序正常结束
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

4.2 使用 const 计算圆的面积和周长
编写一个 C 程序,实现以下功能:
- 使用 const double 定义一个常量 PI,值为 3.14159;
- 提示用户从键盘输入一个 double 类型的半径 radius;
- 计算并输出该圆的面积(Area = π * radius²)和周长(Perimeter = 2 * π * radius);
- 确保输出的面积和周长结果保留两位小数。
通过本练习,你将巩固 const 关键字的正确用法以及 double 浮点数的计算。
#include <stdio.h> // 引入标准输入输出库
// 在 main 函数外部(全局)定义 const 常量 PI
// 这样在本文件的所有函数中都可以安全地使用它
const double PI = 3.14159;
int main(void)
{
// 1. 声明需要的变量
double radius; // 用于存储用户输入的半径
double area; // 用于存储计算出的面积
double perimeter; // 用于存储计算出的周长
// 2. 提示用户输入,并使用 scanf 读取
printf("请输入圆的半径 (radius): ");
scanf("%lf", &radius); // 注意:读取 double 类型需要使用 %lf
// 3. 计算面积和周长
area = PI * radius * radius;
perimeter = 2 * PI * radius;
// 4. 输出结果,使用 %.2lf 格式化到小数点后两位
printf("圆的面积 (Area) 是: %.2lf\n", area);
printf("圆的周长 (Perimeter) 是: %.2lf\n", perimeter);
return 0; // 程序正常结束
}
程序运行结果如下(环境:Win64 + VS Code + MinGW-w64 UCRT):

更多推荐



所有评论(0)