预处理器
# 13.预处理器
# 目录介绍
- 12.1 预处理器指令
- 12.2 include头文件
- 12.3 define定义宏
- 12.4 undef取消定义宏
- 12.5 条件编译
- 12.6 pragma特定功能
- 12.7 错误或警告信息
- 12.8 预定义宏
- 12.9 宏的注意事项
- 12.10 综合示例
# 12.1 预处理器指令
预处理器指令以 # 开头,常见的指令包括:
#include:包含头文件。#define:定义宏。#undef:取消宏定义。#if、#ifdef、#ifndef、#else、#elif、#endif:条件编译。#pragma:提供编译器特定的功能。#error:生成编译错误。#warning:生成编译警告。
# 12.2 include头文件
用于包含头文件,将指定文件的内容插入到当前文件中。
语法
#include <header.h> // 系统头文件
#include "header.h" // 用户自定义头文件
2
示例
#include <stdio.h> // 包含标准输入输出头文件
#include "myheader.h" // 包含用户自定义头文件
2
# 12.3 define定义宏
用于定义宏,宏可以是常量、函数或代码片段。
语法
#define MACRO_NAME value
示例
#define PI 3.14159 // 定义常量宏
#define SQUARE(x) ((x) * (x)) // 定义函数宏
2
注意事项
- 宏是简单的文本替换,不会进行类型检查。
- 使用括号避免宏展开时的优先级问题。
# 12.3.1 综合案例与思考
综合案例:宏定义的常见用法与陷阱
#include <stdio.h>
// 常量宏
#define PI 3.14159265358979
#define MAX_SIZE 100
// 函数宏(注意括号的使用)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) >= 0 ? (x) : -(x))
#define SQUARE(x) ((x) * (x))
// 字符串化运算符 #
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)
// 连接运算符 ##
#define CONCAT(a, b) a##b
#define MAKE_FUNC(name) void func_##name() { printf("调用 func_" #name "\n"); }
// 多行宏
#define SWAP(type, a, b) do { \
type _temp = (a); \
(a) = (b); \
(b) = _temp; \
} while(0)
// 生成函数
MAKE_FUNC(hello)
MAKE_FUNC(world)
int main() {
// 常量宏
printf("PI = %.10f\n", PI);
// 函数宏
int x = 10, y = 20;
printf("MAX(%d, %d) = %d\n", x, y, MAX(x, y));
printf("ABS(-42) = %d\n", ABS(-42));
// 宏的陷阱:副作用
int a = 5;
printf("\n=== 宏的陷阱 ===\n");
printf("SQUARE(a++) 前 a=%d\n", a);
// SQUARE(a++) 展开为 ((a++) * (a++)),a被递增两次!
int result = SQUARE(a++);
printf("SQUARE(a++) = %d, a=%d (a被递增了2次!)\n", result, a);
// 字符串化
printf("\n=== 字符串化 ===\n");
int count = 42;
PRINT_VAR(count); // 展开为 printf("count" " = %d\n", count);
printf("STRINGIFY(hello) = %s\n", STRINGIFY(hello));
// 连接运算符
printf("\n=== 连接运算符 ===\n");
int CONCAT(my, var) = 100; // 展开为 int myvar = 100;
printf("myvar = %d\n", myvar);
func_hello();
func_world();
// SWAP宏
printf("\n=== SWAP宏 ===\n");
int p = 10, q = 20;
printf("交换前: p=%d, q=%d\n", p, q);
SWAP(int, p, q);
printf("交换后: p=%d, q=%d\n", p, q);
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
原理说明:预处理器在编译之前对源代码进行文本替换。宏没有类型检查、没有作用域,只是简单的文本展开。# 运算符将宏参数转换为字符串字面量,## 运算符将两个标记连接成一个标记。多行宏用 do { ... } while(0) 包裹是为了保证宏在任何上下文(如 if-else 中)都能正确工作。宏的最大优势是零运行时开销(编译期展开),缺点是难以调试、容易因副作用产生Bug。
思考题:
#define SQUARE(x) x * x为什么不正确?SQUARE(2+3)的结果是什么?#和##运算符在宏中的作用是什么?实际开发中有哪些应用场景?- C11引入了
_Generic关键字,它能解决宏的什么问题?
用于取消已定义的宏。
语法
#undef MACRO_NAME
示例
#define PI 3.14159
#undef PI // 取消 PI 的定义
2
# 12.5 条件编译
根据条件决定是否编译某段代码。
常用指令
#if:如果条件为真,则编译后续代码。#ifdef:如果宏已定义,则编译后续代码。#ifndef:如果宏未定义,则编译后续代码。#else:与#if、#ifdef、#ifndef配套使用。#elif:类似于else if。#endif:结束条件编译块。
示例
#define DEBUG 1
#if DEBUG
printf("Debug mode is on.\n");
#else
printf("Debug mode is off.\n");
#endif
2
3
4
5
6
7
# 12.5.1 综合案例与思考
综合案例:条件编译的实际应用
#include <stdio.h>
// 编译时定义:gcc -DDEBUG -DPLATFORM=1 main.c
#ifndef PLATFORM
#define PLATFORM 0 // 0=通用, 1=Linux, 2=Windows
#endif
// 调试日志宏
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", \
__FILE__, __LINE__, ##__VA_ARGS__)
#else
#define LOG(fmt, ...) // 空定义,Release模式下无开销
#endif
// 平台相关代码
void print_platform() {
#if PLATFORM == 1
printf("平台: Linux\n");
#elif PLATFORM == 2
printf("平台: Windows\n");
#else
printf("平台: 通用\n");
#endif
}
// 头文件保护(防止重复包含)
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件内容...
void my_function(void);
#endif
// 编译器版本检测
void check_compiler() {
#if defined(__GNUC__)
printf("编译器: GCC %d.%d\n", __GNUC__, __GNUC_MINOR__);
#elif defined(_MSC_VER)
printf("编译器: MSVC %d\n", _MSC_VER);
#elif defined(__clang__)
printf("编译器: Clang %d.%d\n", __clang_major__, __clang_minor__);
#else
printf("编译器: 未知\n");
#endif
}
int main() {
print_platform();
check_compiler();
LOG("程序启动");
LOG("变量值: x=%d, y=%d", 10, 20);
// C标准版本检测
#if __STDC_VERSION__ >= 201112L
printf("C标准: C11或更高\n");
#elif __STDC_VERSION__ >= 199901L
printf("C标准: C99\n");
#else
printf("C标准: C89/C90\n");
#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
原理说明:条件编译是预处理器最强大的功能之一,它让同一份源代码能适应不同的编译环境和配置。#ifdef/#ifndef 检查宏是否已定义,#if 可以计算常量表达式。条件编译的代码在编译阶段就被确定,未选中的分支完全不会生成机器码,因此没有运行时开销。头文件保护(#ifndef ... #define ... #endif)防止头文件被重复包含导致的重定义错误,#pragma once 是非标准但广泛支持的替代方案。
思考题:
#ifdef DEBUG和#if DEBUG有什么区别?如果#define DEBUG 0,两者的行为分别是什么?#pragma once和传统的#ifndef头文件保护有什么优缺点?- 如何用条件编译实现一个跨平台的
sleep函数(Linux用usleep,Windows用Sleep)?
用于提供编译器特定的功能。
示例
#pragma once // 防止头文件被重复包含
#pragma warning(disable: 4996) // 禁用特定警告
2
# 12.6 pragma特定功能
#pragma 是 C/C++ 中的预处理指令,用于向编译器传递特定实现的功能和控制指令。它不是标准 C++ 的一部分,但大多数编译器都支持各种 #pragma 指令。
以下是一些常见的 #pragma 指令及其用途:
# 12.6.1 #pragma once
确保头文件只被编译一次,替代传统的头文件保护符(#ifndef, #define, #endif)。最常见的用法:
#pragma once
// 头文件内容...
2
# 12.6.2 #pragma message
在编译时输出消息:
#pragma message("正在编译这个文件...")
# 12.6.3 #pragma warning (MSVC)
控制编译器警告:
#pragma warning(disable: 4996) // 禁用特定警告
#pragma warning(push) // 保存当前警告状态
#pragma warning(pop) // 恢复警告状态
2
3
# 12.6.4 #pragma pack
控制结构体的内存对齐:
#pragma pack(push, 1) // 1字节对齐
struct TightStruct {
char a;
int b;
};
#pragma pack(pop) // 恢复默认对齐
2
3
4
5
6
# 12.6.5 #pragma region (MSVC)
在 Visual Studio 中创建可折叠的代码区域:
#pragma region 初始化代码
void init() {
// 初始化逻辑
}
#pragma endregion
2
3
4
5
# 12.6.6 #pragma omp (OpenMP)
用于并行计算:
#include <omp.h>
#pragma omp parallel for
for (int i = 0; i < 100; i++) {
// 并行执行的代码
}
2
3
4
5
6
# 12.6.7 #pragma comment (Windows)
在目标文件中嵌入注释:
#pragma comment(lib, "user32.lib") // 链接库
#pragma comment(linker, "/STACK:1048576") // 设置栈大小
2
# 12.6.8 编译器特定的优化指令
// GCC/Clang
#pragma GCC optimize("O3")
// MSVC
#pragma optimize("", off) // 禁用优化
2
3
4
5
# 12.6.9 注意事项
- 可移植性问题:
#pragma指令是编译器特定的 - 替代方案:尽量使用标准替代方案
// 替代 #pragma once #ifndef MYHEADER_H #define MYHEADER_H // 内容... #endif1
2
3
4
5 - 条件编译:可配合编译器检测使用
#ifdef _MSC_VER #pragma warning(disable: 4996) #endif1
2
3
# 12.7 错误或警告信息
用于在编译时生成错误或警告信息。
示例
#ifndef VERSION
#error "VERSION is not defined!"
#endif
#ifndef DEBUG
#warning "DEBUG is not defined!"
#endif
2
3
4
5
6
7
# 12.8 预定义宏
C 语言提供了一些预定义宏,用于获取编译环境信息。
常用预定义宏
__DATE__:当前日期(字符串)。__TIME__:当前时间(字符串)。__FILE__:当前文件名(字符串)。__LINE__:当前行号(整数)。__STDC__:如果编译器符合 ANSI C 标准,则定义为 1。
示例
printf("Date: %s\n", __DATE__);
printf("Time: %s\n", __TIME__);
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
2
3
4
# 12.9 宏的注意事项
宏的副作用: 宏是简单的文本替换,可能导致副作用。
#define SQUARE(x) ((x) * (x)) int a = 5; int b = SQUARE(a++); // 展开为 ((a++) * (a++)),a 被递增两次1
2
3多行宏: 使用
\将宏扩展到多行。#define PRINT_SUM(a, b) \ printf("%d + %d = %d\n", a, b, (a) + (b))1
2宏与函数的区别:
- 宏是文本替换,没有类型检查。
- 函数有类型检查,且会生成调用开销。
# 12.10 综合示例
#include <stdio.h>
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
#define DEBUG
int main() {
#ifdef DEBUG
printf("Debug mode is on.\n");
#endif
double radius = 5.0;
double area = PI * SQUARE(radius);
printf("Area of circle: %.2f\n", area);
printf("Compiled on %s at %s\n", __DATE__, __TIME__);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出:
Debug mode is on.
Area of circle: 78.54
Compiled on Oct 10 2023 at 14:30:00
2
3
# 12.10.1 综合案例与思考
综合案例:预处理器的高级应用——实现断言和日志系统
#include <stdio.h>
#include <stdlib.h>
// 自定义断言宏
#define MY_ASSERT(expr) do { \
if (!(expr)) { \
fprintf(stderr, "断言失败: %s\n 文件: %s\n 行号: %d\n 函数: %s\n", \
#expr, __FILE__, __LINE__, __func__); \
abort(); \
} \
} while(0)
// 日志级别
#define LOG_LEVEL_ERROR 0
#define LOG_LEVEL_WARN 1
#define LOG_LEVEL_INFO 2
#define LOG_LEVEL_DEBUG 3
#ifndef LOG_LEVEL
#define LOG_LEVEL LOG_LEVEL_INFO
#endif
// 日志宏
#define LOG_ERROR(fmt, ...) do { \
if (LOG_LEVEL >= LOG_LEVEL_ERROR) \
fprintf(stderr, "[ERROR] " fmt "\n", ##__VA_ARGS__); \
} while(0)
#define LOG_WARN(fmt, ...) do { \
if (LOG_LEVEL >= LOG_LEVEL_WARN) \
fprintf(stderr, "[WARN] " fmt "\n", ##__VA_ARGS__); \
} while(0)
#define LOG_INFO(fmt, ...) do { \
if (LOG_LEVEL >= LOG_LEVEL_INFO) \
printf("[INFO] " fmt "\n", ##__VA_ARGS__); \
} while(0)
#define LOG_DEBUG(fmt, ...) do { \
if (LOG_LEVEL >= LOG_LEVEL_DEBUG) \
printf("[DEBUG] " fmt "\n", ##__VA_ARGS__); \
} while(0)
// 数组大小宏(类型安全)
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// 位操作宏
#define BIT_SET(x, bit) ((x) |= (1 << (bit)))
#define BIT_CLEAR(x, bit) ((x) &= ~(1 << (bit)))
#define BIT_CHECK(x, bit) (((x) >> (bit)) & 1)
#define BIT_TOGGLE(x, bit) ((x) ^= (1 << (bit)))
int main() {
// 日志系统演示
LOG_ERROR("这是错误信息");
LOG_WARN("这是警告信息");
LOG_INFO("这是普通信息");
LOG_DEBUG("这是调试信息 (LOG_LEVEL=%d)", LOG_LEVEL);
// 编译信息
printf("\n=== 编译信息 ===\n");
printf("编译日期: %s\n", __DATE__);
printf("编译时间: %s\n", __TIME__);
printf("源文件: %s\n", __FILE__);
printf("当前行: %d\n", __LINE__);
// 断言演示
printf("\n=== 断言演示 ===\n");
int x = 10;
MY_ASSERT(x > 0); // 通过
printf("断言通过: x > 0\n");
// ARRAY_SIZE
int arr[] = {1, 2, 3, 4, 5};
printf("\n数组大小: %zu\n", ARRAY_SIZE(arr));
// 位操作宏
printf("\n=== 位操作 ===\n");
unsigned int flags = 0;
BIT_SET(flags, 0); // 设置第0位
BIT_SET(flags, 3); // 设置第3位
printf("设置位0和3后: 0x%X (二进制: %d%d%d%d)\n", flags,
BIT_CHECK(flags,3), BIT_CHECK(flags,2),
BIT_CHECK(flags,1), BIT_CHECK(flags,0));
BIT_TOGGLE(flags, 0);
printf("翻转位0后: 0x%X\n", flags);
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
82
83
84
85
86
87
88
89
90
原理说明:预处理器宏虽然不是"正统"的编程方式,但在C语言实际项目中不可或缺。日志系统通过宏实现可以做到:1)编译时确定日志级别,未激活的日志语句不产生任何代码;2)自动插入文件名、行号等调试信息。##__VA_ARGS__ 是GCC扩展,当可变参数为空时自动去掉前面的逗号。ARRAY_SIZE 宏是获取数组长度的惯用方法(但对指针无效)。这些宏在Linux内核、GLib等大型C项目中被广泛使用。
思考题:
ARRAY_SIZE宏如果传入一个指针而非数组,结果会怎样?如何用编译期检查防止这种误用?##__VA_ARGS__中##的作用是什么?这是标准C还是编译器扩展?- 为什么很多项目不用
assert.h中的assert而是自定义断言宏?自定义版本有什么优势?