【原創】Performanced C++ 經驗規則 第五條:再談重載、覆蓋和隱藏

第五條:再談重載、覆蓋和隱藏

在C++中,無論在類作用域內還是外,兩個(或多個)同名的函數,可能且僅可能是以下三種關系:重載(Overload)、覆蓋(Override)和隱藏(Hide),因為同名,區分這些關系則是根據參數是否相同、是否帶有const成員函數性質、是否有virtual關鍵字修飾以及是否在同一作用域來判斷。在第四條中,我們曾提到了一些關于重載、覆蓋的概念,但只是一帶而過,也沒有提到隱藏,這一篇我們將詳細討論。

1、首先說的是重載,有一個前提必須要弄清楚的是,如果不在類作用域內進行討論,兩個(或多個)同名函數之間的關系只可能是重載或隱藏,這里先說重載。考慮以下事實:

1 int foo(char c){...}
2 void foo(int x){...}

這兩個函數之間的關系是重載(overload),即相同函數名但參數不同,并注意返回類型是否相同并不會對重載產生任何影響

也就是說,如果僅僅是返回類型不相同,而函數名和參數都完全相同的兩個函數,不能構成重載,編譯器會告知"ambiguous"(二義性)等詞以表達其不滿:

1 //Can't be compiled!
2 
3 int fooo(char c){...}
4 void fooo(char c){...}
5 
6 char c = 'A';
7 fooo(c); // Which one? ambiguous

在第四條中,已經講述過,重載是編譯期綁定的靜態行為,不是真正的多態性,那么,編譯器是根據什么來進行靜態綁定呢?又是如何確定兩個(或多個)函數之間的關系是重載呢?

有以下判定依據:

(1)相同的范圍:即作用域,這里指在同一個類中,或同一個名字空間,即C++的函數重載不支持跨越作用域進行(讀者可再次對比Java在這問題上的神奇處理,既上次Java給我們提供了未卜先知的動態綁定能力后,Java超一流的意識和大局觀再次給Java程序員提供了跨類重載的能力,如有興趣可詳細閱讀《Thinking in Java》的相關章節,其實對于學好C++來講,去學一下Java是很有幫助的,它會告訴你,同樣或類似的問題,為什么Java要做這樣的改進),這也是區別重載和隱藏的最重要依據。

關于“C++不能支持跨類重載”,稍后筆者會給出代碼來例證這一點。

(2)函數名字相同(基本前提)

(3)函數參數不同(基本前提,否則在同一作用域內有兩個或多個同名同參數的函數,將產生ambiguous,另外注意,對于成員函數,是否是const成員函數,即函數聲明之后是否帶有const標志, 可理解為“參數不同“),第(2)和第(3)點統稱“函數特征標”不同

(4)virtual關鍵字可有可無不產生影響(因為第(1)點已經指出,這是在同一個類中)

“相同的范圍,特征標不同(當然同名是肯定的),發生重載“

?

2、覆蓋(override),真正的多態行為,通過虛函數來實現,所以,編譯器根據以下依據來進行判定兩個(注意只可能是兩個,即使在繼承鏈中,也只是最近兩個為一組)函數之間的關系是覆蓋:

(1)不同的范圍:即使用域,兩個函數分別位于基類和派生類中

(2)函數名字相同(基本前提)

(3)函數參數也相同(基本前提),第(2)和第(3)點統稱“函數特征標”相同

(4)基類函數必須用virtual關鍵字修飾

“不同的范圍,特征標相同,且基類有virtual聲明,發生覆蓋“

?

3、隱藏(Hide),即:

(1)如果派生類函數與基類函數同名,但參數不同(特征標不同),此時,無論是否有virtual關鍵字,基類的所有同名函數都將被隱藏,而不會重載,因為不在同一個類中;

(2)如果派生類函數與基類函數同名,且參數也相同(特征標相同),但基類函數沒有用virtual關鍵字聲明,則基類的所有同名函數都將被隱藏,而不會覆蓋,因為沒有聲明為虛函數。

“不同的范圍,特征標不同(當然同名是肯定的),發生隱藏”,或"不同的范圍,特征標相同,但基類沒有virtual聲明,發生隱藏“

可見有兩種產生隱藏的情況,分別對應不能滿足重載和覆蓋條件的情況。

另外必須要注意的是,在類外討論時,也可能發生隱藏,如在名字空間中,如下述代碼所示:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 void foo(void) { cout << "global foo()" << endl; }
 5 int foo(int x) { cout << "global foo(int)" << endl; return x; }
 6 namespace a
 7 {
 8         void foo(void) { cout << "a::foo()" << endl; }
 9         void callFoo(void) 
10         { foo();
11            // foo(10); Can't be compiled! }
12 }
13 
14 int main(int argc, char** argv)
15 {
16         foo();
17         a::callFoo();
18         return 0;
19 }    

輸出結果:

1 global foo()
2 a::foo()

注意,名字空間a中的foo隱藏了其它作用域(這里是全局作用域)中的所有foo名稱,foo(10)不能通過編譯,因為全局作用域中的int foo(int)版本也已經被a::foo()隱藏了,除非使用::foo(10)顯式進行調用。

這也告訴我們,無論何時,都使用完整名稱修飾(作用域解析符調用函數,或指針、對象調用成員函數)是一種好的編程習慣

?好了,上面零零散散說了太多理論的東西,我們需要一段實際的代碼,來驗證上述所有的結論:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Other
 5 {
 6         void* p;
 7 };
 8 
 9 class Base
10 {
11 public:
12         int iBase;
13         Base():iBase(10){}
14         virtual void f(int x = 20){ cout << "Base::f()--" << x << endl; }
15         virtual void g(float f) { cout << "Base::g(float)--" << f << endl; }
16         void g(Other& o) { cout << "Base::g(Other&)" << endl; }
17         void g(Other& o) const { cout << "Base::g(Other&) const" << endl;}
18 };
19 
20 class Derived : public Base
21 {
22 public:
23         int iDerived;
24         Derived():iDerived(100){}
25         void f(int x = 200){ cout << "Derived::f()--" << x << endl; }
26         virtual void g(int x) { cout << "Derived::g(int)--" << x << endl; }
27 };
28 
29 int main(int argc, char** argv)
30 {
31         Base* pBase = NULL;
32         Derived* pDerived = NULL;
33         Base b;
34         Derived d;
35         pBase = &b;
36         pDerived = &d;
37         Base* pBD = &d;
38         const Base* pC = &d;
39         const Base* const pCCP = &d;
40         Base* const pCP = &d;
41 
42         int x = 5;
43         Other o;
44         float f = 3.1415926;
45 
46         b.f();
47         pBase->f();
48         d.f();
49         pDerived->f();
50         pBD->f();
51 
52         b.g(x);
53         b.g(o);
54         d.g(x);
55         d.g(f);
56         // Can't be compiled!
57         // d.g(o);
58 
59         pBD->g(x);
60         pBD->g(f);
61         pC->g(o);
62         pCCP->g(o);
63         pCP->g(o);
64 
65         return 0;
66 }

在筆者Ubuntu 12.04 + gcc 4.6.3運行結果:?

 1 Base::f()--20 //b.f(),通過對象調用,無虛特性,靜態綁定
 2 Base::f()--20 //基類指針指向基類對象,雖然是動態綁定,但沒有使用到覆蓋
 3 Derived::f()--200 //d.f,通過對象調用,無虛特性,靜態綁定
 4 Derived::f()--200 //子類指針指向子類對象,雖然是動態綁定,但沒有使用到覆蓋
 5 Derived::f()--20 //基類指針指向子類對象,動態綁定,子類f()覆蓋基類版本。但函數參數默認值,是靜態聯編行為,pBD的類型是基類指針,所以使用了基類的參數默認值,注意此處!
 6 
 7 Base::g(float)--5 //通過對象調用,int被提升為float
 8 Base::g(Other&) //沒什么問題,基類中三個g函數之間的關系是重載
 9 Derived::g(int)--5 //沒什么問題
10 Derived::g(int)--3 //注意基類的g(float)已經被隱藏!所以傳入的float參數調用的卻是子類的g(int)方法!
11 
12 Base::g(float)--5 //注意!pBD是基類指針,雖然它指向了子類對象,但基類中的所有g函數版本它是可見的!所以pBD->g(5)調用到了g(float)!雖然產生了動態聯編也發生了隱藏,但子類對象的虛表中,仍可以找到g(float)的地址,即基類版本!
13 Base::g(float)--3.14159 //原理同上
14 
15 //d.g(o)
16 //注意此處!再注意代碼中被注釋了的一行,d.g(o)不能通過編譯,因為d是子類對象,在子類中,基類中定義的三個g函數版本都被隱藏了,編譯時不可見!不會重載
17 
18 Base::g(Other&) const //pC是指向const對象的指針,將調用const版本的g函數
19 Base::g(Other&) const //pCCP是指向const對象的const指針,也調用const版本的g函數
20 Base::g(Other&) //pCP是指向非cosnt對象的const指針,由于不指向const對象,調用非const版本的g函數

上述結果,是否和預想的是否又有些出入呢?問題主要集中于結果的第5、12、13和15行。

第5行輸出結果證明:當函數參數有默認值,又發生多態行為時,函數參數默認值是靜態行為,在編譯時就已經確定,將使用基類版本的函數參數默認值而不是子類的

而第12、13、15行輸出結果則說明,盡管已經證明我們之前說的隱藏是正確的(因為d.g(o)不可以通過編譯,確實發生了隱藏),但卻可以利用基類指針指向派生類對象后,來繞開這種限制!也就是說,編譯器根據參數匹配函數原型的時候,是在編譯時根據指針的類型,或對象的類型來確定,指針類型是基類,那么基類中的g函數版本就是可見的;指針類型是子類,由于發生了隱藏,基類中的g函數版本就是不可見的。而到動態綁定時,基類指針指向了子類對象,在子類對象的虛函數表中,就可以找到基類中g虛函數的地址。

寫到這里,不知道讀者是否已經明白,這些繞來繞去的關系。在實際代碼運用中,可能并不會寫出含有這么多“陷阱”的測試代碼,我們只要弄清楚重載、覆蓋和隱藏的具體特征,并頭腦清醒地知道,我現在需要的是哪一種功能(通常也不會需要隱藏),就能寫出清析的代碼。上面的代碼其實是一個糟糕的例子,因為在這個例子中,重載、覆蓋、隱藏并存,我們編寫代碼,就是要盡可能防止這種含混不清的情況發生。

記住一個原則:每一個方法,功能和職責盡可能單一,否則,嘗試將它拆分成為多個方法

轉載于:https://www.cnblogs.com/ccdev/archive/2012/12/26/2833884.html

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

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

相關文章

C++之純虛函數和抽象類

純虛函數和抽象類 1.基本概念 2.案例 #include <iostream> using namespace std;////面向抽象類編程(面向一套預先定義好的接口編程)//解耦合 ....模塊的劃分class Figure //抽象類 { public://閱讀一個統一的界面(接口),讓子類使用,讓子類必須去實現virtual void get…

解決: -bash: $‘\302\240docker‘: command not found

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我只是運行 一條很簡單的啟動容器的命令&#xff0c;多次執行都報錯&#xff0c;報錯如題&#xff1a; -bash: $\302\240docker: comma…

換擋/掛檔

定義 換擋/掛檔是指變速器&#xff0c;用于轉變發動機曲軸的轉矩及轉速&#xff0c;以適應汽車在起步、加速、行駛以及克服各種道路阻礙等不同行駛條件下對驅動車輪牽引力及車速不同要求的需要。作用 使汽車能以非常低的穩定車速行駛&#xff0c;而這種低的轉速只靠內然…

sql:無法解決 equal to 操作中 Chinese_PRC_CI_AS 和 Chinese_Taiwan_Stroke_CI_AS 之間的排序規則沖突。...

--無法解決 equal to 操作中 "Chinese_PRC_CI_AS" 和 "Chinese_Taiwan_Stroke_CI_AS" 之間的排序規則沖突。 CREATE VIEW View_VipBranchStaffBranchList AS select VipBranchStaff.*,geovindu_branch.B_Name,VipExamCountry.ExamCountryName from VipBran…

【汽車取證篇】GA-T 1998-2022《汽車車載電子數據提取技術規范》(附下載)

【汽車取證篇】GA-T 1998-2022《汽車車載電子數據提取技術規范》&#xff08;附下載&#xff09; GA-T 1998-2022《汽車車載電子數據提取技術規范》標準—【蘇小沐】 總結 公眾號回復關鍵詞【汽車取證】自動獲取資源合集&#xff0c;如鏈接失效請留言&#xff0c;便于…

解決: Client does not support authentication protocol requested by server; consider upgrading MySQL

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. 在服務器上把 mysql 裝好后&#xff0c;運行起來。 2. navicat 死活連接不上&#xff0c;在網上查說是要改數據庫賬號、密碼什么的&…

C++之STL理論基礎

1.基本概念 STL&#xff08;Standard Template Library&#xff0c;標準模板庫)是惠普實驗室開發的一系列軟件的統稱。雖然主要出現在C中&#xff0c;但在被引入C之前該技術就已經存在了很長的一段時間。 STL的從廣義上講分為三部分&#xff1a;algorithm&#xff08;算法&am…

方向盤

定義 方向盤是汽車、輪船、飛機等的操縱行駛方向的輪狀裝置。 構成 一般由骨架和發泡組合起來就是最簡單的方向盤了&#xff0c;而方向盤上都會有和主駕駛氣囊對應的安裝卡扣或螺釘孔&#xff0c;其下方一般會有多功能開關模塊。作用 方向盤不僅可以控制車輛的方向…

數據庫范式俗話

1NF&#xff1a;一個table中的列是不可再分的&#xff08;即列的原子性&#xff09; 2NF&#xff1a;一個table中的行是可以唯一標示的&#xff0c;&#xff08;即table中的行是不可以 重復的&#xff09; 3NF&#xff1a;一個table中的列不依賴于另一個table中的非主鍵列 4NF&…

STL之string類型

1.String概念 string是STL的字符串類型&#xff0c;通常用來表示字符串。而在使用string之前&#xff0c;字符串通常是用char*表示的。 string和char*的區別&#xff1a; string是一個類, char*是一個指向字符的指針。 string封裝了char*&#xff0c;管理這個字符串&#x…

解決maven打包報錯:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、報錯經歷&#xff1a; 今天使用eclipse通過maven install打war包的時候&#xff0c;出現了下圖所示的錯誤 二、問題分析&#xff1a…

離合器

離合器的定義 汽車離合器位于發動機和變速箱之間的飛輪殼內&#xff0c;用螺釘將離合器總成固定在飛輪的后平面上&#xff0c;離合器的輸出軸就是變速箱的輸入軸。在汽車行駛過程中&#xff0c;駕駛員可根據需要踩下或松開離合器踏板&#xff0c;使發動機與變速箱暫時分離和…

Python 刪除滿足條件的某些行

數據&#xff1a; data 字段&#xff1a;col 要刪除的內容是 col False 的行 # 方案一 data1 data[~data[col] False] # ~ 取反# 方案二 保留 data[已采] ! False ind data[col] ! False data2 data.loc[ind,]# 方案三 去掉 data[已采] True ind2 data[col] False…

STL之Vector

1.簡介 vector是將元素置于一個動態數組中加以管理的容器。可以隨機存取元素&#xff08;支持索引值直接存取&#xff0c;用[]操作符或at()方法&#xff0c;還支持迭代器方式存取&#xff09;。   vector尾部添加或移除元素非常快速。但是在中部或頭部插入元素或移除元素比…

解決 : Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. 執行 maven install 命令報錯如題&#xff1a; Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:comp…

制動踏板

什么是制動踏板 制動踏板就是限制動力的踏板&#xff0c;即腳剎(行車制動器)的踏板&#xff0c;是長時間摩擦導致剎車片過熱軟化的原因。制動踏板的作用 其主要作用是剎車減速或停車。 制動踏板的工作原理 在機器的高速軸上固定一個輪或盤&#xff0c;在機座上安裝…

實現一個用戶取過的數據不被其他用戶取到

實現一個用戶取過的數據不被其他用戶取到: 問題&#xff1a; 在用ADO訪問數據庫時&#xff0c;從一個表中取一定的記錄&#xff08;比如20行&#xff09;&#xff0c;取出后在程序中使用&#xff0c;使用完后刪除掉記錄&#xff08;不用更新或刪除記錄&#xff09;。在多用戶操…

Docker 鏡像 重命名

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. 鏡像改名命令格式&#xff1a; # 命令格式&#xff1a;docker tag 鏡像id 倉庫&#xff1a;標簽或&#xff1a;docker tag 舊鏡…

STL之deque和其他容器

deque簡介 deque是“double-ended queue”的縮寫&#xff0c;和vector一樣都是STL的容器&#xff0c;deque是雙端數組&#xff0c;而vector是單端的。 deque在接口上和vector非常相似&#xff0c;在許多操作的地方可以直接替換。 deque可以隨機存取元素&#xff08;支持索引…

Java藍橋杯02——第二題集錦:生日蠟燭、星期一、方格計數、猴子分香蕉

第二題 生日蠟燭(結果填空) 某君從某年開始每年都舉辦一次生日party&#xff0c;并且每次都要吹熄與年齡相同根數的蠟燭。 現在算起來&#xff0c;他一共吹熄了236根蠟燭。 請問&#xff0c;他從多少歲開始過生日party的&#xff1f; 請填寫他開始過生日party的年齡數。 注意&a…