从.h .cpp到库函数链接到extern “C”

从.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不用作任何修改。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值