编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

      • README
      • 基础语法
      • 数据类型
      • 运算符
      • 循环和选择
      • 输入输出
      • 函数
      • 指针
      • 数组和容器
      • 类和内存
      • 流与文件
      • 结构体
      • 线程和锁
      • 预处理器
        • 12.1 预处理器指令
        • 12.2 include头文件
        • 12.3 define定义宏
          • 12.3.1 综合案例与思考
        • 12.5 条件编译
          • 12.5.1 综合案例与思考
        • 12.6 pragma特定功能
          • 12.6.1 #pragma once
          • 12.6.2 #pragma message
          • 12.6.3 #pragma warning (MSVC)
          • 12.6.4 #pragma pack
          • 12.6.5 #pragma region (MSVC)
          • 12.6.6 #pragma omp (OpenMP)
          • 12.6.7 #pragma comment (Windows)
          • 12.6.8 编译器特定的优化指令
          • 12.6.9 注意事项
        • 12.7 错误或警告信息
        • 12.8 预定义宏
        • 12.9 宏的注意事项
        • 12.10 综合示例
          • 12.10.1 综合案例与思考
      • 高级数据
    • 综合案例

    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 入门教程
杨充
2025-07-21
目录

预处理器

# 13.预处理器

# 目录介绍

  • 12.1 预处理器指令
  • 12.2 include头文件
  • 12.3 define定义宏
    • 12.3.1 综合案例与思考
  • 12.4 undef取消定义宏
  • 12.5 条件编译
    • 12.5.1 综合案例与思考
  • 12.6 pragma特定功能
  • 12.7 错误或警告信息
  • 12.8 预定义宏
  • 12.9 宏的注意事项
  • 12.10 综合示例
    • 12.10.1 综合案例与思考

# 12.1 预处理器指令

预处理器指令以 # 开头,常见的指令包括:

  • #include:包含头文件。
  • #define:定义宏。
  • #undef:取消宏定义。
  • #if、#ifdef、#ifndef、#else、#elif、#endif:条件编译。
  • #pragma:提供编译器特定的功能。
  • #error:生成编译错误。
  • #warning:生成编译警告。

# 12.2 include头文件

用于包含头文件,将指定文件的内容插入到当前文件中。

语法

#include <header.h>  // 系统头文件
#include "header.h"  // 用户自定义头文件
1
2

示例

#include <stdio.h>  // 包含标准输入输出头文件
#include "myheader.h"  // 包含用户自定义头文件
1
2

# 12.3 define定义宏

用于定义宏,宏可以是常量、函数或代码片段。

语法

#define MACRO_NAME value
1

示例

#define PI 3.14159  // 定义常量宏
#define SQUARE(x) ((x) * (x))  // 定义函数宏
1
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;
}
1
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。

思考题:

  1. #define SQUARE(x) x * x 为什么不正确?SQUARE(2+3) 的结果是什么?
  2. # 和 ## 运算符在宏中的作用是什么?实际开发中有哪些应用场景?
  3. C11引入了 _Generic 关键字,它能解决宏的什么问题?

用于取消已定义的宏。

语法

#undef MACRO_NAME
1

示例

#define PI 3.14159
#undef PI  // 取消 PI 的定义
1
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
1
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;
}
1
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 是非标准但广泛支持的替代方案。

思考题:

  1. #ifdef DEBUG 和 #if DEBUG 有什么区别?如果 #define DEBUG 0,两者的行为分别是什么?
  2. #pragma once 和传统的 #ifndef 头文件保护有什么优缺点?
  3. 如何用条件编译实现一个跨平台的 sleep 函数(Linux用 usleep,Windows用 Sleep)?

用于提供编译器特定的功能。

示例

#pragma once  // 防止头文件被重复包含
#pragma warning(disable: 4996)  // 禁用特定警告
1
2

# 12.6 pragma特定功能

#pragma 是 C/C++ 中的预处理指令,用于向编译器传递特定实现的功能和控制指令。它不是标准 C++ 的一部分,但大多数编译器都支持各种 #pragma 指令。

以下是一些常见的 #pragma 指令及其用途:

# 12.6.1 #pragma once

确保头文件只被编译一次,替代传统的头文件保护符(#ifndef, #define, #endif)。最常见的用法:

#pragma once
// 头文件内容...
1
2

# 12.6.2 #pragma message

在编译时输出消息:

#pragma message("正在编译这个文件...")
1

# 12.6.3 #pragma warning (MSVC)

控制编译器警告:

#pragma warning(disable: 4996)  // 禁用特定警告
#pragma warning(push)           // 保存当前警告状态
#pragma warning(pop)            // 恢复警告状态
1
2
3

# 12.6.4 #pragma pack

控制结构体的内存对齐:

#pragma pack(push, 1)  // 1字节对齐
struct TightStruct {
    char a;
    int b;
};
#pragma pack(pop)      // 恢复默认对齐
1
2
3
4
5
6

# 12.6.5 #pragma region (MSVC)

在 Visual Studio 中创建可折叠的代码区域:

#pragma region 初始化代码
void init() {
    // 初始化逻辑
}
#pragma endregion
1
2
3
4
5

# 12.6.6 #pragma omp (OpenMP)

用于并行计算:

#include <omp.h>

#pragma omp parallel for
for (int i = 0; i < 100; i++) {
    // 并行执行的代码
}
1
2
3
4
5
6

# 12.6.7 #pragma comment (Windows)

在目标文件中嵌入注释:

#pragma comment(lib, "user32.lib")  // 链接库
#pragma comment(linker, "/STACK:1048576")  // 设置栈大小
1
2

# 12.6.8 编译器特定的优化指令

// GCC/Clang
#pragma GCC optimize("O3")

// MSVC
#pragma optimize("", off)  // 禁用优化
1
2
3
4
5

# 12.6.9 注意事项

  1. 可移植性问题:#pragma 指令是编译器特定的
  2. 替代方案:尽量使用标准替代方案
    // 替代 #pragma once
    #ifndef MYHEADER_H
    #define MYHEADER_H
    // 内容...
    #endif
    
    1
    2
    3
    4
    5
  3. 条件编译:可配合编译器检测使用
    #ifdef _MSC_VER
    #pragma warning(disable: 4996)
    #endif
    
    1
    2
    3

# 12.7 错误或警告信息

用于在编译时生成错误或警告信息。

示例

#ifndef VERSION
    #error "VERSION is not defined!"
#endif

#ifndef DEBUG
    #warning "DEBUG is not defined!"
#endif
1
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__);
1
2
3
4

# 12.9 宏的注意事项

  1. 宏的副作用: 宏是简单的文本替换,可能导致副作用。

    #define SQUARE(x) ((x) * (x))
    int a = 5;
    int b = SQUARE(a++);  // 展开为 ((a++) * (a++)),a 被递增两次
    
    1
    2
    3
  2. 多行宏: 使用 \ 将宏扩展到多行。

    #define PRINT_SUM(a, b) \
        printf("%d + %d = %d\n", a, b, (a) + (b))
    
    1
    2
  3. 宏与函数的区别:

    • 宏是文本替换,没有类型检查。
    • 函数有类型检查,且会生成调用开销。

# 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;
}
1
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
1
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;
}
1
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项目中被广泛使用。

思考题:

  1. ARRAY_SIZE 宏如果传入一个指针而非数组,结果会怎样?如何用编译期检查防止这种误用?
  2. ##__VA_ARGS__ 中 ## 的作用是什么?这是标准C还是编译器扩展?
  3. 为什么很多项目不用 assert.h 中的 assert 而是自定义断言宏?自定义版本有什么优势?
上次更新: 2026/06/10, 11:13:41
线程和锁
高级数据

← 线程和锁 高级数据→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式