Skip to content

编译预处理和宏

约 2017 个字 58 行代码 1 张图片 预计阅读时间 7 分钟

编译预处理

"#":编译预处理指令,这不是C语言,其他语言都有!所以后面不加分号! 例子:#include<stdio.h>#define PI 3.14

#define PI 3.14:定义一个宏(是一个符号),PI为名称,3.14是值

范例程序见下

规范

  • 语法:没有等号,没有分号(因为不是一条C语句,其他语言都是用#编译预处理)
  • 值:可以是任何东西,可以空格、标点等:名字再空格后面所有东西
  • 其中可以有注释,该注释不会被替换进去
  • 没有值的宏:

  • 位置: 普通:源代码文件的顶部,或者放在头文件中

宏的所有定义位置

在C或C++中,定义宏(Macro)通常有两种方式:

  1. 预处理器指令: - 宏定义通常写在源代码文件的顶部,或者放在头文件中。它们使用预处理器指令#define来定义。 - 例如,在源文件或头文件中定义一个宏:

    #define MY_MACRO 123
    
    - 这种方式定义的宏在编译器进行预处理阶段之前就会被处理。

  2. 命令行参数: - 你也可以在编译时通过命令行参数定义宏。这在编译器(如GCC)的命令行参数中使用-D选项来实现。 - 例如,在GCC中定义一个宏:

    gcc -DMY_MACRO=123 source_file.c
    
    - 这种方式定义的宏会在预处理阶段被识别和替换。

  3. 配置文件: - 在某些项目中,宏定义可能被放在一个专门的配置文件中,然后通过包含该文件来使用这些宏定义。

  4. Makefile: - 在使用Makefile构建项目时,可以在Makefile中定义宏,并通过-D选项传递给编译器。

  5. 构建系统: - 在使用现代构建系统(如CMake或Bazel)时,宏定义可以在构建脚本中设置,并在编译时传递给编译器。

宏定义的位置取决于你的项目结构和个人偏好。通常,如果宏是项目中多个文件共享的,最好将它们定义在头文件中。如果宏是特定于单个文件的,可以直接在该文件中定义。如果宏的值需要在不同的构建之间改变,那么在命令行或构建系统中定义宏可能更灵活。

当然,让我们更详细地探讨在C/C++项目中定义宏的第2、4和5点:

  1. 命令行参数

在编译时,你可以通过编译器的命令行参数来定义宏。这通常通过-D选项来实现,后面跟着宏的名称和可选的值。这种方法特别适用于:

  • 编译时配置:根据不同的编译选项启用或禁用特定的代码段。
  • 环境变量:在不同的开发环境或部署环境中使用不同的宏值。

示例

gcc -DDEBUG -DMAX_THREADS=4 -o my_program my_program.c

在这个例子中,DEBUGMAX_THREADS被定义为宏,DEBUG没有指定值(通常用来启用调试代码),而MAX_THREADS被赋予了值4

  1. Makefile

在Makefile中定义宏可以在构建过程中提供灵活性,特别是当你需要根据不同的目标或平台调整编译选项时。你可以在Makefile中设置宏,然后在编译命令中使用这些宏。

示例

CFLAGS += -DUSE_FEATURE -DVERSION='"1.2.3"'

all:
    gcc $(CFLAGS) -o my_program my_program.c

在这个Makefile中,CFLAGS变量被用来添加编译器标志,包括定义宏USE_FEATUREVERSION。当你运行make命令时,这些宏会被传递给GCC编译器。

  1. 构建系统

现代构建系统如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编译预处理过程中的文件

    gcc -g try.c -o try --save-temps
    
    ls -l
    
    tail try.i
    

alt text

.c,.i, .o,.s 分别是什么

这些文件扩展名代表了C/C++编程和编译过程中的不同阶段和类型的文件:

  1. .c: - 这是C语言源代码文件的扩展名。它包含了用C语言编写的程序代码。例如,main.c就是一个C语言源文件。

  2. .i: - 这是预处理后的源代码文件的扩展名。当你使用编译器的-E选项时,它会生成一个包含了预处理指令(如宏展开、条件编译指令、头文件包含等)后的文件。这个文件通常用于调试预处理阶段。

  3. .o: - 这是目标文件(Object file)的扩展名。目标文件是编译器编译源代码后生成的中间文件,包含了源代码对应的机器码,但还没有进行链接。目标文件可以被链接器用来生成最终的可执行文件。

  4. .s: - 这是汇编代码文件的扩展名。当你使用编译器的-S选项时,它会生成一个包含了源代码对应的汇编语言代码的文件。这个文件可以被汇编器进一步转换成目标文件。

  5. .a: - 这是静态库文件的扩展名。静态库是一组目标文件的集合,它们被打包在一起,可以在编译时被链接到程序中。静态库通常用于共享代码或资源,而不需要在运行时动态加载。

  6. .so: - 这是共享库文件(在Linux系统中)的扩展名。共享库是一种特殊的库,它们在运行时被动态加载和链接到程序中。这允许多个程序共享同一份库代码,节省内存并减少磁盘空间。

  7. .exe: - 这是可执行文件的扩展名,在Windows系统中使用。可执行文件是编译后的程序,可以直接在操作系统中运行。

  8. a.out: - 这是一个传统的可执行文件的名称,在Unix和类Unix系统中使用。在早期的Unix系统中,编译器默认生成的可执行文件被命名为a.out。尽管现代编译器允许你指定可执行文件的名称,但a.out仍然被用作默认名称,尤其是在某些特定的编译环境或教学示例中。

这些文件类型和扩展名是C/C++编程和编译过程中的基本组成部分,了解它们有助于更好地理解程序的构建和运行过程。

预定义宏: 即C帮我定义好了,我直接使用

__LINE____FILE____DATE____TIME____STDC__

示例:

printf("%s:%d\n", __FILE__, __LINE__);
printf("%s, %s\n", __DATE__, __TIME__);

用法

带参数的宏

语法

#define cube(x) ((x) * (x) * (x)) //后面会被替换
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define PRINT(msg) printf(msg)
//使用:
printf("hello world");

注意

  • 定义语句括号内不需要参数类型
  • 可以多个参数
  • 可以嵌套组合使用

调用,与C函数调用相同。

易错点:“未理解原始文本替换”

所以:一切都要右括号:整个值要有;参数出现的每个地方都要有。

示例:

#define f(x) ((x) * 5)  //对
#define f(x) (x * 5)  //错
#define f(x) (x) * 5  //错
杂项:

  • 非常常见
  • # ##两个运算符实现复杂功能,例如产生函数
  • 部分宏会被inline函数代替
  • 西方程序员比中国人爱用
  • 其他编译预处理指令:
    • 条件编译
    • error

颜色主题调整