文章目录
- 一、预处理概述
- 二、文件包含与代码组织
-
- [1. `#include` 的两种写法](#include` 的两种写法)
- [2. 头文件重复包含问题](#2. 头文件重复包含问题)
-
- [方案 A:标准宏卫兵 (兼容性好)](#方案 A:标准宏卫兵 (兼容性好))
- [方案 B:使用`#pragma once` (简洁,效率高)](#pragma once` (简洁,效率高))
- [方案 C:使用 `_Pragma` 操作符 (C99/C++11)](#方案 C:使用
_Pragma操作符 (C99/C++11))
- 三、宏定义 (`#define`))
- 四、条件编译
- 五、预定义宏
一、预处理概述
在 C++ 代码被编译成机器指令之前,会先进行一次预处理 。预处理器处理所有以 # 开头的指令。它的工作本质上是文本替换:在编译器介入之前,把代码"整理"好。
主要功能:
- 文件包含:把头文件的内容复制到源文件中 (
#include)。 - 宏定义:字符串替换 (
#define)。 - 条件编译:决定哪些代码参与编译,哪些被忽略 (
#ifdef,#ifndef)。
二、文件包含与代码组织
为了方便维护,C++ 项目通常将声明放入头文件 (.h/.hpp),将实现放入源文件(.cpp)。预处理器负责将它们组装在一起。
1. #include 的两种写法
#include <filename>:- 引用标准库 或系统头文件。
- 编译器去系统环境变量指定的目录 查找(如
<iostream>,<vector>)。
#include "filename":- 引用用户自定义头文件。
- 编译器优先在当前项目目录 查找,找不到再去系统目录找。
2. 头文件重复包含问题
如果 A.h 包含了 B.h,而 C.cpp 同时包含了 A.h 和 B.h,那么 B.h 的内容会被复制两次。这会导致"重定义"错误。 我们需要使用头文件保护。
方案 A:标准宏卫兵 (兼容性好)
利用条件编译指令来防止重复复制。
cpp
// math_utils.h
#ifndef MATH_UTILS_H // 1. 检查宏是否未定义
#define MATH_UTILS_H // 2. 如果没定义,则定义它
// ... 声明内容 ...
int add(int a, int b);
#endif // 3. 结束检查
方案 B:使用#pragma once (简洁,效率高)
- 放在文件开头位置。
#pragma once不涉及宏定义,效率更高。- 非标准但被绝大多数现代编译器支持。
cpp
// math_utils.h
#pragma once
// ... 声明内容 ...
方案 C:使用 _Pragma 操作符 (C99/C++11)
cpp
_Pragma("once")
// ... 头文件内容 ...
_Pragma 可以和宏搭配使用,功能更强。
三、宏定义 (#define)
C++源程序中允许用一个标识符来表示一个字符串 ,称为"宏"。被定义为宏的标识符称为"宏名"。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换 或宏展开。
宏定义是由源程序中的宏定义命令完成的,宏替换是由预处理程序自动完成的。宏仅仅是单纯的文本替换,不进行类型检查,使用时需格外小心。
1.无参宏(常量宏)
常用于定义魔法数字。但在现代 C++ 中,建议使用 const 或 constexpr 代替。
cpp
#define PI 3.14
// 预处理后,代码中所有的PI都会变成3.14
2.带参宏(宏函数)
类似函数调用,但省去了函数调用的入栈出栈开销。
语法 : #define 宏名(参数表) 表达式
由于是文本替换,为了防止运算优先级错误,所有参数和整体表达式都必须加括号。
cpp
// 错误写法
#define SQUARE(x) x * x
// 调用 SQUARE(1+1) -> 1+1 * 1+1 -> 1+1+1 = 3 (错误!)
// 正确写法
#define SQUARE(x) ((x) * (x))
// 调用 SQUARE(1+1) -> ((1+1) * (1+1)) = 4 (正确)
注意 :宏函数没有类型检查,且容易产生副作用(例如传入 i++ 时可能会被执行两次),现代 C++ 推荐使用
inline函数或template。
3. 取消宏 (#undef)
cpp
#define MAX_WIDTH 100
// ... 使用宏 ...
#undef MAX_WIDTH // 从这里开始,MAX_WIDTH 不再有效
四、条件编译
在 C++中,一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是"条件编译"。
条件编译允许我们根据不同的环境生成不同的代码(例如区分 Windows/Linux,或者 Debug/Release 模式)。
1.基本指令
#if/#elif/#else/#endif:基于常量表达式判断。#ifdef标识符:如果定义了该宏。#ifndef标识符:如果没定义该宏。
2. 实战场景
场景一:跨平台代码
cpp
#ifdef _WIN32
#include <windows.h>
void clearScreen() { system("cls"); }
#elif __linux__
#include <unistd.h>
void clearScreen() { system("clear"); }
#else
void clearScreen() { /* 不支持的平台 */ }
#endif
场景二:调试开关
通过定义 DEBUG 宏(通常在编译器参数中设置,如 g++ -DDEBUG ...)来控制日志输出。
cpp
#ifdef DEBUG
#define LOG(msg) std::cout << "[DEBUG] " << msg << std::endl
#else
#define LOG(msg) // 发布版本中,LOG 被替换为空,不产生任何代码
#endif
int main() {
LOG("程序启动"); // 只有在定义了 DEBUG 时才会输出
return 0;
}
五、预定义宏
编译器内置了一些特殊的宏,常用于错误排查。
__FILE__:当前源文件名(字符串)。__LINE__:当前代码行号(整数)。__DATE__:编译日期。__TIME__:编译时间。__cplusplus:C++ 标准版本号。