34.混合使用C与C++

前言

 
在混合编程前,首先确保你的C++编译器和C编译器兼容。确认兼容后,还有四个需要考虑的问题:名变换、静态初始化、内存动态分配、数据结构兼容。


名变换

 
名变换,就是C++编译器给程序的每个函数换一个独一无二的名字。C没有函数重载,因此不需要该过程。重载不兼容于绝大部分链接程序,因为链接程序通常无法分辨同名的函数,它坚持函数名必须独一无二。名变换是C++对编译器的妥协。
在C++使用范围内用户无需考虑名变换,但在C运行库中,那么情况截然不同。

问题实例

举例而言,在Cpp包含的头文件中,drawline被声明为:

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

代码体中通常也是调用drawLine,每一个这样的调用都被编译器转换为调用名变换后的函数,所以写下的是
1
drawLine(a, b, c, d);

在obj中被调用的是:
1
xyzzy(a, b, c, d);//编译器变换了名称

但如果drawline是一个C函数,obj文件(或者是动态链接库之类的文件)中包含的编译后的drawline函数仍然叫drawline,不会发生名变换动作。这意味着链接时将发生错误,因为链接程序找不到xyzzy函数的存在。


解决方案(extern “C”)

我们需要某种方法告诉C++编译器不要在这个函数上进行名变换,就是使用C++的extern "C"关键词:

1
2
extern "C"
void drawLine(int x1, int y1, int x2, int y2);

不要以为有一个extern “C”,那么就应该同样有一个extern “Pascal”和extern “FORTRAN”之类。该关键词的意思是下述函数应该被当作C语言写的一样。所以汇编也是用extern “C”声明,甚至可以在C++函数上申明 extern “C”。(用C++写库给其他语言的客户使用),这样编译器就不会对你的函数执行名变换。
如果你需要批量地声明函数无需名变换,extern “C”可以对一组函数生效,只要将它们放入一对大括号中:
1
2
3
4
5
6
extern "C" {
void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...
}

这种技巧普遍适用于头文件中。假设当前头文件可能被C++编译器编译也可能会被C语言编译器编译。当用C++编译时,你应该加 extern “C”,但用C编译时则不必。通过只在C++编译器下定义的宏__cplusplus,头文件的写法很简单:
1
2
3
4
5
6
7
8
9
10
#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

顺便提及一下,名变换没有标准规则,不同的编译器名变换不同。


静态初始化

 
在C++中,main函数执行前和执行后都有大量代码被执行。具体来说,静态的类对象和定义在全局的、命名空间中的或文件体中的类对象的构造函数通常在main执行前就被调用。这个过程称为静态初始化。同样,通过静态初始化产生的对象也要在静态析构过程中调用其析构函数,这个过程通常发生在main结束
运行之后。
为了解决main应该首先被调用,而对象又需要在main执行前被构造的两难问题,许多编译器在main的最开始处插入了一个特别的函数,由它来负责静态初始化。同样地,编译器在main结束处插入了一个函数来析构静态对象:

1
2
3
4
5
int main(int argc, char *argv[]){
performStaticInitialization();
...//the statements you put in main go here;
performStaticDestruction();
}

示例中函数的名称不需要在意,关注要点是:如果一个C++编译器采用这种方法来初始化和析构静态对象,那么除非你用C++写了main函数,否则这些对象既不会构造也不会析构。因此只要代码中有C++部分,就应该使用C++的main函数。
但有时似乎用C写main更有意义————比如当前程序的大部分都是用C写的,C++部分只是一个支持库。然而,这个C++库很可能含有静态对象(即使现在没有,以后也可能会有),所以仍然需要用C++写main函数。但我们并不需要改写C语言代码,只要将C写的main改名为realmain,然后用C++版本的main调用realMain:
1
2
3
4
5
extern "C"
int realMain(int argc, char *argv[]);
int main(int argc, char *argv[]){//in C++
return realMain(argc, argv);
}

如果不这么写,就无法确保确保静态对象是否已经构造和析构。


动态内存分配

 
动态内存分配的规则很简单:C++部分使用new和delete,C部分使用malloc和free。用free释放new 分配的内存或用delete释放malloc分配的内存,其行为没有定义。
说起来容易,做起来未必简单。比如strdup函数,它并不存在于C和C++标准库中,却很常见:

1
char * strdup(const char *ps); // return a copy of the string pointed to by ps

为了避免内存泄漏,strdup的调用者必须释放分配的内存。那是使用delete还是free呢?如果你调用的 strdup来自于C函数库中,使用free,否则使用delete.
在调用strdup后所需要做的操作,在不同的操作系统下不同,在不同的编译器下也不同。为了减少这种可移植性问题,应当尽可能避免调用那些既不在标准运行库中也没有固定形式的函数 。


数据结构兼容性

 
最后一个问题是在C++和C之间传递数据。
C与C++的交互必须限定在C可表示的概念上。因此,不可能传递给C语言对象或者成员函数指针。但C中存在着普通指针的概念,所以C和C++的函数可以安全地交换”指向对象的指针”和”指向非成员的函数或静态成员函数的指针”。同样,struct和内置类型的变量也可自由交换。
因为C++中struct兼容C中的规则,因此C编译器和C++编译器处理struct得到的结果一样。在C++ struct中增加非虚函数不会导致内存结构发生改变,因此,只有非虚函数的strcut的对象兼容于它们在C 中的孪生版本(C++成员函数并不在对象的内存布局中体现)。增加虚函数和加入继承体系会改变内存布局,此时无法完成C与C++的安全交换。
就数据结构而言,我们可以认为在C++和C之间相互传递数据结构是安全的(前提是结构式的定义在C和C++中都可编译)。在 C++版本中增加非虚成员函数或许不影响兼容性,但几乎其它的改变都将影响兼容。