当前位置: 首页 > article >正文

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

动态库(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.lib

extern "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"); // 加载 DLL
    if (!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); // 卸载 DLL
    return 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.lib

extern "C" int __declspec(dllimport) add(int a, int b); // 声明从 DLL 导出的函数

int main() {
    int result = add(5, 3); // 第一次调用时加载 DLL
    printf("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.kler.cn/a/594584.html

相关文章:

  • 再学:ERC721扩展、ERC1155、SBT,OpenSeaNFT市场 NFT Market 习题讲解
  • Multisim学习-01 特点安装使用和第一个仿真实例
  • 智慧人大系统(源码+文档+讲解+演示)
  • [极客大挑战 2019]Upload_3.19BUUCTF练习day3(2)
  • 《信息系统安全》(第一次上机实验报告)
  • 虚拟机如何扩容磁盘
  • PHP实现用户登录与注册功能
  • ruoyi 小程序使用笔记
  • 第四周日志-用网络请求理解bp(2)
  • React如何导入md5,把密码password进行md5加密
  • LeetCode hot 100—只出现一次的数字
  • 目标检测YOLO实战应用案例100讲-面向交通复杂目标场景的机器视觉检测技术研究(续)
  • 初识Brainstorm(matlab)
  • 2025年汽车加气站操作工考试精选题库
  • 数据库的两种模式
  • L1-005-008
  • 掌握 Shopee 商品数据:用爬虫解锁无限商机
  • 鸿蒙NEXT开发之开屏广告实现
  • 力扣hot100——三数之和(双指针)
  • SVN 泄露