编译预处理和宏¶
约 2017 个字 58 行代码 1 张图片 预计阅读时间 7 分钟
编译预处理¶
"#":编译预处理指令,这不是C语言,其他语言都有!所以后面不加分号! 例子:#include<stdio.h>,#define PI 3.14
宏¶
#define PI 3.14:定义一个宏(是一个符号),PI为名称,3.14是值
范例程序见下
规范¶
- 语法:没有等号,没有分号(因为不是一条C语句,其他语言都是用#编译预处理)
- 值:可以是任何东西,可以空格、标点等:名字再空格后面所有东西
- 其中可以有注释,该注释不会被替换进去
-
没有值的宏:
-
位置: 普通:源代码文件的顶部,或者放在头文件中
宏的所有定义位置
在C或C++中,定义宏(Macro)通常有两种方式:
-
预处理器指令: - 宏定义通常写在源代码文件的顶部,或者放在头文件中。它们使用预处理器指令
- 这种方式定义的宏在编译器进行预处理阶段之前就会被处理。#define来定义。 - 例如,在源文件或头文件中定义一个宏: -
命令行参数: - 你也可以在编译时通过命令行参数定义宏。这在编译器(如GCC)的命令行参数中使用
- 这种方式定义的宏会在预处理阶段被识别和替换。-D选项来实现。 - 例如,在GCC中定义一个宏: -
配置文件: - 在某些项目中,宏定义可能被放在一个专门的配置文件中,然后通过包含该文件来使用这些宏定义。
-
Makefile: - 在使用Makefile构建项目时,可以在Makefile中定义宏,并通过
-D选项传递给编译器。 -
构建系统: - 在使用现代构建系统(如CMake或Bazel)时,宏定义可以在构建脚本中设置,并在编译时传递给编译器。
宏定义的位置取决于你的项目结构和个人偏好。通常,如果宏是项目中多个文件共享的,最好将它们定义在头文件中。如果宏是特定于单个文件的,可以直接在该文件中定义。如果宏的值需要在不同的构建之间改变,那么在命令行或构建系统中定义宏可能更灵活。
当然,让我们更详细地探讨在C/C++项目中定义宏的第2、4和5点:
- 命令行参数
在编译时,你可以通过编译器的命令行参数来定义宏。这通常通过-D选项来实现,后面跟着宏的名称和可选的值。这种方法特别适用于:
- 编译时配置:根据不同的编译选项启用或禁用特定的代码段。
- 环境变量:在不同的开发环境或部署环境中使用不同的宏值。
示例:
在这个例子中,DEBUG和MAX_THREADS被定义为宏,DEBUG没有指定值(通常用来启用调试代码),而MAX_THREADS被赋予了值4。
- Makefile
在Makefile中定义宏可以在构建过程中提供灵活性,特别是当你需要根据不同的目标或平台调整编译选项时。你可以在Makefile中设置宏,然后在编译命令中使用这些宏。
示例:
在这个Makefile中,CFLAGS变量被用来添加编译器标志,包括定义宏USE_FEATURE和VERSION。当你运行make命令时,这些宏会被传递给GCC编译器。
- 构建系统
现代构建系统如CMake提供了强大的宏和变量管理功能,允许你在构建过程中定义和使用宏。这些宏可以用于控制编译选项、源文件包含、依赖关系等。
示例:
# CMakeLists.txt
# 定义一个宏
add_definitions(-DENABLE_DEBUG)
# 设置一个变量,可以作为宏的值
set(MY_VERSION "1.2.3")
# 将变量转换为宏
add_definitions(-DVERSION=${MY_VERSION})
# 指定C++标准
set(CMAKE_CXX_STANDARD 11)
# 构建目标
add_executable(my_program my_program.cpp)
在这个CMake配置文件中,我们使用add_definitions来定义宏和添加编译器标志。set命令用于设置变量,这些变量可以被用作宏的值。add_executable定义了一个可执行文件目标,它将使用前面定义的宏和设置。
使用构建系统定义宏的好处包括:
- 跨平台支持:构建系统通常提供了跨平台的构建配置。
- 依赖管理:自动处理源文件和库的依赖关系。
- 可重用性:构建脚本可以在多个项目中重用,只需少量修改。
这些方法提供了在不同层面上控制宏定义的灵活性,使得项目构建更加模块化和可配置。
- 注释:依然作为C语言注释
范例程序:
#include<stdio.h>
#define PI 3.14
#define PI2 2 * PI //不能写成2PI,因为不是合法的标识符 //一个宏中包含另一个宏
#define FORMAT "%f\n" //宏的值可以是任何东西
#define PRT printf("%f\n", PI); \
printf(FORMAT, PI2 * 3) //上面那一行"\"后面不能有任何东西空格、注释啥也不行 FORMAT文本替换,多行,第一行得有分号,因为被替换处需要分号
int main()
{
PRT;
return 0;
}
/*
也可以这样:
#define PRT printf("%f\n", PI); \
printf(FORMAT, PI2 * 3);
int main()
{
PRT
return 0;
}
注意分号
*/
-
原理: 特别简单的原始的文本替换:编译之前,编译预处理程序(cpp)把文件中所有宏的名字换成值
shell查看gcc编译预处理过程中的文件
.c,.i, .o,.s 分别是什么
这些文件扩展名代表了C/C++编程和编译过程中的不同阶段和类型的文件:
-
.c: - 这是C语言源代码文件的扩展名。它包含了用C语言编写的程序代码。例如,
main.c就是一个C语言源文件。 -
.i: - 这是预处理后的源代码文件的扩展名。当你使用编译器的
-E选项时,它会生成一个包含了预处理指令(如宏展开、条件编译指令、头文件包含等)后的文件。这个文件通常用于调试预处理阶段。 -
.o: - 这是目标文件(Object file)的扩展名。目标文件是编译器编译源代码后生成的中间文件,包含了源代码对应的机器码,但还没有进行链接。目标文件可以被链接器用来生成最终的可执行文件。
-
.s: - 这是汇编代码文件的扩展名。当你使用编译器的
-S选项时,它会生成一个包含了源代码对应的汇编语言代码的文件。这个文件可以被汇编器进一步转换成目标文件。 -
.a: - 这是静态库文件的扩展名。静态库是一组目标文件的集合,它们被打包在一起,可以在编译时被链接到程序中。静态库通常用于共享代码或资源,而不需要在运行时动态加载。
-
.so: - 这是共享库文件(在Linux系统中)的扩展名。共享库是一种特殊的库,它们在运行时被动态加载和链接到程序中。这允许多个程序共享同一份库代码,节省内存并减少磁盘空间。
-
.exe: - 这是可执行文件的扩展名,在Windows系统中使用。可执行文件是编译后的程序,可以直接在操作系统中运行。
-
a.out: - 这是一个传统的可执行文件的名称,在Unix和类Unix系统中使用。在早期的Unix系统中,编译器默认生成的可执行文件被命名为
a.out。尽管现代编译器允许你指定可执行文件的名称,但a.out仍然被用作默认名称,尤其是在某些特定的编译环境或教学示例中。
这些文件类型和扩展名是C/C++编程和编译过程中的基本组成部分,了解它们有助于更好地理解程序的构建和运行过程。
预定义宏: 即C帮我定义好了,我直接使用
__LINE__, __FILE__, __DATE__, __TIME__, __STDC__
示例:
用法¶
带参数的宏¶
语法
注意:
- 定义语句括号内不需要参数类型
- 可以多个参数
- 可以嵌套组合使用
调用,与C函数调用相同。
易错点:“未理解原始文本替换”
所以:一切都要右括号:整个值要有;参数出现的每个地方都要有。
示例:
杂项:- 非常常见
- 用
###两个运算符实现复杂功能,例如产生函数 - 部分宏会被
inline函数代替 - 西方程序员比中国人爱用
- 其他编译预处理指令:
- 条件编译
- error
