關于C和CPP中同名函數的思考

首先看一段代碼:

***************************文件名:fun_c.c***********************
int fun(int a);
int fun(int a ,int b);
void fun(int a);
int fun(int a)
{
  printf("This is int fun(int a)\n");
}

int main()
{
    fun(1);
    return 0;
}

使用gcc編譯:

fun_c.c:5: error: conflicting types for ‘fun’
fun_c.c:4: error: previous declaration of ‘fun’ was here
fun_c.c: In function ‘main’:
fun_c.c:14: error: too few arguments to function ‘fun’

使用g++編譯:

fun_c.c:6: error: new declaration ‘void fun(int)’
fun_c.c:4: error: ambiguates old declaration ‘int fun(int)’
fun_c.c: In function ‘int fun(int)’:
fun_c.c:7: error: new declaration ‘int fun(int)’
fun_c.c:6: error: ambiguates old declaration ‘void fun(int)’

首先解釋一下gcc和g++編譯報錯原因:

  1. gcc編譯器默認將代碼當做C語言去編譯,認為函數名相同的函數為同一個函數,以上代碼中聲明了三個函數名相同的函數,所以gcc編譯器報fun重復定義。
  2. g++編譯器默認將代碼當做CPP語言去編譯,認為 int fun(int a); 和 void fun(int a); 兩個函數是同一個函數。
    那為什么CPP只報這兩個函數重定義呢?
    原因是:CPP擁有重載的特性,在同一個作用域中,函數名相同,參數表不同的函數,構成重載關系。 重載與函數的返回類型無關,與參數名也無關,而只與參數的個數、類型和順序有關。CPP會將構成重載關系的函數解析成不同函數。

現在,我們不經要問:為什么CPP要引入重載?CPP是怎樣將構成重載關系或不同作用域的函數解析成不同函數的呢?

1.為什么CPP要引入重載?

剛開始,編譯器編譯源代碼生成目標文件時,符號名和函數名是一致的,但是隨著后來程序越來越大,編寫的目標文件不可避免的會出現符號沖突的問題。比如,當程序很大時,不同模塊由不同部門開發,如果他們之間命名不規范,很有可能出現符號沖突的問題。于是呢,CPP等后來設計語言就開始引入了重載和命名空間來解決這個問題。

2.CPP是怎樣將構成重載關系或不同作用域的函數解析成不同函數的呢?

首先,看一段代碼:

int fun(int);
int fun(int,int);

class Cfun_class1{
    int fun(int);
    class Cfun_class2{
        int fun(int);
    };
};
namespace N {
    int fun(int);
    class Cfun_class3{
        int fun(int);
    };
}

以上代碼中有6個同名函數fun,但是他們的參數類型和參數個數以及所在的namespace不同。CPP利用函數簽名來識別不同的函數。函數簽名包括函數名,參數類型,所在的類和namespace。以上6個函數的函數簽名分別是:

函數簽名
int fun(int)
int fun(int,int)
int::Cfun_class1:: fun(int)
int::Cfun_class1::Cfun_class2:: fun(int)
int::N:: fun(int)
int::N::Cfun_class3:: fun(int)

編譯器在將CPP源代碼編譯成目標文件時,會利用某種名稱修飾方法將函數簽名編碼成一個符號名。此外,以上的簽名和修飾的方法不僅用在了函數上,CPP中全局變量和靜態變量也用到了同樣的方法。

通過以上的闡述,我們了解到C和CPP的編譯鏈接規約是不同的,也就是說編譯器會將C和CPP中國函數名編碼成不同的符號名。這里我們想一個問題,如果一個項目中,即有C文件又有CPP文件,該怎么編譯?這就涉及到了extern "C"

3.extern "C"

先看一段代碼:

cHeader.h

#ifndef C_HEADER
#define C_HEADER

void print_fun(int i);

#endif C_HEADER

cHeader.c

#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

main.c

#include "cHeader.h"

int main(int argc,char** argv)
{
    print(3);
    return 0;
}

編譯鏈接:

gcc -c cHeader.c  -o cHeader.o
ar cqs libCheader.a cHeader.o
g++ -o mian main.cpp -L/root/Desktop -lCheader

結果:

/tmp/ccUgVIT7.o: In function `main':
main.cpp:(.text+0x19): undefined reference to `print(int)'
collect2: ld returned 1 exit status

編譯后報錯:未定義print函數。這就是因為編譯器對CPP和C的編譯規約不同,編譯器認為print是一個CPP函數,將print編碼成一個CPP符號,鏈接器拿著這個CPP符號在靜態庫中找不到對應的print函數,所以編譯器認為print函數為定義。

為解決上述問題,CPP引入了extern "C"。將CHeader.h中代碼改成如下代碼,即可編譯通過。

 extern "C"{
     void print(int i);
}

CPP編譯器會把在extern "C"大括號內部的代碼當做C代碼來處理。這樣編譯器會將print函數編碼成一個C符號,鏈接器就可以從靜態庫中找到對應的print函數。為進一步方便操作,CPP提供了宏__cplusplus ,CPP編譯器會在編譯CPP代碼時默認這個宏,我們可以使用條件宏來判斷當前的編譯單元是不是CPP代碼。具體代碼如下:

#ifdef __cplusplus
extern "C"{
#endif
void print(int i);
#ifdef __cplusplus  
}
#endif

如果當前編譯單元是CPP代碼,那么void print(int i);會在 extern "C"里面被聲明;如果是C代碼,就直接聲明。上面代碼技巧幾乎在所有的系統文件被用到。

4.弱引用和強引用

先看一段代碼:

#include <stdio.h>
#include <stdlib.h>
void *malloc(unsigned long size)
{
     printf("I am void *malloc(unsigned long size).\n");
     return NULL;
}

 int main()
{
     char *buf = NULL;
   
     buf = (char *)malloc(10);
     if(NULL == buf)
               printf("failed.\n");
     else
     {
               printf("%p.\n", buf);
               free(buf);         
     }
     return 0;
}

編譯:gcc -g -Wall -Werror test.c -o test 正確無錯誤輸出
運行:./test
運行結果:I am void *malloc(unsigned long size). 正確

按照我們上面的說法C語言不支持同名函數,上面的函數應該報錯才對。

這就涉及到了強引用和弱應用的概念。
強引用:若函數未定義,則鏈接時,鏈接器找不到函數位置報錯;
而對于弱引用則不會報錯,鏈接器默認函數地址為0。我們可以通過attribute((weak))來聲明一個外部函數的應用為弱應用。下面,我們舉一個例子來說明。

強引用實例:

int fun(int a);

int main()
{
    fun(1);
    return 0;
}

編譯后報錯: undefined reference to `fun',鏈接器找不到fun
弱引用實例:

 __attribute__((weak)) int fun(int a);

int main()
{
    fun(1);
    return 0;
}

編譯不報錯,運行報錯:段錯誤。當main函數調用fun函數時,fun函數入口地址為0,發生了非法地址訪問。改進:

 __attribute__((weak)) int fun(int a);
    
int main()
{
    if(fun) fun(1);
    return 0;
}

弱引用對于庫來說十分重要。從上面的強弱引用的特點可看出:

  1. 當一個函數為弱引用時,不管這個函數有沒有定義,鏈接時都不會報錯,而且我們可以根據判斷函數名是否為0來決定是否執行這個函數,這些函數的庫就可以以模塊、插件的形式和我們的引用組合一起,方便使用和卸載;
  2. 并且由于強引用可以覆蓋弱引用可知,我們自己定義函數可以覆蓋庫中的函數。以下,我們給出一個例子予以說明。

fun_c.c

#include "weakref_test.h"

int fun(int a);
int fun(int a)
{
    printf("This is int fun(int a)\n");
}

int main()
{
    fun(1);
    return 0;
}

weakref_test.h

#include <stdio.h>
#include <stdlib.h>

__attribute__ ((weakref)) int fun(int a);

weakref_test.c

#include "weakref_test.h"

__attribute__ ((weakref)) int fun(int a)
{
    printf("This is __attribute__ ((weakref)) int fun(int a)\n");
}

編譯后運行:This is int fun(int a)。

這個例子從說明了這一節開頭拋出的問題,malloc在stdlib庫中的定義為弱應用,“重寫”的malloc為強引用,覆蓋了stdlib庫中的弱引用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 喜歡的朋友可以關注收藏一下 本文實現了一個date類型,原版要求如下(括號為超出要求附加):為Date類實現如下成...
    Effortsto2017閱讀 536評論 0 1
  • 概述:聲明是將一個名稱引入一個程序.定義提供了一個實體在程序中的唯一描述.聲明在單個作用域內可以重復多次(類成員除...
    抓兔子的貓閱讀 642評論 0 3
  • 1.面向對象的程序設計思想是什么? 答:把數據結構和對數據結構進行操作的方法封裝形成一個個的對象。 2.什么是類?...
    少帥yangjie閱讀 5,031評論 0 14
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,354評論 0 6
  • 卿可知否, 千百年前的一次回首, 定格心中不變的永久。 煙花燦爛的橋頭, 垂柳飛舞的雨后, 娉婷嬌俏的佳人, 一眼...
    櫻花子閱讀 197評論 0 1