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__。结果跟系统库冲突了,编译报了一堆莫名其妙的错误。记住,以双下划线开头的标识符是留给编译器和标准库用的,别乱用。

知识体系一览

下面这张图帮你理清头文件防护的核心逻辑:

头文件重复包含防护方案 头文件重复包含 选择防护方式 #ifndef 宏守卫 优点:C标准支持,可移植性好 缺点:需手动起宏名,可能冲突 适用:大型项目、嵌入式、开源库 #pragma once 优点:简洁,无需宏名,不易出错 缺点:非标准,老编译器不支持 适用:个人项目、现代编译器 建议:根据场景选择,也可两者并用

总结一下

头文件防护不是可选项,是必须项。你想想看,一个项目几十上百个头文件,没有防护,编译时到处是redefinition,那画面太美我不敢看。

我个人建议:如果你用的是GCC、Clang或者较新的ARMCC,直接上#pragma once,省心。如果你要写库或者兼容老平台,用#ifndef。如果你跟我一样有点强迫症,那就两个都写上,反正没坏处。

嗯,记住一点:不管用哪种,每个头文件都必须有防护。这是C语言开发的基本素养。

一句话记住:头文件不加防护,就像出门不锁门——不是一定会出事,但出事就是大事。


公众号:蓝海资料掘金营,微信deep3321