《More Effective C++》《杂项讨论——34、如何在同一个程序中结合C++和C》

发布于:2024-07-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

1、Terms34:如何在同一个程序中结合C++和C

在大型项目中一般都用C++进行开发,但是不可避免会用一些C语言进行底层的调用。在确定C++和C的编译器都能产生兼容的目标文件之后你要重点考虑四件事情:名称重整,statics的初始化,动态内存的分配,数据结构的兼容性。

1.1 名称重整

一、问题引出
在 C++ 出现之前,很多实用的功能都是用 C 语言开发的,很多底层的库也是用 C 语言编写的,如果在 C++ 代码中可以兼容 C 语言代码,无疑能极大地提高 C++ 程序员的开发效率。但是在一个项目中,能否既包含 C++ 程序又包含 C 程序呢?
答案是可以的,但是要小心处理,因为C++ 和 C 在程序的编译、链接等方面都存在一定的差异,这些差异往往会导致程序运行失败。
C++编译器会为程序中的每个函数编出独一无二的名称;但是在C语言中没有必要,因为C语言不支持函数重载。但是发展到现在,C++项目中几乎都会有一些函数拥有相同的名称,但是重载并不兼容大部分链接器,因此名称重整(修饰),是对链接器的一个让步。
例如你有一个函数funca(),被编译器重整为xyzzy,你可以使用funca()的名称,没人在乎编译器重整的名称。但是如果funca处于C函数库中,你的C++原始代码会有一个头文件,有如下声明。

void funca(int x1,int x2,int x3,int x4);
//调用未经重整的函数名称

当你使用funca(a,b,c,d)时,你的目标文件内含的是这样的代码:

xyzzy(a,b,c,d)
//调用重整后的函数名称

但是如果funca()是一个C函数,那么funca()代码在目标文件会有一个名为funca的函数,名称并未重整。当你试图链接那个目标文件,就会获得一个错误信息,因为链接器寻找xyzzy的函数,并不存在。
解决方法就是告诉C++编译器,不要重整某些函数名称。如果调用一个名为funca的C函数,它的真正名称就叫做funca,你的目标代码内含有一份reference,指向那个名称,而非一个重整后的名称。

举个例子:
比如下面是一个用 C++ 和 C 混合编程实现的实例项目:
myfun.h文件内容:

void display();

myfun.c文件内容:

#include <stdio.h>
#include "myfun.h"
void display(){
   printf("C++:http://c.biancheng/net/cplus/");
}

main.cpp文件内容:

#include <iostream>
#include "myfun.h"
using namespace std;
int main(){
   	display();
   	return 0;
}

可见主程序mian.cpp文件是用 C++ 编写的,而 display() 函数的定义myfun.c文件是用 C 编写的。
表面上看这个项目很完整,但调用 GCC 编译器运行此项目(见利用GCC编译器编译C/C++程序),提示错误信息如下:

In function `main': undefined reference to `display()'

它表示编译器无法找到 main.cpp 文件中 display() 函数的实现代码。
导致此错误的原因,是 C++ 和 C 编译程序时,对函数名的处理方式不同。
(1)通过函数重载详解可知,C++ 之所以支持函数的重载,是因为在程序的编译阶段,C++会对函数的函数名进行“重命名”,比如:

void Swap(int a, int b) 会被重命名为_Swap_int_int;
void Swap(float x, float y) 会被重命名为_Swap_float_float。

(2)但是C 语言不支持函数重载,它不会在编译阶段对函数的名称做较大的改动,比如:

void Swap(int a, int b) 会被重命名为_Swap;
void Swap(float x, float y)也会被重命名为_Swap。

不同的编译器有不同的重命名方式,但根据 C++ 标准编译后的函数名几乎都由原有函数名和各个参数的数据类型构成,而根据 C 语言标准编译后的函数名则仅由原函数名构成。
(3)这就意味着,使用 C 和 C++ 进行混合编程时,两者对函数名的处理方式不同,势必会造成编译器在程序链接阶段无法找到函数具体的实现,导致链接失败。
(4)幸运的是,C++ 给出了相应的解决方案,即借助 extern “C”,就可以轻松解决 C++ 和 C 在处理代码方式上的差异性。
二、extern "C"详解
extern 是C和C++的一个关键字,但我们可以将 extern “C” 看做一个整体,和 extern 毫无关系。
extern “C” 既可以修饰一句 C++ 代码,也可以修饰一段 C++ 代码。
它的功能是让编译器以处理 C 语言代码的方式,来处理它所修饰的 C++ 代码。
仍以上面的例子进行说明。main.cpp 和 myfun.c 文件中都包含 myfun.h 头文件,当程序进行预处理操作时,myfun.h 头文件中的内容会被分别复制到这 2 个源文件中。对于 main.cpp 文件中包含的 display() 函数来说,编译器会以 C++ 代码的编译方式来处理它;而对于 myfun.c 文件中的 display() 函数来说,编译器会以 C 语言代码的编译方式来处理它。
为了避免 display() 函数以不同的编译方式处理,我们应该使其在 main.cpp 文件中仍以 C 语言代码的方式处理,这样就可以解决函数名不一致的问题。因此,可以像如下这样来修改 myfun.h:

#ifdef __cplusplus
    extern "C" void display();
#else
    void display();
#endif

可以看到,当 myfun.h 被引入到 C++ 程序中时,会选择带有 extern “C” 修饰的 display() 函数;反之如果 myfun.h 被引入到 C 语言程序中,则会选择不带 extern “C” 修饰的 display() 函数。由此,无论 display() 函数位于 C++ 程序还是 C 语言程序,都保证了 display() 函数可以按照 C 语言的标准来处理。
再次运行该项目,会发现之前的问题消失了,可以正常运行:

C++:http://c.biancheng/net/cplus/

在实际开发中,对于解决 C++ 和 C 混合编程的问题,通常在头文件中使用如下格式:

#ifdef __cplusplus
    extern "C" {
#endif
 
void display();
 
#ifdef __cplusplus
    }
#endif

由此可以看出,extern “C” 大致有 2 种用法,当仅修饰一句 C++ 代码时,直接将其添加到该函数代码的开头即可;如果用于修饰一段 C++ 代码,只需为 extern “C” 添加一对大括号{},并将要修饰的代码囊括到括号内即可。

1.2 statics的初始化

许多代码会在main之前和之后执行代码。更明确说,static class对象、全局对象、namespace内对象以及文件范围(file scope)内的对象,其constructors总是在main之前执行,这个过程称为static initialization。通过static initialization产生出来的对象,其destructors必须在所谓的static destruction过程中被调用。那是发生在main结束之后。
经过编译的main,看起来像这样:

int main()
{
	performStaticInitialization();	//此行由编译器加入
	
	the statements you put in main go here;
	
	performStaticDestruction();		//此行由编译器加入
}

重点是:如果一个C++编译器采用这种方法来构造和析构对象,那么除非程序中有main,否则这种对象既不会被构造也不会被析构。
有时候,在C成分中撰写main似乎比较合理——如果程序主要以C完成而C++只是个支持库的话。尽管如此,C++程序库中内含static对象仍是极有可能的,所以如果能够,还是尽量在C++中撰写main的好。然而这并非意味你需要重写你的C代码。只要将你的C main重新命名的realMain,然后让C++ main调用realMain:

extern "C"
int realMain(int argc,char* argv[]); //以C语言完成此函数
 
int main(int argc,char* argv[])
{
	realMain(argc,argv);
}

1.3 动态内存的分配

动态分配规则很简单:程序的C++部分使用new和delete,程序的C部分则使用malloc和free。
其次,严密地将new/delete与malloc/free分隔开来。
有时候说比做容易很多,考虑粗糙(但好用)的strdup函数,它虽然并非C或C++标准的一份子,却被广泛使用:

char* strdup(const char* ps); //返回一个ps所指字符串的副本

strdup分配的内存必须由strdup的调用者负责释放。如果它自C函数库,使用free;如果它来自一个C++程序库,那么应该用delete。因此调用strdup后,你应该做的事情不只随系统的不同而不同,也随编译器的不用而不同。为了降低这种头痛的移植问题,请避免调用标准程序库以外的函数或是大部分计算平台上尚未稳定的函数。

1.4 数据结构的兼容性

如果你的C++和C编译器有着兼容的输出,两个语言的函数便可以安全的交换对象指针、non-member函数指针或者static函数指针。很自然的,structs以及内建类型的变量也可以安全跨越C++/C边界。
对于struct来说没如果只是加上一些非虚函数,其内存布局应该不会改变,如果加上虚函数,或者继承也会改变struct的布局,所以一个struct如果带有base structs(或classes),无法和C函数交换。

2、总结

  • 确定你的C++和C编译器产出兼容的目标文件(object files) 。
  • '将双方都使用的函数声明为extern “C”。
  • 如果可能,尽量在C++中撰写main。
  • 总是以delete删除new返回的内存;总是以free释放malloc返回的内存。
  • 将两个语言间的“数据结构传递”限制于C所能了解的形式;
  • C++ structs如果内含非虚函数,倒是不受此限制。

3、参考

3.1 《More Effective C++》
3.2 如何在同一个程序中结合C++和C


网站公告

今日签到

点亮在社区的每一天
去签到