结合 C 和 C++ 代码
- 需要了解的知识
- 在 C++ 中调用 C 语言函数
- 在 C 中调用 C++ 语言函数
- 在 C++ 中包含一个标准的 C 头文件
- 在 C++ 中包含一个非系统的 C 头文件
- 修改自己的 C 头文件以便 C++ 容易使用 include 语句包含
- 在 C++ 中调用非系统的 C 函数 f(int,char,float)
- 创建一个可被 C 调用的 C++ 函数 f(int,char,float)
- 链接器在 C/C++ 调用 C++/C 函数时报错的原因
- 传递一个 C++ 类的对象给/从 C 函数
- C 函数是否可以直接访问 C++ 类对象的数据
- 使用 C++ 比 C 更觉得远离机器代码
- 参考
需要了解的知识
- 有一些关键点(即使一些编译器尝试不要求,检查编译器厂商的文件)
- 当编译
main()
时必须使用 C++ 编译器(比如为了静态初始化) - C++ 编译器应该管理链接过程(这样才可以得到指定的库)
- C 和 C++ 编译器可能需要来自同一厂商,且具有兼容版本(这样才有相同的调用惯例)
- 当编译
- 除此之外,你需要阅读剩余的章节以找到 可被 C/C++ 调用的 C++/C 函数
- 有一个方式可以解决所有的问题:即使用 C++ 编译器编译所有的代码(即使是 C 风格的代码)
- 优点:可以解决结合 C 和 C++ 代码的问题,也更容易发现 C 代码的错误
- 缺点:需要更新 C 风格的代码,原因
- 但是更细代码的代价可能比结合二者的代价更小,同时可以清除 C 风格的代码
在 C++ 中调用 C 语言函数
在 C++ 中用
extern "C"
声明 C 函数,然后在 C/C++ 中调用extern "C" void f(int);//方式1 extern "C" {//方式2 int g(double); double h(); }; void cppode(int i, double d) { f(i); int ii = g(d); double dd = h(); //... };
//c code void f(int i) { //... } int g(double d) { //... } double h() { //... }
在 C 中调用 C++ 语言函数
在 C 中调用 C++ 非成员函数
在 C++ 中用
extern "C"
声明 C++ 函数,然后在 C/C++ 中调用extern "C" void f(int); void f(int i) { //... }
void f(int); void ccode(int i) { f(i); //... }
在 C 中调用 C++ 成员函数
如果需要在 C 中调用成员函数(包括虚函数),需要提供一个简单的封装
class C { //... virtual double f(int); }; //封装函数 extern "C" double call_C_f(C* p, int i) { return p->f(i); }
struct C { //... }; double call_C_f(struct C*, int); void ccode(struct C* p, int i) { double d = call_C_f(p, i); //... }
在 C 中调用 C++ 重载函数
如果需要在 C 中调用重载函数,必须为 C 语言提供不同名称的封装函数
void f(int); void f(double) extern "C" void f_i(int); extern "C" void f_d(double);
void f_i(int); void f_d(double); void ccode(int i, double d) { f_i(i); f_d(d); //... }
这种方式适用于在 C 中调用 C++ 库,即使不能修改 C++ 的头文件
C++ 名称重整
- C++ 支持函数重载。比如可以有多个函数名称相同,但参数不同。当生成目标代码时,C++ 编译器通过添加参数信息修改名称来区分不同的函数。这个添加额外信息到函数名的技术叫做名称重整(name mangling)
C++ 标准没有对名称重整指定任何详细的技术,因此不同编译器可能添加不同信息到函数名
int f(void) {return 1;} int f(int) {return 0;} void g(void) (int i=f(), j=f(0);)
上述代码可能被 C++ 编译器改成
int __f_v(void) {return 1;} int __f_i(int) {return 0;} void __g_v(void) (int i=__f_v(), j=__f_i(0);)
C++ 链接器处理 C 符号
- C 不支持函数重载,名称不会被重整。当把代码放到
extern "C"
块时,C++ 编译器确保函数名不会被重整,即编译器生成一个二进制文件,且没有修改函数名 extern "C"
在 C++ 调用 C 时:告诉 g++ 预期得到 gcc 生成的未重整的符号extern "C"
在 C 调用 C++ 时:告诉 g++ 生成未重整的符号给 gcc 使用
测试
反编译一个 g++ 生成的二进制代码
void f() {} void g(); extern "C" { void ef() {} void eg(); } void h() { g(); eg();}
使用 g++ 编译
g++ -c main.cpp
反编译符号表
readelf -s main.o
Symbol table '.symtab' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS cppcode.cpp 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 7 FUNC GLOBAL DEFAULT 1 _Z1fv 9: 0000000000000007 7 FUNC GLOBAL DEFAULT 1 ef 10: 000000000000000e 17 FUNC GLOBAL DEFAULT 1 _Z1hv 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z1gv 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND eg
可以看到:
ef
和eg
在符号表存储的名字和代码中的名字相同;其他的名称被重整过解开(unmangle)这些名字
kiki@ubuntu:/tmp/test$ c++filt _Z1fv f() kiki@ubuntu:/tmp/test$ c++filt _Z1hv h() kiki@ubuntu:/tmp/test$ c++filt _Z1gv g()
在 C++ 中包含一个标准的 C 头文件
直接使用
#include
包含所需头文件,比如#include <cstdio>
#include <cstdio> int main() { printf("Hello!\n");// or std::printf return 0; }
如果使用 C++ 编译器编译 C 代码,又不希望将类似
printf
改成std::printf
,可以在 C 代码中使用旧风格的头文件stdio.h
替换新风格的头文件cstdio
#include <stdio.h> int main() { printf("Hello!\n"); return 0; }
在 C++ 中包含一个非系统的 C 头文件
如果在 C++ 中包含一个非系统提供的 C 头文件,需要使用
extern "C" {/**/}
结构包裹#include
语句,这个告诉 C++ 编译器此头文件声明的函数是 C 函数extern "C" { //get declaration for void sum(int, int) #include "my-c-code.h" } int main() { sum(1, 2); return 0; }
修改自己的 C 头文件以便 C++ 容易使用 include 语句包含
- 如果在 C++ 中包含一个非系统提供的 C 头文件,且 C 头文件可以修改,强烈建议在头文件添加
extern "C" {/**/}
语句使得 C++ 开发者更加容易使用#include
包含此头文件到 C++ 代码 因为 C 编译器不理解
extern "C" {/**/}
结构,必须使用#ifdef
包裹extern "C" {
和}
,以便 C 编译器看不到这个结构//my-c-code.h //有且只有 C++ 编译器会定义 __cplusplus 符号 #ifdef __cplusplus extern "C" { #endif //c code #ifdef __cplusplus } #endif }
#include "my-c-code.h" int main() { sum(1, 2); return 0; }
在 C++ 中调用非系统的 C 函数 f(int,char,float)
如果需要调用一个 C 函数,但是不需要或者不想包含声明此函数的 C 头文件,可以在 C++ 代码中使用
extern "C"
语法声明这一个 C 函数。一般需要使用完整的函数原型extern "C" void f(int,char,float); int main() { int i=1; char c='c'; float ff=2; f(i, c, ff); return 0; }
多个 C 函数可以使用
extern "C" {/**/}
创建一个可被 C 调用的 C++ 函数 f(int,char,float)
C++ 编译器必须通过
extern "C"
知道f(int,char,float)
会被 C 编译器调用extern "C" void f(int,char,float); //define f(int,char,float) in some c++ module void f(int i, char c, float ff) [ //... ]
extern "C"
告诉编译器发送给链接器的外部信息应该使用 C 的调用管理和名称重整(name-mangling)(比如前置一个下划线)。因为 C 不支持名称重载,你不能在 C 程序中同时调用多个重载的函数
链接器在 C/C++ 调用 C++/C 函数时报错的原因
- 如果 `extern “C” 语法不正确,会有一些链接错误而不是编译器错误。因为 C++ 编译器通常会重整(mangle) 函数名称(比如为了支持函数重载)而跟 C 编译器不同
传递一个 C++ 类的对象给/从 C 函数
// fred.h: this header can be read by c/c++ compilers
#ifndef FRED_H
#define FRED_H
#ifdef __cplusplus
class Fred {
public:
Fred();
void wilma(int);
private:
int a_;
};
#else
typedef struct Fred Fred;
#endif
#ifdef __cplusplus
extern "C" {
#endif
#if defined(__STDC__) || defined(__cplusplus)
extern void c_function(Fred*);///* ANSI C prototypes */
extern Fred* cplusplus_callback_function(Fred*);
#else
extern void c_function();///* K&R style */
extern Fred* cplusplus_callback_function();
#endif
#ifdef __cplusplus
}
#endif
#endif
// fred.cpp
#include "fred.h"
#include <stdio.h>
Fred::Fred() : a_(0)
{
}
void Fred::wilma(int a)
{
a_ = a;
printf("a=%d\n", a);
}
Fred* cplusplus_callback_function(Fred* fred)
{
fred->wilma(123);
return fred;
}
// ccode.c
#include "fred.h"
void c_function(Fred* fred)
{
cplusplus_callback_function(fred);
}
// cppcode.cpp
#include "fred.h"
int main()
{
Fred fred;
c_function(&fred);
return 0;
}
- 编译命令
gcc fred.h fred.cpp ccode.c cppcode.cpp
或gcc fred.h fred.cpp ccode.c cppcode.cpp
,输出a=123
和 C++ 代码不同,C 代码不能识别统一对象的两个指针,除非这两个指针完全是同一类型。比如,C++中容易检查指向同一对象的衍生类指针 dp 和基类指针 bp
- 判断
if(dp == bp)
,C++ 编译器会自动转化两个指针到同一类型,这种情况下到基类指针,然后进行比较。这取决于 C++ 编译器的具体实现,有时候这种转化会改动一个指针值的比特位 - 技术层面上,大部分 C++ 编译器使用一个二进制对象格式以便多继承和/或虚继承的转换。但是 C++ 语言不会暴露对象格式,因此从原则上说,一个转化也会发生在非虚单继承
- 关键点是 C 编译器不知道如何做指针转换,所以从衍生类到基类指针的转换必须发生在 C++ 编译器编译的代码,而不是 C 编译器编译的代码
注意:当转换衍生类和基类指针到 void* 时必须要小心,因为这个不支持 C/C++ 编译器做适合的指针调整
void f(Base* bp, Derived *dp) { if(bp ==dp) //valid to compare a Base* to Derived* { //... } void* xp = bp; void* yp = dp; if(x == y) //bad form! do not use this! { //... } }
如上所述,上述指针转换会发生在多继承和/或虚继承
使用 void* 指针的安全方式
void f(Base* bp, Derived *dp) { void* xp = bp; // If conversion is needed, it will happen in the static_cast<> void* yp = static_cast<Base*>(dp); if(x == y)//valid to compare a Base* to Derived* { //.... } }
- 判断
C 函数是否可以直接访问 C++ 类对象的数据
- 如果一个 C++ 类满足下面的条件,C 函数可以安全访问 C++ 对象的数据
- 没有虚函数(包括继承的虚函数)
- 所有数据的访问权限相同(private/protected/public)
- 不含带有虚函数的完全包含的子对象
- 如果 C++ 类由任何基类(或者其完全包含的子对象有基类),对这些数据的访问都是不可移植的。因为语言不会暴露带有继承的类格式。但实际上,所以的 C++ 编译器的处理方式相同:基类对象在前面(多重继承时按照从左至右的顺序),然后是成员对象
- 此外,如果该类(或任何基类)包含虚函数,几乎所有 C++ 编译器会在对象内放置一个 void*,或者是在第一个虚函数的位置,或者是在对象最开始的位置。这一点也不是语言要求的,但是每个语言都这样处理
- 如果类包含需基类,情况更加复杂且更难移植。一个通用的实现技术是在对象最后包含一个虚基类的对象(不管虚基类在继承中的层次结构)。对象的其他部分还是正常的顺序。每个衍生的类实际上有一个指向虚基类的指针???
使用 C++ 比 C 更觉得远离机器代码
- 作为面向对象(OO)的编程语言,C++ 支持模型化问题域,这支持在问题域的语言进行编程而不是使用解决方案域的语言编程
- C 的一个很大优势是没有隐藏机制:看到的就是得到的。可以阅读一个 C 程序并看到每一个时钟周期。但 C++ 不支持。虽然 C++ 为编程者隐藏了一些机制,它也提供了一个抽象层和表达上的经济,以便降低维护成本且不会破坏运行时性能