将文件间的编译依存关系降至最低

xiaoxiao2021-02-28  79

Effective C++条款31:将文件间的编译依存关系降至最低

转自:http://www.cnblogs.com/jerry19880126/p/3551836.html

在说这一条款之前,先要了解一下C/C++的编译知识,假设有三个类ComplexClass, SimpleClass1和SimpleClass2,采用头文件将类的声明与类的实现分开,这样共对应于6个文件,分别是ComplexClass.h,ComplexClass.cpp,SimpleClass1.h,SimpleClass1.cpp,SimpleClass2.h,SimpleClass2.cpp。

ComplexClass复合两个BaseClass,SimpleClass1与SimpleClass2之间是独立的,ComplexClass的.h是这样写的:

#ifndef COMPLESS_CLASS_H #define COMPLESS_CLASS_H #include “SimpleClass1.h” #include “SimpleClass2.h” class ComplexClass { SimpleClass1 xx; SimpleClass2 xxx; }; … #endif /* COMPLESS _CLASS_H */

我们来考虑以下几种情况:

Case 1:

现在SimpleClass1.h发生了变化,比如添加了一个新的成员变量,那么没有疑问,SimpleClass1.cpp要重编,SimpleClass2因为与SimpleClass1是独立的,所以SimpleClass2是不需要重编的。

那么现在的问题是,ComplexClass需要重编吗?

答案是“是”,因为ComplexClass的头文件里面包含了SimpleClass1.h(使用了SimpleClass1作为成员对象的类),而且所有使用ComplexClass类的对象的文件,都需要重新编译!

如果把ComplexClass里面的#include “SimpleClass1.h”给去掉,当然就不会重编ComplexClass了,但问题是也不能通过编译了,因为ComplexClass里面声明了SimpleClass1的对象xx。那如果把#include “SimpleClass1.h”换成类的声明class SimpleClass1,会怎么样呢?能通过编译吗?

答案是“否”,因为编译器需要知道ComplexClass成员变量SimpleClass1对象的大小,而这些信息仅由class SimpleClass1是不够的,但如果SimpleClass1作为一个函数的形参,或者是函数返回值,用class SimpleClass1声明就够了。如:

// ComplexClass.h class SimpleClass1; … SimpleClass1 GetSimpleClass1() const; … 但如果换成指针呢?像这样:

// ComplexClass.h #include “SimpleClass2.h” class SimpleClass1; class ComplexClass: { SimpleClass1* xx; SimpleClass2 xxx; };

答案是“是”,因为编译器视所有指针为一个字长(在32位机器上是4字节),因此class SimpleClass1的声明是够用了。但如果要想使用SimpleClass1的方法,还是要包含SimpleClass1.h,但那是ComplexClass.cpp做的,因为ComplexClass.h只负责类变量和方法的声明。

那么还有一个问题,如果使用SimpleClass1*代替SimpleClass1后,SimpleClass1.h变了,ComplexClass需要重编吗?

先看Case2。

Case 2:

回到最初的假定上(成员变量不是指针),现在SimpleClass1.cpp发生了变化,比如改变了一个成员函数的实现逻辑(换了一种排序算法等),但SimpleClass1.h没有变,那么SimpleClass1一定会重编,SimpleClass2因为独立性不需要重编,那么现在的问题是,ComplexClass需要重编吗?

答案是“否”,因为编译器重编的条件是发现一个变量的类型或者大小跟之前的不一样了,但现在SimpleClass1的接口并没有任务变化,只是改变了实现的细节,所以编译器不会重编。

Case 3:

结合Case1和Case2,现在我们来看看下面的做法:

// ComplexClass.h #include “SimpleClass2.h” class SimpleClass1; class ComplexClass { SimpleClass1* xx; SimpleClass2 xxx; }; // ComplexClass.cpp void ComplexClass::Fun() { SimpleClass1->FunMethod(); }

请问上面的ComplexClass.cpp能通过编译吗?

答案是“否”,因为这里用到了SimpleClass1的具体的方法,所以需要包含SimpleClass1的头文件,但这个包含的行为已经从ComplexClass里面拿掉了(换成了class SimpleClass1),所以不能通过编译。

如果解决这个问题呢?其实很简单,只要在ComplexClass.cpp里面加上#include “SimpleClass1.h”就可以了。换言之,我们其实做的就是将ComplexClass.h的#include “SimpleClass1.h”移至了ComplexClass1.cpp里面,而在原位置放置class SimpleClass1。如下:

// ComplexClass.h #include “SimpleClass2.h” class SimpleClass1; class ComplexClass { SimpleClass1* xx; SimpleClass2 xxx; }; // ComplexClass.cpp #include "SimpleClass1.h" void ComplexClass::Fun() { SimpleClass1->FunMethod(); }

这样做是为了什么?假设这时候SimpleClass1.h发生了变化,会有怎样的结果呢?

SimpleClass1自身一定会重编,SimpleClass2当然还是不用重编的,ComplexClass.cpp因为包含了SimpleClass1.h,所以需要重编,但换来的好处就是所有用到ComplexClass的其他地方,它们所在的文件不用重编了因为ComplexClass的头文件没有变化,接口没有改变!

总结一下,对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。

因此,避免大量依赖性编译的解决方案就是:在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件

上述方法称为Handle classes, 降低文件间的编译依存关系还有一种方法,称为Interface classes,如下。

从上面也可以看出,避免重编的诀窍就是保持头文件(接口)不变化,而保持接口不变化的诀窍就是不在里面声明编译器需要知道大小的变量,Handler Classes的处理就是把变量换成变量的地址(指针),头文件只有class xxx的声明,而在cpp里面才包含xxx的头文件。Interface Classes则是利用继承关系和多态的特性,在父类里面只包含成员方法(成员函数),而没有成员变量,像这样:

// Person.h #include <string> using namespace std; class MyAddress; class MyDate; class RealPerson; class Person { public: virtual string GetName() const = 0; virtual string GetBirthday() const = 0; virtual string GetAddress() const = 0; virtual ~Person(){} }; // RealPerson.h #include "Person.h" #include "MyAddress.h" #include "MyDate.h" class RealPerson: public Person { private: string Name; MyAddress Address; MyDate Birthday; public: RealPerson(string name, const MyAddress& addr, const MyDate& date):Name(name), Address(addr), Birthday(date){} virtual string GetName() const; virtual string GetAddress() const; virtual string GetBirthday() const; };

在RealPerson.cpp里面去实现GetName()等方法。从这里我们可以看到,只有子类里面才有成员变量,也就是说,如果Address的头文件变化了,那么子类一定会重编,所有用到子类头文件的文件也要重编,所以为了防止重编,应该尽量少用子类的对象。利用多态特性,我们可以使用父类的指针,像这样Person* p = new RealPerson(xxx),然后p->GetName()实际上是调用了子类的GetName()方法。

但这样还有一个问题,就是new RealPerson()这句话一写,就需要RealPerson的构造函数,那么RealPerson的头文件就要暴露了,这样可不行。还是只能用Person的方法,所以我们在Person.h里面加上这个方法:

// Person.h #include <string> using namespace std; class MyAddress; class MyDate; class RealPerson; class Person { public: static Person* CreatePerson(const string &name, const MyAddress& addr, const MyDate& date); virtual string GetName() const = 0; virtual string GetBirthday() const = 0; virtual string GetAddress() const = 0; virtual ~Person(){} }; 注意这个方法是静态的(没有虚特性),它被父类和所有子类共有,可以在子类中去实现它: // RealPerson.cpp #include “Person.h” Person* Person::CreatePerson(string name, const MyAddress& addr, const MyDate& date) { return new RealPerson(name, addr, date); } 这样在客户端代码里面,可以这样写: // Main.h class MyAddress; class MyDate; void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date); // Main.cpp #include "Person.h" #include “MyAddress.h”; #include “MyDate.h”; void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date) { Person* p = Person::CreatePerson(name, addr, date); … }

就可以减少编译依赖了。

总结一下,Handler classes与Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。减少编译依存性的关键在于保持.h文件不变化,具体地说,是保持被大量使用的类的.h文件不变化,这里谈到了两个方法:Handler classes与Interface classes。

Handler classes化类的成员变量为指针,在.h文件里面只包含class xxx的外来类声明,而不包含其头文件,在.cpp涉及到具体外来类的使用时,才包含xxx.h的头文件,这样最多只影响本身类的cpp重编,但因为.h文件没有变化,所以此类的对象存在的文件不必重编。

当然,书上说的Handler classes更想让我们在类A的基础上另造一个中间类AImp(成员函数完全与类A一致),这个中间类的成员中里面放置了所有类A需要的外来类的对象,然后类的逻辑细节完全在Almp.cpp中实现,而在A.cpp里面只是去调用Almp.cpp的同名方法。A.h的成员变量只有Almp的指针,这看上去好像一个Handler,因此而得名。

Interface classes则是将细节放在子类中,父类只是包含虚方法和一个静态的Create函数声明,子类将虚方法实现,并实现Create接口。利用多态特性,在客户端只需要使用到Person的引用或者指针,就可以访问到子类的方法。由于父类的头文件里面不包含任何成员变量,所以不会导致重编(其实由于父类是虚基类,不能构造其对象,所以也不用担心由于父类头文件变化导致的重编问题)。

请记住:

1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式,基于此构想的两个手段是Handler classes和Interface classes。

2. 程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。

转载请注明原文地址: https://www.6miu.com/read-27920.html

最新回复(0)