4. 预处理指令.md 14 KB

4 预处理指令

4.1 完整概念讲解

预处理器(preprocessor)在编译之前对源代码进行文本替换和条件处理。预处理指令以 # 开头,由预处理器在编译阶段展开。

#define 宏定义

对象宏(Object-like Macro)

#define PI 3.14159265358979
#define BUFFER_SIZE 1024
#define NULL ((void *)0)
  • 纯文本替换,不做类型检查
  • 替换发生在编译之前,调试时可能看不到宏的名字

函数宏(Function-like Macro)

#define MAX(a, b)  ((a) > (b) ? (a) : (b))
#define SQUARE(x)  ((x) * (x))
  • 参数必须紧跟宏名,中间不能有空格:MAX(a,b) 是宏,MAX (a,b) 不是
  • 每个参数都应加括号,防止运算符优先级问题

宏的副作用

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5, y = 10;
int result = MAX(x++, y++);
// 展开为: ((x++) > (y++) ? (x++) : (y++))
// x++ 执行了 2 次,y++ 执行了 2 次(或 1 次)

这是宏最危险的陷阱:参数被多次求值。解决方案:使用 GCC 的语句表达式(({...})) 或改用 inline 函数。

#define 与 const 的区别

特性 #define const
处理阶段 预处理(文本替换) 编译(类型检查)
类型安全
调试 可能显示替换后的值 保留符号名
作用域 定义后到 #undef 或文件末尾 遵循 C 作用域规则
内存占用 不占用(除非取地址) 占用(有存储地址)
适用场景 编译时常量、条件编译、位掩码 运行时常量

#include

#include <stdio.h>    /* 在系统头文件目录中查找 */
#include "myheader.h" /* 先在当前目录查找,再在系统目录查找 */
  • 尖括号:系统/标准库头文件
  • 双引号:用户自定义头文件
  • #include 可以嵌套,但深度有限制(通常 8-16 层)

头文件保护宏

方式一:传统 include guard

#ifndef MYHEADER_H
#define MYHEADER_H

/* 头文件内容 */

#endif /* MYHEADER_H */

方式二:#pragma once

#pragma once

/* 头文件内容 */

对比:

特性 #ifndef/#define/#endif #pragma once
标准支持 C89 起所有编译器 非标准,但所有主流编译器支持
可移植性 完美 99% 场景可用
符号冲突风险 有(宏名可能冲突)
对符号链接/硬链接 可能重复包含 正确处理

条件编译

#ifdef DEBUG
    /* 调试代码 */
#endif

#ifndef NDEBUG
    /* 非调试代码 */
#endif

#if defined(__linux__)
    /* Linux 特定代码 */
#elif defined(_WIN32)
    /* Windows 特定代码 */
#else
    /* 其他平台 */
#endif

#if VERSION >= 2
    /* 新版本特性 */
#endif

条件编译在 Linux 内核中大量使用,用于支持多平台编译(ARM、x86、MIPS 等)。

#undef

#define LIMIT 100
/* ... */
#undef LIMIT
/* LIMIT 不再定义 */

用于重新定义宏或限制宏的作用域。

#error

#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#error "This code requires C11 or later"
#endif

在编译时产生错误消息,用于编译期条件检查。

预定义宏

含义
__FILE__ 当前源文件名(字符串)
__LINE__ 当前行号(整数)
__DATE__ 编译日期(字符串,如 "Aug 31 2026")
__TIME__ 编译时间(字符串)
__func__ 当前函数名(C99,字符串)
__STDC__ 编译器遵循 ANSI C(1)
__STDC_VERSION__ C 标准版本号(C99: 199901L, C11: 201112L, C17: 201710L)
__STDC_HOSTED__ 实现是否 hosted(1)或 freestanding(0)

## 和 # 运算符

# 运算符(Stringification)

将宏参数转换为字符串字面量:

#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)

int x = 42;
PRINT_VAR(x);  /* 展开为: printf("x" " = %d\n", x); 即 printf("x = %d\n", x); */

## 运算符(Token Pasting)

将两个 token 拼接为一个:

#define CONCAT(a, b) a##b
#define MAKE_STRUCT(prefix, name) struct prefix##_##name

int var10 = 100;
printf("%d\n", CONCAT(var, 10));  /* 输出 100 */

MAKE_STRUCT(sensor, data) {       /* 展开为: struct sensor_data { ... }; */
    int value;
};

4.2 核心 API/语法

可变参数宏(Variadic Macros)

#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
#define DEBUG_LOG(fmt, ...) \
    fprintf(stderr, "[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)

C99 引入 __VA_ARGS__,C23 引入 __VA_OPT__(,) 用于处理可变参数为空的情况。

多行宏

#define SWAP(a, b) do { \
    typeof(a) _tmp = (a); \
    (a) = (b); \
    (b) = _tmp; \
} while (0)

do { ... } while (0) 模式确保宏在 if-else 语句中安全使用。


4.3 代码示例

示例 1:宏定义与字符串化、token 拼接

#include <stdio.h>

#define PI          3.14159265358979
#define SQUARE(x)   ((x) * (x))
#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#define MIN(a, b)   ((a) < (b) ? (a) : (b))

#define STRINGIFY(x)  #x
#define CONCAT(a, b)  a##b

#define DEBUG_LOG(fmt, ...) \
    fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)

#define SWAP(a, b) do { \
    __typeof__(a) _tmp = (a); \
    (a) = (b); \
    (b) = _tmp; \
} while (0)

int main(void) {
    printf("=== 对象宏 ===\n");
    printf("PI = %.15f\n", PI);
    printf("sizeof(int) = %zu\n", sizeof(int));

    printf("\n=== 函数宏 ===\n");
    int x = 5;
    printf("SQUARE(%d) = %d\n", x, SQUARE(x));
    printf("MAX(3, 7) = %d\n", MAX(3, 7));
    printf("MIN(3, 7) = %d\n", MIN(3, 7));

    printf("\n=== # 运算符(字符串化)===\n");
    int var = 42;
    printf("STRINGIFY(hello) = \"%s\"\n", STRINGIFY(hello world));
    printf("STRINGIFY(1+2) = \"%s\"\n", STRINGIFY(1 + 2));
    printf("var 的值: ");
    printf(#var " = %d\n", var);  /* 等价于 printf("var = %d\n", var); */

    printf("\n=== ## 运算符(token 拼接)===\n");
    int val10 = 100;
    int val20 = 200;
    printf("CONCAT(val, 10) = %d\n", CONCAT(val, 10));
    printf("CONCAT(val, 20) = %d\n", CONCAT(val, 20));

    printf("\n=== SWAP 宏 ===\n");
    int a = 10, b = 20;
    printf("交换前: a=%d, b=%d\n", a, b);
    SWAP(a, b);
    printf("交换后: a=%d, b=%d\n", a, b);

    printf("\n=== DEBUG_LOG ===\n");
    DEBUG_LOG("程序启动,x = %d", x);

    return 0;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o preprocess1 preprocess1.c
./preprocess1

示例 2:#include 与头文件保护

config.h(自定义头文件):

#ifndef CONFIG_H
#define CONFIG_H

#define APP_NAME    "PreprocessorDemo"
#define APP_VERSION "1.0.0"
#define MAX_DEVICES 16

typedef struct {
    int id;
    char name[32];
} Device;

#endif /* CONFIG_H */

main.c:

#include <stdio.h>
#include "config.h"

int main(void) {
    printf("App: %s v%s\n", APP_NAME, APP_VERSION);
    printf("Max devices: %d\n", MAX_DEVICES);

    Device dev = { .id = 1, .name = "sensor_01" };
    printf("Device: [%d] %s\n", dev.id, dev.name);

    return 0;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o include_demo main.c
./include_demo

示例 3:条件编译与预定义宏

#include <stdio.h>

/* 编译器检测 */
#if defined(__GNUC__)
    #define COMPILER "GCC " __VERSION__
#elif defined(_MSC_VER)
    #define COMPILER "MSVC " _MSC_VER
#else
    #define COMPILER "Unknown Compiler"
#endif

/* 平台检测 */
#if defined(__linux__)
    #define PLATFORM "Linux"
#elif defined(_WIN32)
    #define PLATFORM "Windows"
#elif defined(__APPLE__)
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

/* 调试模式开关 */
#ifdef DEBUG
    #define LOG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
    #define LOG(fmt, ...) ((void)0)
#endif

/* C 标准版本检查 */
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
    #define C_STANDARD "C11"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
    #define C_STANDARD "C99"
#else
    #define C_STANDARD "C89/C90"
#endif

/* 错误检查 */
#if MAX_DEVICES < 1
    #error "MAX_DEVICES must be at least 1"
#endif

/* 调试与发布模式下不同的数组大小 */
#ifdef DEBUG
    #define TRACE_SIZE 1024
#else
    #define TRACE_SIZE 256
#endif

void print_info(void) {
    LOG("print_info 被调用");
    printf("Compiler: %s\n", COMPILER);
    printf("Platform: %s\n", PLATFORM);
    printf("C Standard: %s\n", C_STANDARD);
    printf("File: %s\n", __FILE__);
    printf("Date: %s %s\n", __DATE__, __TIME__);
    printf("Function: %s\n", __func__);
    printf("Trace buffer size: %d\n", TRACE_SIZE);
}

int main(void) {
    LOG("程序启动");

    print_info();

    /* 条件编译示例:不同平台不同行为 */
#if defined(__linux__)
    printf("Linux 特定代码\n");
#elif defined(_WIN32)
    printf("Windows 特定代码\n");
#else
    printf("其他平台代码\n");
#endif

    LOG("程序结束");
    return 0;
}

编译命令:

# 普通模式
gcc -std=c99 -Wall -Wextra -DDEBUG -o preprocess3 preprocess3.c
./preprocess3

# 发布模式(关闭 DEBUG)
gcc -std=c99 -Wall -Wextra -o preprocess3_release preprocess3.c
./preprocess3_release

示例 4:#error 与版本检查

#include <stdio.h>

/* 编译期版本检查 */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L
    #error "This code requires C99 or later"
#endif

/* 检查特定头文件是否可用 */
#if defined(__has_include)
    #if __has_include(<stdint.h>)
        #define HAS_STDINT_H 1
    #else
        #define HAS_STDINT_H 0
    #endif
#else
    #define HAS_STDINT_H 1  /* 假设有 */
#endif

#if HAS_STDINT_H
    #include <stdint.h>
#endif

/* 用 #undef 重定义宏 */
#define LIMIT 100
#undef LIMIT
#define LIMIT 200

int main(void) {
    printf("LIMIT = %d\n", LIMIT);
#if HAS_STDINT_H
    printf("stdint.h available, sizeof(uint32_t) = %zu\n", sizeof(uint32_t));
#endif
    return 0;
}

编译命令:

gcc -std=c99 -Wall -Wextra -o preprocess4 preprocess4.c
./preprocess4

4.4 注意事项与易错点

  1. 宏参数不加括号的危险

    #define SQUARE_BAD(x) x * x
    SQUARE_BAD(2 + 3)  /* 展开为 2 + 3 * 2 + 3 = 11,而非 25 */
    
  2. 宏中的分号陷阱

    #define SET(x) x = 0;
    if (cond)
       SET(val)
    else   /* 语法错误!分号导致 if-else 断裂 */
       do_something();
    
  3. #define 不加分号#define PI 3.14; 会导致 PI 被替换为 3.14;,使用时产生语法错误。

  4. 头文件保护宏命名冲突:不同头文件使用相同保护宏名会导致其中一个被静默跳过。使用唯一命名(如 PROJECT_MODULE_FILENAME_H)。

  5. ## 拼接空 token:C 标准禁止 ## 产生空 token,但 GCC 扩展允许。可移植代码应避免。

  6. #if 中的 0 而非 false#if 只接受整数常量表达式,不能用 true/false(C99 之前没有 _Bool)。

  7. 多行宏的续行符 \:反斜杠后不能有空格,否则续行失败。续行符后必须紧跟换行符。


4.5 面试要点

Q1:#define 和 inline 函数有什么区别?

A:(1) #define 是文本替换,无类型检查;inline 是函数,有完整类型检查。(2) #define 可能导致参数多次求值(副作用);inline 只求值一次。(3) #define 在预处理阶段展开,调试困难;inline 函数在编译阶段处理,保留符号信息。(4) #define 可以用于条件编译、token 拼接等无法用函数替代的场景。

Q2:#pragma once 和 include guard 哪个更好?

A:两者各有优势。#pragma once 更简洁、无符号冲突风险、对硬链接正确。include guard 是 C 标准方式,可移植性完美(适用于所有符合标准的编译器)。实际中,主流编译器(GCC、Clang、MSVC)都支持 #pragma once,但如果需要极致可移植性,用 include guard。

Q3:为什么内核代码大量使用条件编译?

A:Linux 内核需要在数百种硬件平台上编译运行。条件编译用于:(1) 平台特定代码(ARM vs x86);(2) 编译配置(CONFIG_SMP 多处理器支持);(3) 调试/性能分析开关;(4) 不同编译器版本兼容。这使得同一份代码可以在极不同的环境下编译。

Q4:#include 尖括号和双引号的本质区别是什么?

A:预处理器在不同路径集合中查找。尖括号 <stdio.h> 在系统指定的 include 路径(如 /usr/include)中查找。双引号 "myheader.h" 先在当前源文件所在目录查找,找不到再回退到系统路径。用户自定义头文件应始终用双引号。

Q5:如何安全地定义函数宏?

A:遵循以下规则:(1) 每个参数都加括号;(2) 整个表达式加括号;(3) 使用 do { ... } while (0) 包裹多语句宏;(4) 使用双下划线前缀的临时变量名避免命名冲突;(5) 如果可能,优先使用 static inline 函数替代函数宏。