文章目錄
- 十二、繼承
- 8. 繼承和組合
- 十三、多態
- 1. 多態的概念
- 2. 多態的定義和實現
- 虛函數重寫的兩個特殊情況
- override 和 ?nal
- 3. 多態的原理
- 1. 虛函數表
- 未完待續
十二、繼承
8. 繼承和組合
我們已經知道了什么是繼承,那組合又是什么?下面這種情況就是 組合 。
class A
{//
};class B
{
private:A _a;
};
組合和繼承都是讓代碼復用,但是繼承的復用是一種 白箱復用 ,父類的內部細節是對子類透明的,根透明箱子一樣。而組合的復用是一種 黑箱復用 ,因為對象的內部細節是不可見的。
繼承一定程度破壞了父類的封裝,父類的改變,對子類有很大的影響。子類和父類間的依賴關系很強,耦合度高 。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于保持每個類被封裝 。
優先使用對象組合,而不是繼承。
public繼承是一種 is-a 的關系。也就是說每個子類對象都是一個父類對象。
組合是一種 has-a 的關系。假設B組合了A,每個B對象中都有一個A對象。
十三、多態
1. 多態的概念
多態 通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成某個行為時會產生出不同的狀態 。舉個栗子:比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票。
2. 多態的定義和實現
我們先實現一下多態,來嘗嘗鮮:
#include<iostream>
using namespace std;class Person
{
public:virtual void BuyTicket(){cout << "買票-全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "買票-半價" << endl;}
};// 多態
void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);// 子類可以賦值給父類---切片Func(st);return 0;
}
在繼承中想要構成多態是有條件的。
1. 必須通過父類的指針或者引用調用虛函數。
2. 被調用的函數必須是 虛函數 ,且子類必須對父類的虛函數進行重寫。
虛函數的重寫(覆蓋/隱藏):子類中有一個跟父類完全相同的虛函數(即子類虛函數與父類虛函數的 返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了父類的虛函數。(實際上父類的虛函數可以被子類繼承,所以只要父類寫上 virtual ,子類即使不寫 virtual 也能構成重寫)
關于重寫:重寫是重寫的 實現 ,僅僅會改變實現方式,聲明并不會改變 。
虛函數重寫的兩個特殊情況
協變
在虛函數重寫時,父類和子類的虛函數返回類型可以不同,但要求返回類型必須是父子類關系的指針和引用,則稱為 協變 。
#include<iostream>
using namespace std;class A {};
class B : public A {};class Person
{
public:// 虛函數重寫,返回類型是對應的指針或引用virtual A* f(){cout << "A::f()" << endl;return new A;}
};class Student : public Person
{
public:// 虛函數重寫,返回類型是對應的指針或引用virtual B* f(){cout << "B::f()" << endl;return new B;}
};int main()
{Person* p = new Student;p->f();return 0;
}
當返回類型是對應的指針或引用時成功實現多態,當返回類型不是時:
#include<iostream>
using namespace std;class A {};
class B : public A {};class Person
{
public:// 返回類型不同且不說相應的指針或引用virtual A f(){cout << "A::f()" << endl;return *new A;}
};class Student : public Person
{
public:// 返回類型不同且不說相應的指針或引用virtual B f(){cout << "B::f()" << endl;return *new B;}
};int main()
{Person* p = new Student;p->f();return 0;
}
析構函數的重寫
如果父類的析構函數為虛函數,此時子類析構函數只要定義,無論是否加 virtual 關鍵字,都與父類的析構函數構成重寫。原因是編譯器對析構函數的名稱做了特殊處理,編譯后所以析構函數的名稱統一處理成 destructor 。
當父類的析構函數不是虛函數時,如下情況則會:
#include<iostream>
using namespace std;class Person
{
public:~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:~Student(){cout << "~Student()" << endl;}
};int main()
{// 父類指針指向父類對象Person* p1 = new Person;// 父類指針指向子類對象Person* p2 = new Student;delete p1;cout << endl;delete p2;return 0;
}
沒能成功進行多態調用,訪問的還是父類的析構函數。當父類的析構函數是虛函數時:
#include<iostream>
using namespace std;class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;}
};class Student : public Person
{
public:// 子類可以不寫 virtual ,自動構成虛函數重寫~Student(){cout << "~Student()" << endl;}
};
// 只有派生類Student的析構函數重寫了Person的析構函數
//下面的delete對象調用析構函數,才能構成多態
//才能保證p1和p2指向的對象正確的調用析構函數
int main()
{// 父類指針指向父類對象Person* p1 = new Person;// 父類指針指向子類對象Person* p2 = new Student;delete p1;cout << endl;delete p2;return 0;
}
成功構成多態調用。我們怎么分辨 普通調用 和 多態調用 呢?
普通調用?看指針或引用或者對象的類型。
多態調用?看指針或引用指向的對象。
override 和 ?nal
如果我們想實現一個類,使其不能被繼承,應該怎么做?方法一:將父類的構造函數私有化,由于子類的構造函數必須調用父類的構造函數,所以父類的構造函數私有化會導致子類無法實例出對象。方法二:使用關鍵字 final 。
// 父類增加關鍵詞 final
class A final
{//
};class B : public A
{//
};
?nal 還可以修飾虛函數,表示該虛函數不能再被重寫。
class Car
{
public:virtual void Drive() final{//}
};class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒適" << endl;}
};
override 可以檢查子類虛函數是否重寫了父類某個虛函數,如果沒有重寫則編譯報錯。
class Car
{
public:void Drive(){//}
};class Benz :public Car
{
public:// override 寫在子類后面virtual void Drive() override{cout << "Benz-舒適" << endl;}
};
3. 多態的原理
1. 虛函數表
這里常考一道筆試題:sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{Base bb;cout << sizeof(Base) << endl;return 0;
}
答案是:8;原因是,int 占 4 個字節,而只要類里面有虛函數,類就會在內部 額外生成一個指針 ,指針指向函數指針數組,函數指針數組里存的都是虛函數的地址,稱為 虛函數表 。指針占 4 個字節,故答案是 8 。
對于上面的代碼,我們再進行改造一下:
#include<iostream>
using namespace std;class Base
{
public:// 虛函數virtual void Func1(){cout << "Base::Func1()" << endl;}// 虛函數virtual void Func2(){cout << "Base::Func2()" << endl;}// 普通函數void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:// 虛函數重寫virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}
我們發現,父類b對象和子類d對象虛函數表是不一樣的,這里我們發現Func1完成了重寫,所以d的虛函數表中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋,覆蓋就是指虛函數表中虛函數的覆蓋。b對象的虛函數表先拷貝一份父類的虛函數表,然后子類重寫的函數覆蓋進b對象的虛函數表。重寫是語法的叫法,覆蓋是原理層的叫法。Func3由于不是虛函數,所以沒有進入虛函數表。
運行時是通過本身的父類虛函數表或者切片的父類虛函數表(自己的)找到相應的虛函數,不同的對象虛函數表不同,因此實現多態。