天天看點

Effective C++ 34

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就行了。

句柄類和協定類分離了接口與實作,進而降低了檔案間的依賴性,當一定程度上有時間和空間上的消耗。對于一個程式轉變為産品時,要用具體的類來取代句柄類和協定類。

繼續閱讀