从.h .cpp到库函数链接到extern “C”
最近研究了下C/C++工程性的问题,把自己的问题、实验和结论拿出来分享一下,希望能帮到有同样疑惑的同学。
问题1:.h文件和.cpp(.c等等)文件需要同名么?
问题2:标准库下的.h文件和对应的lib文件是关联的么?
问题3:可以定义(或覆盖)和库函数同名的函数么(C语法下)?
前提铺垫:
1. 为验证问题2和3,我们的工程中定义了和math库中原型一样的double sin(double) 函数
2. 编译过程:编译(.cpp文件分别独立编译)—>链接(先链接用户工程下的.obj,再链接标准库下的.lib)—>生成可执行程序
3. 编译工具VC++6.0
问题1实验过程:
我们在fun2.cpp中定义了函数double sin(double),在fun1.h中声明这个函数,然后在main.cpp中调用sin(1.0),内容如下:
####################
main.cpp内容:
####################
#include "fun1.h"
void main()
{
sin(1.0);//这里的sin()函数是为验证问题2自定义的,在第一个问题里大家不必关心
}
#####################
fun1.h内容:
#####################
double sin(double a);
#####################
fun2.cpp内容:
#####################
#include<stdio.h>
double sin(double a)
{
printf("thisis my sin\n");
return1.0;
}
运行结果:
this is my sin
Press any key to continue
由该结果其实已经可以得出结论,.cpp和.h文件不需要同名,不存在.cpp和.h文件的绑定云云。为什么会这样呢,我们分析一下。
实际的过程是main.cpp 包含fun1.h,在预编译阶段结束后,main.cpp就变成了以下内容:
####################
main.cpp内容:
####################
double sin(doublea);
void main()
{
sin(1.0);
}
即#inclu ”fun1.h”这一预编译指令被替换为它的文本内容doublesin(double a);所以其实不管这个头文件叫fun1.h还是fun2.h都没有区别。
接下来在编译阶段编译器分别编译main.cpp和fun2.cpp(这里大家需要注意一下,.cpp文件的编译是互相独立的,在编译阶段,彼此之间并没有关系)生成main.obj和fun2.obj,fun2.obj编译后有sin(double)的声明和完整定义,而main.cpp中sin(double)的调用在编译阶段只是生成一个符号,并不会去查找sin(double)的定义(事实上在C++环境下double sin(double)编译后生成的符号是double _cdelc sin(double) ,即既包含了函数名又包含了参数链表。这里大家留意一下,后文分析extern “C”时会讲到)。
然后在链接阶段链接器将main.obj和fun2.obj链接到一起(因为sin(double)调用了printf()函数,所以这里其实还有对标准库的链接,但是makefile文件只指明链接用户工程下的.obj,标准库是隐含链接的)。因为在编译阶段fun2.obj中生成了double _cdelc sin(double)的完整定义,所以链接成功。
所以,从以上实验和分析可以得出结论,c语言中的.cpp和.h文件不需要同名
问题2实验过程:
为验证标准库头文件是否和其对应的lib库有绑定关系的问题,此时,我们将main.cpp中的头文件包含#include ”fun1.h”替换为#include<math.h>(我的环境下标准库头文件math.h的路径是D:\MicrosoftVisual Studio\VC98\Include),同时将math.h内容重写如下:
#####################
math.h内容:
#####################
double sin(double a);
此时的math.h只包含了double sin(double)函数的声明,运行结果如下:
this is my sin
Press any key to continue
和之前的结果一样。我们发现,此时链接器并没有因为我们是包含了标准库头文件(#include<math.h>)而链接到标准库中的sin()函数,而仍然是链接我们自己定义的sin()函数。这是为什么呢?分析一下原因,math.h在预编译的时候同样是替换为了double sin(double a);效果和包含fun1.h是一样的,所以最终导致链接的仍然是我们自己定义的sin()函数。
从这个过程中可以得出结论,程序中包含头文件的路径和最终链接的模块是没有关系的,即不管是包含用户自定义的头文件(#include”fun1.h”)还是包含标准库头文件(#include<math.h>),预编译都只是做了简单的文本替换而已,不同的只是””包含会先在用户目录下查找头文件,而<>包含会在标准库下去找头文件,这并不影响预编译后的结果。
在这个例子中,我们将math.h重写为了只声明了doublesin(double a);导致的结果是链接到了我们自定义的sin()函数,接下来,我们再做一个实验,即如果我们没有重写math.h,又会发生什么。我们直接来看运行结果:
Press any key to continue
结果是链接到了标准库中的sin()函数,为什么会是这样呢?我们来看一下原始的math.h,发现sin()函数的声明是这样的(这里略过其他不相关的部分):
extern “C”{
double __cdeclsin(double);
}
对比以上两个实验,我们发现导致链接器链接到不同的sin()函数的原因,正是因为对sin()函数的声明的差异所导致的:
doublesin(double)链接到自定义的sin()函数
extern “C”{
double __cdecl sin(double);链接到标准库中的sin()函数
}
为什么会有这样的结果呢?其实是这样的,因为我们的IDE是VC++,所以默认的编译环境是C++的编译环境,而C++是支持函数重载的,所以编译器在对函数编译的时候不仅要编译函数名,同时还要编译参数链表,所以不管是在fun1.h还是重写的math.h中,doublesin(double)经编译后都变成了double __cdecl sin(double),同样fun2.cpp中的sin()函数也被编译成了double __cdecl sin(double),所以在链接阶段链接器就可以在fun2.obj中找到原型完全一样的函数定义,然后就链接到了fun2.cpp中的sin()函数。那么为什么包含原始的math.h就链接到了标准库中的sin()函数呢?其实就是extern ”C”在发挥作用了,extern “C”的作用就是告诉编译器按照C的语法来编译函数,而我们知道C语言是不支持函数重载的,所以函数编译生成的符号只需要函数名就可以了,不需要参数链表,所以在原始的math.h中,double __cdecl sin(double)编译后就变成了_ sin。为验证C语法编译器的编译结果是_sin,我们再来做个实验:
在math.h中声明一个标准库中不存在的函数my(double),math.h变成如下内容:
#####################
math.h内容:
#####################
extern "C"{
double __cdecl sin(double a);
double __cdecl my(double);
}
在main.cpp中包含math.h,即#include <math.h>,然后调用my(1.0);
#####################
main.cpp内容:
#####################
#include <math.h>
void main()
{
my(1.0);
sin(1.0);
}
发现编译可以通过,但是链接的时候会报如下错误:
--------------------Configuration: test1 -Win32Debug--------------------
Linking...
main.obj : error LNK2001: unresolvedexternal symbol _my
Debug/test1.exe : fatal error LNK1120: 1unresolvedexternals
Error executing link.exe.
test1.exe - 2 error(s), 0 warning(s)
所以我们可以发现,编译main.cpp的时候,my(1.0)被编译成了_my。
再回到之前的问题,我们知道fun2.cpp中的doublesin(double)编译后变成了double __cdecl sin(double),然后链接器在链接的时候因为_sin和fun2.obj中的double__cdecl sin(double)原型不匹配,就不会链接到fun2.obj中的sin()函数。那么在main.obj和fun2.obj中都没有_sin是不是链接器就要报告链接错误了?因为链接器如果在用户工程下的.obj文件中没有找到某个符号就会到标准库中去找(链接器首先会链接用户工程下的.obj文件,然后链接标准库,同时标准库是隐含链接的,不需要用户指定),所以链接器在标准库中找啊找,结果发现还真有一个_sin符号,所以最终就链接到了标准库中的sin()函数。从这里我们也可以得出另一个结论,就是我们调用标准库的时候并不是一定要包含标准库中的头文件(但是为了能通过编译,一定要声明函数,同时为了能在C++环境下调用C标准库函数,一定要将函数声明为extern “C”,这样才能将函数声明为C的形式,即“_***”,然后正确链接到C标准库),也更进一步说明头文件和最终链接的模块没有关系,不管是标准库头文件还是用户头文件,都只是简单的文本内容替换,不存在某个头文件和某个.obj或者.lib的绑定关系。
问题3实验过程:
最后一个问题,可以定义和标准库中同名的函数么?
我们来考虑一种需求,假如我们希望定义自己的double sin(double)函数,同时又希望能调用math库中的double cos(doble)函数,我们该怎么做?
我们自然而然得会想到这样来实现,包含原始的math.h以调用标准库中的doublecos(double )函数,同时在fun2.cpp中定义自己的double sin(double)函数,在fun1.h中声明该函数,然后在main.cpp中包含fun1.h。main.cpp的包含顺序是这样的:
#include <stdio.h>
#include "fun1.h"
#include <math.h>
但是经过实验发现,这样做编译的时候会报如下错误:
--------------------Configuration: test1 -Win32Debug--------------------
Compiling...
main.cpp
d:\microsoft visualstudio\vc98\include\math.h(168) : errorC2732: linkage specificationcontradicts earlier specification for 'sin'
d:\microsoftvisual studio\vc98\include\math.h(168) : see declaration of'sin'
Error executing cl.exe.
main.obj - 1 error(s), 0 warning(s)
即在fun1.h中声明了C++形式的sin()函数(编译后成为double__cdecl sin(double)),然后又在math.h中声明了C形式的_sin,导致冲突,编译失败。
解决方案1:
因为math.h中的double __cdeclsin(double)和自定义的_sin会有冲突,所以只能强行修改math.h,删掉其中对double __cdecl sin(double)函数的声明来达到我们的目的。这样做最终符合了我们的预期,即包含math.h,调用了其中的cos()函数,然后包含fun1.h,调用了我们自己定义的sin()函数。但是这样做的结果是程序丧失了可移植性,只能在我们自己的机器上跑,除非其他机器也删掉math.h中sin()函数的声明。因此得出结论,还是不要试图覆盖标准库中的函数,如果你包含了对应的标准库头文件。
解决方案2:
另外一种解决方案是不要包含math.h,而是在fun1.h中做如下声明:
extern “C”{
double cos(double);//告诉编译器这是一个C语法的函数,链接器链接的时候如果没有在用户工程下的.obj中找到_cos符号就会链接到标准库中的cos()函数。ps.即使不包含math.h
}
double sin(double);//告诉编译器这是一个C++语法的函数,链接器链接的时候在用户工程下的.obj找到double _cdecl sin(double),然后链接到自定义的sin()函数。
解决方案3:
还有第三种方案是使用纯C的编译器,这样math.h和fun1.h中double sin(double)都会被编译为_sin符号,链接器链接的时候首先在用户工程下.obj中找到_sin符号从而链接到用户自定义的sin()函数。ps.在VC++6.0中默认以C语法编译的方法是源文件都以.c为后缀。
此时,各文件内容如下:
#####################
main.cpp内容:
#####################
#include <math.h>
void main()
{
sin(1.0);
printf("%f\n",cos(1.0));
}
#####################
fun2.c内容:
#####################
#include<stdio.h>
double sin(doublea)
{
printf("this is my sin\n");
return 1.0;
}
#####################
fun1.h内容:
#####################
double sin(double);
math.h为原始的内容
此时输出结果如下:
this is my sin
0.540302
Press any key to continue
可见,以C语法进行编译,包含原始的math.h,可以调用标准库中的cos()函数,同时包含fun1.h,可以调用自定义的sin()函数,而且math.h不用作任何修改。