C++函式過載實現原理淺析
版權宣告:本文首發於http://blog.csdn.net/candcplusplus,轉載請註明出處... https://blog.csdn.net/shimachao/article/details/12746975
---------------------------------------------------獻給所有和我一樣還沒拿到office的同學-----------------------------------------------------------------------------------
C++函式過載實現原理淺析
C++實現函式過載的技術手段是函式符號改名,所以我們可以通過分析編譯器的函式符號改名機制來驗證C++函式過載規則。
1.函式過載的概念
函式過載:出現在相同作用域中的多個函式,具有相同的名字而形參表不同。
注意:不能僅僅基於不同的返回型別而實現函式過載。返回值是不影響函式簽名的。
2.函式呼叫:
函式呼叫時會發生什麼?學過8086彙編時,我們都知道函式呼叫是程式執行點跳轉到一個符號所在的地方轉而執行符號所在地址的程式碼,然後再跳回去。這個符號就是函式。
我們用一個簡單的例子來說明一下函式呼叫
//在這個簡單的例項中,我們只是簡單的在main函式中呼叫了一下printhello函式來列印hello world!
#include <stdio.h>
void printhello()
{
printf("hello world!\n");
}
int main()
{
printhello();//這裡呼叫函式printhello
return 0;
}
//函式呼叫部分對應的彙編程式碼為:
main:.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
call printhello ;這裡call printhello,跳轉到符號printhello出執行
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
C語言中函式符號名和對應的函式名是一樣的,而C++為了支援函式過載,符號名是在對應的函式名上改編的。如下圖所示,函式名為func,而對應的符號名為_Z4funcv。
3.C++的函式符號命名規則
在前面的的圖示中,我們給出了C++函式編譯符號例項,貌似函式名是對應符號的子串額。實際上函式的編譯符號是根據函式名,函式的引數表(包括引數型別和數量)相關的。而且不同的編譯器的命名規則不一樣。只要能保證相同的函式名和不同的函式引數列表生成的符號名不一樣就行。下面我們來感受一下GCC的C++編譯器的命名規則。
3.1函式返回型別不影響生成的符號名
前面我們說不能僅僅基於不同的返回型別而實現函式過載,原因是函式返回值並不影響最後生成的符號。我現在就驗證一下:
我們分別在兩個cpp檔案中定義兩個同名但返回值不同的函式,看一看他們在彙編程式碼中的符號是否一樣。
第一個函式返回型別為void,生成的符號名為:_Z4funcv
第二個函式名也為func,但返回型別為int,生成的符號名還是為:_Z4funcv
上面兩個同名但返回值型別不同函式生成相同符號名,說明返回型別是不影響符號名的。如果你定義兩個函式,只是返回型別不同,那麼它們生成的符號一樣,肯定會發生符號重定義錯誤。
3.2 函式名,引數列表(引數型別、數目)才是影響符號名的因數
下面我們觀察多個引數列表不同的同名函式,看看它們對應的符號名是什麼。這次我們不直接觀察彙編程式碼(彙編程式碼太長了),而是用objdump -t命令直接觀察程式碼對應的目標檔案中的符號表。
假設有下面的這些函式(左),以及它們生成符號(右)
看來改編的符號名是在函式名前加了一個字首,如果沒有引數就在後面加一個字母v,如果是int引數就加一個i,如果是char引數就加一個c,float引數就加一個f,double引數加一個d。引用加R,指標加P。貌似我們找到了某種規則。不過不同的規律改編的方法不一樣,我們沒必要在意某個編譯器使用的改名規則。只需要知道函式名+引數列表決定了符號名 就行。也可以看到第二個函式的int返回型別並沒對函式的符號名有什麼影響。
3.3 const形參對函式的符號名有影響嗎?
3.3.1 第1組實驗:
理論上const int a和int a是不同型別的變數,那const對函式對應的符號名有影響嗎?
我們用下面的程式碼測試一下:
哦,func1和func2形參只是一個有const限定,一個木有。它們出現了重定義錯誤,說明它們對應的符號是一樣的,看來const對符號名木有影響啊,加或不加都一樣。真的是這樣嗎?我們再來測兩組。
3.3.2 第2,3兩組實驗:
可以看出和前面一組實驗不一樣,這次的兩組函式雖然引數只是一個有cosnt限定,一個沒const限定,生成的符號名卻都不一樣,能通過編譯。這說明了什麼?(看到&和*沒)
實際上僅當形參是引用或指標時,const形參才對符號名有影響。(實際上我也是在Primer書上看到的,這裡只是驗證一下,要不然我的腦殼可想不到)
注意如果形參本身是const指標不是這種情況(這和第一組實驗型別)
3.3.3 第4,5組實驗:
注意const int*和int *const的區別。實際上int & const不存在,因為一個引用繫結到一個物件後,不可能再繫結到另一個物件。所以int & const在語法中是不需要存在的。
背景知識補充:常量指標(const int*或者int const*)與指標常量(int * const)。
常量指標是指不可通過指標給該指標指向的變數賦值(即不可以修改該變數),但是可以改變該指標的指向。定義函式時,如果不想在函式總修改所指向的引數,可以把形參宣告問常量指標。C/C++標準庫函式就是這麼做的。
指標常量是指不可以改變指標的指向,但能通過指標給該指標指向的變數賦值(即可以修改該變數)。
4. 這也是extern “C”的由來
分析到了這裡,我們已經驗證為了支援函式過載C++編譯器的函式符號命名機制和C語言是不一樣的。實際上C++的符號命名機制也適合全域性變數。
所以然,如果你在C++中直接呼叫C語言編譯的函式,連結時會找不到符號,傳送符號未定義錯誤(undefined reference to之類的錯誤)。下面驗證一下:
我們在檔案cfunctest.h中聲明瞭兩個函式,並在8.c中實現了這兩個函式,然後用C語言編譯器編譯8.c生成目標檔案8.1.o。然後在8.cpp呼叫這個兩個函式。先用8.cpp生成8.2.o。然後嘗試將8.1.o和8.2.o連結。
我們來編譯並連結一下
可以看出雖然8.c和8.cpp雖然可以各自編譯成功,但是連結到一起時候卻連結不到要呼叫的函式符號。
我們來看看這兩個函式在8.1.o和8.2.o中的符號各是什麼:
可以看到兩個函式在8.1.o和8.2.o中的符號名是不同的。當然連結不上啦。如果檢視8.c和8.cpp對應的彙編程式碼,也會發現生成的符號是不同的。
如果我硬是要在C++程式碼中呼叫用C編譯器編譯的函式,那該怎麼辦呢?這時候該extern “C”登場了。之所以會出現連結錯誤,是因為C++在呼叫函式時候,把函式符號改名了,而且這種改名機制和C編譯器的符號命名機制是不同的。所以我們要告訴C++編譯器,在呼叫某個用C編譯的函式時,不要用C++的符號命名機制,而是用C語言的符號命名機制。這就是extern “C”的功能。
把8.cpp中#include “cfunctest.h”改成extern “C”{#include “cfunctest.h”}再試一下就行了。
在cpp檔案中加上extern “C”後,重新編譯,發現生成的目標檔案8.2.o中函式的符號名和前一張圖中8.1.o中的符號名相同了。連結也無錯誤了。
實際上你也可以直接在cpp檔案把對應函式呼叫處的彙編程式碼call _Z6cfunc1v改成call cfuncv1,call _Z6cfunc2v改成call cfunc2(反正保證兩個目標檔案中的函式符號一樣就行)再用這個彙編程式碼編譯,也可以正確連結的。
至於C++過載函式的匹配規律,有那麼一點點複雜。看書就好。我這裡只做這些分析了。
5.參考資料:《C++ Primer中文版》第四版 第7.8節:過載函式
善良超哥哥的吐血之作,轉載請註明出處:http://blog.csdn.net/candcplusplus/article/details/12746975
---------------------
作者:流沙的刺客
來源:CSDN
原文:https://blog.csdn.net/candcplusplus/article/details/12746975
版權宣告:本文為博主原創文章,轉載請附上博文連結!