Mixing C and C++ Code in the Same Program
By Stephen Clamage , Sun Microsystems, Sun ONE Studio Solaris Tools Development Engineering
Translator: Qiu Longbin <robin.qiu(at)yeah.net>
C++ 语言提供了一个混合代码的机制,使得代码可以在同一个程序中被兼容的 C 和 C++ 编译器编译。在你移植代码到不同的平台和编译器时,你会体验到不同的成功度。本文展示了当你混合使用 C,C++ 时,如何解决出现的一般的问题。文中所有情况,展示了使用 Sun C 和 C++ 编译器时所要做的事情。(译注: GCC 的 gcc 和 g++ 也是这一对组合。)
-
| |
-
| |
-
| |
-
| |
-
| |
-
| |
-
|
使用兼容的编译器
![]() |
混合代码的第一个要求就是你使用的 C 和 C++ 编译器必须是兼容的。他们必须以同样的方式,(例如),定义了基本类型如 int, float 或指针。 Solaris 操作系统指定了 C 程序的应用程序的二进制接口( ABI ) , 它包含关于基本类型和函数如何被调用的信息。任何 Solaris Os 上可用的编译器都必须遵循 ABI 。
Sun C 和 C++ 编译器遵循 Solaris OS ABI 并且是兼容的。第三方的 Solaris OS C 编译器也必须遵循 ABI 。任何与 Solaris Os 兼容的 C 编译器也同样与 Sun C++ 编译器兼容。
Sun C 和 C++ 编译器使用兼容的头文件,并且使用同样的 C 运行时库。他们是完全兼容的。
![]() |
C++ 语言提供了一个“链接规范( linkage specification )”,用它你可以声明函数或对象遵循特定语言的程序链接约定。对象和函数的默认链接是 C++ 的。所有 C++ 编译器也为兼容的 C 编译器提供了 C 链接。
当你需要访问一个用 C 链接编译的函数(例如,某个函数被 C 编译器编译),就要声明那个函数具备 C 链接(译注:在 C++ 代码中)。即使大多数 C++ 编译器对 C 和 C++ 数据对象的链接没有什么不同,你也需要在你的 C++ 代码中声明 C 数据对象( data objects )具有 C 链接。类型( types )没有 C 或 C++ 链接,除了 指向函数的指针( pointer-to-function )类型。
声明链接规范
extern "language_name
" declaration
;
extern "language_name
" { declaration
; declaration
; ... }
|
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++ 代码中包含 C 头文件
如果你想使用一个 C 库,它定义的头文件意欲为 C 编译器所准备,你可以在 extern “C” 花括号中包含这个头文件:
extern "C" {
#include "header.h"
}
|
Warning- 警告 - 不用为 Solaris OS 上的系统头文件使用该技术。 Solaris 头文件,并且所有赖于 Sun C 和 C++ 编译器的头文件都已经为 C 和 C++ 编译器做好了准备。如果你指定了一个链接,你可能使得声明于 Solaris 头文件中的声明失效。 |
创建混合语言( Mixed-Languge )的头文件
如果你想使得头文件同时适合于 C 和 C++ 编译器,你可能把所有声明都放置在了 extern “C” 花括号中,但是 C 编译器并不认识这些语法。每个 C++ 编译器都预定义了宏 __cplusplus ,这样你就可以使用这个宏来防卫 C++ 语法扩展:
#ifdef __cplusplus
extern "C" {
#endif ... /* body of header */
#ifdef __cplusplus
} /* closing brace for extern "C" */
#endif
|
给 C structs 增加 C++ 特征
假定你想在你的 C++ 代码中更容易地使用 C 库。并且假定你不使用 C 风格的访问方式,你可能想增加成员函数,或许虚函数,也可能从 class 派生等等。你如何完成这个变换并确保 C 库函数仍然能识别你的 struct? 考虑下面这个例子中 C 的 struct buf 的使用:
/* buf.h */
struct buf {
char* data;
unsigned count;
};
void buf_clear(struct buf*);
int buf_print(struct buf*); /* return status, 0 means fail */
int buf_append(struct buf*, const char*, unsigned count); /* same return */
|
你想把这个 struct 转变进 C++ class ,并做下述改变,使它更容易使用:
extern "C" {
#include "buf.h"
}
class mybuf { // first attempt -- will it work?
public:
mybuf() : data(0), count(0) { }
void clear() { buf_clear((buf*)this); }
bool print() { return buf_print((buf*)this); }
bool append(const char* p, unsigned c) { return buf_append((buf*)this, p, c); } private:
char* data;
unsigned count;
};
|
class mybuf 的接口看来更象 C++ 代码,并且更容易整合进面向对象风格的编程 ─ 如果它可行的话。
C++ 标准对 buf 和 class mybuf 的兼容性没有任何保证。这里的代码,没有虚函数,可以工作,但你不能指望这个。如果你增加了虚函数,这个代码会失败,因为编译器增加了额外的数据(比如指向虚表的指针)放在 class 的开始处。
可移植的方案是把 struct buf 单独放着不动它,尽管你想保护数据成员并仅仅通过成员函数提供访问。仅当你不改变声明的情况下,你才能保证 C 和 C++ 的兼容性。
你可以从 C struct buf 派生出一个 C++ class mybuf ,并且传递指向基类 buf 的指针给 mybuf 的成员函数。当转换 mybuf* 到 buf* 时,如果指向 mybuf 的指针没有指向 buf 数据的起始处, C++ 编译器会自动调整它。 mybuf 的布局在 C++ 编译时可能发生改变,但是操纵 mybuf 和 buf 对象的 C++ 源代码将到处都可以工作。下面的例子展示了一个可移植 方法给 C struct 增加 C++ 和面向对象特征:
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++ 函数具有 C 链接,它就可以在由 C 编译器编译的函数中被调用。一个声明具有 C 链接的函数可以使用所有 C++ 的特征,如果你想在 C 代码中访问它,它的参数和返回值必须是在 C 中可访问的。例如,如果一个函数声明有一个 IOstream 类的引用作为参数,就没有(可移植的)方法来解析这个参数类型给 C 编译器。 C 语言没有引用,模板,或具备 C++ 特征的 class.
#include <iostream>
extern "C"
int print(int i, double d)
{
std::cout << "i = " << i << ", d = " << d;
}
|
#ifdef __cplusplus
extern "C"
#endif
int print(int i, double d);
|
你可以至多声明重载集中的一个函数作为 extern “C” ,因为一个 C 函数仅仅可以有一个给定的名字。如果你要在 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); }
|
int g_int(int);
double g_double(double);
|
你也需要包裹 (wrapper) 函数来调用 template functions ,因为 template functions 不能声明为 extern “C”:
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); }
|
C++ 代码仍然可以访问重载函数和 template functions 。 C 代码必须使用 wrapper functions 。
在 C 中访问 C++ class
![]() |
能够从 C 代码中访问 C++ class 吗?你可以声明一个 C struct ,看上去象一个 C++ class 并能以某种方式调用成员函数吗?答案是肯定的,虽然你必须为维持可移植性增加一些复杂性。任何对 C++ class 的定义的修改都要求你重新审查你的 C 代码。
假定你有一个 C++ class 如下:
class M {
public:
virtual int foo(int); // ... private:
int i, j;
};
|
你不能在 C 代码中声明 class M 。你能做的最好的事就是传递指向 class M 对象的指针,这类似于在 C 标准 I/O 中传递 FILE 对象。你可以在 C++ 中写 extern “C” 函数访问 class M 对象并在 C 代码中调用这些函数。下面是一个 C++ 函数,被设计来调用成员函数 foo:
extern "C" int call_M_foo(M* m, int i) { return m->foo(i); }
|
下面是 C 代码的一个例子,它使用了 class M:
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);
}
|
![]() |
你可以在 C++ 程序中使用来自于标准 C 头文件 <stdio.h> 的 C 标准 I/O ,因为 C 标准 I/O 是 C++ 的一部分。
Sun C 和 C++ 使用同样的 C 运行时库,这在关于兼容的编译器小节中注明过了。使用 Sun 编译器,你可以在同一个程序中自由地在 C 和 C++ 代码中使用标准 I/O 。
![]() |
指向函数的指针必须指明是否指向一个 C 函数或 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
|
Line 1 声明了 pfun 指向一个 C++ 函数,因为它缺少链接说明符。
Line 2 声明 foo 为一个 C 函数,它具有一个指向 C++ 函数的指针。
Line 5 试图用指向 g 的指针调用 foo , g 是一个 C 函数,所以类型不匹配。
要确保指向函数的指针的链接规范与它将要指向的函数匹配。在下面这个正确的例子中,所有声明都包含在 extern “C” 花括号中,确保了类型匹配。
extern "C" {
typedef int (*pfun)(int);
void foo(pfun);
int g(int);
}
foo( g ); // now OK
|
typedef int (*pfn)(int);
extern "C" void foo(pfn p) { ...
} // definition
extern "C" void foo( int (*)(int) ); // declaration
|
extern "C" {
typedef int (*pfn)(int);
void foo(pfn p) { ...
}
}
|
|
传播( Propagating )异常
从 C 函数中调用 C++ 函数,并且 C++ 函数抛出了一个异常,将会发生什么?在是否会使得该异常有适当的行为这个问题上 C++ 标准有些含糊,并且在一些系统上你不得不采取特别的预防措施。一般而言,你必须得求诸用户手册来确定代码是否以适当的方式工作。
Sun C++ 中不需要预防措施。 Sun C++ 中的异常机制不影响函数调用的方式。当 C++ 异常被抛出时,如果一个 C 函数正处于活动状态, C 函数将转交给异常处理过程。
混合异常和 set_jmp , long_jmp
最好的建议是在包含 C++ 代码的程序中不要使用 long_jmp 。 C++ 异常机制和 C++ 关于销毁超出作用域对象的规则可能被 long_jmp 违反,从而得到不确定的结果。一些编译器整合了异常和 long_jmp ,允许它们协同工作,但你不能依赖这样的行为。 Sun C++ 使用与 C 编译器相同的 set_jmp 和 long_jmp 。
如果你在混合有 C++ 的 C 代码中使用 long_jmp ,要确保 long_jmp 不要跨越( cross over )活动的 C++ 函数。如果你不能确保这点,查看一下是否你可以通过禁用异常来编译那个 C++ 代码。如果对象的析构器被绕过了,你仍旧可能有问题。
![]() |
某时,多数 C++ 编译器要求 main 函数要被 C++ 编译。这个要求今天来说并不常见, Sun C++ 就不要求这点。如果你的 C++ 编译器需要编译 main 函数,但你由于某种原因不能这么做,你可以改变 C main 函数的名字并从一个 C++ main 的包裹函数中调用它。例如,改变 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 。如上注解,使用 Sun C++ 是不会有这个麻烦。
假定你有 C 程序文件 main.o, f1.o 和 f2.o ,你可以使用 C++ 程序库 helper.a 。用 Sun C++ ,你要如下引发命令行:
CC -o myprog main.o f1.o f2.o helper.a
|
更多信息
|
-
| Sun ONE Studio C/C++ Documentation |
![]() |
Steve Clamage 从 1994 年在 Sun 至今 ? 。它当前是 C++ 编译器和 Sun ONE Studio 编译器套件的技术领导。它从 1995 年开始是 ANSI C++ 委员会的主席。