混编C和C++

Table of Contents

C++代码中嵌入C语言,是可能的;C语言中嵌入C++也是可能的。这里将介绍如何将它们混编,如何解决混编时出现的问题。

有些编译器不支持混编,但clang和gcc都是支持的,这里使用的就是这两种编译器。

1 声明链接规范

混编C和C++是C++提供的功能,所以只能在C++代码中想办法。C++编译器允许在声明中带 extern "C" ,表示按照C的方式链接,这样声明的代码可以与C交互。 extern "language" 是可以嵌套的:

extern "C" {
  void a(); // C linkage
  extern "C++" {
    void b(); // C++ linkage
    extern "C" void c(); //  C linkage
  }
  extern "C++" void d(); // C++ linkage  
}

2 C++中访问C

C++中引用C必须在C的声明中带 extern "C" 。最简单的引用C语言代码的方式,是在C++ 原代码文件中,在包含头文件的地方使用 extern "C" :

// 通过将头文件包含包括在 extern "C" 中的方式,
// 将C代码的声明全部指定为 extern "C"
extern "C" {
#include "header_of_c.h"
}
// ...
// 之后便可以正常使用 C 函数
std::cout << c_code_get_string() << std::endl;

如果你希望你的C代码可以被C或C++引用,可以在C头文件中将所有声明放在 extern "C" 大括号里,但C编译器不认识这个语法,需要在C编译器使用头文件的时候,就排除掉 extern "C" ,C++编译器使用头文件的时候,就包含 extern "C" 。所有的C++编译器都预定义了宏 __cplusplus ,所以在C的头文件可以是如下结构:

// 这是几乎每个头文件都需要的,保证头文件只被包含一次
#ifndef xxx_HEADER_GUARD_H_
#define xxx_HEADER_GUARD_H_

// 如果使用C++编译器,则将头文件中所有声明包含在 extern "C" 中
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus

// ... body of header

#ifdef __cplusplus
} // closing brace for extern "C"
#endif // __cplusplus

#endif // xxx_HEADER_GUARD_H_

有时候,你可能希望使用C++的方式使用一个C的数据结构,这就需要使用派生。声明一个 C++的类,以C的数据结构为父类,再定义成员函数,在成员函数中直接调用C的操作函数即可。

// 在 C 代码 buf.h 中
struct buf {
  char *data;
  size_t len;
};
int buf_print(struct buf* buf);
int buf_compare(struct buf* a, struct buf* b);
// ...
// 在C++代码中
extern "C" {
#include "buf.h"
}
class BufInCPP : public buf {
  BufInCPP(): data(0), len(0) {}
  int print() { return buf_print(this); }
  int compare() { return buf_compare(this); }
};

这样,就将一个C的数据结构转化为C++的数据结构。这不是必须的,但这能让C++代码拥有更统一的接口。需要注意的是,在析构结构时,要认真考虑是否需要让C代码释放内存(free)。

3 在C中访问C++

用 extern "C" 声明的函数可以被C访问。例如,有一个C++函数:

int print(int i, double d) {
  std::cout << "i=" << i << ", d=" << d;
}

在头文件中,可以将其声明为 extern "C" :

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

这样,C语言代码只要包含这个头文件,就可以使用 print() 函数了。

C++是支持多态和模板的,同一个函数名可以输入不同的参数类型,但C不能那么做,需要区分函数名才能使用多态或者模板的C++函数。例如,在C++代码中,有一个模板函数f(),要让C语言能访问这个函数,需要重新写几个函数包含它,曲线救国:

template<typename T>
T f(T v) {/* ... */}

int f_int(int v) { return f(v); }
double f_double(double v) { return f(v); }

在和C语言共享的头文件中,需要添加 extern "C" 声明:

#ifdef __cplusplus
extern "C" {
#endif

int f_int(int v);
double f_double(double v);

#ifdef __cplusplus
}
#endif

4 C访问C++类

要访问C++类,最好的办法是将C++类包含在C的数据结构中,或者在提供C的接口时,使用 void* 来表示对象的类型。也有直接使用的办法,没有多少区别。

假设有一个C++类:

class A {
public:
  int f();
// ...
};
// 定义C语言接口函数
int A_f(A* a) { return a->f(); }

在C的头文件中,要这样声明:

#ifdef __cplusplus
extern "C" {
#endif
struct A;
int A_f(struct A); 
#ifdef __cplusplus
}
#endif

这样就可以通过调用 A_f() 来调用 A::f() 了。

5 函数指针

函数指针在混编C和C++时容易出错,C++有时候不知道函数指针参数到底是C还是C++。

typedef int (*fn)(int);
extern "C" void caller(fn);
extern "C" int my_fn(int);
// ...
caller(my_fn); // 编译错误

上面的例子中,fn 是指向C++函数的指针,my_fn 是 C 的函数,这是个类型错误。要编译通过,只需将相关函数都声明成C的链接方式:

extern "C" {
typedef int (*fn)(int);
void caller(fn);
int my_fn(int);
}
caller(my_fn); // 编译成功

6 异常和long_jmp

最好的方法就是不要抛出异常,不要使用 long_jmp。如果必须要使用的话,异常不要超出 C++的范围,long_jmp不要跨越C++代码。

7 编译和链接

C语言代码使用 C 编译器编译,C++代码使用C++编译器编译。生成的 .o 或者 .a 文件正常使用C编译器链接即可。

CC -c a.c
CXX -c b.cpp
CC -o programname a.o b.o

By .