c++ 虛函數多態、純虛函數、虛函數表指針、虛基類表指針詳解


靜態多態、動態多態

靜態多態:程序在編譯階段就可以確定調用哪個函數。這種情況叫做靜態多態。比如重載,編譯器根據傳遞給函數的參數和函數名決定具體要使用哪一個函數。
動態多態:在運行期間纔可以確定最終調用的函數。需要通過虛函數+封裝+繼承實現。




虛函數

  • 虛函數都必須有定義
  • 虛函數一般用在繼承中。多個子類繼承同一基類,若在某種行爲上不同的派生類有着自己的實現方式。這種情況我們就會用到多態
    採用在基類中將此函數定義成虛函數,派生類中定義這個函數的不同實現。當我們使用基類的引用或指針調用一個虛成員函數時會執行動態綁定。因爲直到運行的時候才能知道到底調用了哪個版本的虛函數,判斷依據是根據引用或指針所綁定的對象的真實類型。
  • 若子類中重寫了父類的方法,但是父類中此方法並沒有設置爲虛函數。那麼通過指向子類的指針或引用調用此方法的時候,調用的是父類的方法
  • 基類中某個函數一旦被聲明爲虛函數,則在所有派生類中它都是虛函數,不需要在派生類中再一次通過virtual關鍵字指出該函數的性質。
  • 當且僅當通過指針或引用調用虛函數時,纔會在運行時解析該調用。



哪些函數類型不可以被定義成虛函數?

  • 內聯函數
  • 構造函數
  • 靜態成員函數:static成員函數是屬於類的。不屬於任何對象。
  • 友元函數:不支持繼承,沒有實現爲虛函數的必要
  • 賦值運算符:賦值運算符要求形參類型與本身類型相同,基類中賦值操作符形參爲基類類型,即使聲明成虛函數,也不能作爲子類的賦值操作符



內聯函數爲什麼不能被定義成虛函數?
內聯函數是爲了在代碼中直接展開,減少函數調用花費的代價。
inline 函數是在編譯時確定的,而 virtual 屬性是在運行時確定的,因此這個兩個屬性是不可能同時定義的。
即使虛函數被聲明爲內聯函數,編譯器遇到這種情況根本不會把這樣的函數內聯展開,而是當作普通函數來處理。



構造函數爲什麼不能被定義成虛函數?
如下:
1:繼承情況下,構造函數的執行順序時:A() B(),先執行父類的構造函數,在執行子類的構造函數
2:如果A的構造函數是虛函數,B類也定義了構造函數(即也爲虛函數),則只會執行子類的構造函數。即只會執行B類的構造函數,不會再執行A類的構造函數,這樣的話父類A就不能構造了
這樣的話1和2就發生了矛盾。並且 virtual 函數是在不同類型的對象產生不同的動作,現在對象還沒產生,是不存在通過virtual實現不同動作的想法的。

class A
{
	A() {}
};

class B : public A
{
	B() : A() {}
};

int main()
{
	B b;
	B * pb = &b;
}



虛函數的訪問方式

  • 對象名: 通過對象名訪問虛函數的時候,此時採用的是靜態聯編。調用哪個類的函數取決於定義對象名的類型。對象類型是基類時,就調用基類的函數;對象類型是子類時,就調用子類的函數。
  • 指針: 通過指針訪問虛函數的時候,編譯器根據指針所指對象的類型來決定要調用哪個函數(動態聯編),而與指針本身的類型無關
  • 引用:指針訪問虛函數類似。不同之處在於,引用一經聲明後,引用變量本身無論如何改變,其調用的函數就不會在改變,始終指向其開始定義時的函數。引用在一定程度上提高了代碼的安全性,可以將引用理解爲一種“受限制的指針”。



析構函數中的虛函數

如 B:A,A是基類,B是子類
構造子類對象時,先運行基類構造函數初始化基類部分,再執行子類的構造函數初始化子類部分。在執行基類構造函數時,派生部分還是未初始化的。實際上,此時對象還不是一個派生類對象。即先 A 後 B
撤銷子類對象時,首先撤銷子類部分,然後按照與構造順序的逆序撤銷它的基類部分。即先B 後 A

下面看一個例子:

#include <iostream>
using namespace std;


class Father
{
    public:
        Father()
        {
            cout << "Father Constructor" << endl;
        }
        void calcMethod()
        {
            cout << "Father calcMethod()" << endl;
        }
        virtual void virtualMethod()
        {
            cout << "Father virtualMethod()" << endl;
        }
		virtual void virtualCommon()
        {
            cout << "Father virtualCommon()" << endl;
        }
        ~Father()
        {
            cout << "Father disConstruct" << endl;
        }
};

class Son:public Father
{
    public:
        Son()
        {
            cout << "Son Constructor" << endl;
        }
        void calcMethod()
        {
            cout << "Son calcMethod()" << endl;
        }
        void virtualMethod()
        {
            cout << "Son virtualMethod()" << endl;
        }
        ~Son()
        {
            cout << "Son disConstruct" << endl;
        }
};


int main()
{
    Father *f = new Son();          //先執行father構造函數,在執行son構造函數
    f->calcMethod();                //Father calcMethod()。父,子--->父。如果父類中的方法有自己的實現,則會去調用父類的方法。  見上述第3條。
    f->virtualMethod();             //Son virtualMethod()。父虛,子虛--->子。若把父類中的方法定義成虛函數,子類中有自己的實現,則會去調用指向的子類中對應的方法。  見上述第2條。
	f->virtualCommon();				//Father virtualCommon()。父虛,子無--->父。若把父類中的方法定義成虛函數,子類中有沒有覆蓋這個虛函數,則會直接調用父類的虛函數。
	
    delete f;
    return 0;
}

控制檯打印:

Father Constructor
Son Constructor
Father calcMethod()
Son virtualMethod()
Father disConstruct

可以發現調用 delete 的時候只執行了父類的析構函數,沒有執行子類的析構函數。因爲父類的析構函數不是虛函數,這不是造成了內存泄漏?怎麼解決這個問題呢?



虛析構函數

delete後面跟父類指針,則只會執行父類的析構函數。
delete後面跟子類指針,那麼即會執行子類的析構函數,也會執行父類的析構函數。

可以通過在基類中把析構函數定義成虛函數來解決這個問題。因爲若不定義成虛函數,通過指向子類的指針或引用調用delete的時候會默認執行父類的析構函數(可參考上述虛函數介紹的第3條),而不會去執行子類的析構函數。

#include <iostream>
using namespace std;


class Father
{
	public:
		Father()
		{
			cout << "Father Constructor" << endl;
		}
		virtual ~Father()
		{
			cout << "Father Destruct" << endl;
		}
};

class Son:public Father
{
	public:
		Son()
		{
			cout << "Son Constructor" << endl;
		}
		~Son()
		{
			cout << "Son Destruct" << endl;
		}
	
};


int main()
{
	Father *f = new Son();			
	delete f;
	
	cout << "--------------" << endl;
	
	Son *s = new Son();
	delete s;
	
	return 0;
}

控制檯打印

Father Constructor
Son Constructor
Son Destruct
Father Destruct
--------------
Father Constructor
Son Constructor
Son Destruct
Father Destruct



虛函數表指針 vptr

class Base
{
	public:
		virtual void f() 
		{
			cout << "Base::f" << endl;
		}
		
		virtual void g() 
		{
			cout << "Base::g" << endl;
		}
			
		virtual void h() 
		{
			cout << "Base::h" << endl;
		}		
};


int main()
{
	// 函數指針
	typedef void (*Func) (void);
	
	Base b;
	
	cout << sizeof(b) << endl;				// B 函數只有一個虛函數表指針,即 4 
	cout << "虛函數表地址:" << (int*)(&b) << endl; 
	cout << "虛函數表-第一個函數地址:" << (int *)*(int *)(&b) << endl;
	
	Func pFunc = NULL;
	pFunc = (Func)*((int*)*(int*)(&b));			// 通過 Func * 把 int*強制轉換成函數指針 
	pFunc();
	
	return 0;
}

在這裏插入圖片描述

多繼承下的虛函數表

class A
{
	public:
		virtual void a() { cout << "a() in A" << endl; }	
		virtual void b() { cout << "b() in A" << endl; }	
		virtual void c() { cout << "c() in A" << endl; }	
		virtual void d() { cout << "d() in A" << endl; }	
};
 
class B : public A
{
	public:
		virtual void a() { cout << "a() in B" << endl; }	
		virtual void b() { cout << "b() in B" << endl; }	
};

class C : public A
{
	public:
		virtual void a() { cout << "a() in C" << endl; }	
		virtual void b() { cout << "b() in C" << endl; }	
};

class D : public B, public C
{
	public:
		virtual void a() { cout << "a() in D" << endl; }	
		virtual void d() { cout << "d() in D" << endl; }	
};

每個類的虛函數表結構如下:
在這裏插入圖片描述

B :public A 重寫了 a() b()
在這裏插入圖片描述

C :public A 重寫了 a() b()
在這裏插入圖片描述

D :public B , public C 重寫了 a() d()
在這裏插入圖片描述

可見,多繼承時,有幾個基類就有幾個vptr。D類中的函數 a與d 覆蓋了B類中的同名函數




虛基類表指針 bptr

菱形繼承即如下圖繼承方式:B、C 虛擬繼承A,D普通繼承B,C

B : virtual public A
C : virtual public A
D : public B, public C

在這裏插入圖片描述

虛擬繼承情況下,基類不管在繼承串鏈中被派生多少次,永遠只會存在一個實體
在虛擬繼承基類的子類中,子類會增加某種形式的指針,指向虛基類子對象或指向相關表格(表格中存放的不是虛基類子對象的地址,就是其偏移量),此指針被稱爲 bptr

菱形繼承時的對象佈局:
在這裏插入圖片描述

// 菱形繼承
class A {};
 
class B : virtual public A {};
class C : virtual public A {};

class D : public B, public C {};


int main()
{
	cout << "sizeof(A) : " << sizeof(A) << endl;	// 1 空類,編譯器會爲空類安插一個字節
	cout << "sizeof(B) : " << sizeof(B) << endl;	// 4 bptr指針
	cout << "sizeof(C) : " << sizeof(C) << endl;	// 4 bptr指針
	cout << "sizeof(D) : " << sizeof(D) << endl;	// 8 一個虛基類子對象只會在繼承類中存在一份實體,即A佔用1B(現在編譯器做了優化,可以爲0),0 + 4 + 4 = 8
}



純虛函數

純虛函數相當於定義了一個接口,不同的子類必須定義自己的實現。

#include <iostream>
using namespace std;


//抽象類
class Father
{
    public:
        virtual void calcMem() = 0;         //=0表示這是個純虛函數。純虛函數不需要定義,沒有方法體。
		virtual void anotherMethod() = 0;	//純虛函數,也可以定義。
};

void Father::anotherMethod()
{
	cout << "Father anotherMethod" << endl;
}

class Son:public Father
{
    public:
        virtual void calcMem()				//這裏的virtual也可以不顯示聲明。
        {
            cout << "son calcMem" << endl;
        }
		
		void anotherMethod()
		{
			cout << "Son anotherMethod" << endl;
		}
};

int main()
{
    Son *s = new Son();
    s->calcMem();				//son calcMem
	s->anotherMethod();			//Son anotherMethod

    Father *f = new Son();
    f->calcMem();				//son calcMem
	f->anotherMethod();			//Son anotherMethod
	f->Father::anotherMethod();	//Father anotherMethod。也可以顯示的調用父類的方法

   
    return 0;
}

控制檯打印:

son calcMem
Son anotherMethod
son calcMem
Son anotherMethod
Father anotherMethod



抽象類

抽象類不能聲明對象,只是作爲基類的派生類服務
抽象類不能定義對象,但是可以作爲指針或者引用類型使用

1:含有純虛函數的類成爲抽象類。
      除非派生類中完全實現基類中所有的純虛函數,否則,派生類也是抽象類,不能實例化對象。

2:只定義了protected型構造函數的類也是抽象類。因爲無論是在外部還是派生類中都不能創建該對象。但是可以由其派生出新的類。
      這種能派生出新類,但是不能創建自己對象的類時另一種形式的抽象類。

抽象類爲什麼不能實例化?
因爲抽象類中的純虛函數沒有具體的實現,所以沒辦法實例化。




虛函數和純虛函數的比較

(1)如果基類中定義了虛函數AF,派生類中對於這個虛函數可以覆蓋也可以不覆蓋。
派生類中如果覆蓋了這個虛函數AZ,則通過指向子類的指針或引用調用的就是派生類中AZ
如果派生類中沒有對這個AF進行覆蓋,那麼通過指向子類的指針或引用調用的就是基類中AF
(2)如果基類中定義了純虛函數AAF,相當於是個接口。那麼在派生類中就必須覆蓋基類的這個純虛函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章