预处理器
预处理器是 C 语言编译过程的第一个阶段,在编译之前处理源代码。预处理器指令以 # 开头,用于宏定义、条件编译和文件包含等。
预处理器概述
预处理器的功能:
- 宏定义和展开
- 文件包含
- 条件编译
- 行控制
- 错误和警告生成
预处理发生在编译之前,预处理器不进行语法检查,只是文本替换。
宏定义
简单宏
使用 #define 定义常量:
#define PI 3.14159
#define MAX_SIZE 100
#define AUTHOR "张三"
printf("圆周率: %f\n", PI);
printf("最大大小: %d\n", MAX_SIZE);
宏定义没有分号结尾,替换时原样展开。
带参数的宏
类似函数的宏:
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
int result = SQUARE(5); // 展开为 ((5) * (5))
int max_val = MAX(3, 7); // 展开为 ((3) > (7) ? (3) : (7))
宏的注意事项
括号的重要性
#define BAD_SQUARE(x) x * x
int result = BAD_SQUARE(2 + 3); // 展开为 2 + 3 * 3 + 2 = 13,而不是 25
#define GOOD_SQUARE(x) ((x) * (x))
int correct = GOOD_SQUARE(2 + 3); // 展开为 ((2 + 3) * (2 + 3)) = 25
副作用问题
#define SQUARE(x) ((x) * (x))
int i = 5;
int result = SQUARE(i++); // 展开为 ((i++) * (i++)),i 被递增两次!
对于有副作用的表达式,应使用函数:
inline int square(int x) {
return x * x;
}
多行宏
使用反斜杠续行:
#define SWAP(a, b, type) do { \
type temp = a; \
a = b; \
b = temp; \
} while (0)
int x = 1, y = 2;
SWAP(x, y, int); // x=2, y=1
do { ... } while (0) 确保宏在任何上下文中都能正确工作。
# 和 ## 运算符
# 字符串化
将参数转换为字符串:
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define VALUE 100
printf("%s\n", STRINGIFY(VALUE)); // 输出: VALUE
printf("%s\n", TOSTRING(VALUE)); // 输出: 100
# 直接字符串化,TOSTRING 先展开再字符串化。
## 连接
连接两个标记:
#define CONCAT(a, b) a##b
#define MAKE_FUNC(name) void func_##name(void) { printf(#name "\n"); }
int CONCAT(var, 1) = 10; // 创建变量 var1
MAKE_FUNC(test); // 创建函数 func_test
func_test(); // 调用函数
可变参数宏
C99 支持可变参数宏:
#define DEBUG_PRINT(fmt, ...) \
fprintf(stderr, "[DEBUG] " fmt "\n", __VA_ARGS__)
DEBUG_PRINT("值: %d", 42);
// 展开为: fprintf(stderr, "[DEBUG] " "值: %d" "\n", 42);
#define LOG(level, fmt, ...) \
printf("[%s] " fmt "\n", level, __VA_ARGS__)
LOG("INFO", "用户 %s 登录", "张三");
__VA_ARGS__ 代表可变参数部分。
取消宏定义
使用 #undef 取消宏定义:
#define MAX_SIZE 100
printf("%d\n", MAX_SIZE); // 100
#undef MAX_SIZE
#define MAX_SIZE 200
printf("%d\n", MAX_SIZE); // 200
文件包含
#include
#include 将指定文件的内容插入到当前位置:
#include <stdio.h> // 尖括号:搜索系统目录
#include "myheader.h" // 引号:先搜索当前目录,再搜索系统目录
头文件保护
防止头文件重复包含:
#ifndef MYHEADER_H
#define MYHEADER_H
// 头文件内容
#endif
现代 C 还可以使用 #pragma once:
#pragma once
// 头文件内容
#pragma once 更简洁,但不是标准的一部分(虽然广泛支持)。
常用标准头文件
| 头文件 | 功能 |
|---|---|
<stdio.h> | 标准输入输出 |
<stdlib.h> | 标准库函数 |
<string.h> | 字符串处理 |
<math.h> | 数学函数 |
<ctype.h> | 字符处理 |
<time.h> | 时间日期 |
<assert.h> | 断言 |
<stddef.h> | 标准定义 |
<stdbool.h> | 布尔类型(C99) |
<stdint.h> | 固定宽度整数(C99) |
<inttypes.h> | 整数格式转换(C99) |
条件编译
条件编译根据条件决定是否编译某段代码。
#if、#elif、#else、#endif
#define DEBUG 1
#if DEBUG == 1
printf("调试模式\n");
#elif DEBUG == 2
printf("详细调试模式\n");
#else
printf("发布模式\n");
#endif
#ifdef、#ifndef
检查宏是否定义:
#ifdef DEBUG
printf("调试信息\n");
#endif
#ifndef RELEASE
printf("非发布版本\n");
#endif
defined 运算符
#if defined(DEBUG) && !defined(RELEASE)
printf("调试模式,非发布版本\n");
#endif
平台判断
#ifdef _WIN32
// Windows 特定代码
#include <windows.h>
#elif defined(__linux__)
// Linux 特定代码
#include <unistd.h>
#elif defined(__APPLE__)
// macOS 特定代码
#include <mach/mach.h>
#endif
调试代码
#ifdef DEBUG
#define DPRINTF(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", __VA_ARGS__)
#else
#define DPRINTF(fmt, ...) ((void)0)
#endif
DPRINTF("变量 x = %d", x); // 仅在 DEBUG 定义时输出
预定义宏
C 语言定义了一些预定义宏:
| 宏 | 说明 |
|---|---|
__FILE__ | 当前源文件名 |
__LINE__ | 当前行号 |
__func__ | 当前函数名(C99) |
__DATE__ | 编译日期 |
__TIME__ | 编译时间 |
__STDC__ | 是否符合 ANSI C |
__STDC_VERSION__ | C 标准版本 |
使用示例:
void log_error(const char* message) {
fprintf(stderr, "错误: %s\n", message);
fprintf(stderr, "位置: %s:%d, 函数: %s\n",
__FILE__, __LINE__, __func__);
}
int main(void) {
printf("文件: %s\n", __FILE__);
printf("行号: %d\n", __LINE__);
printf("日期: %s\n", __DATE__);
printf("时间: %s\n", __TIME__);
printf("C标准: %ld\n", __STDC_VERSION__);
return 0;
}
#error 和 #warning
#error
生成编译错误:
#ifndef __STDC_VERSION__
#error "需要 C99 或更高版本"
#endif
#if __STDC_VERSION__ < 199901L
#error "需要 C99 或更高版本"
#endif
#warning
生成编译警告(GCC 扩展):
#ifdef OLD_API
#warning "OLD_API 已弃用,请使用 NEW_API"
#endif
#pragma
#pragma 提供编译器特定的指令。
常用 pragma
#pragma once // 头文件保护
#pragma message("编译信息") // 输出编译信息
#pragma warning(disable: 4996) // 禁用特定警告(MSVC)
#pragma pack(push, 1) // 1 字节对齐
struct Packed {
char a;
int b;
};
#pragma pack(pop) // 恢复对齐
#pragma GCC
GCC 特定的 pragma:
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_var;
#pragma GCC diagnostic pop
#pragma GCC optimize("O3") // 优化级别
行控制
#line 改变编译器报告的行号和文件名:
#line 100 "custom.c"
// 错误将报告在第 100 行,文件名为 custom.c
常用于代码生成工具。
断言
assert
<assert.h> 提供断言功能:
#include <assert.h>
void process(int* ptr) {
assert(ptr != NULL); // 如果 ptr 为 NULL,程序终止
// 使用 ptr...
}
断言在 NDEBUG 定义时被禁用:
#define NDEBUG // 禁用断言
#include <assert.h>
static_assert
C11 引入静态断言,在编译时检查:
#include <assert.h>
static_assert(sizeof(int) == 4, "int 必须是 4 字节");
static_assert(sizeof(void*) == 8, "需要 64 位平台");
宏与函数的选择
使用宏的场景
- 简单的类型无关操作
- 编译时计算
- 条件编译
- 代码生成
使用函数的场景
- 复杂逻辑
- 需要调试
- 有副作用的参数
- 需要类型安全
内联函数
C99 提供内联函数,结合宏和函数的优点:
static inline int max(int a, int b) {
return a > b ? a : b;
}
实用宏定义
数组大小
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
int arr[] = {1, 2, 3, 4, 5};
for (size_t i = 0; i < ARRAY_SIZE(arr); i++) {
printf("%d ", arr[i]);
}
最小值和最大值
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
安全比较
#define CMP(a, b) (((a) > (b)) - ((a) < (b)))
// 返回 1(a > b)、0(a == b)、-1(a < b)
位操作
#define SET_BIT(x, n) ((x) |= (1U << (n)))
#define CLEAR_BIT(x, n) ((x) &= ~(1U << (n)))
#define TOGGLE_BIT(x, n) ((x) ^= (1U << (n)))
#define CHECK_BIT(x, n) (((x) >> (n)) & 1U)
字符串转换
#define STR(x) #x
#define XSTR(x) STR(x)
#define VERSION 100
printf("版本: %s\n", XSTR(VERSION)); // 输出: 版本: 100
日志宏
#ifdef DEBUG
#define LOG_DEBUG(fmt, ...) \
fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", \
__FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG_DEBUG(fmt, ...) ((void)0)
#endif
#define LOG_INFO(fmt, ...) \
fprintf(stdout, "[INFO] " fmt "\n", __VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
fprintf(stderr, "[ERROR %s:%d] " fmt "\n", \
__FILE__, __LINE__, __VA_ARGS__)
LOG_DEBUG("变量值: %d", x);
LOG_INFO("程序启动");
LOG_ERROR("文件打开失败: %s", filename);
小结
本章介绍了 C 语言的预处理器:
- 宏定义:简单宏、带参数宏、字符串化、连接
- 文件包含:
#include和头文件保护 - 条件编译:
#if、#ifdef、#ifndef、defined - 预定义宏:
__FILE__、__LINE__、__func__等 #error和#warning:编译时信息#pragma:编译器指令- 断言:
assert和static_assert - 实用宏定义技巧
下一章将学习 标准库,了解常用的库函数。