Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#include相关 #39

Open
UNIDY2002 opened this issue Mar 19, 2020 · 0 comments
Open

#include相关 #39

UNIDY2002 opened this issue Mar 19, 2020 · 0 comments

Comments

@UNIDY2002
Copy link

关于#include

#include其实并不是一个非常聪明的机制——直接全文复制,也不管包含了多少用不着的代码;你也不甚清楚你包含的代码中有什么牛鬼蛇神,会不会碰巧撞上了math.h中的y1;假如处理不当,还可能惹来重复定义等令人头秃的麻烦……

在此,我列举一些初次深入了解#include时可能遇到的困扰,并加以说明。


套娃

事情开始于这样的代码:

// A.h
#pragma once
#include "B.h"

class A {
    B b;
};
// B.h
#pragma once
#include "A.h"

class B {
    A* a;
};
// main.cpp
#include "A.h"

// Do your thing...

我们在类A中设置了B类型的成员变量,因此需要#include "B.h"然而,出于某种需求,我们还希望在类B中保留对应的A的指针,因此还需#include "A.h" 看起来顺理成章。

可是,当我们编译时,g++报了错:

B.h:6:5:error: 'A' does not name a type
     A* a;
     ^

是在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,那是最好的。

不过,如果这一需求不可避免呢?那也有办法:

// B.h
#pragma once

class A;  // 声明类A
class B {
    A* a;
};

我们在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置于文件的末尾,这样才能保证整个文件只被包含一次。

我之前见到过这样的写法:

#ifndef __HEADER__
#define __HEADER__

#include <iostream>
#include <algorithm>

#endif

class Test {
    // ...
};

这就违背保证整个文件只被包含一次的初衷了。假如这一头文件被包含多次,那也会造成Test的重复定义。

(当然,我个人以为出现这样的错误也与课件上只给了用法没给示例有关。)

#pragma once#ifndef...的区别

#pragma once可以简单快捷地保证物理上的这一文件只被包含一次,不过缺点在于一些编译器可能不支持。(当然,越来越多的编译器已经支持这一功能了。)

#ifndef XXX则是从代码层面保证单次包含,且类似写法可以在其它场合有一些灵活的运用。缺点在于你需要保证不同头文件的XXX不要撞车,否则也会导致预期之外的结果。(当然,许多IDE会为新建的.h文件自动加上#ifndef...等语句,可以省去不少麻烦。)


写在最后

读到这里,或许你对#include的机制更加不理解了还有一些困惑。也许,你很想亲自看到,编译器对这些带#的语句到底做了些什么。

这时,我们来了解一下g++的预编译指令。例如:

g++ -E main.cpp -o main.i

-E表示当前的任务是对main.cpp进行预编译。预编译的一个任务就是将这些带#的宏命令进行处理,比如#include的内容会在预编译时展开。这时,你就能看到那些头文件到底是谁先谁后了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant