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++));
执行过程:
-
比较x++(5)和y++(8)→ 5 > 8为假,执行y++ → y变为9
-
取y++的值(9)赋给z → z=9,y再自增变为10
-
x仅在比较时自增一次 → x=6
最终输出: x=6 y=10 z=9
⚠️ 结论:避免在宏参数中使用带有副作用的表达式,否则结果难以预测。
5. 宏替换的规则
-
参数预处理:调用宏时,先检查参数是否包含#define定义的符号,若有则先替换。
-
文本替换:将宏体中的参数名替换为实际参数值,再将结果插入到原宏调用位置。
-
再次扫描:对替换后的结果再次扫描,检查是否包含其他#define符号,重复上述过程。
注意事项
• 宏参数和宏定义中可以包含其他#define符号,但宏不能递归定义。
• 预处理器搜索#define符号时,字符串常量中的内容不会被搜索(如printf("MAX")中的MAX不会被替换)。
6. 宏与函数的对比
宏的优势
-
执行速度更快:宏是预处理阶段的文本替换,无函数调用/返回的开销。
-
类型无关:宏参数无类型限制,可适用于整数、浮点数等可比较的类型(如MAX宏可处理任意类型)。
-
可接受类型参数:宏可以将类型作为参数(如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. 条件编译
在编译时,根据条件选择性地编译部分代码,常用于调试、跨平台开发。
常见条件编译指令
- 单分支条件编译
cpp
#if 常量表达式
// 代码段
#endif
示例:调试开关
cpp
#define __DEBUG__ 1
#if __DEBUG__
printf("调试信息\n");
#endif
- 多分支条件编译
cpp
#if 常量表达式1
// 代码段1
#elif 常量表达式2
// 代码段2
#else
// 代码段3
#endif
- 判断是否被定义
cpp
#if defined(symbol) // 等价于 #ifdef symbol
#if !defined(symbol) // 等价于 #ifndef symbol
- 嵌套条件编译
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() | 调整结构体成员的内存对齐方式 |
核心总结
-
预处理阶段:完成宏替换、头文件包含、条件编译等工作,不参与实际编译。
-
宏的本质:文本替换,无类型检查,需注意优先级和副作用问题。
-
宏 vs 函数:宏适合简单、高频、类型无关的运算;函数适合复杂、需类型检查、可调试的逻辑。
-
头文件保护:必须使用#ifndef或#pragma once防止重复包含。
-
条件编译:灵活控制代码编译,实现调试、跨平台等需求。