找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 2299|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

同一程序中混合調(diào)用C和C++代碼

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

1. 使用可兼容的編譯器

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

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

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

能夠?qū)Α斑B接修飾”進(jìn)行嵌套,如下,這樣不會創(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庫及其對應(yīng)的.H頭文件,往往可以這樣做:
   
extern "C" {
    #include "header.h";
}

建立支持多語言的.H頭文件,如同時(shí)支持C和C++的頭文件時(shí),需要把所有的聲明放在extern "C"的大括號里頭,但是“C編譯器”卻不支持 " extern "C" "這種語法。每一個C++編譯器都會預(yù)定義__cplusplus宏,可以用這個宏確保C++的語法擴(kuò)展。
   
#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++中可以方便的使用這個結(jié)構(gòu),如下:

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,它能夠更加容易的被集成到面向?qū)ο缶幊讨。但是,這個例子是在沒有虛函數(shù)、且類的數(shù)據(jù)區(qū)開頭沒有冗余數(shù)據(jù)的情況下。

另一個可供替代的方案是,保持struct buf的獨(dú)立性,而從其派生出C++的類。當(dāng)傳遞指針到struct buf的成員函數(shù)時(shí),即使指向mybuf的指針數(shù)據(jù)與struct buf位置不完全吻合,C++編譯器也會自動調(diào)整,把類的類型協(xié)變到struct buf。class mybuf的layout可能會隨不同的C++編譯器而不同,但是這段操作mybuf和buf的C++源代碼也能到哪里都工作。如下是這種派生的源代碼,它也隱含了struct結(jié)構(gòu)具有的面向?qū)ο蟮奶匦裕?br />
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代碼中,能很好的工作,當(dāng)然,如果給mybuf加入了別的成員變量,C代碼是不知道的。這是“派生類”的一種常規(guī)設(shè)計(jì)思路。

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

如果聲明C++函數(shù)采用“C連接”,那么它就能夠被"C代碼"引用,前提是這個函數(shù)的參數(shù)和返回值必須能夠被"C代碼"所接受。如果該函數(shù)接受一個IOStream的類作為參數(shù),那么C將不能使用,因?yàn)镃編譯器沒有沒有C++的這個模板庫。下面是一個C++函數(shù)采用“C連接”的例子:

#include <iostream>
extern "C" int print(int i, double d)
{
    std::cout << "i = " << i << ", d = " << d;
}

可以這樣定義一個能同時(shí)被C和C++使用的頭文件:

#ifdef __cplusplus
extern "C"
#endif
int print(int i, double d);

對于C++同名重載函數(shù),利用extern "C"聲明時(shí),最多只能聲明“重載函數(shù)系列”中的一個函數(shù)。如果想引用所有重載的函數(shù),就需要對C++重載的函數(shù)外包一個Wrapper。代碼實(shí)例如下:

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ù),達(dá)到C代碼訪問C++類的目的呢?答案是可以的,但是,為了保持可移植性,必須要加入一個兼容的措施。修改C++類時(shí),也要考慮到調(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標(biāo)準(zhǔn)I/O

C++程序中,可以通過C標(biāo)準(zhǔn)頭文件<stdio.h>使用C標(biāo)準(zhǔn)I/O,因?yàn)镃標(biāo)準(zhǔn)I/O是C++的一部分。程序中混合使用IOstream和標(biāo)準(zhǔn)I/O與程序是否含有C代碼沒有必然聯(lián)系。
C++標(biāo)準(zhǔn)說可以在同一個目標(biāo)stream上混合C標(biāo)準(zhǔn)I/O和IOstream流,例如標(biāo)注輸入流、標(biāo)準(zhǔn)輸出流,這一點(diǎn)不同的C++編譯器實(shí)現(xiàn)卻不盡相同,有的系統(tǒng)要求用戶在進(jìn)行I/O操作前顯式地調(diào)用sync_with_stdio()。其它還有程序調(diào)用性能方面的考慮。

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

必須確定一個函數(shù)指針究竟是指向C函數(shù)還是C++函數(shù)。因?yàn)镃和C++函數(shù)采用不同的調(diào)用約定。如果不明確指針究竟是C函數(shù)還是C++函數(shù),編譯器就不知道應(yīng)該生成哪種調(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ù)指針(因?yàn)闆]有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

當(dāng)把linkage specification作用于函數(shù)參數(shù)或返回值類型時(shí),函數(shù)指針還有一個難以掌握的誤區(qū)。當(dāng)在函數(shù)參數(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)在頭文件中,因?yàn)椴幌胼敵鲆粋私有定義的typedef。盡管這樣做的目的是為了使函數(shù)聲明和定義吻合,但結(jié)果卻是相反的。foo的定義是接受一個C++的函數(shù)的指針,而foo的聲明卻是接受一個C函數(shù)指針,這樣就構(gòu)成兩個同名函數(shù)的重載。為了避免這種情況,應(yīng)該使typedef緊靠函數(shù)聲明。例如,如果想聲明foo接受一個C函數(shù)指針,可以這樣定義:

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

7. 處理C++異常

C函數(shù)調(diào)用C++函數(shù)時(shí),如果C++函數(shù)拋出異常,應(yīng)該怎樣解決呢?可以在C程序使用用long_jmp處理,只要確信long_jmp的跳轉(zhuǎn)范圍,或者直接把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);
}

當(dāng)然,C_main必須在C程序中被聲明為返回值為int型的函數(shù)。

分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享淘帖 頂 踩
回復(fù)

使用道具 舉報(bào)

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機(jī)版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表