《C++ Primer》讀書筆記——第十五章_面向對象程序設計

OOP基於三個基本蓋面:數據抽象、繼承和動態綁定。 
以下的例子:基類Quote->派生類Bulk_quote 

對於某些類,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數聲明稱虛函數(virtual function)。


class Quote
{
public:
    std::string isbn() const;
    virtual double net_price(std::size_t) const;
}
派生類必須通過派生類列表來明確指出它是從哪個(哪些基類)繼承而來的。 
每個基類前面可以有訪問說明符public或private或protected

class Bulk_quote : public Quote  //Bulk_Quote公有繼承了Quote
{
public:
    double net_price(std::size_t) const override;    
}

因爲Bulk_quote公有繼承Quote,我們完全可以把Bulk_quote的對象當成Quote的對象來用。 

如果派生類沒有覆蓋它繼承的虛函數,那麼派生類會直接繼承其在基類中的版本。

派生類覆蓋他繼承的虛函數時,前面的virtual可有可無。
net_price後的override顯式地註明這個函數將改寫基類的虛函數。(可能派生類有多個同名函數,但只有聲明override的那個是虛函數??

override只是爲了顯式的註明將,增加可讀性。還可以防止一些微小難以發現的錯誤:http://blog.csdn.net/liyuanbhu/article/details/43816371

動態綁定 
通過使用動態綁定,我們能用同一段代碼分別處理Quote 和 Bulk_quote的對象。

void f(Quote &item){ }

因爲item的形參是基類Quote的一個引用(引用或者指針才能實現多態/動態綁定),所以我們既能用基類的對象調用該函數,也能使用派生類的對象調用該函數。
Quote base;
Bulk_quote derived;
f(base); //調用base的f
f(derived); //調用derived的f

在運行時選擇函數的版本成爲動態綁定(dynamic binding),也成運行時綁定(run-time binding)。

用基類的引用或者指針調用一個虛函數時將發生動態綁定。

15.2 定義基類和派生類

class Quote
{
public:
    Quote() = default;
    Quote(const std::string &book, double sales_price) :
        bookNo(book), price(sales_price) { }
    std::string isbn() const {return bookNo;}
    //返回定數量的書籍的銷售總額
    //派生類負責改寫並使用不同的折扣計算算法
    virtual double net_price(std::size_t n) const
        { return n*price; }
    virtual ~Quote() = default;  //對析構函數進行動態綁定
private:
    std::string bookNo;
protected:
    double price = 0.0;
};
基類通常都應該定義一個虛析構函數,即使函數不執行任何操作也是如此(用於給派生類銷燬派生類獨有的那部分內存,否則只能銷燬派生類中從基類繼承而來的那部分內存)。

成員函數與繼承

當遇到如net_price這樣的和類型有關的操作時,派生類必須對其重新定義。換句話說,派生類需要對這些操作提供自己的新定義以覆蓋(override)從基類繼承而來的舊定義。

指針和引用才能動態綁定。

關鍵字virtual只能出現在類內的聲明語句之前而不能用於類外部的函數定義。(static也不能)

如果基類把一個函數聲明爲virtual,則該函數在派生類中也是隱式virtual的。也可以顯式在聲明一次virtual。

虛函數的解析過程發生在運行時,非虛函數解析過程發生在編譯時。

構造函數和static成員函數不能是virtual的。因爲:

(1) static 函數和實例無關,只和類有關,可以把static成員看成某個namespace裏的函數。

(2) virtual函數要用到虛函數表(vtable),而vtable是在構造函數中建立的。所以調用構造函數的時候還沒有vtable,不能把構造函數設爲virtual。


訪問控制與繼承:

protected成員: 能被派生類訪問,不能被其他用戶訪問。


15.2.2

 定義派生類 

class Bulk_quote : public QUote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, std::size_t, double);
    //覆蓋基類的函數版本以實現基類大量購買的折扣政策。
    double net_price(std::size_t) const override;
private:
    std::size_t min_qty = 0; //適用折扣政策的最低購買量
    double discount = 0.0;  //以小數表示的折扣額
};
如果派生是公有的,那麼基類的公有成員也是派生類接口的組成部分。
如果派生類是struct,那麼默認從基類到派生類是public繼承。

如果派生類是class,那麼默認從基類到派生類是private繼承。

如果一個派生類只繼承自一個基類,那麼稱爲單繼承。

new返回的是指針

Foo f = new Foo(); //錯誤,類型不匹配

Foo *f = new Foo(); //正確


派生類不一定會覆蓋他繼承的虛函數,如果派生類沒有覆蓋其基類的某個虛函數,那麼這個函數在派生類的表現和其他普通成員函數沒有區別(功能和在基類中一樣)。


c++11允許顯式表示函數使用某個成員參數覆蓋了它繼承的虛函數,方法是在函數體前加override(其實不加也可以覆蓋,override只是顯式表示)。


c++標準並沒有明確規定派生類的對象在內存中如何分佈,但是我們可以認爲如下


繼承自基類的部分和派生類自定義的部分不一定是連續存儲的,上圖只是概念模型,而非物理模型。


Quote item; //基類對象

Bulk_quote bulk; //派生類對象

Quote *p  = &item;  //p指向Quote對象

p = &bulk;          //  p 綁定到bulk的Quote部分

Quote &r = bulk;  // r 綁定到bulk的Quote部分

這種轉換通常稱爲派生類到基類的轉換。

這種隱式特性意味着我們可以把派生類對象或者派生類對象的引用用在需要基類引用的地方,同樣的,我們也可以把派生類對象的指針用在需要基類指針的地方。(但是隻有引用和指針才能多態)。

在派生類對象中含有與其基類對應的組成部分,這一事實是繼承的關鍵所在。

派生類並不能直接構造初始化基類的成員,而是要調用基類的構造函數來初始化它的基類部分。(不算是委託構造函數,委託構造函數是同類之間的

struct Base
{
    int x;
    Base(int t):x(t){}
};

struct Derived : public Base
{
    int y;
    Derived():Base(10), y(20){}
};
如果派生類構造函數不寫Base(10)的話,會報錯,因爲Derived默認構造函數會調用Base的默認構造函數,而Base的默認構造函數沒了(被Base(int t) 隱藏了)。


首先初始化基類的部分,然後按照聲明的順序依次初始化派生類的成員。

派生類可以訪問基類的公有成員和受保護成員。

要想與類的對象交互必須使用類的接口,即這個對象是派生類的基類部分也是如此。所以派生類不能直接初始化基類的成員。而是用調用基類的接口(構造函數)初始化基類。


繼承和靜態成員:

如果基類中定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義(但是基類能不能訪問就是另一回事了,private)。不論從基類中派生出多少個派生類,對於每個靜態成員來說都只存在唯一的實例。

靜態成員也有public,private,protected之分。


class Base
{
public:
    static void statmem();
};
class Derived : public Base
{
    void f(const Derived&);
};

如果基類成員是private的,則派生類無權訪問。假設某靜態成員是可訪問的,我們可以通過基類使用它,也可以通過派生類使用它。

class A
{
protected:
    static int x;
public:
    A(){cout << x << endl;}
};
int A::x = 1 ;                         //爲什麼這一行不報錯???????????
class B : public A
{
  public:  B(){cout << x <<endl;}
};
protected的static成員要在類外聲明纔可用。private的在類外聲明不了(相當於只能本類在用,繼承也不行?)。


void Derived::f(const Derived &derived_obj)
{
    Base::statmem(); //正確
    Derived::statmem(); //正確
    derived_obj.statmem(); //通過Derived訪問
    statmem();  //通過this對象訪問
}

派生類的聲明中包含類名,但是不包含它的派生列表

class Bulk_quote : public Quote;  //錯誤:派生列表不能出現在這裏

class Bulk_quote;   //正確;聲明派生類的正確方式。

一條生命語句的目的是令程序知曉某個名字的存在以及該名字表示一個什麼樣的實體,如一個類、一個函數或一個變量等。派生列表以及與定義有關的其他細節必須與類的主題一起出現。


如果派生類中有和基類名字相同的成員,派生類的成員會把基類的隱藏。

class Base
{
public:
    int x;
    Base(int t):x(t){}
};
class Derived : public Base
{
public:
    int x;
    Derived(int t):Base(t){}
};
int main()
{
    Derived d(20);
    cout << d.x << endl;
    return 0;
}


會輸出一個垃圾值,因爲Derived(int t) : Base(t) {} 中初始化的是Base中的x,而不是Derived中的x。要想初始化Derived中的x就要Derived(int t) : x (t)  { } 



如果我們想將某個類用作基類,則該類必須已經定義而非僅僅聲明。(類定義之後才能被繼承)因爲派生類必須要知道基類成員是什麼。還隱含的意思就是:一個類不能派生它本身。

class Base;
class Derived : public Base    //錯誤
{
};


一個類是派生類,同時他也可以是其他類的基類(派生鏈)。(基類和派生類的關係是相對的)

每個類都會繼承直接積累的所有成員,所以繼承鏈的頂端的那個派生類,將包含了它的直接基類的子對象以及每個間接基類的子對象。


在一個類定義中加入final可防止類被繼承:在類名後接final 

c++ 11 之前的防止繼承的方法   http://www.cnblogs.com/kingstarspe/archive/2013/06/06/virtualpublic.html

class NoDerived final { /* */ };      //派生鏈一開始就斷了

class Base {/* */};

class Last final : Base {/* */};     、、派生鏈在在中間斷

class Bad : NoDervied {/* */};

class Bad2 : Last {/* */}; 


15.2.3 類型轉換與繼承

通常情況下,如果我們想把引用或指針綁定到一個對象上,則引用或指針的類型應予對象的類型一致,或者對象的類型含有一個可接受的const類型轉換規則。存在繼承關係的類是一個重要的例外:我們可以將基類的指針或引用綁定到派生類對象中。

Base *p =  new Bulk_quote(); 

Bulk_quote bq;

Base &r = bq;

所以使用基類的指針和引用時,我們不知道該引用或指針所綁定對象的真實類型。該對象可能是基類的對象,也可能是派生類的對象。


靜態類型與動態類型:

靜態類型:編譯時已知,是自身的類型。如:Base b;

動態類型:運行時才知,是該對象(所指向的)內存中的變量或表達式表示的內容。如Base *r = Derived();

變量是指針或者引用時,纔可能有靜態類型和動態類型的不一樣。如果表達式既不是變量也不是指針,那麼它的動態類型永遠和靜態類型一致(沒有多態)。如Quote類型的變量永遠是一個Quote對象。


因此,基類的指針或引用的靜態類型可能與其動態類型不一致。


不存在基類向派生類的隱式類型轉換。(不可由簡單往復雜的轉)。因爲派生類一定包含基類的成分,而基類卻不一定包含派生類的成分。否則我們可能將會訪問到不存在的成員。


派生類對基類的類型轉換隻對指針和引用類型有效。


對象之間不存在類型轉換(即 Base b = Derived();  b.f() 調用的還是 Base::f( ))


struct Base
{
    int x;
    Base() = default;
    Base(const Base& b){cout << "Base" << endl;}
};
struct Derived : Base
{
    int y;
    Derived() = default;
    Derived(const Derived& d) {cout << " Derived" << endl;}
};
int main()
{
    Derived d;
    Base b(d); //輸出Base
    return 0;
}
即使我們給基類的拷貝構造函數傳一個派生類的實參,拷貝構造函數執行的還是基類的那一個,該構造函數只能處理基類自己的成員。

Bulk_quote bulk;   //派生類對象

Quote item(bulk);   //執行Quote::Quote(const Quote&) ; 該函數只能處理基類中的成員,忽略派生類中的成員

item = bulk;      //執行 Quote::operator=(const Quote&); 該函數只能處理基類中的成員,忽略派生類中的成員

因爲上述過程會忽略Bulk_quote部分,所以我們稱bulk的Bulk_quote 部分被切掉(slice down)了。


當我們用一個派生類對象爲一個基類對象初始化或賦值時,只有給派生類對象中的基類部分會被拷貝、移動或賦值,他的派生類部分將被忽略掉。


派生類到基類的轉換也可能由於訪問受限而不可行(p544).


15.3

 虛函數

虛函數一定要有定義,或者標爲 =0(純虛函數)。

通常我們不用某個函數就不必要把它定義,聲明即可。但虛函數除外!!!!!!!!!!因爲連編譯器都不知道用到了哪個虛函數(運行時纔會報錯,編譯期不報錯)。


對虛函數的調用可能在運行時才被解析。

動態綁定只有我們通過【指針或引用】調用【虛函數】時纔會發生!(關鍵字:指針  引用  虛函數)

引用或指針的動態類型與靜態類型不同是c++多態性的根本所在。

通過對象調用的虛函數或非虛函數也在編譯期綁定。因爲是對象,而不是指針或引用,對象的靜態類型和動態類型相同。


派生類中的虛函數:

派生類中的虛函數前的virtual寫不寫都可,默認爲虛函數了。

派生類中的函數如果覆蓋某個繼承而來的虛函數,則它的形參類型必須是與被它覆蓋的基類函數完全一致。否則就是派生類中定義的新函數,且隱藏原函數,可用override防止不小心寫錯了參數列表或返回值這種錯誤,因爲加了override之後就肯定是要覆蓋基類中的虛函數了。


基類有同名的虛函數時,才能在派生類中寫override。


派生類中虛函數的返回類型必須和基類函數相一致。但當類的虛函數返回類型是類本身的指針或引用時例外,上述規則無效。如果D由B派生得到,那麼即使基類虛函數返回類型爲B* 或者B& , 派生類虛函數返回類型也可以是D*或者D&。(條件是D到B的轉換是可訪問的。(貌似只有public繼承可以), 類型轉換的可訪問性見p544)。


struct B
{
    virtual void f1(int) const;
    virtual void f2(){};
    void f3();
};

struct D1 : B
{
    void f1(int) const override; //正確
    //void f2(int) override;   //錯誤,沒有const,找不到匹配,但又聲明瞭override
    //void f3() override;    // 錯誤,B中的f3 不是虛函數
    //void f4() override;    //錯誤,B中無f4。
};

我們使用override的意思是我們希望覆蓋該函數,而不是重新定義,並且隱藏原來那個。


override 不能再類外出現!!!!!!!!!!!!!!!!

struct D2 : B
{
    //從B繼承f2() 和f3(), 覆蓋f1(int)
    void f1(int) const final;

};

struct D3 : D2
{
    void f2();     //正確,覆蓋從間接基類B繼承而來的f2
    void f1(int) const; //錯誤,D2已經將f2聲明爲final
};

在派生類中,要麼不寫虛函數的聲明(直接隱式使用基類的定義),一旦寫了聲明,就必須把定義也寫了,否則虛函數表會出錯。

定義與聲明的本質區別:有無內存分配

final和override出現在形參列表(包括const和引用修飾符)以及尾置返回類之後


虛函數允許有默認實參,實參值由本次調用的靜態類型決定。

Base ===》 virtual void show(int x = 0) { cout << x << endl;}

Derived ===》virtual void show(int x = 1) {cout << x << endl; cout << "sb" << endl;}

Base *b = new Derived();

b-show();    //輸出靜態類型的形參 0 和 字符 sb.  除了默認形參用的是靜態類型的,其他函數體什麼的都是用動態類型的。


迴避虛函數的機制:

我們可以通過作用域運算符來回避虛函數:

Base *p = Derived();

p -> Base::f();   //執行的是基類的f().


一個派生類的虛函數調用基類的虛函數時,一定要加Base::符號,否則會導致派生類虛函數調用自己,無限遞歸,直至爆棧。


抽象基類:

將基類的某個成員函數定義爲純虛的(pure virtual),這個基類就是抽象類。

在類成員函數的聲明語句前加上 = 0,就可以將一個虛函數聲明爲純虛函數。和virtual、override和static一樣,= 0 只能出現在類內部。

雖然一個抽象基類不能定義對象,但是這個基類的派生類的構造函數會調用基類的構造函數來初始化派生類中的基類部分。所以該定義的函數還是要正常定義(實現)。


我們也可以爲純虛函數提供定義,不過函數體必須定義在淚外部。

含有(或者未經覆蓋直接繼承)抽象函數的類是抽象基類。

抽象基類負責定義接口。派生類可以覆蓋該接口。不能創建基類對象,因爲純虛函數沒有實現。

派生類的構造函數只初始化它的直接基類。


假設有:

struct Base
{
    int x;
    Base(int t) : x(t) {} 
    void f(){}
};
struct D1:public Base  //抽象類
{
    int y;
    virtual void f() = 0;
}; 
struct D2 : public D1{};


D2沒有自己的數據成員,但它依然需要一個接受一個參數的構造函數,該函數將他的實參傳遞給D1的構造函數,隨後D1的構造函數繼續調用Base的構造函數,Base的構造函數首先初始化Base的x,當Base的構造函數結束後,開始運行D1的構造函數並初始化y成員,最後運行D2的構造函數,該函數無須執行實際的初始化操作。

protected :只有 【自身】、【派生類】和【友元】能訪問pritected,private: 只有【自身】和【友元】可訪問。public:誰都能訪問。派生類能訪問protected指的是能訪問自身從基類繼承下來的protected部分。而不是說給一個派生類的函數傳進去一個基類的對象就能直接訪問基類的pritected成員。

class Base
{
protected:
    int x;
};

class Derived : public Base
{
    friend void f(Derived &d); //能訪問Derived::x;
    friend void f(Base &b); //不能訪問Base::x;
};
void f(Derived &d){cout << d.y << endl;} //正確
void f(Base &b){cout << b.x << endl;}  //錯誤

原因很簡單,如果不這樣規定的話,我們只要隨便公有繼承一個類,就可以訪問它的protected對象,從而破壞封裝性。
總結:派生類的成員和友元只能訪問派生類對象中基類部分的受保護成員;對於普通的基類中的成員不具有特殊的訪問性。

三種繼承方法:
某個類對其繼承而來的成員的訪問權限收兩個因素影響:(1)該成員在基類中的訪問說明符。(2)派生類的派生列表中的訪問說明符。
類似 Z = min(X, Y),X和Y分別爲在基類中的訪問權限和派生類型對應的訪問權限。只有protected 和 public能被訪問,一旦X 或 Y其中一個爲private,  就不可訪問了。 在派生類中就是private的了。(可訪問,但只有派生類的成員函數和友元可訪問)
如果D繼承B的方法是private的,那麼,B的成員或友元還是可以訪問原來是public和protect的成員的,但從D繼續派生出去的類就不可訪問了,無論是成員還是友元。

class A
{
X:  //.......
}
class B : Y A
{
Z = min(X, Y)
Z表示成員在B中的可訪問性。
}

class Base
{
public:
    int x;
protected:
    int y;
private:
    int z;
};

struct Pub_Derv : protected Base
{
    int f1() {return x;}//正確
    int f2() {return y;}//正確
    int f3() {return z;}//錯誤
};
struct Priv_Derv : private Base
{
    // private不影響派生類的訪問權限。但在之後,xyz都變成private的了。
    int f1() {return y;}
};
派生類訪問說明符對於派生類的成員(及友元)能否訪問其【直接基類】沒有什麼影響。對基類成員的訪問權限只與基類中的訪問說明符有關。

Pub_Derv d1;  //繼承自Base的成員是public的

Priv_Derv d2;  //繼承自Base的成員是private的

d1.pub_mem(); //正確

d2.pub_mem(); //錯誤,只能通過公有成員或友元訪問private部分。


繼承說明符只是指明瞭在繼承之後,類中的成員的可訪問性(只能“越來越”private,不能“越來越”public)
如果在派生鏈的某一個節點,某個成員變了private,那麼後面的任何子派生類都不能再訪問該成員了。否則會破壞封裝性。
派生類向基類轉換的可訪問性(特指指針或者引用指向派生類對象,注意:類不可相互轉換,即使能賦值):
只有當D公有繼承B時,用戶代碼才能使用派生類向基類的轉化;如果D繼承B的方式是受保護或者私有的,則用戶代碼不能使用該轉換。
不論D以什麼方式繼承B,D的成員函數和友元都能使用派生類向基類的轉換;派生類向其直接基類的類型轉換對於派生類的成員和友元來說永遠是可訪問的。
如果D繼承B的方式是公有的或者受保護的,則D的派生類的成員和友元可以使用D向B的轉換;反之,如果D繼承B的方式是私有的,則不能使用。
總結:轉換的可見性
用戶代碼:公有繼承,轉換纔可見。
派生類的成員和友元:怎麼繼承都可見。
派生類的子派生類:public和protected纔可見。


友元和繼承:


class Base
{
    friend class Pal;
private:
    int xb;
};

class Sneaky : public Base     //如果不是Base就不行了
{
private:
    int xs;
};
class Pal
{
public:
    int f1(Base b) { return b.xb; } //正確,友元
    int f2(Sneaky s){ return s.xs; } //錯誤,xs是Sneaky新的成員。
    int f3(Sneaky s) { return s.xb; } //正確,xb部分是屬於Base類,Pal是Base的友元
};

基類的友元對基類的可訪問性會隨着基類的public派生鏈延續下去。即所有public派生類中的基類成員都能被友元訪問。
但是Pal類的派生類不能訪問Base類。即被Base聲明爲友元的類的派生類,對Base不具有可訪問性。否則容易破壞封裝性。

class D2 : public Pal
{
public:
    int mem(Base b)
    {
        return b.xb;//錯誤
    }
};
當一個類將另一個類聲明爲友元時,這種友元關係只對做出聲明的類有效。對於原來那個類來說,其友元的基類或者派生類不具有特殊的訪問能力。



總結:友元可訪問一個基類及該基類在所有派生類中的部分,但繼承鏈中的每一步都必須是public 繼承。

改變個別成員的可訪問性:
可通過using聲明改變派生類繼承的某個名字的訪問級別。


class Base
{
public:
    std::size_t size() const {return n;}
protected:
    std::size n;
};

class Derived : private Base   //注意,是private繼承,如果不處理的話,下面就全部默認private了
{
public:
    //保持對象尺寸相關的成員的訪問級別
    using Base::size;

protected:
    using Base::n;
};

因爲Derived使用了私有繼承,所以繼承而來的成員size和n默認是Derived的私有成員。然而,我們使用using聲明改變了這些成員的可訪問性。改變之後Derived的用戶可以使用size成員(因爲是public),而Derived的派生類能夠使用n(因爲是protected)。


派生類只能爲哪些它可以訪問的名字提供using聲明。
struct : ①默認成員public;
     ②默認public繼承(當一個派生類是struct時)


class:  ①默認成員private;
    ②默認private繼承(當一個派生類是class時)


繼承中的類作用域
當使用一個成員時,先在派生類中尋找,找不到再找它的直接基類(尋找方向和繼承方向相反)。




在編譯時進行名字查找。
一個對象、引用或指針的靜態類型。決定了該對象的哪些成員是可見的。即使靜態成員和動態成員可能不一致(基類指針或引用綁定到派生類對象中時)。但是我們能使用哪些成員是由靜態類型(也就是說只能用靜態類型擁有的成員,虛函數也算基類擁有,因爲名字相同,而只不過是實現不同)決定的。但是如果是虛函數,而派生類又沒有重載的話,就由動態類型決定。
如果靜態類型和動態類型不同,而動態類型覆蓋了靜態類型的成員函數,那麼還是會調用靜態類型的哪個被覆蓋了的函數。


class Base
{
public:
    void bf(){}
};
class Derived : public Base
{
public:
    void df(){}
};
int main()
{
    Base *p = new Derived;
    p->df();
    return 0;
}
總結:【有沒有】是由靜態類型決定,而在都有的情況下虛函數【用哪個】由動態類型決定。


名字衝突與繼承
派生類可以重用基類的名字,會把基類的同名成員隱藏。(先找派生類,再找基類)


struct A
{
    int x = 1;
};
struct B : public A
{
};

struct C : public B
{
    int x = 3;
    void show_x(){cout << B::x << endl;}
};
C c;
c.show_x();  會打印1,因爲現在B中找x,找不到,再往B的基類找,永遠不會找到C::x。

作用域運算符將覆蓋原有的查找規則,並指示編譯器從Base類的作用域開始查找x。


名字查找的步驟:以p->mem()/obj.mem()爲例:
首先確定p(或obj)的靜態類型。因爲我們調用一個的是一個成員,所以該類型必須是一個類類型。
在p(或obj)的靜態類型對應的類中查找mem,如果找不到,則依次在直接基類中不斷查找直至到達繼承鏈的頂端。如果還是找不到,將報錯。
一旦找到了mem,將進行常規的類型檢查,以確定對於當前找到的mem,本次調用是否合法。
假設調用合法,則編譯器將根據調用的是否是虛函數而產生不同的代碼: 
(1)如果mem是虛函數且我們是通過指針或引用進行的調用。則編譯器產生的代碼將在運行時確定到底運行該虛函數的哪個版本,依據的是對象的動態類型。
(2)反之,如果mem不是虛函數或者我們是通過對象(而非引用或指針)進行的調用,則編譯器產生一個常規函數調用。


名字查找先於類型檢查:如果派生類的成員和基類的某個成員同名,則派生類將在其作用域內隱藏該基類成員。即使派生類成員和基類成員的形參列表不一致。

class Base
{
public:
    void f(){ }
};
class Derived : public Base
{
public:
    void f(int x){ }
};
Derived d;
Base b;
b.f();
d.f(10);
d.f();  //錯誤,被void f(int x) 隱藏了。
所以基類和派生類的虛函數必須要有相同的形參列表,否則我們將無法通過引用或指針調用派生類的虛函數了。


一個派生類中的新函數(形參類型不同)可以把基類中的虛函數隱藏,使它不可見,但還是可以用。p550
判斷有沒有一個函數時,以靜態類型爲準;如果找到函數,但要確定執行哪個版本時,以動態類型爲準。


class Base
{
public:
    virtual void f(){ cout << "v-Base" << endl; }
};
class Derived : public Base
{
public:
    void f( ){ cout << "Derived" << endl; }   //跟上面的同名,還是那個虛函數,並不是新函數
};
Base *p = new Derived;
p->f();// 打印 "Derived"

這裏說的隱藏:指的是同名不同參數的函數。


覆蓋重載的函數:
如果派生類希望所有的重載版本對於它來說都是可見的,那麼它就需要覆蓋所有的版本,或者一個也不覆蓋。否則如果只覆蓋一部分的話,另一部分會被隱藏(報錯)。但是如果加上”Base::”,就不會報錯了。


也可以使用一個using語句指定一個名字而不指定形參列表。。此時派生類只需要定義其特有的函數就可以了。而無須爲繼承而來的其他函數重新定義。
using Base::f;
//後面再重載部分函數。


在一個類裏面,同名函數也可以是部分爲虛函數,部分非虛。只要形參不同即可......


15.7  構造函數與拷貝控制
如果基類的析構函數不是虛函數,則delete一個指向派生類對象的基類指針將產生未定義的行爲。
基類總是需要定義析構函數,是因爲要顯式地把析構函數設爲virtual。
派生類的虛析構函數可以把比基類多出來的那部分成員數據delete掉。


虛析構函數將阻止合成移動操作。(?????)


15.7.2 合成拷貝控制與繼承
構造函數執行的時候,先執行派生類的構造函數。再調用其直接基類的構造函數,直到繼承鏈頂端。但是如果在函數體中顯式的話,會先顯示基類的(類似於遞歸中最後被調用那個函數的東西先顯示)。


先執行基類構造函數,再執行派生類構造函數。 析構函數的順序相反。






要使用成員的拷貝控制,就要求成員可訪問,並且不是一個被刪除的函數。




派生類中刪除的拷貝控制與基類的關係:
(1)如果基類中的某個【合成的特殊函數】是派生類不可訪問的或者是刪除的,那麼派生類中的對應的成員將是被刪除的,原因是編譯器不能使用基類成員來執行派生類對象基類部分的構造、賦值與銷燬工作。
(2)如果在基類中有一個不可訪問或刪除掉的【析構函數】,則派生類中合成的默認和拷貝構造函數將是被刪除的。原因同上。
(3)編譯器不會合成一個刪除掉的移動操作。。。。。。。。。。。。。。。?????????(待續)


移動操作與繼承。。。。。。。。。。??????????待續


15.7.3派生類的拷貝控制成員。
派生類的除析構函數外的函數,既要處理派生類的成員,也要處理基類的成員(賦值運算符要在函數體內處理基類,而構造函數和拷貝構造函數在初始化列表處調用基類的構造函數)。而析構函數只需要銷燬自己的成員,會隱式調用基類的析構函數來處理基類的成員。
當派生類定義了【拷貝】或【移動】操作時,該操作負責拷貝或移動包括基類在內的整個對象,但析構不用。


派生類賦值運算符和派生類構造函數:



class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};

class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};


如果不寫拷貝基類成員那部分,會造成基類成員爲默認值,而派生類成員爲拷貝值的情況。
類似的:基類的賦值運算符不會被自動調用。
應該寫成:


class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};


D& operator=(const D &rhs)

{

Base::operator=(rhs); //爲基類部分賦值

//爲派生類的成員賦值

//酌情處理自賦值及釋放已有資源等情況

reuturn *this;

}

class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};
























派生類的析構函數:

class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};

class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};



//Base::~Base()被自動調用執行

~D() 

{

// 不用顯式調用基類的析構函數。

 /*該處由用戶定義清楚派生類成員的操作*/ 

}



class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};


派生類的析構函數:


對象銷燬的順序正好與其創建的順序相反:派生類析構函數首先執行,然後是基類的析構函數,以此類推,沿着繼承體系的反方向直至最後。


如果一個類對象處於正在構建的過程中,我們認爲它的類型和當前執行的構造函數(析構函數)的類型相同。而把整個類構造的過程看成類型轉化的過程。




在c++11中,我們可以用using語句直接使用基類的構造函數。


class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};


派生類的析構函數:


class Bulk_quote : public DIsc_quote

{

public:

using Disc_quote::Disc_quote;    //繼承Disc_quote的構造函數

double net_price(std::size_t) const; 

};










































15.8  容器與繼承

vector<Quote> basket;
basket.push_back(Quote("0-123-123", 50));
basket.push_back(Bulk_quote("0-123-123", 50, 10, 25)); //正確,但是隻能把對象的Quote部分拷貝給basket
cout << basket.back().net_price(15) << endl; //調用Quote版本的函數。
向vector中添加一個Bulk_quote時,派生類部分會被忽略掉。


在容器中放置(智能)指針而非對象:

當我們希望在容器中存放具有繼承關係的對象時,我們實際上存放的通常是基類的指針(最好是智能指針)。這些指針所指向動態對象可能是基類類型,也可能是派生類類型。

vector<shared_ptr<Quote> > basket;
basket.push_back(make_shared<Quote>("0-123-123", 50));
basket.push_back(make_shared<Bulk_quote>("0-123-123", 50, 10, .25)); 
cout << basket.back()->net_price(15) << endl; //調用Bulk_quote版本的函數。

上述代碼成立的原因是可以把派生類的指針轉換成基類的指針。

類似於通過基類指針調用派生類的虛函數,在編譯時並不知道該指針指向的類型,所以能夠將Bulk_quote的指針存到Quote類型的容器中。儘管形式上有區別,但實際上basket的所有元素類型都是相同的



15.8.1  編寫Basket類


class D : public Base

{

public:

D(const D &d) : Base(d)   // 拷貝基類成員

{

//D的(獨有)成員的初始值

 }

D(D&& d) : Base (std::move(d))   // 移動基類成員

{

//D的(獨有)成員的初始值

}

};

派生類的析構函數:


發佈了64 篇原創文章 · 獲贊 10 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章