跳到主要内容

预处理器

预处理器是 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#ifndefdefined
  • 预定义宏:__FILE____LINE____func__
  • #error#warning:编译时信息
  • #pragma:编译器指令
  • 断言:assertstatic_assert
  • 实用宏定义技巧

下一章将学习 标准库,了解常用的库函数。