C++深入淺出講解函數重載
前言
自然語言中,一個詞可以有多重含義,人們可以通過上下文來判斷該詞真實的含義,即該詞被重載瞭。
比如:以前有一個笑話,國有兩個體育項目大傢根本不用看,也不用擔心。一個是乒乓球,一個是男足。前者是“誰也贏不瞭!”,後者是“誰也贏不瞭!”
函數重載
1.1 函數重載的概念
函數重載:
- 它是函數的一種特殊情況,C++允許在同一作用域中同一作用域中聲明幾個功能類似的同名函數
- 函數重載的關鍵是函數的參數列表,也稱為“函數特征標”
- 這些同名函數的形參列表(參數個數、類型和順序(不同類型的順序))必須不同,常用來處理實現功能類似數據類型不同的問題
- 函數重載也是多態的一種,多態指的是“有多種形式”
//C語言不支持重載,C++支持重載 int Add(int left, int right) { return left+right; } double Add(double left, double right) { return left+right; } int Add(int left, double right) { return left+right; } int Add(double left, int right) { return left+right; } int main() { Add(10, 20); Add(10.0, 20.0); Add(10, 20.0); Add(10.0, 20.0) return 0; }
下面兩個函數屬於函數重載嗎?
short Add(short left, short right) { return left+right; } int Add(short left, short right) { return left+right; } int main() { Add(10, 20); Add(10, 20); return 0; }
代碼解析:
- 上述代碼中的兩個函數不屬於函數重載
- 因為重載的形參列表(參數個數、類型和順序)必須不同
- 函數重載與函數返回值的類型無關,並且在函數調用時,也是無法識別它的
1.2 函數重載的意義
意義:
在C語言中,想要定義多個不同類型交換數據的子函數,需要不同的函數名來命名,比如SweapA、SweapB…等等
void SweapA(int *pa, int *pb) { int temp = *pa; *pa = *pb; *pb = temp; } void SweapB(double *pa, double *pb) { double temp = *pa; *pa = *pb; *pb = temp; } int main() { int a = 10, b = 20; double c = 10.0, d = 20.0; SweapA(&a, &b); SweapB(&c), &d); return 0; }
- 但是,在C++中,通過函數重載,隻需要命名一次就可以瞭
- 雖然跟C語言一樣要重復定義函數,但是後面會學到函數模板後,可以很好的解決這個重復定義問題
void Sweap(int *pa, int *pb) { int temp = *pa; *pa = *pb; *pb = temp; } void Sweap(double *pa, double *pb) { double temp = *pa; *pa = *pb; *pb = temp; } int main() { int a = 10, b = 20; double c = 10.0, d = 20.0; Sweap(&a, &b); Sweap(&c), &d); return 0; }
1.3 名字修飾(name Mangling)
名字修飾(name Mangling):
- C++為瞭跟蹤每一個重載函數,它都會給這些函數指定一個私密身份
- 使用C++編譯器編寫函數重載程序時,C++編譯器將執行一些奇特的操作 — — —名稱修飾 或 名稱矯正
- 它根據函數原型中指定的形參對每個函數名進行加密
- 對參數數目和類型進行編碼,添加的一組符號符合隨函數形參列表而異,修飾時使用的約定(函數名)隨編譯器而異
為什麼C++支持重載,而C語言不支持呢?
- 在C/C++中,一個程序要運行起來,需要經歷以下幾個階段:預處理、編譯、匯編、鏈接
- 預處理(.i):文件展開、宏替換、條件編譯、去註釋
- 編譯(.s):檢查語法是否正確,生成匯編代碼
- 匯編(.o):將匯編代碼轉化成二進制的機器碼
- 鏈接(a.out):生成符號表,找調用函數的地址,鏈接匹配,合並到一起
- 實際我們的項目通常是由多個頭文件和多個源文件構成,當前a.cpp中調用瞭b.cpp中定義的Add函數
- 在編譯後鏈接前的處理階段,a.o的目標文件中沒有Add的函數地址,因為Add是在b.cpp中定義的,所以Add的地址在b.o中。那麼怎麼辦呢?
- 鏈接器看到a.o調用Add,但是沒有Add的地址,就會到b.o的符號表中找Add的地址,然後鏈接到一起
- 鏈接時,面對Add函數,鏈接器會使用哪個名字去找呢?這裡每個編譯器都有自己的函數名修飾規則
在Linux下使用gcc和g++編譯器演示函數名被修飾後的名字
采用C語言編譯器編譯後結果(反匯編)
結論:在Linux下,采用gcc編譯完成後,函數名字的修飾沒有發生改變
采用C++編譯器編譯後結果(反匯編)
結論:在Linux下,采用g++編譯完成後,函數名字的修飾發生改變,編譯器將函數參數類型信息添加到修改後的名字中
總結
gcc的函數修飾後名字不變。而g++的函數修飾後變成(_Z+函數長度+函數名+類型首字母)
C語言沒辦法支持重載,因為同名函數沒辦法區分。而C++是通過函數修飾規則來區分,隻要參數不同,修飾出來的名字就不一樣,就支持瞭重載
Windows下名字修飾規則
結論:對比Linux會發現,windows下C++編譯器對函數名字修飾非常奇怪,但道理都是一樣的
擴展學習:C/C++函數調用約定和名字修飾規則
C++函數重載
C/C++的調用約定
接下來,再演示一個例子
f.h
#include <stdio.h>void f(int a, double b);
void f(double b, int a);f.cpp
#include "f.h"void f(int a, double b);
{
printf("%d %lf\n", a, b)
}void f(double b, int a);
{
printf("%lf %d\n", b, a)
}
Test.cpp
#include "f.h"int main()
{
f(1, 2.222);
f(2.222, 1);
return 0;
}
編譯後,生成匯編指令;鏈接時,生成符號表
Linux下g++(C++)編譯器的命名:
Linux下gcc(C)編譯器的命名:
1.4 extern "C"
- 有時候在C++工程中可能需要將某些函數按照C的風格來編譯
- 但是,大多數情況下是C工程需要將某些函數按照C++的風格來編譯
- C可以調用CPP的靜態/動態庫,而CPP也可以調用C的靜態/動態庫
- extern “C”是告訴編譯器,它所聲明的函數,是C的庫,要用C的鏈接方式去調用靜態庫或動態庫
那麼CPP是怎麼調用C中的靜態/動態庫呢?(vs2022演示)
首先,我們用C來生成一個靜態庫或動態庫
Test.h #include <stdio.h> void PrintArray(int* p, int n); //顯示數組內容 void InsertSort(int* p, int n); //插入排序 Test.C #include "Test.h" void InsertSort(int* p, int n) { for (int i = 0; i < n - 1; ++i) { int end = i; int tmp = p[end + 1]; while (end >= 0) { if (tmp < p[end]) { p[end + 1] = p[end]; --end; } else { break; } } p[end + 1] = tmp; } }
配置類型改成靜態庫後,生成解決方案,就得到後綴.lib文件瞭
在CPP項目中添加新的庫目錄(這個庫是你生成的靜態庫的路徑)
增加新的依賴項(依賴項為生成靜態庫的文件名+後綴"Test.lib")
做完這些準備後,我們來進行編譯程序
- 編譯後我們發現鏈接階段時出現瞭錯誤
- 原因是:C++調用C時,它們之間的函數命名規則(名稱修飾)不同
- 我們需要C++中的extern "C"來解決
- extern “C”是告訴編譯器,它所聲明的函數,是C的庫,要用C的鏈接方式去調用靜態庫或動態庫
extern "C" { //"../"是在當前目錄的上一個目錄中找文件 #include "../../Test/Test/Test.h" } #include <iostream> using namespace std; void TestInsertSort() { int Array[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; InsertSort(Array, sizeof(Array) / sizeof(Array[0])); for (int i = 0; i < 10; ++i) cout << Array[i] << " "; cout << " " << endl; } int main() { TestInsertSort(); return 0; }
如果C想調用CPP的靜態或動態庫呢?
- C調用CPP庫時,也會遇到名稱修飾的問題
- 這裡需要對CPP的名稱修飾規則改成C的規則
- 這裡我們需要用到"條件編譯"來解決問題
Test.h #include <stdio.h> #ifdef __cplusplus #define EXTERN_C extern "C" #else #define EXTERN_C #endif EXTERN_C void PrintArray(int* p, int n); EXTERN_C void InsertSort(int* p, int n); Test.cpp #include "Test.h" void PrintArray(int* p, int n) { for (int i = 0; i < n; ++i) { printf("%d ", p[i]); } printf("\n"); } void InsertSort(int* p, int n) { for (int i = 0; i < n - 1; ++i) { int end = i; int tmp = p[end + 1]; while (end >= 0) { if (tmp < p[end]) { p[end + 1] = p[end]; --end; } else { break; } } p[end + 1] = tmp; } }
感謝大傢支持!!!
到此這篇關於C++深入淺出講解函數重載的文章就介紹到這瞭,更多相關C++函數重載內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!