
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- [C 语言预处理(下):宏与函数对比 +#/## 运算符 + 条件编译 + 头文件包含 ✨](#/## 运算符 + 条件编译 + 头文件包含 ✨)
-
- [前景回顾:预处理基础速记 📝](#前景回顾:预处理基础速记 📝)
- [一、宏与函数的终极对比 ------ 选对工具才高效 🆚](#一、宏与函数的终极对比 —— 选对工具才高效 🆚)
-
- [1. 核心差异对比表](#1. 核心差异对比表)
- [2. 宏的优势场景:简单运算+通用类型](#2. 宏的优势场景:简单运算+通用类型)
- [3. 函数的优势场景:复杂逻辑+可调试](#3. 函数的优势场景:复杂逻辑+可调试)
- [4. 命名约定:区分宏和函数](#4. 命名约定:区分宏和函数)
- [二、`#`和`##`运算符 ------ 宏的高级玩法 🪄](#
和##`运算符 —— 宏的高级玩法 🪄) - [三、`#undef` ------ 宏的开关 🎚️](#undef` —— 宏的开关 🎚️)
-
- [1. 基本语法](#1. 基本语法)
- [2. 使用示例](#2. 使用示例)
- [四、命令行定义 ------ 编译时动态传参 🖥️](#四、命令行定义 —— 编译时动态传参 🖥️)
-
- [1. 使用示例:动态指定数组大小](#1. 使用示例:动态指定数组大小)
- [2. 编译并运行(Linux/macOS)](#2. 编译并运行(Linux/macOS))
- [五、条件编译 ------ 按需编译代码 📦](#五、条件编译 —— 按需编译代码 📦)
-
- [1. 单个分支:`#if + #endif`](#if + #endif`)
- [2. 多个分支:`#if + #elif + #else + #endif`](#if + #elif + #else + #endif`)
- 3. 判断宏是否定义:`#ifdef`/`#ifndef`
- [4. 嵌套条件编译](#4. 嵌套条件编译)
- [六、头文件包含 ------ 避免重复包含的技巧 📂](#六、头文件包含 —— 避免重复包含的技巧 📂)
-
- [1. 头文件包含的两种方式](#1. 头文件包含的两种方式)
- [2. 重复包含的问题](#2. 重复包含的问题)
- [3. 解决方法:两种方案任选](#3. 解决方法:两种方案任选)
-
- 方案1:条件编译(跨编译器兼容)
- [方案2:`#pragma once`(简洁高效)](#pragma once`(简洁高效))
- [七、其他预处理指令 ------ 拓展了解 📖](#七、其他预处理指令 —— 拓展了解 📖)
- [写在最后 📝](#写在最后 📝)
C 语言预处理(下):宏与函数对比 +#/## 运算符 + 条件编译 + 头文件包含 ✨
在上一篇中,我们掌握了预定义符号、#define定义常量和宏的基础用法,这一篇我们继续深挖预处理的进阶知识点:宏与函数的核心对比、#和##运算符的妙用、条件编译的实战技巧,以及头文件包含的避坑指南,帮你彻底吃透预处理的所有核心考点!
前景回顾:预处理基础速记 📝
回顾上一篇核心知识点,衔接本篇进阶内容:
- 预定义符号是C语言内置的调试工具,
__FILE__/__LINE__等可快速定位代码位置; #define定义常量时结尾不加;,多行用\续行;- 带参数宏必须给参数和整体加括号,避免优先级问题;
- 带副作用的表达式(如
a++)不适合作为宏参数,易导致变量多次修改。
一、宏与函数的终极对比 ------ 选对工具才高效 🆚
宏和函数都能实现"代码复用",但适用场景和特性差异极大。很多初学者分不清二者的区别,我们从多个维度拆解对比,帮你精准选择。
1. 核心差异对比表
| 特性 | #define定义宏 |
函数 |
|---|---|---|
| 代码长度 | 每次调用都会替换代码,除极小宏外,会大幅增加程序长度 | 代码只写一次,调用时跳转执行,程序长度更精简 |
| 执行速度 | 无函数调用开销(栈帧创建/销毁),速度更快 | 有参数传递、栈帧创建等额外开销,相对较慢 |
| 操作符优先级 | 易因优先级问题出错(需加括号规避) | 只在调用时求值,无优先级问题 |
| 带副作用参数 | 参数多次替换,副作用会放大(如a++多次执行) |
参数先求值再传递,副作用仅执行一次 |
| 参数类型 | 不关注类型,适配任意类型(通用) | 严格匹配类型,不同类型需重载/重写 |
| 调试 | 预处理阶段完成替换,无法调试 | 可打断点,逐行调试 |
| 递归 | 不支持递归(预处理无法处理) | 支持递归 |
2. 宏的优势场景:简单运算+通用类型
宏最适合实现简单、高频的运算逻辑,尤其是需要适配多种类型的场景:
c
// 通用内存分配宏(适配任意类型)
#define MALLOC(n, type) (type*)malloc(n * sizeof(type))
// 使用示例:无需关心类型,宏自动适配
int* p1 = MALLOC(10, int);
double* p2 = MALLOC(5, double);
char* p3 = MALLOC(20, char);
如果用函数实现,需要为每种类型写一个函数,代码冗余且维护成本高。
3. 函数的优势场景:复杂逻辑+可调试
当逻辑复杂、需要调试,或参数有副作用时,优先用函数:
c
// 求两数最大值(函数版,无副作用问题)
int Max(int x, int y) {
return x > y ? x : y;
}
// 复杂运算(如排序、递归),函数更易维护
int Factorial(int n) {
if (n <= 1) return 1;
return n * Factorial(n-1); // 递归仅函数支持
}
4. 命名约定:区分宏和函数
为了让代码更易读,行业通用约定:
- 宏名:全部大写(如
MAX、SQUARE、MALLOC); - 函数名:小写/驼峰命名(如
max、factorial、getMax); - 特例:
offsetof是宏但小写(历史原因,无需纠结)。
二、#和##运算符 ------ 宏的高级玩法 🪄
C语言为宏提供了两个特殊运算符:#(字符串化)和##(记号粘合),能大幅提升宏的灵活性,是预处理的进阶考点。
1. #运算符:参数字符串化
#的核心作用是将宏参数直接转换为字符串(双引号包裹),常用于简化打印逻辑:
c
#include <stdio.h>
// 简化打印宏(#将参数转为字符串)
#define PRINT(n, format) printf("the value of "#n " is " format "\n", n)
int main() {
int a = 10;
float f = 3.14f;
char c = 'A';
// 宏替换后自动拼接字符串,无需重复写变量名
PRINT(a, "%d"); // 等价于:printf("the value of a is %d\n", a);
PRINT(f, "%f"); // 等价于:printf("the value of f is %f\n", f);
PRINT(c, "%c"); // 等价于:printf("the value of c is %c\n", c);
return 0;
}
输出结果:
the value of a is 10
the value of f is 3.140000
the value of c is A
2. ##运算符:符号粘合
##的核心作用是将两个符号(变量名、函数名、类型名等)拼接成一个新符号,常用于自动生成代码:
c
#include <stdio.h>
// 自动生成对应类型的max函数
#define GENERIC_MAX(type) type type##_max(type x, type y) { return x>y ? x:y; }
// 调用宏生成函数
GENERIC_MAX(int); // 生成:int int_max(int x, int y) { ... }
GENERIC_MAX(float); // 生成:float float_max(float x, float y) { ... }
GENERIC_MAX(short); // 生成:short short_max(short x, short y) { ... }
int main() {
printf("%d\n", int_max(10, 20)); // 输出20
printf("%.1f\n", float_max(3.1f, 2.9f)); // 输出3.1
return 0;
}
💡 注意:
##拼接的符号必须是合法的标识符(不能以数字开头、不能是关键字),否则编译报错。
三、#undef ------ 宏的开关 🎚️
#undef用于移除已定义的宏,相当于"关闭"这个宏,移除后该宏名不再有效,可重新定义。
1. 基本语法
c
#undef 宏名
2. 使用示例
c
#include <stdio.h>
#define MAX 100
int main() {
printf("MAX = %d\n", MAX); // 输出100
#undef MAX // 移除MAX的定义
// printf("MAX = %d\n", MAX); // 编译报错:MAX未定义
#define MAX 200 // 重新定义MAX
printf("MAX = %d\n", MAX); // 输出200
return 0;
}
适用场景:临时关闭某个宏(如调试宏)、根据场景重新定义宏的值。
四、命令行定义 ------ 编译时动态传参 🖥️
部分编译器(如GCC、Clang)支持在命令行编译时,通过-D参数动态定义宏,无需修改源代码。
1. 使用示例:动态指定数组大小
c
// test.c
#include <stdio.h>
int main() {
int arr[SZ]; // SZ未在代码中定义
for (int i=0; i<SZ; i++) {
arr[i] = i+1;
printf("%d ", arr[i]);
}
return 0;
}
2. 编译并运行(Linux/macOS)
bash
# 编译时通过-D定义SZ=5
gcc test.c -DSZ=5 -o test
# 运行程序
./test
输出结果:1 2 3 4 5
💡 拓展:Windows下的MinGW编译器也支持
-D参数,VS编译器可通过"项目属性→C/C++→预处理器→预处理器定义"添加。
五、条件编译 ------ 按需编译代码 📦
条件编译允许我们根据条件决定哪些代码参与编译,哪些代码被忽略,常用于调试、跨平台开发。
1. 单个分支:#if + #endif
c
#define DEBUG 1 // 调试模式:1开启,0关闭
#include <stdio.h>
int main() {
int a = 10;
#if DEBUG == 1 // 条件为真,代码参与编译
printf("调试信息:a = %d\n", a); // 调试时打印
#endif
printf("正常逻辑执行\n"); // 始终编译
return 0;
}
当DEBUG=0时,printf("调试信息:...")会被预处理阶段移除,不参与编译。
2. 多个分支:#if + #elif + #else + #endif
c
#define PLATFORM 2 // 1=Windows,2=Linux,3=macOS
#include <stdio.h>
int main() {
#if PLATFORM == 1
printf("Windows系统\n");
#elif PLATFORM == 2
printf("Linux系统\n");
#else
printf("macOS系统\n");
#endif
return 0;
}
输出结果:Linux系统
3. 判断宏是否定义:#ifdef/#ifndef
c
#include <stdio.h>
// #define TEST // 注释/取消注释,控制编译逻辑
#ifdef TEST // 等价于 #if defined(TEST)
printf("TEST宏已定义\n");
#else
printf("TEST宏未定义\n");
#endif
#ifndef DEBUG // 等价于 #if !defined(DEBUG)
#define DEBUG 0 // 未定义则默认关闭调试
#endif
4. 嵌套条件编译
条件编译支持嵌套(类似if语句嵌套),大型项目中常见:
c
#define OS 1
#define BIT 64
#if OS == 1
#if BIT == 32
printf("Windows 32位\n");
#elif BIT == 64
printf("Windows 64位\n");
#endif
#else
printf("其他系统\n");
#endif
六、头文件包含 ------ 避免重复包含的技巧 📂
#include是最常用的预处理指令,但多次包含同一个头文件会导致编译冗余(如重复定义、重复声明),我们需要掌握避坑方法。
1. 头文件包含的两种方式
| 方式 | 适用场景 | 查找策略 |
|---|---|---|
#include <xxx.h> |
标准库头文件(如stdio.h、stdlib.h) |
直接在标准库路径查找 |
#include "xxx.h" |
自定义头文件(如test.h、common.h) |
先找源文件所在目录 → 再找标准库路径 |
💡 建议:严格遵循"标准库用
<>,自定义用""",提升代码可读性。
2. 重复包含的问题
比如:a.h包含test.h,b.h也包含test.h,main.c包含a.h和b.h,则test.h会被包含两次,导致重复定义报错。
3. 解决方法:两种方案任选
方案1:条件编译(跨编译器兼容)
在头文件开头和结尾添加条件编译指令,确保只编译一次:
c
// test.h
#ifndef __TEST_H__ // 如果未定义__TEST_H__
#define __TEST_H__ // 定义__TEST_H__
// 头文件内容(函数声明、宏定义、结构体定义等)
#define PI 3.14159
void print_info();
#endif // __TEST_H__ // 结束条件编译
方案2:#pragma once(简洁高效)
部分编译器(GCC、VS、Clang)支持#pragma once,直接声明头文件只编译一次:
c
// test.h
#pragma once // 核心指令,无需额外代码
// 头文件内容
#define PI 3.14159
void print_info();
💡 推荐:优先用
#pragma once(简洁),如需兼容所有编译器,用条件编译。
七、其他预处理指令 ------ 拓展了解 📖
除了上述核心指令,还有一些优先级较低的预处理指令(如#error、#line、#pragma),感兴趣可参考《C语言深度解剖》深入学习:
#error:强制触发编译错误,输出自定义提示;#line:修改当前行号和文件名;#pragma:编译器特定指令(如#pragma pack调整内存对齐)。
写在最后 📝
到这里,C语言预处理的所有核心知识点就全部讲解完毕了!从基础的预定义符号、#define常量/宏,到进阶的宏与函数对比、#/##运算符、条件编译、头文件包含,我们形成了完整的知识体系。
预处理是C语言编译的第一步,也是写出"灵活、高效、可维护"代码的关键。重点记住:
- 宏适合简单运算、通用类型,函数适合复杂逻辑、可调试场景;
- 条件编译和头文件防重复包含是大型项目的必备技巧;
- 预处理指令发生在编译前,是"文本替换"而非"代码执行"。
至此,C语言的基础语法已全部学完!推荐拓展书籍:《C陷阱和缺陷》《高质量的C/C++编程》《剑指offer》,下一站可以进军《数据结构》啦~
核心要点总结
- 宏无调用开销但易出错,函数可调试但有开销,根据场景选择;
#将参数字符串化,##拼接符号,是宏的高级用法;- 条件编译可按需编译代码,常用于调试和跨平台开发;
- 头文件用
#ifndef或#pragma once避免重复包含; - 命令行定义(
-D)可动态修改宏的值,无需改源代码。
寄语:人和人最小的差距是智商,最大的差距是坚持~ 坚持敲代码、多实战,你一定能吃透C语言!