本文主要分析C++头文件的相互引用,与类的相互依赖问题
1. C++头文件的相互引用
如果C++头文件相互引用,编译无法通过:
// A.cpp
#include "A.h"
int main()
{
return 0;
}
// A.h
#include "B.h"
// B.h
#include "A.h"
尝试编译,报错
from A.h:1,
from B.h:1,
from A.h:1,
from B.h:1,
from A.h:1,
...
...
from B.h:1,
from A.h:1,
from B.h:1,
from A.cpp:1:
B.h:1:15: error: #include nested too deeply
#include "A.h"
这是由于预处理阶段,A.h与B.h,相互嵌套,导致头文件展开无限循环。
使用#ifndef或#pragma once
为了避免同一个头文件被包含(include)多次,C/C++中有两种宏实现方式:一种是#ifndef方式;另一种是#pragma once方式。
在能够支持这两种方式的编译器上,二者并没有太大的区别。但两者仍然有一些细微的区别
方式一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // 声明、定义语句
#endif
#ifndef的方式可以保证同一文件不会被包含多次,也能保证内容完全相同的两个文件不会被同时包含。需要注意不同文件中的__SOMEFILE_H__宏命名不能相同。
方式二:
#pragma once
... ... // 声明、定义语句
#pragma once由编译器保证同一个文件不会被包含多次,不能处理两个文件内容相同的情况,如果相同的文件在两个位置就会被处理两次。GCC3.4版本以前不支持#pragma once。
2. 类的循环依赖
#ifndef和#pragma once解决了头文件循环引用的问题,但是如果存在类的相互依赖,编译会出现新的问题:
// A.cpp
#include "A.h"
int main()
{
return 0;
}
// A.h
#include "B.h"
class A
{
B b;
}
// B.h
#include "A.h"
class B
{
A a;
}
此时编译报错:
In file included from A.h:2:0,
from A.cpp:1:
B.h:5:5: error: ‘A’ does not name a type
A a;
^
错误原因:
在A.h:2,处理语句#include “B.h”,进行头文件展开
在B.h:5进行的是在class B中声明一个A类型的成员变量,而此时class A还没有被声明
因此编译报错:‘A’ does not name a type
解决循环依赖的问题有两种方式:
1.使用前向声明(forward declaration)
2.设计层面避免循环引用
a.使用前向声明
C++的类可以进行前向声明,此例子中在B.h文件中对class A进行前向声明,就可以编译通过,如下:
// #include "A.h"
class A;
class B
{
A* a;
};
由于前向声明而没有定义的类是不完整的,所以class A只能用于定义指针、引用、或者用于函数形参的指针和引用,不能用来定义对象,或访问类的成员。
这是因为需要确定class B空间占用的大小,而类型A还没有定义不能确定大小,但A是指针类型大小已知,因此Class B中可以使用A定义成员变量。
前向声明的作用:
1.不需要include头文件,大量引入的头文件会导致编译变慢
2.可以解决两个类相互循环调用的情况
b.重新设计程序结构,避免循环依赖
良好的程序设计可以避免循环依赖,参考接口隔离原则和依赖倒置原则:
接口隔离原则:
客户端不应该依赖它不需要的接口
类间的依赖关系应该建立在最小的接口上
通俗来讲提供给每个模块单一的接口
依赖倒置原则:
上层模块不应该依赖于底层模块,它们都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
在A依赖B,B依赖A的情况下,考虑为什么A类要内含B类的成员呢,那么可以抽象出一个接口,一个抽象类class IB,B是它的实现,class A和B都依赖于class IB,循环依赖便消除了。
编码示例:
// C++中,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类
class IB
{
public:
virtual int getVal() = 0;
}
class B : public IB
{
public:
A a;
int val = 2;
int getVal(){
return val;
}
};
class A
{
public:
IB *b;
int getValOfB()
{
return b->getVal();
}
};
int main()
{
IB *b = new B();
A a;
a.b = b;
cout << a.getValOfB() << endl;
return 0;
}
此时class A和class B依赖于IB,class B依赖于class A,循环依赖被打破,并且class A仍能以接口形式得到class B的数据。