一、多態的概念
多態就是多種形態,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");
}
爲什麼子類和父類的函數名不一樣,還可以構成重寫呢?
因爲編譯器對析構函數的名字做了特殊處理,在內部函數名是一樣的。