4. 静态库与动态库:创建Qt静态库、创建Qt动态库、库的链接与导出
说到库,这几乎是每个Qt项目都绕不开的话题。我记得刚入行那会儿,有个同事把一堆源码直接复制到各个项目里,结果改一个bug要改七八个地方。嗯,后来他学会了用库,世界清净了。
说白了,库就是把你写好的代码打包成二进制,让别人(或者别的项目)直接调用。Qt里主要分两种:静态库和动态库。今天咱们就把它们彻底搞明白。
4.1 静态库 vs 动态库:到底选哪个?
先看一张图,帮你快速建立整体认知:
我个人习惯是:如果是公司内部的基础组件,比如日志库、网络库,我倾向于用静态库。为什么?部署省心,一个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 目录里。这样使用者一眼就知道该包含哪些文件,不会把内部细节暴露出去。
嗯,关于静态库和动态库,核心就是这些。记住一个原则:静态库省心但体积大,动态库灵活但管理复杂。具体选哪个,看你的项目需求。没有绝对的对错,只有合不合适。