上学期在Linux下打算用C写一个FTP,使用Stevents写的一些C库函数,结果发现在链接的时候老是说找不到那些C库函数,后来才发现我虽然用C来编写代码,但是我在编译的时候却是使用了C++编译器,结果由于Stevents的C库没有考虑给C++客户端使用,所以就找不到了。后来改成用C编译器就没有问题了。

gcc -g -O2 -D_REENTRANT -o ftpClient utility.o ftpClient.o /home/forrest/Projects/socket/unpv13e/libunp.a -lpthread

有些同学会觉得奇怪,C++不是包含C吗,难道要是C被C++调用还需要作什么处理吗?答案是是的。原因在C++为了支持重载在编译期间将函数名称“重命名”了。因此很多时候我们会看到C++报错时候输出一些很奇怪的函数声明。如:

int foo();
int foo(int a);
int foo(doule a);

在C中当然是不允许的,但是在C++,我们知道由于重载是可以的。但是从编译器的角度来看,三个函数必须在符号表中有三个不同的入口,这意味着必须对他们进行区分。对此,C++采用了一种最便利的方法:对同名函数进行重命名。

因此,以上三个函数在C++编译器中会被重命名为类似于下面的名字:

foo_void;
foo_int;
foo_doule;

回忆C++中重载的规则:函数名相同,参数列表不同。这个规则就是来自于重命名的规则。

现在我们可以回到一开始的问题了:

外部C库是用C编译器进行编译的,因此他们的函数声明和定义都是只是标示了函数名(如foo),而在C++客户端中我们声明或通过包含C库的头文件或自己对C库中要调用的函数进行extern声明(两者其实都是声明,因为函数默认就是extern)。C++编译器却会对该声明进行重命名(如foo_int),然后用该修改后的名称到C库的符号表中查找,结果当然是找不到了。由于有声明,所以编译没有问题,但是链接的时候却找不到定义,所以在链接的时候出错。

知道了原因,那么我们怎样避免这类问题呢?答案很简单,不外如下两种选择:

法一 用C++编译器将C库编译成C++库

这又分为以下两种:

  1. 告诉C库作者,我要在C++中调用你们,赶紧用C++编译器(g++)编译后发给我。 2 更现实一点的做法是,自己动手,丰衣足食。当然前提是你拥有这个外部C库的源代码。

法二 告示C++编译器,我现在要调用的这个函数是一个C函数,你不要自作聪明给我乱命名

那么怎样告诉他呢?回想C++编译器根本不知道该C函数的存在,他之所以编译通过是因为你通过包含C库头文件或对要调用的C函数进行extern声明告诉他,这个函数存在,它的定义在外部编译单元。所以他才会乐滋滋的让你编译通过。那么能不能更明确的告诉他:这是一个外部函数,他存在外部编译单元,他是一个C函数声明与定义。幸运的是,C++编译器确实存在这样的入口让我们告诉他。做法是:如果你是通过包含C库头文件来调用C函数,那么你需要修改该C库头文件,如在sqlite3.h中有如下声明:

/*
* Make sure we can call this stuff from C++.
*/
#ifdef __cplusplus
  extern "C" {
#endif

以下是C函数正常声明

#ifdef __cplusplus
  } /* End of the 'extern "C" block */
#endif

这样当遇到C++编译器时(#ifdef __cplusplus为真),则会自动告诉他以下这些函数都是C函数,不要用C++编译器编译和链接。

如果你不打算包含头文件(这种情形下是一般是自己混合C和C++两种语言编写同一个项目,如MySQL的做法),那么你可以在C++文件中对特定的C函数进行如下声明:

extern "C" int foo(int a);

这样就可以了。Enjoy!

–EOF–