在使用Visual Studio在Windows下开发应用程序时,可能面临须要引用第三方库来支撑自身代码的状况。第三方库一般如下面两种方式提供:函数
一、静态LIB库:这种提供形式一般包含LIB库文件、头文件及相关文档说明。工具
二、动态DLL库:这种提供形式一般也包含LIB库文件(有些厂商不提供LIB库文件),头文件,DLL文件以及相关文档说明。spa
不管以上那种形式的库,在使用时都会面临连接这个步骤(LoadLibrary->GetProcAddress方式载入DLL库不在本文讨论范围内, 下同),而连接步骤又因为将要生成的目标工程的不一样类型变得愈加复杂。为何这么说呢,咱们继续往下看。code
一般在连接一个第三方库的LIB文件时,咱们使用下面两种方法:游戏
一、#pragma comment(lib, "XXX.LIB") 杂注方式。开发
二、项目->属性->连接器->输入->附加依赖项方式。文档
乍一看这两种方式咱们都用过,并且在使用时并无感受到两种方式有何不一样。但实际上仔细分析仍是有些地方值得商榷的。两种不一样的连接方法在生成不一样类型的目标工程时表现出的行为区分明显。为了展现方便,咱们作了以下几个实验:io
目标工程类型 | 使用#pragma杂注连接 | 使用附加依赖项连接 |
可执行文件(EXE) | √ | √ |
动态连接库(DLL) | √ | √ |
静态库(LIB) | √ | √ |
可执行文件(EXE),并连接上一步骤生成的静态库。 | √ | √ |
为此我创建了一个解决方案,包含以下工程:Exe、Dll、Lib、Exe2,分别对应上面四个实验所的目标项目。这个解决方案中还包含另一个LIB库工程:Dep,上面的四个实验工程的生成直接或间接依赖这个静态库。为了节约空间以及方便阅读,我关闭了全部工程的预编译头功能,删除了stdafx.h/cpp文件,将代码组织到单一文件中。整个解决方案以下图所示:编译
而每一个工程的源代码都很是简单,仅用于证实咱们的实验结论。首先咱们看一下Dep这个静态库中包含的内容以下:table
//depmain.cpp //它很是简单,仅仅定义了一个函数。其余工程连接这个静态库后即可以调用这个函数。 #include <stdio.h> void depExportFunction() { printf("This is Dep, and I export a function."); }
下面咱们开始第一个目标工程Exe。Exe这个工程中也没有包含任何复杂的代码,咱们分别经过#pragma杂注及项目依赖项两种方式连接Dep项目,观察是否可以连接成功。
//exemain.cpp #include <stdio.h> //因为是Debug配置, 解决方案输出目录在这个位置。 #pragma comment(lib, "../Debug/dep.lib") //声明Dep工程中的函数。 void depExportFunction(); int main() { printf("This is Exe, and I want to call a function in Dep.\n"); depExportFunction(); return 0; }
上面的代码展现了使用#pragma杂注方式连接到Dep的方法。咱们也可使用项目依赖项方法连接Dep工程。
咱们用两种连接方式都可以正确连接,并获得可执行文件,运行获得的结果也与咱们的意愿相符。
若是你稍微动手实践一下,你就会发现,生成Dll工程的连接实验的结果应该与生成Exe工程的实验结果相同。这好像证实了#pragma杂注与附加依赖项在行为上没有任何差异,但是真的是这样嘛?咱们继续进行后面的实验(为了不篇幅冗长啰嗦,我省略了Dll工程的实验过程,由于我很懒,并且结果与Exe相同)。
咱们把重点放在生成Lib工程上来。
首先咱们观察下面的代码:
//libmain.cpp #include <stdio.h> #pragma comment(lib, "../Debug/dep.lib") void depExportFunction(); void libExportFunction() { printf("This is Lib, and I export another function, and I want to call the function in Dep.\n"); depExportFunction(); }
这段代码能够正常生成名称为Lib.lib的静态库文件。对于这种文件,咱们可使用微软提供的Lib.exe工具来查看其中包含了哪些obj文件,连接了哪些其余的LIB库文件。咱们能够看到咱们刚刚编译好的Lib.lib库文件包含了下面的内容:
咱们好像发现了一些问题,新生成的Lib.lib文件中怎么没有包含Dep.lib中的内容?好吧,咱们暂时先把这个问题放在这,先看一下使用项目依赖项生成的库文件是什么样子,而后在来对比一下。下面使用项目依赖项属性从新生成了一次:
哦,原来真的是有区别,使用#pragma杂注生成的Lib居然没有包含Dep的任何内容!这是什么鬼?好吧,咱们看一下MSDN上怎么解释这个现象:
简单说就是#pragma杂注仅仅是告知连接器在连接时别忘了去指定的路径寻找另外一个静态库,不然就缺失某些二进制obj文件了,至于找到的库中到底有啥,连接器本身去分析。而项目依赖项属性则会将所依赖的静态库文件中的全部obj文件连接到即将生成新库文件中。
那么好吧,咱们玩一个游戏试试,如今咱们把#pragma杂注所连接的Dep.lib文件路径和项目依赖项属性中的Dep.lib文件路径所有删除,但保留对Dep中函数的生命和调用,猜猜会发生什么事情?你能够本身尝试编译一下,是否是很惊讶,居然编译经过了!赶忙来看一下生成的Lib.lib文件中都包含了些什么?
不出所料,只有libmain.obj。虽然咱们在libmain.cpp中调用了Dep.lib中的函数depExportFunction(),但这里丝毫没有出现有关Dep库的影子。
试想一下,若是Exe2工程依赖了Lib工程,那么在你发行Lib库文件给Exe2时,因为Lib又依赖了Dep,但Dep却没有被连接到Lib中来,相信你会被一大堆无头脑的LNK2019(没法解析的外部符号)错误淹没。或者,即便你使用了#pragma杂注连接了Dep到Lib,在发行Lib给Exe2工程时,也会由于上面的缘由获得一条莫名其妙的错误LNK1104(没法打开文件XXX.lib)。
实际上在生成一个静态库库时,连接器对于所依赖的其余库中的内容并不敏感(生成过成不报错),若经过项目依赖项配置了一个有效的依赖库路径,则最后生成时可以将依赖库中的所有内容囊括到生成的库中,若使用#pragma杂注指示依赖库路径,则会在生成的静态库中也包含这条杂注(但不包含依赖库中的obj文件),最终在连接这个生成的库到其余EXE/DLL中时才会去搜索杂注路径中标识的依赖库文件(本例中Lib.lib不包含Dep.lib,使用Lib.lib时还须要连接Dep.lib或depmain.obj)。固然,若写错了路径,在生成目标库时也不会出错,只是生成的静态库中缺失了依赖库的obj文件,须要在使用生成库的EXE/DLL工程中单独连接依赖库或依赖库内的obj文件集合(例如Exe2连接Lib.exe,还须要单独连接Dep.lib或depmain.obj)。
接下来咱们继续调戏链接器。咱们将Lib工程中的cpp文件libmain.cpp更名为depmain.cpp,并使用项目依赖项方式从新生成Lib.lib库,看下咱们的新库中包含了什么:
咦?怎么只有一个depmain.obj?难道两个同名的depmain.obj被合并成一个了?咱们在Exe2工程中连接一下这个新生成的Lib.lib,并调用其中的libExportFunction()函数(该函数又调用Dep库中的depExportFunction()函数),Exe2工程的代码以下.
//exe2main.cpp #include <stdio.h> //生成EXE/DLL时使用何种方法结果相同. #pragma comment(lib, "../Debug/Lib.lib") //声明Lib工程中的函数. void libExportFunction(); int main() { printf("This is Exe2, and I want to call the function in Lib.\n"); libExportFunction(); return 0; }
咱们的到了下面的错误输出:
链接器居然替换了相同名称的obj文件而不是合并(后面没有找到Dep中函数的符号也证实了仅连接了目标工程中的obj文件,依赖库中的同名obj文件被忽略)!
试想一下你本身的代码,若是你写的静态库被别人连接,而你恰巧在stdafx.cpp中书写了大量的代码(我知道这不科学,但有人这么作),又恰巧连接你的静态库的人也要生成一个静态库,他也开启了预编译头,使用stdafx.cpp,那么抱歉,他生成的stdafx.obj将会被连接到目标库中,而你提供的依赖库中的stdafx.obj将会被忽略,多么悲催的事情。
前面的实验好像隐约印证了库文件中所包含的obj文件和文件名有某种联系,那么咱们还能够换个姿式继续调戏,咱们在Exe2工程中创建一个子目录Exe2Sub,在该目录中新建一个与原来Exe2工程中同名的cpp文件(exe2main.cpp),文件中只须要定义一个空函数便可,而后将这个子目录添加到Exe2工程中。
工程中两个exe2main.cpp同名,但不在同一个目录下,咱们注释掉Exe2工程对于Lib库的连接杂注,接着编译一下这个工程,观察结果:
MSBUILD提示你发生了一个警告,由于不管源文件组织的方式如何,最终obj文件都会放到一个目录里,这个目录中发现了两个同名的obj,那么后生成的obj会覆盖早先生成的obj,因此MSBUILD要告知你这可能引起错误。紧接着错误就来了,很不幸,咱们的包含空函数的exe2main.obj忽略了包含main函数的exe2main.obj,链接器找不到主函数了,连接器蒙圈了!
是否是很刺激,仅仅是连接这个环节还会有这么多好玩的地方?实际上在软件开发过程当中,咱们极可能会遇到其中的一到两个问题,殊不知道缘由,仔细分析这篇文章相信你可以从中得到一些收益。对于上面讨论的各类问题,我在个人笔记中有简略的总结,但愿能够帮助到还在被连接器这么个程序猿们:
一、#pragma杂注会写入一条连接指令到目标LIB中,该连接指令指明目标LIB依赖源LIB,但并不会将源LIB中的OBJ连接到目标LIB中,待目标LIB被连接至EXE/DLL时,若源LIB不存在,则连接失败,找不到#pragma杂注所指明的源LIB。
二、附加依赖项会将源LIB中的OBJ文件连接至目标LIB,但在连接过程当中若是目标LIB中的OBJ文件与源LIB中的OBJ文件同名,则目标LIB中的OBJ替换源LIB中的同名OBJ文件,在此种状况下,目标LIB被连接到EXE/DLL时,须要单独连接被覆盖的源LIB中的OBJ文件,不然会出现连接错误,找不到对应的符号。
三、当目标LIB库应用了源LIB中的符号时,若忘记书写#pragma杂注,或忘记添加项目依赖项,生成目标LIB的过程不会被中断,也不会报错,源LIB中的OBJ不会被连接到目标LIB中,在使用目标LIB生成EXE/DLL时,必须同时将源LIB也带上,不然出现连接错误,找不到对应的符号。
四、当源LIB被两次以上间接连接到目标LIB或EXE/DLL时,链接器会忽略相同符号中的一个,但不肯定是哪个,须要手动明确指定忽略的LIB,不然可能致使新版LIB被旧版LIB代替的问题。