4、防止重复包含:#ifndef / #define / #endif 宏定义、#pragma once 的使用

重复包含,说白了就是同一个头文件被编译器处理了两次以上。你想想看,一个头文件里如果定义了结构体、声明了变量,被包含两次,编译器就会报「重复定义」的错误。我刚开始写C程序那会儿,就经常被这种错误搞得一头雾水。

为什么会出现重复包含?

举个简单的例子。假设你有三个文件:a.hb.hmain.ca.h 里定义了一个结构体,b.h 里包含了 a.h,然后 main.c 又同时包含了 a.hb.h。这样一来,a.h 就被处理了两次。

我在项目中遇到过更夸张的情况——一个头文件被嵌套包含了七八层,最后编译器报了几百个重复定义错误。排查起来真是欲哭无泪。

解决方案一:#ifndef / #define / #endif 宏定义

这是C语言最经典、最通用的做法。也叫「包含卫士」或「头文件卫士」。原理很简单:

  • 第一次包含时,宏没定义,所以进入 #ifndef#endif 之间的代码
  • 同时 #define 定义了这个宏
  • 第二次再包含时,宏已经定义了,#ifndef 条件不成立,直接跳过整个文件内容

标准写法如下:

#ifndef __MY_HEADER_H__
#define __MY_HEADER_H__

/* 头文件内容写在这里 */
typedef struct {
    int id;
    char name[32];
} Student;

void print_student(const Student *s);

#endif /* __MY_HEADER_H__ */
命名规范小建议:我个人习惯用 __模块名_文件名_H__ 这种格式。比如 __DRIVER_UART_H__。注意不要用下划线开头加单个大写字母这种(比如 _HEADER_H),因为C标准保留了一些下划线开头的标识符。

解决方案二:#pragma once

#pragma once 是另一种防止重复包含的方式。它不是一个宏,而是一个编译器指令。告诉编译器:这个文件只被包含一次。

#pragma once

/* 头文件内容写在这里 */
typedef struct {
    int id;
    char name[32];
} Student;

void print_student(const Student *s);

嗯,这里要注意——#pragma once 不是C语言标准的一部分。它是编译器扩展。主流的编译器(GCC、Clang、MSVC、IAR、Keil)都支持,但一些老旧的或者小众的编译器可能不支持。

两种方式的对比

对比项 #ifndef / #define / #endif #pragma once
标准性 C语言标准,所有编译器都支持 非标准,主流编译器支持
可读性 代码稍多,但意图明确 简洁,一行搞定
防错能力 依赖宏名唯一性,命名冲突会失效 由编译器保证,不会冲突
编译速度 编译器仍需打开文件检查宏 编译器可以跳过文件打开操作,略快
适用场景 所有项目,尤其是跨平台项目 单一平台、现代编译器项目

我的个人建议

如果你问我怎么选,我的答案是:优先用 #ifndef 方案。原因很简单——它保证在任何编译器上都能工作。我在做嵌入式项目时,经常要换编译器(比如从GCC换到IAR,或者换到Keil),#pragma once 有时候就会出问题。

不过,如果你确定项目只用GCC或者Clang,用 #pragma once 也没问题。它确实更简洁,编译速度也快一点点。

避坑指南:我曾经在一个项目里看到有人把两种方式混着用——既写了 #ifndef 又写了 #pragma once。这其实没必要,反而容易让人困惑。选一种,坚持用下去就好。

核心知识体系

下面这张图帮你理清防止重复包含的两种方式及其关系:

防止重复包含的两种方式 防止重复包含 #ifndef / #define / #endif #pragma once C语言标准 所有编译器支持 依赖宏名唯一 编译器扩展 主流编译器支持 编译器保证不冲突 个人建议:优先使用 #ifndef 方案

实际项目中的最佳实践

说了这么多,总结几条我实际项目中的经验:

  1. 每个头文件都必须有防止重复包含的机制——没有例外。哪怕这个头文件目前只包含一个注释,也要加上。因为未来谁也不知道它会变成什么样。
  2. 团队统一风格——要么全用 #ifndef,要么全用 #pragma once。混用只会增加维护成本。
  3. 宏名要有辨识度——别用 _H_ 这种太通用的名字。万一两个头文件都用了 _H_,那包含卫士就失效了。
  4. 不要依赖包含顺序——每个头文件都应该能独立编译。也就是说,它自己就应该包含它依赖的所有头文件,不要指望别人先包含。

核心要点:防止重复包含不是可选项,而是必须项。它保证了头文件可以被安全地多次包含,是模块化编程的基础设施之一。选 #ifndef 还是 #pragma once?我的建议是——如果你不确定编译器环境,就用 #ifndef。如果你确定只用现代主流编译器,用 #pragma once 也没问题。


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