C++支援函式過載,而C語言卻不支援,原來是這個原因!

丶阿部發表於2020-09-24

1.函式過載的概念

如果你接觸過C++,那麼一定使用過插入運算子"<<“和提取運算子”>>"吧。這倆個運算子是C和C++位運算子中的左移運算子右移運算子,而C++又把它作為輸入輸出運算子。允許一個運算子可以用於不同場合,不同的場合就有不同的含義,這就叫做運算子的 “過載”,即重新賦予它新的含義。這其實就是 “一物多用”
在C++中,函式也可以過載。C++允許在同一作用域中用同一函式名定義多個函式,這些函式的引數列表不同,這些同名的函式用來實現不同的功能。這就是函式的過載,即一個函式名多用

例項:分別求倆個int,double及long型別的資料之和。

#include <iostream>
using namespace std;

int Add(int x, int y)
{
	return x + y;
}

double Add(double x, double y)
{
	return x + y;
}

long Add(long x, long y)
{
	return x + y;
}

int main()
{
	int ret1 = Add(1, 2);
	double ret2 = Add(1.23, 2.34);
	long ret3 = Add(10L, 20L);
	cout << "int: " << ret1 << endl;
	cout << "double: " << ret2 << endl;
	cout << "long: " << ret3 << endl;

	return 0;
}

main函式三次呼叫Add函式,但每次實參的型別不同。系統會根據實參的型別找到與之匹配的函式,然後呼叫該函式。

注意: 上述情況只是函式引數列表不同的其中一種表現,函式引數列表不同,總共包括如下三種種情況

  • 引數的個數不同
  • 引數的型別相同
  • 引數的次序不同

函式的函式名相同,函式的引數列表滿足以上三種情況中的一種或多種,都可以構成函式的過載。

2.函式名修飾----造成差異的真正原因

函式的編譯過程+連結

要深入理解這塊的內容,我們還得從程式的編譯過程說起,C/C++中,一個程式要執行起來,必須經歷如下幾個階段:預處理、編譯、彙編、連結。
我在C語言學習階段寫過一篇部落格來總結程式的編譯過程,其中畫過一張圖拿到這裡也同樣適用(並附上我之前那篇部落格的連結,有興趣的讀者可以瀏覽瀏覽-----深入理解程式的編譯過程+連結
在這裡插入圖片描述
有必要把那篇部落格中的部分內容也放到這裡做一個參考。
在這裡插入圖片描述

而在這裡,我們著重需要研究的是連結這個部分。我們實際的專案通常是由多個原始檔構成的,這裡假設在main.c/cpp中呼叫了在add.c/cpp中定義的Add函式,我們根據之前的知識可以知道,在編譯生成.o檔案之後,連結之前,main.o中是沒有Add的函式地址的,因為Add是在add.c/cpp中定義的,所以Add的地址是在add.o中。那怎麼處理呢?
連結階段就是專門用來處理這種問題,連結器看到main.o呼叫Add,但是沒有Add的地址,就會到add.o的符號表中找Add的地址,然後連結到一起。 那麼重點就來了,連結時,面對Add函式,連結器會根據什麼樣的名字去尋找它呢,在這裡,不同的編譯器會有不同的函式名修飾規則。

Linux下的函式名修飾規則

由於Linux下gcc/g++的修飾規則簡單易懂,下面我們以gcc/g++為例來觀察這個函式修飾後的名字。

  • 使用gcc編譯test.c

在這裡插入圖片描述
可以看出,在Liunx下采用gcc編譯後,函式的名字的修飾沒有發生改變, 和原函式名無區別。

  • 使用g++編譯test.cpp

在這裡插入圖片描述
而採用g++編譯完成後,函式名字的修飾發生改變,編譯器將函式引數型別資訊新增到修改後的名字中。
其實上面的例子對比下來以足以說明問題,但為了進一步感受函式過載,我們再觀察一下下邊的這個案例,從而更加確定我們的結論。
在這裡插入圖片描述

結論: 通過上述舉例,我們可以看出gcc的函式修飾後名字不變。而g++的函式修飾後變成[_Z+函式長度+函式名+型別首字母]。

看到這裡,大家可能就會豁然開朗,正是因為C語言中的函式名無法區分,而C++的函式修飾中增加了函式型別的緣故,從而儘管是函式名相同但只要引數列表不同,編譯器也會認為是不同的函式, 這樣就支援了過載。

Windows下VS中的函式名修飾

怎樣檢視VS中編譯器對函式名是怎樣修飾的呢?這裡我提供一個有趣的做法:
還是以Add函式為例,我們新建三個原始檔,分別是main.c(呼叫Add函式)、add.h(放置Add函式宣告)、add.c(Add函式實現)。
做好上述工作之後,如果各環節程式碼均完整且無誤,則可順利通過編譯及連結,但這卻並不是我們的目的。我們需要做的是,將add.c中的函式實現刪掉(或註釋掉即可),再次編譯並連結,就會得到報錯資訊如下(需要注意如果是僅編譯並不會報錯,因為程式碼語法本身不存在問題):
在這裡插入圖片描述
回憶文章前邊描述連結過程可知,由於程式在連結時找不到Add函式的實現,則出現上述報錯,而我們需要關注的是函式名由Add改成了_Add, 僅僅是前邊多了一個下劃線,這也正是VS2013的編譯器對C語言中函式名字的修飾,同樣沒有和型別相關的任何內容。
這時,原始碼完全不進行更改,只需將所有的.c檔案換成.cpp檔案,再次進行編譯連結,這時則會看到如下的報錯資訊:
在這裡插入圖片描述
同樣型別的錯誤提示,但可以很明顯的看到,對於同樣的Add函式,函式名由Add改成了 ?Add@@YAHHH@Z。雖然我們好像看不太懂具體是什麼意思,但有了以上的經驗,我們也能大致猜測是C++的編譯器對函式名字的修飾中加入了型別等元素導致的,因為C++需要支援函式的過載。
至於這種複雜的命名修飾到底分別代表什麼含義,這裡就不在繼續解釋,原因之一是博主到現在都還沒完成整明白呢!有興趣的讀者自己可以深入研究一下。

3.extern “C”

通過上述的結論可知,C語言和C++對於函式的修飾規則不同,這也是倆種語言編譯風格不同的一種體現。
有時候在C++工程中可能需要將某些函式按照C的風格來編譯, 在函式前加extern “C”,意思是告訴編譯器,將該函式按照C語言的規則來編譯。 比如:tcmalloc是google用C++實現的一個專案,他提供tcmallc()和tcfree兩個介面來使用,但如果是C專案就沒辦法使用,那麼他就使用extern “C”來解決。
上述舉例可能不太容易驗證,但我們可以通過一個簡單的反例來證明extern "C"的效果。
還是用上邊那個Add函式的例子來說明(我今天好像和這個Add函式過不去了,哈哈哈)
同樣不修改其他程式碼,Add函式的實現部分任然是刪除或者註釋狀態(總之就是沒有),而在其宣告處前加上extern “C”, 再次編譯連結檢視報錯資訊。

extern "C" int Add(int x, int y); //採用C語言的編譯規則

在這裡插入圖片描述
看到這裡,不需要再繼續解釋了吧!(本文完)

相關文章