C++學習記錄(六)——類模型之繼承與多態

OOP的特性(繼承)

走到這一步,咱們能夠進一步討論有關OOP在編程上的具體實現。
上一次,咱們提出來類的概念。其實類對應了OOP中的抽象這一律念。咱們將事物的共同點提取出來,抽象成了類。
這一次,爲了提升代碼的重用性,C++提出了繼承語法。很好理解,咱們將各類種類的羊抽象出來,寫出了class 羊。將各類牛特色提取出來,抽象成了 class 牛。若是咱們還有抽象出來 馬 這一動物。咱們發現了牛,羊,馬自己也都有共同點,就是它們動物,通常動物會吃喝跑,它們也都會。因此咱們抽象出動物這一特性,讓牛羊馬來繼承動物的特性。這樣能夠有效提升咱們代碼的效率。ios

類繼承

基本語法

經過以上的例子,咱們稱動物類爲父類(又稱基類),牛羊馬類爲繼承動物類的子類(又稱派生類)程序員

//父類
class Base
{

};

//子類繼承父類
class Son : public Base
{

};

繼承的格式:
class 派生類名 : 繼承方式 基類名
{
//派生類新增的成員變量或者成員函數
}編程

  • 派生類對象存儲了基類的數據成員(派生類繼承了基類的實現)
  • 派生類對象可使用基類的方法(派生類繼承了基類的接口)

固然,這不意味這子類能夠啃老。子類還須要本身實現一些事情:數組

  • 派生類須要本身的構造函數
  • 派生類會根據須要添加額外的數據成員和成員函數

有關訪問權限(繼承方式)安全

  • public
  • private
  • protected

clipboard.png
在這裏有一件事須要重點關注:ide

派生類不能直接訪問基類的private函數和變量,可是能夠訪問protected函數和變量
即,對於外部世界來講,protected和private是同樣的;對於派生類來講,protected和public是同樣的

派生類不能直接訪問基類的私有成員,必須經過基類的方法來進行訪問(get和set函數)
這使得咱們想到構造函數,不過很遺憾派生類的構造函數不能直接設置繼承成員,而必須使用基類的公有方法來訪問私有基類成員。即,派生類構造函數必須使用基類構造函數函數

Son::Son(int a,int b,int c,int d):Base(_b,_c,_d)
{
    this->a = a;
}

上述代碼如何理解呢?
子類Son的構造函數其實只賦值了a這一成員變量。後面Base(_b,_c,_d)咱們叫成員初始化列表。舉一個例子,若是咱們給子類Son實例化 Son son(1,2,3,4);時Son的構造函數把實參 2, 3, 4賦值給形參_a,_b,_c而後將這些參數做爲實參賦值給父類Base構造函數,後者將嵌套一個Base對象,並將數據存入這個Base對象,而後進入Son的構造函數,完成對Son對象的建立,並將參數a賦值給this->a。
固然若是使用基類的拷貝構造函數也是能夠的:性能

Son::Son(int a,int b,int c,int d):Base(base)
{
    this->a = a;
}

這裏使用的是拷貝構造函數,這種方式咱們稱是隱式的,而上述方法是顯式
若是說咱們後面什麼都不寫,編譯器默認調用默認構造函數,即:this

Son::Son(int a,int b,int c,int d)
{
    this->a = a;
}

就等於spa

Son::Son(int a,int b,int c,int d):Base()
{
    this->a = a;
}

咱們總結一下剛剛說過的要點

  • 基類對象首先被建立
  • 派生類構造函數應經過成員初始化列表將基類信息傳遞給基類構造函數
  • 派生類構造函數應初始化派生類新增的數據成員

咱們在這裏並無討論析構函數,可是須要強調的是:析構函數的順序與構造函數是相反的,也就是先調用子類的析構,再調用父類的析構。

在使用子類的時候請記住要包含頭文件。
[注]:能夠和函數的初始化列表作一個聯繫

有關基類和派生類的一個特殊的用法

這個用法其實須要注意:
基類指針能夠在沒有進行顯示類型轉的狀況下指向派生類對象;同時基類的引用能夠在不進行顯示類型轉的狀況下引用派生類對象。
這樣作聽起來很美好,不過要注意的是:
基類的指針或者引用只用於調用基類的方法。
這一點是相當重要的,即

Son son1(1,2,3,4);
Base & sn = son1;
Base * psn = &son1;
//這樣是容許的。可是使用sn 或者 *psn調用派生類(Son)的方法是不容許的!

其實,我到目前爲止一直在避免說起內存的問題,可是時至今日也應該慢慢開始C++的內存管理問題了。沒錯,這裏就是涉及一個內存的問題,請慢慢看下去:
首先,毋庸置疑的是子類的存儲空間確定比父類的存儲空間大。(父類有的子類都有,子類有的父類卻不必定有)
因此,一個指向父類的指針 的尋址範圍是否是比起子類的存儲空間要小。這時,你用這個指向父類的指針去尋子類方法的地址,頗有可能會超出這個指向父類的指針的尋址能力,這樣是會有很大的安全隱患,編譯器是不容許的。
固然引用也是這個道理。(咱們等等還會繼續說這個問題,目前先這樣。)
可是,反過來——指向派生類的指針或者引用能夠調用父類的方法嗎?
答案是能夠的,咱們把這種手法稱之爲「多態」。

多繼承

C++中的繼承並不像Java中只能單繼承。C++的子類是能夠繼承多個父類。
可是,在多繼承中很容易引起二義性。這時請使用做用域運算符進行解決。
實際上,多繼承容易出現的問題並不只僅是命名問題,還有一個就是菱形繼承。
(這裏就不給UML圖了,本人仍是懶)
clipboard (1).png

  1. 羊繼承了動物,駝也一樣繼承了動物。當羊駝調用屬性或者方法時就會出現二義性。
  2. 羊駝繼承了羊和駝,而羊和駝都繼承了動物,因此羊駝這裏就會將動物的數據複製兩份,這樣就形成了空間的浪費。

這時,咱們又要引入C++的一個解決辦法:虛基類
首先,具體怎麼作?在繼承方式前加 vitual 關鍵字

class Animal { public: int Age; };
class Sheep : virtual public Animal {  };//虛基類
class Tuo : virtual public Animal {  };
class SheepTuo: public Sheep,public Tuo {  };
虛基類的工做原理

虛繼承能夠解決多種繼承前面提到的兩個問題:

虛繼承底層實現原理與編譯器相關,通常經過虛基類指針和虛基類表實現,每一個虛繼承的子類都有一個虛基類指針(佔用一個指針的存儲空間,4字節)和虛基類表(不佔用類對象的存儲空間)(須要強調的是,虛基類依舊會在子類裏面存在拷貝,只是僅僅最多存在一份而已,並非不在子類裏面了);當虛繼承的子類被當作父類繼承時,虛基類指針也會被繼承。

實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;經過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份一樣的拷貝,節省了存儲空間。

後面咱們會和虛函數(多態)進行比較。
同時這裏會有一個使用Visual Studio命令提示功能來查看內存分佈的技巧,就不展開說了。

OOP的特性(多態)

其實,剛剛的描述已經解釋了什麼是多態了。用指向子類對象的指針或者引用去調用父類的函數。爲何要這麼作?咱們從常識來理解一下。好比:貓和魚均可以繼承動物這個類。若是動物這個類裏面有 move() 移動這個方法。可是,咱們都知道貓和魚的移動方式是不一樣的。因此咱們要利用多態,來使得咱們的程序更符合現實。

類多態

重載(overload)

函數重載

函數重載,從常識來考慮。好比:咱們經過一個函數來計算得某個結果。可是,給這個函數一個參數 函數能夠計算,給函數兩個參數 函數也能夠計算(計算的方法可能不同),或者給函數三個參數仍然能夠計算(可能計算出來的精確度進一步提高了)。這樣的話,咱們的函數名字同樣,可是參數卻不同。這樣的就是函數重載。固然沒有必要計算的意義也同樣。簡單的來講,函數重載就名字同樣,返回值類型同樣,就是參數不同了
因此,咱們提煉出幾點:

  • 函數名稱相同
  • 函數返回值相同
  • 函數參數的個數,類型能夠不一樣
  • 須要在同一做用域下

[注]:當函數重載遇到默認參數時,要避免二義性。

void func(int a,int b = 10) { }
void func(int a) { }
void test() { func(10);//二義性,編譯器不知道使用哪一個func()了 }

簡單的說一下默認參數,其實很簡單。就是給函數參數設置一個默認值,在參數列表直接等於就好了,按照以上例子你能夠不給b值,默認是10;可是默認參數必須是最後面。不能插入沒有設定默認值的參數void test(int a = 10 ,int b);這樣是不行的。

重載的原理

在面對func()時,編譯器會可能默認把名字改爲_func;當碰到func(int a)時,可能會默認改爲_func_int;當碰到func(int a,char b)編譯器可能會默認該成_func_int_char。這個「可能」意思時指,如何修飾函數名,編譯器並無一個統一的標準,因此不一樣編譯器會產生不一樣的內部名。

算符重載

C++同時也容許給算符賦予新的意義
返回值 opertaor算符 (參數列表)
可是C++中並非全部算符均可以重載的:
如下是能夠重載的算符:
image.png
如下是不能夠重載的算符:
image.png
雖然在規則上是能夠重載 && 和 ||,可是在實際應用中最好很差重載這兩個運算符。其緣由是內置版本的&& ||首先計算左邊的表達式,若是能夠肯定結果,就無需計算右邊了,咱們已經習慣這種特性了,一旦重載便會失去這種特性。

  • =,[] ,->,() 操做符只能經過成員函數進行重載
  • <<和>> 只能經過全局函數配合友元函數進行重載
  • 不要重載&& 和||,由於沒法實現其運算規則
算符重載的重要應用——智能指針

因爲C++語言沒有自動內存回收機制,程序員每次new出來的內存都要手動delete。程序員忘記delete,流程太複雜,最終致使沒有delete,異常致使程序過早退出,沒有執行delete的狀況並不罕見。
因此,開發者能夠經過算符重載,從而達到智能管理內存的效果。
1.對於編譯器來講,智能指針其實是一個棧對象,並不是指針類型,在棧對象生命期即將結束時,智能指針經過析構函數釋放有它管理的堆內存。全部智能指針都重載了「operator->」操做符,直接返回對象的引用,用以操做對象。訪問智能指針原來的方法則使用「.」操做符。
2.所謂智能指針,是根據不一樣的場景來定製智能指針。如下給出一個最簡單的應用:

class Person
{
public:
    Person(int age)
    {
        this->Age = age;
    }
    ~Person()
    {
    
    }
    void showAge()
    {
        cout<<"年齡爲:"<<this->Age<<endl;
    }
private:
    int Age;
};
//智能指針,用來託管自定義類型的對象,讓對象自動釋放。
class smartPointer
{
public:
    smartPointer(Person * person)
    {
        this->person = person;
    }
    
    //重載->讓智能指針像Person *p同樣去使用
    Person * operator->()
    {
        return this->person;
    }
    //重載*
    Person & operator*()
    {
        return * this->person;
    }
    ~smartPointer()
    {
        cout<<"智能指針析構了!"<<endl;
        if(this->person != NULL)
        {
            delete this->person;
            this->oerson = NULL;
        }
    }
private:
    Person * person;
}

void test()
{
    Person p(10);//自動析構
    //至關於:
    //Person * p = new Person(10);
    //delete p;
    smartPointer sp(new Person(10));//開闢到棧上,自動析構
    sp->showAge();//自己sp不支持這樣的調用,因此要重載->
    (*sp).showAge();//一樣做爲智能指針,也要支持這樣的寫法。因此依舊重載*
}
有關C++11中的智能指針

咱們上文中是經過算符重載來實現的智能指針,在C++11標準中引入了智能指針概念。
1.理解智能指針

  • 從較淺的層面看,智能指針是利用了一種叫作RAII(資源獲取即初始化)的技術對普通的指針進行封裝,這使得智能指針實質是一個對象,行爲表現的卻像一個指針。
  • 智能指針的做用是防止忘記調用delete釋放內存和程序異常的進入catch塊忘記釋放內存。另外指針的釋放時機也是很是有考究的,屢次釋放同一個指針會形成程序崩潰,這些均可以經過智能指針來解決。
  • 智能指針還有一個做用是把值語義轉換成引用語義。

C++和Java有一處最大的區別在於語義不一樣,在Java裏面下列代碼:

Animal a = new Animal();
    Animal b = a;
     //你固然知道,這裏其實只生成了一個對象,a和b僅僅是把持對象的引用而已。但在C++中不是這樣,
    Animal a;
    Animal b = a;
     //這裏倒是就是生成了兩個對象。

2.智能指針的本質
智能指針是一個類對象,這樣在被調函數執行完,程序過時時,對象將會被刪除(對象的名字保存在棧變量中),
這樣不只對象會被刪除,它指向的內存也會被刪除的。

3.智能指針的使用
智能指針在C++11版本以後提供,包含在頭文件<memory>中,shared_ptr、unique_ptr、auto_ptr
這裏只給出建議(智能指針會涉及到不少知識,屬於C++的綜合題):

  • 每種指針都有不一樣的使用範圍,unique_ptr指針優於其它兩種類型,除非對象須要共享時用shared_ptr。
  • 若是你沒有打算在多個線程之間來共享資源的話,那麼就請使用unique_ptr。
  • 使用make_shared而不是裸指針來初始化共享指針。
  • 在設計類的時候,當不須要資源的全部權,並且你不想指定這個對象的生命週期時,能夠考慮使用weak_ptr代替shared_ptr。

重寫(override)

簡單來講,重寫就是返回值,參數,函數名都和圓臉同樣,以後函數體裏面的方法重寫了。
下面將詳細介紹。

動態聯編和靜態聯編

程序調用函數時,編譯器將源代碼中的函數調用解釋爲特定函數代碼塊被稱爲函數名聯編(binding)。C語言中沒有重載,因此每一個函數名字都不一樣,因爲C++中有重載的概念,因此編譯器必須查看函數參數以及函數名才能肯定使用哪一個函數。C/C++編譯器能夠在編譯過程當中完成聯編。而在編譯過程實現的聯編稱靜態聯編(static binding)。所謂動態聯編(dynamic binding)是指聯編在程序運行時動態地進行,根據當時的狀況來肯定調用哪一個同名函數,其實是在運行時虛函數的實現。國內教材有的稱之爲束定。
經過動態聯編引出了虛函數

虛函數

語法上來講,虛函數的寫法是:在類成員函數聲明的時候添加 vitual關鍵字。
咱們繼續剛剛有關基類和派生類的特殊用法繼續說,
將派生類的引用或指針轉換成基類的引用和指針咱們稱之爲:向上強制轉換(upcasting)
相反,將基類的引用或指針轉換成派生類的引用和指針咱們稱之爲:向下強制轉換(downcasting)
咱們如今知道,向下轉型是不被容許的。

虛函數的工做原理——虛函數表和虛函數指針

虛函數指針

虛函數指針 (virtual function pointer) 從本質上來講就只是一個指向函數的指針,與普通的指針並沒有區別。它指向用戶所定義的虛函數,具體是在子類裏的實現,當子類調用虛函數的時候,其實是經過調用該虛函數指針從而找到接口。

虛函數指針是確實存在的數據類型,在一個被實例化的對象中,它老是被存放在該對象的地址首位,這種作法的目的是爲了保證運行的快速性。與對象的成員不一樣,虛函數指針對外部是徹底不可見的,除非經過直接訪問地址的作法或者在DEBUG模式中,不然它是不可見的也不能被外界調用。

只有擁有虛函數的類纔會擁有虛函數指針,每個虛函數也都會對應一個虛函數指針。因此擁有虛函數的類的全部對象都會由於虛函數產生額外的開銷,而且也會在必定程度上下降程序速度。與JAVA不一樣,C++將是否使用虛函數這一權利交給了開發者,因此開發者應該謹慎的使用

虛函數表(如下解釋,來自於https://blog.csdn.net/haoel/a...。由於作圖太麻煩,因此直接選擇性的截取一點。)
在這個表中(V-Table),主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,因此,當咱們用父類的指針來操做一個子類的時候,這張虛函數表就顯得由爲重要了,它就像一個地圖同樣,指明瞭實際所應該調用的函數。

編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——若是有多層繼承或是多重繼承的狀況下)。 這意味着咱們經過對象實例的地址獲得這張虛函數表,而後就能夠遍歷其中函數指針,並調用相應的函數。

舉個例子:

class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};

按照上面的說法,咱們經過把Base實例化,來得到虛函數表。

typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虛函數表地址:" << (int*)(&b) << endl;
cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function 
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

實際結果以下:

虛函數表地址:0012FED4
虛函數表—第一個函數地址:0044F148
Base::f

經過這個示例,咱們能夠看到,咱們能夠經過強行把&b轉成int ,取得虛函數表的地址,而後,再次取址就能夠獲得第一個虛函數的地址了,也就是Base::f(),這在上面的程序中獲得了驗證(把int 強制轉成了函數指針)。經過這個示例,咱們就能夠知道若是要調用Base::g()和Base::h(),其代碼以下:

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()
(Fun)*((int*)*(int*)(&b)+1);  // Base::g()
(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

o_vtable1.jpg
在上面這個圖中,我在虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符「/0」同樣,其標誌了虛函數表的結束。這個結束標誌的值在不一樣的編譯器下是不一樣的。(有多是NULL也有多是0)

同時,派生類是否對父類函數進行了覆蓋,虛函數表也是不同的,因此咱們分狀況來討論。

1.無覆蓋
定義以下的繼承關係:
o_Drawing3.jpg
對於實例而言,其虛函數表:
o_vtable2.jpg

  • 虛函數按照其聲明順序放於表中。
  • 父類的虛函數在子類的虛函數前面。

2.有覆蓋(這纔是通常狀況,由於虛函數不覆蓋便毫無心義)
o_Drawing4.jpg
咱們只重載了f()。因此其虛函數表:
o_vtable3 (1).jpg

  • 覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
  • 沒有被覆蓋的函數依舊。
Base \*b =newDerive();
b->f();

由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,因而在實際調用發生時,是Derive::f()被調用了。這就實現了多態

3.有多個繼承可是無覆蓋
o_Drawing1.jpg
這是其虛函數表:
o_vtable4.jpg

  • 每一個父類都有本身的虛表。
  • 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

4.有多個繼承且有覆蓋
o_Drawing2.jpg
其虛函數表是:
o_vtable5.jpg

三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,咱們就能夠任一靜態類型的父類來指向子類,並調用子類的f()了。

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
總結

走到這一步,咱們就能夠總結一下了。

剛剛一直在說一個新的詞彙——覆蓋。但其實,可能不少人如今已經知道了,這裏的覆蓋就是重寫
所謂靜態聯編就是函數重載,所謂動態聯編就是函數重寫
向下強制轉型不被編譯器容許
同時,咱們利用虛函數表的特性仍能夠作非法的行爲:
訪問non-public的函數
若是父類的虛函數是private或是protected的,但這些非public的虛函數一樣會存在於虛函數表中,因此,咱們一樣可使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易作到的。

class Base {
private:
    virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
    Derive d;
    Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}

咱們如今應該明白:編譯對虛函數使用動態聯編的意思了。

Q:爲何編譯默認是靜態聯編?
A:咱們除了功能之外始終不能忽視就是效率。由於根據上文的描述,咱們不難想到用一些方法來跟蹤基類指針或引用指向的類模型這件事自己其實增長咱們的開銷。所謂C++編譯器選擇了開銷更低的方式。咱們應該優先選擇效率更高的方式來開發程序。

當咱們知道了虛函數的原理的同時也必須知道虛函數到底增長哪些開銷:

  • 每一個對象都會增長存儲地址的空間。
  • 對於每個類,編譯器都會建立虛函數表(數組)
  • 每個函數調用時都增長了額外操做——查找地址。
注意
  • 在基類方法中聲明關鍵字virtual可以使該方法在基類以及全部派生類中是虛擬的
  • 若是使用指向對象的指針或引用來調用虛方法,程序將使用爲對象類型定義的方法,而不使用爲指針或引用類型定義的方法。這稱爲動態聯編
  • 若是定義的類被用作基類,則應將那些要在派生類中從新定義的類方法聲明中虛擬
  • 構造函數不能是虛函數,派生類不會繼承基類的構造函數
  • 析構函數應該是虛函數,除非不是基類。(最好這麼作,由於普通析構不會調用子類析構函數,會致使釋放不乾淨)
  • 友元不能是虛函數,友元根本就不是類成員。
  • 若是你在編程的時候寫出了以下代碼:
class A
{
public:
    vitual void A(int a){...}
};
class B:piblic A
{
public:
    vitual void A(){...}
};

派生類中沒有參數的A把基類中有參數的A給隱藏了,並無重寫。有可能編譯器給你警告,也有可能不會。在《C++ Prime Plus》中將這樣的錯誤稱爲「返回類型協變(covariance of return type)」

  • 若是兩個函數構成了重寫的關係,必須兩個都加vitual關鍵字。

抽象類與接口

抽象類(abstract base class ,ABC),這裏的抽象類其實就是Java中所說的接口。並不難理解。
這裏舉一個例子:羊類。咱們能夠寫出山羊類來繼承羊類,一樣也能夠寫綿羊類來繼承羊類,也許咱們還能寫出更不同的羊來繼承羊類。但別忘了,咱們必須給羊類的成員函數作出一個定義,即使羊類的成員函數里根本沒有有意義的代碼。那咱們與其寫沒有意義的代碼,倒不如干脆什麼都別寫。再具體一點: 羊會跑——void run() 同時 void run()中可能會用到羊類裏面的一個屬性——奔跑的速度。可是,不一樣種類的羊跑的速度又不同快。這是咱們會在void run()裏面什麼都不寫。直接一個{}就完事。等待子類重寫這個void run()。因此,這裏的run()雖然有定義,可是這倒是一個接口的思想。因此,咱們能夠把void run()寫出ABC的樣子:vitual void run() = 0;這樣這個就變成了抽象類,而run這個函數就成爲了純虛函數即,這個羊類純粹是爲了讓其餘類繼承重寫而出現的。這樣若是之後有新的需求能夠直接來實現這個羊的接口

  • 只要類裏有一個純虛函數,這個類就是抽象類
  • 當繼承一個抽象類時,必須實現其全部純虛函數。若是不這麼作的話,派生類還是抽象類
  • 抽象類不能實例化!

有關繼承和動態內存分配

派生類不使用 new 的狀況

  • 析構函數使用默認析構函數便可。默認析構函數也是執行一些操做:執行完自身後調用基類的析構函數。
  • 拷貝構造函數使用默認的拷貝構造函數便可。
  • 賦值操做符也是使用系統默認便可。
綜上所述,若是沒有new運算符,析構函數,拷貝構造函數和賦值操做符使用默認便可

派生類使用 new 的狀況

  • 派生類的析構函數自動調用基類析構函數,故其自身的職責就是對派生類的構造函數申請的堆空間進行清理
  • 派生類的拷貝構造函數只能訪問派生類的數據,因此派生類的拷貝構造函數必須調用基類的拷貝構造函數來處理共享的基類數據。
  • 賦值操做符:因爲派生類使用了new動態分配了內存,因此它須要一個顯式賦值運算符。由於派生類的方法只能訪問派生類的數據,可是派生類的賦值運算符必須負責全部繼承的基類對象的賦值,能夠顯式調用基類賦值操做符來完成這個工做。
綜上所述,當基類和派生類都動態分配內存時,派生類的析構函數,拷貝構造函數,複製運算符都必須使用相應基類的方法來處理基類元素。固然這三者完成這項任務的手段都不一樣:
  • 析構函數是自動完成
  • 拷貝構造函數經過初始化成員列表中調用基類的拷貝構造函數來完成,若是這麼作就會默認調用基類的默認構造函數
  • 賦值運算符,是經過做用域運算符來顯式調用基類的賦值運算符來完成的。

來自《C++ Prime Plus》的一個範例:

//"dma.h"
#ifndef DMA_H_
#define DMA_H_
#include <iostream>

class baseDMA
{
private:
    char * label;
    int rating;
public:
    baseDMA(const char * l = "null", int r = 0);
    baseDMA(const baseDMA & rs);
    virtual ~baseDMA();
    baseDMA & operator= (const baseDMA & rs);
    friend std::ostream & operator<<(std::ostream & os,const baseDMA & rs);
};

class lacksDMA:public baseDMA
{
private:
    enum{COL_LEN = 40};
    char color[COL_LEN];
public:
    lacksDMA(const char * c = "blank", const char * l = "null", int r = 0);
    lacksDMA(const char * c, const baseDMA & rs);
    friend std::ostream & operator<<(std::ostream & os,const lacksDMA & rs);
};

class hasDMA:public baseDMA
{
private:
    char * style;
public:
    hasDMA(const char * s = "none", const char * l = "null", int r = 0);
    hasDMA(cosnt char * s, const baseDMA & rs);
    ~hasDMA();
    hasDMA & operator= (cosnt hasDMA & rs);
    friend std::ostream & operator<< (std::ostream & os,const hasDMA & rs);
};
#ennif
#include "dam.h"
#include <cstring>

baseDMA::baseDMA(const char * l, int r)
{
    label = new char[std::strlen(l) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
}
baseDMA::~baseDMA()
{
    delete [] label;
}
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
    if(this == &rs)
        reurn *this;
    delete [] label;
    label = new char[std::strlen(rs.label) + 1];
    std::strcpy(label, rs.label);
    rating = rs.rating;
    return *this;
}
std::ostream & operator<<(std::ostream & os, const baseDMA & rs)
{
    os<<"Label:"<< rs.label <<std::endl;
    os<<"Rating:"<< rs.rating << std::endl;
    return os;
}

lacksDMA::lacksDMA(const char * c, const char * l, int r):baseDMA(l,r)
{
    std::strcpy(color, c, 39);
    color[39] = '\0';
}
lacksDMA::lacksDMA(const char * c, const baseDMA & rs):baseDMA(rs)
{
    std::strncpy{color, c, COL_LEN - 1};
    color[COL_LEN - 1] = '\0';
}
std::ostream & operator<<(std::ostream & os, const lacksDMA & ls)
{
    os<< (const baseDMA &) ls;
    os<<"Color: "<< ls.color << std::endl;
}

hasDMA::hasDMA(cosnt char * s, const char * l, int r):baseDMA(l, r)
{
    style = new char[std::strlen(s) + l];
    std::strcpy(style,s);
}
hasDMA::hasDMA(const char * s, const baseDMA & rs):baseDMA(rs)
{
    style = new char[std::strlen(s) + l];
    std::strcpy(style,hs.style);
}
hasDMA::~hasDMA()
{
    delete [] style;
}
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
    if(this == &hs)
        return *this;
    baseDMA::operator=(hs);
    style = new  char[std::strlen(hs.style) + 1];
    std::strcpy(style, hs.style);
    return *this;
}
std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
    os << (cosnt baseDMA & ) hs;
    os << "Style: " << hs.style << std::endl;
    return os;
}
//main.cpp

#include <iostream>
#include "dma.h"
int main()
{
    using std::cout;
    using std::endl;
    
    baseDMA shirt("Porablelly", 8);
    lacksDMA balloon("red", "Blimpo", 4);
    hasDMA map("Mercator", "Buffalo Keys", 5);
    cout << shirt << endl;
    cour << balloon << endl;
    lacksDMA balloon2(balloon);
    hasDMA map2;
    map2 = map;
    cout << balloon2 << endl;
    cout << map2 << endl;
    
    return 0;
}

-----本文僅我的觀點,歡迎討論。

相關文章
相關標籤/搜索