当前位置: 首页 > news >正文

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 中的运行结果如下所示: 

相关文章:

  • JavaScript 版本号比较
  • 软件设计师/系统架构师---计算机网络
  • C++:在条件判断时何时为if,何时为else (易混淆※※※)
  • Leetcode 3524. Find X Value of Array I
  • NFS服务共享和安装命令的补充
  • 辅助函数构造题目(缓慢更新,遇到更道)
  • next.js 如何实现动态路由?
  • 云点数据读写
  • 【小沐杂货铺】基于Three.JS绘制卫星轨迹Satellite(GIS 、WebGL、vue、react,提供全部源代码)
  • Java编程基础(第四篇:字符串初次介绍)
  • 8、constexpr if、inline、类模版参数推导、lambda的this捕获---c++17
  • PySide6 GUI 学习笔记——常用类及控件使用方法(常用类矩阵QRect)
  • 基于Spring AI Alibaba实现MCP协议的SSE实时流式服务深度解析
  • 力扣刷题 - 203.移除链表元素
  • leetcode(01)森林中的兔子
  • 六、小白如何用Pygame制作一款跑酷类游戏(静态障碍物和金币的添加)
  • 深入浅出:LDAP 协议全面解析
  • LangChain 单智能体模式示例【纯代码】
  • IPv6 公网设置技巧
  • 初识javascript
  • 中国体育报:中国乒协新周期新起点再出发
  • 聚焦“共赢蓝色未来” “海洋命运共同体”上海论坛举行
  • 海南:谈话提醒9名缺点明显或有苗头性、倾向性问题的省管干部
  • 生态环境部:我国正在开展商用乏燃料后处理厂的论证
  • 2024年我国数字阅读用户规模达6.7亿
  • 国际货币基金组织报告:将今年全球经济增长预期由此前的3.3%下调至2.8%