首先看一段代碼:
***************************文件名: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++編譯報錯原因:
- gcc編譯器默認將代碼當做C語言去編譯,認為函數名相同的函數為同一個函數,以上代碼中聲明了三個函數名相同的函數,所以gcc編譯器報fun重復定義。
- 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;
}
弱引用對于庫來說十分重要。從上面的強弱引用的特點可看出:
- 當一個函數為弱引用時,不管這個函數有沒有定義,鏈接時都不會報錯,而且我們可以根據判斷函數名是否為0來決定是否執行這個函數,這些函數的庫就可以以模塊、插件的形式和我們的引用組合一起,方便使用和卸載;
- 并且由于強引用可以覆蓋弱引用可知,我們自己定義函數可以覆蓋庫中的函數。以下,我們給出一個例子予以說明。
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庫中的弱引用。