#include
其实并不是一个非常聪明的机制——直接全文复制,也不管包含了多少用不着的代码;你也不甚清楚你包含的代码中有什么牛鬼蛇神,会不会碰巧撞上了math.h
中的y1
;假如处理不当,还可能惹来重复定义等令人头秃的麻烦……
在此,我列举一些初次深入了解#include
时可能遇到的困扰,并加以说明。
套娃
事情开始于这样的代码:
1 | // A.h |
1 | // B.h |
1 | // main.cpp |
我们在类A
中设置了B
类型的成员变量,因此需要#include "B.h"
。然而,出于某种需求,我们还希望在类B
中保留对应的A
的指针,因此还需#include "A.h"
。看起来顺理成章。
可是,当我们编译时,g++报了错:
1 | B.h:6:5:error: 'A' does not name a type |
是在B.h
中报了找不到类型A
的错。
奇怪,我们明明在B.h
中包含了A.h
啊……
探因
我们将目光聚焦到A.h
上——原来,A.h
标上了#pragma once
。也就是说,假如A.h
之前已经被包含过了,那么这次就不会再包含它了。再一看main.cpp
,确实,A.h
早已被包含过了。
破案了!
好,我们将A.h
中的#pragma once
去掉总行了吧?还不行,这次又报找不到类型B
了。
那就把B.h
中的#pragma once
也去掉吧……停下来!不然那编译器的报错……太美……
不过,至此,这背后的原因已可见端倪——**套娃include
**。C++的include
最忌讳的就是套娃了。如果不加#pragma once
等处理,则头文件就会永无止境地包含下去;如果加了,那你写代码时可能以为自己include
过了,实际上却被编译器拦下了。
总之,这种循环包含的行为是不可取的,在实际编程中应当避免。
解决
那么,应当如何修改代码,才能既满足需求,又不出现套娃的现象呢?
在动手之前,先想想,是否真的需要在B
中保留A
的指针。因为,这种情况的发生,很有可能意味着你的代码设计时耦合度有些高,才会剪不断理还乱。如果能重新设计代码,让B
干脆不依赖A
,那是最好的。
不过,如果这一需求不可避免呢?那也有办法:
1 | // B.h |
我们在B.h
中不去#include "A.h"
,而是声明class A
,供B
使用,具体的细节则在A.h
中给出。这样,既免去了循环包含,又能够在类B
中用到类A
。
至此,“套娃”的问题暂告一段落。下面,再简单提一下#pragma once
和#ifndef...
的事。
重复定义
我们知道,在C++中,对同一个名称,声明可以多次,但定义只能一次。为此,我们需要引入一些保证单次包含的机制,来防止因多次包含同一头文件而造成的重复定义。
#pragma once
和#ifndef...
的用法,在课件上都有写到。这里,对使用过程中可能遇到的疑惑和误区简单说明一下。
#ifndef XXX
含义的理解
#ifndef XXX
和#endif
配套,可以理解为if not defined XXX
,则……,end if
。而在解析……所示的代码之前,需要先#define XXX
,从而下次解析到这一头文件时,因为宏定义过XXX
了,ifndef
条件不满足,就不再解析……部分的代码了,从而保证了单次包含。
#ifndef XXX
插入的位置
合理的使用方法,应当是#ifndef XXX
和#define XXX
置于文件的开头而#endif
置于文件的末尾,这样才能保证整个文件只被包含一次。
我之前见到过这样的写法:
1 |
|
这就违背保证整个文件只被包含一次的初衷了。假如这一头文件被包含多次,那也会造成Test
的重复定义。
(当然,我个人以为出现这样的错误也与课件上只给了用法没给示例有关。)
#pragma once
和#ifndef...
的区别
#pragma once
可以简单快捷地保证物理上的这一文件只被包含一次,不过缺点在于一些编译器可能不支持。(当然,越来越多的编译器已经支持这一功能了。)
#ifndef XXX
则是从代码层面保证单次包含,且类似写法可以在其它场合有一些灵活的运用。缺点在于你需要保证不同头文件的XXX
不要撞车,否则也会导致预期之外的结果。(当然,许多IDE会为新建的.h
文件自动加上#ifndef...
等语句,可以省去不少麻烦。)
写在最后
读到这里,或许你对#include
的机制更加不理解了还有一些困惑。也许,你很想亲自看到,编译器对这些带#的语句到底做了些什么。
这时,我们来了解一下g++的预编译指令。例如:
1 | g++ -E main.cpp -o main.i |
-E
表示当前的任务是对main.cpp
进行预编译。预编译的一个任务就是将这些带#的宏命令进行处理,比如#include
的内容会在预编译时展开。这时,你就能看到那些头文件到底是谁先谁后了。
本文链接:https://www.unidy.cn/articles/include/