虛擬函式,虛擬函式表

audience_fzn發表於2018-08-07

一、虛擬函式:

  • 虛擬函式——類成員函式前面加virtual關鍵字,則這個成員函式稱為虛擬函式
  • 虛擬函式重寫:當在子類裡定義了一個和父類完全相同的虛擬函式時,則稱子類的這個函式重寫(覆蓋)了父類的這個虛擬函式。
  • 重寫就是將子類裡面虛擬函式表裡的被重寫的父類函式地址全都改成了子類函式的地址

虛擬函式覆蓋(重寫)

class person
{
public:
	virtual void BuyTickets()
	{
		cout << "買票" << endl;
	}
};

class student :public person
{
public:
	virtual void BuyTickets()
	{
		cout << "買票——半價" << endl;
	}
};

總結:

  1. 派生類重寫基類的虛擬函式實現多型,要求函式名、引數列表、返回值完全相同(協變除外)
  2. 基類中定義了虛擬函式,在派生類中該函式始終保持虛擬函式的特性
  3. 只有類的成員函式才能定義為虛擬函式
  4. 如果在類外定義虛擬函式,只能在宣告函式時加virtual,類外定義函式時不能加virtual
  5. 靜態成員函式不能定義為虛擬函式
  6. 建構函式不能為虛擬函式,雖然可以將operator=定義為虛擬函式,薩博事故遭遇過不要將operator定義為虛擬函式,因為容易使用時一起混淆
  7. 不要在建構函式和解構函式裡呼叫虛擬函式,在建構函式和虛構函式中,物件時不完整的,可能發生未定義的行為
  8. 最好把基類的解構函式宣告為虛擬函式,why?(解構函式比較特殊,因為派生類的解構函式和基類的解構函式名稱不一樣,但是構成覆蓋,這裡是因為編譯器做了特殊處理)

純虛擬函式:

純虛擬函式:

  • 在成員函式的形參後面寫上=0,則成員函式為純虛擬函式。包含純虛擬函式的類叫做抽象類(也叫介面類)
  • 抽象類不能例項化出物件
  • 純虛擬函式在子類裡重新定義以後,子類才能例項化出物件
class person//不能例項化出物件
{
public:
	virtual void Display() = 0; //純虛擬函式
protected:
	string _name;
};

class student : public person
{
	virtual void Display();
};

 為什麼要引入抽象基類和純虛擬函式?

 

  • 為了方便使用多型
  • 在很多情況下,基類本身生成物件是不合理的
  • 如果程式函式在基類中沒有定義,必須在子類中加以實現
  • 如果一個基類含有一個或多個純虛擬函式,那麼他就屬於抽象基類,不能被例項化

二、虛擬函式表

  • 一個有虛擬函式的類
class Base
{
public:
	virtual void fun1()
	{}
	virtual void fun2()
	{}
private:
	int a;
};

void Test()
{
	Base b;
}

我們可以看到物件b裡面有一個_vfptr,這個_vfptr指向的內容就是虛擬函式表。無論是單繼承還是多繼承都會有這個虛擬函式表

 單繼承的虛擬函式表:

class Base
{
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}
private:
	int a;
};

class Derive :public Base
{
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Derive::fun4()" << endl;
	}
private:
	int b;
};

我們來看這個單繼承的虛擬函式表: 

我們預想的是fun1(),fun2(),fun3(),fun4() 的地址應該都在這個虛擬函式表中,但是為什麼這裡只有fun1(),fun2()的虛擬函式表呢?

因為編譯器在這裡做了一些處理,所以我們需要手動的去列印這些函式的地址

class Base
{
public:
	virtual void fun1()
	{
		cout << "Base::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base::fun2()" << endl;
	}
private:
	int a;
};

class Derive :public Base
{
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "Derive::fun4()" << endl;
	}
private:
	int b;
};

typedef void(*FUNC)(void);
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;
}

int main()
{
	Derive d;
	PrintVTable((int*)(*(int*)(&d)));
	system("pause");
	return 0;
}

執行後結果為下:

這裡就可以看出來,在Derive中重寫了Base中的fun1(), 虛擬函式表中就將的被重寫的父類函式地址全都改成了子類函式的地址。

多繼承的虛擬函式表

class Base1
{
public:
	virtual void fun1()
	{
		cout << "Base1::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base1::fun2()" << endl;
	}
private:
	int a;
};

class Base2
{
public:
	virtual void fun1()
	{
		cout << "Base2::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "Base2::fun2()" << endl;
	}
private:
	int b;
};

class Derive :public Base1, public Base2
{
public:
	virtual void fun1()
	{
		cout << "Derive::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "Derive::fun3()" << endl;
	}
private:
	int d;
};

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 Test()
{
	Derive d1;
	int* VTable = (int*)(*(int*)&d1);
	PrintVTable(VTable);
	int* VTable2 = (int*)(*(int*)&d1);
	PrintVTable(VTable2);
}

int main()
{
	Test();
	system("pause");
	return 0;
}

Dervie類中的fun3()在Base1的徐哈桑農戶表中,而Base1是先繼承的類

  • 子類的虛擬函式會存在自己先繼承的那個父類的虛擬函式表中
  • 子類如果重寫父類的虛擬函式,則父類虛擬函式表中父類的地址都換成了子類虛擬函式的地址

三、

1.為什麼靜態成員哈桑農戶不能定義為虛擬函式?

  • 因為靜態成員函式是所有物件共享的資源,但是這個靜態成員函式沒有this指標,而且虛擬函式只要物件才能呼叫,但是靜態成員函式不需要物件就可以呼叫,所以這裡有衝突

2.為什麼不要再建構函式和解構函式裡面呼叫虛擬函式?

  • 建構函式:在建構函式中,物件是不完整的,還沒有為“虛擬函式表”分配記憶體,所以這個呼叫也是違背先例項化,後呼叫的準則
  • 解構函式:一般解構函式先析構的是子類物件,而在父類中呼叫一個重寫的fun()函式,虛擬函式表裡存放的實際是子類的fun()函式,這時候子類也叫析構了,當你呼叫的時候就會呼叫不到

 

相關文章