当前位置:C++技术网 > 资讯 > 多文件手动编译链接实现探究分析,加深对编译和链接过程的理解

多文件手动编译链接实现探究分析,加深对编译和链接过程的理解

更新时间:2017-03-02 13:24:12浏览次数:1+次

    做了这么久的开发工作,基本上都是在VS环境完成编译链接的。现在转入Linux做开发,一开始对Linux非常不熟悉,所以开始用手动编译方法对单文件进行编译链接。而对整个项目的很多个文件的编译链接,竟然没有概念。作为一个程序员,竟然对这个是懵逼的,非常的惭愧。

    通过认真学习,把Linux下的Gcc编译链接研究了一下,编译单个文件是没有问题的。后来为了项目开发,就探索出了一个开发模式:VS写代码、VisualGDB调试和上传代码到Linux服务器。当然整个过程全部是VisualGDB自动化了。在调试的时候,就和VC的代码调试一样方便。有一次搭上了强大IDE的便车呀。连FTP以及代码同步工具之类的都没有用,还是熟悉的环境,还是熟悉的调试,还是熟悉的味道。这样做的目的也是为了提高开发效率,用什么工具熟悉就用什么做开发。不然一下子去适用通常Linux程序员用的各种软件,还真是不知所措。

    有了这个开发模式后,似乎又将自己给包起来了。但是都到了Linux环境了,不了解手动多文件编译链接,就说不过去了。所以就开始了这次的探索学习。我不太习惯什么都看书看文档来学习,一方面是本来很多也不全面,还不能把自己不懂的都讲到。而探索学习则是自己针对问题发起一次挑战,通过科学的方法假设、试验论证。最后和你的假设一致或者不一致,慢慢结合你已经学习到的知识,就可以将你不清楚的东西都整合起来。这样的学习非常有意思,适合那种书上不讲或一般很少讲而你又特别想知道的知识。当然也适合没有资料需要你自己研究的知识的学习。

    下面是一段代码:


int test();
void main()
{
    test();
}
    在一个文件中,不写定义体,直接调用,提示错误:
1>m.obj : error LNK2001: 无法解析的外部符号 "int __cdecl test(void)" (?test@@YAHXZ)
1>c:\users\wdx\documents\visual studio 2010\Projects\tmp_link\Debug\tmp_link.exe : fatal error LNK1120: 1 个无法解析的外部命令

    这是链接错误,而不是编译错误。关于不定义函数体和接口不实现出现的链接错误的理解,请参考文章《C++抽象类与继承引起的编译问题》。
    编译的过程是将我们写的语法翻译成汇编语法,进而翻译成二进制的机器码。而编译是分块进行的,以文件为块。一个个的块都翻译好后,就得到了二进制的文件。然后再通过链接,组装起来。而组装的依据就是头文件包含关系和函数声明、调用关系等。
    所以,我们在编译这份代码的时候,test函数的声明就是一个链接入口。有了这个入口,编译就可以留一个标记,编译时就不需要管test函数的具体实现,而是留给链接时组装。声明的意义就在于,我提前告知编译器,这个函数是在其他地方定义的,这里可以放心的使用,只要后面一链接就OK。这也就是为什么编译通过,链接却不通过的原因。
    我们再来看看多个文件的编译链接情况:、
m.cpp代码:
#include "1.h"
void main()
{
    test();
}

1.h代码:
int test();

    此时编译的结果:
1>m.obj : error LNK2019: 无法解析的外部符号 "int __cdecl test(void)" (?test@@YAHXZ),该符号在函数 _main 中被引用
1>c:\users\wdx\documents\visual studio 2010\Projects\tmp_link\Debug\tmp_link.exe : fatal error LNK1120: 1 个无法解析的外部命令
    从这里的错误提示可以看到,发生的错误也是链接错误。但是提示和前面的稍微不同。前面的是提示test函数的并没有指明在main函数中,而这里指明了。这说明了什么呢?
    这两种情况是有差别的。前面在m.cpp文件里,test是有声明的。而这里在m.cpp文件中,test没有声明,而是在1.h头文件声明的。虽然最后头文件展开后,和前面单个文件是一样的效果,这里的提示可以拿来区分这样的两种情况,便于定位问题。这是一个小经验。在VC编译器中可以应用,其他的编译器就不一定了。
    不管是单文件,还是多文件,本质都是一样的。在单文件中,如果写好了函数定义,就会直接编译在一个块里,不需要链接。而多个文件,因为定义分布在不同的文件,也就需要将生成的多块二进制内容链接在一起。
    两个文件如果有头文件包含的关系,被包含的文件就是被依赖的文件。编译之后,被包含的文件得到中间的二进制代码文件。包含的文件也会生成中间的二进制文件。那么这两个文件的先后编译顺序有关系吗?
    没有。编译的时候,只要我们知道了被包含的文件里的函数的声明就够了。我们可以先单独将所有的源码都编译成中间文件,然后再挨个的链接在一起就行了。
    如果1.h文件中的test函数声明时就定义了,那么在头文件展开的时候,就相当于直接把代码写在了m.cpp上一样,也就相当于单文件的编译。而通常的情况是,我们提供1.cpp来写test函数的定义。


1.cpp文件内容如下:

#include <stdio.h>
void test()
{
    printf("hello\n");
}

    这样的话,在编译m.cpp的时候,我们只引用了test函数,但是不知道test函数的定义,这个需要在链接的时候组合起来。所以编译是没有问题的。而1.cpp我们也可以单独编译出来。这个就是一个完整的函数定义,只是依赖了stdio.h文件。这样编译的时候,会将printf函数引入到函数来,但是printf的定义,我们是不知道的,也是在链接的时候组合进来。
    所以我们在编译1.cpp和m.cpp并没有先后顺序。
    在VC中,我们使用cl.exe程序编译代码,使用link.exe程序来链接。而在GCC中,我们使用gcc -c 1.cpp m.cpp这样来编译,gcc 1.o m.o来链接两个文件。文件的先后顺序无所谓,GCC会自动链接,只要你放在这里就行了。
    那么带有包含关系的文件,也就是这样手工来编译链接。而如果是两个相互不相关的文件,各自编译各自的就行了。
    多文件编译也就是要分清楚链接的情况。有相互依赖的也就是有头文件包含关系的。相互依赖的文件的中间文件要放在一起链接。如果不放在一起或不提供就会出现链接错误。而系统库函数不需要我们自己输入,链接器会自动识别系统库函数,然后去链接系统库。