本文主要分析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,相互嵌套,导致头文件展开无限循环。
为了避免同一个头文件被包含(include)多次,C/C++中有两种宏实现方式:一种是#ifndef方式;另一种是#pragma once方式。
在能够支持这两种方式的编译器上,二者并没有太大的区别。但两者仍然有一些细微的区别
方式一:
#ifndef __SOMEFILE_H__ #define __SOMEFILE_H__ ... ... // 声明、定义语句 #endif
#ifndef的方式可以保证同一文件不会被包含多次,也能保证内容完全相同的两个文件不会被同时包含。需要注意不同文件中的__SOMEFILE_H__宏命名不能相同。
方式二:
#pragma once ... ... // 声明、定义语句
#pragma once由编译器保证同一个文件不会被包含多次,不能处理两个文件内容相同的情况,如果相同的文件在两个位置就会被处理两次。GCC3.4版本以前不支持#pragma once。
#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.设计层面避免循环引用
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.可以解决两个类相互循环调用的情况
良好的程序设计可以避免循环依赖,参考接口隔离原则和依赖倒置原则:
接口隔离原则:
客户端不应该依赖它不需要的接口
类间的依赖关系应该建立在最小的接口上
通俗来讲提供给每个模块单一的接口
依赖倒置原则:
上层模块不应该依赖于底层模块,它们都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
在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的数据。