C/C++ 防止头文件被重复引入
C++ 防止头文件被重复引入
在 C/C++ 多文件编程中,可能会出现头文件被重复包含的情况。
例如在编译如下的程序时:
1 | |
1 | |
1 | |
编译器会提示 “Book 类型重定义” 的错误,而错误的原因也显而易见,编译器在预处理头文件时可以简单理解为将头文件直接打开并放在文件最开始,即在编译器眼中 main.cpp 的代码将会变成如下模样:
1 | |
可以看到同一个 Book 类型被重复定义了两次,而在 C++ 中这是不允许的。
当然我们也可以选择在 main.cpp 中去掉 #include"book.h",这样也可以避免重复引入 Book 类,但实际上此方法并不适用于所有“重复引入”的场景。那我们怎样才能避免这个问题呢?对于该问题,有如下三种解决方法:
使用宏定义避免重复引入
实际多文件开发中,为避免重复声明,通常会利用宏加入 include 防范 (include guard) :
1 | |
对于如上代码,当程序中第一次引入该头文件时,由于 _NAME_H 尚未定义,所以会定义 _NAME_H 宏并保留 #ifndef 所包含的代码;而之后若想再次引入时,由于之前已经定义了 _NAME_H 宏,所以预处理时 #ifndef 所包含的代码就不会保留。
其中设置的宏名必须是独一无二的,不能与项目中其他宏的名称相同,否则会导致部分代码在预处理时被舍弃。
以刚才的代码为例,我们可以对 book.h 文件做如下修改:
1 | |
再次编译执行该程序就会发现不再报错,成功执行。
使用 #pragma once 避免重复引入
除前文所述的使用宏定义的方式之外,还可使用 #pragma one 预编译指令来避免该问题,#pragma once 是 C/C++ 中的一个非标准但广泛支持的预处理指令,它与 include guards 有相同的作用, 用于使当前文件在单次编译中只被包含一次。将其附加到指定文件的最开头位置,该文件就只会被包含一次。
仍旧以文章开头代码为例,#pragma once 使用方法如下:
1 | |
那么 #pragma once 和 #ifndef 有什么区别呢?
#ifndef 方法
使用 #ifndef 的方法是受 C/C++ 语言标准支持的,不受任何编译器的限制,因此移植性更好。该方法不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。但需要特别注意的是宏名不可重复。
但由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,#ifndef 会使得编译时间相对较长,因此一些编译器逐渐开始支持 #pragma once 的方式。
#pragma once 方法
#pragma once 并不是C++的原生语法,而是编译器的一种支持,所以并不是所有的编译器都能够支持,一些较老版本的编译器就并不支持该指令,兼容性不是非常好。
目前,几乎所有常见的编译器都支持 #pragma once 指令,甚至于 Visual Studio 2017 新建头文件时就会自带该指令。可以这么说,在 C/C++ 中,#pragma once 是一个非标准但却逐渐被很多编译器支持的指令。
该指令可以使同一个文件不会被包含多次。但需要注意这里所说的 “同一个文件” 是指物理上的一个文件,而不是指内容相同的两个文件,因此如果某个头文件有多份拷贝,该方法不能保证它们不被重复包含。
此外该方法只能针对整个文件,而无法对文件中的某一段代码作 #pragma once 声明。
相对于 #ifndef 方法,该方法可以避免宏名冲突问题,且由于不涉及宏定义,当编译器遇到它时就会立刻知道当前文件只会被包含一次,因此效率很高;但该方法不支持跨平台!
另外,这种方式不支持跨平台!
使用 _Pragma 操作符
C99 标准中新增加了一个和 #pragma 指令类似的 _Pragma 操作符,其可以看做是 #pragma 的增强版,不仅可以实现 #pragma 所有的功能,更重要的是,_Pragma 还能和宏搭配使用。
_pragma 操作符与 sizeof 等操作符类似,将字符串字面量作为参数写在括号内即可,具体格式如下:
1 | |
因此若要实现与上例中 #pragma once 类似的效果,以文章开头代码为例,只需使用如下代码即可:
1 | |
而相比预处理指令 #pragma,由于 _pragma 是一个操作符,因此可以用在一些宏中,且在宏定义中是以内联方式使用的。
此处以设置编译器优化等级为例,如果使用#pragma,则需要这样写:
1 | |
由于每次都需要重复写 #pragma OPT_LEVEL n (可能更长) 相对来说比较麻烦,违反编程的 DRY 原则,因此我们可能会想到利用宏定义来简化书写,例如我们可以定义如下的宏:
1 | |
之后我们就只需要写 OPT_L(n) 即可,然而这在 C90 中并不能实现,字符 “#” 在预处理指令中有特殊的用途,编译器会将指令中的数字符号(“#”)解释为字符串化运算符(#),例如我们定义一个宏 #define C90(x) #x ,那么 C90(test) 的替换结果为 "test"。也就是,通过 #define 来定义一个关于#pragma 的宏是不可行的。
而新的关键字 _pragma 就很好的解决了这个问题,由于 _pragma并没有字符 “#” ,因此可以直接定义宏:
1 | |
总结
对于本文中提到的 3 种避免头文件被重复包含的方法,其中 #pragma once 和 _Pragma(“once”) 可算作是同一类,其特点是编译效率高,但可移植性差 (编译器不支持,会发出警告,但不会中断程序的执行);而 #ifndef 的特点是可移植性高,编译效率差。实际使用中我们可根据实际情况,挑选最符合实际需要的解决方案。
事实上,无论是 C 语言还是 C++,为防止用户重复引入系统库文件,几乎所有库文件中都采用了以上 3 种结构中的一种,这也是为什么重复引入系统库文件编译器也不会报错的原因。
另外在某些场景中,考虑到编译效率和可移植性,#pragma once 和 #ifndef 经常被结合使用来避免头文件被重复引入,例如:
1 | |
当编译器可以识别 #pragma once 时,则整个文件仅被编译一次;反之,即便编译器不识别 #pragma once 指令,此时仍有 #ifndef 在发挥作用。