4. 静态库与动态库:创建Qt静态库、创建Qt动态库、库的链接与导出

说到库,这几乎是每个Qt项目都绕不开的话题。我记得刚入行那会儿,有个同事把一堆源码直接复制到各个项目里,结果改一个bug要改七八个地方。嗯,后来他学会了用库,世界清净了。

说白了,库就是把你写好的代码打包成二进制,让别人(或者别的项目)直接调用。Qt里主要分两种:静态库和动态库。今天咱们就把它们彻底搞明白。

4.1 静态库 vs 动态库:到底选哪个?

先看一张图,帮你快速建立整体认知:

Qt 库类型选择决策图 静态库 (.a / .lib) • 编译时直接嵌入可执行文件 • 生成单个 .exe,无需额外文件 • 体积大,但部署简单 • 适合:小工具、内部工具 动态库 (.dll / .so / .dylib) • 运行时动态加载 • 可执行文件小,但需附带 .dll • 可独立更新库,无需重编主程序 • 适合:大型项目、插件系统 对比 💡 我的建议 团队内部共享代码 → 静态库 | 需要热更新或插件 → 动态库 | 不确定 → 先静态库 创建库项目 编写导出宏 链接到主项目 核心流程:创建 → 导出 → 链接

我个人习惯是:如果是公司内部的基础组件,比如日志库、网络库,我倾向于用静态库。为什么?部署省心,一个exe搞定。但如果是插件系统,或者需要热更新的模块,那必须上动态库。

4.2 创建Qt静态库

创建静态库其实很简单。在Qt Creator里新建项目时,选择「Library」→「C++ Library」,然后选「Static Library」就行。

但这里有个坑——静态库不需要导出宏。为什么呢?因为静态库的代码在编译时直接嵌入到主程序里,不存在「导出符号」这个概念。你想想看,所有代码都揉在一起了,还需要什么导出?

静态库的.pro文件示例:

# mylib_static.pro
QT       -= gui
TEMPLATE = lib
CONFIG  += staticlib

TARGET = mylib_static

SOURCES += \
    myclass.cpp

HEADERS += \
    myclass.h

注意那个 CONFIG += staticlib,这是关键。少了它,Qt会默认生成动态库。

小技巧:静态库的命名通常以 lib 开头,比如 libmylib_static.a。这在Linux和macOS上是惯例,Windows上则是 .lib 后缀。

4.3 创建Qt动态库

动态库就稍微复杂一点了。核心在于「导出宏」。我在项目中遇到过好几次,明明代码写对了,但链接时就是报「无法解析的外部符号」。查了半天,发现是导出宏没加对。

Qt提供了一个非常方便的宏:Q_DECL_EXPORT 和 Q_DECL_IMPORT。通常我们会这样封装:

// myglobal.h
#ifndef MYGLOBAL_H
#define MYGLOBAL_H

#include <QtCore/qglobal.h>

#if defined(MYLIB_LIBRARY)
#  define MYLIB_EXPORT Q_DECL_EXPORT
#else
#  define MYLIB_EXPORT Q_DECL_IMPORT
#endif

#endif // MYGLOBAL_H

然后在类定义前加上这个宏:

// myclass.h
#include "myglobal.h"

class MYLIB_EXPORT MyClass
{
public:
    MyClass();
    void doSomething();
};

对应的.pro文件:

# mylib_dynamic.pro
QT       -= gui
TEMPLATE = lib
CONFIG  += dll

TARGET = mylib_dynamic

DEFINES += MYLIB_LIBRARY

SOURCES += \
    myclass.cpp

HEADERS += \
    myclass.h \
    myglobal.h

注意: DEFINES += MYLIB_LIBRARY 这一行必须加。它告诉编译器:当前是在编译库本身,所以用 Q_DECL_EXPORT;如果是外部项目引用这个头文件,则用 Q_DECL_IMPORT。我曾经漏掉这个,结果折腾了半小时。

4.4 库的链接与导出

库写好了,怎么用?在主项目的.pro文件里链接就行。

链接静态库:

# main_app.pro
QT       += core gui
TEMPLATE = app

SOURCES += main.cpp

# 链接静态库
LIBS += -L$$PWD/../libs -lmylib_static
INCLUDEPATH += $$PWD/../libs/include

链接动态库:

# main_app.pro
QT       += core gui
TEMPLATE = app

SOURCES += main.cpp

# 链接动态库
LIBS += -L$$PWD/../libs -lmylib_dynamic
INCLUDEPATH += $$PWD/../libs/include

看起来差不多对吧?但有个关键区别:动态库在运行时需要能找到 .dll 文件。Windows上通常放在exe同目录,或者加到PATH里。Linux上可以用 LD_LIBRARY_PATH 环境变量,或者把 .so 放到 /usr/lib。

导出符号的几种方式:

方式 说明 适用场景
Q_DECL_EXPORT/IMPORT Qt推荐的宏方式 绝大多数Qt项目
.def 文件 Windows专用,手动列出导出符号 需要精确控制导出符号时
__declspec(dllexport) MSVC原生方式 非Qt的纯C++项目
Q_PLUGIN_METADATA Qt插件系统专用 Qt插件开发

我个人最推荐第一种。Qt的宏封装得很好,跨平台也省心。你想想看,如果项目要在Windows和Linux上同时编译,用 .def 文件就得维护两份,多麻烦。

4.5 避坑指南

做库开发这些年,踩过的坑不少。分享几个典型的:

  • 静态库里的全局对象:如果静态库里有个全局单例,多个静态库都链接了它,可能会出现多个实例。我曾经排查了一整天,最后发现是链接顺序的问题。
  • 动态库的版本管理:Windows上叫 mylib.dll,Linux上叫 libmylib.so.1.0.0。版本号搞错了,程序启动时直接崩溃。建议用 CONFIG += skip_target_version_ext 来控制。
  • 导出整个类 vs 导出个别函数:导出整个类最省事,但会暴露所有成员。如果只想暴露几个接口,可以用 extern "C" 导出C风格函数。
  • 调试符号:Release版本的动态库默认不带调试信息。如果线上崩溃需要分析dump,记得编译时加上 CONFIG += force_debug_info。

我的习惯:每个库项目都单独建一个 include 目录,只放对外暴露的头文件。内部实现用的头文件放在 src 目录里。这样使用者一眼就知道该包含哪些文件,不会把内部细节暴露出去。

嗯,关于静态库和动态库,核心就是这些。记住一个原则:静态库省心但体积大,动态库灵活但管理复杂。具体选哪个,看你的项目需求。没有绝对的对错,只有合不合适。