// 觉得这篇文章写的还可以,比较详细有点学究的味道,所以就翻译过来。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型的函数。