使用Visual Studio链接LIB库文件注意事项

浪尽此生 提交于 2020-04-27 18:53:56

在使用Visual Studio在Windows下开发应用程序时,可能面临需要引用第三方库来支撑自身代码的情况。第三方库通常以下面两种方式提供:

1、静态LIB库:这种提供形式通常包含LIB库文件、头文件及相关文档说明。

2、动态DLL库:这种提供形式通常也包含LIB库文件(有些厂商不提供LIB库文件),头文件,DLL文件以及相关文档说明。

无论以上那种形式的库,在使用时都会面临链接这个步骤(LoadLibrary->GetProcAddress方式载入DLL库不在本文讨论范围内, 下同),而链接步骤又由于将要生成的目标工程的不同类型变得越发复杂。为什么这么说呢,我们继续往下看。

通常在链接一个第三方库的LIB文件时,我们使用下面两种方法:

1、#pragma comment(lib, "XXX.LIB") 杂注方式。

2、项目->属性->链接器->输入->附加依赖项方式。

乍一看这两种方式我们都用过,而且在使用时并没有感觉到两种方式有何不同。但实际上仔细分析还是有些地方值得商榷的。两种不同的链接方法在生成不同类型的目标工程时表现出的行为区分明显。为了展示方便,我们做了如下几个实验:

目标工程类型 使用#pragma杂注链接 使用附加依赖项链接
可执行文件(EXE)
动态链接库(DLL)
静态库(LIB)
可执行文件(EXE),并链接上一步骤生成的静态库。

为此我建立了一个解决方案,包含如下工程:Exe、Dll、Lib、Exe2,分别对应上面四个实验所的目标项目。这个解决方案中还包含另外一个LIB库工程:Dep,上面的四个实验工程的生成直接或间接依赖这个静态库。为了节约空间以及方便阅读,我关闭了所有工程的预编译头功能,删除了stdafx.h/cpp文件,将代码组织到单一文件中。整个解决方案如下图所示:

而每个工程的源代码都非常简单,仅用于证明我们的实验结论。首先我们看一下Dep这个静态库中包含的内容如下:

//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,连接器找不到主函数了,链接器蒙圈了!

是不是很刺激,仅仅是链接这个环节还会有这么多好玩的地方?实际上在软件开发过程中,我们很可能会遇到其中的一到两个问题,却不知道原因,仔细分析这篇文章相信你能够从中获得一些收益。对于上面讨论的各种问题,我在我的笔记中有简略的总结,希望可以帮助到还在被链接器这么个程序猿们:

1、#pragma杂注会写入一条链接指令到目标LIB中,该链接指令指明目标LIB依赖源LIB,但并不会将源LIB中的OBJ链接到目标LIB中,待目标LIB被链接至EXE/DLL时,若源LIB不存在,则链接失败,找不到#pragma杂注所指明的源LIB。

2、附加依赖项会将源LIB中的OBJ文件链接至目标LIB,但在链接过程中如果目标LIB中的OBJ文件与源LIB中的OBJ文件同名,则目标LIB中的OBJ替换源LIB中的同名OBJ文件,在此种情况下,目标LIB被链接到EXE/DLL时,需要单独链接被覆盖的源LIB中的OBJ文件,否则会出现链接错误,找不到对应的符号。

3、当目标LIB库应用了源LIB中的符号时,若忘记书写#pragma杂注,或忘记添加项目依赖项,生成目标LIB的过程不会被中断,也不会报错,源LIB中的OBJ不会被链接到目标LIB中,在使用目标LIB生成EXE/DLL时,必须同时将源LIB也带上,否则出现链接错误,找不到对应的符号。

4、当源LIB被两次以上间接链接到目标LIB或EXE/DLL时,连接器会忽略相同符号中的一个,但不确定是哪一个,需要手动明确指定忽略的LIB,否则可能导致新版LIB被旧版LIB代替的问题。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!