- 结合 C 和 C++ 代码
- 有一些关键点(即使一些编译器尝试不要求,检查编译器厂商的文件)
- 当编译
main()
时必须使用 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++ 中用
extern "C"
声明 C++ 函数,然后在 C/C++ 中调用
extern "C" void f(int);
void f(int i)
{
//...
}
void f(int);
void ccode(int i)
{
f(i);
//...
}
- 如果需要在 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 语言提供不同名称的封装函数
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++ 编译器通过添加参数信息修改名称来区分不同的函数。这个添加额外信息到函数名的技术叫做名称重整(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 不支持函数重载,名称不会被重整。当把代码放到
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()
- 直接使用
#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 头文件,需要使用
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 头文件,且 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 头文件,可以在 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++ 编译器必须通过
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 程序中同时调用多个重载的函数
- 如果 `extern "C" 语法不正确,会有一些链接错误而不是编译器错误。因为 C++ 编译器通常会重整(mangle) 函数名称(比如为了支持函数重载)而跟 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++ 对象的数据
- 没有虚函数(包括继承的虚函数)
- 所有数据的访问权限相同(private/protected/public)
- 不含带有虚函数的完全包含的子对象
- 如果 C++ 类由任何基类(或者其完全包含的子对象有基类),对这些数据的访问都是不可移植的。因为语言不会暴露带有继承的类格式。但实际上,所以的 C++ 编译器的处理方式相同:基类对象在前面(多重继承时按照从左至右的顺序),然后是成员对象
- 此外,如果该类(或任何基类)包含虚函数,几乎所有 C++ 编译器会在对象内放置一个 void*,或者是在第一个虚函数的位置,或者是在对象最开始的位置。这一点也不是语言要求的,但是每个语言都这样处理
- 如果类包含需基类,情况更加复杂且更难移植。一个通用的实现技术是在对象最后包含一个虚基类的对象(不管虚基类在继承中的层次结构)。对象的其他部分还是正常的顺序。每个衍生的类实际上有一个指向虚基类的指针???
- 作为面向对象(OO)的编程语言,C++ 支持模型化问题域,这支持在问题域的语言进行编程而不是使用解决方案域的语言编程
- C 的一个很大优势是没有隐藏机制:看到的就是得到的。可以阅读一个 C 程序并看到每一个时钟周期。但 C++ 不支持。虽然 C++ 为编程者隐藏了一些机制,它也提供了一个抽象层和表达上的经济,以便降低维护成本且不会破坏运行时性能