C语言——预处理详解

1. 预定义符号

C语言内置了一些预定义宏,用于获取编译相关信息:

|----------|------------------------------|
| 预定义符号 | 含义 |
| FILE | 进行编译的源文件名(字符串) |
| LINE | 文件当前的行号(整数) |
| DATE | 文件被编译的日期(字符串,格式:Mmm dd yyyy) |
| TIME | 文件被编译的时间(字符串,格式:hh:mm:ss) |
| STDC | 若编译器遵循ANSI C,值为1;否则未定义 |

示例代码:

cpp 复制代码
printf("file:%s line:%d\n", __FILE__, __LINE__);

2. #define 定义常量

基本语法

cpp 复制代码
#define name stuff

• name:宏名(建议大写)

• stuff:替换文本(可以是常量、表达式、语句等)

示例代码

cpp 复制代码
#define MAX 1000          // 定义数值常量
#define reg register      // 为关键字创建别名
#define do_forever for(;;)// 用符号替换循环实现
#define CASE break;case   // 简化case语句写法

多行宏定义(续行符 \)

若stuff过长,可使用反斜杠\续行(最后一行不加\):

cpp 复制代码
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" , \
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

⚠️ 重要注意:不要在宏定义后加 ;

错误示例:

cpp 复制代码
#define MAX 1000;  // 错误:多了分号

问题场景:

cpp 复制代码
if(condition)
    max = MAX;  // 替换后为 max = 1000;; → 两条语句
else
    max = 0;    // else 无匹配if,语法错误

建议:宏定义末尾不要加;,避免语法错误。

3. #define 定义宏(带参数)

宏的申明方式

cpp 复制代码
#define name( param-list ) stuff

• param-list:逗号分隔的参数表(左括号必须与name紧邻,否则参数表会被视为stuff的一部分)

• stuff:替换文本,可包含参数

示例:求平方宏

cpp 复制代码
#define SQUARE( x ) x * x

调用:SQUARE(5) → 替换为 5 * 5

⚠️ 宏的优先级问题

错误示例:

cpp 复制代码
#define SQUARE( x ) x * x
int a = 5;
printf("%d\n" ,SQUARE( a + 1 ));  // 预期输出36,实际输出11

替换后:a + 1 * a + 1 → 5 + 1*5 + 1 = 11(乘法优先级高于加法)

解决方法:给参数加括号

cpp 复制代码
#define SQUARE(x) (x) * (x)

替换后:(a + 1) * (a + 1) → 正确输出36

⚠️ 宏的整体优先级问题

错误示例:

cpp 复制代码
#define DOUBLE(x) (x) + (x)
int a = 5;
printf("%d\n" ,10 * DOUBLE(a));  // 预期输出100,实际输出55

替换后:10 * (5) + (5) → 50 + 5 = 55(乘法优先级高于加法)

解决方法:给整个宏表达式加括号

cpp 复制代码
#define DOUBLE(x) ((x) + (x))

替换后:10 * ((5) + (5)) → 正确输出100

✅ 最佳实践:

• 宏定义中,每个参数都用括号包裹

• 整个宏表达式再用一对括号包裹,避免优先级问题

4. 带有副作用的宏参数

当宏参数在宏定义中出现多次时,若参数带有副作用(如++、--),会导致不可预测的结果。

示例:MAX宏的副作用问题

cpp 复制代码
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);

预处理替换后:

cpp 复制代码
z = ((x++) > (y++) ? (x++) : (y++));

执行过程:

  1. 比较x++(5)和y++(8)→ 5 > 8为假,执行y++ → y变为9

  2. 取y++的值(9)赋给z → z=9,y再自增变为10

  3. x仅在比较时自增一次 → x=6

最终输出: x=6 y=10 z=9

⚠️ 结论:避免在宏参数中使用带有副作用的表达式,否则结果难以预测。

5. 宏替换的规则

  1. 参数预处理:调用宏时,先检查参数是否包含#define定义的符号,若有则先替换。

  2. 文本替换:将宏体中的参数名替换为实际参数值,再将结果插入到原宏调用位置。

  3. 再次扫描:对替换后的结果再次扫描,检查是否包含其他#define符号,重复上述过程。

注意事项

• 宏参数和宏定义中可以包含其他#define符号,但宏不能递归定义。

• 预处理器搜索#define符号时,字符串常量中的内容不会被搜索(如printf("MAX")中的MAX不会被替换)。

6. 宏与函数的对比

宏的优势

  1. 执行速度更快:宏是预处理阶段的文本替换,无函数调用/返回的开销。

  2. 类型无关:宏参数无类型限制,可适用于整数、浮点数等可比较的类型(如MAX宏可处理任意类型)。

  3. 可接受类型参数:宏可以将类型作为参数(如MALLOC宏),函数无法做到。

宏的劣势

|-------|---------------------------|---------------------|
| 对比项 | 宏 | 函数 |
| 代码长度 | 每次使用都会插入宏代码,多次使用会大幅增加程序长度 | 代码仅存在一处,多次调用共享同一份代码 |
| 调试难度 | 无法逐语句调试(预处理后已替换) | 可逐语句调试 |
| 类型严谨性 | 类型无关,不够严谨 | 参数类型固定,类型检查严格 |
| 优先级问题 | 易因运算符优先级导致错误(需加括号避免) | 参数仅求值一次,结果可预测 |
| 副作用 | 参数多次求值,副作用易引发问题 | 参数仅求值一次,结果可控 |
| 递归 | 不能递归 | 可以递归 |

示例:类型参数宏

cpp 复制代码
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))

// 使用
int* p = MALLOC(10, int);
// 预处理后:int* p = (int*)malloc(10 * sizeof(int));

7. # 和 ## 运算符

7.1 # 运算符(字符串化)

将宏参数转换为字符串字面量,仅允许出现在带参数的宏中。

示例代码:

cpp 复制代码
#define PRINT(n) printf("the value of " #n " is %d\n", n);

int a = 10;
PRINT(a);

#n 是「字符串化运算符」

◦ 它会把宏参数 n 直接变成字符串字面量

◦ 这里 #n → 如果传 a,就变成 "a"

C 语言相邻字符串自动拼接

cpp 复制代码
printf("the value of " "a" " is %d\n", a);
// 等价于:printf("the value of a is %d\n", a);

C 编译器会自动把相邻字符串拼成一个:

cpp 复制代码
"the value of a is %d\n"

输出: the value of a is 10

7.2 ## 运算符(记号粘合)

将位于它两边的符号合成一个新的标识符,连接结果必须是合法的标识符。

示例代码:生成泛型最大值函数

cpp 复制代码
type##_max

• 如果 type = int → 变成 int_max

• 如果 type = float → 变成 float_max

cpp 复制代码
#define GENERIC_MAX(type) \
type type##_max(type x, type y) \
{ \
    return x > y ? x : y; \
}

// 使用宏生成不同类型的max函数
GENERIC_MAX(int)    // 生成 int_max(int, int)
GENERIC_MAX(float)  // 生成 float_max(float, float)

int main() {
    int m = int_max(2, 3);
    float fm = float_max(3.5f, 4.5f);
    printf("%d\n%f\n", m, fm);
    return 0;
}

输出:
3
4.500000

8. 命名约定

• 宏名:全部大写(如MAX、SQUARE),与函数名区分。

• 函数名:不要全部大写(如int_max),避免混淆。

9. #undef 移除宏定义

用于移除一个已定义的宏:

cpp 复制代码
#undef NAME

场景:若需重新定义宏,必须先使用#undef移除旧定义。

10. 命令行定义

许多C编译器支持在编译命令行中定义符号,无需修改源码。

示例代码:动态定义数组大小

cpp 复制代码
// test.c
#include <stdio.h>
int main() {
    int array[ARRAY_SIZE];
    int i = 0;
    for(i = 0; i < ARRAY_SIZE; i++) {
        array[i] = i;
    }
    for(i = 0; i < ARRAY_SIZE; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");
    return 0;
}

编译命令(Linux/GCC):

cpp 复制代码
gcc -D ARRAY_SIZE=10 test.c

运行程序时,数组大小为10,输出:0 1 2 3 4 5 6 7 8 9

11. 条件编译

在编译时,根据条件选择性地编译部分代码,常用于调试、跨平台开发。

常见条件编译指令

  1. 单分支条件编译
cpp 复制代码
#if 常量表达式
    // 代码段
#endif

示例:调试开关

cpp 复制代码
#define __DEBUG__ 1
#if __DEBUG__
    printf("调试信息\n");
#endif
  1. 多分支条件编译
cpp 复制代码
#if 常量表达式1
    // 代码段1
#elif 常量表达式2
    // 代码段2
#else
    // 代码段3
#endif
  1. 判断是否被定义
cpp 复制代码
#if defined(symbol)  // 等价于 #ifdef symbol
#if !defined(symbol) // 等价于 #ifndef symbol
  1. 嵌套条件编译
cpp 复制代码
#if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

示例代码:调试代码选择性编译

cpp 复制代码
#include <stdio.h>
#define __DEBUG__

int main() {
    int i = 0;
    int arr[10] = {0};
    for(i = 0; i < 10; i++) {
        arr[i] = i;
    #ifdef __DEBUG__
        printf("%d\n", arr[i]); // 仅调试时打印
    #endif
    }
    return 0;
}

12. 头文件的包含

12.1 头文件包含方式

12.1.1 本地文件包含

cpp 复制代码
#include "filename"

• 查找策略:先在源文件所在目录查找,若未找到,再去标准库路径查找。

• 适用场景:包含自定义头文件。

12.1.2 库文件包含

cpp 复制代码
#include <filename.h>

• 查找策略:直接去标准库路径查找。

• 适用场景:包含系统标准库头文件。

标准头文件路径

• Linux:/usr/include

• VS环境(VS2013):C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

12.2 嵌套文件包含与重复引入问题

若头文件被多次#include,其内容会被重复编译,导致代码冗余、编译变慢,甚至引发重定义错误。

解决方法:条件编译防重复包含

方法1:#ifndef 方式(经典方案)

cpp 复制代码
#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif // __TEST_H__

• 原理:首次包含时__TEST_H__未定义,编译头文件内容并定义__TEST_H__;后续包含时因__TEST_H__已定义,跳过内容。

方法2:#pragma once(现代方案)

cpp 复制代码
#pragma once
// 头文件内容

• 原理:编译器保证该头文件仅被编译一次,无需手动定义宏。

• 注意:部分老旧编译器不支持,兼容性略逊于#ifndef方式。

13. 其他预处理指令

|----------------|------------------------------------------|
| 指令 | 作用 |
| #error | 编译时输出错误信息,终止编译 |
| #pragma | 向编译器发送特定指令(如#pragma once、#pragma pack()) |
| #line | 重置当前行号和文件名(用于调试) |
| #pragma pack() | 调整结构体成员的内存对齐方式 |

核心总结

  1. 预处理阶段:完成宏替换、头文件包含、条件编译等工作,不参与实际编译。

  2. 宏的本质:文本替换,无类型检查,需注意优先级和副作用问题。

  3. 宏 vs 函数:宏适合简单、高频、类型无关的运算;函数适合复杂、需类型检查、可调试的逻辑。

  4. 头文件保护:必须使用#ifndef或#pragma once防止重复包含。

  5. 条件编译:灵活控制代码编译,实现调试、跨平台等需求。

相关推荐
寻寻觅觅☆12 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
时代的凡人12 小时前
0208晨间笔记
笔记
YJlio12 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
l1t12 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
今天只学一颗糖12 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
赶路人儿13 小时前
Jsoniter(java版本)使用介绍
java·开发语言
testpassportcn13 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it
ceclar12313 小时前
C++使用format
开发语言·c++·算法
码说AI14 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS14 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化