Effective C++ 34

最后更新于:2022-04-01 15:49:54

34.将文件间的编译依赖性降到最低。 对于一个大型程序,其结构是错综复杂的,当你对一个类进行一些改动时,修改的不是接口,而是类的实现,即只是一些细节部分,但重新生成程序时,所有用到这个类的的文件都要重新编译。这里题目指的是这个意思。但实际上,我在vs2012实践了一下,对于类B与类A相关联,类B的实现依赖于类A,若类A的实现发生了改变,并不会影响B,即生成时,编译器只会去重新编译A,而对于依赖于A的用户程序,并不会像其所说那样全部重新编译。好吧,我这里总算是明白其所说的修改其实现的意思了。 修改类的实现: 类的接口只是类中提供给外部的函数, 而类的实现是指类实现接口函数所需要的内部逻辑和数据结构,如一些私有的函数,以及一些私有的成员数据。修改这些类实现的,对于实现函数的修改就必须修改函数的声明,而数据成员的修改就是数据成员的类型以及数量的修改。当进行这些修改时,就必定会导致调用这个类的用户程序都要重新编译。 一般的解决方法是将实现与接口分开,即对于本来使用的一个类,将其转换为两个类,一个类为接口类,供用户程序调用,一个类为实现类,有具体的实现逻辑以及实现所需的数据成员,且接口类能够指向对应的实现类,对于实现逻辑的更改不会影响用户程序,因为用户程序只与接口类连接,而隐藏了实现逻辑修改造成的影响,只有当接口改变时,才需要重新编译。分离的关键是,对类定义的依赖 被 对类声明的依赖取代,降低编译依赖性,将 提供类定义 即#include 指令 的任务由原来的函数声明头文件转交给包含函数调用的用户文件。 即不在头文件中包含其他头文件,除非缺少它们就不能编译,而一个一个地声明所需要的类,让使用这个头文件的用户自己通过include去包含这些头文件,以使用户代码通过编译。 实现这种接口与实现分离,在c++中一般有两种方法,一种是 将一个对象的实现隐藏在指针的背后,即用一个指针指向某个不确定的实现。这样的类称为句柄类或信封类。而指向的实现类称为 主体类或者信件类。句柄类,即接口只是将所有函数调用转移到对应的主体类中,有主题类也就是实现类来真正完成工作。接口中要将原来的实现需要的数据成员转换为函数,而去调用实现类中的数据成员 来实现功能,即接口中使用函数来实现对实现类中的数据成员实现传递和返回。 假如简单实现两个类,A ,B,C,A中放一个int,b中放一个doubel,C中放两者之和,写出代码如下: classA.h: ~~~ #pragma once class ClassA{ public: public: int a; ClassA(int x){a = x;} ClassA(){a = 0;} int getA() const{return a;}; }; ~~~ ClassB.h: ~~~ class ClassB{ public: double b; double getB() const{return b;} ClassB(double x){b = x;} ClassB(){b = 0;} }; ~~~ ClassC.h,即接口: ~~~ //这个头文件就是所谓的接口,如此将接口与实现分离后,只有修改接口时,才会导致使用该接口的用户程序重新编译 class ClassA;//只声明,在接口中只要知道有这些类,而在实现中才去include这些头文件 class ClassB; class ClassCImpl; class ClassC{ public: ClassC(const ClassA& xa,const ClassB& xb); virtual ~ClassC(); int getA() const;//函数来返回实现类中的数据成员 double getB() const; double getC() const; private: ClassCImpl *impl;//使用指针来指向实现类 //int aaa;//在接口中任意进行修改,就要重新编译其与其用户程序 }; ~~~ ClassC.cpp,接口的函数,调用 实现类中的函数进行返回。 ~~~ //这里也是对于接口的实现,修改这里的数据,不会导致其他程序的重新编译 #include "ClassC.h"//这是接口ClassC的函数具体实现 #include "ClassCImpl.h"//要包含实现类的定义,且实现类中与ClassC中有一样的成员函数 ClassC::ClassC(const ClassA& xa,const ClassB& xb){ impl = new ClassCImpl(xa,xb); } ClassC::~ClassC(){ delete impl; } int ClassC::getA() const{ return impl->getA(); } double ClassC::getB() const{ return impl->getB(); } double ClassC::getC() const{ return impl->getC(); } ~~~ ClassCImpl ,实现类的定义: ~~~ #include "ClassA.h" #include "ClassB.h" class ClassCImpl{ public: ClassCImpl(const ClassA& xa,const ClassB& xb); int getA() const;//函数实现接口中函数 double getB() const; double getC() const; private: ClassA A; ClassB B; ClassB C; }; ~~~ ClassCImpl.cpp,实现类的简单的操作: ~~~ #include "ClassCImpl.h"//要包含实现类的定义,且实现类中与ClassC中有一样的成员函数 ClassCImpl::ClassCImpl(const ClassA& xa,const ClassB& xb){ A = xa; B = xb; C = (B.getB() + A.getA()); } int ClassCImpl::getA() const{ return A.getA(); } double ClassCImpl::getB() const{ return B.getB(); } double ClassCImpl::getC() const{ return C.getB(); } ~~~ 这样就实现了接口与实现的分离,在ClassC中定义接口,在接口固定的情况下,在接口实现类ClassCImpl中进行任意的修改,编译器都只会重新编译实现类,而不会全部重新编译。这是使用句柄类实现的接口与实现分离。 另外一种方法成为协议类,即是这个类成为特殊类型的抽象基类。协议类只是为派生类确定接口,它没有数据成员,没有构造函数,有一个虚析构函数,有一些纯虚函数,这些纯虚函数组成了接口。 协议类的用户通过一个类似构造函数的的函数来创建新的对象,而这个构造函数所在的类就是隐藏在后的派生类。这种函数一般称为工厂函数,返回一个指针,指向支持协议类接口的派生类的动态分配对象。这个工厂函数与协议类解密相连,所以一般将它声明为协议类的静态成员。若重新声明一个ClassD,完成之前的功能,,但是为一个协议类,有: ClassD.h: ~~~ //这个为协议类 class ClassA;//只声明,在接口中只要知道有这些类,而在实现中才去include这些头文件 class ClassB; class ClassD{ public: virtual ~ClassD(){} virtual int getA() const = 0;//函数来返回实现类中的数据成员 virtual double getB() const = 0; virtual double getD() const = 0; static ClassD* makeClassD(const ClassA& xa,const ClassB& xb);//这里使用静态成员来返回 }; ~~~ 再写一个派生类来实现CLassD的功能,RealClassD.h: ~~~ #include "ClassA.h" #include "ClassB.h" #include "ClassD.h" class RealClassD:public ClassD{ public: RealClassD(const ClassA& xa,const ClassB& xb):A(xa),B(xb),D(B.getB() + A.getA()){} virtual ~RealClassD(){} int getA() const; double getB() const ; double getD() const; private: ClassA A; ClassB B; ClassB D; }; ~~~ 而在这个派生类定义中,顺带实现ClassD中的返回指针的makeClassD的函数。这里:先从协议类中继承接口规范,然后在实现中实现接口中的函数。 ~~~ #include "RealClassD.h" int RealClassD::getA() const{ return A.getA(); } double RealClassD::getB() const{ return B.getB(); }; double RealClassD::getD() const{ return D.getB(); } ClassD* ClassD::makeClassD(const ClassA& xa,const ClassB& xb){ return new RealClassD(xa,xb); } ~~~ 而在需要使用的地方,如此调用这个函数来指向需要的接口: ~~~ ClassD* dd = ClassD::makeClassD(a,b); cout<<dd->getD()<<endl; ~~~ dd的指针动态绑定到返回的派生类对象,而在派生类中修改其实现的成员只要重新编译派生类的cpp就行了。 句柄类和协议类分离了接口与实现,从而降低了文件间的依赖性,当一定程度上有时间和空间上的消耗。对于一个程序转变为产品时,要用具体的类来取代句柄类和协议类。
';