// 覺得這篇文章寫的還可以,比較詳細有點學究的味道,所以就翻譯過來。C++和C混合編碼雖然不難理解,但C庫、C++庫、extern "C"、extern "C++"、#inlcude <stdio.h>、#include <CStdio>等等,區別起來也有點困難。發生誤解的根源在於沒有把編譯和連接理解透徹。一個程序使用了某個函數,不管該函數是在某個頭文件中定義的函數,還是通過extern定義的外部函數,還是本地已經定義好的函數,該函數都要經過編譯、連接兩個步驟。在編譯階段,C++編譯器會根據函數返回類型、參數類型等,進行函數名修飾;之後纔會根據修飾後的函數名,進行連接。(注意函數名修飾發生在編譯階段)。因此,在定義可同時被C、C++使用的頭文件時,要考慮到C、C++編譯器的編譯過程,綜合使用extern "C"、#ifdef __cplusplus(所有C++編譯器都會預定義這個頭文件)來聲明該頭文件。
// 本文中:源代碼(Source),程序(Program)是指未編譯的程序;代碼(Code)應該指的是頭文件(.H)加庫(.LIB / .DLL)的組合。
C++語言提供了這種機制:它允許在同一個程序中有“C編譯器”和“C++編譯器”編譯的代碼(程序庫)混合存在。本文主要解決由於C和C++代碼混合使用所引起的一些通用問題,同時註明了幾個容易引起的誤區。
主要內容
-使用可兼容的編譯器
-C++源程序中調用C代碼
-C源程序中調用C++代碼
-混合IOstream和C標準I/O
-函數指針的處理
-C++異常的處理
-程序的連接
1. 使用可兼容的編譯器
本文的討論建立在這樣的基礎上:所使用C和C++編譯器是兼容的;它們都以同種方式定義int、float、pointer等數據類型。
C編譯器所使用的C運行時庫也要和C++編譯器兼容。C++包含了C運行時庫,視爲它的一個子集。如果C++編譯器提供它自己的C版本頭文件,這些頭文件也要和C編譯器兼容。
2. 從C++源程序中調用C代碼
C++語言爲了支持重載,提供了一種連接時的“函數名修飾”。對C++文件(.CPP)文件的編譯、連接,缺省採用的是這種“C++的方式”,但是所有C++編譯器都支持“C連接”(無函數名修飾)。
當需要調用“C連接”(由C編譯器編譯得到的)時,即便幾乎所有C++編譯器對“數據”的“連接修飾”與C編譯器無任何差異,但還是應該在C++代碼中聲明“C連接”;指向函數的指針沒有“C連接”或“C++連接”。
能夠對“連接修飾”進行嵌套,如下,這樣不會創建一個scope,所有函數都處於同一個全局scope。
extern "C" {
void f(); // C linkage
extern "C++" {
void g(); // C++ linkage
extern "C" void h(); // C linkage
void g2(); // C++ linkage
}
extern "C++" void k(); // C++ linkage
void m(); // C linkage
}
如果使用C庫及其對應的.H頭文件,往往可以這樣做:
extern "C" {
#include "header.h";
}
建立支持多語言的.H頭文件,如同時支持C和C++的頭文件時,需要把所有的聲明放在extern "C"的大括號裏頭,但是“C編譯器”卻不支持 " extern "C" "這種語法。每一個C++編譯器都會預定義__cplusplus宏,可以用這個宏確保C++的語法擴展。
#ifdef __cplusplus
extern "C" {
#endif
/* body of header */
#ifdef __cplusplus
}
#endif
假如想在C++代碼中更加方便的使用C庫,例如在C++類的成員函數/虛函數中使用"C庫",怎樣確保"C庫"中的函數能夠正確識別出"C++"的類?利用extern "C"可以這樣做:
struct buf {
char* data;
unsigned count;
};
void buf_clear(struct buf*);
int buf_print(struct buf*);
int buf_append(struct buf*, const char*, unsigned count);
在C++中可以方便的使用這個結構,如下:
extern "C" {
#include "buf.h";
}
class mybuf {
public:
mybuf() : data(0), count(0) {}
void clear() { buf_clear((buf*)this); }
bool print() { return buf_print((buf*)this); }
bool append()...
private:
char* data;
unsigned count;
} ;
提供給class mybuf的接口看起來更像C++的Code,它能夠更加容易的被集成到面向對象編程中。但是,這個例子是在沒有虛函數、且類的數據區開頭沒有冗餘數據的情況下。
另一個可供替代的方案是,保持struct buf的獨立性,而從其派生出C++的類。當傳遞指針到struct buf的成員函數時,即使指向mybuf的指針數據與struct buf位置不完全吻合,C++編譯器也會自動調整,把類的類型協變到struct buf。class mybuf的layout可能會隨不同的C++編譯器而不同,但是這段操作mybuf和buf的C++源代碼也能到哪裏都工作。如下是這種派生的源代碼,它也隱含了struct結構具有的面向對象的特性:
extern "C" {
#include "buf.h"
}
class mybuf : public buf { // a portable solution
public:
mybuf() : data(0), count(0) { }
void clear() { buf_clear(this); }
bool print() { return buf_print(this); }
bool append(const char* p, unsigned c)
{ return buf_append(this, p, c); }
};
C++代碼能夠自由地使用mybuf類,傳遞自身到struct buf的C代碼中,能很好的工作,當然,如果給mybuf加入了別的成員變量,C代碼是不知道的。這是“派生類”的一種常規設計思路。
3. 從C源代碼中調用C++代碼
如果聲明C++函數採用“C連接”,那麼它就能夠被"C代碼"引用,前提是這個函數的參數和返回值必須能夠被"C代碼"所接受。如果該函數接受一個IOStream的類作爲參數,那麼C將不能使用,因爲C編譯器沒有沒有C++的這個模板庫。下面是一個C++函數採用“C連接”的例子:
#include <iostream>
extern "C" int print(int i, double d)
{
std::cout << "i = " << i << ", d = " << d;
}
可以這樣定義一個能同時被C和C++使用的頭文件:
#ifdef __cplusplus
extern "C"
#endif
int print(int i, double d);
對於C++同名重載函數,利用extern "C"聲明時,最多隻能聲明“重載函數系列”中的一個函數。如果想引用所有重載的函數,就需要對C++重載的函數外包一個Wrapper。代碼實例如下:
int g(int);
double g(double);
extern "C" int g_int(int i) { return g(i); }
extern "C" double g_double(double d) { return g(d); }
wrapper的頭文件可以這樣寫:
int g_int(int);
double g_double(double);
模板函數不能用extern "C"修飾,也可以採取wrapper的方式,如下:
template<class T> T foo(T t) { ... }
extern "C" int foo_of_int(int t) { return foo(t); }
extern "C" char* foo_of_charp(char* p) { return foo(p); }
4. 從C代碼中訪問C++的類
能否聲明一個類似與C++類的Struct,從而調用其成員函數,達到C代碼訪問C++類的目的呢?答案是可以的,但是,爲了保持可移植性,必須要加入一個兼容的措施。修改C++類時,也要考慮到調用它的C代碼。加入有一個C++類如下:
class M {
public:
virtual int foo(int);
// ...
private:
int i, j;
};
在C代碼中無法聲明Class M,最好的方式是採用指針。C++代碼中聲明如下:
extern "C" int call_M_foo(M* m, int i) { return m->foo(i); }
在C代碼中,可以這樣調用:
struct M; /* you can supply only an incomplete declaration */
int call_M_foo(struct M*, int); /* declare the wrapper function */
int f(struct M* p, int j) /* now you can call M::foo */
{ return call_M_foo(p, j); }
5. 混合IOstream和C標準I/O
C++程序中,可以通過C標準頭文件<stdio.h>使用C標準I/O,因爲C標準I/O是C++的一部分。程序中混合使用IOstream和標準I/O與程序是否含有C代碼沒有必然聯繫。
C++標準說可以在同一個目標stream上混合C標準I/O和IOstream流,例如標註輸入流、標準輸出流,這一點不同的C++編譯器實現卻不盡相同,有的系統要求用戶在進行I/O操作前顯式地調用sync_with_stdio()。其它還有程序調用性能方面的考慮。
6. 如何使用函數指針
必須確定一個函數指針究竟是指向C函數還是C++函數。因爲C和C++函數採用不同的調用約定。如果不明確指針究竟是C函數還是C++函數,編譯器就不知道應該生成哪種調用代碼。如下:
typedef int (*pfun)(int); // line 1
extern "C" void foo(pfun); // line 2
extern "C" int g(int) // line 3
...
foo( g ); // Error! // line 5
第一行聲明一個C++函數指針(因爲沒有link specifier);
第二行聲明foo是一個C函數,但是它接受一個C++函數指針;
第三行聲明g是一個C函數;
第五行出現類型不匹配;
解決這個問題可以如下:
extern "C" {
typedef int (*pfun)(int);
void foo(pfun);
int g(int);
}
foo( g ); // now OK
當把linkage specification作用於函數參數或返回值類型時,函數指針還有一個難以掌握的誤區。當在函數參數聲明中嵌入一個函數指針的聲明時,作用於函數聲明的linkage specification 也會作用到這個函數指針的聲明中。如果用typedef聲明的函數指針,那麼這個聲明可能會失去效果,如下:
typedef int (*pfn)(int);
extern "C" void foo(pfn p) { ... } // definition
extern "C" void foo( int (*)(int) ); // declaration
假定前兩行出現在源程序中。
第三行出現在頭文件中,因爲不想輸出一個私有定義的typedef。儘管這樣做的目的是爲了使函數聲明和定義吻合,但結果卻是相反的。foo的定義是接受一個C++的函數的指針,而foo的聲明卻是接受一個C函數指針,這樣就構成兩個同名函數的重載。爲了避免這種情況,應該使typedef緊靠函數聲明。例如,如果想聲明foo接受一個C函數指針,可以這樣定義:
extern "C" {
typedef int (*pfn)(int);
void foo(pfn p) { ... }
};
7. 處理C++異常
C函數調用C++函數時,如果C++函數拋出異常,應該怎樣解決呢?可以在C程序使用用long_jmp處理,只要確信long_jmp的跳轉範圍,或者直接把C++函數編譯成不拋出異常的形式。
8. 程序的連接
過去大部分C++編譯器要求把main()編譯到程序中,目前這個需求已經不太普遍。如果還需要,可以通過更改C程序的main函數名,在C++通過wrapper的方式調用。例如,把C程序的main函數改爲
C_main,這樣寫C++程序:
extern "C" int C_main(int, char**); // not needed for Sun C++
int main(int argc, char** argv)
{
return C_main(argc, argv);
}
當然,C_main必須在C程序中被聲明爲返回值爲int型的函數。