標題: 同一程序中混合調(diào)用C和C++代碼 [打印本頁]

作者: 51黑黑黑    時間: 2016-2-22 00:24
標題: 同一程序中混合調(diào)用C和C++代碼
// 覺得這篇文章寫的還可以,比較詳細有點學究的味道,所以就翻譯過來。C++和C混合編碼雖然不難理解,但C庫、C++庫、extern "C"、extern "C++"、#inlcude <stdio.h>、#include <CStdio>等等,區(qū)別起來也有點困難。發(fā)生誤解的根源在于沒有把編譯和連接理解透徹。一個程序使用了某個函數(shù),不管該函數(shù)是在某個頭文件中定義的函數(shù),還是通過extern定義的外部函數(shù),還是本地已經(jīng)定義好的函數(shù),該函數(shù)都要經(jīng)過編譯、連接兩個步驟。在編譯階段,C++編譯器會根據(jù)函數(shù)返回類型、參數(shù)類型等,進行函數(shù)名修飾;之后才會根據(jù)修飾后的函數(shù)名,進行連接。(注意函數(shù)名修飾發(fā)生在編譯階段)。因此,在定義可同時被C、C++使用的頭文件時,要考慮到C、C++編譯器的編譯過程,綜合使用extern "C"、#ifdef __cplusplus(所有C++編譯器都會預定義這個頭文件)來聲明該頭文件。
// 本文中:源代碼(Source),程序(Program)是指未編譯的程序;代碼(Code)應該指的是頭文件(.H)加庫(.LIB / .DLL)的組合。
C++語言提供了這種機制:它允許在同一個程序中有“C編譯器”和“C++編譯器”編譯的代碼(程序庫)混合存在。本文主要解決由于C和C++代碼混合使用所引起的一些通用問題,同時注明了幾個容易引起的誤區(qū)。
主要內(nèi)容
-使用可兼容的編譯器
-C++源程序中調(diào)用C代碼
-C源程序中調(diào)用C++代碼
-混合IOstream和C標準I/O         
-函數(shù)指針的處理
-C++異常的處理
-程序的連接

1. 使用可兼容的編譯器

本文的討論建立在這樣的基礎上:所使用C和C++編譯器是兼容的;它們都以同種方式定義int、float、pointer等數(shù)據(jù)類型。
C編譯器所使用的C運行時庫也要和C++編譯器兼容。C++包含了C運行時庫,視為它的一個子集。如果C++編譯器提供它自己的C版本頭文件,這些頭文件也要和C編譯器兼容。

2. 從C++源程序中調(diào)用C代碼

C++語言為了支持重載,提供了一種連接時的“函數(shù)名修飾”。對C++文件(.CPP)文件的編譯、連接,缺省采用的是這種“C++的方式”,但是所有C++編譯器都支持“C連接”(無函數(shù)名修飾)。
當需要調(diào)用“C連接”(由C編譯器編譯得到的)時,即便幾乎所有C++編譯器對“數(shù)據(jù)”的“連接修飾”與C編譯器無任何差異,但還是應該在C++代碼中聲明“C連接”;指向函數(shù)的指針沒有“C連接”或“C++連接”。

能夠對“連接修飾”進行嵌套,如下,這樣不會創(chuàng)建一個scope,所有函數(shù)都處于同一個全局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++類的成員函數(shù)/虛函數(shù)中使用"C庫",怎樣確保"C庫"中的函數(shù)能夠正確識別出"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,它能夠更加容易的被集成到面向對象編程中。但是,這個例子是在沒有虛函數(shù)、且類的數(shù)據(jù)區(qū)開頭沒有冗余數(shù)據(jù)的情況下。

另一個可供替代的方案是,保持struct buf的獨立性,而從其派生出C++的類。當傳遞指針到struct buf的成員函數(shù)時,即使指向mybuf的指針數(shù)據(jù)與struct buf位置不完全吻合,C++編譯器也會自動調(diào)整,把類的類型協(xié)變到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代碼是不知道的。這是“派生類”的一種常規(guī)設計思路。

3. 從C源代碼中調(diào)用C++代碼

如果聲明C++函數(shù)采用“C連接”,那么它就能夠被"C代碼"引用,前提是這個函數(shù)的參數(shù)和返回值必須能夠被"C代碼"所接受。如果該函數(shù)接受一個IOStream的類作為參數(shù),那么C將不能使用,因為C編譯器沒有沒有C++的這個模板庫。下面是一個C++函數(shù)采用“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++同名重載函數(shù),利用extern "C"聲明時,最多只能聲明“重載函數(shù)系列”中的一個函數(shù)。如果想引用所有重載的函數(shù),就需要對C++重載的函數(shù)外包一個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);

模板函數(shù)不能用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,從而調(diào)用其成員函數(shù),達到C代碼訪問C++類的目的呢?答案是可以的,但是,為了保持可移植性,必須要加入一個兼容的措施。修改C++類時,也要考慮到調(diào)用它的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代碼中,可以這樣調(diào)用:

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代碼沒有必然聯(lián)系。
C++標準說可以在同一個目標stream上混合C標準I/O和IOstream流,例如標注輸入流、標準輸出流,這一點不同的C++編譯器實現(xiàn)卻不盡相同,有的系統(tǒng)要求用戶在進行I/O操作前顯式地調(diào)用sync_with_stdio()。其它還有程序調(diào)用性能方面的考慮。

6. 如何使用函數(shù)指針

必須確定一個函數(shù)指針究竟是指向C函數(shù)還是C++函數(shù)。因為C和C++函數(shù)采用不同的調(diào)用約定。如果不明確指針究竟是C函數(shù)還是C++函數(shù),編譯器就不知道應該生成哪種調(diào)用代碼。如下:

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++函數(shù)指針(因為沒有l(wèi)ink specifier);
第二行聲明foo是一個C函數(shù),但是它接受一個C++函數(shù)指針;
第三行聲明g是一個C函數(shù);
第五行出現(xiàn)類型不匹配;

解決這個問題可以如下:

extern "C" {
    typedef int (*pfun)(int);
    void foo(pfun);
    int g(int);
}
foo( g ); // now OK

當把linkage specification作用于函數(shù)參數(shù)或返回值類型時,函數(shù)指針還有一個難以掌握的誤區(qū)。當在函數(shù)參數(shù)聲明中嵌入一個函數(shù)指針的聲明時,作用于函數(shù)聲明的linkage specification 也會作用到這個函數(shù)指針的聲明中。如果用typedef聲明的函數(shù)指針,那么這個聲明可能會失去效果,如下:

typedef int (*pfn)(int);
extern "C" void foo(pfn p) { ... }     // definition
extern "C" void foo( int (*)(int) );   // declaration

假定前兩行出現(xiàn)在源程序中。
第三行出現(xiàn)在頭文件中,因為不想輸出一個私有定義的typedef。盡管這樣做的目的是為了使函數(shù)聲明和定義吻合,但結果卻是相反的。foo的定義是接受一個C++的函數(shù)的指針,而foo的聲明卻是接受一個C函數(shù)指針,這樣就構成兩個同名函數(shù)的重載。為了避免這種情況,應該使typedef緊靠函數(shù)聲明。例如,如果想聲明foo接受一個C函數(shù)指針,可以這樣定義:

extern "C" {
    typedef int (*pfn)(int);
    void foo(pfn p) { ... }
};

7. 處理C++異常

C函數(shù)調(diào)用C++函數(shù)時,如果C++函數(shù)拋出異常,應該怎樣解決呢?可以在C程序使用用long_jmp處理,只要確信long_jmp的跳轉范圍,或者直接把C++函數(shù)編譯成不拋出異常的形式。

8. 程序的連接

過去大部分C++編譯器要求把main()編譯到程序中,目前這個需求已經(jīng)不太普遍。如果還需要,可以通過更改C程序的main函數(shù)名,在C++通過wrapper的方式調(diào)用。例如,把C程序的main函數(shù)改為
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型的函數(shù)。






歡迎光臨 (http://www.torrancerestoration.com/bbs/) Powered by Discuz! X3.1