同一程序中混合調用C和C++代碼

// 覺得這篇文章寫的還可以比較詳細有點學究的味道所以就翻譯過來。C++C混合編碼雖然不難理解,但C庫、C++庫、extern "C"extern "C++"#inlcude <stdio.h>#include <CStdio>等等,區別起來也有點困難。發生誤解的根源在於沒有把編譯和連接理解透徹。一個程序使用了某個函數,不管該函數是在某個頭文件中定義的函數,還是通過extern定義的外部函數,還是本地已經定義好的函數,該函數都要經過編譯、連接兩個步驟。在編譯階段,C++編譯器會根據函數返回類型、參數類型等,進行函數名修飾;之後纔會根據修飾後的函數名,進行連接。(注意函數名修飾發生在編譯階段)因此,在定義可同時被CC++使用的頭文件時,要考慮到CC++編譯器的編譯過程,綜合使用extern "C"、#ifdef __cplusplus(所有C++編譯器都會預定義這個頭文件)來聲明該頭文件。

// 本文中:源代碼(Source),程序(Program)是指未編譯的程序;代碼(Code)應該指的是頭文件(.H)加庫(.LIB / .DLL)的組合。
C++語言提供了這種機制:它允許在同一個程序中有C編譯器C++編譯器編譯的代碼(程序庫)混合存在。本文主要解決由於CC++代碼混合使用所引起的一些通用問題,同時註明了幾個容易引起的誤區。
主要內容
-使用可兼容的編譯器
C++源程序中調用C代碼
C源程序中調用C++代碼
-混合IOstreamC標準I/O        
-函數指針的處理
C++異常的處理
-程序的連接
 
1. 使用可兼容的編譯器
 
本文的討論建立在這樣的基礎上:所使用CC++編譯器是兼容的;它們都以同種方式定義intfloatpointer等數據類型。
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頭文件如同時支持CC++的頭文件時需要把所有的聲明放在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 bufclass mybuflayout可能會隨不同的C++編譯器而不同,但是這段操作mybufbufC++源代碼也能到哪裏都工作。如下是這種派生的源代碼,它也隱含了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 bufC代碼中,能很好的工作,當然,如果給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;
}
 
可以這樣定義一個能同時被CC++使用的頭文件:
 
#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. 混合IOstreamC標準I/O
 
C++程序中可以通過C標準頭文件<stdio.h>使用C標準I/O因爲C標準I/OC++的一部分。程序中混合使用IOstream和標準I/O與程序是否含有C代碼沒有必然聯繫。
C++標準說可以在同一個目標stream上混合C標準I/OIOstream流,例如標註輸入流、標準輸出流,這一點不同的C++編譯器實現卻不盡相同,有的系統要求用戶在進行I/O操作前顯式地調用sync_with_stdio()。其它還有程序調用性能方面的考慮。
 
6. 如何使用函數指針
 
必須確定一個函數指針究竟是指向C函數還是C++函數。因爲CC++函數採用不同的調用約定。如果不明確指針究竟是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型的函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章