3、包含卫士(Include Guard):#ifndef/#define/#endif 与 #pragma once 的对比与选择
说到头文件,有个老生常谈但又极其重要的问题——重复包含。
你想想看,一个工程里几十上百个头文件,A包含了B,B又包含了C,C又回头包含了A……编译器看到同一个头文件被展开两次,立马翻脸,给你一堆「redefinition」错误。这时候,包含卫士就是你的救星。
我个人习惯,在每一个头文件里都加上包含卫士。这不是可选项,是必须项。哪怕你确定这个头文件只会被包含一次,也请加上。为什么?因为需求会变,代码会重构,你今天觉得不会重复,明天可能就被别人间接包含了两次。
3.1 两种主流方案
目前业界有两种做法:
- 传统宏定义法:
#ifndef / #define / #endif - 编译器指令法:
#pragma once
先看代码,直观感受一下区别。
3.1.1 宏定义法
// file: my_module.h
#ifndef MY_MODULE_H
#define MY_MODULE_H
/* 头文件内容 */
#endif /* MY_MODULE_H */
这是C89标准就支持的方式,跨平台、跨编译器,没有任何兼容性问题。它的原理很简单:第一次包含时,MY_MODULE_H未定义,进入条件编译块,定义宏,然后展开内容。第二次再包含时,宏已定义,直接跳过整个文件。
3.1.2 #pragma once 法
// file: my_module.h
#pragma once
/* 头文件内容 */
简洁,非常简洁。一行搞定,不用费心去想宏名字,也不用担心宏名字冲突。编译器自己会记住这个文件是否已经被包含过。
我在项目中遇到过,有些新手同事觉得#pragma once写起来爽,所有头文件都用它。嗯,大部分情况下确实没问题。但这里有个坑——它不是C标准的一部分。
3.2 对比表格
| 对比维度 | #ifndef / #define / #endif | #pragma once |
|---|---|---|
| 标准支持 | C89/C99/C11/C17 全支持 | 非标准,编译器扩展 |
| 编译器兼容性 | 所有编译器 | 主流编译器均支持(GCC、Clang、MSVC、IAR、ARMCC等) |
| 代码简洁性 | 需要3行 + 宏命名 | 1行搞定 |
| 宏名冲突风险 | 有(需规范命名) | 无 |
| 对符号链接/硬链接的处理 | 可靠(基于宏判断) | 部分编译器可能误判(基于文件路径) |
| 跨平台能力 | 完美 | 主流平台没问题,小众编译器可能不支持 |
3.3 我的选择建议
说实话,这两个方案我都在用,但场景不同。
场景一:通用库、跨平台项目、开源项目
我建议用#ifndef / #define / #endif。为什么?因为你不知道用户会用哪个编译器。万一有人用了个老掉牙的编译器,或者某个嵌入式小众编译器,#pragma once直接报错,那你的库就废了。我曾经在一个ARM Cortex-M0的项目里,用的编译器就不支持#pragma once,结果同事写的头文件全得改,那叫一个痛苦。
场景二:内部项目、团队可控、编译器固定
我个人习惯用#pragma once。简洁、省事、不容易出错。比如我们团队做STM32项目,统一用GCC或ARMCC,那#pragma once完全没问题。少写三行代码,少一个宏命名的烦恼,何乐而不为?
#ifndef方案。如果你100%确定编译器支持#pragma once,且项目不会移植,那用#pragma once更清爽。
3.4 避坑指南
- 宏名字太随意,比如
#ifndef _HEADER_H_,结果另一个头文件也用了这个名字,两个文件互相覆盖,编译报错找了一下午。 - 在同一个文件里混用两种方案,先写
#pragma once,又写#ifndef,虽然不会报错,但多此一举。 - 忘记在
#endif后面加注释,代码量大了之后,根本不知道每个#endif对应哪个头文件。
宏名 = 项目名_模块名_文件名_H
例如:
PROJ_DRV_UART_H全大写,下划线分隔,末尾加_H。这样基本不会冲突。
3.5 总结
说白了,包含卫士这件事,没有绝对的对错。我个人的经验是:
- 新项目、团队可控 →
#pragma once(省事) - 开源库、跨平台、不确定编译器 →
#ifndef(安全) - 嵌入式裸机项目,尤其涉及多种工具链 → 老老实实用
#ifndef
最后说一句:不管你选哪种,一定要加。不加包含卫士的头文件,就像没锁门的房子——迟早出事。