C++多型的實現原理
一、多型的概念
多型就是多種形態,C++的多型分為靜態多型與動態多型。
靜態多型就是過載,因為在編譯期決議確定,所以稱為靜態多型。在編譯時就可以確定函式地址。
動態多型就是通過繼承重寫基類的虛擬函式實現的多型,因為實在執行時決議確定,所以稱為動態多型。執行時在虛擬函式表中尋找呼叫函式的地址。
在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。
如果物件型別是子類,就呼叫子類的函式;如果物件型別是父類,就呼叫父類的函式,(即指向父類調父類,指向子類調子類)此為多型的表現。
例:
class Person { public : virtual void BuyTickets() { cout<<" 買票"<< endl; } protected : string _name ; // 姓名 }; class Student : public Person { public : virtual void BuyTickets() { cout<<" 買票-半價 "<<endl ; } protected : int _num ; //學號 }; //void Fun(Person* p) void Fun (Person& p) { p.BuyTickets (); } void Test () { Person p ; Student s ; Fun(p ); Fun(s ); }
二、多型的實現原理
一個介面,多種方法
1. 用virtual關鍵字申明的函式叫做虛擬函式,虛擬函式肯定是類的成員函式。
2. 存在虛擬函式的類都有一個一維的虛擬函式表叫做虛表。當類中宣告虛擬函式時,編譯器會在類中生成一個虛擬函式表。
3. 類的物件有一個指向虛表開始的虛指標。虛表是和類對應的,虛表指標是和物件對應的。
4. 虛擬函式表是一個儲存類成員函式指標的資料結構。
5. 虛擬函式表是由編譯器自動生成與維護的。
6. virtual成員函式會被編譯器放入虛擬函式表中。
7. 當存在虛擬函式時,每個物件中都有一個指向虛擬函式的指標(C++編譯器給父類物件,子類物件提前佈局vptr指標),當進行test(parent *base)函式的時候,C++編譯器不需要區分子類或者父類物件,只需要再base指標中,找到vptr指標即可)。
8. vptr一般作為類物件的第一個成員。
三、探索虛表
虛表是通過一塊連續的記憶體來儲存虛擬函式的地址。這張表解決了繼承、虛擬函式(重寫)的問題。在有虛擬函式的物件例項中都存在這樣一張虛擬函式表,它就像一張地圖,指向了實際呼叫的虛擬函式。
例:
class Base
{
public :
virtual void func1()
{}
virtual void func2()
{}
private :
int a ;
};
void Test1 ()
{
Base b1;
}
- 單繼承物件模型
class Base
{
public :
virtual void func1()
{
cout<<"Base::func1" <<endl;
}
virtual void func2()
{
cout<<"Base::func2" <<endl;
}
private :
int a ;
};
class Derive :public Base
{
public :
virtual void func1()
{
cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
cout<<"Derive::func3" <<endl;
}
virtual void func4()
{
cout<<"Derive::func4" <<endl;
}
private :
int b ;
};
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
{
cout<<" 虛表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != 0; ++i)
{
printf(" 第%d個虛擬函式地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}
void Test1 ()
{
Base b1 ;
Derive d1 ;
int* VTable1 = (int*)(*( int*)&b1 );
int* VTable2 = (int*)(*( int*)&d1 );
PrintVTable(VTable1 );
PrintVTable(VTable2 );
}
可以看到派生類Derive::func1重寫基類Base::func1,覆蓋了相應虛表位置上的函式。 ps:可以看到這裡沒有看到派生類Derive中的func3和func4,這兩個函式就放在func2的後面,這裡沒有顯示是VS的問題(bug)。
- 多繼承物件模型
class Base1
{
public :
virtual void func1()
{
cout<<"Base1::func1" <<endl;
}
virtual void func2()
{
cout<<"Base1::func2" <<endl;
}
private :
int b1 ;
};
class Base2
{
public :
virtual void func1()
{
cout<<"Base2::func1" <<endl;
}
virtual void func2()
{
cout<<"Base2::func2" <<endl;
}
private :
int b2 ;
};
class Derive : public Base1, public Base2
{
public :
virtual void func1()
{
cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
cout<<"Derive::func3" <<endl;
}
private :
int d1 ;
};
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
{
cout<<" 虛表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != 0; ++i)
{
printf(" 第%d個虛擬函式地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}
void Test1 ()
{
Derive d1 ;
int* VTable = (int*)(*( int*)&d1 );
PrintVTable(VTable );
// Base2虛擬函式表在物件Base1後面
VTable = (int *)(*((int*)&d1 + sizeof (Base1)/4));
PrintVTable(VTable );
}
四、一些考題為什麼呼叫普通函式比呼叫虛擬函式的效率高?
因為普通函式是靜態聯編的,而呼叫虛擬函式是動態聯編的。
聯編的作用:程式呼叫函式,編譯器決定使用哪個可執行程式碼塊。
- 靜態聯編 :在編譯的時候就確定了函式的地址,然後call就呼叫了。
- 動態聯編 : 首先需要取到物件的首地址,然後再解引用取到虛擬函式表的首地址後,再加上偏移量才能找到要調的虛擬函式,然後call呼叫。
明顯動態聯編要比靜態聯編做的操作多,肯定就費時間。
為什麼要用虛擬函式表(存函式指標的陣列)?
- 實現多型,父類物件的指標指向父類物件呼叫的是父類的虛擬函式,指向子類呼叫的是子類的虛擬函式。
- 同一個類的多個物件的虛擬函式表是同一個,所以這樣就可以節省空間,一個類自己的虛擬函式和繼承的虛擬函式還有重寫父類的虛擬函式都會存在自己的虛擬函式表。
為什麼要把基類的解構函式定義為虛擬函式?
在用基類操作派生類時,為了防止執行基類的解構函式,不執行派生類的解構函式。因為這樣的刪除只能夠刪除基類物件, 而不能刪除子類物件, 形成了刪除一半形象, 會造成記憶體洩漏.如下程式碼:
#include<iostream>
using namespace std;
class Base
{
public:
Base() {};
~Base()
{
cout << "delete Base" << endl;
};
};
class Derived : public Base
{
public:
Derived() {};
~Derived()
{
cout << "delete Derived" << endl;
};
};
int main()
{
//操作1
Base* p1 = new Derived;
delete p1;
//因為這裡子類的解構函式重寫了父類的解構函式,雖然子類和父類的解構函式名不一樣,
//但是編譯器對解構函式做了特殊的處理,在內部子類和父類的解構函式名是一樣的。
//所以如果不把父類的解構函式定義成虛擬函式,就不構成多型,由於父類的解構函式隱藏了子類
//的解構函式,所以只能調到父類的解構函式。
//但是若把父類的解構函式定義成虛擬函式,那麼呼叫時就會直接呼叫子類的解構函式,
//由於子類析構先要去析構父類,在析構子類,這樣就把子類和繼承的父類都析構了
system("pause");
}
為什麼子類和父類的函式名不一樣,還可以構成重寫呢?因為編譯器對解構函式的名字做了特殊處理,在內部函式名是一樣的。ps.暫時就到這兒了