19、头文件重复包含:#ifndef、#pragma once 怎么用?
头文件重复包含,这问题看着小,但坑起来真能让你怀疑人生。
我记得刚入行那会儿,写了一个不小的项目,编译突然报了一堆「redefinition」错误。我盯着代码看了半天,明明每个函数只定义了一次啊。后来才发现,是头文件被间接包含了两次,导致结构体、宏定义全冲突了。嗯,从那以后,我写每个头文件第一件事就是加防护。
为什么会重复包含?
说白了,就是你的头文件被#include了多次。比如a.h包含了b.h,c.h又包含了a.h和b.h。那么b.h就被间接包含了两次。编译器看到同一个结构体定义了两遍,直接翻脸。
核心问题:C语言不允许同一个类型、宏、变量被重复定义。头文件一旦被多次展开,就会触发编译错误。
两种主流防护手段
1. #ifndef 宏守卫(传统方式)
这是C89标准就支持的方式,几乎所有编译器都能用。原理很简单:用一个独一无二的宏来标记这个头文件是否已经被包含过。
// my_header.h
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件内容写在这里
typedef struct {
int id;
char name[32];
} MyStruct;
void my_function(void);
#endif // MY_HEADER_H
第一次包含时,MY_HEADER_H没定义,所以往下走,定义它,然后处理头文件内容。第二次再包含时,MY_HEADER_H已经定义了,预处理器直接跳过整个文件。
我的习惯:宏名用全大写,格式为 __项目名_模块名_文件名_H__。比如 __PROJ_DRIVER_UART_H__。这样基本不会跟别的库冲突。
2. #pragma once(现代方式)
这个就简单粗暴了。告诉编译器:这个文件只处理一次。你不需要自己起宏名,也不用担心名字冲突。
// my_header.h
#pragma once
typedef struct {
int id;
char name[32];
} MyStruct;
void my_function(void);
就一行,干净利落。我个人非常喜欢这种方式,少写代码,少犯错。
注意:#pragma once不是C标准的一部分。它是编译器扩展。不过好消息是,GCC、Clang、MSVC、IAR、ARMCC等主流编译器都支持。如果你要写跨平台、跨编译器的代码,或者要兼容一些老旧的嵌入式编译器,那还是老老实实用#ifndef。
到底选哪个?
我自己的经验是这样的:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 个人项目、小团队 | #pragma once | 简洁,不易出错 |
| 大型项目、开源库 | #ifndef | 标准兼容,可移植性好 |
| 嵌入式裸机开发 | #ifndef | 有些老编译器不认识#pragma once |
| 混合使用 | 两者都写 | 既享受便利,又保证兼容 |
你可能会问:两者都写会不会有问题?不会。#ifndef和#pragma once是独立工作的,互不干扰。我有时候就这么干,尤其是写库的时候,确保万无一失。
// my_header.h
#pragma once
#ifndef MY_HEADER_H
#define MY_HEADER_H
// 头文件内容
#endif // MY_HEADER_H
一个常见的坑
我曾经遇到过一个问题:两个头文件用了相同的宏名。比如a.h和b.h都用了_HEADER_H_。结果a.h先被包含,b.h就被跳过了。编译时一堆函数找不到定义,报undefined reference。
所以,用#ifndef时,宏名一定要起得足够独特。别偷懒用_H_、_HEADER_这种。我建议带上项目名和路径信息。
避坑指南:我曾经在某个项目里看到有人用 #ifndef __HEADER_H__。结果跟系统库冲突了,编译报了一堆莫名其妙的错误。记住,以双下划线开头的标识符是留给编译器和标准库用的,别乱用。
知识体系一览
下面这张图帮你理清头文件防护的核心逻辑:
总结一下
头文件防护不是可选项,是必须项。你想想看,一个项目几十上百个头文件,没有防护,编译时到处是redefinition,那画面太美我不敢看。
我个人建议:如果你用的是GCC、Clang或者较新的ARMCC,直接上#pragma once,省心。如果你要写库或者兼容老平台,用#ifndef。如果你跟我一样有点强迫症,那就两个都写上,反正没坏处。
嗯,记住一点:不管用哪种,每个头文件都必须有防护。这是C语言开发的基本素养。
一句话记住:头文件不加防护,就像出门不锁门——不是一定会出事,但出事就是大事。
公众号:蓝海资料掘金营,微信deep3321