写代码时,总有这种时刻:同一处常量改十遍、调试信息删了又加、不同平台来回注释。预处理就像给你一把“遥控器”,按几个键就能全局生效、按场景切换。本文用好懂的例子带你把它玩顺手。

目录

1. 预定义符号

2. #define 定义常量

3. #define 定义宏(带参数)

3.1 给形参和整体都加括号

4. 宏参数的副作用

5. 宏替换规则(发生了什么)

6. 宏 vs 函数(何时用宏,何时用函数)

7. # 和 ##

7.1 #(字符串化,stringize)

7.2 ##(记号粘合,token pasting)

8. 命名约定

9. #undef

10. 命令行定义

11. 条件编译(按需“开关”代码)

12. 头文件的包含

12.1 两种包含方式

12.2 防止重复包含

13. 其他预处理指令


1. 预定义符号

这些是预处理阶段就能用的“内置变量”,无需自己声明:

  • __FILE__:当前编译的源文件名(字符串)

  • __LINE__:当前行号(整型)

  • __DATE__:编译日期(字符串)

  • __TIME__:编译时间(字符串)

  • __STDC__:遵循 ANSI C 则为 1,否则未定义

示例:定位日志信息

#include <stdio.h>
int main() {
    printf("file: %s  line: %d  date: %s  time: %s\n",
           __FILE__, __LINE__, __DATE__, __TIME__);
    return 0;
}

2. #define 定义常量

语法:

#define NAME stuff

示例:

#define MAX 1000
#define reg register         // 给关键字取“别名”
#define do_forever for(;;)   // 语义化代替实现
#define CASE break;case      // 写 switch 时自动补上 break

// 续行写法(行尾加反斜杠)
#define DEBUG_PRINT printf("file:%s\tline:%d\tdate:%s\ttime:%s\n", \
                           __FILE__, __LINE__, __DATE__, __TIME__)

注意:常量宏后面别加分号 ;

示例:

#define MAX 1000;  // ❌
if (ok)
    x = MAX;      // 展开后 if 分支里有两条语句,且无 {},语法容易出错
else
    x = 0;

正确写法:

#define MAX 1000   // ✅

3. #define 定义宏(带参数)

语法:

#define NAME(param-list) stuff
  • 左括号必须紧贴宏名,中间不能有空格,否则参数表会被当成替换文本的一部分。

注意:运算优先级与括号

3.1 给形参和整体都加括号

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

int a = 5;
printf("%d\n", SQUARE(a + 1));  // 期望是 36
// 预处理后: (a + 1) * (a + 1) ✅

如果忘记括号:

#define SQUARE_BAD(x) x * x
printf("%d\n", SQUARE_BAD(a + 1)); // 变成 a + 1 * a + 1 => 11 ❌

再看一个:

#define DOUBLE(x) ((x) + (x))

printf("%d\n", 10 * DOUBLE(5));   // 10 * ((5) + (5)) => 100 ✅

如果没有给“整体”再套一层括号:

#define DOUBLE_BAD(x) (x) + (x)
printf("%d\n", 10 * DOUBLE_BAD(5)); // 10 * 5 + 5 => 55 ❌

结论:
用于数值表达式的宏,应该写成:
对每个形参加括号,再对整个表达式加一层括号。


4. 宏参数的副作用

同一个参数在宏里被用多次,而这个参数带副作用(如 x++),会出现多次执行的问题。

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

int x = 5, y = 8;
int z = MAX(x++, y++);
// 展开:z = ((x++) > (y++) ? (x++) : (y++));
// x、y 可能各自自增两次,结果不可控(x=6 y=10 z=9 这类“意外”)

结论:

  • 不要对带副作用的实参做多次求值。

  • 若无法避免,考虑改成内联函数(C99 也可用 static inline)或普通函数。


5. 宏替换规则(发生了什么)

当你写下一个宏调用,预处理器的大致流程是:

  1. 先检查实参里是否还包含其他宏,若有,先替换实参里的宏。

  2. 把宏体替换到源代码上,对应参数名替换为刚处理过的实参文本。

  3. 对替换后的结果再次扫描,若还包含能替换的宏,继续替换。

补充:

  • 宏的参数和宏体中,都可以再出现其他 #define 的名字,但宏不能递归展开。

  • 字符串字面量中的内容不会被继续展开。


6. 宏 vs 函数(何时用宏,何时用函数)

宏的优势:

  • 零调用开销:没有函数调用/返回成本。适合非常短小、频繁调用的逻辑。

  • 类型无关:参数只做“文本替换”,可对多种类型工作(只要操作符支持)。

宏的劣势:

  • 可读性/可调试性差:宏无法单步调试,错误定位不直观。

  • 代码膨胀:每用一次就“贴”一段代码,太长会让目标文件暴增。

  • 容易踩坑:优先级、副作用、命名冲突等。

例:类型作为参数(函数做不到,但宏能“生成代码”)

#define MALLOC(num, type) ((type*)malloc((num) * sizeof(type)))

int *p = MALLOC(10, int);
// 展开后: (int*)malloc(10 * sizeof(int))

结论实用版:

  • 短小、纯表达式、不涉及副作用的场景,可以用宏。

  • 复杂逻辑、需要调试/类型检查、安全性更高时,用函数或内联函数


7. ###

7.1 #(字符串化,stringize)

宏参数变成字符串字面量

#include <stdio.h>
#define PRINT(n) printf("the value of " #n " is %d\n", (n))

int a = 10;
PRINT(a);  // 预处理后 => printf("the value of " "a" " is %d\n", a);

输出:

the value of a is 10

7.2 ##(记号粘合,token pasting)

## 两边的符号拼接生成新的记号(标识符等)。

示例:用一个“模板宏”生成不同类型的 max 函数名

#define GENERIC_MAX(type)         \
type type##_max(type x, type y) { \
    return x > y ? x : y;         \
}

// 生成两个函数:int_max / float_max
GENERIC_MAX(int)
GENERIC_MAX(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;
}

8. 命名约定

  • 宏名通常全大写,例如:MAX_SIZEPRINT_DEBUG

  • 函数名遵循普通命名法,不全部大写。
    这能让读代码时一眼区分“这是宏还是函数”。


9. #undef

用于移除一个已有的宏定义,常见场景:准备重新定义

#define MAX 1000
#undef MAX
#define MAX 2048  // 重新定义

10. 命令行定义

很多编译器支持在命令行直接定义宏,常用于按环境切配置

示例:数组大小由命令行决定

#include <stdio.h>

#ifndef ARRAY_SIZE
#define ARRAY_SIZE 8
#endif

int main() {
    int a[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; ++i) a[i] = i;
    for (int i = 0; i < ARRAY_SIZE; ++i) printf("%d ", a[i]);
    puts("");
    return 0;
}

编译:

# Linux 示例
gcc -DARRAY_SIZE=16 main.c -o main

11. 条件编译(按需“开关”代码)

开发中常常希望“调试时多打印,发布时不打印”,或“不同平台走不同实现”——用条件编译就很舒服。

最常用的几类:

// 1) 简单开关
#define __DEBUG__
#ifdef __DEBUG__
    printf("debug info...\n");
#endif

// 2) 常量表达式
#define FOO 1
#if FOO
    puts("FOO is on");
#endif

// 3) 多分支
#if defined(OS_UNIX)
    unix_impl();
#elif defined(OS_MSDOS)
    msdos_impl();
#else
    generic_impl();
#endif

// 4) 是否被定义
#if defined(SYMBOL)   // 等价于 #ifdef SYMBOL
#if !defined(SYMBOL)  // 等价于 #ifndef SYMBOL

示例:只在调试时打印数组赋值过程

#include <stdio.h>
// #define __DEBUG__   // 打开试试

int main() {
    int arr[10] = {0};
    for (int i = 0; i < 10; ++i) {
        arr[i] = i;
#ifdef __DEBUG__
        printf("arr[%d]=%d\n", i, arr[i]);
#endif
    }
    return 0;
}

12. 头文件的包含

12.1 两种包含方式

  • 本地文件#include "filename.h"
    先从当前源文件目录找;找不到再去标准路径。

  • 库头文件#include <filename.h>
    直接从标准路径找。

需要效率/语义清晰时:库头用尖括号,本地头用引号。

12.2 防止重复包含

一个头文件被多次 #include,其内容会被拷贝多次,导致重复定义编译极慢
解决方案:头文件保护(Include Guard)#pragma once

写法一:Include Guard

// test.h
#ifndef __TEST_H__
#define __TEST_H__

void test(void);
struct Stu {
    int id;
    char name[20];
};

#endif  // __TEST_H__

写法二:#pragma once

#pragma once
// 其余内容同上

多数主流编译器支持 #pragma once,简洁,但 Include Guard 更标准、可移植


13. 其他预处理指令

  • #error:在预处理阶段主动报错并终止编译(常用于条件不满足时阻断构建)。

  • #pragma:编译器相关的特殊指令(如结构体对齐 #pragma pack)。

  • #line:改变编译器认为的行号/文件名(主要给工具链用)。

这类指令按需了解即可;#pragma pack 在结构体内存布局时最常用。


Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐