C++学习笔记:静态库和动态库


前言

在大型工程中会经常使用到库文件,一个原因是软件的不同功能通常由不同的开发人员负责,如果都使用源文件,维护和测试将是个十分头疼的问题。此外,开发者有时并不想将代码开源,库文件也可以将源代码隐藏起来。在Windows和Linux下,库文件的本质和基本使用方式是相同的。

1. 编译流程

编译就是从源文件生成可执行文件(二进制文件)的过程:

1)预编译(编译预处理)

引入头文件(#include)、去除注释、处理条件编译指令(#ifndef)、宏替换(#define)

2)编译
语法分析,生成汇编代码。将.cpp文件翻译成.s汇编代码。

3)汇编

将汇编代码.s翻译成.o文件。一个.cpp文件生成一个.o文件。

4)链接

文件A中调用了另一个文件B中的内容,需要将其链接为一个整体。静态库和动态库的本质区别就在这个环节体现。

2. 静态库

2.1 概念

静态库在链接的时候会将整个库中的代码拷贝到最终的可执行文件中,当程序被执行,静态库中的代码也会一并被加载到内存中。

在 Linux 系统下,静态链接库一般以 .lib 为前缀、 .a 为后缀;在 Windows 系统下,静态链接库一般以 .lib 为前缀、 .lib 为后缀。

优点:

  • 加载速度快:因为可执行程序具备了程序运行的所有内容。
  • 移植方便:静态库在程序编译时会被连接到目标程序中,程序运行时将不再需要该静态库。因此发布程序无需提供静态库文件,没有外部依赖。

缺点:

  • 浪费内存:只要一个程序调用静态库,就会进行一份拷贝。多个程序调用会产生多个拷贝,导致内存空间浪费。
  • 更新困难:如果静态库文件进行了更新操作,就需要重新进行编译链接生成可执行程序。

2.2 gcc生成和使用静态库

生成静态库

# 1. -c生成.o文件, -I添加头文件路径
$ gcc a.cpp b.cpp c.cpp -I./include/ -c
# 2. 将生成的目标文件.o通过ar工具打包成静态库
$ ar rcs libMyStaticLib.a a.o b.o c.o
# 3. 发布静态库: 头文件(函数声明)+ 库文件(函数定义)

使用静态库

# 编译的时候指定库信息
	-L: 指定库所在的目录(相对或者绝对路径)
	-l: 指定库的名字, 掐头(lib)去尾(.a) ==> calc
# -L -l, 参数和参数值之间可以有空格, 也可以没有  -L ./lib -l calc
$ gcc main.cpp -I./include -L./lib -lMyStaticLib -o app

2.3 CMake生成和使用静态库

生成静态库

cmake_minimum_required(VERSION 3.0.0)
# 项目名称
project(MyStaticLib VERSION 0.1.0)
# 添加头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 添加源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LIST)
# 生成库文件
add_library(${PROJECT_NAME} STATIC ${SRC_LIST})

使用静态库

# 链接静态库文件的目录
link_directories (${PROJECT_SOURCE_DIR}/lib)
# 生成可执行文件
add_executable(${PROJECT_NAME} ${SRC_LIST})
# 链接库文件
target_link_libraries(${PROJECT_NAME} MyStaticLib)

3. 动态库

3.1 概念

动态库链接的时候只是存储指向动态库的引用,库文件中的代码不会被拷贝到可执行文件中。当程序运行时,动态库中的函数在程序中被调用了,这个时候动态库才加载到内存,如果不被调用就不加载,且运行的多个程序可以使用同一个加载到内存中的动态库。

Linux 系统下,动态链接库一般以 .lib 为前缀、.so 为后缀;在 Windows 系统下,动态链接库一般以 .lib 为前缀、 .dll 为后缀。

优点:

  • 可实现不同进程间的资源共享。
  • 更新方便:只需要将库文件进行替换,无需对程序进行重新编译。
  • 节约内存:运行的多个程序可以使用同一个加载到内存中的动态库。

缺点:

  • 依赖性强:发布程序时需要同时提供库文件程序才能运行。

目前常见的大型程序基本都使用动态库。

3.2 g++生成和使用动态库

生成动态库

# 1. 将.cpp文件编译打包为动态库
# -fpic -I./include/将.cpp文件汇编为.o文件
# -shared -o将.o文件打包为动态库
$ gcc a.cpp b.cpp c.cpp -fpic -I./include/ -shared -o libMySharedLib.so
# 2. 发布动态库: 头文件(函数声明)+ 库文件(函数定义)

使用动态库

# 编译的时候指定库信息
$ g++ main.cpp -Iinclude -Llib -lMySharedLib -o main
# 运行动态链接库会报错:可执行程序执行的时候找不到动态库
$ ./main 
./main: error while loading shared libraries: libMySharedLib.so: cannot open shared object file: No such file or directory

动态链接器:

程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息。当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载。动态库的检测和内存加载操作都是由动态连接器来完成的。

Linux 下动态库的库文件搜索路径是:

  1. 可执行文件内部的DT_RPATH

  2. 系统的环境变量LD_LIBRARY_PATH

  3. 系统动态库的缓存文件/etc/ld.so.cache

  4. 存储动态库和静态库的系统目录/lib/, /usr/lib

常用的添加动态库文件路径的方法为:

1) 将库路径添加到环境变量 LD_LIBRARY_PATH

LD_LIBRARY_PATH环境变量用于在程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径,注意,LD_LIBRARY_PATH中指定的路径会在系统默认路径之前进行查找。

  1. 编辑相关配置文件
vim ~/.bashrc
  1. 在文件最后添加
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:动态库的绝对路径
  1. 使配置文件生效
source ~/.bashrc

2) 拷贝动态库文件到系统库目录 /lib/ 或者 /usr/lib 中 (或者将库的软链接文件放进去)

# 库拷贝
sudo cp /xxx/xxx/libxxx.so /usr/lib
# 创建软连接
sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so

3.3 CMake生成和使用动态库

生成动态库

cmake_minimum_required(VERSION 3.0.0)
# 项目名称
project(MySharedLib VERSION 0.1.0)
# 添加头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 添加源文件
aux_source_directory(${PROJECT_SOURCE_DIR}/src SRC_LIST)
# 生成库文件
add_library(MySharedLib SHARED ${SRC_LIST})

使用动态库

CMake中添加链接库文件目录十分方便,只需要一行link_directories()就可以搞定!

# 链接动态库文件的目录
link_directories (${PROJECT_SOURCE_DIR}/lib)
# 测试的可执行文件
add_executable(${PROJECT_NAME} ${SRC_LIST})
# 链接库文件
target_link_libraries(${PROJECT_NAME} MySharedLib)

3.4 Windows下生成和调用动态库

生成动态库

在ELF(Linux下动态库的格式)下,共享库中所有的全局函数和变量在默认情况下都可以被其他程序使用,即ELF默认导出所有的全局符号。而在Windows下,编写动态库时需要显式地声明要导出的变量和函数,否则编译器默认所有的符号都不导出。

使用 Visual Studio 创建一个动态链接库项目

image-20221207220159443

添加实现动态库功能的头文件和源文件,注意在头文件开头添加如下条件编译指令:

#pragma once

#ifdef MYSHAREDLIB_EXPORTS //这里替换为自己的动态库名称
#define MYSHAREDLIB_API __declspec(dllexport)
#else
#define MYSHAREDLIB_API __declspec(dllimport)
#endif

MYSHAREDLIB_API 宏会对函数和变量声明设置 __declspec(dllexport) 修饰符。 此修饰符指示编译器和链接器从 DLL 导出函数或变量,以便其他应用程序可以使用它。

// 导出数据
extern MYSHAREDLIB_API std::vector<char> szBuffer;

// 导出类
class MYSHAREDLIB_API CMySharedLib
{
public:
	/* something */
};

// 导出函数
extern "C" MYSHAREDLIB_API void myFunc();
extern MYSHAREDLIB_API std::shared_ptr<CMySharedLib> getMySharedLibImpl();

尽管 DLL 的代码是用 C++ 编写的,但我们还是为导出的函数使用了 C 样式接口。 有两个主要原因:首先,许多其他语言支持导入 C 样式函数。其次,这样可避免一些与导出的类和成员函数相关的常见缺陷。但如果函数使用了C++特有的数据结构就不能使用 C 样式接口。

接下来,在源文件中编写动态库功能的实现,完成后进行编译就可以得到我们所需的 MySharedLib.lib(这个不是静态库而是导入库) 和 MySharedLib.dll 文件。

使用动态库

1) 隐式调用:.h .lib .dll 三件套

  • 将 DLL 标头(.h文件)添加到包含路径

使用第三方库时,可以将动态库的头文件复制到项目目录下,然后将其作为现有项添加到项目中。

当客服端应用程序包含动态库头文件时,此时未定义 MYSHAREDLIB_EXPORTSMYSHAREDLIB_API 会将 __declspec(dllimport) 修饰符应用于声明。 此修饰符可优化应用程序中函数或变量的导入。

但是,如果 DLL 的代码和客户端的代码在进行同步开发,则头文件可能会不是最新。要避免此问题,设置项目中的“附加包含目录”路径,使其包含指向原始头文件的路径。

项目属性 - C/C++ - 常规 - 附加包含目录

image-20221207220225761

注意:尽量使用相对路径,减小程序对系统环境的依赖。

  • 将 DLL 导入库(.lib文件)添加到项目中

首先,设置项目中的“附加库目录” 路径,使其在链接时包含指向原始库的路径。

项目属性 - 链接器 - 常规 - 附加库目录

默认情况下,生成的导入库位于 DLL 解决方案文件夹下的“Debug”文件夹中。如果创建发布版本,该文件会放置在“Release”文件夹中 。 可以使用 $(IntDir) 宏,这样无论创建的是哪种版本,链接器都可找到 DLL。

image-20221207220313612

然后,设置”附加依赖项“,告诉生成系统项目依赖于 MySharedLib.lib。

项目属性 - 链接器 - 输入 - 附加依赖项

image-20221207220331107

也可以通过代码设置,在客户端应用程序开头添加:

#pragma comment(lib, "MySharedLib.lib")
  • 复制 DLL 文件

Windows 下动态库的库文件搜索路径是:

  1. 本地应用目录
  2. Windows 系统目录,如 C:\Windows\System32
  3. Windows 目录, 如 C:\Windows
  4. 环境变量 PATH 中列出的目录

当操作系统加载应用时,它会查找调用的动态库文件(.dll 文件),如果在系统目录、环境路径或本地应用目录中找不到 DLL,则加载会失败。最简单有效的方法是将 DLL 文件拷贝到可执行文件同一目录下,也可以在环境变量 PATH 中添加 DLL 文件的所在目录

Windows 中不重启电脑让环境变量生效的方法:cmd 中输入 echo %path%

在 Visual Studio 中, 可将“后期生成事件”添加到项目中,以此添加一条命令,将 DLL 复制到生成输出目录 。

项目属性 - 生成事件 - 生成后事件 - 命令行

输入以下指令:

xcopy /y /d "存放DLL文件的路径" "$(OutDir)"
image-20221207220402380

2) 显式调用

显式调用无需导入库(.lib),因此不需要设置“附加库目录”和“附加依赖项”。但通过显式链接使用 DLL 的可执行文件必须显式加载和卸载 DLL。同时还必须设置函数指针,用于访问它从 DLL 使用的每个函数。 下面是使用显式调用的一些常见原因:

  • 应用程序直到运行时才知道它所加载的 DLL 的名称。 例如,应用程序可能会在启动时从配置文件获取 DLL 的名称和导出函数。
  • 当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用程序甚至不需要包含动态链接库的头文件。
  • 显示调用可以在程序执行的过程中随时加载和卸载 DLL 文件。

显式调用通常包含三步:加载动态库、获取函数并调用、卸载动态库。

  1. 调用 LoadLibrary() 函数加载 DLL 并获取模块句柄。
  2. 调用 GetProcAddress() 函数以获取应用程序调用的每个导出函数的函数指针。必须有一个 typedefusing 语句来定义所调用的导出函数的调用签名。
  3. 处理完 DLL 时,调用 FreeLibrary() 卸载 DLL。
#include <iostream>
#include <tchar.h>
#include <Windows.h>

//定义接口函数指针类型
typedef int (*pAdd)(int, int);
typedef int (*pSubstract)(int, int);
typedef int (*pMultiply)(int, int);
typedef double (*pDivide)(int, int);

int main()
{
    //获取 DLL 句柄
    HINSTANCE hDLL = LoadLibrary(_T("MySharedLib")); //_T 设置为宽字符
    if (nullptr == hDLL)
    {
        std::cout << "error: cannot load library" << std::endl;
        return -1;
    }

    //获取导出函数
    pAdd add = (pAdd)GetProcAddress(hDLL, "add");
    pSubstract substract = (pSubstract)GetProcAddress(hDLL, "substract");
    pMultiply multiply = (pMultiply)GetProcAddress(hDLL, "multiply");
    pDivide divide = (pDivide)GetProcAddress(hDLL, "divide");

    std::cout << "5 + 3 = " << add(5, 3) << std::endl;
    std::cout << "5 - 3 = " << substract(5, 3) << std::endl;
    std::cout << "5 * 3 = " << multiply(5, 3) << std::endl;
    std::cout << "6 / 4 = " << divide(6, 4) << std::endl;

    //卸载 DLL
    FreeLibrary(hDLL);
    return 0;
}

参考资料

Linux 静态库和动态库

C++动态库封装及调用

在 Visual Studio 中创建 C/C++ DLL

演练:创建和使用自己的动态链接库 (C++)

将可执行文件链接到 DLL

Windows/Linux链接器加载动态库的搜索路径顺序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值