找回密码
 立即注册
收起左侧

【独家连载】Qt入门与提高:KS04-01打基础-exe+dll编程

4
回复
6579
查看
[复制链接]
累计签到:41 天
连续签到:1 天
来源: 原创 2018-10-12 11:26:45 显示全部楼层 |阅读模式

马上注册,查看详细内容!注册请先查看:注册须知

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
本帖最后由 baizy77 于 2019-7-2 20:27 编辑

版权声明
---------------------------------------------------------------------------------------------------------------------
该文章原创于Qter开源社区(www.qter.org
作者: 女儿叫老白
转载请注明出处!
---------------------------------------------------------------------------------------------------------------------

-----------------------------------------------------------------------------
引言
----------------------------------------------------------------------------------------------------------------------
在软件项目研发过程中,我们不可避免的要碰到代码复用问题,也就是说我们实现一个功能后,供不同的开发者使用,或者为了使代码层次更清晰而将模块划分成不同的部分,这时候就需要把功能模块封装成dll。那么在使用Qt时,该怎样才能实现dll编写呢?
正文
----------------------------------------------------------------------------------------------------------------------
      利用Qt编写dll,大概分为两大步,第一步:封装dll,第二步:在其他dll或exe中使用该dll。详细步骤如下:
       1,将 dll头文件移到include目录           
       2, 修改dll的pro            
       3, 在头文件中定义dll的引出宏
       4, 在dll的头文件中使用引出宏
       5, exe项目添加对dll的引用
       6, exe中调用dll的接口
       下面我们来详细了解一下。
       首先我们假设我们封装的dll为ks04_01_dll,这个dll所引出的类所在的原始头文件为:
myclass.h:
  1. /*! \file: myclass.h
  2. \brief exe+dll编程示例,引出类的定义头文件
  3. \author 星点课堂:女儿叫老白
  4. \Date 2018/9
  5. */

  6. #ifndef _MYCLASS_DLL_H
  7. #define _MYCLASS_DLL_H

  8. class CPrint
  9. {
  10. public:
  11.     CPrint(){}
  12.     ~CPrint(){}
  13. public:
  14.     void printOnScreen(const char*);
  15. };

  16. int func1();
  17. int func2(int, float);

  18. #endif  // _MYCLASS_DLL_H
复制代码

Step 1 :将dll头文件移到include目录
       因为要把dll作为公共组件,所以应该把dll中需要引出的头文件转移到公共的include目录,而不应该继续放在dll所在目录。所以,您应该为整个项目设置专门的公共include目录。该include目录下可以继续设置子目录,用来区分不同的子模块。
Step 2 :修改dllpro
既然把头文件转移到其他目录了,那么首先需要把pro中的INCLUDEPATH做修改,否则就找不到这个头文件了:
代码清单:
ks04_01.pro:

  1. INCLUDEPATH += $TRAIN_INCLUDE_PATH/ks04_01
复制代码

       除此之外,我们还要修改HEADERS配置项,也是因为头文件位置变了:
ks04_01.pro:
  1. HEADERS+= .    \
  2.                     $TRAIN_INCLUDE_PATH/ks04_01/myclass.h
复制代码

----------------------------------------------------------------------------------------------------------------------
       在头文件转移之前,我们可以不用写全路径,但是既然转移了,就需要把头文件的目录补上。
下面我们来说关键内容。
我们需要在dll的pro中定义一个宏,用来在编译器解析该头文件时区分:到底是在编译dll还是编译exe。以便把下面定义的宏定义解析成不同的内容。在linux/unix环境下开发时,无需对引出类或者接口做特殊声明,而在windows下就不同了:对于同一个头文件,在编译exe和编译头文件所在的dll时,编译器需要明确知道自己正在编译exe还是正在编译头文件所在的dll,因为如果是在编译exe,那么它看到的头文件中的引出类应该定义成这样:

代码清单:
myclass.h:
  1. class  __declspec(dllimport)  CMyClass {
  2.        ……
  3. };
复制代码
当编译dll时,编译器看到的头文件中的引出类应该定义成这样:
myclass.h:
  1. class  __declspec(dllexport)  CMyClass {
  2.        ……
  3. };
复制代码
       对比这两个头文件后我们可以得知,对于同一个类CMyClass来说,编译器在编译exe时和编译dll时,使用的头文件内容应该是不同的,也就是说需要两个头文件。这两个头文件内容基本一致,仅仅是在对引出类或接口的定义稍有不同,不同之处就在于需要分别使用__declspec(dllimport)__declspec(dllexport)这是windows下C++编译器的语法解析要求所导致的结果。
       如果需要提供两个内容基本一致的头文件,那么这个工作量就太大了而且造成代码冗余并容易引入其他问题。该怎么解决这个问题呢?别急,我们现在就一步步解决它。
首先在dll的pro中定义一个宏__KS04_01_DLL_SOURCE__ :
代码清单:
  1. win32{
  2. DEFINES *= __KS04_01_DLL_SOURCE__
  3. }
复制代码
       这种定义宏的语法我们在前面章节已经讲过。此处需要注意的是该宏定义的命名原则:
       1,该dll中的宏定义不能与项目中其他宏定义重名。
       2,该宏定义最好与项目的名称相关。
       定义这个宏有什么作用呢?我们下面来看一下。

Step 3 :在头文件中定义dll的引出宏
       既然在windows上需要区分__declspec(dllimport)__declspec(dllexport),而我们只希望提供一个头文件而非两个,那么我们就把它们定义成一个宏,当然了,还有一个要求:编译器
在编译exe和编译该头文件所在的dll时,需要把这个宏分别解析成__declspec(dllimport)和__declspec(dllexport)。我们来看一下怎么实现:
ks04_01_export.h:
  1. #ifndef _KS04_01_EXPORT_H
  2. #define _KS04_01_EXPORT_H

  3. // 动态库导出宏定义
  4. #ifdef WIN32// windows platform
  5. #    if defined __KS04_01_DLL_SOURCE__
  6. #        define KS04_01_Export __declspec(dllexport)
  7. #    else
  8. #        define KS04_01_Export __declspec(dllimport)
  9. #endif

  10. #else// other platform
  11. #    define KS04_01_Export
  12. #endif // WIN32

  13. #endif  // _KS04_01_EXPORT_H
复制代码
       在这个头文件中,我们根据操作系统进行了不同的区分:
windows操作系统(上述代码中WIN32分支),根据是否定义了__KS04_01_DLL_SOURCE__宏来进行不同的处理,如果定义了这个宏,那么就把KS04_01_Export宏定义成__declspec(dllexport),而这正是编译dll所需要的,因为dll的pro中已经定义了__KS04_01_DLL_SOURCE__,所以会执行第一个分支。如果没有定义这个宏,就把KS04_01_Export宏定义成__declspec(dllimport),也就是执行后一个分支,而这正是编译exe时所需要的。
       在非windows操作系统(如unix/linux)则走else// otherplatform分支,也就是单纯定义KS04_01_Export,以便编译器在解析后面的代码时看到这个符号可以把它当成合法的符号,在linux/unix上,这个符号没有其他含义,仅仅是个符号而已。
这个引出宏定义用的头文件实际上也可以不用独立存在,如果您的引出类或者引出接口只有一个头文件,那么您就可以把上述头文件的内容直接放到引出类所在头文件的开头部分。
Step4 在dll的头文件中使用引出宏。
       做了前面这些铺垫之后,我们只需要在引出类或者引出接口前面使用KS04_01_Export
       首先,应该在引出类所在头文件中包含上面定义好的头文件:
#include "ks04_01_export.h"
然后,修改引出类的定义,也就是在引出类或引出接口定义代码中增加KS04_01_Export
myclass.h:
  1. /*! \file: myclass.h
  2. \brief exe+dll编程示例,引出类的定义头文件
  3. \author 星点课堂:女儿叫老白
  4. \Date 2018/9
  5. */

  6. #ifndef _MYCLASS_DLL_H
  7. #define _MYCLASS_DLL_H

  8. #include "ks04_01_export.h"

  9. class  KS04_01_Export CPrint {
  10. public:
  11.     CPrint(){}
  12.     ~CPrint(){}
  13. public:
  14.     void printOnScreen(const char*);
  15. };

  16. KS04_01_Export int func1();
  17. KS04_01_Export int func2(int, float);
复制代码

       请注意引出类与引出接口的编码方式有所不同。引出类时在class关键字和类名之间添加引出宏KS04_01_Export,而引出接口时是在整个接口定义前面添加引出宏KS04_01_Export

step5  exe项目添加对dll的引用
       dll编写完成后,我们需要在exe中或者其他dll中引入这个dll,方法是修改调用者的pro文件:
ks04_01_exe.pro:
  1. debug_and_release {
  2.     CONFIG(debug, debug|release) {
  3.         LIBS+= -lks04_01_dll_d
  4.         TARGET = ks04_01_exe_d
  5. }
  6.     CONFIG(release, debug|release) {
  7.         LIBS+= -lks04_01_dll
  8.         TARGET= ks04_01_exe
  9. }
  10. } else {
  11.     debug {
  12.         LIBS+= -lks04_01_dll_d
  13.         TARGET= ks04_01_exe_d
  14. }
  15.     release {
  16.         LIBS+= -lks04_01_dll
  17.         TARGET = ks04_01_exe
  18. }
  19. }
复制代码

       也就是修改LIBS配置项,增加对ks04_01_dll项目的引用。语法在前面的章节介绍过:在dll的模块名前面加上”-l”(小写的L)。
       这样就能保证编译器在编译调用者时去链接被调用dll的lib文件。

step 6 exe中调用dll的接口
       现在进入最后一个环节,在exe或者其他dll中调用引出dll的接口。这跟调用普通接口没有什么区别,一共分两步,第一步include被调用者所在的头文件,第二步,调用引出接口或使用引出类定义对象。
       第一步,include被调用者所在的头文件:
main.cpp:
  1. #include "myclass.h"
复制代码
       第二步,调用引出接口或使用引出类定义对象:
main.cpp:
  1. CPrint pr;

  2. pr.printOnScreen("it is a test!");
  3. [pre]INCLUDEPATH += $TRAIN_INCLUDE_PATH/ks04_01[/pre]
  4. func1();
  5. func2(2, 3.f);

复制代码

结语
----------------------------------------------------------------------------------------------------------------------
       本节我们详细介绍了把一个类或接口修改为封装到dll并引出的方法与步骤,这种方法在利用Qt进行跨平台软件研发中经常会用到,希望读者能熟练掌握。

上一节:KS03-03   God!全是英文,我的翻译呢
下一节:KS04-02   命名空间
回复

使用道具 举报

累计签到:256 天
连续签到:1 天
2020-3-4 17:04:33 显示全部楼层
受教了 感谢!
另外老师可以增加说明一下__stdcall和_cdecl等相关知识吗?
回复 支持 反对

使用道具 举报

累计签到:256 天
连续签到:1 天
2020-3-4 17:12:40 显示全部楼层
另外这种方法做出来的库文件,非Qt框架的程序可以直接使用吗?
回复 支持 反对

使用道具 举报

累计签到:256 天
连续签到:1 天
2020-3-5 10:46:20 显示全部楼层
老师 如果引出的头文件中包括了其他的头文件,那么应该怎么处理?
这种库使用了Qt的模块提供给非Qt框架的程序用,需要修改吗?
例如

#ifndef _MYCLASS_DLL_H
#define _MYCLASS_DLL_H

#include "ks04_01_export.h"
#include <QTcpSocket>
class  KS04_01_Export CPrint {
public:
    CPrint(){}
    ~CPrint(){}
public:
    void printOnScreen(const char*);
    QTcpSocket soc;
};

KS04_01_Export int func1();
KS04_01_Export int func2(int, float);
回复 支持 反对

使用道具 举报

累计签到:41 天
连续签到:1 天
2020-3-5 17:39:45 显示全部楼层
有这么几种情况:
1. 调用该DLL的模块不允许使用Qt, 那就不能用这个DLL了。因为DLL运行时,必须依赖Qt。
2. 调用该DLL的模块在编译时不允许依赖Qt,而在运行时没有限制。
   那么,就应该把所有的Qt类型的成员变量、接口参数,都改成指针类型,然后在类的前面进行前向声明(前置声明),以你举的例子进行说明:
   1)在类定义的前面写上:class QTcpSocket;
   2)成员变量改为:QTcpSocket* m_pSocket;

如果有其他疑问,欢迎关注微信公众号:软件特工队。里面有QQ群号,加群时,请注明:编程爱好者。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了