effective C++(四)

聲明一個純虛函數的目的是爲了讓派生類只繼承函數接口,但是基類聲明瞭純虛函數也可以給它提供一份實現代碼,c++並不會發生怨言,但調用他的唯一途徑是調用時明確指出其類的名稱。

聲明簡樸的非純虛函數的目的,是讓派生類繼承該函數的接口和缺省實現,爲了避免錯誤的使用缺省實現,可以這樣實現

class Airplane{
 public:
 virtual void fly(const Airport& destination)=0;
 protected:
 void defaultFly(const Airport& destination);
};
void AirpLane::defaultFly(const Airport& destination)
{
 缺省行爲
}
class ModelA:public Airplane{
 public:
 virtual void fly(const Airport& destination)
{ defaultFly(destination);}...
};
class ModelC:public Airplane{
 public:
 virtual void fly(const Airport& destination);
 ..
};
void ModelC::fly(const Airplane& destination)
{
 自己的行爲
}

如果成員函數是個非虛函數,意味着它並不打算在派生類中有不同行爲

替換虛函數的三種方法:一令客戶通過public non-virtual成員函數間接調用私有虛函數,被稱爲NVI手法,它的優點在於可以在執行主要代碼前後做一些事前工作和事後工作,事實上此手法下的虛函數不一定爲私有,要視情況而定

class GameCharacter{
 public:
 int healthValue() const
 {
   ...                   //做一些事前準備工作
   int reVal=doHealthValue();
   ...                   //做一些事後準備工作
   return retVal;
 }
 ...
 private:
  virtual int doHealthValue() const
  {
    ...
   }
};

另外一種手法是稱爲strategy分解表現形式,是通過運用函數指針替換虛函數,優點是每個對象可各自擁有自己的計算函數,和可在運行期改變計算函數的優點,不過缺點爲可能必須降低類的封裝性。

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);   //計算函數的缺省算法
class GameCharacter{
 public:
 typedef int(*HealthCalcFunc)(const GameCharacter&);
 explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
  :healthFunc(hcf) {}
 int healthValue() const
 { return healthFunc(*this);}
 ...
 private:
 HealthCalcFunc healthFunc;
};

最後一種方法是使用函數對象,下面的簽名代表的函數是接受一個reference指向const GameCharacter,並返回int。這個trl::function類型,產生的對象可以持有保存任何與此簽名式兼容的可調用物,所謂兼容,意思是這個可調用物的參數可被隱式轉換爲const GameCharacter&,而其返回類型可被隱式轉換爲Int.

...如上
typedef std::trl::function<int (const GameCharacter&)>HealthCalcFunc;
...同上

這使得一下函數都可被調用

short calcHealth(const GameCharacter&);
struct HealthCalculator{
 int operator()(const GameCharacter&)const
 { ... }
};
class GameLevel{
 public:
 float health(const GameCharacter&) const;
 ...
};
class EvilBadGuy::public GameCharacter{
 ...
};
EvilBadGuy ebg1(calcHealth);
EvilBadGuy ebg2(HealthCalculator());
GameLevel currentLevel;
EvilBadGuy ebg3(std::trl::bind(&GameLevel::health,currentLevel,_1)
);

GameLevel::health宣稱自己接受一個參數,但它實際上接受兩個參數,因爲它也獲得一個隱式參數GameLevel,也就是那個this所指,如果我們使用GameLevel::health作爲計算函數,我們必須以某種方式轉換它,使它不再接受兩個參數,用bind,它指出ebg3的計算函數應該總是以currentLevel作爲GameLevel對象。

任何情況下都不該重新定義一個繼承而來的非虛函數,否則會發生如下問題:

class B{
 public:
 void mf()
};
class D:public B
{
 public:
 void mf();       //遮掩了B::mf;
};
D x;
B* PB=&x;
D* PD=&x;
PB->mf();         //調用B::mf
PD->mf();         //調用D::mf

原因在於,非虛函數時靜態綁定的,由於PB被聲明爲一個pointer-to b,通過PB調用的非虛函數永遠是B所定義的版本,即使PB所指爲B派生的對象。如如果mf爲虛函數,則不論PD還是PB所指都爲D::mf因爲PB和PD真正指的都是一個D類型的對象。

對象所謂的靜態類型,就是它在程序中被聲明時所採用的類型。動態類型則是指目前所指對象的類型,也就是說,動態類型可以表現出一個對象將會有聲明行爲。虛函數時動態綁定而來的,意思是調用一個虛函數,究竟調用哪一份代碼,取決於動態類型

class Shape{
 public:
 virtual void draw(ShapeColor color=Red) const=0;};
 class Rectangle:public Shape{
 public:
 virtual void draw(ShapeColor color=Green)const;//賦予不同的參數值,真糟糕!
 ...
};
class Circle:public Shape{
 public:
 virtual void draw(ShapeColor color)const;//這樣寫當調用此函數要指定參數值,因爲靜態綁定下這個函數並不從其基類繼承缺省參數值,
 ...                                         當若以指針或引用調用此函數,可以不指定參數值,因爲動態綁定下這個函數會從其基類繼承缺省參數值
};
Shape* pc=nnew Circle;            //靜態類型都爲Shape
Shape* pr=new Rectangle;
pc->draw(Shape::Red);       //調用Circle::draw(Shape::Red)
pr->drwa();                //本來Rectangle::draw的缺省參數爲GREEN,但由於pr的靜態類型是Shape*,所以調用的缺省參數爲Shape的

原因在於draw是個虛函數,而它有個缺省參數值在派生類中被重新定義了,我們也絕對不要重新定義一個繼承而來的缺省參數,因爲缺省參數值都是靜態綁定,而虛函數卻是動態綁定的,爲了避免重複代碼,可使用NVI手法來處理

class Shape{
 public:
 void draw(ShapeColor color=Red) const
{
  doDraw(color);
}
 ...
private:
 virtual void doDraw(ShapeColor color)const=0;
};
class Rectangle:public Shape{
 private:
 virtual void doDraw(ShapeColor color)const;        //不需指定缺省參數了,巧妙的避開了
 ..
};

程序中對象其實相當於你所塑造的世界中的某些事物,例如人,汽車等等,這樣的對象屬於應用域,其他對象則純粹是實現細節上的人工製品,像緩衝區,互斥器,等等這些對象屬於你的軟件的實現域,當複合(類中含有類)發生於應用域內對象之間,變現出has-a的關係,當它發生於實現域則是表現出is-implemented-in-terms-of的關係(根據某物實現之),當兩個類之間並非is-a的關係,但它們又需要彼此的相互功能時,可以使用複合。

類之間的繼承關係是private,編譯器不會自動將一個派生類對象轉換爲一個基類對象,私有繼承純粹只是一種實現技術,如果D以私有繼承形式繼承B,意思是D對象根據B對象實現而得,再沒有其他意義了。

儘可能使用複合,必要時才使用私有繼承,大概有兩種情況,第一種是當類之間不存在is-a關係,但是其中一個需要訪問另一個的保護成員,或需要重新定義其一或多個虛函數,第二種是你所處理的類不帶任何數據,你需要對它造成最優化的情況。

看一個複合代替私有繼承的情況

class Widget:private Timer{
 private:
 virtual void onTick() const;        //查看widget的數據
..
};
class Widget{
 private:
 class WigdetTimer:public Timer{
 public:
 virtual void onTick() const;
 ...
 };
 WidgetTimer timer;
 ..
};

優點,WidgetTimer是Widget內部的一個私有成員並且繼承Timer,Widget的派生類將無法取用WidgetTimer,因此無法繼承它或重新定義它的虛函數,這是C++的阻止派生類重新定義虛函數的能力表現方法!

對於第二種情況

class Empty{}    //沒有數據,其對象應該不適用任何內存
class HoldsAnInt{
 private:
 int x;
 Empty e;
};

我們會發現HoldAnInt的大小大於一個整型,在多數編譯器中sizeof(Empty)=1,因爲面對大小爲零之獨立對象,通常會給它一個char到空對象內,但是如果我們用私有繼承,就可以確保它。

class HoldsAnInt:private Empty{
 private:
 int x;
};

幾乎可以確定HoldAnInt的大小等於一個整型,這是所謂的EBO,空白基類最優化。

使用多重繼承的時候一定要避免造成命名的歧義性

class BorrowableItem{
 public:
 void checkOut();
 ..
};
class ElectronicGadget{
 private:
 bool checkOut() const;
 ...
};
class Mp3Player:public BorrowableItem,public ElectronicGadget
{...};
Mp3Player mp;
mp.checkOut();//造成歧義,到底該調用哪個checkOut函數     可以如此:mp.BorrowableItem::checkOut()

當這種情況需要使用虛繼承

class File{...};
class InputFile:virtual public File{...};
class OutputFile:virtual public File{...};
class IOFile:public InputFile,public OutputFile{...};

從正確行爲來看,Public繼承總是應該是virutal,但你得爲virtual繼承付出代價,支配虛基類初始化的規則比起非虛基類的情況遠爲複雜且不直觀,虛基類的初始化責任是由繼承體系中的最底層類負責的,這暗示類若派生自虛基類而需要初始化,必須認知其虛基類,不論那些基類距離多遠,當一個新的派生類加入體系中,它必須承擔其虛基類不論直接或間接,的初始化責任。因此,非必要不要使用虛繼承,其次必須使用時,儘可能避免在其中放置數據。

多種繼承的確有正當用途,其中一個情節涉及public繼承某個interface class和private繼承某個協助實現的類的兩相組合

顯式接口,在源碼中明確可見的,通常由函數的簽名式也就是函數名稱,參數類型和返回類型構成。

隱式接口由有效表達式組成

template<typename T>
void doProcessing(T& w)
{
 if(w.size()>10&& w!=someNastyWidget){
 T temp(w);
 temp.normalize();
 temp.swap(w);
 }
}

從上可以看出w的類型T好像必須支持size,normalize,swap,copy構造函數,不等比較,這些便是T必須支持的一組隱式接口,凡是涉及w的任何函數調用,有可能造成模板具現化,使這些調用得以成功,這樣的具現行爲發生在編譯器,以不同的template參數具現化function templates會導致調用不同的函數,這便是所謂的編譯器多態。

隱式接口僅僅是有一組有效表達式構成,表達式自身可能看起來很複雜,但它們要求的約束條件一般而言相當直接又明確,加諸於template參數身上的隱式接口就像加諸於class對象身上的顯式接口一樣真實,而且兩者都在編譯器完成檢查,你無法再template中使用不支持template所要求之隱式接口的對象

template內出現的名稱如果相依於某個template參數,稱之爲從屬名稱,如果從屬名稱在類內呈嵌套狀,我們稱它爲嵌套從屬名稱,像基本內置類型並不依賴於任何template參數的名稱,這樣的名稱是非從屬名稱。

C++有個規則,如果解析器在template中遭遇一個嵌套從屬名稱,它便假設這名稱不是一個類型,除非你告訴它是,所以在缺省情況下,嵌套從屬名稱不是類型。typename只被用來驗明嵌套從屬類型名稱,其他的不能添加

template<typename C>
void print2nd(const C& container)
{
 C::const_iterator* x;        //編譯器會以爲是相乘!
 ...
}
void print2nd(const C& container)
{
 if(container.size()>=2){
   C::const_iterator iter(container,begin());       //無法通過編譯,iter不是一個類型
 ...
void print2nd(const C& container)
{
 if(container.size()>2){
  typename C::const_iterator iter(container.begin());      //正確寫法
 ..
}
}

typename作爲嵌套從屬類型名稱的前綴詞的例外是不可以出現在基類列表內的嵌套從屬類型名稱之前,也不可以在成員初值列中作爲基類的修飾符。如:

template<typename T>
class Derived:public Base<T>::Nested{      //base class list中不允許出現typename
 public:
 explicit Derived(int x)
 :Base<T>::Nested(x)                    //初值列中不允許出現
  {
    typename Base<T>::Nested temp;
     ...
   }
   ...
};

聲明template參數時,前綴關鍵字class 和typename是一樣效果的,typedef 和typename可以一起使用

處理模板化基類內的名稱

template<typename Company>
class MsgSender{
 public:
 ...
 void sendClear(const MsgInfo& info)
 {
   ...
 }
};
template<typename Company>
class LoggingMsgSender:public MsgSender<company>{
 public:
 ...
 void sendClearMsg(const MsgInfo& info)
 {
  sendClear(info);               //調用基類函數,無法通過編譯
 }
 ...
};

當編譯器遭遇class template loggingMsgSender定義式時,並不知道它繼承什麼樣的class,當然它繼承的是MsgSender<company>,但其中的company是個template參數,不到後來當loggingMsgSender被具現化,無法確切知道它是什麼。而不知道company是什麼,就無法知道 它是否有個sendclear函數。

更確切的是,假設有個函數不需要這個函數,則需要對其有一個特化版的MsgSender template,這是所謂的模板全特化

template<>
class MsgSender<CompanyZ>
{
 public:
 ...
 void sendSecret(const MsgInfo& info)
 {...}
};

這就是爲什麼C++拒絕這個調用的原因,它知道基類模板有可能被特化,而那個特化版本可能不提供和一般性template相同的接口,因此它往往拒絕在模板化基類內尋找繼承而來的名稱。解決方法有三種

template<typenname Company>
class LoggingMsgSender:public MsgSender<Company>
{
 public:
 1.using MsgSender<Company>::sendClear;
 ...  
 void sendClearMsg(const MsgInfo& info)
 {
   2.this->sendClear(info);
   3.MsgSender<Company>::sendClear(info);  //最不滿意的一個揭發,如果調用的是虛函數,會關閉virtual綁定行爲
 }
...
};
將於參數無關的代碼抽離template,template生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關係,因非類型模板參數而造成的膨脹,往往可以消除,做法是以函數參數或class成員變量替換參數
發佈了47 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章