QT - Linux下动态库&静态库的创建及使用指南

写在前面

环境

  • Ubuntu 18.04
  • Qt 5.9.3

Qt 库的分类

QtCreator 中新建项目,选择 Library - C++库

在这里插入图片描述
可以发现,其提供了三种类型,分别是:

  • 共享库(动态链接库)
    选用该类型,将生成动态链接库,linux 下为 *.so ,而在 Windows 下为 *.dll

  • 静态链接库
    选用该类型,将生成静态链接库,最终生成的库为 *.a

  • Qt Plugin
    该类型与插件有关,这里暂且 PASS

ps:静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。编译之后程序文件大,但加载快,隔离性也好。动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次即可 1

如何生成库文件

共享库

新建项目,模版选择 Liabary - C++库,类型选择 共享库

LocationKits 略,模块选择如下:

在这里插入图片描述
默认选中 QtCore,若去除该模块,则 Qt 很多类型均无法使用,那只能使用 C/C++ 的数据类型了。
若该库包含 GUI,还需勾选 QtGui

创建完成后,得到以下文件:
在这里插入图片描述
这里有一个 testso_global.h 文件,存放的是一些宏定义

#include <QtCore/qglobal.h>

#if defined(TESTSO_LIBRARY)
#  define TESTSOSHARED_EXPORT Q_DECL_EXPORT
#else
#  define TESTSOSHARED_EXPORT Q_DECL_IMPORT
#endif

若库中只构建了一个类,为了避免后面使用 so库 的时候麻烦,我们可以将上面该部分的宏定义及头文件增加到 testso.h 中,然后注释掉 #include "testso_global.h"

然后就可以把 testso_global.h 删除了。这样使用隐式链接的时候就只需要移植 testso.h

若库中存在多个类,还是留着比较方便。就是使用隐式链接的时候需要把 testso_global.h 捎上。

这里我在 testso 中添加了两个函数测试

testso.h

#ifndef TESTSO_H
#define TESTSO_H

#include "testso_global.h"
#include <QString>
#include <QDebug>

class TESTSOSHARED_EXPORT Testso
{
    
    

public:
    Testso();

    QString getName();
    void testDebug();
};

#endif // TESTSO_H

testso.c

#include "testso.h"

Testso::Testso()
{
    
    
}

QString Testso::getName(){
    
    
    QString re = "testso";
    return re;
}

void Testso::testDebug(){
    
    
    qDebug() << "Debug test success.";
}

只构建的话就会直接生成共享库,如果运行的话就是下面这个样子:

在这里插入图片描述
出现该弹窗表示编译成功了,然后我们就得到了整整齐齐四兄弟。

在这里插入图片描述

静态库

静态库的生成,操作上与动态库相同,不再赘述。

生成的文件如下:
在这里插入图片描述
可以发现相比动态库,少了一个 *_global.h 文件。

同样我们也给它增加一个用于测试的函数:

test_staticdll.h

#ifndef TEST_STATICDLL_H
#define TEST_STATICDLL_H

#include <QDebug>


class Test_staticdll
{
    
    

public:
    Test_staticdll();
    void test();
};

#endif // TEST_STATICDLL_H

test_staticdll.c

#include "test_staticdll.h"


Test_staticdll::Test_staticdll()
{
    
    
}

void Test_staticdll::test(){
    
    
    qDebug() << "test Static dll is success.";
}

编译完成后,发现其生成了一个 *.a 文件

在这里插入图片描述

如何调用库文件

隐式链接

借助Qt工具

借助Qt工具,添加库 -> 外部库 -> 选择平台 -> 选择库文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

导入后在 *.pro 中会自动添加:

DISTFILES +=

unix:!macx: LIBS += -L$$PWD/../build-testso-Desktop_Qt_5_9_3_GCC_64bit-Debug/ -ltestso

INCLUDEPATH += $$PWD/../build-testso-Desktop_Qt_5_9_3_GCC_64bit-Debug
DEPENDPATH += $$PWD/../build-testso-Desktop_Qt_5_9_3_GCC_64bit-Debug

然后我们将 testso.h 复制到项目路径下。若 testso_global.h 有调用,则需将其一并复制。

在这里插入图片描述
然后在项目中需要使用到的地方导入头文件即可。

使用demo

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    
    
    ui->setupUi(this);

    Testso *test = new Testso();

    ui->label->setText(test->getName());
    test->testDebug();
}
MainWindow::~MainWindow()
{
    
    
    delete ui;
}

效果
在这里插入图片描述

手动添加

即不借助 Qt向导 ,直接修改 *.pro 实现库的导入。
可以参照前面利用向导生成的 *.pro

补充知识

  • -L 表示后面跟的是文件夹,工程会将这个路径加入库文件的搜索路径中
  • -l 表示后面跟的是一个库文件的名字
  • $$PWD 表示的是当前路径
  • /.. 表示返回上一路径

显式链接

仅供参考demo

即使用 Qlibrary 进行显式调用,该方法不需移植头文件,以下代码可供参考:


    QLibrary mylibrary("/home/hsy/SW/Qt5.9.3/Project/build-testso-Desktop_Qt_5_9_3_GCC_64bit-Debug/testso");

    if(!mylibrary.load()){
    
    
        //加载so失败
        qDebug() << "Load Testso.so is failed!";
        qDebug() << mylibrary.errorString();
    }
    //声明函数指针
    typedef QString (*Fun_getName)();
    typedef void (*Fun_testDebug)();

    //resolve得到库中函数地址
    Fun_getName getName = (Fun_getName)mylibrary.resolve("_ZN6Testso7getNameEv");
    Fun_testDebug testDebug = (Fun_testDebug)mylibrary.resolve("_ZN6Testso9testDebugEv");

    if(nullptr == getName){
    
    
       qDebug() << "Load fun() getName failed!";
    }else{
    
    
        ui->label->setText(getName());
    }

    if(nullptr == testDebug){
    
    
        qDebug() << "Load fun() testDebug failed!";
    }else{
    
    
        testDebug();
    }

    //卸载库
    mylibrary.unload();

代码解析

QLibrary 实例

常用的是以下方法:

通过构造函数传入文件名,这个文件名官方的建议是去除前缀及后缀的。例如我们在 Ubuntu 上生成的是 libtestso.so

我们传入 testso 即可,这个名字也被叫作 基名 ,至于前缀和后缀 QLibrary 会根据系统给你尝试加上。所以使用基名的写法是 有利于跨平台移植 的。

在这里插入图片描述
未使用绝对路径,则 QLibrary 的规则是先尝试加上前缀后缀,再去搜索所有系统特定的库位置(Ubuntu下为/usr/lib)。
在这里插入图片描述
所以如果已将 so库 移至系统库中,可以这样写:

QLibrary mylibrary("testso");

当我们传入 绝对路径 时,QLibrary 会先去尝试加载该目录,如果找不到文件再根据不同的平台特定的文件前缀或后缀再次尝试。

此外,我们也可以在创建实例后使用 setFileName( ) 显式的设置要加载的文件名 2

Ubuntu 下操作我还遇到了一个坑,请看so文件

在这里插入图片描述
在这里插入图片描述
libtestso.so 是链接到 libtestso.so.1.0.0 的,若将 libtestso.so 复制到需要调用该库的项目中,会出现什么呢?
在这里插入图片描述
感情你以为人家是葫芦兄弟…

在这里插入图片描述
其实人家是影分身!
在这里插入图片描述
心情就和 Winodw 下拷贝了快捷方式,而且原文件还不移动了一样。

解决方法

  • 拷贝 libtestso.so.1.0.0 ,然后将文件名改为 libtestso.so
  • 自行编译生成 so文件,然后拷贝该 so库

在这里插入图片描述

load( )

该函数用于 动态加载库文件,加载完成后,可通过 isLoaded( ) 判断加载成功与否,当然通过 load( ) 的返回值来判断也是可以的。

若加载失败,我们可以通过 errorString( ) 获取错误详情。

加载后,库将保留在内存中,直到应用程序终止。我们可以尝试使用 unload( ) 来卸载库,但是如果 QLibrary 的其他实例正在使用同一库,则调用将失败,并且仅当每个实例都调用了 unload( )时才进行卸载 3

resolve( )

QLibrary库 的典型用法是去解析一个库中的导出符号,并调用该符号表示的 C函数。这被称为“显式链接” ,使用的就是 resolve( )

所以使用该函数必须将符号从库中导出为 C函数,以便 resolve()起作用。这意味如果使用 C ++编译器 编译该库,则必须使用 extern "C" 将该函数包装在一个块中 3

那么如果 so库 中没有使用 extern "C" 呢,例如我…

我们知道 C++之所以能实现 多态,是因为其编译规则与 C 不同,C 编译生成的函数仅带 函数类型,例如 int_add,而 C++ 编译生成的函数还带上 参数类型,例如 int_add_int_int。当然具体实现和编译器有关。

我的想法是先查看 so库 中的函数 4Linux下可使用的指令有:

  • objdump -tT xxx.so
  • nm -D xxx.so

个人比较喜欢 nm,还可以使用 awk过滤

在这里插入图片描述
于是就有了参考代码中那串奇怪的函数名。

这里强烈建议在找不到函数的时候,使用该方法去查看 so库中是否包含所需函数,以及其函数名是什么,毕竟这依赖于编译器,以防被坑。

参考鸣谢


  1. Linux下动态库(.so)和静态库(.a) 的区别 ↩︎

  2. 使用QLibrary加载动态库 ↩︎

  3. QLibrary Class ↩︎ ↩︎

  4. Linux查看动态库.so导出函数列表 ↩︎

猜你喜欢

转载自blog.csdn.net/weixin_40774605/article/details/105724118