每次写 C 代码,最开头总有一行 #include <stdio.h>。你有没有好奇过,那个小小的 # 到底藏着什么魔法?

它其实是一位在编译之前就登场的小魔法师——预处理(Preprocessing)。在正式编译前,它会先帮我们把代码「整理」一遍。今天就来认识一下它会的那些小把戏。

天生就会的预定义符号

有几位符号是 C 语言自带、可以直接用的「隐身好友」,它们由预处理在幕后生成:

__FILE__ 当前源文件名 · __LINE__ 当前行号 · __DATE__ 编译日期 · __TIME__ 编译时间 · __STDC__(遵循 ANSI C 时为 1)

printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);

#define:给常量起个可爱名字

最常见的小魔法就是定义常量,语法是「名字 + 值」:

#define MAX 100
#define MESSAGE "Hello, World!"

需要注意一个容易踩到的小坑:宏定义结尾不要加分号 ;。因为宏只是「简单文本替换」,多一个分号往往会让后面的代码悄悄报错哦。

#define 定义宏:会变身的替换术

宏还可以接收参数,把参数「替换」进文本里:

#define SQUARE(x) ((x) * (x))

int result = SQUARE(5 + 1);   // 展开后是 ((5 + 1) * (5 + 1))

看到那一层层括号了吗?它们是宏的「护身符」——防止运算符优先级捣乱。

带副作用的参数,要小心

#define MAX(a,b) ((a) > (b) ? (a) : (b))

int a = 10, b = 20;
int m = MAX(a++, b++);   // 展开后 a、b 可能被加了不止一次!

像 ++a 这种「有副作用」的表达式,传进只做文本替换的宏里,就会发生产生意料之外的结果。记住:宏是替换,不是调用。

宏 vs 函数:一场小小的较劲

宏更快的理由:省去了函数调用的开销,而且类型无关——比较大小这种小事,int、float 都能用同一个宏。

宏更麻烦的地方:每次使用都会复制一份代码(可能让程序变长)、没法调试、类型不严谨、还可能踩优先级坑。

两个神秘符号:# 和 ##

# 能把参数「字符串化」,## 能把两边的符号「粘」成一个新标识符:

#define PRINT(n, format) printf("the value of " #n " is " format "\n", n)
// PRINT(a, "%d")  →  #n 变成字符串 "a"

#define GENERIC_MAX(type) \
type type##_max(type x, type y) { return (x>y?x:y); }
// GENERIC_MAX(int)  →  int##_max 生成 int_max 这个函数名

条件编译:写着备用的代码

有时候我们想留几行「调试用」的代码,又不想每次都编译进去,这时候条件编译就派上用场了:

#define __DEBUG__
for (int i = 0; i < 10; i++) {
#ifdef __DEBUG__
    printf("%d\n", arr[i]);   // 定义了 __DEBUG__ 才会编译这行
#endif
}

还有 #if / #elif / #else / #endif、#if defined(M)、#ifndef 这些亲戚,本质都是在「编译前」决定哪些代码留下来。

头文件的包含与防重复

#include "xxx.h" 会先在源文件目录找,#include <xxx.h> 直接去标准路径找。而同一个头文件被重复包含多次,会让编译变得吃力,于是就有了防重复的保护:

#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif

现在更推荐简洁的 #pragma once。此外还有 #undef(移除宏)、#pragma pack()(改对齐数)等许多小指令,都在《C 语言深度解剖》里能挖到更多细节。

预处理魔法师的可爱插画
在编译登场之前,先把代码温柔地整理好

✿ 摘要

  • 预处理在编译之前运行,主要处理 # 开头的指令。
  • #define 定义常量或宏,本质是文本替换,结尾不加分号。
  • 宏类型无关、更快,但没法调试、易踩优先级坑。
  • # 字符串化、## 记号粘合。
  • 条件编译(#ifdef/#if/#else)与头文件防重复(#ifndef / #pragma once)。

愿当下的花如你所见般美丽