c++中的函式過載、函式重寫、函式重定義
目錄
一、函式過載
二、函式重寫
三、函式重定義
為了更加深刻的理解 函式過載、重寫、重定義,我們可以帶著如下這兩個問題去思考:
1、子類中是否可以定義父類中的同名成員?為什麼?
可以,因為子類與父類的名稱空間不同;
2、子類中定義的函式是否可以過載父類中的同名函式?
不可以,因為函式過載必須在同一個作用域中。
一、函式過載(Function Overloading)
1、什麼是函式過載
在同一個類中(同一個作用域中/在類的內部),存在一組函式名相同,函式的引數列表不同(引數的個數、型別、順序),函式有無 virtual 關鍵字都可以,我們把這組函式稱為函式過載。2、為什麼使用函式過載(函式過載的好處)
由於函式過載可以在同一個作用域內,使用同一個函式名 命名一組功能相似的函式,這樣做減少了函式名的數量,避免了程式設計師因給函式名命名所帶來的煩惱,從而提高程式的開發的效率。
3、函式過載的條件
1. 必須在同一作用域下
2. 函式名相同但是引數列表不同
3. 返回值的型別不會影響過載
4. const屬性相同
4、函式過載的原理(本質:c++編譯器對同名函式進行重新命名)
編譯器在編譯.cpp檔案中當前使用的作用域裡的同名函式時,根據函式形參的型別和順序會對函式進行重新命名(不同的編譯器在編譯時對函式的重新命名標準不一樣);
但是總的來說,他們都把檔案中的同一個函式名進行了重新命名;
- 在vs編譯器中:
根據返回值型別(不起決定性作用)+形參型別和順序(起決定性作用)的規則重新命名並記錄在map檔案中。
- 在linux g++ 編譯器中:
根據函式名字的字元數+形參型別和順序的規則重新命名記錄在符號表中;從而產生不同的函式名,當外面的函式被呼叫時,便是根據這個記錄的結果去尋找符合要求的函式名,進行呼叫;
為什麼c語言不能實現函式過載?
編譯器在編譯.c檔案時,只會給函式進行簡單的重新命名;
具體的方法是給函式名之前加上”_”;所以加入兩個函式名相同的函式在編譯之後的函式名也照樣相同;呼叫者會因為不知道到底呼叫那個而出錯;
1 #include<stdio.h> 2 3 int Add(int a, int b) 4 { 5 return a + b; 6 } 7 8 9 float Add(float a, float b) 10 { 11 return a + b; 12 } 13 14 void testFunc() 15 { 16 Add(10, 20); 17 Add(20.0f, 30.0f); 18 } 19 20 int main(int argc, char *argv[]) 21 { 22 testFunc(); 23 24 return 0; 25 }
1. 將上述程式碼儲存到.c檔案中
若上述程式碼用c編譯器編譯,由於c語言中無函式過載,所以,在程式執行時出錯。
出錯原因:因為在c語言中,c編譯器只是在函式名的前面加下劃線進行簡單的重新命名;
為了驗證結果,將上述的程式碼稍作修改( float Add(float a, float b) -> float Add1(float a, float b) )。然後用 vs Debug模式編譯.c檔案,之後在.map檔案中就可以看到結果。
在vs中,map檔案生成的步驟設定:工程名右擊—>屬性—->配置屬性—->連結器—–>除錯—->生成對映檔案—>選擇是;
2. 將上述程式碼儲存到.cpp檔案中
若上述程式碼用c++編譯器編譯,由於c++語言支援函式過載,所以程式正常執行;但是,在不同c++編譯器之間對函式過載的機制也是不一樣,接下來分別用vs 和 g++介紹。
(1)用 vs Debug模式編譯.cpp檔案,之後就可以在map檔案中看到如下結果,
// ‘?’表示名稱開始,‘?’後邊是函式名;“@@YA”表示引數表開始,後邊的3個字元分別表示返回值型別,兩個引數型別;“@Z”表示名稱結束。
(2)在Ubuntu下測試(需要安裝g++編譯器),執行以下指令:
1)g++ test.cpp
2)objdump a.out -t > test.out // -t是表示生成符號表,最後是將生成的符號表用重定向符號放在test.out檔案。
3)vi test.out
開啟test.out檔案,就會發現,整形數相加的函式Add(int a,int b)
生成的符號表中,Add函式名被記錄為_Z3Addii。
其中,_Z表示符號表名稱開始, 3代表函式名的字元個數,ii代表引數列表順序中2個形參的型別;
綜述,無論使用何種編譯器,在.cpp檔案中,雖然兩個函式的函式名一樣,但是他們在符號表中生成的名稱不一樣,所以是可以編譯通過的。
由上述分析可知,c編譯器 與 c++編譯器 對函式的重新命名規則不一樣;那麼,在c++中如何確保將一段c程式碼以c編譯器的方式被編譯呢?---- 使用 extern 關鍵字
1 // 使用方式1 2 extern "C" 3 { 4 // C-Style Compilation 5 } 6 7 // 使用方式2 8 //__cplusplus 是 c++ 編譯器內建的標準巨集定義 9 //__cplusplus 的意義:確保C程式碼以統一的C方式被編譯成目標檔案 10 11 #ifdef __cplusplus 12 extern "C" { 13 #endif 14 15 // C-Style Compilation 16 17 #ifdef __cplusplus 18 } 19 #endifextern "C" 的使用方式
參考連結:https://blog.csdn.net/qq_37791134/article/details/81502017、https://blog.csdn.net/gogogo_sky/article/details/71189499、https://blog.csdn.net/fantian_/article/details/80719144
5、函式過載的結論
1. 函式過載的本質:多個不同的函式;
2. 函式名和引數列表是唯一的標識;
3. 函式過載必須發生在同一個作用域中;
4. c++編譯器 和 c編譯器 對函式重新命名的規則不同;
5. 編譯器決定符號表中函式名被編譯後的最終目標名;
c++ 編譯器 將函式名和引數列表編譯成目標名;
c 編譯器將函式名編譯成目標名;
6. 函式過載是在編譯期間根據引數型別和個數決定函式呼叫
7. 函式過載是一種靜態多型;
(1)多型:用同一個東西表示不同的形態;
(2)多型分為:靜態多型(編譯時的多型)、動態多型(執行時的多型);
6、編譯器呼叫函式過載的規則
1. 將所有同名函式作為候選者;
2. 嘗試尋找可行的候選者函式
(1)精確匹配實參;
(2)通過預設引數能夠匹配實參;
(3)通過預設型別轉換匹配實參;
3. 匹配失敗
(1)最終尋找的候選函式不唯一,則出現二義性,編譯失敗;
(2)無法匹配所有的候選函式,函式沒定義,編譯失敗;
7、函式過載與預設引數
當函式過載遇到預設引數時,就會發生二義性;
程式碼如下:
1 #include<iostream> 2 using namespace std; 3 4 class A 5 { 6 void func(int a, int b, int c = 0) {} 7 void func(int a, int b) {} 8 }; 9 10 int main() 11 { 12 A a; 13 a.func(1, 2); // 二義性出現 14 15 return 0; 16 }函式過載的二義性案例
8、函式過載 與 函式指標
將過載函式名賦值給函式指標時,
1. 根據過載規則挑選與函式指標引數列表一致的候選者;
2. 嚴格匹配候選者的函式型別與函式指標的函式型別;
1 #include <stdio.h> 2 #include <string.h> 3 4 int func(int x) 5 { 6 return x; 7 } 8 9 int func(int a, int b) 10 { 11 return a + b; 12 } 13 14 int func(const char* s) 15 { 16 return strlen(s); 17 } 18 19 typedef int(*PFUNC)(int a); 20 21 22 int main(int argc, char *argv[]) 23 { 24 int c = 0; 25 26 PFUNC p = func; 27 28 c = p(1); 29 30 printf("c = %d\n", c); // c = 1 31 32 return 0; 33 }函式過載與函式指標
二、函式重寫(也稱為覆蓋, Function override)
1、什麼是函式重寫
函式重寫分為 虛擬函式重寫(會發生多型) 與 非虛擬函式重寫(重定義的一種形式);
函式重寫:也叫做覆蓋。子類重新定義父類中有相同返回值、名稱和引數的虛擬函式。函式特徵相同。但是具體實現不同,主要是在繼承關係中出現的 。
注:一般而言,函式重寫 就是 虛擬函式重寫,為的是實現多型呼叫;
2、函式重寫的條件
1. 函式的返回型別、方法名、引數列表完全相同;
2. 必須發生在不同的作用域中(基類與派生類中);
3. 基類中有 virtual 關鍵字宣告,派生類中可有可無,不能有 static (虛擬函式重寫);
3、函式重寫的意義
在面向物件的繼承關係中,我們瞭解到子類可以擁有父類中的所有屬性與行為;但是,有時父類中提供的方法並不能滿足現有的需求,所以,我們必須在子類中重寫父類中已有的方法,來滿足當前的需求。
三、函式重定義(也稱為隱藏,Function redefining)
1、什麼是函式重定義
子類重新定義父類中有相同名稱的函式 ( 不包括虛擬函式重寫 ) 。
2、重定義的表現形式
1. 必須發生在不同的作用域中(基類與派生類中);
2. 函式名相同;
3. 返回值可以不同;
4. 引數列表不同,此時,無論基類中的同名函式有無 virtual 關鍵字,基類中的同名函式都會被隱藏。
5. 引數列表相同,此時,基類中的同名函式沒有 virtual 關鍵字,則基類中的同名函式將會被隱藏 --- 非虛擬函式重寫 。
3、關於同名覆蓋的結論(歸納:基類與派生類中存在同名成員;--- 同名覆蓋)
1. 子類將隱藏父類中的同名成員;
2. 父類中的同名成員依然存在於子類中;
3. 可以通過作用域分辨符(::)訪問被隱藏的父類中的同名成員;
4. 不可以直接通過子類物件訪問父類成員;
注:同名覆蓋規則適用於類的成員變數與成員函式;
相關程式碼展示:
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Parent 7 { 8 public: 9 int mi; 10 11 Parent() 12 { 13 cout << "Parent() : " << "&mi = " << &mi << endl; 14 } 15 }; 16 17 class Child : public Parent 18 { 19 public: 20 int mi; 21 22 Child() 23 { 24 cout << "Child() : " << "&mi = " << &mi << endl; 25 } 26 }; 27 28 int main() 29 { 30 Child c; 31 32 c.mi = 100; 33 34 c.Parent::mi = 1000; 35 36 cout << "&c.mi = " << &c.mi << endl; 37 cout << "c.mi = " << c.mi << endl; 38 39 cout << "&c.Parent::mi = " << &c.Parent::mi << endl; 40 cout << "c.Parent::mi = " << c.Parent::mi << endl; 41 42 return 0; 43 } 44 45 /** 46 * Parent() : &mi = 0x7ffe98191450 47 * Child() : &mi = 0x7ffe98191454 48 * &c.mi = 0x7ffe98191454 49 * c.mi = 100 50 * &c.Parent::mi = 0x7ffe98191450 51 * c.Parent::mi = 1000 52 */同名成員變數案例
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Parent 7 { 8 public: 9 int mi; 10 11 void add(int v) 12 { 13 mi += v; 14 } 15 16 void add(int a, int b) 17 { 18 mi += (a + b); 19 } 20 }; 21 22 class Child : public Parent 23 { 24 public: 25 int mi; 26 27 void add(int v) 28 { 29 mi += v; 30 } 31 32 void add(int a, int b) 33 { 34 mi += (a + b); 35 } 36 37 void add(int x, int y, int z) 38 { 39 mi += (x + y + z); 40 } 41 }; 42 43 int main() 44 { 45 Child c; 46 47 c.mi = 100; 48 c.Parent::mi = 1000; 49 50 cout << "c.mi = " << c.mi << endl; 51 cout << "c.Parent::mi = " << c.Parent::mi << endl; 52 53 c.add(1); 54 c.add(2, 3); 55 c.add(4, 5, 6); 56 c.Parent::add(10); 57 c.Parent::add(11, 12); 58 59 cout << "c.mi = " << c.mi << endl; 60 cout << "c.Parent::mi = " << c.Parent::mi << endl; 61 62 return 0; 63 } 64 /** 65 * c.mi = 100 66 * c.Parent::mi = 1000 67 * c.mi = 121 68 * c.Parent::mi = 1033 69 */重定義案例
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Parent 7 { 8 public: 9 int mi; 10 11 virtual void add(int v) 12 { 13 mi += v; 14 } 15 }; 16 17 class Child : public Parent 18 { 19 public: 20 int mi; 21 22 virtual void add(int v) 23 { 24 mi += v; 25 } 26 27 void add(int a, int b) 28 { 29 mi += (a + b); 30 } 31 }; 32 33 int main() 34 { 35 Child c; 36 Parent &p = c; // 父類引用指向子類物件,多型發生 37 38 c.mi = 100; 39 c.Parent::mi = 1000; 40 41 cout << "c.mi = " << c.mi << endl; 42 cout << "c.Parent::mi = " << c.Parent::mi << endl; 43 44 c.add(1); 45 c.add(2, 3); 46 p.add(100); // 實際呼叫的是子類中 add(int v) 函式 47 c.Parent::add(10); 48 49 cout << "c.mi = " << c.mi << endl; // c.mi = 1 + 2 + 3 + 100 50 cout << "c.Parent::mi = " << c.Parent::mi << endl; // c.Parent::mi = 1000 + 10 51 52 return 0; 53 } 54 /** 55 * c.mi = 100 56 * c.Parent::mi = 1000 57 * c.mi = 206 58 * c.Parent::mi = 1010 59 */重寫案例
本節總結:
1、 過載 必須在 一個類之間, 而 重寫、重定義 是在 2個類 之間
2、 過載是在 編譯期間 根據引數型別和個數決定函式呼叫; 多型(虛擬函式重寫)是在 執行期間 根據具體物件的型別決定函式呼叫
3、 發生重寫、重定義後,遵循 同名覆蓋 規則;