混合调用C/C++代码

来源:互联网 发布:常用平面设计软件 编辑:程序博客网 时间:2024/05/16 08:19
// 觉得这篇文章写的还可以,比较详细有点学究的味道,所以就翻译过来。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型的函数。