1. 程式人生 > >虛擬函式,純虛擬函式的解釋和內部實現&&虛擬函式表的真實樣子

虛擬函式,純虛擬函式的解釋和內部實現&&虛擬函式表的真實樣子

本篇文章由zg51747708曾廣 原創,未經允許不可以轉載

注:本文章內的程式程式碼全部是在Window 7 sp1  VS2015 Update3上測試

在學習C++中我總體感覺比較難理解的概念就是虛擬函式的理解,而且比較難想到他的內部實現。於是寫下這篇部落格,來幫助大家更深入的理解虛擬函式,純虛擬函式,虛擬函式表。希望大家帶著批判來閱讀,如有錯誤請私聊我,謝謝!

一.虛擬函式與純虛擬函式定義的解釋

首先我在網上發現有些人的解釋有明顯錯誤的地方。比如說“虛擬函式是沒有定義的成員函式”。這裡要說明的是,這應該是對於純虛擬函式而言。純虛擬函式就是沒有定義的。

1.      所以虛擬函式是有定義的。

虛擬函式定義的語法如下。

virtual void fun(){}

2.      虛擬函式的產生是為了能使得父類(基類)的指標,或者引用能夠呼叫到子類(派生類)的同名函式(實現多型)。

比如下列程式(這個是用指標的例子,引用的我就寫明瞭):

#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class Moveable {
public:
	virtual void move() {
		cout << "你有腿哦!" << endl;
	}
};
class Car : public Moveable {
public:
	void eat() {}
	void move() {
		cout << "能送四個騷貨哦!" << endl;
	}
};
class Bus : public Moveable {
public:
	void move() {
		cout << "能送一堆騷貨哦!" << endl;
	}
};

void Move(Moveable * p) {
	p->move();
}
int main() {
	Bus a;
	Car b;
	Moveable c;
	Move(&a);
	Move(&b);
	Move(&c);
	system("pause");
	return 0;
}

3.純虛擬函式才沒有定義。

純虛擬函式定義的語法如下。

virtual void fun() = 0;

虛擬函式表就是實現虛擬函式,純虛擬函式的內部方法。

---------------------------------------------------------------------------------------------------------------------------------

一.虛擬函式表的真實樣子

首先我們要知道虛擬函式表在記憶體中真正儲存的形式。可以用一下方式來測試:

#include <iostream>
#include <stdlib.h>
using namespace std;
class A {
public:
	virtual void a() {

	}
};
int main() {
	cout << sizeof(A) << endl;
	system("pause");
	return 0;
}

輸出的結果是:4

注:程式記憶體包含,程式碼段,資料段,堆段,棧段

這裡要說明的是,sizeof中並不包括成員函式所佔有的空間,因為函式存在於程式碼段,sizeof是檢測的物件佔有的記憶體空間而不包括函式。(最簡單的檢測方法是,你再在類中加一個函式,再看看sizeof返回的值,是否增加了。)

那麼問題來了,這個4個位元組是存放的什麼呢?答案就是虛擬函式表的記憶體地址。

那麼如果類中有資料成員,這個指標會在哪個位置呢?可以這樣分析,如果把這個指標放在後面。因為資料成員數量的不確定,那麼尋找這個指標將是比較費時間,而且相對而言難以實現的。而且在多重繼承或者更復雜的情況下,能夠訪問速度最大化。這樣的分析是正確的,虛擬函式表的記憶體地址就是存放在物件地址的開頭。

-------------------------------------------------------------------------------------

到這裡總結了兩點重要的,1.儲存的是虛擬函式表的記憶體地址,2.地址在物件記憶體區域的開頭。

-------------------------------------------------------------------------------------

那麼對應的虛擬函式的入口地址,在表中又是如何排列的呢?讓我們使用函式指標來測試虛擬函式在虛擬函式表中的位置吧。

1.  再沒有繼承關係時

//編譯環境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
	virtual void a(void) {
		cout << "A::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "A::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "A::c 函式發騷了" << endl;
	}
};
int main() {
	void (*fun)(void);//函式指標,其資料型別是void (*)(void)
	A A_demo;
	cout << &A_demo << endl;//輸出的是A_demo物件的首地址
	//(&A_demo)物件首地址
	//*((int *)&A_demo)虛擬函式表首地址
	//(*((int *)(*((int *)&A_demo))))虛擬函式表的第一個函式地址。
	fun = (void(*)(void))(*((int *)(*((int *)&A_demo))));
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)&A_demo)) + 1));
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)&A_demo)) + 2));
	fun();
	system("pause");
	return 0;
}

這裡能夠看出虛擬函式表內的排列是按照public下,函式的定義順序來的。

也就是


2.  在存在繼承關係而且有同名函式時。

class A {
public:
	virtual void a(void) {
		cout << "A::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "A::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "A::c 函式發騷了" << endl;
	}
};
class B : public A {
public:
	void a(void) {
		cout << "B::a 函式發騷了" << endl;
	}
};

虛擬函式表內的B::a函式地址覆蓋了本屬於A::a的位置。

3.  存在繼承關係,都是虛擬函式,但是沒有同名時。

//編譯環境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
	virtual void a(void) {
		cout << "A::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "A::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "A::c 函式發騷了" << endl;
	}
};
class B : public A {
public:
	virtual void ab(void) {
		cout << "B::a 函式發騷了" << endl;
	}
	virtual void bb(void) {
		cout << "B::b 函式發騷了" << endl;
	}
	virtual void cb(void) {
		cout << "B::c 函式發騷了" << endl;
	}
};
int main() {
	void (*fun)(void);//函式指標,其資料型別是void (*)(void)
	B B_demo;
	A * pA_demo = &B_demo;
	cout << pA_demo << "," << &B_demo << endl;//輸出的是A_demo物件的首地址
	cout << "pA_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)pA_demo) << endl;
	/*cout << "&B_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/

	//(&A_demo)物件首地址
	//*((int *)&A_demo)虛擬函式表首地址
	//(*((int *)(*((int *)&A_demo))))虛擬函式表的第一個函式地址。
	cout << "派生類的虛擬函式表" << endl;
	fun = (void(*)(void))(*((int *)(*((int *)&B_demo))));
	cout << (*((int *)(*((int *)pA_demo)))) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)&B_demo)) + 1));
	cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl;
	fun();
	
	fun = (void(*)(void))(*((int *)(*((int *)&B_demo)) + 2));
	cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl;
	fun();
	//-----------------------------------------------------------------
	cout << "基類的虛擬函式表" << endl;
	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3));
	cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4));
	cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5));
	cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl;
	fun();
	system("pause");
	return 0;
}

這種情況下:在原有的虛擬函式表上,前天三個入口是基類的虛擬函式入口地址。

後三個入口是派生類的虛擬函式入口地址。



1.  多重繼承,但是派生類中有虛擬函式過載的情況。

//編譯環境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
	virtual void a(void) {
		cout << "A::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "A::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "A::c 函式發騷了" << endl;
	}
};
class B {
public:
	virtual void a(void) {
		cout << "B::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "B::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "B::c 函式發騷了" << endl;
	}
};
class C :public A,public B{
public:
	virtual void ac(void) {
		cout << "C::a 函式發騷了" << endl;
	}
	virtual void bc(void) {
		cout << "C::b 函式發騷了" << endl;
	}
	virtual void cc(void) {
		cout << "C::c 函式發騷了" << endl;
	}
};

int main() {
	void(*fun)(void);//函式指標,其資料型別是void (*)(void)
	cout << sizeof(long long int) << endl;
	C C_demo;
	A * pA_demo = &C_demo;
	cout << pA_demo << "," << &C_demo << endl;//輸出的是A_demo物件的首地址
	cout << "pA_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)pA_demo) << endl;
	/*cout << "&B_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/
	//(&A_demo)物件首地址
	//*((int *)&A_demo)虛擬函式表首地址
	//(*((int *)(*((int *)&A_demo))))虛擬函式表的第一個函式地址。
	cout << "派生類的虛擬函式表" << endl;
	fun = (void(*)(void))(*((int *)(*((int *)pA_demo))));
	cout << (*((int *)(*((int *)pA_demo)))) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 1));
	cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 2));
	cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl;
	fun();
	//-----------------------------------------------------------------
	cout << endl;

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3));
	cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4));
	cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5));
	cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl;
	fun();

	cout << endl;


	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 0));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 0)) << endl;
	fun();
	
	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 1));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 1)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 2));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 2)) << endl;
	fun();


	cout << endl;
	/*fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 3));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 3)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 4));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 4)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 5));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 5)) << endl;
	fun();*/
	system("pause");
	return 0;
}

這裡這種C繼承了A和B的情況下,按照之前的測試方法測試了很久,在虛擬函式表中一直找不到B類的函式入口,我覺得既然繼承了,就應該存在吧,因為畢竟使用了virtual就應該在虛擬函式表裡(這就證明了B類的函式入口應該是在資料區),而且A和B並不存在繼承關係,因為我可以通過C_demo.B::a()這個還是能呼叫到類B的成員函式。

突然,想到會不會有兩張虛擬函式表呢?於是在物件的起始地址那裡向後偏移了4個位元組。發現,這裡也是一張虛擬函式表。

原來一個物件會建立兩張虛擬函式表,現在這種情況的排列形式如下圖。

那為什麼A和C的會在一張表裡面,而不是和B在一起呢?,繼續探知發現,是和C繼承AB時的順序有關。

改成這樣:class C :publicB,public A {

並且,基類指標改成B的B * pA_demo =&C_demo;(或者直接使用&C_demo是一樣的,同一地址)

結果就變成下面的圖的樣子。


①  問題又來了。如果還是上面那種多繼承,如果A和B類的函式名不相同呢?

沒錯,還是和上面一樣的結果,這裡就證明了,兩個基類的函式名稱相同並不會對編譯產生影響。

還有一個問題,上面多繼承A和B的情況下,記憶體中出現了兩張表,那麼虛擬函式表的個數是和基類個數有關嗎?

果然,在單層的多繼承的情況下,虛擬函式表的個數是和基類的個數相等的。

那麼在多重繼承下呢,虛擬函式表個數會和基類個數有什麼關係呢?繼續程式設計測試。(當然這裡測試時,不會使用同名,因為同名會實現派生類的函式入口覆蓋了虛擬函式表中基類的同名虛擬函式入口)這個就是C++裡的覆蓋,隱藏就是派生類的函式與基類同名,於是基類就隱藏了。需要使用::號去訪問。

測試結果如下圖,


結果顯示只有一張虛擬函式表,而且是以多重繼承的繼承順序排列下來。也就是A->B->C.用語言來描述就是在單純的多重繼承中,虛擬函式表的個數只有一個。並按照繼承的順序成員函式以線性排列在虛擬函式表中。

1.  如果是多繼承中派生類與兩個基類中的一個有過載的情況呢?

測試程式碼如下:

//編譯環境Windows 7 VS2015 Update3
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
class A {
public:
	virtual void a(void) {
		cout << "A::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "A::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "A::c 函式發騷了" << endl;
	}
};
class B {
public:
	virtual void ab(void) {
		cout << "B::a 函式發騷了" << endl;
	}
	virtual void bb(void) {
		cout << "B::b 函式發騷了" << endl;
	}
	virtual void cb(void) {
		cout << "B::c 函式發騷了" << endl;
	}
};
class C :public A, public B {
public:
	virtual void a(void) {
		cout << "C::a 函式發騷了" << endl;
	}
	virtual void b(void) {
		cout << "C::b 函式發騷了" << endl;
	}
	virtual void c(void) {
		cout << "C::c 函式發騷了" << endl;
	}
};

int main() {
	void(*fun)(void);//函式指標,其資料型別是void (*)(void)
	cout << sizeof(long long int) << endl;
	C C_demo;
	A * pA_demo = &C_demo;
	cout << pA_demo << "," << &C_demo << endl;//輸出的是A_demo物件的首地址
	cout << "pA_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)pA_demo) << endl;
	/*cout << "&B_demo指向的物件的虛擬函式表首地址: " << (int *)*((int *)&B_demo) + 1 << endl;*/
	//(&A_demo)物件首地址
	//*((int *)&A_demo)虛擬函式表首地址
	//(*((int *)(*((int *)&A_demo))))虛擬函式表的第一個函式地址。
	cout << "派生類的虛擬函式表" << endl;
	fun = (void(*)(void))(*((int *)(*((int *)pA_demo))));
	cout << (*((int *)(*((int *)pA_demo)))) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 1));
	cout << (*((int *)(*((int *)pA_demo)) + 1)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 2));
	cout << (*((int *)(*((int *)pA_demo)) + 2)) << endl;
	fun();
	//-----------------------------------------------------------------
	/*cout << endl;

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 3));
	cout << (*((int *)(*((int *)pA_demo)) + 3)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 4));
	cout << (*((int *)(*((int *)pA_demo)) + 4)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo)) + 5));
	cout << (*((int *)(*((int *)pA_demo)) + 5)) << endl;
	fun();*/

	cout << endl;


	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 0));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 0)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 1));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 1)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 2));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 2)) << endl;
	fun();


	cout << endl;
	/*fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 3));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 3)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 4));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 4)) << endl;
	fun();

	fun = (void(*)(void))(*((int *)(*((int *)pA_demo + 1)) + 5));
	cout << (*((int *)(*((int *)pA_demo + 1)) + 5)) << endl;
	fun();*/
	system("pause");
	return 0;
}

從結果看出,這裡A的虛擬函式的入口被在C類中過載的對應函式將入口覆蓋了。第二張表中是B的函式入口。

1.  如果是多繼承中派生類與兩個基類都有過載的情況呢?u

程式程式碼更改很簡單,直接把B類的函式名字改成和A類相同的。

結果是,A和B這兩個基類給派生類的虛擬函式表,都被派生類C的過載函式覆蓋,這個結果表明,不管有多少個基類存在虛擬函式在派生來中過載,相互之間是沒有任何影響的。

到這裡基本上所有情況我都涉及到了。如有漏情況,或者錯誤,請私聊我,謝謝,本人是菜鳥。可能程式碼寫的並不好,而且部落格書寫也也不好,請諒解。

部落格撰寫者:曾廣

2017/02/24