C++函數重載實現原理淺析


                                

                                                           C++函數重載實現原理淺析

 

       C++實現函數重載的技術手段是函數符號改名,所以我們可以通過分析編譯器的函數符號改名機制來驗證C++函數重載規則。



 

1.函數重載的概念

          函數重載 出現在相同作用域中的多個函數,具有相同的名字而形參表不同。
    注意:不能僅僅基於不同的返回類型而實現函數重載。返回值是不影響函數簽名的。




2.函數調用:

          函數調用時會發生什么?學過8086匯編時,我們都知道函數調用是程序執行點跳轉到一個符號所在的地方轉而執行符號所在地址的代碼,然后再跳回去。這個符號就是函數。
    我們用一個簡單的例子來說明一下函數調用
//在這個簡單的實例中,我們只是簡單的在main函數中調用了一下printhello函數來打印hello world!
  1. #include <stdio.h>  
  2. void printhello()  
  3. {          
  4.    printf("hello world!\n");  
  5. }  
  6.   
  7. int main()  
  8. {  
  9.         printhello();//這里調用函數printhello  
  10.         return 0;  
  11. }  
  1. //函數調用部分對應的匯編代碼為:  
  2.       main:.LFB1:  
  3.         .cfi_startproc  
  4.         pushq        %rbp  
  5.         .cfi_def_cfa_offset 16  
  6.         .cfi_offset 6, -16  
  7.         movq        %rsp, %rbp  
  8.         .cfi_def_cfa_register 6  
  9.         movl        $0, %eax  
  10.         call        printhello            ;這里call  printhello,跳轉到符號printhello出執行        
  11.   
  12.         movl        $0, %eax  
  13.         popq        %rbp  
  14.         .cfi_def_cfa 7, 8  
C語言中函數符號名和對應的函數名是一樣的,而C++為了支持函數重載,符號名是在對應的函數名上改編的。如下圖所示,函數名為func,而對應的符號名為_Z4funcv。

    

C  函數名和符號名是不一樣的.png 



 

3. C++的函數符號命名規則

          在前面的的圖示中,我們給出了C++函數編譯符號實例,貌似函數名是對應符號的子串額。實際上函數的編譯符號是根據函數名,函數的參數表(包括參數類型和數量)相關的。而且不同的編譯器的命名規則不一樣。只要能保證相同的函數名和不同的函數參數列表生成的符號名不一樣就行。下面我們來感受一下GCC的C++編譯器的命名規則。

3.1函數返回類型不影響生成的符號名

    前面我們說不能僅僅基於不同的返回類型而實現函數重載,原因是函數返回值並不影響最后生成的符號。我現在就驗證一下:
    我們分別在兩個cpp文件中定義兩個同名但返回值不同的函數,看一看他們在匯編代碼中的符號是否一樣。
             返回類型對符號無影響2.png  
    第一個函數返回類型為void,生成的符號名為:_Z4funcv
           函數返回不影響符號名.png  
          第二個函數名也為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對函數對應的符號名有影響嗎?
    我們用下面的代碼測試一下:
           const形參對符號名有影響嗎?.png  
    哦,func1和func2形參只是一個有const限定,一個木有。它們出現了重定義錯誤,說明它們對應的符號是一樣的,看來const對符號名木有影響啊,加或不加都一樣。真的是這樣嗎? 我們再來測兩組。

3.3.2 第2,3兩組實驗:
           const形參對符號名有影響嗎?2.png  
    可以看出和前面一組實驗不一樣,這次的兩組函數雖然參數只是一個有cosnt限定,一個沒const限定,生成的符號名卻都不一樣,能通過編譯。這說明了什么?(看到&和*沒)
      實際上僅當形參是引用或指針時,const形參才對符號名有影響。(實際上我也是在Primer書上看到的,這里只是驗證一下,要不然我的腦殼可想不到)
      注意如果形參本身是const指針不是這種情況(這和第一組實驗類型)



3.3.3 第4,5組實驗:
           const 4,5組實驗.png  
    注意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.o8.2.o 鏈接

8.cpp.png
   我們來編譯並鏈接一下
生成8.1.o 8.2.o並鏈接.png  
          可以看出雖然8.c8.cpp雖然可以各自編譯成功,但是鏈接到一起時候卻鏈接不到要調用的函數符號。

     我們來看看這兩個函數在8.1.o8.2.o中的符號各是什么:
objdump -t 8.1.o 8.2.o.png
      可以看到兩個函數在8.1.o8.2.o中的符號名是不同的。當然鏈接不上啦。如果查看8.c8.cpp對應的匯編代碼,也會發現生成的符號是不同的。
      如果我硬是要在C++代碼中調用用C編譯器編譯的函數,那該怎么辦呢?這時候該extern “C”登場了。之所以會出現鏈接錯誤,是因為C++在調用函數時候,把函數符號改名了,而且這種改名機制和C編譯器的符號命名機制是不同的。所以我們要告訴C++編譯器,在調用某個用C編譯的函數時,不要用C++的符號命名機制,而是用C語言的符號命名機制。這就是extern “C”的功能。
      把8.cpp#include “cfunctest.h”改成extern “C”{#include “cfunctest.h”}再試一下就行了。
加extern后.png  

      在cpp文件中加上extern “C”后,重新編譯,發現生成的目標文件8.2.o中函數的符號名和前一張圖中8.1.o中的符號名相同了。鏈接也無錯誤了。
實際上你也可以直接在cpp文件把對應函數調用處的匯編代碼call  _Z6cfunc1v改成call  cfuncv1call  _Z6cfunc2v改成call  cfunc2(反正保證兩個目標文件中的函數符號一樣就行)再用這個匯編代碼編譯,也可以正確鏈接的。


   

至於C++重載函數的匹配規律,有那么一點點復雜。看書就好。我這里只做這些分析了。
 
5.參考資料:《C++ Primer中文版》第四版 第7.8節:重載函數

 

                                                   

                                             

  善良超哥哥的吐血之作,轉載請注明出處:http://blog.csdn.net/candcplusplus/article/details/12746975


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM