baizy77的个人主页

http://www.qtcn.org/bbs/u/88608  [收藏] [复制]

baizy77

小白

  • 2

    关注

  • 8

    粉丝

  • 1

    访客

  • 等级:新手上路
  • 总积分:0
  • 男,1977-11-27

最后登录:2021-01-06

更多资料

日志

4.1 案例6 开发一个DLL(Dynamic Link Library动态链接库)

2020-12-24 14:50


本案例对应的源代码目录:src/chapter04/ks04_01。程序运行效果见图4-1。

图4-1 案例6运行效果
在软件项目开发过程中会不可避免地碰到代码复用问题,比如,在项目A中实现的功能也会在项目B中使用。这时就可以把重复的功能封装到DLL模块中。那么,用Qt怎样开发DLL呢?利用Qt开发DLL,大概分为两大步:封装DLL,使用DLL。
下面介绍具体步骤。
(1)将DLL中引出类的头文件移动到include目录。
(2)在DLL的pro文件中定义宏。
(3)编写DLL引出宏的头文件。
(4)在DLL引出类的头文件中使用引出宏。
(5)在EXE项目中添加对DLL的引用。
(6)在EXE中调用DLL的接口。
假设封装后的DLL为ks04_01_dll,这个DLL引出的类CPrint所在的原始头文件(未引出类时的代码)见代码清单4-1。
代码清单4-1

// myclass.h
#pragma once
class CPrint {
public:
    CPrint(){}
    ~CPrint(){}
public:
    void printOnScreen(const char*);
};
int func1();
int func2(int, float);

现在介绍将CPrint类封装到DLL中的详细开发步骤。
1.将DLL中引出类的头文件移动到include目录
因为要把DLL作为公共模块,所以应该把DLL中类CPrint所在的头文件myclass.h移动到公共的include目录,而不是继续放在DLL源代码目录。所以,应该为整个项目创建公共include目录。该include目录下可以继续新建子目录,从而区分不同子模块的公共头文件。
2.在DLL的pro文件中定义宏
既然把头文件移动到其他目录了,那么首先要修改DLL的pro文件中的INCLUDEPATH,否则在构建时就找不到这个头文件了。除此之外,还要修改pro文件中的HEADERS配置项,原因也是头文件位置变了。移动头文件之前,在HEADERS配置项中包含该头文件时可以不写路径,移动后就需要把头文件的路径写全。另外,别忘记把DLL的pro文件中的TEMPLATE选项设置为lib。

TEMPLATE   = lib
...
INCLUDEPATH += $$TRAIN_INCLUDE_PATH/ks04_01
HEADERS+= .    \
            $$TRAIN_INCLUDE_PATH/ks04_01/myclass.h

在Linux/Unix环境下开发DLL时,无须对引出类或者引出接口做特殊声明,但在Windows下情况有所不同。在Windows下编译引出类所在的头文件时,编译器需要明确知道自己正在构建EXE模块还是DLL模块。如果是构建EXE,编译器看到的头文件中的引出类应该用__declspec(dllimport)进行声明,如代码清单4-2所示。
代码清单4-2

// myclass.h
class  __declspec(dllimport)  CPrint {
    ...
};

如果是构建DLL,编译器看到的头文件中的引出类应该用__declspec(dllexport)进行声明,如代码清单4-3所示。
代码清单4-3

class  __declspec(dllexport)  CPrint {
    ...
};

对比代码清单4-2和代码清单4-3后可以得知,编译器在构建EXE和构建DLL时看到的同一个头文件中的内容应该不同,这就需要编写两个头文件。这两个头文件内容基本一致,仅仅是对引出类或引出接口的定义稍有不同,不同之处就在于需要分别使用__declspec(dllimport)和__declspec(dllexport)关键字。这是Windows下使用MSVC的C++编译器导致的结果。如果需要为所有引出类都提供两个内容基本一致的头文件,那么工作量就太大了,这样不但造成代码冗余还容易引入其他问题。那该怎么解决这个问题呢?别急,现在就一步步解决它。在DLL的pro文件中定义一个宏__KS04_01_DLL_SOURCE__,宏的拼写最好跟项目名称有关,以防跟其他项目冲突。定义这个宏目的是为另一个宏定义做准备。

// ks04_01_dll.pro
win32{
    DEFINES *= __KS04_01_DLL_SOURCE__
}

3.编写DLL引出宏的头文件
既然在Windows下需要区分__declspec(dllimport)和__declspec(dllexport)这两个关键字,而且只能为EXE项目和DLL项目提供同一个头文件,那就可以把这两个关键字定义成宏,如代码清单4-4所示。编译器在构建EXE和构建该头文件所属的DLL时,再把这个宏分别解析成__declspec(dllimport)和__declspec(dllexport)。
代码清单4-4

// ks04_01_export.h
#pragma once
// 动态库导出宏定义
#ifdef WIN32// Windows platform
#    if defined __KS04_01_DLL_SOURCE__
#        define KS04_01_Export __declspec(dllexport)                            
#    else
#        define KS04_01_Export __declspec(dllimport)                            
#endif
#else// other platform
#    define KS04_01_Export                                                      
#endif // WIN32

在代码清单4-4中,根据操作系统的不同将KS04_01_Export定义为不同的关键字。Windows下(WIN32分支)根据是否定义了__KS04_01_DLL_SOURCE__宏来进行不同的处理。因为已经在DLL的pro文件中定义了__KS04_01_DLL_SOURCE__,所以构建DLL时会执行标号①处的代码,即把KS04_01_Export定义成__declspec(dllexport)。而在EXE项目的pro文件中并未定义__KS04_01_DLL_SOURCE__,因此构建EXE时会执行标号②处的代码,即把KS04_01_Export定义成__declspec(dllimport)。在Unix/Linux等非Windows操作系统中构建项目时则执行标号③处的代码,也就是单纯定义KS04_01_Export宏,以便编译器在解析后面的代码时看到这个符号可以把它当成合法的符号。在Linux/Unix中,这个符号没有其他含义,仅仅是个符号而已。
代码清单4-4所示的头文件ks04_01_export.h实际上也可以不用独立存在。如果DLL只提供了一个头文件用来定义引出类、引出接口,那么就不用创建ks04_01_export.h文件,而是把该头文件的内容直接复制到引出类所在头文件的开头部分即可。
4.在DLL引出类的头文件中使用引出宏
现在只需要在引出类、引出接口的前面编写KS04_01_Export就可以把类或接口引出了。在引出类所在头文件中包含ks04_01_export.h,见代码清单4-5中标号②处。然后在引出类或引出接口定义代码中增加KS04_01_Export字样,见代码清单4-5中标号③处、标号④处、标号⑤处。请注意KS04_01_Export宏用来定义引出类与引出接口时语法上的不同。在标号③处是在class关键字和类名之间编写KS04_01_Export,而标号④处、标号⑤处将KS04_01_Export写在整个引出接口定义之前。
代码清单4-5

/*!
* Copyright (C) 2018
\file: myclass.h
\brief ...
\author ...
\Date ...
* please import ks04_01_dll.dll                                                        
*/
#pragma once
#include "ks04_01_export.h"                                                            
class  KS04_01_Export CPrint{                                                          
    ...
};
KS04_01_Export int func1();                                                            
KS04_01_Export int func2(int, float);                                                  

还有很重要的一点,标号①处的注释用来说明:在使用该头文件时需要引入哪个库文件。本案例中,如果需要用到myclass.h这个头文件,就要引入ks04_01_dll这个动态链接库。也就是说,在使用该头文件的项目的pro文件中需要引入ks04_01_dll。
LIBS += -lks04_01_dll
5.在EXE项目中添加对DLL的引用
完成DLL的修改后,需要在EXE中或者其他DLL中引入这个DLL。这需要修改调用者的pro文件,在其LIBS配置项中添加对DLL的引用,如代码清单4-6所示。
代码清单4-6

# ks04_01_exe.pro
debug_and_release {
    CONFIG(debug, debug|release) {
        LIBS+= -lks04_01_dll_d                                                    
        TARGET = ks04_01_exe_d
    }
    CONFIG(release, debug|release) {
        LIBS+= -lks04_01_dll                                                      
        TARGET= ks04_01_exe
    }
} else {
    debug {
        LIBS+= -lks04_01_dll_d                                                    
        TARGET= ks04_01_exe_d
    }
    release {
        LIBS+= -lks04_01_dll                                                      
        TARGET = ks04_01_exe
    }
}

在代码清单4-6中的标号①处、标号③处,对构建Debug版的项目进行配置,在标号②处、标号④处,对Release版进行配置。这样就能保证编译器在构建项目时去链接对应版本的lib文件。
6.在EXE中调用DLL的接口
现在进入最后一个环节,在EXE或者其他DLL中调用本案例DLL的接口。其实这跟调用同一个项目中的接口没什么区别。调用一共分两步:第一步include被调用者所在的头文件;第二步,使用引出类定义对象或调用引出接口。
(1)编写include语句包含引出类所在的头文件,见代码清单4-7。
代码清单4-7

// main.cpp
#include "myclass.h"

(2)使用引出类定义对象或调用引出接口,见代码清单4-8。
代码清单4-8

// main.cpp
int main(int argc, char * argv[]) {
    ...
    CPrint pr;
    pr.printOnScreen("it is a test!");
    func1();
    func2(2, 3.f);
}


----------------------------------------------------------------------------------------------------------------------------------------------
《Qt 5/PyQt 5实战指南》目录

分类:Qr入门与提高|回复:0|浏览:345|全站可见|转载
 

Powered by phpwind v8.7 Certificate Copyright Time now is:05-02 18:57
©2005-2016 QTCN开发网 版权所有 Gzip disabled