在C++中用虛函數的作用是什么? 為什么要用到虛函數?

***************************************************

更多精彩,歡迎進入:http://shop115376623.taobao.com

***************************************************

虛函數聯系到多態,多態聯系到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什么都沒得談。
下面是對C++的虛函數這玩意兒的理解。一,  什么是虛函數(如果不知道虛函數為何物,但有急切的想知道,那你就應該從這里開始)
簡單地說,那些被virtual關鍵字修飾的成員函數,就是虛函數。虛函數的作用,用專業術語來解釋就是實現多態性(Polymorphism),多態性是將接口與實現進行分離;用形象的語言來解釋就是實現以共同的方法,但因個體差異而采用不同的策略。下面來看一段簡單的代碼class A{
public:
void print(){ cout<<”This is A”<<endl;}
};class B:public A{
public:
void print(){ cout<<”This is B”<<endl;}
};int main(){   //為了在以后便于區分,我這段main()代碼叫做main1A a;B b;a.print();b.print();}通過class A和class B的print()這個接口,可以看出這兩個class因個體的差異而采用了不同的策略,輸出的結果也是我們預料中的,分別是This is A和This is B。但這是否真正做到了多態性呢?No,多態還有個關鍵之處就是一切用指向基類的指針或引用來操作對象。那現在就把main()處的代碼改一改。int main(){   //main2
A a;
B b;A* p1=&a;
A* p2=&b;//定義為虛函數后,可以通過基類指針或引用來訪問基類和派生類中的同名函數p1->print();
p2->print();
}運行一下看看結果,喲呵,驀然回首,結果卻是兩個This is A。問題來了,p2明明指向的是class B的對象但卻是調用的class A的print()函數,這不是我們所期望的結果,那么解決這個問題就需要用到虛函數class A{
public:virtual void print(){ cout<<”This is A”<<endl;}  //現在成了虛函數了
//說明:虛函數的作用是允許在派生類中重新定義與基類同名的函數,并且可以通過基類指針或引用來訪問基類和派生類中的同名函數};class B:public A{public:void print(){ cout<<”This is B”<<endl;}  //這里需要在前面加上關鍵字virtual嗎?};毫無疑問,class A的成員函數print()已經成了虛函數,那么class B的print()成了虛函數了嗎?回答是Yes,我們只需在把基類的成員函數設為virtual,其派生類的相應的函數也會自動變為虛函數。所以,class B的print()也成了虛函數。那么對于在派生類的相應函數前是否需要用virtual關鍵字修飾,那就是你自己的問題了。現在重新運行main2的代碼,這樣輸出的結果就是This is A和This is B了。現在來消化一下,我作個簡單的總結,指向基類的指針在操作它的多態類對象時,會根據不同的類對象,調用其相應的函數,這個函數就是虛函數。二,  虛函數是如何做到的(如果你沒有看過《Inside The C++ Object Model》這本書,但又急切想知道,那你就應該從這里開始)虛函數是如何做到因對象的不同而調用其相應的函數的呢?現在我們就來剖析虛函數。我們先定義兩個類class A{   //虛函數示例代碼public:virtual void fun(){cout<<1<<endl;}virtual void fun2(){cout<<2<<endl;}};class B:public A{public:void fun(){cout<<3<<endl;}void fun2(){cout<<4<<endl;}};由于這兩個類中有虛函數存在,所以編譯器就會為他們兩個分別插入一段你不知道的數據,并為他們分別創建一個表。那段數據叫做vptr指針,指向那個表。那個表叫做vtbl,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址,請看圖通過上圖,可以看到這兩個vtbl分別為class A和class B服務。現在有了這個模型之后,我們來分析下面的代碼A *p=new A;p->fun();毫無疑問,調用了A::fun(),但是A::fun()是如何被調用的呢?它像普通函數那樣直接跳轉到函數的代碼處嗎?No,其實是這樣的,首先是取出vptr的值,這個值就是vtbl的地址,再根據這個值來到vtbl這里,由于調用的函數A::fun()是第一個虛函數,所以取出vtbl第一個slot里的值,這個值就是A::fun()的地址了,最后調用這個函數。現在我們可以看出來了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對應類的虛函數地址,所以這樣虛函數就可以完成它的任務。而對于class A和class B來說,他們的vptr指針存放在何處呢?其實這個指針就放在他們各自的實例對象里。由于class A和class B都沒有數據成員,所以他們的實例對象里就只有一個vptr指針。通過上面的分析,現在我們來實作一段代碼,來描述這個帶有虛函數的類的簡單模型。#include<iostream>using namespace std;//將上面“虛函數示例代碼”添加在這里int main(){void (*fun)(A*);A *p=new B;long lVptrAddr;memcpy(&lVptrAddr,p,4);memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);fun(p);delete p;system("pause");}用VC或Dev-C++編譯運行一下,看看結果是不是輸出3,如果不是,那么太陽明天肯定是從西邊出來。現在一步一步開始分析void (*fun)(A*);  這段定義了一個函數指針名字叫做fun,而且有一個A*類型的參數,這個函數指針待會兒用來保存從vtbl里取出的函數地址A* p=new B;  這個我不太了解,算了,不解釋這個了long lVptrAddr;  這個long類型的變量待會兒用來保存vptr的值memcpy(&lVptrAddr,p,4);  前面說了,他們的實例對象里只有vptr指針,所以我們就放心大膽地把p所指的4bytes內存里的東西復制到lVptrAddr中,所以復制出來的4bytes內容就是vptr的值,即vtbl的地址現在有了vtbl的地址了,那么我們現在就取出vtbl第一個slot里的內容memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);  取出vtbl第一個slot里的內容,并存放在函數指針fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指針,所以我們要把它先轉變成指針類型fun(p);  這里就調用了剛才取出的函數地址里的函數,也就是調用了B::fun()這個函數,也許你發現了為什么會有參數p,其實類成員函數調用時,會有個this指針,這個p就是那個this指針,只是在一般的調用中編譯器自動幫你處理了而已,而在這里則需要自己處理。delete p;和system("pause");  這個我不太了解,算了,不解釋這個了如果調用B::fun2()怎么辦?那就取出vtbl的第二個slot里的值就行了memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 為什么是加4呢?因為一個指針的長度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 這更符合數組的用法,因為lVptrAddr被轉成了long*型別,所以+1就是往后移sizeof(long)的長度三,  以一段代碼開始#include<iostream>using namespace std;class A{   //虛函數示例代碼2public:virtual void fun(){ cout<<"A::fun"<<endl;}virtual void fun2(){cout<<"A::fun2"<<endl;}};class B:public A{public:void fun(){ cout<<"B::fun"<<endl;}void fun2(){ cout<<"B::fun2"<<endl;}};  //end//虛函數示例代碼2int main(){void (A::*fun)();  //定義一個函數指針A *p=new B;fun=&A::fun;(p->*fun)();fun = &A::fun2;(p->*fun)();delete p;system("pause");}你能估算出輸出結果嗎?如果你估算出的結果是A::fun和A::fun2,呵呵,恭喜恭喜,你中圈套了。其實真正的結果是B::fun和B::fun2,如果你想不通就接著往下看。給個提示,&A::fun和&A::fun2是真正獲得了虛函數的地址嗎?首先我們回到第二部分,通過段實作代碼,得到一個“通用”的獲得虛函數地址的方法#include<iostream>using namespace std;//將上面“虛函數示例代碼2”添加在這里void CallVirtualFun(void* pThis,int index=0){void (*funptr)(void*);long lVptrAddr;memcpy(&lVptrAddr,pThis,4);memcpy(&funptr,reinterpret_cast<long*>(lVptrAddr)+index,4);funptr(pThis); //調用}int main(){A* p=new B;CallVirtualFun(p);  //調用虛函數p->fun()CallVirtualFun(p,1);//調用虛函數p->fun2()system("pause");}現在我們擁有一個“通用”的CallVirtualFun方法。這個通用方法和第三部分開始處的代碼有何聯系呢?聯系很大。由于A::fun()和A::fun2()是虛函數,所以&A::fun和&A::fun2獲得的不是函數的地址,而是一段間接獲得虛函數地址的一段代碼的地址,我們形象地把這段代碼看作那段CallVirtualFun。編譯器在編譯時,會提供類似于CallVirtualFun這樣的代碼,當你調用虛函數時,其實就是先調用的那段類似CallVirtualFun的代碼,通過這段代碼,獲得虛函數地址后,最后調用虛函數,這樣就真正保證了多態性。同時大家都說虛函數的效率低,其原因就是,在調用虛函數之前,還調用了獲得虛函數地址的代碼。最后的說明:本文的代碼可以用VC6和Dev-C++4.9.8.0通過編譯,且運行無問題。其他的編譯器小弟不敢保證。其中,里面的類比方法只能看成模型,因為不同的編譯器的低層實現是不同的。例如this指針,Dev-C++的gcc就是通過壓棧,當作參數傳遞,而VC的編譯器則通過取出地址保存在ecx中。所以這些類比方法不能當作具體實現

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/458941.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/458941.shtml
英文地址,請注明出處:http://en.pswp.cn/news/458941.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

mysql 主主互備實現

今天星期天&#xff0c;么事就寫個MYSQL的主主架構的博客&#xff0c;原理如下圖&#xff0c;不是我畫的網上找的。主機作用操作系統mysql版本對應IPvip數據庫mysqlA(主)centos6.4mysql 5.5.48192.168.48.129192.168.48.126mysqlB(備)centos6.4mysql 5.5.48192.168.48.132一&am…

Linux Deepin 版本大全,Deepin下載-Deepin linuxv20.0 官方版下載-6188手游網

Deepin linux安裝準備一&#xff1a;為 Deepin 留出硬盤空間以便安裝。1.打開磁盤管理(Windows 徽標上右鍵單擊&#xff0c;選擇磁盤管理)。2.選中一個分區(空閑空間大于 30G)&#xff0c;我選擇的是 C 盤&#xff0c;因為是固態硬盤所以安裝啟動會快很多。磁盤 1 的 200G 分區…

動手實踐 Linux VLAN - 每天5分鐘玩轉 OpenStack(13)

本節我們來看如何在實驗環境中實施和配置如下 VLAN 網絡 配置 VLAN 編輯 /etc/network/interfaces&#xff0c;配置 eth0.10、brvlan10、eth0.20 和 brvlan20。 下面用 vmdiff 展示了對 /etc/network/interfaces 的修改 重啟宿主機&#xff0c;ifconfig 各個網絡接口 用 brct…

Socket的3次握手鏈接與4次斷開握手

*************************************************** 更多精彩&#xff0c;歡迎進入&#xff1a;http://shop115376623.taobao.com *************************************************** 連接握手&#xff1a; 1.客戶端發送建立連接請求 &#xff08;發送請求&#xff09;2.…

Linux桌面需要強制訪問控制,RHCSA 系列(十三): 在 RHEL 7 中使用 SELinux 進行強制訪問控制...

RHCSA 認證&#xff1a;SELinux 精要和控制文件系統的訪問盡管作為第一級別的權限和訪問控制機制是必要的&#xff0c;但它們同樣有一些局限&#xff0c;而這些局限則可以由安全增強 Linux(Security Enhanced Linux&#xff0c;簡稱為 SELinux)來處理。這些局限的一種情形是&am…

使用canvas實現擦玻璃效果

體驗效果:http://hovertree.com/texiao/html5/25/效果圖&#xff1a;代碼如下&#xff1a; <!DOCTYPE html> <html> <head lang"zh"> <meta name"viewport" content"initial-scale1.0, maximum-scale1.0, user-scalableno, widt…

如何計算時間復雜度

*************************************************** 更多精彩&#xff0c;歡迎進入&#xff1a;http://shop115376623.taobao.com *************************************************** 求解算法的時間復雜度的具體步驟是&#xff1a; ⑴ 找出算法中的基本語句&#xff1b;…

linux顯示系統信息軟件下載,linux查看系統信息軟件安裝信息命令學習筆記

查看LINUX安裝版本[rootlocalhost etc]# unameLinux[rootlocalhost etc]# uname -aLinux localhost.localdomain 2.6.32-279.11.1.el6.i686 #1 SMP Tue Oct 16 14:40:53 UTC 2012 i686 i686 i386 GNU/Linux[rootlocalhost etc]# cat /proc/versionLinux version 2.6.32-279.11.…

Bzoj 2662: [BeiJing wc2012]凍結 dijkstra,堆,分層圖,最短路

2662: [BeiJing wc2012]凍結 Time Limit: 3 Sec Memory Limit: 128 MBSubmit: 647 Solved: 348[Submit][Status][Discuss]Description “我要成為魔法少女&#xff01;” “那么&#xff0c;以靈魂為代價&#xff0c;你希望得到什么&#xff1f;” “我要將有關魔法和奇…

[轉]opencv學習資料

轉自&#xff1a;http://blog.csdn.net/poem_qianmo/article/details/20537737 1&#xff1a;Mat imread(const string& filename, intflags1 ); eg: Mat image0imread("dota.jpg",CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR);//載入最真實的圖像 ge1i…

linux servlet 亂碼問題,Servlet一次亂碼排查后的總結

由來在寫一個小小的表單提交功能的時候&#xff0c;出現了亂碼&#xff0c;很奇怪request上來的參數全部是亂碼&#xff0c;而從數據庫查詢出來的中文顯示到頁面正常&#xff0c;鎖定肯定是request對象那里出了問題。后來經過排查&#xff0c;發現是我封裝的框架中出了問題&…

C/C++回調函數

*************************************************** 更多精彩&#xff0c;歡迎進入&#xff1a;http://shop115376623.taobao.com *************************************************** 對于很多初學者來說&#xff0c;往往覺得回調函數很神秘&#xff0c;很想知道回調函數…

Linux 命令[2]:mkdir

make directories mkdir -p [目錄名] -p 遞歸創建 [rootlocalhost ~]# mkdir -p test [rootlocalhost ~]# ls anaconda-ks.cfg install.log install.log.syslog test 當然只創建一個目錄 -p 是可以省略的 注&#xff1a;如果創建多級目錄沒有 -p 會報錯 如&#xff1a; [roo…

jQuery動態設置樣式List item

前段時間&#xff0c;Insus.NET有修改一個功能《激活當前視圖菜單高亮呈現》http://www.cnblogs.com/insus/p/5287093.html 今天Insus.NET想改用另外一個方法來實現&#xff0c;使用jQuery。在ASP.NET MVC 環境實現&#xff1a; 代碼&#xff1a; <ul><li><a hr…

linux telnet 權限,允許telnet 通過root用戶進行訪問

允許telnet 通過root用戶進行訪問RHEL6:[rootclovem ~]# yum install telnet-server -y //安裝telnet服務端[rootclovem ~]# cat /etc/xinetd.d/telnet //開啟telnet的托管服務# default: on# description: The telnet server serves telnet sessions; it uses \#unencrypt…

TOUGHRADIUS 項目介紹

2019獨角獸企業重金招聘Python工程師標準>>> TOUGHRADIUS 項目介紹 ToughRADIUS是一個開源的Radius服務軟件&#xff0c;采用于 Apache License 2.0 許可協議發布&#xff0c;從創立之日起&#xff0c;他的宗旨就是服務于中小微ISP&#xff0c;讓運營變得更簡單。 T…

轉:Jmeter 用戶思考時間(User think time),定時器,和代理服務器(proxy server)...

在負載測試中需要考慮的的一個重要要素是思考時間&#xff08;think time&#xff09;&#xff0c; 也就是在兩次成功的訪問請求之間的暫停時間。 有多種情形揮發導致延遲的發生&#xff1a; 用戶需要時間閱讀文字內容&#xff0c;或者填表&#xff0c;或者查找正確的鏈接等。未…

linux sql語句傳參數,Linux/Unixshell參數傳遞到SQL腳本

在數據庫運維的過程中&#xff0c;Shell 腳本在很大程度上為運維提供了極大的便利性。而shell 腳本參數作為變量傳遞給SQL以及SQL腳本也是DB在數據庫運維的過程中&#xff0c;Shell 腳本在很大程度上為運維提供了極大的便利性。而shell 腳本參數作為變量傳遞給SQL以及SQL腳本也…

Myeclipse5.5獲取注冊碼

2019獨角獸企業重金招聘Python工程師標準>>> import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class MyEclipseGen {private static final String LL "Decompiling this copyrighted software is a vi…

虛函數和純虛函數的區別

*************************************************** 更多精彩&#xff0c;歡迎進入&#xff1a;http://shop115376623.taobao.com *************************************************** 首先&#xff1a;強調一個概念 定義一個函數為虛函數&#xff0c;不代表函數為不被實…