10 C 语言常量详解:#define 与 const 定义常量及其区别与应用
1 初识常量
1.1 常量概述
常量(Constant)是程序运行中值固定不变的量,通常用于存储不可更改的数据,例如数学常数 π、程序中用于逻辑判断或控制的固定值等。
1.2 常量分类
-
字面量常量:直接在程序中书写的固定值,无需定义或声明即可使用,包括整数常量(如:666)、浮点数常量(如:66.66)、字符常量(如:'A')等。
-
例如:int number = 666; 在这句代码中,666 是一个整数字面量常量。
-
-
标识符常量:通过特定标识符引用的常量值,需通过标识符命名,常见形式包括:
- 使用 #define 宏定义的常量;
- 使用 const 关键字定义的常量;
- 枚举类型(enum)定义的常量。
2 常量定义方法
字面量常量可直接使用,无需专门定义或声明。而对于标识符常量,我们需要学习其定义方式,按照惯例,常量名通常采用全大写字母来命名,以此与变量进行区分。
2.1 使用 #define 定义常量
2.1.1 基本概念
#define 用于定义常量,这种方式被称为宏定义。
宏定义的作用是用一个标识符来代表一个常量值。
使用 #define 定义的常量通常也被称为符号常量。
在代码的预处理阶段,预处理器会将代码中出现的该标识符全部替换为指定的常量值,这一过程被称为宏替换,即用宏体替换所有宏名。
2.1.2 语法格式
#define 常量名 常量值
注意事项:
- #define 语句不要以分号结尾。若添加分号,分号会被视为常量值的一部分,可能导致代码逻辑错误。
- #define 语句必须写在 main 函数的外部。虽然某些编译器的扩展功能允许在 main 函数内部使用 #define,但为了避免潜在的问题和提高代码的可移植性,强烈不建议这样做。
2.1.3 案例演示
首先,我们创建一个名为 “Chapter3_constant” 的文件夹,并用 VS Code 打开。接着,在该文件夹中新建一个名为 “1define.c” 的源文件,具体操作如下:
然后编写如下代码:
#include <stdio.h>// 使用宏定义定义常量 PI,其值为 3.14,用于表示圆周率
#define PI 3.14int main()
{double area; // 定义一个双精度浮点型变量,用于存储圆的面积double r = 1.2; // 定义圆的半径,初始值为 1.2// 根据圆的面积公式:面积 = π * 半径 * 半径,计算圆的面积area = PI * r * r;// 输出圆的面积,保留到小数点后两位printf("圆的面积是(保留到小数点后两位) : %.2f\n", area);// 将圆的半径扩大为原来的二倍,即变为 2.4r = 2.4;// 再次根据圆的面积公式计算圆的面积area = PI * r * r;// 输出此时圆的面积,由于半径扩大为原来的二倍,面积会扩大为原来的四倍printf("圆的面积是(保留到小数点后两位) : %.2f\n", area);return 0;
}
程序在 VS code 中的运行结果如下所示:
2.1.4 执行时机
对于 #define 指令,预处理器在预处理阶段会执行一项关键的文本替换操作:逐行扫描整个源代码文件,寻找所有与宏定义名称相匹配的文本实例,并将这些实例直接替换为宏定义中所指定的内容。
需要特别注意的是,这个替换过程仅仅是文本层面的操作,预处理器不会进行任何语法或类型的检查。因此,如果宏替换后的代码存在语法错误或类型不匹配的问题,这些问题在预处理阶段并不会被发现,只有在预处理后的代码交由编译器进一步处理时才会暴露出来。
以上面这个程序为例,#define PI 3.14 这行代码定义了一个名为 PI 的宏,其替换文本为 3.14。在预处理阶段,预处理器会遍历源代码,查找所有的 PI,并将它们替换为 3.14。这一过程在编译之前(即预处理阶段)就已经完成,所以编译器实际看到的代码是已经完成替换的源代码。随后,编译器会对这些经过替换的代码进行编译,最终生成可执行文件或目标代码。
为了更直观地观察预处理过程,我们可以在终端中输入预处理指令:
gcc -E .\1define.c -o .\1define.i
执行该指令后,会生成一个名为 1define.i 的文件。通过查看该文件的内容,我们可以发现代码中的 PI 已经被全部替换成了对应的数值 3.14。
2.1.5 注意事项
由于 #define 指令的宏替换只是纯粹的文本替换,所以在使用时需要注意可能发生的意外情况,比如宏展开导致的运算符优先级问题。例如:
#include <stdio.h>// 纯粹的文本替换
#define PI 3 + 2int main()
{int i = PI * 2; // 3 + 2 * 2 = 7printf("i = %d\n", i); // i = 7 而不是 10return 0;
}
上面这个程序的最终输出结果是 i = 7 而不是 i = 10,原因在于宏常量 PI 是通过文本替换实现的。在预处理阶段,PI 被直接替换为 3 + 2,因此表达式 PI * 2 实际上变为 3 + 2 * 2。由于乘法运算符 * 的优先级高于加法运算符 +,表达式按 3 + (2 * 2) 的顺序计算,最终结果为 7。
程序在 VS code 中的运行结果如下所示:
2.2 使用 const 定义常量
2.2.1 基本概念
const 关键字用于声明一个变量为常量,一旦该变量被初始化,其值便不可再被修改。这一用于定义常量的特性自 C89/ANSI C 标准(即早期的 C 标准,常被称为 C90 的前身)起就已引入,并在后续的标准(如 C99、C11 等)中得到了进一步的完善与明确。在使用 const 定义常量时,需要在变量的数据类型前添加 const 关键字。
与使用 #define 定义宏常量相比,使用 const 定义的常量具有显著的优势。const 常量具有明确的数据类型,这使得它在编译阶段能够接受类型检查。这种类型检查机制能够提前发现潜在的类型错误,从而提高了代码的安全性和可靠性。因此,在实际编程中,推荐使用 const 来定义常量。
2.2.2 语法格式
const 数据类型 常量名 = 常量值; // 注意后面有分号 ;
2.2.3 案例演示
我们首先新建一个名为 “3const.c” 的源文件,然后编写如下代码:
#include <stdio.h>// 使用 const 关键字定义常量 PI
const double PI = 3.14; // const 定义常量时,需要加分号int main()
{double area; // 声明变量 area 用于存储圆的面积double r = 1.2; // 声明并初始化变量 r 为圆的半径// 计算圆的面积并赋值给变量 areaarea = PI * r * r;// 使用 printf 函数输出圆的面积,保留两位小数printf("圆的面积是(保留到小数点后两位) : %.2f\n", area);return 0;
}
程序在 VS code 中的运行结果如下所示:
2.2.4 执行时机
与 #define 不同,const 常量是在编译过程中进行处理的,而非预处理阶段。
为了验证这一点,我们可以在终端中输入预处理指令:
gcc -E .\3const.c -o .\3const.i
执行该指令后,查看生成的 3const.i 文件内容,会发现代码中的 PI 并没有被替换成对应的数值,而是依然以常量的形式存在。
2.2.5 注意事项
在 C 语言里,用 const 声明的变量具有不可变性,这就要求在声明该变量时必须同时赋予它一个初始值。如此一来,就能保证该变量在程序运行的整个过程中都维持固定不变。
倘若在声明时未给 const 变量提供初始值,而试图在后续代码中对其进行赋值操作,编译器会报错。这是因为 const 常量需要有一个明确且不可更改的初始状态,以确保其不可变的特性。
2.2.6 定义位置与作用域
在 main 函数外面定义:
- 全局作用域:在 main 函数外面定义的 const 常量具有全局作用域,意味着它可以在定义它的文件中的所有函数中被访问。
- 静态存储期:这些常量在程序开始执行时分配内存,并在程序结束时释放。
#include <stdio.h>// 在 main 函数外面定义 const 常量
const int globalConst = 100;void printGlobalConst()
{printf("Global const: %d\n", globalConst); // 100
}int main()
{printf("Main: Global const: %d\n", globalConst); // 100printGlobalConst();return 0;
}
程序在 VS code 中的运行结果如下所示:
在 main 函数里面定义:
- 局部作用域:在 main 函数(或任何函数)里面定义的 const 常量具有局部作用域,意味着它只能在该函数内部被访问。
- 自动存储期:这些常量在函数被调用时分配内存,并在函数返回时释放。
#include <stdio.h>// 声明一个函数,尝试访问 main 函数中的局部 const 常量(这将不会成功)
void tryAccessLocalConst();int main()
{// 在 main 函数里面定义 const 常量const int localConst = 200;printf("Local const in main: %d\n", localConst);// 调用另一个函数tryAccessLocalConst(); // 这个函数将尝试(但无法)访问 localConstreturn 0;
}void tryAccessLocalConst()
{// 以下代码会导致编译错误,因为 localConst 在此作用域中未定义printf("Trying to access localConst: %d\n", localConst); // 错误:localConst 未定义// 正确的做法是:只能访问自己作用域内的变量或常量// const int anotherConst = 300;// printf("Local const in tryAccessLocalConst: %d\n", anotherConst);
}
在 tryAccessLocalConst 函数中,尝试访问 localConst 会导致编译错误,因为 localConst 在该函数的作用域中未定义。
作用域和生命周期的详细讲解将在后续知识点中展开,此处仅作简要了解。
3 #define 和 const 定义常量的区别
在 C 语言中,#define 和 const 都可以用于定义常量,但它们在多个方面存在显著的区别。以下是对两者区别的详细阐述:
3.1 执行时机差异
- #define:
- #define 是一种预处理指令,属于预处理阶段的一部分。在编译之前,预处理器会扫描源代码,查找所有使用 #define 定义的宏,并将这些宏名替换为对应的宏体。这种替换是纯粹的文本替换,不涉及任何语法或语义分析。
- 例如,#define PI 3.14,在预处理阶段,代码中所有的 PI 都会被替换为 3.14。
- const:
- const 是 C 语言的关键字,用于声明常量。与 #define 不同,const 常量的处理是在编译过程中进行的。
- 编译器在编译阶段会对 const 常量进行类型检查和其他语义分析,确保常量的使用符合语法规则。
3.2 类型检查机制
- #define:
- #define 定义常量时不需要指定类型,它只是进行简单的文本替换。
- 由于没有类型信息,编译器在预处理阶段无法对 #define 定义的常量进行类型检查。这可能导致在后续编译过程中出现类型不匹配的错误,增加了调试的难度。
- 例如,#define MAX_VALUE 100,在代码中 MAX_VALUE 可以被当作任何类型使用,编译器不会对其进行类型检查。
- const:
- const 定义常量时需要指定数据类型,如 const int maxUsers = 100;。
- 编译器在编译过程中会对 const 常量进行类型检查,确保常量的使用与其声明的类型一致。这提高了代码的类型安全性,减少了类型错误的发生。
- 如果尝试将 const int 类型的常量赋值给 float 类型的变量,编译器会报错,提示类型不匹配。
3.3 作用域和存储方式
- #define:
- #define 定义的宏没有作用域的限制,它在整个源文件中都有效,除非被显式地 #undef。
- 宏替换是在预处理阶段完成的,不涉及内存分配,因此 #define 定义的常量没有实际的存储位置。
- const:
- const 常量具有块作用域,其作用域取决于其声明的位置。例如,在函数内部声明的 const 常量只在该函数内部有效。
- const 常量在编译时会被分配内存,并且其值在程序的整个运行期间都保持不变。
3.4 调试便利性与代码可读性
- #define:
- 由于 #define 只是文本替换,在调试时可能难以确定宏的实际值。调试器通常不会显示宏的展开结果,这给调试带来了一定的困难。
- 宏的使用可能会降低代码的可读性,特别是当宏的定义比较复杂时,阅读代码的人可能难以理解宏的实际含义。
- const:
- const 常量在调试时可以被调试器识别和显示,方便开发人员进行调试。
- const 常量的使用使代码更加清晰和易读,因为常量的类型和值在声明时就已经明确。
使用建议:
- 在现代 C 语言编程中,推荐使用 const 来定义常量,因为它具有类型安全性、作用域限制和更好的调试支持。
- #define 更适合用于定义一些简单的、不需要类型检查的宏,如宏函数或条件编译中的宏。
4 编程练习
4.1 计算圆的面积与周长
- 目标:
- 编写一个程序,使用 #define 和 const 分别定义圆周率 π 的值(例如 3.14159)。程序将提示用户手动输入圆的半径,然后计算并输出该半径对应的圆的面积和周长。
- 提示:
- 使用 #define 定义 π 为一个宏常量。
- 使用 const 定义另一个 π 为常量变量。
- 分别使用这两个常量计算圆的面积和周长,并输出结果。
- 圆的半径由用户通过键盘输入。
#include <stdio.h>// 使用 #define 定义 π 为一个宏常量
#define PI_MACRO 3.14159// 使用 const 定义另一个 π 为常量变量
const double PI_CONST = 3.14159;int main()
{// 定义半径变量double radius;// 提示用户输入半径printf("请输入圆的半径: ");// 从键盘读取用户输入的半径值scanf("%lf", &radius);// 使用宏常量计算面积和周长double area_macro = PI_MACRO * radius * radius;double circumference_macro = 2 * PI_MACRO * radius;// 使用 const 常量计算面积和周长double area_const = PI_CONST * radius * radius;double circumference_const = 2 * PI_CONST * radius;// 输出使用宏常量计算的结果printf("\n使用 #define 定义的 π 计算:\n");printf("圆的面积: %.2f\n", area_macro);printf("圆的周长: %.2f\n", circumference_macro);// 输出使用 const 常量计算的结果printf("\n使用 const 定义的 π 计算:\n");printf("圆的面积: %.2f\n", area_const);printf("圆的周长: %.2f\n", circumference_const);return 0;
}
程序在 VS code 中的运行结果如下所示:
4.2 程序配置与版本信息管理
- 目标:
- 编写一个程序,使用 #define 定义程序的最大用户数和最大连接数,使用 const 定义程序的版本号,并输出这些信息。
- 提示:
- 使用 #define 定义整数类型的配置常量。
- 使用 const 定义整数类型的版本号常量。
- 在 main 函数中输出这些常量的值。
#include <stdio.h>#define MAX_USERS 100 // 使用 #define 定义最大用户数
#define MAX_CONNECTIONS 50 // 使用 #define 定义最大连接数int main()
{// const 常量也可以放在 main 主函数里面定义const int VERSION = 1; // 使用 const 定义版本号printf("程序配置:\n");printf("最大用户数: %d\n", MAX_USERS);printf("最大连接数: %d\n", MAX_CONNECTIONS);printf("程序版本号: %d\n", VERSION);return 0;
}
程序在 VS code 中的运行结果如下所示: