Windows静态链接库与动态链接库的创建和显式与隐式调用

一、链接库简介

库是写好的现有的,成熟的,可以复用的代码。链接库主要可分为静态库与动态库两种,动态库调用又可分为隐式调用与显式调用两种,下面给出两者间的主要区别。如果理解有困难可以先继续往下看,实践一遍后再回味。

1.1 静态库与动态库区别

  • 静态链接库的后缀名为lib,动态链接库的导入库的后缀名也为lib。不同的是,静态库中包含了函数的实际执行代码,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息;
  • 由于静态库是在编译期间直接将代码合到可执行程序中,而动态库是在执行期时调用DLL中的函数体,所以执行速度比动态库要快一点;
  • 静态库链接生成的可执行文件体积较大,且包含相同的公共代码,造成内存浪费;
  • 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;
  • DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性,适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。

1.2 动态库隐式与显式调用的区别

  • 隐式调用需要调用者写的代码量少,调用起来和使用当前项目下的函数一样直接;而显式调用则要求程序员在调用时,指明要加载的动态库的名称和要调用的函数名称;
  • 隐式调用由系统加载完成,对程序员透明;显式调用由程序员在需要使用时自己加载,不再使用时,自己负责卸载;
  • 由于显式调用由程序员负责加载和卸载,好比动态申请内存空间,需要时就申请,不用时立即释放,因此显式调用对内存的使用更加合理, 大型项目中应使用显式调用;
  • 当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用方甚至不需要包含动态链接库的头文件(需要调用的函数名是通过GetProcAddress()函数的参数指明的),而使用隐式调用时,则调用方不可避免要加上动态库中的头文件和导入库文件;
  • 显式调用更加灵活,可以模拟多态效果(具体见后文)。

二、Windows静态库的创建和使用

2.1 Windows静态库的创建

如果是使用VS命令行生成静态库,也是分两个步骤来生成程序:

  • 首先,通过使用带编译器选项 /c 的 Cl.exe 编译代码 (cl /c mystatic.cpp),创建名为“mystatic.obj”的目标文件。
  • 然后,使用库管理器 Lib.exe 链接代码 (lib mystatic.obj),创建静态库mystatic.lib。

我们一般不这么用,使用Visual Studio工程设置更方便。创建win32控制台程序时,勾选静态库类型;或者打开工程“属性面板”–>”配置属性”–>”常规”–>配置类型–>选择静态库(.lib)也可以生成静态库。

示例代码使用跟Linux篇的一致,方便对比,下面仅列出头文件代码:

// mylib.h

#ifndef _TEST_H
#define _TEST_H

extern "C" float add(float a, float b);
extern "C" float sub(float a, float b);
extern "C" float mul(float a, float b);

#endif

编译生成静态库文件在目录.\staticlib\Debug\staticlib.lib下。

2.2 Windows静态库的调用

2.2.1菜单链接包含静态库文件和头文件

  • “属性面板”–>”配置属性”–>“链接器”–>”常规”–>“附加库目录”–>输入静态库所在目录;
  • “属性面板”–>”配置属性”–>“链接器”–>”输入”–>“附加依赖库”–>输入静态库名staticlib.lib;
  • “属性面板”–>”配置属性”–>“C/C++”–>” 常规”–>“附加包含目录”–>键入mylib.h 头文件所在目录的路径或浏览至该目录;
  • 编译生成可执行文件在目录.\test\Debug\test.exe,已链接静态库,可独立运行。

2.2.2命令链接包含静态库文件和头文件

先将前面生成的staticlib.lib静态库文件和头文件mylib.h拷贝到测试案例test.cpp所在目录(也可以包含全路径),在test工程添加现有头文件mylib.h,给出test.cpp示例代码如下:

#include "stdafx.h"
#include "mylib.h"
#include <cstdlib>
#include <iostream>
#pragma comment(lib, "staticlib.lib")

using namespace  std;

int main(int argc, char *argv[])
{
    float a = 3.7, b = 2.9;

    for(int i = 0; i < argc; ++i){
        printf("argv[%d]: %s\n", i, argv[i]);
        if(i == 1)
            a = atof(argv[1]);
        if(i == 2)
            b = atof(argv[2]);
    }

    cout << "a + b = " << add(a, b) << endl;
    cout << "a - b = " << sub(a, b) << endl;
    cout << "a * b = " << mul(a, b) << endl;

    return 0;
}

生成可执行文件,Ctrl+F5执行结果如下图示:
Windows静态链接库与动态链接库的创建和显式与隐式调用
也可以通过VS给main函数传递参数运行调试,具体方法如下:

  • “属性面板”–>”配置属性”–>”调试”–>”命令参数“–>输入参数,比如”345.21 123.89“

Windows静态链接库与动态链接库的创建和显式与隐式调用

三、Windows动态库的创建和使用

3.1 Windows动态库的创建

创建win32控制台程序时,勾选DLL类型;或者打开工程“属性面板”–>”配置属性”–>”常规”–>配置类型–>选择动态库(.dll)也可以生成静态库。

创建好的工程默认提供了一个dllmain.cpp里面有一个DllMain函数是动态链接库的入口函数,VS已经帮我们创建好了,不需要改动该文件,示例代码如下:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}
// mydll.h头文件声明函数以extern "C" __declspec(dllexport)导出为dll

#pragma once
#ifdef DYNAMIC_H_			//通过该宏是否定义,自动判断要导出还是导入dll的函数或变量符号
#define DLL_EXPORT_IMPORT extern "C" __declspec(dllexport)
#else
#define DLL_EXPORT_IMPORT extern "C" __declspec(dllimport)
#endif

DLL_EXPORT_IMPORT float add(float a, float b);
DLL_EXPORT_IMPORT float sub(float a, float b);
DLL_EXPORT_IMPORT float mul(float a, float b);

包含库函数定义的源文件与静态链接库一致,只是多了如下的宏定义,编译时弹出警告,建议宏定义放到预编译头文件内,把该宏定义放到"stdafx.h"头文件内再次编译无警告,编译生成静态库文件在目录.DLL\dynamic\Debug\dynamic.dll下。

#define DYNAMIC_H_

补充一点关于"mydll.h"宏定义的说明,查微软MSDN有下面一段话。本文示例并没有导出变量,所以没有__declspec(dllimport)直接使用extern “C” __declspec(dllexport)也是可以的,但如果要导出变量,就要使用上面"mydll.h"宏定义的形式了。

不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。

3.2 Windows动态库的隐式调用

动态库的隐式调用与前面提到的静态库调用很相似,同样需要包含一个头文件mydll.h和一个动态导入库文件dynamic.lib,同样支持菜单操作和命令包含两种方式,这里不再详细赘述,只给出测试文件test.cpp包含的头文件如下:

//test.cpp包含的头文件,包括mydll.h和dynamic.lib

#include "stdafx.h"
#include "mydll.h"
#include <cstdlib>
#include <iostream>
#pragma comment(lib, "dynamic.lib")

这里可能大家有个疑问,动态库怎么还有一个dynamic.lib文件?即无论是静态链接库还是动态链接库,最后都有lib文件,那么两者区别是什么呢?其实,两个是完全不一样的东西。
Windows静态链接库与动态链接库的创建和显式与隐式调用
staticlib.lib的大小为18KB,dynamic.lib的大小为2KB,静态库对应的lib文件叫静态库,动态库对应的lib文件叫导入库。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。

测试程序执行结果及传参过程跟静态库方法一致,只有一点区别,可执行程序必须能找到其对应的依赖动态库dynamic.dll,否则执行报错。

四、Windows动态库的显式调用

Windows显式调用动态库,#include <windows.h>头文件中提供了下面几个接口:

  • HMODULE LoadLibraryA(LPCSTR lpLibFileName):加载 DLL 和获取模块句柄;
  • FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName):获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接;
  • BOOL FreeLibrary(HMODULE hLibModule):使用完 后释放DLL 。

下面给出示例代码,生成动态链接库(libdynamicmath.so)的代码跟前面一致,这里只列出测试代码explicit.cpp如下:

// explicit.cpp不再包含"mydll.h"头文件和"dynamic.lib"导入库文件,通过程序内部命令显式加载和释放

#include "stdafx.h"
#include <cstdlib>
#include <iostream>
#include <Windows.h>

using namespace  std;

int main(int argc, char *argv[])
{
    
    if(argc < 2){
        cout << "Argument error." << endl;
        exit(1);
    }
    
    float a = 3.7, b = 2.9;
    char *libname = nullptr;

    for(int i = 0; i < argc; ++i){
        printf("argv[%d]: %s\n", i, argv[i]);
        if(i == 1)
            libname = argv[1]; 
        if(i == 2)
            a = atof(argv[2]);
        if(i == 3)
            b = atof(argv[3]);
    }

    //open the lib
    HMODULE hMod = LoadLibraryA(libname);
    if(hMod == NULL){
        cout << "Load " << libname << " failed." << endl;
       exit(1);
	}else{
		cout << "Load " << libname << " successfully." << endl;
	}
    //get function pointer
    typedef float (*pf_t)(float, float);
    pf_t add = (pf_t)GetProcAddress(hMod, "add");
    pf_t sub = (pf_t)GetProcAddress(hMod, "sub");
    pf_t mul = (pf_t)GetProcAddress(hMod, "mul");
	
    if(!(add && sub && mul)){
        cout << "Can't find symbol function." << endl;
		FreeLibrary(hMod);
        exit(1);
	}else{
		cout << "Find this symbol function." << endl; 
	}
    //call library function
    cout << "a + b = " << add(a, b) << endl;
    cout << "a - b = " << sub(a, b) << endl;
    cout << "a * b = " << mul(a, b) << endl;
    //close the lib
    if(!FreeLibrary(hMod)){
        cout << "Close " << libname << " failed." << endl;
        exit(1);
    }

    return 0;
}

通过VS给main函数传递参数(“dynamic.dll 876.456 345.897”)运行调试,结果如下图示:
Windows静态链接库与动态链接库的创建和显式与隐式调用
把动态链接库以参数形式传递给可执行程序进行选择调用,如果多个dll中有同一函数的不同实现,便可以通过这种方式实现多态调用效果。读者也可以稍加改动,把函数名也通过参数传递给可执行程序实现选择调用。可以通过dumpbin /EXPORTS dynamic.dll命令查看符号表,从中找到库文件里面的函数名。如果dumpbin显示不识别或不是内部命令,需要找到其所在路径并添加到环境变量。
Windows静态链接库与动态链接库的创建和显式与隐式调用

本文的源代码可以到GitHub下载:https://github.com/StreamAI/LinkLibrary (不熟悉GitHub使用的可以参考文章:GitHub社会化编程)。

如果想了解Linux环境动态库与静态库的创建和使用,可以查看我的另一个博文:Linux静态链接库与动态链接库的创建和显式与隐式调用

如果想深入了解动态库与静态库的链接与调用过程,推荐一本书《程序员的自我修养—链接、装载与库》。