前置知识: C

预处理器与宏

00:00
4 min Intermediate 2026/6/13

C预处理器指令、宏定义与展开、条件编译、文件包含与常见陷阱详解。

1. 预处理器概述

1.1 什么是预处理器

C预处理器(Preprocessor)是编译器在正式编译之前执行的一个文本替换工具。它不进行语法检查,只是按照指令对源代码进行文本变换。

预处理阶段在编译流程中的位置

源代码(.c) → [预处理] → 预处理后的代码(.i) → [编译] → 汇编代码(.s) → [汇编] → 目标文件(.o) → [链接] → 可执行文件

使用 gcc -E 可以只执行预处理,查看预处理结果:

gcc -E source.c -o source.i

1.2 预处理器的功能

  • 文件包含#include 将头文件内容插入源文件
  • 宏定义与展开#define 定义常量和代码片段
  • 条件编译#if#ifdef 等根据条件选择编译代码
  • 行控制#line 修改行号和文件名
  • 错误指令#error 在编译时产生错误信息
  • 编译指示#pragma 向编译器传递特殊指令

2. 宏定义

2.1 无参宏(对象式宏)

#include <stdio.h>

// 基本常量定义
#define PI 3.14159265
#define MAX_SIZE 100
#define NEWLINE '\n'
#define GREETING "Hello, World!"

// 使用宏
int main(void) {
    double area = PI * 5 * 5;
    int arr[MAX_SIZE];
    printf("Area = %f%c", area, NEWLINE);
    printf("%s\n", GREETING);
    return 0;
}

注意事项

  • 宏定义末尾不要加分号,分号会成为替换内容的一部分
  • 宏名通常全大写,以区分变量名
  • 宏定义不分配内存,只是文本替换

2.2 带参宏(函数式宏)

#include <stdio.h>

// 基本带参宏
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) >= 0 ? (x) : -(x))

// 多语句宏 - 使用 do-while(0)
#define SWAP(a, b) do { \
    typeof(a) _temp = (a); \
    (a) = (b); \
    (b) = _temp; \
} while(0)

// 字符串化与拼接
#define STR(x) #x              // 将参数转为字符串
#define CONCAT(a, b) a##b      // 拼接两个token

int main(void) {
    int a = 5, b = 10;
    printf("SQUARE(5) = %d\n", SQUARE(5));      // 25
    printf("MAX(3, 7) = %d\n", MAX(3, 7));      // 7
    printf("ABS(-3) = %d\n", ABS(-3));           // 3

    SWAP(a, b);
    printf("After swap: a=%d, b=%d\n", a, b);   // a=10, b=5

    printf("%s\n", STR(hello world));            // "hello world"
    int CONCAT(var, 1) = 42;                     // 等价于 int var1 = 42;
    printf("var1 = %d\n", var1);                 // 42

    return 0;
}

2.3 宏的括号陷阱

宏定义中括号的使用至关重要,缺少括号会导致意想不到的结果:

// 错误示例:缺少括号
#define SQUARE_BAD(x) x * x
SQUARE_BAD(3 + 1)    // 展开为 3 + 1 * 3 + 1 = 7,而非16

// 正确写法
#define SQUARE_GOOD(x) ((x) * (x))
SQUARE_GOOD(3 + 1)   // 展开为 ((3 + 1) * (3 + 1)) = 16

// 另一个陷阱:参数有副作用
#define SQUARE_SIDE(x) ((x) * (x))
int i = 3;
SQUARE_SIDE(i++)     // 展开为 ((i++) * (i++)),行为未定义!

宏定义括号规则

  1. 整个宏体用括号包围
  2. 每个参数出现的地方都用括号包围
  3. 避免在宏参数中使用自增/自减运算符

2.4 可变参数宏

#include <stdio.h>
#include <stdarg.h>

// __VA_ARGS__ 代表可变参数部分
#define DEBUG_LOG(fmt, ...) \
    fprintf(stderr, "[DEBUG] %s:%d: " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

// GCC扩展:##__VA_ARGS__ 允许可变参数为空
#define LOG(fmt, ...) \
    printf("[LOG] " fmt "\n", ##__VA_ARGS__)

int main(void) {
    int value = 42;
    DEBUG_LOG("value = %d", value);  // [DEBUG] test.c:12: value = 42
    LOG("simple message");            // [LOG] simple message(无额外参数)
    return 0;
}

3. 文件包含

3.1 #include 的两种形式

// 尖括号:在系统目录中搜索头文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 双引号:先在当前目录搜索,找不到再到系统目录
#include "myheader.h"
#include "utils/math_utils.h"

3.2 头文件保护(Include Guard)

防止头文件被重复包含:

// 方式1:传统宏保护(推荐,可移植性好)
#ifndef MYHEADER_H
#define MYHEADER_H

// 头文件内容
int my_function(int x);

typedef struct {
    int x;
    int y;
} Point;

#endif /* MYHEADER_H */

// 方式2:#pragma once(编译器扩展,简洁但非标准)
#pragma once

int my_function(int x);

3.3 头文件组织原则

/* mymodule.h - 头文件 */
#ifndef MYMODULE_H
#define MYMODULE_H

/* 1. 包含必要的头文件 */
#include <stddef.h>

/* 2. 宏定义 */
#define MYMODULE_VERSION "1.0"
#define MAX_ITEMS 256

/* 3. 类型定义 */
typedef enum {
    MYMODULE_OK = 0,
    MYMODULE_ERROR = -1
} MyModuleStatus;

typedef struct MyModule MyModule;  // 不透明指针模式

/* 4. 函数声明 */
MyModule *mymodule_create(void);
void mymodule_destroy(MyModule *mod);
MyModuleStatus mymodule_process(MyModule *mod, const char *input);

#endif /* MYMODULE_H */
/* mymodule.c - 实现文件 */
#include "mymodule.h"
#include <stdlib.h>
#include <string.h>

struct MyModule {
    char buffer[MAX_ITEMS];
    size_t len;
};

MyModule *mymodule_create(void) {
    MyModule *mod = calloc(1, sizeof(MyModule));
    return mod;
}

void mymodule_destroy(MyModule *mod) {
    free(mod);
}

MyModuleStatus mymodule_process(MyModule *mod, const char *input) {
    if (!mod || !input) return MYMODULE_ERROR;
    strncpy(mod->buffer, input, MAX_ITEMS - 1);
    mod->len = strlen(mod->buffer);
    return MYMODULE_OK;
}

4. 条件编译

4.1 基本条件编译指令

// #if / #elif / #else / #endif
#define VERSION 3

#if VERSION == 1
    const char *server = "v1.example.com";
#elif VERSION == 2
    const char *server = "v2.example.com";
#elif VERSION == 3
    const char *server = "v3.example.com";
#else
    const char *server = "default.example.com";
#endif

// #ifdef / #ifndef - 检查宏是否定义
#ifdef DEBUG
    #define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
    #define LOG(msg) ((void)0)
#endif

#ifndef BUFFER_SIZE
    #define BUFFER_SIZE 1024
#endif

4.2 defined 运算符

// defined 用于在 #if 中检查宏是否定义
#if defined(DEBUG) && defined(VERBOSE)
    // 调试和详细模式都启用时的代码
    #define DUMP_STATE(state) dump_full_state(state)
#elif defined(DEBUG)
    // 仅调试模式
    #define DUMP_STATE(state) dump_summary(state)
#else
    #define DUMP_STATE(state) ((void)0)
#endif

// 多条件组合
#if defined(__linux__)
    #define PLATFORM "Linux"
#elif defined(_WIN32)
    #define PLATFORM "Windows"
#elif defined(__APPLE__)
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

4.3 条件编译的实际应用

// 1. 跨平台代码
#ifdef _WIN32
    #include <windows.h>
    #define SLEEP(ms) Sleep(ms)
    #define PATH_SEPARATOR '\\'
#else
    #include <unistd.h>
    #define SLEEP(ms) usleep((ms) * 1000)
    #define PATH_SEPARATOR '/'
#endif

// 2. 调试与发布版本
#ifdef NDEBUG
    #define assert(condition) ((void)0)
#else
    #define assert(condition) \
        do { \
            if (!(condition)) { \
                fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
                    #condition, __FILE__, __LINE__); \
                abort(); \
            } \
        } while(0)
#endif

// 3. 功能开关
#define FEATURE_NETWORK 1
#define FEATURE_CRYPTO  1

#if FEATURE_NETWORK
    void init_network(void);
    void send_data(const char *data);
#endif

#if FEATURE_CRYPTO
    void encrypt_data(char *data, const char *key);
#endif

5. 预定义宏

5.1 标准预定义宏

#include <stdio.h>

int main(void) {
    printf("源文件名: %s\n", __FILE__);       // 当前源文件名
    printf("当前行号: %d\n", __LINE__);       // 当前行号
    printf("编译日期: %s\n", __DATE__);       // 编译日期 "Jun 13 2026"
    printf("编译时间: %s\n", __TIME__);       // 编译时间 "14:30:00"
    printf("C标准版本: %ld\n", __STDC_VERSION__); // C标准版本号

    // __func__ 是C99关键字,不是宏,返回当前函数名
    printf("当前函数: %s\n", __func__);

    return 0;
}

5.2 编译器扩展预定义宏

// GCC/Clang
#ifdef __GNUC__
    printf("GCC版本: %d.%d.%d\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#endif

#ifdef __clang__
    printf("Clang版本: %d.%d.%d\n", __clang_major__, __clang_minor__, __clang_patchlevel__);
#endif

// MSVC
#ifdef _MSC_VER
    printf("MSVC版本: %d\n", _MSC_VER);
#endif

6. #pragma 指令

6.1 常用 pragma

// 结构体对齐
#pragma pack(push, 1)    // 保存当前对齐,设为1字节对齐
struct PackedData {
    char a;
    int b;
    short c;
};
#pragma pack(pop)        // 恢复之前的对齐

// 禁用特定警告(MSVC)
#pragma warning(disable: 4996)  // 禁用"不安全函数"警告

// 循环优化提示
#pragma GCC optimize("O3")      // GCC优化级别

// 未使用参数
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
void callback(int event, void *data) {
    // 不使用 event 参数
}
#pragma GCC diagnostic pop

7. 常见问题与解决方案

7.1 宏展开的副作用

问题:带副作用的参数导致未定义行为

#define SQUARE(x) ((x) * (x))
int i = 3;
int result = SQUARE(i++);  // 未定义行为!

// 解决方案:使用内联函数代替
static inline int square(int x) {
    return x * x;
}
int result = square(i++);  // 安全,i只自增一次

7.2 多语句宏的if-else陷阱

问题:多语句宏在if-else中使用时出错

// 错误写法
#define BAD_SWAP(a, b) { \
    int temp = a; a = b; b = temp; \
}

if (condition)
    BAD_SWAP(x, y);   // 展开后多了一个分号,else匹配错误
else
    do_something();

// 正确写法:do-while(0)
#define GOOD_SWAP(a, b) do { \
    int temp = a; a = b; b = temp; \
} while(0)

7.3 头文件循环依赖

问题:A.h包含B.h,B.h又包含A.h

解决方案

  • 使用前向声明代替包含
  • 重新组织文件结构提取公共部分
  • 使用不透明指针模式隐藏实现

7.4 宏与函数的选择

特性内联函数
类型检查
调试困难容易
副作用可能有
代码膨胀可能可能
递归支持支持
指针不能取地址可以

建议:优先使用内联函数,仅在需要文本替换(如字符串化、拼接)时使用

8. 总结与最佳实践

8.1 宏定义最佳实践

  1. 名全大写,区分变量函数
  2. 体加括号参数加括号
  3. 多语句宏用 do-while(0)
  4. 避免参数副作用
  5. 复杂逻辑内联函数替代

8.2 头文件最佳实践

  1. 始终使用 Include Guard
  2. 头文件只放声明,不放定义
  3. 最小,能用前向声明就不用
  4. 头文件自身能编译通过

8.3 条件编译最佳实践

  1. 使用 defined() 检查,而非直接 #ifdef
  2. 提供默认,#ifndef + #define 模式
  3. 保持条件逻辑清晰,避免深层嵌套
  4. 注释 #endif,标明对应的 #if

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式