C++ 虛函數與動態綁定原理剖析

C++ 虛函數與動態綁定

1. 虛函數基本概念

    基類(Base)我們記作 B ,派生類(Derive)我們記作 D 。有時候會出現這麼一種狀況:對於某些函數,基類 B 希望它的派生類 D 各自定義適合自身的版本,此時基類 B 就將這些函數聲明成虛函數(virtual function)

將一個成員函數聲明成虛函數,只需要在函數前添加 virtual 關鍵字。
Tips:
派生類必須在其內部對所有重新定義的虛函數進行聲明

2. 虛函數的幾點說明

  • 所有的虛函數都必須有定義
  • 派生類中對繼承來的虛函數提供自己的新定義,我們稱之爲 覆蓋
  • 如果基類把一個函數聲明成虛函數,則 該函數在派生類中隱式地也是虛函數

3. 動態綁定基本概念

    面向對象程序設計基於三個基本概念:數據抽象繼承動態綁定。此處的動態綁定是指:我們使用基類的引用或指針去調用虛函數,將會發生動態綁定即如果基類指針或引用指向的對象是基類對象,則調用基類的該函數如果基類的指針或引用指向的是派生類對象則調用的是派生類中的該函數

4. 動態綁定的原理剖析

    Why 基類對象的指針或引用可實現動態綁定? 這其中的原理是什麼呢?

每一個具有虛函數的類都叫做多態類,這個虛函數或者是從基類繼承來的,或者是自己增加的。C++ 編譯器必須爲每一個多態類至少創建一個【虛函數表(vtable)】,其本質是一個【函數指針數組】,其中存放着這個類所有的【虛函數的地址】及該類的類型信息,其中也包括那些【繼承但未改寫(Override)的虛函數】
————摘自:林銳博士《高質量程序設計指南第三版》

4.1 C++ 空類大小

#include <iostream>
using namespace std;

class A{};

int main(int argc, char const *argv[])
{
    cout << sizeof(A) << endl; //1
    return 0;
}

爲什麼 C++ 空類大小是 1 ?

C++ 不允許任何一個對象大小爲 0 ,因爲這樣無法爲該變量分配存儲空間。
當類爲空時,C++ 編譯器會向其中插入一個字節的數據,因此空類類型大小爲 1 字節。

4.2 C++ 非空類大小

#include <iostream>
using namespace std;

class A{
int m_a;
};

int main(int argc, char const *argv[])
{
    cout << sizeof(A) << endl; //4
    return 0;
}

就本例而言,因爲此時 A 含有一個 int 型成員變量,因此編譯器不會再給 A 類增加 1 個的數據,所以本例 A 的大小爲 4。
總體而言,一個不含虛函數的類,類的大小是【大於或等於】類內所有非靜態成員變量的總和。因爲存在內存對齊問題,因此可能會大於非靜態成員總和。

4.3 成員函數是否佔用類的大小?

#include <iostream>
using namespace std;

class A{
void fun(){}
int m_a;
};

int main(int argc, char const *argv[])
{
    cout << sizeof(A) << endl; //4
    return 0;
}

由本例可知,非虛成員函數,不佔用類的大小。

4.4 含虛函數的類的大小

#include <iostream>
using namespace std;

class A{
virtual void vfun(){}
};

int main(int argc, char const *argv[])
{
    
    cout << sizeof(A) << endl; //32 位機器 大小爲 4
    return 0;
}

本例中,類型 A 的大小爲 4 。此時類爲空類,按理講應該是 大小爲 1。而此時不爲 1 ,說明類中含有別的成員==> 此處正是 指向【虛函數表】的指針 【* __vptr】 所佔用的存儲空間。

4.5 含多個虛函數的類大小

#include <iostream>
using namespace std;

class A{
public:
	virtual void vfun1(){}
	virtual void vfunc2(){}
	virtual void vfunc3(){}
};

int main(int argc, char const *argv[])
{
    cout << sizeof(A) << endl; //32 位機器 大小爲 4
    return 0;
}

此處可以得知,虛函數不佔用類對象的存儲空間,所以含有一個以上的虛函數的類對象大小與僅含一個虛函數大小相同。因爲:針對每個類,只維護一個【虛函數表(函數指針數組數組)】用於存放該類中虛函數的地址,每個【含一個及以上虛函數的對象都會含有一個指向該類虛函數表的指針】

4.6 非虛函數例子

#include <iostream>
using namespace std;

class B{
public:
	void fun(){
		cout << "B func" << endl;
	}
};
class D:public B{
public:
	void func(){
		cout << "D func" << endl;
	}
};

int main(int argc, char const *argv[])
{
    B* b = new B;
    b->func();	// label1:	B func
    D* d = new D;
    d->func();	// label2:  D func
    B* pb = new D;
    pb->func();// label3:  B func
    return 0;
}
  • label1 處:
    基類指針指向基類對象,自然調用基類中的函數。
  • label2 處:
    派生類與基類的【函數同名時】,【子類會覆蓋掉父類所有的同名函數】。 因此此處調用的是派生類中的同名函數。
  • label3處:
    此時調用基類中的同名成員,因爲不存虛函數故而沒有動態綁定,在父類作用域下,自然調用父類的同名函數。

4.7 虛函數例子

#include <iostream>
using namespace std;

class B{
public:
	virtual void VFun(){
		cout << "B vFunc" << endl;
	}
};
class D:public B{
public:
	void vFunc(){
		cout << "D vFunc" << endl;
	}
};

int main(int argc, char const *argv[])
{
    B* b = new B;
    b->vFunc();	// label1:	B vFunc
    D* d = new D;
    d->vFunc();	// label2:  D vFunc
    B* pb = new D;
    pb->vFunc();// label3:  D vFunc
    return 0;
}
  • label1 處:基類指針指向基類對象,調用的是基類中的 vFunc
  • label2 處:
    當子類與父類擁有同名的成員函數,子類會隱藏父類中所有版本的同名成員函數。
  • label3 處:
    因爲 派生類【覆蓋】(重寫)了基類虛函數,給出了派生類的版本,此時 派生類中【虛函數表內 vFun 函數的指針替換爲派生類 vFun 的函數指針】,故而由基類實現了【動態綁定】調用了子類同名函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章