程序库创建和使用

这篇笔记我们介绍在Windows和Visual Studio环境下如何创建和使用C/C++静态库,以及如何创建和使用C/C++动态链接库。

静态库

静态库(Static Library)是一组编译好的目标代码的集合,在其他C/C++工程引入静态库时,静态库的代码在链接阶段被直接复制到可执行文件中,这个过程也被称为静态链接,程序构建完成后,运行时将不再需要静态库文件。Windows操作系统中,静态库文件的后缀名是.lib

创建静态库

创建静态库十分简单,我们直接在Visual Studio中创建一个静态库工程即可。

在静态库工程中,我们编写如下代码。我们定义了一个PrintHello函数,其中打印一条信息。虽然代码使用C++语言编写,但这里我们将其声明为了extern "C"方式,这样无论C还是C++都能使用我们的静态库。当然,我们也可以不使用extern "C",不过这样由于C++的函数名改写机制,该库就只能被C++工程使用了。

mylib.h

#pragma once

extern "C" void PrintHello();

main.cpp

#include <iostream>
#include "mylib.h"

void PrintHello()
{
    std::cout << "Hello, world!" << std::endl;
}

上述代码编译后会生成一个.lib静态库文件,我们可以将静态库文件和头文件复制到其它位置,供其他工程引用。

链接静态库

对于需要引用静态库的工程,我们需要将头文件加入包含目录,然后配置链接器的库和库查找路径,使其能够正确找到并链接我们编写的静态库。注意这里头文件其实不是一定必须的(虽然它通常都是存在的),头文件存在的意义其实是提供函数声明,如果真的没有头文件,我们自己手写一个函数声明也是可以正确编译运行的。

这里我们以一个控制台工程为例,在Visual Studio中如下设置编译器和链接器的选项,如下图依次添加了头文件包含路径、库查找路径和具体链接的库。

此时我们就可以在代码中引入头文件,并正确编译、链接和运行了。

main.cpp

#include "mylib.h"

int main()
{
    PrintHello();
    return 0;
}

运行结果如下。

动态链接库

Windows中的动态链接库(Dynamic Link Library)即我们常说的DLL,它也是一组编译好的代码集合,但在其他C/C++工程引入动态链接库时,动态链接库中的代码不会被复制到可执行文件中,而是可执行文件在运行时动态加载和链接到动态链接库,如果可执行程序运行时在搜索路径内找不到动态链接库,程序就会报错。Windows操作系统中,动态链接库文件的后缀名是.dll

在Windows下开发时,我们的程序如果要使用动态链接库,通常需要配合使用导入库。导入库是一个和动态链接库同名的.lib文件,虽然它看起来有点像静态库,但导入库确实不是静态库。MSDN中描述导入库“包含链接器解析的对导出动态链接库函数的外部引用所需的信息”,简而言之就是我们的程序编译时需要导入库中的信息,否则程序运行时不知道去哪个地址调用动态链接库中的函数,也是同样的原因,我们也可以理解,程序只在构建阶段需要导入库,运行时不需要导入库。

动态链接库能够在程序运行中动态的装载和卸载,但Windows操作系统对动态链接库的处理也要比静态库复杂很多,Windows中我们开发的一个可执行程序对动态链接库的链接和装载过程大致如下:

  1. 当程序在链接阶段,编译器会检查程序中使用的导入库,并将动态链接库的名称和导出函数信息记录在程序的导入表中。
  2. 程序运行时,操作系统的动态链接器会根据程序的导入表查找所需的动态链接库,并将它们映射到进程的地址空间中。
  3. 动态链接器会遍历动态链接库的导出表,找到程序需要的函数的地址,并将程序的导入表中的函数符号重定位为实际的函数地址。
  4. 如果动态链接库依赖另一个动态链接库,递归执行上述过程。
  5. 如果动态链接库有初始化代码(例如全局变量的构造函数),动态链接器会调用动态链接库的入口点函数(DllMain),并传递DLL_PROCESS_ATTACH的参数,让动态链接库执行初始化操作。
  6. 当程序不再需要动态链接库时,动态链接器会调用动态链接库的入口点函数,并传递DLL_PROCESS_DETACH的参数,让动态链接库执行清理操作,并将动态链接库从进程的地址空间中卸载。

此外,动态链接库实际上有两种装载方式:隐式装载和显式装载。

隐式装载:隐式装载是指在程序编译链接时,就确定了程序需要使用的动态链接库,并将动态链接库的名称和导出函数的信息记录在程序的导入表中。当程序运行时,操作系统的动态链接器会自动根据导入表查找所需的动态链接库,并将它们映射到进程的地址空间中。

显式装载:显式装载是指在程序运行时,通过调用Win32API中的LoadLibraryGetProcAddressFreeLibrary等函数手动装载和卸载动态链接库,动态链接库装载的时机和位置由程序员决定。这种方式的优点是灵活高效,缺点是需要编写额外的代码和处理异常情况。

创建动态链接库

创建动态链接库,我们可以在Visual Studio中选择动态链接库项目模板。

这里还有一个问题我们需要关注。Windows中,对于动态链接库的函数声明有一些特殊的要求。对于导出函数,需要使用__declspec(dllexport)声明;对于导入函数,需要使用__declspec(dllimport)声明,这个__declspec其实是MSVC编译器的扩展语法,我们没必要纠结,但这里有一个问题,我们导出和导入的函数声明需要使用不同的方式来编写,难道我们要分别编写两个头文件分别用于导出和导入吗?这显然是不合理的。

一个最佳实践是使用宏定义方式,在头文件中区分库编译环境和引用该库的可执行程序的编译环境。这里我们的工程名为DemoDll,实际上在Visual Studio中创建该工程后,工程已经默认设置编译器在编译时增加一个宏DEMODLL_EXPORTS。我们借助这个宏即可区分编译的环境,当设置该宏时使用__declspec(dllexport),未设置时使用__declspec(dllimport)

DemoDll工程中的代码例子如下。

mydll.h

#pragma once

#ifdef DEMODLL_EXPORTS
#define DEMODLL_API __declspec(dllexport)
#else
#define DEMODLL_API __declspec(dllimport)
#endif

extern "C" DEMODLL_API int MyAdd(int a, int b);

main.cpp

#include "mydll.h"

int MyAdd(int a, int b)
{
    return a + b;
}

代码很简单,我们导出了一个MyAdd函数,它可以实现加法运算。

我们可以查看Visual Studio中库项目的相关设置,可以看到默认设置的DEMODLL_EXPORTS宏。

库项目构建完成后,会生成DemoDll.dllDemoDll.lib两个文件,分别是动态链接库和导入库。

使用动态链接库

需要引入动态链接库的工程需要设置头文件包含目录、库文件查找目录和具体的库文件,不过这些设置和引入静态库是完全一致的,唯一的区别是我们这里设置的不是静态库而是动态链接库的导入库,这里我就不重复黏贴了。

下面代码例子使用隐式装载的方式调用DemoDll.dll动态链接库。

#include <iostream>
#include "mydll.h"

int main()
{
    std::cout << MyAdd(1, 2) << std::endl;
    return 0;
}

运行结果如下。

实际上,使用动态链接库还有一点需要注意:动态链接库顾名思义是在程序运行时动态链接并调用其中的函数,简单来说就是程序运行时也需要动态链接库文件,可执行程序如果找不到动态链接库文件,运行时就会报错。运行一个程序时,Windows操作系统的动态链接器会从如下几个目录寻找动态链接库:

  1. 可执行文件的同目录
  2. 系统的Windows目录
  3. 系统的Windows\System目录
  4. 系统的Windows\System32目录
  5. 环境变量PATH指定的目录

实际开发中,我们最好将动态链接库一同发布到可执行文件的同目录下,放在系统目录下通常是不合适的,因为多个软件一旦库名相同就可能造成库冲突,库相互覆盖程序就会报错,甚至导致操作系统无法正常运行,很多早期的软件都存在类似的问题,我们实际开发中应该尽量避免修改系统的库目录。

DllMain入口函数

动态链接库有一个可选的入口函数DllMain,它是用于处理动态链接库生命周期事件的函数,它在动态链接库被加载或卸载时被调用,可以用于执行一些库装载初始化和库卸载清理等操作,DllMain的函数定义如下。

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // 程序实例句柄
    DWORD fdwReason,     // 事件类型
    LPVOID lpReserved    // 保留参数,通常为NULL
);

fdwReason可以取如下值:

  • DLL_PROCESS_ATTACH:动态链接库被装载时发生
  • DLL_PROCESS_DETACH:动态链接库被卸载时发生
  • DLL_THREAD_ATTACH:当前进程创建线程时发生,该事件用于通知DLL创建一些线程局部变量
  • DLL_THREAD_DETACH:线程退出时发生

DllMain的返回值如果是TRUE表示初始化成功,如果返回FALSE可以阻止动态链接库的装载。

下面代码是一个DllMain函数的例子,它在装载成功时打印一条消息。

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
    if (fdwReason == DLL_PROCESS_ATTACH)
    {
        std::cout << "DLL装载啦" << std::endl;
    }
    return TRUE;
}

显式装载动态链接库

前面介绍的使用动态链接库的方式其实是隐式装载,装载动态链接库由操作系统帮我们隐式完成,实际上还有一种显式装载方式。显式装载仅需要动态链接库.dll文件,不需要导入库,查找函数地址功能是我们在代码中手动编写的。下面是一个显式装载链接动态链接库的例子。

#include <iostream>
#include <windows.h>

typedef int(*MyAdd)(int, int);

int main()
{
    // 装载动态链接库
    HINSTANCE hInstanceDll = LoadLibrary(TEXT("DemoDll.dll"));
    if (hInstanceDll != nullptr)
    {
        // 找到函数调用位置
        MyAdd myAdd = (MyAdd)GetProcAddress(hInstanceDll, "MyAdd");
        if (myAdd != nullptr)
        {
            // 调用动态链接库中的函数
            std::cout << myAdd(1, 2) << std::endl;
            // 释放动态链接库
            FreeLibrary(hInstanceDll);
        }
    }
    return 0;
}

代码中,我们使用LoadLibrary装载动态链接库并获取了动态链接库的实例句柄,随后我们调用了GetProcAddress函数,这个函数能够通过函数名获取函数的调用地址(函数指针),此时我们就可以调用该函数了。最后,我们调用了FreeLibrary释放了动态链接库。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。