虛函式和多型
01 虛函式
- 在類的定義中,前面有
virtual關鍵字的成員函式稱為虛函式; virtual關鍵字只用在類定義里的函式宣告中,寫函式體時不用,
class Base
{
virtual int Fun() ; // 虛函式
};
int Base::Fun() // virtual 欄位不用在函式體時定義
{ }
02 多型的表現形式一
- 「派生類的指標」可以賦給「基類指標」;
- 通過基類指標呼叫基類和派生類中的同名「虛函式」時:
- 若該指標指向一個基類的物件,那么被呼叫是
基類的虛函式; - 若該指標指向一個派生類的物件,那么被呼叫
的是派生類的虛函式,
- 若該指標指向一個基類的物件,那么被呼叫是
這種機制就叫做“多型”,說白點就是呼叫哪個虛函式,取決于指標物件指向哪種型別的物件,
// 基類
class CFather
{
public:
virtual void Fun() { } // 虛函式
};
// 派生類
class CSon : public CFather
{
public :
virtual void Fun() { }
};
int main()
{
CSon son;
CFather *p = &son;
p->Fun(); //呼叫哪個虛函式取決于 p 指向哪種型別的物件
return 0;
}
上例子中的 p 指標物件指向的是 CSon 類物件,所以 p->Fun() 呼叫的是 CSon 類里的 Fun 成員函式,
03 多型的表現形式二
- 派生類的物件可以賦給基類「參考」
- 通過基類參考呼叫基類和派生類中的同名「虛函式」時:
- 若該參考參考的是一個基類的物件,那么被調
用是基類的虛函式; - 若該參考參考的是一個派生類的物件,那么被
呼叫的是派生類的虛函式,
- 若該參考參考的是一個基類的物件,那么被調
這種機制也叫做“多型”,說白點就是呼叫哪個虛函式,取決于參考的物件是哪種型別的物件,
// 基類
class CFather
{
public:
virtual void Fun() { } // 虛函式
};
// 派生類
class CSon : public CFather
{
public :
virtual void Fun() { }
};
int main()
{
CSon son;
CFather &r = son;
r.Fun(); //呼叫哪個虛函式取決于 r 參考哪種型別的物件
return 0;
}
}
上例子中的 r 參考的物件是 CSon 類物件,所以 r.Fun() 呼叫的是 CSon 類里的 Fun 成員函式,
04 多型的簡單示例
class A
{
public :
virtual void Print() { cout << "A::Print"<<endl ; }
};
// 繼承A類
class B: public A
{
public :
virtual void Print() { cout << "B::Print" <<endl; }
};
// 繼承A類
class D: public A
{
public:
virtual void Print() { cout << "D::Print" << endl ; }
};
// 繼承B類
class E: public B
{
virtual void Print() { cout << "E::Print" << endl ; }
};
A類、B類、E類、D類的關系如下圖:

int main()
{
A a; B b; E e; D d;
A * pa = &a;
B * pb = &b;
D * pd = &d;
E * pe = &e;
pa->Print(); // a.Print()被呼叫,輸出:A::Print
pa = pb;
pa -> Print(); // b.Print()被呼叫,輸出:B::Print
pa = pd;
pa -> Print(); // d.Print()被呼叫,輸出:D::Print
pa = pe;
pa -> Print(); // e.Print()被呼叫,輸出:E::Print
return 0;
}
05 多型作用
在面向物件的程式設計中使用「多型」,能夠增強程式的可擴充性,即程式需要修改或增加功能的時候,需要改動和增加的代碼較少,
LOL 英雄聯盟游戲例子
下面我們用設計 LOL 英雄聯盟游戲的英雄的例子,說明多型為什么可以在修改或增加功能的時候,可以較少的改動代碼,
LOL 英雄聯盟是 5v5 競技游戲,游戲中有很多英雄,每種英雄都有一個「類」與之對應,每個英雄就是一個「物件」,
英雄之間能夠互相攻擊,攻擊敵人和被攻擊時都有相應的動作,動作是通過物件的成員函式實作的,
下面挑了五個英雄:
- 探險家 CEzreal
- 蓋樓 CGaren
- 盲僧 CLeesin
- 無極劍圣 CYi
- 瑞茲 CRyze

基本思路:
- 為每個英雄類撰寫
Attack、FightBack和Hurted成員函式,
Attack函式表示攻擊動作;FightBack函式表示反擊動作;Hurted函式表示減少自身生命值,并表現受傷動作,
- 設定基類
CHero,每個英雄類都繼承此基類

02 非多型的實作方法
// 基類
class CHero
{
protected:
int m_nPower ; //代表攻擊力
int m_nLifeValue ; //代表生命值
};
// 無極劍圣類
class CYi : public CHero
{
public:
// 攻擊蓋倫的攻擊函式
void Attack(CGaren * pGaren)
{
.... // 表現攻擊動作的代碼
pGaren->Hurted(m_nPower);
pGaren->FightBack(this);
}
// 攻擊瑞茲的攻擊函式
void Attack(CRyze * pRyze)
{
.... // 表現攻擊動作的代碼
pRyze->Hurted(m_nPower);
pRyze->FightBack( this);
}
// 減少自身生命值
void Hurted(int nPower)
{
... // 表現受傷動作的代碼
m_nLifeValue -= nPower;
}
// 反擊蓋倫的反擊函式
void FightBack(CGaren * pGaren)
{
....// 表現反擊動作的代碼
pGaren->Hurted(m_nPower/2);
}
// 反擊瑞茲的反擊函式
void FightBack(CRyze * pRyze)
{
....// 表現反擊動作的代碼
pRyze->Hurted(m_nPower/2);
}
};
有 n 種英雄,CYi 類中就會有 n 個 Attack 成員函式,以及 n 個 FightBack
成員函式,對于其他類也如此,
如果游戲版本升級,增加了新的英雄寒冰艾希 CAshe,則程式改動較大,所有的類都需要增加兩個成員函式:
void Attack(CAshe * pAshe);
void FightBack(CAshe * pAshe);
這樣作業量是非常大的!!非常的不人性,所以這種設計方式是非常的不好!
03 多型的實作方式
用多型的方式去實作,就能得知多型的優勢了,那么上面的栗子改成多型的方式如下:
// 基類
class CHero
{
public:
virtual void Attack(CHero *pHero){}
virtual voidFightBack(CHero *pHero){}
virtual void Hurted(int nPower){}
protected:
int m_nPower ; //代表攻擊力
int m_nLifeValue ; //代表生命值
};
// 派生類 CYi:
class CYi : public CHero {
public:
// 攻擊函式
void Attack(CHero * pHero)
{
.... // 表現攻擊動作的代碼
pHero->Hurted(m_nPower); // 多型
pHero->FightBack(this); // 多型
}
// 減少自身生命值
void Hurted(int nPower)
{
... // 表現受傷動作的代碼
m_nLifeValue -= nPower;
}
// 反擊函式
void FightBack(CHero * pHero)
{
....// 表現反擊動作的代碼
pHero->Hurted(m_nPower/2); // 多型
}
};
如果增加了新的英雄寒冰艾希 CAshe,只需要撰寫新類CAshe,不再需要在已有的類里專門為新英雄增加:
void Attack( CAshe * pAshe) ;
void FightBack(CAshe * pAshe) ;
所以已有的類可以原封不動,那么使用多型的特性新增英雄的時候,可見改動量是非常少的,
多型使用方式:
void CYi::Attack(CHero * pHero)
{
pHero->Hurted(m_nPower); // 多型
pHero->FightBack(this); // 多型
}
CYi yi;
CGaren garen;
CLeesin leesin;
CEzreal ezreal;
yi.Attack( &garen ); //(1)
yi.Attack( &leesin ); //(2)
yi.Attack( &ezreal ); //(3)
根據多型的規則,上面的(1),(2),(3)進入到 CYi::Attack 函式后
,分別呼叫:
CGaren::Hurted
CLeesin::Hurted
CEzreal::Hurted
多型的又一例子
出一道題考考大家,看大家是否理解到了多型的特性,下面的代碼,pBase->fun1()輸出結果是什么呢?
class Base
{
public:
void fun1()
{
fun2();
}
virtual void fun2() // 虛函式
{
cout << "Base::fun2()" << endl;
}
};
class Derived : public Base
{
public:
virtual void fun2() // 虛函式
{
cout << "Derived:fun2()" << endl;
}
};
int main()
{
Derived d;
Base * pBase = & d;
pBase->fun1();
return 0;
}
是不是大家覺得 pBase 指標物件雖然指向的是派生類物件,但是派生類里沒有 fun1 成員函式,則就呼叫基類的 fun1 成員函式,Base::fun1() 里又會呼叫基類的 fun2 成員函式,所以輸出結果是Base::fun2() ?
假設我把上面的代碼轉換一下, 大家還覺得輸出的是 Base::fun2() 嗎?
class Base
{
public:
void fun1()
{
this->fun2(); // this是基類指標,fun2是虛函式,所以是多型
}
}
this 指標的作用就是指向成員函式所作用的物件, 所以非靜態成員函式中可以直接使用 this 來代表指向該函式作用的物件的指標,
pBase 指標物件指向的是派生類物件,派生類里沒有 fun1 成員函式,所以就會呼叫基類的 fun1 成員函式,在Base::fun1() 成員函式體里執行 this->fun2() 時,實際上指向的是派生類物件的 fun2 成員函式,
所以正確的輸出結果是:
Derived:fun2()
所以我們需要注意:
在非建構式,非解構式的成員函式中呼叫「虛函式」,是多型!!!
建構式和解構式中存在多型嗎?
在建構式和解構式中呼叫「虛函式」,不是多型,編譯時即可確定,呼叫的函式是自己的類或基類中定義的函式,不會等到運行時才決定呼叫自己的還是派生類的函式,
我們看如下的代碼例子,來說明:
// 基類
class CFather
{
public:
virtual void hello() // 虛函式
{
cout<<"hello from father"<<endl;
}
virtual void bye() // 虛函式
{
cout<<"bye from father"<<endl;
}
};
// 派生類
class CSon : public CFather
{
public:
CSon() // 建構式
{
hello();
}
~CSon() // 解構式
{
bye();
}
virtual void hello() // 虛函式
{
cout<<"hello from son"<<endl;
}
};
int main()
{
CSon son;
CFather *pfather;
pfather = & son;
pfather->hello(); //多型
return 0;
}
輸出結果:
hello from son // 構造son物件時執行的建構式
hello from son // 多型
bye from father // son物件析構時,由于CSon類沒有bye成員函式,所以呼叫了基類的bye成員函式
多型的實作原理
「多型」的關鍵在于通過基類指標或參考呼叫一個虛函式時,編譯時不能確定到底呼叫的是基類還是派生類的函式,運行時才能確定,
我們用 sizeof 來運算有有虛函式的類和沒虛函式的類的大小,會是什么結果呢?
class A
{
public:
int i;
virtual void Print() { } // 虛函式
};
class B
{
public:
int n;
void Print() { }
};
int main()
{
cout << sizeof(A) << ","<< sizeof(B);
return 0;
}
在64位機子,執行的結果:
16,4
從上面的結果,可以發現有虛函式的類,多出了 8 個位元組,在 64 位機子上指標型別大小正好是 8 個位元組,這多出 8 個位元組的指標有什么作用呢?
01 虛函式表
每一個有「虛函式」的類(或有虛函式的類的派生類)都有一個「虛函式表」,該類的任何物件中都放著虛函式表的指標,「虛函式表」中列出了該類的「虛函式」地址,
多出來的 8 個位元組就是用來放「虛函式表」的地址,
// 基類
class Base
{
public:
int i;
virtual void Print() { } // 虛函式
};
// 派生類
class Derived : public Base
{
public:
int n;
virtual void Print() { } // 虛函式
};
上面 Derived 類繼承了 Base類,兩個類都有「虛函式」,那么它「虛函式表」的形式可以理解成下圖:

多型的函式呼叫陳述句被編譯成一系列根據基類指標所指向的(或基類參考所參考的)物件中存放的虛函式表的地址,在虛函式表中查找虛函式地址,并呼叫虛函式的指令,
02 證明虛函式表指標的作用
在上面我們用 sizeof 運算子計算了有虛函式的類的大小,發現是多出了 8 位元組大小(64位系統),這多出來的 8 個位元組就是指向「虛函式表的指標」,「虛函式表」中列出了該類的「虛函式」地址,
下面用代碼的例子,來證明「虛函式表指標」的作用:
// 基類
class A
{
public:
virtual void Func() // 虛函式
{
cout << "A::Func" << endl;
}
};
// 派生類
class B : public A
{
public:
virtual void Func() // 虛函式
{
cout << "B::Func" << endl;
}
};
int main()
{
A a;
A * pa = new B();
pa->Func(); // 多型
// 64位程式指標為8位元組
int * p1 = (int *) & a;
int * p2 = (int *) pa;
* p2 = * p1;
pa->Func();
return 0;
}
輸出結果:
B::Func
A::Func
- 第 25-26 行代碼中的
pa指標指向的是B類物件,所以pa->Func()呼叫的是B類物件的虛函式Func(),輸出內容是B::Func; - 第 29-30 行代碼的目的是把
A類的頭 8 個位元組的「虛函式表指標」存放到p1指標和把B類的頭 8 個位元組的「虛函式表指標」存放到p2指標; - 第 32 行代碼目的是把
A類的「虛函式表指標」 賦值給B類的「虛函式表指標」,所以相當于把B類的「虛函式表指標」 替換 成了A類的「虛函式表指標」; - 由于第 32 行的作用,把
B類的「虛函式表指標」 替換 成了A類的「虛函式表指標」,所以第 33 行呼叫的是A類的虛函式Func(),輸出內容是A::Func
通過上述的代碼和講解,可以有效的證明了「虛函式表的指標」的作用,「虛函式表的指標」指向的是「虛函式表」,「虛函式表」里存放的是類里的「虛函式」地址,那么在呼叫程序中,就能實作多型的特性,
虛解構式
解構式是在洗掉物件或退出程式的時候,自動呼叫的函式,其目的是做一些資源釋放,
那么在多型的情景下,通過基類的指標洗掉派生類物件時,通常情況下只呼叫基類的解構式,這就會存在派生類物件的解構式沒有呼叫到,存在資源泄露的情況,
看如下的例子:
// 基類
class A
{
public:
A() // 建構式
{
cout << "construct A" << endl;
}
~A() // 解構式
{
cout << "Destructor A" << endl;
}
};
// 派生類
class B : public A
{
public:
B() // 建構式
{
cout << "construct B" << endl;
}
~B()// 解構式
{
cout << "Destructor B" << endl;
}
};
int main()
{
A *pa = new B();
delete pa;
return 0;
}
輸出結果:
construct A
construct B
Destructor A
從上面的輸出結果可以看到,在洗掉 pa指標物件時,B 類的解構式沒有被呼叫,
解決辦法:把基類的解構式宣告為virtual
- 派生類的解構式可以 virtual 不進行宣告;
- 通過基類的指標洗掉派生類物件時,首先呼叫派生類的解構式,然后呼叫基類的解構式,還是遵循「先構造,后虛構」的規則,
將上述的代碼中的基類的解構式,定義成「虛解構式」:
// 基類
class A
{
public:
A()
{
cout << "construct A" << endl;
}
virtual ~A() // 虛解構式
{
cout << "Destructor A" << endl;
}
};
輸出結果:
construct A
construct B
Destructor B
Destructor A
所以要養成好習慣:
- 一個類如果定義了虛函式,則應該將解構式也定義成虛函式;
- 或者,一個類打算作為基類使用,也應該將解構式定義成虛函式,
- 注意:不允許建構式不能定義成虛建構式,
純虛函式和抽象類
純虛函式: 沒有函式體的虛函式
class A
{
public:
virtual void Print( ) = 0 ; //純虛函式
private:
int a;
};
包含純虛函式的類叫抽象類
- 抽象類只能作為基類來派生新類使用,不能創建抽象類的物件
- 抽象類的指標和參考可以指向由抽象類派生出來的類的物件
A a; // 錯,A 是抽象類,不能創建物件
A * pa ; // ok,可以定義抽象類的指標和參考
pa = new A ; // 錯誤, A 是抽象類,不能創建物件
推薦閱讀:
C++ this指標的理解和作用
C++ 一篇搞懂繼承的常見特性
C++ 賦值運算子'='的多載(淺拷貝、深拷貝)
C++ 手把手教你實作可變長的陣列

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/69137.html
標籤:C++
上一篇:判斷字串A中是否含有另一個字串B,其中字串A可以不連續
下一篇:ffmpeg
