预处理器
# 第 17 章 C++ 预处理器
# 目录介绍
- 17.1 头文件
- 17.2 宏定义
- 17.3 头文件保护
- 17.4 预定义宏
- 17.5 条件编译
- 17.6 错误/警告指令
- 17.7 行控制
- 17.8 空指令
- 17.9 综合宏案例
- 17.10 预处理器底层原理
- 17.11 预处理器训练题
- 17.12 综合思考题
# 17.1 头文件
# 17.1.1 头文件作用
声明与定义分离:头文件通常包含类、函数、变量的声明,而源文件(.cpp)包含具体的实现。
代码复用:通过包含头文件,可以在多个源文件中复用相同的代码。
模块化:将代码划分为多个模块,便于管理和维护。
# 17.1.2 基本结构
一个典型的头文件包含以下内容:
- 防止重复包含的预处理指令(#ifndef, #define, #endif)。
- 类、函数、变量的声明。
- 必要的库头文件包含。
# 17.1.3 头文件包含规则
#include 用于将其他文件的内容插入到当前文件中。通常用于包含头文件。
#include <iostream> // 包含标准库头文件
#include "myheader.h" // 包含用户定义的头文件
2
< >:用于包含标准库头文件,编译器会在系统路径中查找。" ":用于包含用户定义的头文件,编译器会先在当前目录中查找,然后在系统路径中查找。
防止重复包含,使用预处理指令防止头文件被重复包含:
#ifndef HEADER_NAME_H
#define HEADER_NAME_H
// 头文件内容
#endif
2
3
4
(4)避免循环包含,如果两个头文件相互包含,会导致编译错误。可以通过前置声明(Forward Declaration)解决。
# 17.1.4 综合案例与思考
下面通过一个"模块化计算器"案例,综合演示头文件的声明与定义分离、包含规则以及前置声明:
// ===== calculator.h =====
#ifndef CALCULATOR_H
#define CALCULATOR_H
#include <string> // 包含标准库头文件
// 前置声明(避免不必要的头文件包含)
class Logger;
class Calculator {
public:
double add(double a, double b);
double subtract(double a, double b);
double multiply(double a, double b);
double divide(double a, double b);
std::string getHistory() const;
private:
std::string history_;
};
#endif // CALCULATOR_H
// ===== calculator.cpp =====
#include "calculator.h" // 包含自己的头文件
#include <stdexcept> // 标准库头文件
#include <sstream>
double Calculator::add(double a, double b) {
double result = a + b;
history_ += std::to_string(a) + "+" + std::to_string(b) + "=" + std::to_string(result) + "\n";
return result;
}
double Calculator::divide(double a, double b) {
if (b == 0) throw std::invalid_argument("除数不能为零");
double result = a / b;
history_ += std::to_string(a) + "/" + std::to_string(b) + "=" + std::to_string(result) + "\n";
return result;
}
std::string Calculator::getHistory() const { return history_; }
// ===== main.cpp =====
#include "calculator.h" // 用 "" 包含用户头文件
#include <iostream> // 用 <> 包含标准库头文件
int main() {
Calculator calc;
std::cout << "3 + 5 = " << calc.add(3, 5) << std::endl;
std::cout << "10 / 3 = " << calc.divide(10, 3) << std::endl;
std::cout << "历史记录:\n" << calc.getHistory();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
案例知识融合:本案例展示了头文件的核心最佳实践。calculator.h 包含类声明和防重复包含的 #ifndef 保护;用 <> 包含标准库头文件,用 "" 包含用户头文件;Logger 使用前置声明避免不必要的头文件依赖,减少编译时间和循环包含风险。头文件只包含声明,.cpp 文件包含实现,实现了声明与定义分离,使得修改实现不需要重新编译所有引用该头文件的文件。
思考题:
#include <iostream>和#include "iostream"都能编译通过,它们的查找路径有什么区别?为什么标准库建议用<>?- 如果
calculator.h没有#ifndef保护,在main.cpp中两次#include "calculator.h"会发生什么? - 什么情况下必须包含完整头文件而不能只用前置声明?(提示:考虑
sizeof、继承、成员变量等场景)
# 17.2 宏定义
# 17.2.1 什么是宏
在C++中,宏定义(Macro)是使用预处理器指令 #define 定义的符号常量或代码片段。宏在编译之前由预处理器处理,直接替换代码中的宏名称。
# 17.2.2 基本语法
#define MACRO_NAME value
MACRO_NAME:宏的名称,通常使用大写字母。value:宏的值,可以是常量、表达式或代码片段。
示例:定义常量
#define PI 3.14159
示例:定义代码片段
#define SQUARE(x) ((x) * (x))
# 17.2.3 宏的使用
示例:使用常量宏
double area = PI * radius * radius;
预处理器会将其替换为:
double area = 3.14159 * radius * radius;
示例:使用代码片段宏
int result = SQUARE(5);
预处理器会将其替换为:
int result = ((5) * (5));
# 17.2.4 宏的作用域
宏没有作用域,全局有效。可以使用 #undef 取消宏定义:
#define PI 3.14159
#undef PI
2
# 17.2.5 宏与函数区别
宏是文本替换,没有类型检查。
函数有类型检查,更安全。
# 17.2.6 宏的优缺点
优点
- 简单易用,直接替换代码。
- 可以用于条件编译和调试。
缺点
- 没有类型检查,容易出错。
- 难以调试,因为宏在预处理阶段被替换。
- 可能导致代码可读性差。
# 17.2.7 综合案例与思考
下面通过一个"调试工具宏"案例,综合演示宏定义、带参宏、宏作用域以及宏与函数的对比:
#include <iostream>
#include <string>
using namespace std;
// 1. 常量宏
#define VERSION "1.0.0"
#define MAX_BUFFER_SIZE 1024
// 2. 带参宏(注意括号保护)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define CLAMP(val, lo, hi) (MIN(MAX((val), (lo)), (hi)))
// 3. 多语句宏(do-while-0 技巧)
#define PRINT_VAR(var) \
do { \
cout << #var << " = " << (var) << endl; \
} while(0)
// 4. 字符串化和连接
#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b
#define MAKE_UNIQUE_NAME(prefix) CONCAT(prefix, __LINE__)
// 5. 对比:inline 函数替代宏
template<typename T>
inline T safeMax(const T& a, const T& b) { return (a > b) ? a : b; }
int main() {
cout << "版本: " << VERSION << endl;
cout << "缓冲区大小: " << MAX_BUFFER_SIZE << endl;
// 带参宏使用
int x = 15, y = 8;
PRINT_VAR(x);
PRINT_VAR(y);
cout << "MAX(x,y) = " << MAX(x, y) << endl;
cout << "CLAMP(20, 0, 10) = " << CLAMP(20, 0, 10) << endl;
// 宏的陷阱:副作用
int a = 5;
int result = MAX(a++, 3); // a 可能被递增两次!
cout << "MAX(a++, 3) 后 a=" << a << ", result=" << result << endl;
// inline 函数无副作用
int b = 5;
int safeResult = safeMax(b++, 3); // b 只递增一次
cout << "safeMax(b++, 3) 后 b=" << b << ", result=" << safeResult << endl;
// 字符串化
cout << "STRINGIFY(Hello World) = " << STRINGIFY(Hello World) << endl;
// 连接生成唯一变量名
int MAKE_UNIQUE_NAME(temp_) = 42;
cout << "连接宏生成的变量值: " << MAKE_UNIQUE_NAME(temp_) << endl;
// 取消宏定义
#undef MAX_BUFFER_SIZE
// cout << MAX_BUFFER_SIZE; // 编译错误:宏已被 #undef
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
案例知识融合:本案例覆盖了宏定义的所有核心知识。常量宏替代魔法数字提高可读性;带参宏通过括号保护避免运算符优先级问题;do-while-0 技巧保证多语句宏在 if-else 中使用安全;#(字符串化)和 ##(连接)是宏的高级特性;通过 a++ 副作用的对比实验,直观展示了宏与 inline 函数的关键差异——宏只是文本替换没有类型检查,而 inline 函数有完整的语义保障。#undef 可以取消宏定义限制其作用域。
思考题:
MAX(a++, 3)展开后是什么?为什么a可能被递增两次?如何用 C++ 模板或 inline 函数避免这个问题?CLAMP宏嵌套调用了MIN和MAX,如果参数有副作用,问题会如何放大?- 现代 C++ 中,哪些场景仍然必须用宏而无法用
constexpr、inline或模板替代?
# 17.3 头文件保护
# 17.3.1 理解头文件保护
#ifndef, #define, 和 #endif 是 C++ 中的预处理指令,用于防止头文件被多次包含(即防止重复包含)。 这种技术称为 头文件保护 或 包含保护。
# 17.3.2 头文件保护作用
在 C++ 中,如果一个头文件被多次包含(例如,多个源文件都包含了同一个头文件),可能会导致重复定义错误。头文件保护通过条件编译来避免这种情况。
# 17.3.3 示例说明
示例
// File: MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
class MyClass {
public:
void doSomething();
};
#endif // MYCLASS_H
2
3
4
5
6
7
8
9
10
#ifndef MYCLASS_H:检查是否未定义MYCLASS_H宏。如果未定义,则继续编译;如果已定义,则跳过整个头文件内容。#define MYCLASS_H:定义MYCLASS_H宏,表示该头文件已被包含。#endif:结束条件编译块。
优点
- 避免重复定义:如果多个源文件包含了同一个头文件,头文件保护可以防止类、函数或变量的重复定义。
- 提高编译效率:避免重复编译相同的内容,减少编译时间。
- 防止编译错误:避免因重复包含导致的编译错误,例如重复定义类或函数。
# 17.3.4 不用头文件保护
如果头文件没有使用头文件保护,可能会导致以下问题:
- 重复定义错误:如果多个源文件包含了同一个头文件,编译器会多次处理头文件的内容,导致重复定义错误。 例如:
// File: MyClass.h
class MyClass {
public:
void doSomething();
};
// File: main.cpp
#include "MyClass.h"
#include "MyClass.h" // 重复包含,导致编译错误
2
3
4
5
6
7
8
9
编译效率降低:头文件的内容会被多次编译,增加编译时间。
难以调试:重复定义错误可能难以定位,尤其是在大型项目中。
# 17.3.5 使用场景
所有头文件都应使用头文件保护,以确保代码的健壮性和可维护性。
# 17.3.6 头文件保护原理
头文件保护通过条件编译指令实现:
#ifndef MYCLASS_H:检查MYCLASS_H是否未定义。#define MYCLASS_H:定义MYCLASS_H,表示头文件已被包含。#endif:结束条件编译块。
当第一次包含头文件时,MYCLASS_H 未定义,头文件内容会被编译。
当第二次包含头文件时,MYCLASS_H 已定义,头文件内容会被跳过。
# 17.3.7 头文件保护总结
| 特性 | 使用头文件保护 | 不用头文件保护 |
|---|---|---|
| 重复定义错误 | 避免 | 可能导致 |
| 编译效率 | 提高 | 降低 |
| 代码健壮性 | 更健壮 | 容易出错 |
| 使用场景 | 所有头文件 | 不推荐 |
# 17.3.8 综合案例与思考
下面通过一个"多文件项目"案例,综合演示头文件保护的必要性、#ifndef/#pragma once 的对比以及典型错误:
// ===== math_utils.h =====
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
struct Point {
double x, y;
};
double distance(const Point& a, const Point& b);
// 内联函数可以安全地放在头文件中
inline double square(double x) { return x * x; }
#endif // MATH_UTILS_H
// ===== shape.h =====
#ifndef SHAPE_H
#define SHAPE_H
#include "math_utils.h" // 需要 Point 的定义
class Shape {
public:
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
Point center_;
double radius_;
public:
Circle(Point c, double r) : center_(c), radius_(r) {}
double area() const override { return 3.14159 * square(radius_); }
Point getCenter() const { return center_; }
};
#endif // SHAPE_H
// ===== main.cpp =====
#include "math_utils.h" // 第一次包含
#include "shape.h" // shape.h 内部也包含了 math_utils.h
// 没有头文件保护的话,Point 会被重复定义!
#include <iostream>
#include <cmath>
using namespace std;
double distance(const Point& a, const Point& b) {
return sqrt(square(a.x - b.x) + square(a.y - b.y));
}
int main() {
Point p1{0, 0}, p2{3, 4};
cout << "距离: " << distance(p1, p2) << endl;
Circle c({1, 2}, 5.0);
cout << "面积: " << c.area() << endl;
cout << "圆心: (" << c.getCenter().x << ", " << c.getCenter().y << ")" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
输出结果:
距离: 5
面积: 78.5397
圆心: (1, 2)
2
3
案例知识融合:本案例展示了真实项目中头文件保护的必要性。main.cpp 直接包含了 math_utils.h,同时 shape.h 内部也包含了 math_utils.h——如果没有 #ifndef 保护,Point 结构体会被编译器看到两次,导致"重复定义"编译错误。头文件保护的原理是:第一次包含时 MATH_UTILS_H 未定义,宏被定义并编译内容;第二次包含时 MATH_UTILS_H 已定义,#ifndef 条件为假,整个内容被跳过。inline 函数可以安全地放在头文件中,因为编译器允许多个翻译单元中存在相同的 inline 定义。
思考题:
#pragma once和#ifndef/#define/#endif各有什么优缺点?为什么一些项目仍然坚持使用传统的#ifndef方式?- 如果
math_utils.h中没有头文件保护,但Point只在shape.h中通过前置声明struct Point;使用(不包含完整头文件),是否能避免重复定义问题? - 头文件保护能防止同一翻译单元内的重复包含,但能防止不同
.cpp文件中的重复定义吗?(提示:如果头文件中定义了非 inline 的全局函数会怎样?)
# 17.4 预定义宏
C++ 中的预定义宏是由编译器在编译时自动定义的宏,用于提供有关编译环境、编译器、操作系统、标准库等信息。这些宏可以帮助开发者编写跨平台的代码或根据不同的编译环境调整代码行为。
# 17.4.1 标准预定义宏
这些宏由 C++ 标准定义,所有符合标准的编译器都支持。
| 宏名 | 描述 |
|---|---|
__cplusplus | 表示 C++ 标准的版本。例如,C++11 为 201103L,C++14 为 201402L,C++17 为 201703L,C++20 为 202002L。 |
__DATE__ | 当前源文件的编译日期,格式为 "Mmm dd yyyy"(例如 "Oct 15 2023")。 |
__TIME__ | 当前源文件的编译时间,格式为 "hh:mm:ss"(例如 "14:30:45")。 |
__FILE__ | 当前源文件的文件名(包括路径)。 |
__LINE__ | 当前代码行的行号。 |
__func__ | 当前函数的名称(C++11 引入)。 |
__STDC__ | 如果编译器符合 C 标准,则定义为 1,否则未定义。 |
__STDC_HOSTED__ | 如果编译器是托管环境(支持完整的标准库),则定义为 1,否则为 0。 |
__STDC_VERSION__ | 表示 C 标准的版本(仅适用于 C 代码)。 |
# 17.4.2 编译器相关宏
这些宏由特定的编译器定义,用于标识编译器及其版本。
| 宏名 | 描述 |
|---|---|
__GNUC__ | GCC 编译器的主版本号。 |
__GNUC_MINOR__ | GCC 编译器的次版本号。 |
__GNUC_PATCHLEVEL__ | GCC 编译器的补丁版本号。 |
__clang__ | 如果使用 Clang 编译器,则定义为 1。 |
__clang_major__ | Clang 编译器的主版本号。 |
__clang_minor__ | Clang 编译器的次版本号。 |
__clang_patchlevel__ | Clang 编译器的补丁版本号。 |
_MSC_VER | Microsoft Visual C++ 编译器的版本号。例如,MSVC 2019 为 1920。 |
_MSC_FULL_VER | Microsoft Visual C++ 编译器的完整版本号。 |
__INTEL_COMPILER | Intel 编译器的版本号。 |
# 17.4.3 操作系统相关宏
这些宏用于标识目标操作系统。
| 宏名 | 描述 |
|---|---|
_WIN32 | 如果目标系统是 Windows(32 位或 64 位),则定义为 1。 |
_WIN64 | 如果目标系统是 64 位 Windows,则定义为 1。 |
__linux__ | 如果目标系统是 Linux,则定义为 1。 |
__APPLE__ | 如果目标系统是 macOS 或 iOS,则定义为 1。 |
__unix__ | 如果目标系统是 Unix 或类 Unix 系统,则定义为 1。 |
__ANDROID__ | 如果目标系统是 Android,则定义为 1。 |
__CYGWIN__ | 如果目标系统是 Cygwin(Windows 上的 Unix 环境),则定义为 1。 |
# 17.4.4 标准库相关宏
这些宏用于标识使用的标准库及其版本。
| 宏名 | 描述 |
|---|---|
__GLIBC__ | GNU C 库(glibc)的主版本号。 |
__GLIBC_MINOR__ | GNU C 库(glibc)的次版本号。 |
_LIBCPP_VERSION | LLVM 的 libc++ 标准库版本号。 |
_MSVC_STL_VERSION | Microsoft Visual C++ 标准库版本号。 |
# 17.4.5 其他常用宏
| 宏名 | 描述 |
|---|---|
NDEBUG | 如果定义了 NDEBUG,则禁用 assert 宏。通常用于发布模式。 |
__has_include | 检查是否包含某个头文件(C++17 引入)。 |
__has_cpp_attribute | 检查是否支持某个 C++ 属性(C++20 引入)。 |
# 17.4.6 预定义宏案例
以下是一个使用预定义宏的示例:
#include <iostream>
int main() {
std::cout << "C++ version: " << __cplusplus << std::endl;
std::cout << "Compilation date: " << __DATE__ << std::endl;
std::cout << "Compilation time: " << __TIME__ << std::endl;
std::cout << "Current file: " << __FILE__ << std::endl;
std::cout << "Current line: " << __LINE__ << std::endl;
std::cout << "Current function: " << __func__ << std::endl;
#ifdef __GNUC__
std::cout << "GCC version: " << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__ << std::endl;
#endif
#ifdef _MSC_VER
std::cout << "MSVC version: " << _MSC_VER << std::endl;
#endif
#ifdef __linux__
std::cout << "Running on Linux" << std::endl;
#elif _WIN32
std::cout << "Running on Windows" << std::endl;
#elif __APPLE__
std::cout << "Running on macOS" << std::endl;
#endif
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 17.4.7 综合案例与思考
下面通过一个"跨平台诊断工具"案例,综合演示各类预定义宏的实际应用:
#include <iostream>
#include <string>
using namespace std;
// 利用预定义宏构建诊断信息
#define DIAG_INFO() \
cout << "=== 编译诊断信息 ===" << endl; \
cout << "文件: " << __FILE__ << endl; \
cout << "行号: " << __LINE__ << endl; \
cout << "函数: " << __func__ << endl; \
cout << "编译日期: " << __DATE__ << " " << __TIME__ << endl;
// 自定义断言宏(利用 __FILE__, __LINE__, __func__)
#define MY_ASSERT(expr) \
do { \
if (!(expr)) { \
cerr << "断言失败: " #expr << endl; \
cerr << " 位置: " << __FILE__ << ":" << __LINE__ << endl; \
cerr << " 函数: " << __func__ << endl; \
} \
} while(0)
// 检测 C++ 标准版本
string getCppVersion() {
#if __cplusplus >= 202002L
return "C++20";
#elif __cplusplus >= 201703L
return "C++17";
#elif __cplusplus >= 201402L
return "C++14";
#elif __cplusplus >= 201103L
return "C++11";
#else
return "C++03 或更早";
#endif
}
// 检测编译器
string getCompiler() {
#if defined(__clang__)
return "Clang " + to_string(__clang_major__) + "." + to_string(__clang_minor__);
#elif defined(__GNUC__)
return "GCC " + to_string(__GNUC__) + "." + to_string(__GNUC_MINOR__);
#elif defined(_MSC_VER)
return "MSVC " + to_string(_MSC_VER);
#else
return "未知编译器";
#endif
}
// 检测操作系统
string getOS() {
#if defined(_WIN32)
return "Windows";
#elif defined(__APPLE__)
return "macOS";
#elif defined(__linux__)
return "Linux";
#elif defined(__ANDROID__)
return "Android";
#else
return "未知操作系统";
#endif
}
void testFunction() {
DIAG_INFO();
cout << "\nC++标准: " << getCppVersion() << endl;
cout << "编译器: " << getCompiler() << endl;
cout << "操作系统: " << getOS() << endl;
// 断言测试
int value = 42;
MY_ASSERT(value == 42); // 通过
MY_ASSERT(value > 100); // 失败,输出诊断信息
}
int main() {
testFunction();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
案例知识融合:本案例将各类预定义宏整合为一个实用的诊断工具。__FILE__、__LINE__、__func__ 用于精确定位代码位置,构建自定义断言宏;__DATE__ 和 __TIME__ 记录编译时间用于版本追踪;__cplusplus 检测 C++ 标准版本实现条件编译兼容;编译器宏(__GNUC__、__clang__、_MSC_VER)和操作系统宏(_WIN32、__APPLE__、__linux__)用于编写跨平台代码。这些宏都在编译期展开,不会产生运行时开销。
思考题:
__LINE__在宏定义中和在宏调用处展开的行号不同,为什么MY_ASSERT中的__LINE__显示的是调用处而非定义处的行号?__func__和__FUNCTION__有什么区别?哪个是 C++ 标准规定的?- 如果一个项目需要同时支持 GCC 和 MSVC,使用预定义宏做条件编译时需要注意什么?能否用
__has_include来检测特性支持而非检测编译器?
# 17.5 条件编译
C++ 中的 条件编译 是一种在编译时根据特定条件选择性地包含或排除代码的技术。它通过预处理器指令(如 #if、#ifdef、#ifndef、#else、#elif 和 #endif)实现。
条件编译通常用于编写跨平台代码、启用或禁用调试信息、根据编译环境调整代码行为等。
# 17.5.1 基本语法
以下是条件编译的基本语法:
#if condition
// 如果 condition 为真,编译此部分代码
#elif another_condition
// 如果 another_condition 为真,编译此部分代码
#else
// 如果前面的条件都不为真,编译此部分代码
#endif
2
3
4
5
6
7
# 17.5.2 常用指令
| 指令 | 描述 |
|---|---|
#if | 如果条件为真,编译后续代码。 |
#ifdef | 如果宏已定义,编译后续代码。 |
#ifndef | 如果宏未定义,编译后续代码。 |
#else | 如果前面的条件为假,编译后续代码。 |
#elif | 如果前面的条件为假且当前条件为真,编译后续代码。 |
#endif | 结束条件编译块。 |
#define | 定义宏。 |
#undef | 取消定义宏。 |
# 17.5.3 示例代码案例
示例 1:根据平台选择代码
#include <iostream>
int main() {
#ifdef _WIN32
std::cout << "Running on Windows" << std::endl;
#elif __linux__
std::cout << "Running on Linux" << std::endl;
#elif __APPLE__
std::cout << "Running on macOS" << std::endl;
#else
std::cout << "Unknown platform" << std::endl;
#endif
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
示例 2:启用或禁用调试信息
#include <iostream>
#define DEBUG
int main() {
#ifdef DEBUG
std::cout << "Debug mode is enabled" << std::endl;
#else
std::cout << "Debug mode is disabled" << std::endl;
#endif
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 17.6 错误/警告指令
C++ 中的 错误/警告指令 是预处理器指令,用于在编译时生成自定义的错误或警告信息。这些指令可以帮助开发者在代码中标记潜在问题、强制约束条件或提供调试信息。
# 17.6.1 错误指令
#error 指令用于在编译时生成一个错误消息,并终止编译过程。通常用于强制检查某些条件或标记不支持的配置。
语法
#error "错误消息"
示例
#if __cplusplus < 201103L
#error "This code requires C++11 or later."
#endif
2
3
如果编译器不支持 C++11,编译时会输出错误消息并终止:
error: This code requires C++11 or later.
# 17.6.2 警告指令
#warning 指令用于在编译时生成一个警告消息,但不会终止编译过程。通常用于提醒开发者注意某些问题或潜在风险。
语法
#warning "警告消息"
示例
#ifdef DEBUG
#warning "Debug mode is enabled. Performance may be affected."
#endif
2
3
如果 DEBUG 宏已定义,编译时会输出警告消息:
warning: Debug mode is enabled. Performance may be affected.
# 17.6.3 使用的场景
场景 1:检查编译器版本
#if __cplusplus < 201703L
#error "This code requires C++17 or later."
#endif
2
3
场景 2:检查平台支持
#ifndef _WIN32
#error "This code is only supported on Windows."
#endif
2
3
场景 3:提醒未完成的功能
#warning "This feature is under development and may not work as expected."
场景 4:检查宏定义
#ifndef MY_CUSTOM_MACRO
#error "MY_CUSTOM_MACRO must be defined."
#endif
2
3
场景 5:提醒弃用的 API
#warning "This API is deprecated. Use the new API instead."
# 17.6.4 注意事项
#error和#warning的区别:#error会终止编译过程,而#warning` 只会生成警告消息,编译会继续。- 跨编译器支持:
#error是所有 C++ 编译器都支持的,但#warning并非所有编译器都支持(例如 MSVC 不支持#warning)。 - 消息内容:错误或警告消息应尽量清晰明确,帮助开发者快速定位问题。
# 17.6.5 综合案例与思考
下面通过一个"编译期安全检查框架"案例,综合演示 #error、#warning 在实际项目中的应用:
#include <iostream>
using namespace std;
// ===== 编译期安全检查 =====
// 1. 强制要求 C++11 或以上
#if __cplusplus < 201103L
#error "本项目要求 C++11 或更高版本,请使用 -std=c++11 编译"
#endif
// 2. 强制要求定义目标平台
#if !defined(_WIN32) && !defined(__linux__) && !defined(__APPLE__)
#error "未检测到支持的操作系统,请在 Windows/Linux/macOS 上编译"
#endif
// 3. 检查必要的配置宏
#ifndef APP_VERSION
// 提供默认值,但给出警告
#define APP_VERSION "0.0.0-dev"
#ifdef __GNUC__
#warning "APP_VERSION 未定义,使用默认值 0.0.0-dev"
#endif
#endif
// 4. 弃用功能警告
#ifdef USE_OLD_API
#ifdef __GNUC__
#warning "USE_OLD_API 已弃用,请迁移到新 API。将在 2.0 版本移除。"
#endif
#endif
// 5. 调试模式提醒
#ifdef DEBUG
#ifdef __GNUC__
#warning "调试模式已开启,请勿在生产环境使用此构建"
#endif
#endif
// 6. 编译期静态检查(C++11 static_assert 配合)
static_assert(sizeof(int) >= 4, "本程序要求 int 至少为 4 字节");
static_assert(sizeof(void*) == 8 || sizeof(void*) == 4,
"仅支持 32 位或 64 位平台");
// 7. 条件编译 + 错误指令:互斥配置检查
#if defined(USE_OPENGL) && defined(USE_VULKAN)
#error "不能同时启用 OpenGL 和 Vulkan,请只选择一个渲染后端"
#endif
int main() {
cout << "编译通过!所有安全检查已通过。" << endl;
cout << "APP_VERSION: " << APP_VERSION << endl;
cout << "指针大小: " << sizeof(void*) << " 字节 ("
<< (sizeof(void*) == 8 ? "64位" : "32位") << ")" << endl;
#ifdef _WIN32
cout << "平台: Windows" << endl;
#elif __APPLE__
cout << "平台: macOS" << endl;
#elif __linux__
cout << "平台: Linux" << endl;
#endif
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
案例知识融合:本案例构建了一个实用的编译期安全检查框架。#error 用于硬性约束——不满足条件时终止编译,适用于强制要求 C++ 版本、检测操作系统支持、防止互斥配置同时启用等场景。#warning 用于软性提醒——编译继续但提示开发者注意,适用于弃用 API 迁移提示、默认值警告、调试模式提醒等。案例还展示了 #error/#warning 与 C++11 static_assert 的互补关系——前者在预处理阶段工作,后者在编译阶段工作并支持类型检查。注意 #warning 不是所有编译器都支持(MSVC 不支持),因此用 #ifdef __GNUC__ 保护。
思考题:
#error和static_assert都能在编译期阻止错误代码,它们分别在编译的哪个阶段工作?各适合检查什么类型的条件?#warning在 MSVC 中不被支持,如何编写一个在所有编译器上都能产生警告的替代方案?- 如果想检查"编译器是否支持某个头文件"(如
<filesystem>),应该用什么预定义宏?在 C++17 之前如何处理?
# 17.9 综合宏案例
# 17.9.1 日志记录宏
下面是一个使用 #define 和 do { ... } while (0) 的宏案例,并详细说明其作用和原理。
案例:日志记录宏
#define LOG_IF_ERROR(ret, msg) \
do { \
if (ret != SUCCESS) { \
std::cerr << "ERROR: " << msg << " (Code: " << ret << ")" << std::endl; \
} \
} while (0)
2
3
4
5
6
案例作用
- 功能:该宏用于检查返回值
ret是否表示错误(ret != SUCCESS),如果是,则输出错误信息到标准错误流(std::cerr)。 - 用途:在代码中快速检查函数返回值,并在发生错误时记录日志,方便调试和问题排查。
使用示例
#include <iostream>
#define SUCCESS 0
int SomeFunction() {
// 模拟一个可能失败的操作
return -1; // 返回错误码
}
int main() {
int ret = SomeFunction();
LOG_IF_ERROR(ret, "SomeFunction failed");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
输出:
ERROR: SomeFunction failed (Code: -1)
原理分析
#define是 C/C++ 中的预处理指令,用于定义宏。- 宏在编译前会被预处理器展开为代码。例如,
LOG_IF_ERROR(ret, "SomeFunction failed")会被展开为:do { if (ret != SUCCESS) { std::cerr << "ERROR: " << "SomeFunction failed" << " (Code: " << ret << ")" << std::endl; } } while (0);1
2
3
4
5 do { ... } while (0)是一种常见的宏定义技巧,用于将多行代码封装成一个逻辑块。- 作用:
- 确保宏在展开后能够正确工作,尤其是在条件语句或循环中使用时。
- 避免宏展开后与上下文代码产生语法错误。
- 示例:
如果没有
do { ... } while (0),宏展开后可能会导致问题。例如:展开后:if (condition) LOG_IF_ERROR(ret, "SomeFunction failed"); else DoSomethingElse();1
2
3
4这样语法是正确的。如果没有if (condition) do { if (ret != SUCCESS) { std::cerr << "ERROR: " << "SomeFunction failed" << " (Code: " << ret << ")" << std::endl; } } while (0); else DoSomethingElse();1
2
3
4
5
6
7
8do { ... } while (0),展开后会导致else与if不匹配。
4. 错误日志
- 如果
ret不等于SUCCESS,则输出错误信息到标准错误流(std::cerr)。 - 错误信息包括:
- 自定义的错误描述(
msg)。 - 错误码(
ret)。
- 自定义的错误描述(
优化建议,在错误信息中包含函数名称或行号,方便定位问题:
#define LOG_IF_ERROR(ret, msg) \
do { \
if (ret != SUCCESS) { \
std::cerr << "ERROR: " << msg << " (Code: " << ret << ") at " << __FILE__ << ":" << __LINE__ << std::endl; \
} \
} while (0)
2
3
4
5
6
输出:
ERROR: SomeFunction failed (Code: -1) at example.cpp:10
通过 #define 和 do { ... } while (0),可以定义功能强大且安全的宏。这种技巧在日志记录、错误处理等场景中非常有用,能够提高代码的简洁性和可维护性。
# 17.10 预处理器底层原理
# 17.10.1 预处理的编译阶段
C++源代码的编译分为四个阶段,预处理是第一个阶段:
源代码(.cpp/.h) → 预处理器(cpp) → 编译器(cc1) → 汇编器(as) → 链接器(ld)
↓ ↓ ↓ ↓
展开后的源码 汇编代码(.s) 目标文件(.o) 可执行文件
2
3
预处理器做的工作(全部是文本替换,不理解C++语法):
- 删除注释:
//和/* */被替换为空格 - 处理
#include:将头文件的内容原封不动地插入到当前位置 - 展开宏:将所有
#define定义的宏替换为对应的文本 - 处理条件编译:根据
#if/#ifdef等决定保留或删除代码段 - 处理
#line和#pragma
可以用g++ -E查看预处理后的结果:
g++ -E main.cpp -o main.i
# main.i 可能有数万行(因为#include展开了标准库头文件)
2
# 17.10.2 宏展开的规则
宏展开是纯文本替换,不理解类型、作用域、语法:
#define SQUARE(x) x * x
int a = SQUARE(3); // 展开为: 3 * 3 = 9 ✓
int b = SQUARE(1 + 2); // 展开为: 1 + 2 * 1 + 2 = 5 ✗ (期望9)
2
3
4
解决方案——加括号:
#define SQUARE(x) ((x) * (x))
int b = SQUARE(1 + 2); // 展开为: ((1 + 2) * (1 + 2)) = 9 ✓
2
#和##运算符:
#define STRINGIFY(x) #x // 将参数变为字符串
#define CONCAT(a, b) a##b // 拼接两个标记
cout << STRINGIFY(hello); // 展开为: "hello"
int CONCAT(var, 1) = 42; // 展开为: int var1 = 42;
2
3
4
5
宏的多次扫描:预处理器会重复扫描宏展开的结果,直到没有宏可以展开为止。但为了避免无限递归,宏不能递归展开自身。
# 17.10.3 头文件包含的实现
#include "file"和#include <file>的区别:
"file":先在当前文件所在目录搜索,再搜索系统路径<file>:只在系统路径(-I指定的路径和标准库路径)搜索
#pragma once vs #ifndef保护的底层差异:
// 方式1:传统include guard
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 内容...
#endif
// 方式2:#pragma once
#pragma once
// 内容...
2
3
4
5
6
7
8
9
#ifndef:每次#include都要打开文件、读取内容、检查宏。但如果宏已定义,预处理器可以快速跳过#pragma once:编译器记录文件的唯一标识(路径或inode),同一文件只打开一次。更快但不标准(主流编译器都支持)
头文件展开导致的编译时间问题:
一个简单的#include <iostream>会展开为约2万行代码。如果100个.cpp文件都包含它,预处理器要处理200万行。这就是C++编译慢的主要原因之一。
C++20 Modules的解决方案:模块(import std;)将编译好的接口信息缓存为二进制格式,不需要每次重新解析头文件文本,编译速度提升10-50倍。
# 17.11 预处理器训练题
训练题1:实现跨平台兼容的实用宏
#include <iostream>
#include <cstring>
using namespace std;
// 1. 平台检测
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM "Windows"
#define PATH_SEP '\\'
#elif defined(__APPLE__)
#define PLATFORM "macOS"
#define PATH_SEP '/'
#elif defined(__linux__)
#define PLATFORM "Linux"
#define PATH_SEP '/'
#else
#define PLATFORM "Unknown"
#define PATH_SEP '/'
#endif
// 2. 调试宏(Release版自动消除)
#ifdef NDEBUG
#define DEBUG_LOG(msg) ((void)0)
#else
#define DEBUG_LOG(msg) \
cout << "[DEBUG " << __FILE__ << ":" << __LINE__ << "] " << msg << endl
#endif
// 3. 编译期断言(C++11之前的做法)
#define STATIC_ASSERT(cond, msg) \
typedef char static_assert_##msg[(cond) ? 1 : -1]
// 4. 安全的MIN/MAX宏
#define SAFE_MIN(a, b) ({ \
auto _a = (a); \
auto _b = (b); \
_a < _b ? _a : _b; \
})
// 5. 数组长度宏
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int main() {
cout << "平台: " << PLATFORM << endl;
cout << "路径分隔符: " << PATH_SEP << endl;
DEBUG_LOG("程序启动");
DEBUG_LOG("编译器版本: " << __cplusplus);
int arr[] = {1, 2, 3, 4, 5};
cout << "数组大小: " << ARRAY_SIZE(arr) << endl;
// C++11直接用static_assert
static_assert(sizeof(int) >= 4, "int必须至少4字节");
cout << "编译日期: " << __DATE__ << " " << __TIME__ << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
练习重点:条件编译实现跨平台、调试宏在Release版消除、预定义宏的实际应用。
训练题2:用宏实现简易单元测试框架
#include <iostream>
#include <string>
#include <vector>
#include <functional>
using namespace std;
// 测试框架宏
static vector<pair<string, function<void()>>> _tests;
static int _passed = 0, _failed = 0;
#define TEST(name) \
void test_##name(); \
static bool _reg_##name = ([]{ \
_tests.push_back({#name, test_##name}); \
return true; \
})(); \
void test_##name()
#define ASSERT_EQ(a, b) do { \
auto _va = (a); auto _vb = (b); \
if (_va != _vb) { \
cout << " FAIL: " << #a << " == " << #b \
<< " (" << _va << " != " << _vb << ")" \
<< " at " << __FILE__ << ":" << __LINE__ << endl; \
_failed++; return; \
} \
} while(0)
#define ASSERT_TRUE(cond) do { \
if (!(cond)) { \
cout << " FAIL: " << #cond \
<< " at " << __FILE__ << ":" << __LINE__ << endl; \
_failed++; return; \
} \
} while(0)
#define RUN_ALL_TESTS() do { \
for (auto& [name, func] : _tests) { \
cout << "运行 " << name << "... "; \
int before = _failed; \
func(); \
if (_failed == before) { _passed++; cout << "OK" << endl; } \
} \
cout << "\n结果: " << _passed << " 通过, " << _failed << " 失败" << endl; \
} while(0)
// 使用测试框架
TEST(addition) {
ASSERT_EQ(1 + 1, 2);
ASSERT_EQ(10 + 20, 30);
}
TEST(string_length) {
string s = "hello";
ASSERT_EQ(s.size(), 5u);
ASSERT_TRUE(s.find("ell") != string::npos);
}
TEST(should_fail) {
ASSERT_EQ(1 + 1, 3); // 故意失败
}
int main() {
RUN_ALL_TESTS();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
练习重点:宏的字符串化#、宏的拼接##、do{...}while(0)技巧、Lambda自动注册测试用例。
# 17.12 综合思考题
宏 vs constexpr vs inline:C++11之后,很多宏的使用场景可以被
constexpr函数和inline函数替代。请对比三者在类型安全、调试支持、作用域控制方面的差异。哪些场景仍然必须使用宏?(提示:__FILE__、__LINE__、字符串化#)头文件地狱与编译时间:一个大型C++项目(如Chromium)有数千个头文件,包含关系错综复杂。前向声明(forward declaration)、Pimpl模式、预编译头(PCH)分别如何减少编译时间?C++20的模块(Modules)是否能彻底解决这个问题?
#pragma的跨平台问题:#pragma once、#pragma pack、#pragma comment等在不同编译器上的行为可能不同。如何编写真正可移植的代码?GCC的__attribute__和MSVC的__declspec如何统一?