您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“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++中虛函數的示例分析”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。