1. 预定义符号
C语言预处理器定义了一些特殊的符号,称为预定义符号。这些符号在编译时会被替换为特定的值,常用于调试和日志记录。
c
#include <stdio.h>
int main() {
printf("当前文件: %s\n", __FILE__); // 编译的源文件名
printf("当前行号: %d\n", __LINE__); // 当前行号
printf("编译日期: %s\n", __DATE__); // 编译日期,格式 "Mmm dd yyyy"
printf("编译时间: %s\n", __TIME__); // 编译时间,格式 "hh:mm:ss"
#ifdef __STDC__
printf("编译器遵循ANSI C标准\n");
#endif
return 0;
}
重要说明:
__FILE__和__LINE__常用于调试宏,帮助定位错误位置__DATE__和__TIME__记录的是编译时刻,不是程序运行时刻__STDC__的值取决于编译器是否遵循 ANSI C 标准
2. #define 定义常量
#define 指令用于定义宏,可以是简单的文本替换(常量宏)或带参数的宏。
2.1 基本语法
c
#define 标识符 替换文本
2.2 示例与注意事项
c
#define PI 3.1415926535
#define MAX_SIZE 100
#define BUFFER_SIZE 1024
#define NEWLINE '\n'
重要规则:
-
不要加分号:宏是简单的文本替换,不是语句
c// 错误示例 #define PI 3.14159; float area = PI * r * r; // 展开后:float area = 3.14159; * r * r; // 正确示例 #define PI 3.14159 float area = PI * r * r; // 展开后:float area = 3.14159 * r * r; -
命名约定:通常使用全大写字母,单词间用下划线分隔
-
作用域 :从定义点开始到文件结束,或遇到
#undef指令
3. #define 定义宏(带参数)
带参数的宏类似于函数,但本质是文本替换。
3.1 基本语法
c
#define 宏名(参数列表) 替换文本
3.2 正确与错误示例对比
错误写法(有空格):
c
#define SQUARE (x) x * x // 注意:宏名和左括号之间有空格
int result = SQUARE(5); // 展开为:int result = (x) x * x(5);
正确写法(无空格):
c
#define SQUARE(x) x * x
int result = SQUARE(5); // 展开为:int result = 5 * 5;
3.3 括号的重要性
问题示例:
c
#define SQUARE(x) x * x
int a = 5;
int result1 = SQUARE(a + 1); // 展开为:a + 1 * a + 1 = 5 + 1 * 5 + 1 = 11
int result2 = 100 / SQUARE(a); // 展开为:100 / 5 * 5 = 100
正确写法(加括号):
c
#define SQUARE(x) ((x) * (x))
int a = 5;
int result1 = SQUARE(a + 1); // 展开为:((a + 1) * (a + 1)) = 36
int result2 = 100 / SQUARE(a); // 展开为:100 / ((5) * (5)) = 4
黄金法则:宏定义中的每个参数和整个表达式都应该用括号括起来。
4. 带有副作用的宏参数
4.1 什么是副作用?
副作用是指表达式求值过程中改变了变量的值。
c
x + 1; // 无副作用:不改变x的值
x++; // 有副作用:x的值增加1
y = x++; // 有副作用:x的值增加1
4.2 宏参数副作用的危险
危险示例:
c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = 10;
int z = MAX(x++, y++); // 展开为:((x++) > (y++) ? (x++) : (y++))
// 执行后:
// x 可能变为 6 或 7(取决于实现)
// y 可能变为 11 或 12(取决于实现)
// z 的值不确定
安全做法:
-
避免在宏参数中使用有副作用的表达式
-
如果必须使用,先计算参数值再传入
cint temp_x = x++; int temp_y = y++; int z = MAX(temp_x, temp_y);
5. 宏替换的规则
预处理器按照以下步骤展开宏:
5.1 替换步骤
- 参数检查 :首先检查参数是否包含其他
#define定义的符号 - 参数替换:将参数替换为实际值
- 重新扫描:再次扫描结果,查找其他需要替换的宏
5.2 示例演示
c
#define PI 3.14
#define AREA(r) (PI * (r) * (r))
#define PRINT_AREA(r) printf("半径为%d的圆面积: %.2f\n", r, AREA(r))
int main() {
PRINT_AREA(5);
// 展开过程:
// 1. PRINT_AREA(5) → printf("半径为%d的圆面积: %.2f\n", 5, AREA(5))
// 2. AREA(5) → (PI * (5) * (5))
// 3. PI → 3.14
// 最终:printf("半径为%d的圆面积: %.2f\n", 5, (3.14 * (5) * (5)))
return 0;
}
5.3 重要限制
-
不能递归:宏不能调用自身
c#define FACTORIAL(n) ((n) <= 1 ? 1 : (n) * FACTORIAL((n)-1)) // 错误! -
字符串常量不搜索:
c#define HELLO "Hello" printf("HELLO World"); // 输出 "HELLO World",不会替换 printf(HELLO " World"); // 输出 "Hello World"
6. 宏和函数的对比
6.1 宏的优势
c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define ABS(x) ((x) < 0 ? -(x) : (x))
优势分析:
- 执行速度:宏是编译时展开,没有函数调用开销
- 类型无关:适用于多种数据类型
- 代码内联:避免函数调用栈操作
6.2 宏的劣势
- 代码膨胀:每次使用都会展开,增加代码大小
- 无法调试:调试器看到的是展开后的代码
- 类型不安全:没有类型检查
- 优先级问题:容易因缺少括号导致错误
- 副作用风险:参数多次求值可能产生意外结果
6.3 宏与函数对比表
| 特性 | 宏 | 函数 |
|---|---|---|
| 代码长度 | 每次使用都展开,可能增加代码大小 | 只有一份代码,调用处只有调用指令 |
| 执行速度 | 快(无调用开销) | 较慢(有调用和返回开销) |
| 操作符优先级 | 容易出错,需要大量括号 | 参数先求值,不会混淆 |
| 副作用参数 | 参数可能被多次求值,危险 | 参数只求值一次,安全 |
| 参数类型 | 类型无关,通用性强 | 类型严格,需要特定类型 |
| 调试 | 困难(看到的是展开后的代码) | 容易(可直接单步调试) |
| 递归 | 不支持 | 支持 |
| 作用域 | 无(只是文本替换) | 有(遵循变量作用域规则) |
6.4 选择建议
- 使用宏:简单计算、类型无关操作、需要内联的小函数
- 使用函数:复杂逻辑、需要递归、参数有副作用、需要类型检查
7. # 和 ## 运算符
7.1 # 运算符(字符串化)
将宏参数转换为字符串字面量。
c
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)
int main() {
int count = 42;
printf("%s\n", STRINGIFY(Hello World)); // 输出: "Hello World"
PRINT_VAR(count); // 输出: count = 42
// 展开过程:
// STRINGIFY(Hello World) → "Hello World"
// PRINT_VAR(count) → printf("count" " = %d\n", count)
return 0;
}
7.2 ## 运算符(记号粘合)
将两个记号连接成一个新的标识符。
c
#define CONCAT(a, b) a##b
#define MAKE_VAR(name, num) name##num
int main() {
int var1 = 10, var2 = 20, var3 = 30;
int CONCAT(var, 1) = 100; // 展开为: int var1 = 100;
int MAKE_VAR(temp, _value) = 50; // 展开为: int temp_value = 50;
// 实用示例:创建一系列变量
#define DECLARE_COUNTER(n) int counter_##n = 0
DECLARE_COUNTER(1); // int counter_1 = 0;
DECLARE_COUNTER(2); // int counter_2 = 0;
return 0;
}
注意事项:
##连接的结果必须是合法的 C 标识符- 常用于自动生成变量名、函数名等
8. 命名约定
为了区分宏和函数,遵循以下约定:
c
// 宏:全大写,单词间用下划线分隔
#define MAX_BUFFER_SIZE 1024
#define CALC_AREA(r) (PI * (r) * (r))
#define DEBUG_PRINT(msg) printf("[DEBUG] %s\n", msg)
// 函数:驼峰命名或小写加下划线
int calculateArea(int radius);
void debug_print(const char* message);
int getMaxValue(int a, int b);
好处:
- 提高代码可读性
- 避免宏与函数混淆
- 符合大多数 C 项目的编码规范
9. #undef 指令
用于取消已定义的宏。
c
#define DEBUG_MODE 1
#ifdef DEBUG_MODE
printf("调试模式开启\n");
#endif
#undef DEBUG_MODE // 取消 DEBUG_MODE 的定义
#ifdef DEBUG_MODE
printf("这行不会执行\n"); // 因为 DEBUG_MODE 已被取消定义
#endif
// 重新定义(必须先取消旧定义)
#define DEBUG_MODE 2
使用场景:
- 限制宏的作用域
- 重新定义宏
- 防止宏名冲突
10. 命令行定义
在编译时通过命令行定义宏。
bash
# GCC 示例
gcc -DDEBUG_MODE -DBUFFER_SIZE=1024 program.c -o program
# 相当于在代码开头添加:
#define DEBUG_MODE
#define BUFFER_SIZE 1024
常用选项:
-DNAME:定义宏 NAME,值为 1-DNAME=VALUE:定义宏 NAME,值为 VALUE-UNAME:取消宏 NAME 的定义
11. 条件编译
根据条件决定是否编译某段代码。
11.1 基本形式
c
#ifdef 宏名
// 如果宏已定义,编译这部分代码
#else
// 如果宏未定义,编译这部分代码
#endif
#ifndef 宏名
// 如果宏未定义,编译这部分代码
#else
// 如果宏已定义,编译这部分代码
#endif
#if 常量表达式
// 如果表达式非零,编译这部分代码
#elif 其他表达式
// 如果前面的条件不满足且此表达式非零
#else
// 如果所有条件都不满足
#endif
11.2 实用示例
c
#include <stdio.h>
#define DEBUG_LEVEL 2
#define PLATFORM_WINDOWS
int main() {
// 根据调试级别编译不同代码
#if DEBUG_LEVEL >= 1
printf("[INFO] 程序启动\n");
#endif
#if DEBUG_LEVEL >= 2
printf("[DEBUG] 详细调试信息\n");
#endif
#if DEBUG_LEVEL >= 3
printf("[TRACE] 跟踪信息\n");
#endif
// 平台相关代码
#ifdef PLATFORM_WINDOWS
printf("Windows 平台\n");
// Windows 特定代码
#elif defined(PLATFORM_LINUX)
printf("Linux 平台\n");
// Linux 特定代码
#else
printf("未知平台\n");
#endif
// 版本控制
#if __STDC_VERSION__ >= 201112L
printf("C11 或更高版本\n");
#elif __STDC_VERSION__ >= 199901L
printf("C99 版本\n");
#else
printf("C89/C90 版本\n");
#endif
return 0;
}
12. 头文件的包含
12.1 两种包含方式
本地文件包含:
c
#include "myheader.h"
搜索顺序:
- 当前源文件所在目录
- 编译器指定的包含目录
- 系统标准包含目录
库文件包含:
c
#include <stdio.h>
#include <stdlib.h>
搜索顺序:
- 编译器指定的系统包含目录
- 不搜索当前目录
12.2 防止头文件重复包含
问题:头文件被多次包含会导致重复定义错误。
解决方案:
c
// myheader.h
#ifndef MYHEADER_H // 如果没有定义 MYHEADER_H
#define MYHEADER_H // 定义 MYHEADER_H
// 头文件内容
int add(int a, int b);
float calculate_average(float arr[], int size);
#endif // 结束 #ifndef
现代简化写法(C/C++):
c
#pragma once
// 头文件内容
int add(int a, int b);
12.3 嵌套文件包含
头文件可以包含其他头文件,但要避免循环包含。
c
// config.h
#ifndef CONFIG_H
#define CONFIG_H
#define MAX_USERS 100
#endif
// user.h
#ifndef USER_H
#define USER_H
#include "config.h" // 包含其他头文件
typedef struct {
int id;
char name[50];
} User;
User* create_user(int id, const char* name);
#endif
13. 其他预处理指令
13.1 #error
在预处理阶段产生错误信息。
c
#ifndef REQUIRED_MACRO
#error "REQUIRED_MACRO 必须被定义"
#endif
#if __STDC_VERSION__ < 201112L
#error "需要 C11 或更高版本"
#endif
13.2 #pragma
编译器特定的指令。
c
#pragma warning(disable: 4996) // MSVC:禁用特定警告
#pragma pack(1) // 设置结构体对齐方式
#pragma message("编译到这一步") // 输出编译消息
13.3 #line
修改行号和文件名。
c
#line 100 "myfile.c"
printf("这行在文件 %s 的第 %d 行\n", __FILE__, __LINE__);
// 输出:这行在文件 myfile.c 的第 100 行
13.4 预定义宏示例程序
c
#include <stdio.h>
// 条件编译示例
#define DEBUG 1
#define VERSION "1.0.0"
// 带参数的宏
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define SQUARE(x) ((x) * (x))
#define PRINT_INT(n) printf(#n " = %d\n", n)
// 使用 ## 创建变量
#define MAKE_VAR(name, value) int var_##name = value
int main() {
// 使用预定义符号
printf("编译信息:\n");
printf(" 文件: %s\n", __FILE__);
printf(" 行号: %d\n", __LINE__);
printf(" 日期: %s\n", __DATE__);
printf(" 时间: %s\n", __TIME__);
// 条件编译
#ifdef DEBUG
printf("\n调试模式开启\n");
printf