一、库的分类
在Windows下库可以分为静态和动态两种。动态库按照链接的时机又可分为两类:
- 隐式链接。使用即加载,有时候又称为静态链接或者加载时动态链接。因为不需要使用函数指针即可使用其中的函数, 编译器帮你默默链接,使用上非常方便;(本文只讲解这一种)
- 显式链接。按需加载,必须使用加载DLL语句和设置DLL函数的指针,有时候称为动态加载或者运行时加载。
与静态链接库的区别在于,静态链接会将所有的对象代码复制到可执行程序中,动态链接则只会在可执行程序中放入含有查找DLL的信息。显然动态链接的可执行程序体积更小,所以除了在生成动态库的时候还需要生成对应的导出符号帮助应用程序在运行时调用动态库符号。
与应用程序相比,DLL程序只能有一个实例,而应用程序可以有多个;应用程序能够作为进程加载,可以管理诸如堆栈、全局内存、文件句柄和消息队列之类的资源,DLL并不能做这些事情。[1]
动态库的优点:
- 节省内存,减少交换。多个进程共享DLL,静态库不能共享;(这就是为什么DLL叫共享库的原因:多个进程共享)
- 节省了磁盘空间和带宽;
- 维护容易。静态库必须重新编译生成exe;
- 新功能方便添加。通过显示链接发现一个DLL方式;
二、库的内容
假设我们需要创建一个动态库,其功能有:
- 打印一个apple的
void printApple()
函数 - 一个计算和的方法
int add(int,int)
分别在library.cpp和library.h增加以下内容:
//library.cpp
#include <iostream>
#include "library.h"
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int myadd(int i,int j)
{
return i+j;
}
//library.h
#ifndef ___LIBRARY_H
#define ___LIBRARY_H
void hello();
int myadd(int,int);
#endif
三、创建并使用动态库(静态链接形式)
在Linux中,源代码中的所有符号是默认导出的,而在Windows下恰好相反。默认全部导出的好处免去繁琐的宏定义用来表示符号,缺点是体积、封装性变差。__declspec标识一个符号是否为需要输出的符号[2],如果你需要输出所有的符号,可以考虑使用set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
,这样就算你不显式指出所有符号,你也可以生成编译所需的*.lib
[3]。
// Example of the dllimport and dllexport class attributes
__declspec( dllimport ) int i;
__declspec( dllexport ) void func();
将需要导入和导出的符号用__declspec(dllimport)
修饰即可。使用宏定义可以增加可读性:
#define DllImport __declspec( dllimport )
#define DllExport __declspec( dllexport )
修改头文件,修改后的头文件如下:
//library.h
#ifndef LIBRARY_H
#define LIBRARY_H
#define DLL_IMPORT __declspec(dllimport)
#define DLL_EXPORT __declspec(dllexport)
#endif //处理宏重复包含问题
#ifdef BUILD_DLL
//根据是否传入宏BUILD_DLL决定是编译库还是使用库
DLL_EXPORT void hello();
DLL_EXPORT int myadd(int,int);
#else
DLL_IMPORT void hello();
DLL_IMPORT int myadd(int,int);
#endif
Tips:无论静态库还是动态库都需要用到
*.lib
文件,前者直接注入可执行程序中,后者用于揭示可执行运行时应该如何找到自己想要的接口(功能实现)。如果不使用__declspec
,默认不会进行任何符号导出,编译器自然不会生成任何.lib
文件。
cmake_minimum_required(VERSION 3.15)
project(buildLib)
set(LIBRARY_OUTPUT_PATH ../lib)
add_library(myLib SHARED library.cpp)
没有加入编译宏,因此只生成了*.dll
文件:
使用这个库的程序,编译时会出现“文件无效或损坏”的错误:
出现这个的原因是因为没有提供有效的调用动态库内功能的信息文件(*.lib
)。加上编译宏BUILD_DLL
:
cmake_minimum_required(VERSION 3.15)
project(buildLib)
set(LIBRARY_OUTPUT_PATH ../lib)
add_library(myLib SHARED library.cpp)
add_compile_definitions(BUILD_DLL) #注意不要再加-D了
可以看到,在lib
目录下既生成了*.lib
,也生成了*.dll
:
使用者使用动态库的时候,直接链接*.lib
进行编译:
运行的时候什么都没有打印,这是因为可执行程序不知道去哪里搜索所需动态库(为啥没点提示,困惑),利用VS开发工具可以查看依赖情况:
Window动态库搜索顺序[4]:
- 已知动态库,如
Kernel32.dll
User32.dll
- 当前进程可执行模块所在目录
- 当前目录
- Windows系统目录。使用
GetSystemDirectory
函数获取这个目录(C:\Windows\system32) - Windows目录。使用
GetWindowsDirectory
函数获取这个目录(C:\Windows) - PATH环境变量
所以只要将我们生成的动态库放到任意一个搜索路径即可,这里我直接放在了与可执行程序同级目录,再次运行程序,结果如下:
至此,动态库创建和使用已经成功。
四、创建并使用静态库
4.1 生成静态库
使用静态库默认符号全部导出,所以不需要特殊处理。CMake的内容如下:
cmake_minimum_required(VERSION 3.15)
project(buildLib)
set(LIBRARY_OUTPUT_PATH ../lib)
add_library(myLib STATIC library.cpp)
新建一个文件夹,名为build(VSCODE默认会帮你做好这些事情,你只需要将CMakeLists.txt放在源码同级即可),然后cmake ..
,新生成的静态库在编译目录的上一级名叫lib
的文件夹。如下:
4.2 使用这个静态库
新建一个文件夹,内含一个主函数,功能是使用我们刚刚新建的静态库。
//main.cpp
#include "library.h"
#include <iostream>
int main()
{
hello();
std::cout<<"the result is 1+2="<<myadd(1,2)<<std::endl;
return 0;
}
在CMakeLists.txt中增加库的头文件搜寻目录,指出具体的库文件名称(绝对、相对均可)或者增加库搜寻目录,这里用的绝对路径。CMakeLists.txt如下:
project(UseLib)
cmake_minimum_required(VERSION 3.15)
include_directories(C:\\Users\\Fred\\buildLib)
add_executable(Demo main.cpp)
target_link_libraries(Demo C:\\Users\\Fred\\buildLib\\lib\\Debug\\mylib.lib)
ok,正确输出了结果,说明调用没有问题。
[1] https://docs.microsoft.com/zh-cn/cpp/build/linking-an-executable-to-a-dll?view=msvc-170
[2] https://docs.microsoft.com/zh-cn/cpp/cpp/dllexport-dllimport?view=msvc-170
[3] https://www.kitware.com/create-dlls-on-windows-without-declspec-using-new-cmake-export-all-feature/
[4] https://docs.microsoft.com/en-us/previous-versions/7d83bc18(v=vs.140)?redirectedfrom=MSDN