extern "C"的作用
extern "C"的作用(一)
前些天,編程式是用到了很久以前寫的C程式,想把裡面的函數利用起來,串連發現出現了找不到具體函數的錯誤:
以下是假設舊的C程式庫
C的標頭檔
/*-----------c.h--------------*/
#ifndef _C_H_
#define _C_H_
extern int add(int x, int y);
#endif
C的源檔案
/*-----------c.c--------------*/
int add(int x, int y){
return x+y;
}
C++的調用
/*-----------cpp.cpp--------------*/
#include "c.h"
void main()
{
add(1, 0);
}
這樣編譯會產生錯誤cpp.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z),原因是找不到add的目標模組
這才令我想起C++重載的函數命名方式和C函數的命名方式,讓我們回顧一下:C中函數編譯後命名會在函數名前加以"_",比如add函數編譯成obj檔案時的實際命名為_add,而c++命名則不同,為了實現函數重載同樣的函數名add因參數的不同會被編譯成不同的名字
例如
int add(int , int)==>add@@YAHHH@Z,
float add(float , float )==>add@@YAMMM@Z,
以上是VC6的命名方式,不同的編譯器會不同,總之不同的參數同樣的函數名將編譯成不同目標名,以便於函數重載是調用具體的函數.
編譯cpp.cpp中編譯器在cpp檔案中發現add(1, 0);的調用而函式宣告為extern int add(int x, int y);編譯器就決定去找add@@YAHHH@Z,可惜他找不到,因為C的源檔案把extern int add(int x, int y);編譯成_add了;
為瞭解決這個問題C++採用了extern "C",這就是我們的主題,想要利用以前的C程式庫,那麼你就要學會它,我們可以看以下標準標頭檔你會發現,很多標頭檔都有以下的結構
#ifndef __H
#define __H
#ifdef __cplusplus
extern "C" {
#endif
extern int f1(int, int);
extern int f2(int, int);
extern int f3(int, int);
#ifdef __cplusplus
}
#endif
#endif /*__H*/
如果我們仿製該標頭檔可以得到
#ifndef _C_H_
#define _C_H_
#ifdef __cplusplus
extern "C" {
#endif
extern int add(int, int);
#ifdef __cplusplus
}
#endif
#endif /* _C_H_ */
這樣編譯
/*-----------c.c--------------*/
int add(int x, int y){
return x+y;
}
這時源檔案為*.c,__cplusplus沒有被定義,extern "C" {}這時沒有生效對於C他看到只是extern int add(int, int);
add函數編譯成_add(int, int);
而編譯c++源檔案
/*-----------cpp.cpp--------------*/
#include "c.h"
void main()
{
add(1, 0);
}
這時源檔案為*.cpp,__cplusplus被定義,對於C++他看到的是extern "C" {extern int add(int, int);}編譯器就會知道 add(1, 0);調用的C風格的函數,就會知道去c.obj中找_add(int, int)而不是add@@YAHHH@Z;
這也就為什麼DLL中常看見extern "C" {},windows是採用C語言編製他首先要考慮到C可以正確調用這些DLL,而使用者可能會使用C++而extern "C" {}就會發生作用
extern "C"的作用(二)
一、修飾名(Decorated Name)
C/C++程式中的函數在內部是通過修飾名來標識的.修飾名是在函數定義或原型編譯階段由編譯器建立字串.當你在LINK等工具中要指定一個函數名時,會用到修飾名.
1、使用修飾名:
大多數情況下,你不必知道函數的修飾名是什麼.連接器等工具通常都能處理函數未修飾的名字.然而,在有些情況下,你可能需要指定函數的修飾名.對於C++重載函數和特定的成員函數(如:建構函式和解構函式),你必須指定這些函數的修飾名,以便連接器等工具能夠匹配名字.同時,你也必須在那些引用c或c++函數名的彙編源檔案中使用修飾名.
2、查看修飾名:
如果你編譯了一個源檔案,該源檔案中包含了函數定義或原型,你可以獲得函數的修飾名形式.
(1)用編譯器列表(compiler listing)來查看:
(i)通過將列表檔案類型編譯器選項(/FA[c|s]) 設定為下面中的一種,來產生列表檔案:Assembly with Machine Code (/FAc); Assembly with Source Code (/FAs); Assembly, Machine Code, and Source (/FAcs).
(ii)在產生的列表檔案中,找到包含未經修飾的函數定義的行.
(iii)尋找前面一行.PROC NEAR 命令標籤前就是函數名經過修飾後的形式.
(2)使用DUMPBIN工具來查看:
在.OBJ或.LIB上運行 DUMPBIN,使用/SYMBOLS選項.在輸出中尋找未經修飾的函數定義.後面跟著的就是經過修飾的函數名,用圓括弧括起來的.
二、替代串連說明:
如果在c++中編寫一個程式需要用到c的庫,那該如何?如果這樣聲明一個c函數:
void f(int a,char b);
c++編譯器就會將這個名字變成相應的修飾名,比如:?f@@YAXHD@Z.
然而,c編譯器編譯的庫的內建函式名(連接器使用)是完全不同的.這樣,當c++連接器串連c的函數庫時,將會產生內部使用函數不匹配.
故,c++中提供了一個替代串連說明(alternate linkage specification),它是通過重載extern關鍵字來實現的.
extern後跟一個字串來指定想聲明的函數的連線類型,後面是函式宣告,比如:
extern "C" void f(int a,char b);
這樣,就是告訴編譯器是c串連,這樣就不會轉換函式名了.此例中,編譯後的內建函式名是_f.