兼容性

许多年来,你一直担心编制程序时一部分使用 C++一部分使用 C,就如同在全部用 C 编 程的年代同时使用多个编译器来生成程序一样。没办法多编译器编程的,除非不同的编译器 在与实现相关的特性(如 int 和 double 的字节大小,传参方式)上相同。但这个问题在语言的标准化中被忽略了,所以唯一的办法就是两个编译器的生产商承诺它们间兼容。C++和 C 混合编程时同样是这个问题,所以在实体混合编程前,确保你的 C++编译器和 C 编译器兼容。

名称重整

名称重整,就是 C++编译器给程序的每个函数换一个独一无二的名字。在 C 中,这个过程 是不需要的,因为没有函数重载,但几乎所有 C++程序都有函数重名(例如,流运行库就申 明了几个版本的 operator«和 operator»)。重载不兼容于绝大部分链接程序,因为链接程序通常无法分辨同名的函数。名称重整是对链接程序的妥协;链接程序通常坚持函数名必须独一无二。

如果只在 C++范围内,名称重整不会影响你。如果你有一个函数叫 drawline 而编译器 将它变换为 xyzzy,你总使用名字 drawLine,不会注意到背后的 obj 文件引用的是 xyzzy 的。

如果 drawLine 位于 C 运行库中,那就是一个不同的故事了。你的 C++源文件包含的头 文件中申明为:

void drawLine(int x1, int y1, int x2, int y2);

代码体中通常也是调用 drawLine。每个这样的调用都被编译器转换为调用名称重整后的 函数,所以写下的是

drawLine(a, b, c, d); // call to unmangled function name

obj文件中调用的是:

xyzzy(a, b, c, d); // call to mangled function mame

但如果 drawLine 是一个 C 函数,obj 文件(或者是动态链接库之类的文件)中包含的 编译后的 drawLine 函数仍然叫 drawLine;没有名称重整动作。当你试图将 obj 文件链接为程 序时,将得到一个错误,因为链接程序在寻找一个叫 xyzzy 的函数,而没有这样的函数存在。

要解决这个问题,你需要一种方法来告诉 C++编译器不要在这个函数上进行名称重整。你 不期望对用其它语言写的函数进行名称重整.总之,如果你调用一个名字为 drawLine 的 C 函数,它实际上就叫 drawLine,你的 obj 文件应该包含这样的一个引用,而不是引用进行了名称重整的版本。

要禁止名称重整,使用 C++的 extern ‘C’指示:

// declare a function called drawLine; don't mangle
// its name
extern "C"
void drawLine(int x1, int y1, int x2, int y2);

不要以为有一个 extern ‘C’,那么就应该同样有一个 extern ‘Pascal’和 extern’FORTRAN’。没有,至少在 C++标准中没有。不要将 extern ‘C’看作是申明这个函数是用 C 语言写的,应该看作是申明在个函数应该被当作好像 C 写的一样而进行调用。

例如,如果不幸到必须要用汇编写一个函数,你也可以申明它为 extern ‘C’:

// this function is in assembler — don't mangle its name
extern "C" 
void twiddleBits(unsigned char bits);

你甚至可以在 C++函数上申明 extern ‘C’。这在你用 C++写一个库给使用其它语言的客户使用时有用。通过禁止这些 C++函数的名称重整,你的客户可以使用你选择的自然而直观的 名字,而不用使用你的编译生成的变换后的名字:

// the following C++ function is designed for use outside
// C++ and should not have its name mangled
extern "C" void simulate(int iterations); 

经常,你有一堆函数不想进行名称重整,为每一个函数添加 extern ‘C’是痛苦的。幸好,这没必要。extern ‘C’可以对一组函数生效,只要将它们放入一对大括号中:

extern "C" { // disable name mangling for
// all the following functions void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits); 
void simulate(int iterations);
...
}

这样使用 extern ‘C’简化了维护那些必须同时供 C++和 C 使用的头文件的工作。当用 C++编译时,你应该加 extern ‘C’,但用 C 编译时,不应该这样。通过只在 C++编译器下定义的宏__cplusplus,你可以将头文件组织得这样:

 #ifdef __cplusplus
 extern "C" {
 #endif
void drawLine(int x1, int y1, int x2, int y2); 
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
 #ifdef __cplusplus
 }
#endif

顺便提一下,没有标准的名称重整规则。不同的编译器可以随意使用不同的变换方式,而事实上不同的编译器也是这么做的。这是一件好事。如果所有的编译器使用同样的变换规 则,你会误认为它们生成的代码是兼容的。现在,如果混合链接来自于不同编译器的 obj 文件,极可能得到应该链接错误,因为变换后的名字不匹配。这个错误暗示了,你可能还有 其它兼容性问题,早些找到它比以后找到要好。

statics的初始化

在掌握了名称重整后,你需要面对一个 C++中事实:在 main 执行前和执行后都有大量代码被执行。尤其是,静态的类对象和定义在全局的、命名空间中的或文件体中的类对象的构 造函数通常在 main 被执行前就被调用。这个过程称为静态初始化。

这和我们对 C++和 C 程序的通常认识相反,我们一直把 main 当作程序的入口。同样,通过静态 初始化产生的对象也要在静态析构过程中调用其析构函数;这个过程通常发生在 main 结束 运行之后。

为了解决 main()应该首先被调用,而对象又需要在 main()执行前被构造的两难问题, 许多编译器在 main()的最开始处插入了一个特别的函数,由它来负责静态初始化。同样地, 编译器在 main()结束处插入了一个函数来析构静态对象。产生的代码通常看起来象这样:

 int main(int argc, char *argv[])
 {
		performStaticInitialization();  			
		...

		performStaticDestruction(); 
  }

不 要 注 重 于 这 些 名 字 。 函 数 performStaticInitialization() 和 performStaticDestruction()通常是更含糊的名字,甚至是内联函数(这时在你的 obj 文件 中将找不到这些函数)。

要点是:如果一个 C++编译器采用这种方法来初始化和析构静态对 象,除非 main()是用 C++写的,这些对象将从没被初始化和析构。因为这种初始化和析构静 态对象的方法是如此通用,只要程序的任意部分是 C++写的,你就应该用 C++写 main()函数。

有时看起来用 C 写 main()更有意义–比如程序的大部分是 C 的,C++部分只是一个支 持库。然而,这个 C++库很可能含有静态对象(即使现在没有,以后可能会有,所以用 C++写 main()仍然是个好主意。这并不意味着你需要重写你的 C 代码。只要将 C 写的 main()改名为 realMain(),然后用 C++版本的 main()调用 realMain():

extern "C" // implement this
int realMain(int argc, char *argv[]); 

int main(int argc, char 	*argv[])
{
return realMain(argc, argv); 
}

如果不能用 C++写 main(),你就有麻烦了,因为没有其它办法确保静态对象的构造和 析构函数被调用了。不是说没救了,只是处理起来比较麻烦一些。编译器生产商们知道这个 问题,几乎全都提供了一个额外的体系来启动静态初始化和静态析构的过程。要知道你的编 译器是怎么实现的,挖掘它的随机文档或联系生产商。

动态内存分配

现在提到动态内存分配。

通行规则很简单:C++部分使用 new 和 delete,C 部分使用 malloc(或其变形)和 free。只要 new 分配的内存使用 delete 释放,malloc 分配的内存用 free 释放,那么就没问题。用 free 释放 new 分配的内存或用 delete 释放 malloc 分配的内存,其行为没有定义。那么,唯一要记住的就是:将你的 new 和 delete 与 malloc 和 free 进行严格的隔离。

数据结构的兼容性

最后一个问题是在 C++和 C 之间传递数据。不可能让 C 的函数了解 C++的特性的,它们的交互必须限定在 C 可表示的概念上。因此,很清楚,没有可移植的方法来传递对象或传递 指向成员函数的指针给 C 写的函数。但是,C 了解普通指针,所以想让你的 C++和 C 编译器生产兼容的输出,两种语言间的函数可以安全地交换指向对象的指针和指向非成员的函数或 静态成员函数的指针。自然地,结构和内建类型(如 int、char 等)的变量也可自由通过。

因为 C++中的 struct 的规则兼容了 C 中的规则,假设“在两类编译器下定义的同一结 构将按同样的方式进行处理”是安全的。这样的结构可以在 C++和 C 见安全地来回传递。

如果你在 C++版本中增加了非虚函数,其内存结构没有改变,所以,只有非虚函数的结构(或类)的对象兼容于它们在 C 中的孪生版本(其定义只是去掉了这些成员函数的申明)。

增加虚函数将结束游戏,因为其对象将使用一个不同的内存结构。从其它结构 (或类)进行继承的结构,通常也改变其内存结构,所以有基类的结构也不能与 C 函数交互。

就数据结构而言,结论是:在 C++和 C 之间这样相互传递数据结构是安全的–在 C++ 和 C 下提供同样的定义来进行编译。在 C++版本中增加非虚成员函数或许不影响兼容性,但几乎其它的改变都将影响兼容。