目錄
- 多態
- 構造函數和析構函數存在多態嗎?
- 虛函數表
- 虛析構函數
- 純虛函數和抽象類
- 運行時多態和編譯時多態的區別
- 繼承
- 設計實例
- 指針對象和普通對象的區別
- 正確初始化派生類方式
- 繼承和賦值的兼容規則
- protected 和 private 繼承
- 基類與派生類的指針強制轉換
- 如何用C實現C++的三大特性
- C語言實現封裝性
- C語言實現繼承
- C語言實現多態
多態
在類的定義中,前面有 virtual 關鍵字的成員函數稱為虛函數;
virtual 關鍵字只用在類定義里的函數聲明中,寫函數體時不用。
「派生類的指針」可以賦給「基類指針」;
通過基類指針調用基類和派生類中的同名「虛函數」時:
若該指針指向一個基類的對象,那么被調用是 基類的虛函數;
若該指針指向一個派生類的對象,那么被調用 的是派生類的虛函數。
調用哪個虛函數,取決于指針對象指向哪種類型的對象。
派生類的對象可以賦給基類「引用」
通過基類引用調用基類和派生類中的同名「虛函數」時:
若該引用引用的是一個基類的對象,那么被調 用是基類的虛函數;
若該引用引用的是一個派生類的對象,那么被 調用的是派生類的虛函數。
調用哪個虛函數,取決于引用的對象是哪種類型的對象。
在面向對象的程序設計中使用「多態」,能夠增強程序的可擴充性,即程序需要修改或增加功能的時候,需要改動和增加的代碼較少。
this 指針的作用就是指向成員函數所作用的對象, 所以非靜態成員函數中可以直接使用 this 來代表指向該函數作用的對象的指針。
pBase 指針對象指向的是派生類對象,派生類里沒有 fun1 成員函數,所以就會調用基類的 fun1 成員函數,在Base::fun1() 成員函數體里執行 this->fun2() 時,實際上指向的是派生類對象的 fun2 成員函數。
構造函數和析構函數存在多態嗎?
在構造函數和析構函數中調用「虛函數」,不是多態。
一般不建議在構造函數或者析構函數中調用虛函數,因為在構造函數和析構函數中調用虛函數不會呈現多態性。
原因是啥呢?你想啊,在構造基類調用基類的構造函數時,派生類的部分還沒有構造,怎么可能能用虛函數實現動態綁定派生生類對象呢,所以構造B基類部分的時候,調用的基類的函數bar;
對于foo函數不是虛函數不會有動態綁定,所以調用的基類部分;
對于第三個bar調用,是虛函數,實現動態綁定,所以調用的是派生類部分。
同樣的道理,當調用繼承層次中某一層次的類的析構函數時,往往意味著其派生類部分已經析構掉,所以也不會呈現出多態。
編譯時即可確定,調用的函數是自己的類或基類中定義的函數,不會等到運行時才決定調用自己的還是派生類的函數。
虛函數表
每一個有「虛函數」的類(或有虛函數的類的派生類)都有一個「虛函數表」,該類的任何對象中都放著虛函數表的指針。「虛函數表」中列出了該類的「虛函數」地址。
可以發現有虛函數的類,多出了 8 個字節,在 64 位機子上指針類型大小正好是 8 個字節,多出來的 8 個字節就是用來放「虛函數表」的地址。
多態的函數調用語句被編譯成一系列根據基類指針所指向的(或基類引用所引用的)對象中存放的虛函數表的地址,在虛函數表中查找虛函數地址,并調用虛函數的指令。
虛函數表的指針」指向的是「虛函數表」,「虛函數表」里存放的是類里的「虛函數」地址,那么在調用過程中,就能實現多態的特性。
虛析構函數
析構函數是在刪除對象或退出程序的時候,自動調用的函數,其目的是做一些資源釋放。
那么在多態的情景下,通過基類的指針刪除派生類對象時,通常情況下只調用基類的析構函數,這就會存在派生類對象的析構函數沒有調用到,存在資源泄露的情況。
解決辦法:把基類的析構函數聲明為virtual
派生類的析構函數可以 virtual 不進行聲明;
通過基類的指針刪除派生類對象時,首先調用派生類的析構函數,然后調用基類的析構函數,還是遵循「先構造,后虛構」的規則。
所以要養成好習慣:
一個類如果定義了虛函數,則應該將析構函數也定義成虛函數;
或者,一個類打算作為基類使用,也應該將析構函數定義成虛函數。
注意:構造函數不能定義成虛構造函數
在構造基類調用基類的構造函數時,派生類的部分還沒有構造,怎么可能能用虛函數實現動態綁定派生生類對象呢,所以構造B基類部分的時候,調用的基類的函數bar;
純虛函數和抽象類
純虛函數:沒有函數體的虛函數。
包含純虛函數的類叫抽象類
抽象類只能作為基類來派生新類使用,不能創建抽象類的對象
抽象類的指針和引用可以指向由抽象類派生出來的類的對象
運行時多態和編譯時多態的區別
編譯時的多態,是指參數列表的不同, 來區分不同的函數, 在編譯后, 就自動變成兩個不同的函數。
運行時多態:用到的是后期綁定的技術, 在程序運行前不知道,會調用那個方法, 而到運行時, 通過運算程序,動態的算出被調用的地址. 運行時多態,也就是動態綁定,是指在執行期間(而非編譯期間)判斷所引用對象的實際類型,根據實際類型判斷并調用相應的屬性和方法
繼承
派生類是通過對基類進行修改和擴充得到的,在派生類中,可以擴充新的成員變量和成員函數。
派生類擁有基類的全部成員函數和成員變量,不論是private、protected、public。需要注意的是:在派生類的各個成員函數中,不能訪問基類的 private 成員。
在派生類對象中,包含著基類對象,而且基類對象的存儲位置位于派生類對象新增的成員變量之前,相當于基類對象是頭部。派生類對象的大小 = 基類對象成員變量的大小 + 派生類對象自己的成員變量的大小
設計實例
假設要寫一個小區養狗管理系統:
需要寫一個「主人」類。
需要些一個「狗」類。
假定狗只有一個主人,但是一個主人可以最多有 10 條狗,應該如何設計和使用「主人」類 和「狗」類呢?我們先看看下面幾個例子。
為狗類設一個主人類的對象指針;
為主人類設一個狗類的對象指針數組。
class CDog;
class CMaster // 主人類
{CDog * pDogs[10]; // 狗類的對象指針數組
};class CDog // 狗類
{CMaster * pm; // 主人類的對象指針
};
因為相當于狗和主人是獨立的,然后通過指針的作用,使得狗是可以指向一個主人,主人也可以同時指向屬于自己的 10 個狗,這樣會更靈活。
指針對象和普通對象的區別
如果不用指針對象,生成 A 對象的同時也會構造 B 對象。用指針就不會這樣,效率和內存都是有好處的。
class Car
{Engine engine; // 成員對象Wing * wing; // 成員指針對象
};
定義一輛汽車,所有的汽車都有 engine,但不一定都有 wing 這樣對于沒有 wing 的汽車,wing 只占一個指針,判斷起來也很方便。
空間上講,用指針可以節省空間,免于構造 B 對象,而是只在對象中開辟了一個指針,而不是開辟了一個對象 B 的大小。
效率上講,使用指針適合復用。對象 B 不但 A 對象能訪問,其他需要用它的對象也可以使用。
指針對象可以使用多態的特性,基類的指針可以指向派生鏈的任意一個派生類。
指針對象,需要用它的時候,才需要去實例化它,但是在不使用的時候,需要手動回收指針對象的資源。
正確初始化派生類方式
class Bug {
private :int nLegs; int nColor;
public:int nType;Bug (int legs, int color);void PrintBug (){ };
};Bug::Bug( int legs, int color)
{nLegs = legs;nColor = color;
}
class FlyBug : public Bug // FlyBug 是Bug 的派生類
{int nWings;
public:FlyBug( int legs,int color, int wings);
};
正確的FlyBug 構造函數:
通過調用基類構造函數來初始化基類,在執行一個派生類的構造函數 之前,總是先執行基類的構造函數。所以派生類析構時,會先執行派生類析構函數,再執行基類析構函數。
// 正確的FlyBug 構造函數:
FlyBug::FlyBug ( int legs, int color, int wings):Bug( legs, color)
{nWings = wings;
}
繼承和賦值的兼容規則
派生類的對象可以賦值給基類對象
派生類對象可以初始化基類引用
派生類對象的地址可以賦值給基類指針
如果派生方式是 private 或 protected,則上述三條不可行。
protected 和 private 繼承
protected 繼承時,基類的 public 成員和 protected 成員成為派生類的 protected 成員;
private 繼承時,基類的 public 成員成為派生類的 private 成員,基類的 protected 成員成 為派生類的不可訪問成員;
派生方式是 private 或 protected,則是無法像 public 派生承方式一樣把派生類對象賦值、引用、指針給基類對象。
基類與派生類的指針強制轉換
public 派生方式的情況下,派生類對象的指針可以直接賦值給基類指針:
Base *ptrBase = & objDerived;
ptrBase 指向的是一個 Derived 派生類(子類)的對象。
*ptrBase 可以看作一個 Base 基類的對象,訪問它的 public 成員直接通過 ptrBase 即可,但不能通過 ptrBase 訪問 。objDerived 對象中屬于 Derived 派生類而不屬于基類的成員。
通過強制指針類型轉換,可以把 ptrBase 轉換成 Derived 類的指針。
Base * ptrBase = &objDerived;
Derived *ptrDerived = ( Derived * ) ptrBase;
如何用C實現C++的三大特性
C語言實現封裝性
構體AchievePackage中有成員變量_a和兩個函數指針,在InitStruct函數中被賦予兩個函數的地址(函數名即為其地址,也可為&fun1,得到的值一樣),故在此處InitStruct函數相當于該結構體的構造函數,既可以初始化其成員變量_a的值,也在對象定義的同時為其函數指針賦值(需顯示調用)
C語言實現繼承
兩個類如果存在繼承關系,其子類必定具有父類的相關屬性(即變量)和方法(即函數)。
用“組合”去實現一下C語言中的繼承:
結構體嵌套:
C語言實現多態
多態是通過父類的指針或引用,調用了一個在父類中是virtual類型的函數,實現動態綁定機制。
若想使用父類的指針/引用調用子類的函數,需要在父類中將其聲明為虛函數(virtual),且必須與子類中的函數參數列表相同,返回值也相同。
C++的多態是通過覆蓋實現的,即父類的函數被子類覆蓋了!
父類的該函數為虛函數,告訴父類的指針/引用,你調用這個函數的時候必須看一看你綁定的對象到底是哪個類的對象,然后去那個類里調用該函數!
//C語言模擬C++的繼承與多態typedef void (*FUN)(); //定義一個函數指針來實現對成員函數的繼承struct _A //父類
{FUN _fun; //由于C語言中結構體不能包含函數,故只能用函數指針在外面實現int _a;
};struct _B //子類
{_A _a_; //在子類中定義一個基類的對象即可實現對父類的繼承int _b;
};void _fA() //父類的同名函數
{printf("_A:_fun()\n");
}
void _fB() //子類的同名函數
{printf("_B:_fun()\n");
}
下面是測試代碼:
//C語言模擬繼承與多態的測試
_A _a; //定義一個父類對象_a
_B _b; //定義一個子類對象_b
_a._fun = _fA; //父類的對象調用父類的同名函數
_b._a_._fun = _fB; //子類的對象調用子類的同名函數_A* p2 = &_a; //定義一個父類指針指向父類的對象
p2->_fun(); //調用父類的同名函數
p2 = (_A*)&_b; //讓父類指針指向子類的對象,由于類型不匹配所以要進行強轉
p2->_fun(); //調用子類的同名函數
效果: