十三、C++库:如何链接外部库、静态链接和动态链接,以及如何自建库并使用
本篇讲C++库,先讲如何在项目中使用外部库,包括静态链接和动态链接的实现;再讲如何在VisualStudio中自建模块或库项目,让所有项目都能使用。先讲牵扯到的理论,再展示实操。
我们使用其他编程语言,比如python时,其包管理器非常好用,添加库是一件非常简单的事情。先pip安装,然后import引入即可,简单易用。感兴趣的同学可以参考我之前写的python博文: https://blog.csdn.net/friday1203/article/details/138354754?spm=1001.2014.3001.5501 中的最后一个小标题。
但是到了C++,情况就完全不同了。当我们引用C++外部库时,基本上我们理想的项目设置是:如果你要迁出别人的远程程序库(比如github上的代码仓库),那你就应该,在你的存储库中有所有你需要的东西,以便你可以直接编译和运行项目的应用程序,而不需要考虑使用包管理器去下载其他你需要的外部库。所以,对于C++来说,最好的方法就是克隆存储库、然后编译和运行就ok了。也所以,我们一般在实际解决方案中的实际项目文件夹中,保留使用的库的版本,也就是我实际上是有那些库的代码的副本或物理二进制文件的。
但是实际情况是:对于一些严肃的项目,我们推荐你一定要取得外部库的源代码(就是代码的副本),然后自己编译源代码。如果你使用的是visualstudio,你可以添加另一个项目,该项目包含你的依赖库的源代码,然后将其编译为静态库或动态库,供你的项目使用。也就是本篇第5小标题讲的内容。
但是,如果你拿不到源代码,或者你的项目只是一个不重要的项目,而且你也不想费时费力,此时你手里有的就是外部库所有者发布的、已经编译好的、各种版本(不同平台和系统的)的二进制文件(pre-compiled binaries),那此时你使用别人已经编译好的二进制文件也是可以的,而且还更快更容易。也就是本篇第3、4小标题讲的内容。
1、以GLFW库为例,简单介绍一下外部库
GLFW全称是"Graphics Library Framework",是一个开源、跨平台的C语言库,主要用于管理输入,包括键盘、鼠标输入等。用于在计算机上创建和管理图形窗口,是OpenGL和现代OpenGL(即OpenGL ES)上下文的扩展库。本篇的静态链接和动态链接外部库都以它为例。
(1)从GLFW官网上下载GLFW库
先进入GLFW官网,先不用下载源代码(上图2处),下载windows版本的预编译文件(上图4处),因为我要拿它展示如何静态和动态的链接外部二进制库文件。
(2)下载的GLFW二进制库文件介绍
下载完毕后是一个压缩包,打开压缩包(上图5处),这是一个C++库的典型组织结构,这里我们先简单介绍一下这些文件:
第一个文件夹docs:打开基本都是html页面文件、js前端页面格式文件、png等页面中的图片等这些文件。直白的讲,就是这些文件构成了这个我们下载的 glfw-3.4.bin/win32.zip 这个文件的页面介绍。这个文件夹可有可无,对我们代码运行毫无关系。只是如果你以后想修改外部库或微小改动外部库时,这是个参考文档。
第二个文件夹include:include的中文翻译是"包含"的意思,所以后面我们都叫它包含目录。这个目录里面(如上图5处所示)是2个.h头文件。头文件里面都是库GLFW所涉及到的变量、函数、类、结构体等对象符号(就是名称)的声明。至于头文件是干啥的有啥用,可以参考我另外一篇博文https://blog.csdn.net/friday1203/article/details/139637472?spm=1001.2014.3001.5501 编译原理,以及https://blog.csdn.net/friday1203/article/details/139737861?spm=1001.2014.3001.5501 中的"头文件"知识点,你就更加不惑了。
第三个文件夹lib-mingw-w64:MinGW是GNU工具(包括编译器GCC和GNU binutils和调试器GDB等)在Win32上的一个移植,是从Cygwin里fork出来的,当初只考虑32位。MinGW-W64是从MinGWork出来的提供扩充x64支持。
我们打开这个文件夹可以看到是一个libglfw3.a文件、一个glfw.dll文件和一个libglfw3dll.a文件。
.a文件是由GLFW的一个或多个相关源文件->通过GCC编译后->生成的一个或多个.o文件->然后使用ar命令将这些.o文件打包成的一个静态库文件.a文件。所以libglfw3.a就是静态链接库文件,也所以libglfw3.a文件里面都是GLFW中实现功能的变量、函数、类等、并且是已经被编译成了二进制的文件。
.dll文件是Dynamic Link Library(动态链接库)文件的缩写。当你动态链接外部库时需要用到这个文件。它是你的源代码编译后生成的.exe程序在执行时需要调用的文件。因为这样可以减小你的程序内存占用过大问题,使得你的程序更轻量。所以glfw.dll文件就是GLFW库的动态链接库文件,供使用者动态链接的,里面也是实现功能的变量、函数、类等、并且已经被编译成了二进制了。
libglfw3dll.a文件则是和glfw.dll搭配一起使用的文件,也是用于动态链接。因为它里面都是一堆指向动态库(glfw.dll库文件)中的所有变量和函数的指针,就是都是一堆变量和函数的位置信息,这样链接器就可以直接链接到这些变量和函数了。
第四个文件夹lib-static-ucrt:是Microsoft Visual Studio中用于链接静态库的Universal C Runtime (UCRT)库。在Visual Studio的编译和链接过程中,选择使用静态库(Static Library)时会涉及到这个库。也就是,当你选择使用静态链接方式编译代码时,Visual Studio会包含这个库以确保程序的正确运行。所以当你选择静态链接方式意味着将UCRT库直接嵌入到最终的可执行文件中,这样可以在没有安装UCRT的环境中运行程序,但会增加可执行文件的大小。
后面的lib-vcxxxx等5个文件夹:你首先要了解的是,Visual Studio是一由微软公司开发的集成开发环境(IDE),支持多种编程语言,包括C++、C#、Visual Basic等。不甚了解的同学可以参考https://blog.csdn.net/friday1203/article/details/139568486?spm=1001.2014.3001.5501 中的"写C++程序的基本流程",以及https://blog.csdn.net/friday1203/article/details/139737861?spm=1001.2014.3001.5501 中的"VS项目设置"这些知识点。而VC++(Visual C++)也是微软公司的C++开发工具,是Visual Studio中的一个组成部分,随着Visual Studio版本的更新,VC++也随之更新。例如,Visual Studio 2010中包含了VC2010,Visual Studio 2015中包含了VC2015。因此,VC++实际上是Visual Studio中的一个特定于C++的开发环境。
所以,这5个文件夹是针对不同VS版本编译的库文件夹,而且每个版本都支持静态链接和动态链接两种方式。下面我们针对每个文件一一说明:
如果你选择静态链接,那你使用glfw3.lib这个静态链接库文件即可。此后的小标题3展示的就是这种方式。
如果你选择动态链接,那你得使用glfw3.dll和glfw3dll.lib两个文件。glfw3.dll是动态链接库文件,是库GLFW所涉及到的变量、函数、类、结构体等对象符号(就是名称)的定义。是你的.exe程序在执行时动态调用的文件。所以是和你的.exe程序文件放在一起的。而glfw3dll.lib文件实际上是一个静态库,是联合glfw3.dll一起使用的,因为glfw3dll.lib中是一堆glfw3.dll中的所有函数、符号的位置,可供链接器在编译时链接它们。但是如果没有glfw3dll.lib这个文件,那我们就需要通过函数名来访问glfw3.dll中的函数了。所以说二者是搭配使用的,也所以叫"静态的"动态库版本。此后的小标题4展示的就是这种方式。
最后还有一个glfw3_mt.lib文件,这个文件是支持多线程操作的静态库文件,它可以在多线程环境中安全地使用。意思就是这个文件是将GLFW的源代码编译成支持多线程环境,可以在多线程应用程序中使用的二进制库文件。这种库文件通常用于提高程序的并发性能和响应速度,特别是在需要同时处理多个任务或请求的应用场景中。
这些基本上就是我们在官网下载的、适用于window平台的、预编译后的、二进制的、glfw压缩包,里面的所有东西了。
(3)小结:
上面的文件夹基本上就是三类:第一类是库的介绍文件;第二类是库的头文件;第三类是针对不同平台(32位还是64位)、不同系统(不同的编译器编译)的、用于静态链接方式的静态库文件,和用于动态链接方式的动态库文件。当然你想静态链接还是动态链接,那得看你的项目适合哪种方式,因为两种方式各有利弊、各有优缺点。
include包含目录中的.h文件,是头文件,是你自己项目预编译时使用的。
静态库文件.lib文件(真实的变量和函数定义所在的文件),是你自己项目在静态编译和静态链接阶段要使用的。
动态库文件(.dll文件和dll.lib文件),其中dll.lib文件是你自己项目在静态编译和静态链接阶段要使用的,而.dll文件则是你项目的.exe可执行文件在执行过程中需要动态链接的。
清楚这些后,以后你要引用别的外部库时,你要下载什么包(源码包还是二进制包)?从你下载的包中拷贝哪些适用于你项目的文件?你心里就有底了。
2、再强调一些基本概念
如果前面讲的内容你不是太明白,本小标题就是针对上面的查漏补缺,或者是针对上面的重复强调。
(1)首先我想说的是,上文中几个链接一定要先吃透,尤其是编译原理部分。编译原理各个子流程你不懂,后面你肯定没法懂。
(2)一般我们下载外部库,其实我们就是需要:include和library,包含目录和库目录:
包含目录(就是include)里面是一堆我们需要使用的头文件。头文件的作用就是提供声明,告诉编译器哪些函数是可用的,就是告诉编译器你放心编译吧,源码中的这些函数在库文件中都是有定义的。所以有了这些头文件,我们自己的源代码中使用这个库中函数或者类啥的,至少就可以预编译了,否则你预编译阶段都过不了。当然你也可以选择不要第三方库的头文件,而是自己写它们的函数声明,这也是可以的,后面静态链接实操中会有相关展示。
库目录(就是.lib文件或者.dll文件)是一个已经预先编译了的二进制库文件。这两个东西才是最最有价值的东西,因为二者都是真实的变量和函数定义所在的文件,只不过一个是用于静态链接,一个是用于动态链接。
还要说明的是,头文件是同时支持静态和动态链接的。当预编译完毕后,就进入真正的编译阶段,此阶段源程序就会被超级细颗粒的打散,然后词法分析、语法分析、语义分析,生成目标代码,或者说最后生成汇编码。所以此时的汇编码也是零散的。所以下一个阶段就是链接阶段。当你是静态链接时,链接器是把编译的所有颗粒链接到一起,这样就可以正确执行了。当你是动态链接时,链接操作不仅发生在代码静态编译,还会发生在程序被加载时以及程序执行时。但是不管发生在什么阶段,链接操作的本质都是找到每个符号和函数在哪里,并把它们连接起来。
(3)什么是静态链接、动态链接?
静态链接意味着这个静态库(.lib库文件)会被放到你的可执行文件中,它是嵌入在你的exe文件中、或者其他操作系统下的,和你的程序合并成一个可执行文件了。
而动态链接则意味着这个动态库(.dll库文件)是在你自己的应用程序运行时被调用的、是独立于你的应用程序之外的、是你的应用程序运行时,随用随调的、肩并肩作战的。所以.dll库文件一般是和你的.exe文件放在同一个文件夹下面的。那我的.exe文件如何能随用随调.dll?方法一是,你的应用程序启动时,就加载与.dll文件搭配的dll.lib文件,就是上文说的静态的动态库。dll.lib中都是.dll中的函数的地址,就直接按照地址执行了。方法二是,如果你没有dll.lib文件,那你得有一个叫loadLibrary的函数或其他类似功能的函数。当你的应用程序运行时,loadLibrary函数就载入.dll动态库,从中拉出函数,然后再调用函数。
(4)静态链接和动态链接的区别
静态链接和动态链接的主要区别就是,库文件是否被编译到exe文件中或链接到exe文件中,还是只是一个单独的文件,在运行时你把它放在你的exe文件旁边或某个地方,然后你的exe文件就可以加载它。
也所以,动态链接是链接发生在运行时,静态链接是在编译时发生的。现实中很多情况我们是更想静态链接而不是动态链接。所以即使你想动态链接,你也得清楚静态链接是怎么回事。
静态链接是发生在编译阶段,程序加载及执行时就不用链接了。
动态链接是发生在运行时,就是只有当你真正启动你的可执行文件时,你的动态链接库才会被加载。所以它实际上不是可执行文件的一部分。 当你启动一个普通的可执行文件时,这个可执行文件就会被加载到内存,然而如果此时有一个动态链接库,这就意味着你的可执行文件在运行中有一个功能是,链接另外一个外部二进制文件,这就是动态的链接。这样当你运行你的可执行程序时,就会实现将一个额外的文件加载到内存中。那现在你的可执行程序的工作方式就变了,因为它在运行时需要某个外部库、某些动态库、某些外部文件。所以在你的可执行程序运行前,你要具备这些外部库、文件等东西,否则你的程序无法顺利运行。也所以我们会经常看到这样的场景:当你启动一个应用程序时,它弹出一个错误消息,比如没有找到dll,或者说需要dll等信息,而不能顺利启动这个程序。这就是动态链接的一种形式。所以之所以有动态链接方式,是因为可执行程序是知道有动态链接库的存在的,动态链接库是可执行程序顺利运行的必须。但是动态链接库也是一个单独的文件、或者是一个单独的模块,并且是在运行时加载的。所以一旦你加载失败,可执行程序就得被迫中断。
也所以你也可以动态的加载动态库,这样可执行文件就与动态库完全没有任何关系了,此时你可以启动你的可执行文件,也就是你的应用程序,它甚至不会要求你包含一个特定的动态库。但是在你的可执行文件中,你可以写代码去查找并运行时加载某些动态库,然后获得函数指针或者其他任何你需要的那个动态库中的东西,也就是使用那个动态库。也所以有些第三方发布的动态库,有时是会是一个"静态的"动态库版本!就是我的应用程序现场需要这个动态链接库,我已经知道里面有什么函数,我可以使用什么。
而另外的版本则是,我想任意加载这个动态库,我甚至都不知道里面有什么,但我就是想从那里取出一些东西,或者我就是想用它做我的事情。 这两种动态库,他们都有自己特定的用途。我们后面小标题4的示例就是第一种"静态的"动态库。就是我知道我的应用程序需要这个库,但我将动态地链接它。
(5)静态链接和动态链接的优缺点
静态链接和动态链接是有实际的性能差异的。静态链接在技术上更快,因为编译器和链接器可以看到全部的、需要链接的变量和函数,此时编译器或链接器实际上还可以执行链接时优化之类的操作。所以静态链接在技术上可以产生更快的应用程序,因为有很多优化方法可以被自动应用。
而对于动态库,编译器和链接器不知道后面会发生什么事情,就只能保存全部的变量和函数,只有当动态链接库被运行时的程序装载时,程序才被补充完整,此时就已经错过很多优化的机会。
事物总是有它的相反一面,使用静态链接会使你的程序更大、更占用内存。而动态链接更节省内存占用。所以要看你自己项目的情况。
(6)在visual studio项目中:
对于编译器,我们必须把它指向头文件(.h文件),也就是我们就有了包含目录中的变量和函数的符号声明,就是编译器就知道哪些变量名和函数名是可用的。也就是引入了名字。
对于链接器,我们必须把它指向库文件(不管是静态库还是动态库都要这样操作),就是告诉链接器,这是我的库文件,里面有变量名和函数名的定义,你把它们链接起来。这样我们的程序就能执行正确的变量和函数的定义。只不过在静态链接方式下,链接操作只发生在静态编译和静态链接阶段。而在动态链接方式下,链接操作除了静态编译阶段发生,还发生在项目程序的加载和执行阶段。
链接的本质不就是根据名称、参数、返回值类型三要素来匹配实现链接的。或者说通过这三要素来找到正确的函数的。
3、静态链接glfw二进制库文件的过程展示
上面的理论部分都清晰了后,我们开始实操如何添加二进制外部静态库。
(1)下图是对项目的介绍和一些前期的准备:
(2)设置编译器的相关设置
下一步我们就开始设置编译器的相关设置,也就是把GLFW库的include包含目录和.lib库文件的实际地址告诉编译器,让编译器可以找到GLFW库的头文件和静态库文件:
这样编译器就可以找到glfw的头文件了,有没有成功,我们看看代码有没有报错即可:
编译器可以顺利找到glfw库的头文件了,也就是glfw库的各种函数各种类等的声明编译器都可以用了,编译器就可以顺利编译了。
(3)设置链接器的相关设置
下面我们开始设置链接器的相关设置,让链接器可以顺利找到glfw这个外部库的glfw3.lib静态库文件,这样我们就可以调用glfw库中的函数和类等的定义了:
我们调用一个glfw库中的函数,看看链接器是否可以顺利链接上glfw库中的函数:
至此我们就成功静态链接了GLFW库,也就是可以正常使用GLFW库了。
(4)最后再澄清的一些事情:Name-mangling问题
上述两种情况,我们都可以顺利编译、顺利链接!那是因为语句A实际上只是提供了函数glfwInit()的声明,而我也可以自己写函数glfwInit()的定义,就是上图的B处, 甚至我也可以A都不要,完全自己写一个glfwInit()定义(就是情况1)就可以编译和链接了。所以我们可以看出,上述的两种情况其实根本就没有调用GLFW库中真正的glfwInit()函数的定义。因为真正的glfwInit()函数返回的是1(上上图所示)。
GLFW库实际上是一个C语言库,上面两种情况就是我们用C++混淆了名字(Name-mangling)。
也所以,其实我们是可以手动添加glfwInit函数的定义:
也所以,头文件和库的链接的作用都只是将项目的所有组成部分链接在一起。
也就是,头文件的作用就是提供声明,告诉编译器哪些函数是可用的,就是告诉编译器你放心编译吧,我源码中的这些函数在库文件中都是有定义的。
当编译器编译完毕后,链接器就开始上场,链接器要顺利链接到这些函数。这样当源代码中有调用这些函数的时候,就正确跳到了这些函数的定义代码,也就是正确执行了这些函数的功能。
4、动态链接GLFW二进制库文件的过程展示
上面的静态链接,我们包括了头文件、添加了静态库。所以此处的动态链接,头文件我们就不需要更改了,只需要把静态库改成动态库就行了。具体步骤如下:
(1)拷贝文件:把静态库改成动态库
我们要确保把glfw.dll文件放在一个可访问的地方。你可以放在整个应用程序中,然后设置库搜索位置。但是放在和可执行文件同一个文件夹里,是一定没有问题的,因为这是首选的自动搜索路径。
(2)设置链接器的相关设置
因为头文件是不需要改的,因为头文件是同时支持静态和动态链接的。所以我们就不需要设置编译器的相关设置了,只要设置链接器即可,而且只要添加文件名即可,路径还用静态链接时的路径:
glfw3.dll文件就是动态库文件,里面都是GLFW库中的变量、函数、类等的定义。
glfw3dll.lib文件基本上就是一堆指向.dll文件的指针。这样我们就不用在运行时去检索所有东西的位置。
说明:我们一定要同时编译这两个文件!因为如果你尝试使用不同的静态库,在运行时链接到dll,你可能会得到不匹配的函数和错误类型的内存地址,函数指针不会正常工作,所以这两个文件是由glfw发行的,所以它们是同时编译的,它俩是直接相关的,是不能分开的。
(3)看看我们的是否链接成功:
至此,我们也演示了如何使用GLFW动态库。
5、在VisualStudio中自建模块或库项目
本小标题讲如何在VisualStudio中创建多个项目,并且再创建一个库,让所有项目都能使用。
如果你的项目规模很大时,我们这样做就是创建模块或库,就可以多次重用这些代码了,而且还允许我们混合语言。本部分我们展示,继续使用C++,如何创建一个当作库的项目,尤其是当作一个静态库,然后把它链接到一个可执行文件中。
待续。。。。