1. 程式人生 > 程式設計 >C++ 基礎教程之虛擬函式例項程式碼詳解

C++ 基礎教程之虛擬函式例項程式碼詳解

虛擬函式的定義

虛擬函式:就是在基類的成員函式前加關鍵字virtual(即被virtual關鍵字修飾的成員函式),並在一個或多個派生類中被重新定義的成員函式;虛擬函式:就是在編譯的時候不確定要呼叫哪個函式,而是動態決定將要呼叫哪個函式。它的作用就是為了能讓這個函式在它的子類裡面可以被過載,這樣的話,編譯器就可以使用後期繫結來達到多型了,也就是用基類的指標來呼叫子類的這個函式;虛擬函式的作用:在於用專業術語來解釋就是實現多型性,多型性是將介面與實現進行分離,通過指向派生類的基類指標或引用,訪問派生類中同名覆蓋成員函式;用形象的語言來解釋就是實現以共同的方法,但因個體差異,而採用不同的策略;虛擬函式用法格式為:virtual 函式返回型別 函式名(引數表) {函式體};

虛擬函式在 c++ 的繼承體系中是一個非常重要概念,讓我們可以在子類中複寫父類的方法。學到這裡我還不知道在 c++ 中是否有抽象類的概念,那麼學習過虛擬函式我們就知道通過(純)虛擬函式可以實現 java 中的抽象類,

要實現虛擬函式需要兩個步驟進行修改

  • 在父類中,在函式GetName()前面加上virtual
  • 在子類中,在函式GetName()後面加上override

再次執行編譯執行程式就得到我們想要結果了

Shap rectangle

下面開始正文

virtual 函式

示例程式碼如下:

#include <stdio.h>
class base {
public:
 virtual void name(){printf("base\n");};
 virtual ~base(){};
};
class plus: public base {
public:
 virtual void name(){printf("plus\n");};
};
void fv(base b){
 b.name();
}
void fp(base &b){
 b.name();
}
int main(){
 base b;
 plus p;
 fv(b);
 fv(p);
 fp(b);
 fp(p);
 return 0;
}

程式輸出:

base base base plus

這裡涉及到一個c++知識點-- 向上強制轉換 :將派生類引用或指標轉換為基類引用或指標。該規則使得公有繼承不需要進行顯示型別轉化,它是is-a 規則的一部分。

相反的過程被稱為-- 向下強制轉換 ,向下強制型別轉換必須是顯示的。因為派生類可能對基類進行拓展,新增的成員變數和函式不能應用於基類。

隱式向上強制轉換使得基類指標或引用可以指向基類物件或派生類物件,因此需要 動態聯編 。C++ 使用虛成員函式函式滿足這種需求。

動態聯編

編譯器在編譯時要將呼叫的函式對應相應的可執行程式碼,此過程為 函式聯編(binding) ,在C++因為函式過載的原因,需要檢視呼叫函式名和傳入引數才能確認是哪一個函式。在編譯的時候可以確認使用哪一個函式的聯編被稱為 靜態聯編 或 早期聯編 。

同時因為virtual函式的存在,編譯工作變得更加複雜,如示例函式所示,具體使用的哪個型別物件不能確認。為此編譯器必須生成一些程式碼,使得在程式執行的時候選擇正確的虛擬函式,這被稱為 動態聯編 ,又被稱為 晚期聯編 。

為了驗證上面所述我們可以做一組對照,首先我們用 gnu 工具 nm 來檢視 sysbols,可以發現如下的部分:

$ nm virtual.exe | grep -c -E "plus|base"

然後我們改造一下上面的程式碼:

class base {
public:
 void name(){printf("base\n");}; // 修改
 virtual ~base(){};
};
class plus: public base {
public:
 void name(){printf("plus\n");}; // 修改
};

編譯後重新執行 nm 命令:

nm virtual_.exe | grep -c -E "plus|base" 45

經過比對後我們會發現修改後缺少了以下symbols:

000000000040509c p .pdata$_ZN4plus4nameEv 0000000000402e00 t .text$_ZN4plus4nameEv 00000000004060a0 r .xdata$_ZN4plus4nameEv 0000000000402e00 T _ZN4plus4nameEv

動態聯編在效率上要低於靜態聯編,在C++ 中預設使用靜態聯編。C++ 之父strousstup 認為 C++ 指導原則之一是不要為不使用的特性付出代價(cpu、memory等)。

所以在派生類不需要去重寫基類函式時,則不要將其宣告為virtual函式。

virtual 函式工作原理

虛擬函式表示每一個使用C++的開發者耳熟能詳的東西,有一個道經典的試題如下:

#include <stdio.h>
class base
{
public:
 base(){};
 virtual ~base() { printf("base\n"); };
};
class plus : public base
{
public:
 plus(/* args */){};
 virtual ~plus() { printf("plus\n"); };
};
class plus2 : public base
{
public:
 plus2(/* args */){};
 ~plus2() { printf("plus2\n"); };
};
class plus3 : public base
{
public:
 virtual void name() { printf("plus3"); };
 plus3(/* args */){};
 virtual ~plus3() { printf("plus3\n"); };
};
class empty
{
private:
 /* data */
public:
 empty(/* args */){};
 ~empty() { printf("empty\n"); };
};
int main()
{
 base b;
 printf("base: %d\n",sizeof(b));
 plus p;
 printf("plus: %d\n",sizeof(p));
 plus2 p2;
 printf("plus2: %d\n",sizeof(p2));
 plus3 p3;
 printf("plus3: %d\n",sizeof(p3));
 empty e;
 printf("empty: %d\n",sizeof(e));
}

其最終輸出的結果如下:

base: 8 plus: 8 plus2: 8 plus3: 8 empty: 1 empty plus3 base plus2 base plus base base

ps: 由於作業系統位數的影響結果可能有變動,在x64位系統中指標記憶體分配大小為 8 位元組,x86 系統中指標記憶體分配大小為 4。

我們可以清楚的看到,只要存在虛擬函式不論是成員函式異或是解構函式,是在類中定義或繼承都會有包含一個虛擬函式表。而這裡的8位元組就是分配給了虛擬函式表的指標。

我們可以通過gnu tool gdb 指令進行驗證,在觸發斷點之後通過 info local 命令去檢視:

(gdb) info locals
b = {_vptr.base = 0x555555755d20 <vtable for base+16>}
p = {<base> = {_vptr.base = 0x555555755d00 <vtable for plus+16>},<No data fields>}
p2 = {<base> = {_vptr.base = 0x555555755ce0 <vtable for plus2+16>},<No data fields>}
p3 = {<base> = {_vptr.base = 0x555555755cb8 <vtable for plus3+16>},<No data fields>}
e = {<No data fields>}

我們可以看到每一個物件內都有一個指標指向vtable。

當一個基類宣告一個虛擬函式後,在建立物件的時候會將該函式地址加入虛擬函式列表中,如果派生類重寫了該函式,則會用新函式地址替換,如果其定義了新函式,則會將新函式的指標加入虛表中。

示例程式碼如下:

#include <stdio.h>
class base
{
public:
 base(){};
 virtual const char* feature(){return "test";};
 virtual void name() {printf("base\n");}
 virtual ~base() { printf("~base\n"); };
};
class plus : public base
{
public:
 plus(/* args */){};
 virtual void name() {printf("plus\n");}
 virtual void parant() {printf("base\n");}
 ~plus() { printf("plus\n"); };
};
int main()
{
 base b;
 printf("base: %ld\n",size_t(&b));
 plus p;
 printf("plus: %ld\n",size_t(&p));
}

仍然用 gdb 來驗證,斷點後通過 info vtbl 命令檢視:

(gdb) info vtbl p
vtable for 'plus' @ 0x555555755d08 (subobject @ 0x7fffffffe010):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554bf8 <plus::name()>
[2]: 0x555555554c30 <plus::~plus()>
[3]: 0x555555554c66 <plus::~plus()>
[4]: 0x555555554c14 <plus::parant()>
(gdb) info vtbl b
vtable for 'base' @ 0x555555755d40 (subobject @ 0x7fffffffe008):
[0]: 0x555555554b4a <base::feature()>
[1]: 0x555555554b5c <base::name()>

當呼叫虛擬函式的時候,會在虛擬函式表中尋找對應的函式地址,因此它每一次呼叫動會多做一步匹配,相比靜態聯編的非虛擬函式要更加耗時。

需要注意的是建構函式不能宣告為虛擬函式,而如果一個類作為除非不作為基類,否則建議宣告一個虛解構函式。

總結

到此這篇關於C++ 基礎教程之虛擬函式例項程式碼詳解的文章就介紹到這了,更多相關c++ 虛擬函式內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!