C++动态库中的静态调用和动态调用,延迟加载

devtools/2025/3/19 13:31:11/

动态库(Dynamic Library)是包含可以由多个程序同时使用的代码和数据的文件。在Windows上,它们通常被称为DLL(动态链接库),而在Linux和macOS上,它们通常被称为共享对象(.so文件)。当程序使用动态库时,有两种主要的调用方式:静态调用(也称为隐式链接)和动态调用(也称为显式链接)。此外,还有一种与动态调用相关的特性叫做延迟加载

静态调用(隐式链接)

在静态调用中,编译器和链接器会处理库的引用。需要在编译时告诉链接器我们想要使用哪些库,通常通过命令行参数或IDE设置。对于Windows平台上的DLL,这通常意味着添加对.lib文件的引用;对于Linux/macOS平台上的共享对象,则是直接链接.so文件。

Windows 示例 (C++)

假设我们有一个名为mydll.dll的动态库,其中定义了一个函数int add(int a, int b)。为了在程序中使用这个函数,我们需要一个相应的导入库mydll.lib。

// main.cpp - 使用静态调用方式// 假设 we have an import library mydll.lib for mydll.dll
#pragma comment(lib, "mydll.lib") // 告诉链接器需要链接 mydll.libextern "C" int add(int a, int b); // 声明从 DLL 导出的函数int main() {int result = add(5, 3); // 直接调用 DLL 中的函数printf("Result: %d\n", result);return 0;
}

Linux/macOS 示例 ©

在Linux或macOS上,如果我们有一个名为libmydll.so的共享库,我们可以这样链接并调用它:

// main.c - 使用静态调用方式#include <stdio.h>// 假定 libmydll.so 提供了 add 函数
extern int add(int a, int b);int main() {int result = add(5, 3); // 直接调用共享库中的函数printf("Result: %d\n", result);return 0;
}// 编译时需要链接共享库
// gcc main.c -o main -L. -lmydll

动态调用(显式链接)

在动态调用中,我们会在运行时手动加载库,并获取库中函数的地址。下面是如何在不同平台上执行此操作的示例。

Windows 示例 (C++)

// main.cpp - 使用动态调用方式#include <windows.h>
#include <iostream>typedef int (*AddFunc)(int, int);int main() {HMODULE hModule = LoadLibrary(L"mydll.dll"); // 加载 DLLif (!hModule) {std::cerr << "Failed to load DLL" << std::endl;return 1;}AddFunc add = (AddFunc)GetProcAddress(hModule, "add"); // 获取函数指针if (!add) {FreeLibrary(hModule);std::cerr << "Failed to get function address" << std::endl;return 1;}int result = add(5, 3); // 通过函数指针调用std::cout << "Result: " << result << std::endl;FreeLibrary(hModule); // 卸载 DLLreturn 0;
}

Linux/macOS 示例 ©

// main.c - 使用动态调用方式#include <dlfcn.h>
#include <stdio.h>typedef int (*AddFunc)(int, int);int main() {void *handle = dlopen("./libmydll.so", RTLD_LAZY); // 加载共享库if (!handle) {fprintf(stderr, "%s\n", dlerror());return 1;}AddFunc add = (AddFunc)dlsym(handle, "add"); // 获取函数指针const char *dlsym_error = dlerror();if (dlsym_error) {fprintf(stderr, "%s\n", dlsym_error);dlclose(handle);return 1;}int result = add(5, 3); // 通过函数指针调用printf("Result: %d\n", result);dlclose(handle); // 卸载共享库return 0;
}

延迟加载

延迟加载是一种特殊的动态库加载机制,它结合了静态调用的简单性和动态调用的灵活性,延迟加载允许我们在首次调用函数时才加载库,而不是在程序启动时就加载。这可以提高启动速度,并减少内存占用。在Windows上,可以通过链接器选项/DELAYLOAD来实现。在Linux上,可以通过RTLD_LAZY标志来实现。

Windows 示例 (C++)

// main.cpp - 使用延迟加载#pragma comment(linker, "/DELAYLOAD:mydll.dll") // 延迟加载 DLL
#pragma comment(lib, "delayimp.lib") // 需要链接 delayimp.libextern "C" int __declspec(dllimport) add(int a, int b); // 声明从 DLL 导出的函数int main() {int result = add(5, 3); // 第一次调用时加载 DLLprintf("Result: %d\n", result);return 0;
}

在上面的代码中,/DELAYLOAD:mydll.dll指令告诉链接器不要在程序启动时加载mydll.dll,而是等到第一次调用add函数时再加载它。delayimp.lib是一个特殊的库,它提供了必要的支持以实现延迟加载

Linux/macOS

使用 dlopen 和 RTLD_LAZY

这是最直接的方法,通过显式链接(动态调用)来实现延迟加载。你可以使用 dlopen 函数并传递 RTLD_LAZY 标志来加载库。这种方式允许你在运行时根据条件选择性地加载库,并且符号解析会在首次引用时进行。

void *handle = dlopen("libexample.so", RTLD_LAZY);
if (!handle) {fprintf(stderr, "%s\n", dlerror());exit(EXIT_FAILURE);
}

编译器和链接器支持的延迟加载

一些编译器和链接器提供了内置的支持来实现延迟加载。例如,GNU 编译器集合 (GCC) 和 GNU 链接器 (ld) 提供了 -Wl,–no-as-needed 和 -Wl,-z,lazy 选项来控制库的加载行为。

-Wl,–no-as-needed:确保所有指定的库都被包含在最终的二进制文件中,即使它们没有被直接引用。

-Wl,-z,lazy:告诉链接器以延迟方式加载符号,即符号解析将在首次引用时进行,而不是在加载时立即解析。

你可以在编译和链接时使用这些选项,例如:

gcc -o myprogram myprogram.o -Wl,--no-as-needed -Wl,-z,lazy -lexample

使用 -Wl,–as-needed 和 -Wl,–unresolved-symbols=ignore-all

另一种方法是结合使用 -Wl,–as-needed 和 -Wl,–unresolved-symbols=ignore-all 选项。–as-needed 确保只有在真正需要的时候才加载库,而 --unresolved-symbols=ignore-all 允许链接器忽略未解析的符号,直到它们在运行时被引用。

gcc -o myprogram myprogram.o -Wl,--as-needed -Wl,--unresolved-symbols=ignore-all -lexample

动态链接器配置

Linux 的动态链接器(如 ld.so 或 ld-linux.so)也提供了一些环境变量来控制库的加载行为:

LD_BIND_NOW:如果设置为非空值,强制所有符号在加载时立即解析,相当于 RTLD_NOW。

LD_BIND_LAZY:如果设置为非空值,使符号解析延迟到首次引用时,相当于 RTLD_LAZY。

可以通过设置这些环境变量来影响整个系统的库加载行为,或者仅对特定的应用程序生效:

export LD_BIND_NOW=1  # 强制立即解析所有符号
export LD_BIND_LAZY=1 # 延迟解析符号

总结

静态调用

优点:

简单易用,不需要额外的代码来加载或卸载库。

函数调用更高效,因为地址在程序启动时就已经确定了。

缺点:

如果库不存在或版本不兼容,程序可能无法启动。

即使程序运行时不使用某些库功能,这些库也会被加载到内存中。

动态调用

优点:

可以根据条件选择性地加载库,节省资源。

可以更容易地支持插件架构或多版本库共存。

缺点:

代码更加复杂,需要管理库的加载、卸载和错误处理。

每次调用库函数时都需要通过指针进行间接调用,这可能会稍微降低性能。

延迟加载

优点

提高了程序的启动速度,因为不必要的库不会在启动时加载。

减少了内存占用,只有实际需要的库才会被加载。

缺点

如果库加载失败,错误可能会在程序运行过程中出现,而不是在启动时。

一些平台或编译器可能不支持延迟加载,或者需要特定的编译选项来启用。


http://www.ppmy.cn/devtools/168351.html

相关文章

34个适合机械工程及自动化专业【论文选题】

论文选题具有极其重要的意义&#xff0c;它直接关系到论文的质量、价值以及研究的可行性和顺利程度。选题明确了研究的具体领域和核心问题&#xff0c;就像给研究旅程设定了方向和目的地。例如&#xff0c;选择 “人工智能在医疗影像诊断中的应用” 这一选题&#xff0c;就确定…

Cursor IDE 入门指南

什么是 Cursor? Cursor 是一款集成了 AI 功能的现代代码编辑器&#xff0c;基于 VSCode 开发&#xff0c;专为提高开发效率而设计。它内置强大的 AI 助手功能&#xff0c;能够理解代码、生成代码、解决问题&#xff0c;帮助开发者更快、更智能地完成编程任务。 基础功能 1.…

SpringData Elasticsearch:索引管理与全文检索

文章目录 引言一、Spring Data Elasticsearch基础配置二、实体映射与索引定义三、索引管理操作四、文档管理与CRUD操作五、高级全文检索实现六、聚合与统计分析七、最佳实践与性能优化总结 引言 Elasticsearch作为一款强大的搜索引擎&#xff0c;被广泛应用于全文检索、日志分…

函数凹凸性

1 凸函数&#xff1a; f ′ ′ ( x ) > 0 f^{\prime \prime}(x)>0 f′′(x)>0&#xff0c;函数向上开口 2 凹函数 f ′ ′ ( x ) < 0 f^{\prime \prime}(x)<0 f′′(x)<0&#xff0c; 函数向下开口

栈和队列在嵌入式系统下的C语言实现

一、引言 二、栈(Stack) 2.1 基本概念 2.2 存储结构与C语言实现 2.3. 栈的实现 三、队列(Queue) 3.1 基本概念 3.2 存储结构与C语言实现 3.3. 队列的实现 一、引言 作为计算机科学中两种基础且关键的线性数据结构,栈(Stack)和队列(Queue)凭借其严格的访问规则…

基于大数据的酒类商品数据可视化分析系统

【大数据】基于大数据的酒类商品数据可视化分析系统 &#xff08;完整系统源码开发笔记详细部署教程&#xff09;✅ 目录 一、项目简介二、项目界面展示三、项目视频展示 一、项目简介 该系统充分利用Python与Flask的强大后端处理能力&#xff0c;结合前端Layui框架&#xff0…

密码学 网络安全 科普 网络安全密码技术

网络加密包括密码技术和网络加密方法两个方面。 一、 密码技术   密码技术一般分为常规密码和公钥密码。   常规密码是指收信方和发信方使用相同的密钥&#xff0c;即加密密钥和解密密钥是相同或等价的。比较著名的常规密码算法有DES及其各种变形、IDEA、FEAL、Skipjack…

基于 Docker 和 Flask 构建高并发微服务架构

基于 Docker 和 Flask 构建高并发微服务架构 一、微服务架构概述 &#xff08;一&#xff09;微服务架构的优点 微服务架构是一种将应用程序拆分为多个小型、自治服务的架构风格&#xff0c;在当今的软件开发领域具有显著的优势。 高度可扩展性&#xff1a;每个微服务可以独…