知识点滴 - 关于头文件的重复包含问题

使用Include guard的目的

C Language Tutorial => Header Include Guards

2.12 — Header guards – Learn C++

C/C++中,#include,这个预处理命令会将此命令后面的文件内容包含进来。

而一般情况下,后面是一个头文件, 比如name.h,因为一般都在源文件的开头来包含,所以叫头文件。

头文件里面的内容其实没有严格限制,可以包含变量或函数的声明或定义,还有类型的定义,宏定义等。

而对一个源文件,.c或.cpp文件,处理完预处理指令,将头文件包含进来并替换宏定义后,得到的就一个可编译单元 - translation unit (TU)。

对于这个可编译单元来讲,是可以包含很多个头文件,头文件里还可以包含其他头文件,所以才会有一个头文件会被间接的多次包含的问题。

对于函数声明来说,如果一个编译单元里出现多次相同的声明没有问题,但如果是一个编译单元里对同一个对象(变量)或类型有多个定义,或者是同一个作用域里有重复定义,比如全局空间有重复定义,就会编译报错。

在C/C++中,一个类型只能定义一次,比如对相同的结构体类型定义只能出现一次,不能重复。

所以在包含头文件时,里面的内容都是一些类型定义,就不能重复包含。这也包括一些宏定义,也不能重复定义。

重复包含一个头文件,可能会导致编译错误,就算头文件中都是可以重复出现的声明之类的内容,多次包含的话,也会增加预处理器或编译器的工作量,增加软件构建的时间。

而一般情况下, 头文件中包含较多的内容都是forward declaration,前向声明,表示还没有给出完整的定义的标识符(表示编程的实体,如数据类型、变量、函数)。有了这个声明,编译的源文件就可以使用此标识符,而不需要关系其定义与否。在链接阶段,才会使用到具体的定义。

尤其在很大的项目中,头文件很多,其内容很长时,这会降低开发效率,增加开发成本。

为了解决重复包含头文件的问题,引入了header guard(或者叫include guard),来防止头文件的多次包含。

header guard使用条件编译指令,一种预处理指令。

在一个源文件中包含此头文件my_include.h时,会检查此宏定义是否定义,如果是第一次包含,没有定义,则下一步定义这个宏,并将后面的内容包含进来。

然后预处理器对此源文件继续操作,后面再出现包含此头文件时,因为这个宏已经定义,则后面内容不会包含进来。

注意,这里宏生效的范围仅在单个源文件内。

例如:

my_include.h

#ifndef SOME_UNIQUE_NAME_HERE

#define SOME_UNIQUE_NAME_HERE

// your declarations (and certain types of definitions) here

#endif

#ifndef HEADER_1_H

#define HEADER_1_H

typedef struct {

    …

} MyStruct;

int myFunction(MyStruct *value);

#endif

#ifndef是预处理器指令,如果后面的宏未定义,则后面代码会被处理,否则略过这些代码。

而下面紧跟着就是#define预处理指令,定义一个宏,作为header guard,这样当前的编译单元再包含这个编译单元时,就会因为此宏已定义,而略过后面的代码。

另外,在Visual Studio Code中可以自动添加Include Guard:

C/C++ Include Guard - Visual Studio Marketplace

这个添加的include guard是一个GUID形式的定义:

// GUID

#define E8A33412_A210_4F05_99A4_F6E2019B7137

不适用Include guard的头文件

有几个头文件没有使用include guard的习惯。一个具体的例子是标准<assert.h>头。它可能在一个翻译单元中被多次包含,而这样做的效果取决于每次包含头文件时是否定义了宏NDEBUG。你可能偶尔会有类似的要求;这种情况很少。通常情况下,你的头文件应该受到include guard习惯用法的保护。

A few headers do not use the include guard idiom. One specific example is the standard <assert.h> header. It may be included multiple times in a single translation unit, and the effect of doing so depends on whether the macro NDEBUG is defined each time the header is included. You may occasionally have an analogous requirement; such cases will be few and far between. Ordinarily, your headers should be protected by the include guard idiom.

使用Include guard的注意事项

1,当创建头文件时,生成一次。

2,创建后无需再考虑

3,要防止重名,要比你中彩票的几率还小。

* generate once, when creating a header

* never have to think about again

* chance of duplicating is less than your chance of winning the lottery

关于include guard使用的宏的名字

c++ - Adding an include guard breaks the build - Stack Overflow

c++ - Naming Include Guards - Stack Overflow

Include guard conventions in C - Stack Overflow

宏定义一般都用大写字母, Include guard一般也使用大写字母,并且起名时尽可能防止出现不同文件会有相同定义的情况。

一般的Include guard的起名方式,个别方法以config.h文件为例:

1, CONFIG_H , _CONFIG_H , CONFIG__H, CONFIG_H__ , __CONFIG_H__ , _CONFIG_H_

2, PROJECT_CONFIG_H

3,  _PATH_CONFIG_H_ 或 <PROJECT>_<PATH>_<FILE>_H ,

4,  CONFIG_20201212_103820   

(使用时间后缀, <name>_<date>_<time> 或 <FILE>_<CREATION DATE>_H)

5,INCLUDE_GUARD_726F6B522BAA40A0B7F73C380AD37E6B

(使用UUID创建)

6,C++的boost库:

<project>_<path_part1>_..._<path_partN>_<file>_<extension>_INCLUDED

// include /pet/project/file.hpp

#ifndef PET_PROJECT_FILE_HPP_INCLUDED

7,Google's style guide:

<PROJECT>_<PATH>_<FILE>_H_ 

(the full path in a project's source tree)

foo/src/bar/baz.h

#ifndef FOO_BAR_BAZ_H_

#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_

8,FILE>_<LARGE RANDOM NUMBER>_H

需要注意的是,下划线或双下划线开头的宏定义标识符,一般是保留给语言内部使用的,所以如果需要加下划线可以在结尾加。

总结:

建议的名字:  MOD_FULL_PATH_FILENAME_H_INCLUDED

MOD可以是模块名字或缩写,也可以是项目名。

另外,有时会在最后的#endif命令后加上注释,表示include guard的结束,最为一种良好的编程实践。

The C style comment after the #endif directive is not mandatory, but it is considered good style.

关于使用#pragma once

#pragma once 与 #ifndef 解析 - 瘦狐狸 - 博客园

What does #pragma once mean in C? - Stack Overflow

为了避免同一个文件被include多次,C/C++中有两种预处理指令使用方式,一种是#ifndef方式,一种是#pragma once方式。

方式一:

    #ifndef __SOMEFILE_H__

    #define __SOMEFILE_H__

    ... ... // 声明、定义语句

    #endif

    方式二:

    #pragma once

    ... ... // 声明、定义语句

#ifndef的方式受C/C++语言标准支持。它不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。

当然,缺点就是如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,编译器却硬说找不到声明的状况——这种情况有时非常让人抓狂。

由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。

#pragma once一般由编译器提供保证:同一个文件不会被包含多次。但它不属于C/C++语言标准,注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件,而且当文件的引用是远程地址或不同硬盘上时,不保证会生效。你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。

其好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。大型项目的编译速度也因此提高了一些。

对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。

#pragma once方式产生于#ifndef之后,因此很多人可能甚至没有听说过。目前看来#ifndef更受到推崇。因为#ifndef受C/C++语言标准的支持,不受编译器的任何限制;而#pragma once方式却不受一些较老版本的编译器支持,一些支持了的编译器又打算去掉它,所以它的兼容性可能不够好。一般而言,当程序员听到这样的话,都会选择#ifndef方式,为了努力使得自己的代码“存活”时间更久,通常宁愿降低一些编译性能。

在C和C++编程语言中,#pragma once是一个非标准但被广泛支持的预处理器指令,旨在使当前源文件在一次编译中只被包含一次。因此,#pragma once的作用与#include guards相同,但有几个优点,包括:代码少,避免名称冲突,提高编译速度。

In the C and C++ programming languages, #pragma once is a non-standard but widely supported preprocessor directive designed to cause the current source file to be included only once in a single compilation. Thus, #pragma once serves the same purpose as #include guards, but with several advantages, including: less code, avoiding name clashes, and improved compile speed.

关于使用双重包含开关的讨论

macros - The use of double include guards in C++ - Stack Overflow

比如在包含一个头文件时,同时使用一个和其内部一样的预处理宏,来防止多次包含:

#ifndef __HEADER_A_HPP__

#include "header_a.hpp"

#endif

添加另一个include guard是一个不好的做法这里有一些原因:

1,为了避免双重包含,只需在头文件本身中添加一个普通的包含防护。它能很好地完成这项工作。在包含的地方再加一个包含防护,只会把代码弄得一团糟,降低可读性。

2,它增加了不必要的依赖性。如果你在头文件中改变了include guard,你必须在所有包含头文件的地方改变它。

3,在整个编译/链接过程中,这绝对不是最昂贵的操作,所以它很难减少总的构建时间。

4,任何有价值的编译器都已经优化了文件范围内的包含保护。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜流冰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值