【C++】多型的理解
一.多型的概念
簡單的講就是同一事物在不同條件下所呈現出來的不同形態
舉例:火車站的同一視窗成人售票就是全價票,學生就是半價票。這就是同一事物,但是在不同的條件下可以呈現處不同的形態。有點見人說人話,見鬼說鬼話的意思。
二.多型的實現
#include<iostream> #include<Windows.h> using namespace std; class Person { public: virtual void Buy() { cout << "全價票" << endl; } private: }; class Student:public Person { public: virtual void Buy() { cout << "半價票" << endl; } private: }; void Test(Person& a) { a.Buy(); } int main() { Person p; Test(p); Student s; Test(s); system("pause"); return 0; }
從上邊的程式碼中我們很容易就可以看出,實現多型的必要的三個條件:
- 首先必須有繼承
- 在父類和子類中都要有虛擬函式(下邊會講解什麼是虛擬函式),並且子類和父類中的虛擬函式要構成重寫(後邊會解釋重寫的概念)
- 通過父類的指標或者引用來呼叫虛擬函式
注意:我們可以看出,普通呼叫和物件型別有關,但是多型的呼叫和具體物件有關
1.虛擬函式
虛擬函式的概念很簡單就是在函式前加virtual關鍵字
注意:只有非成員函式才可以宣告為虛擬函式
virtual void show()
{
cout << "ambition" << endl;
}
2.虛擬函式和純虛擬函式的對比
3.什麼函式不能宣告為虛擬函式??
- 建構函式
因為虛擬函式需要存放在虛表中,這時候虛表就需要有虛表指標。但是虛表指標是在呼叫建構函式的時候完成初始化的。但是又由於虛表是在程式編譯的時候生成的,所以如果將建構函式宣告為虛擬函式,即使生成了虛表,但是不能有虛表指標。虛表中的虛擬函式需要虛表指標來訪問。
- 靜態函式
因為沒有this指標,那麼將無法存入虛標中
- operator=
沒有意義,自身並不會構成重寫
- 行內函數
因為行內函數沒有地址
- 友元函式
因為友元函式不是成員函式
注意:只有非靜態的成員函式才可以宣告為虛擬函式
- 解構函式
注意:解構函式最好宣告為虛擬函式
因為在有些情況下可以保證析構的順序,防止記憶體洩漏。
例如:A* a = new B; delete a;
這種 情況下只是析構了a物件,但是並沒有析構B,只有宣告為虛擬函式,才可以保證兩者都被析構掉,並且保證了的呼叫解構函式的順序(先構造的後析構,後構造的先析構)(在繼承中子類會先呼叫父類的建構函式再去呼叫子類的建構函式)
4.重寫(覆蓋)
- 重寫是針對基類中虛擬函式的,在派生類中實現一個與基類虛擬函式原型(返回值型別、函式名字、引數列表)相同的虛 函式,即派生類與基類中虛擬函式的原型完全相同,才稱之為對基類虛擬函式的重寫。
- 構成重寫的條件
1.)首先必須得有繼承
2.)父類和子類中都必須有虛擬函式
3.)父類和子類中的虛擬函式必須是函式名相同,引數列表相同,返回值相同
- 構成重寫的兩個例外:
1.)協變
父類中虛擬函式返回父類物件的指標或引用,子類與父類同名虛擬函式返回子類物件的指標或引用,此種情 況也構成重寫,但是此時派生類與基類虛擬函式返回值型別不同。
2.)解構函式
父類中的解構函式如果是虛擬函式,只要子類的解構函式顯式提供,就構成重寫,此種情況派生類與基類虛 函式函式名字不同。
- 注意:在夠成重寫時父類的虛擬函式必須有virtual關鍵字,子類中的可以構成重寫的函式可加可不加,建議最好加上
5.針對重寫常見的面試問題
過載,重寫(覆蓋),重定義(隱藏)三者的區別:
- 過載
1.)必須在同一作用域(同一個類中)
2.)函式名相同,引數列表不同(引數個數,順序,型別)
3.) 返回值可同,可不同
- 重寫(覆蓋)(重寫大多出現在實現在多套中)
1.)必須有繼承(最好是公有繼承)
2.)父類和子類中必須有虛擬函式
3.)父,子兩類中的虛擬函式必須是函式名相同,引數列表相同,返回值相同
注意:我們可以理解重寫就是特殊的重定義
- 重定義(隱藏)(出現在繼承中)
1.)必須有繼承(最好是公有繼承)
2.)父,子兩類中有函式名相同的函式
5.動態繫結(動態聯編)和靜態繫結(靜態聯編)
- 靜態繫結
程式在編譯的時候進行確認地址
- 動態繫結
程式在執行的時候進行確認地址
- 注意:通常虛擬函式是動態繫結 ,普通函式是靜態繫結,預設引數值也是靜態繫結
為什麼有兩種型別聯編???既然動態聯編如此之好,為什麼不預設就是動態聯編???
有兩大原因:
- 效率
為了使程式在執行階段進行決策,必須採用一些方法來跟蹤基類指標或引用只想的物件型別,這增加了額外的開銷處理。例如,如果不用作基類,則就不需要動態聯編,同樣如果子類中沒有重定義基類的虛擬函式則也不需要動態聯編。這種情況下使用靜態聯編可以提高效率。因為靜態聯編的效率高,所以將靜態聯編設定為預設的
- 概念模型
在設計類時,可能包含一些派生類沒有重新定義的成員函式。不將這些函式作為虛擬函式有兩方面的好處:首先效率高。其次,指出不要重新定義該函式。
三.帶有虛擬函式的類的剖析
所謂類的例項化就是在記憶體中分配一塊地址.(空類同樣可以被例項化),每個例項在記憶體中都有一個獨一無二的地址,為了達到這個目的,編譯器往往會給一個空類隱含的加一個位元組,這樣空類在例項化後在記憶體得到了獨一無二的地址.因為如果空類不隱含加一個位元組的話,則空類無所謂例項化了(因為類的例項化就是在記憶體中分配一塊地址。 繼承這個類後這個類大小就優化為0了。這就是所謂的空白基類最優化。
2.擁有虛擬函式的類的大小是多少???為什麼是四個位元組??
擁有虛擬函式的類的大小是四個位元組,這四個位元組用來存放虛擬函式指標。一般將存放 虛擬函式的位置稱作為虛擬函式表,簡稱虛表,將指物件前4個位元組指向虛表的指標稱為虛表指標。
class A
{
public:
virtual void show()
{}
};
class B
{
public:
};
int main()
{
A a;
B b;
cout << "A->" << sizeof(a) << endl;
cout << "B->" << sizeof(b) << endl;
return 0;
}
3.虛表存在於每個類中,子類和父類中都有一個虛表。子類中的虛表存放子類的虛擬函式,父類中的虛表存放父類的虛擬函式。他們互不干擾。接下來我們列印一次虛表:
class Base
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
private:
int a;
};
class Driver:public Base
{
public:
virtual void func1()
{
}
virtual void func2()
{
}
virtual void func3()
{
}
virtual void func4()
{
}
private:
int b;
};
typedef void(*VFUNC)();
void PrintVtable(int* vtable)
{
cout << "虛表" << vtable << endl;
for (int i = 0; vtable[i] != 0; ++i)
{
printf("vtable[%d]->%p \n", i, vtable[i]);
VFUNC f = (VFUNC)vtable[i];
f();
}
cout << endl;
}
int main()
{
Base p;
Driver s;
PrintVtable((int*)(*((int*)&p)));
PrintVtable((int*)(*((int*)&s)));
system("pause");
return 0;
}
5.從上邊的圖我們可以看出,虛擬函式在虛表中的存放順序是按照虛擬函式在類中宣告的順序來存放的,這也就解釋了編譯器是如何呼叫在虛表中存放的虛擬函式的。
本文是根據《C++primer plus》和自己的理解整理所得!!!!具體可看《C++primer plus》的第13章
如果有什麼錯誤希望大家指出!!1