这一篇主要介绍在一个解决方案下如何进行多项目开发。
其实开发代码都放在一个项目下也可以实现它的功能,为什么要使用多项目开发?从编程架构上来讲,树状结构是最优秀的架构。树状结构越发散,它的维护任务就越轻松。这句话的意思是:我们在进行C++工程开发的时候对每一个cpp的功能进行清楚的分割,减少不同功能的cpp文件之间的耦合性,即便当系统非常庞大的时候,维护起来也很方便。说得简单点就是分类工作一开始就做好,以后找东西就轻松了。
多项目开发的主旨是尽可能的实现代码的模块化,减少对其它项目的依赖性,最好能够独立实现它设计的功能。比如我创建一个解决方案,要开发一个Windows窗体程序实现发送指定的Http请求数据到指定的服务器地址。首先我会对整个解决方案进行功能划分,一部分是UI窗体部分,另一部分是网络通信部分。所以我在解决方案下会创建两个C++项目:一个是Windows窗体的UI项目,一个基于Http请求的网络通信项目。在网络项目中我不会使用任何关于C++窗体的东西,这就是减少项目间的耦合性,让整个网络模块的功能独立起来不依赖于另一个项目。好处是:在开发本程序时,代码分类清楚;程序开发结束后,网络通信项目可以独立出来给你以后的程序使用,不用再重复开发,或者拷贝原来的代码进行再次编译。
原理讲解这么多,下面就来看下怎么在VS2017下面进行多项目开发。
首先在原来的解决方案里,添加一个新C++项目Project2:
右键解决方案,添加》新建项目》空项目,在Project2的项目属性中把配置类型改成动态库:
这时候,看到解决方案资源管理器里面两个项目一个是高亮的,一个是普通的,如下:
Project1处于高亮状态,这是它作为解决方案'Project1'的启动项目的标识。为了了解启动项目,我们先看下解决方案的属性页:
这里要注意一点:调试的时候只会生成启动项目,如本文中的例子,Project1是启动项目,虽然Project1会使用Project2的生成DLL,但是我们进行本地Windows调试器调试的时候Project2是默认不生成的。虽然解决方案的“生成解决方案”会生成其他非启动项目Project2,但是这是不可行的。因为生成解决方案的生成顺序是和项目添加顺序相关,即不能完全保证Project1在Project2生成之后开始生成。为什么要保证这个顺序?因为Project1需要在链接期间使用Project2的.lib文件。如何让VS能够保证这种生成顺序关系呢?就是添加项目依赖项,也在解决方案属性页里面:
以上就是对添加dll项目的配置工作。接下来回到代码上:我定义Project2是我的一个加密模块,负责传入数据以我定义的算法进行加密。所以我添加上一个cryptint.cpp实现对于int数据的加密算法:
- #include "Project2/cryptint.h"
- int CryptInt(int a)
- {
- return (a - 1) * 2 % 100;
- }
然后我们还需要它的一个头文件cryptint.h,这个头文件的作用是告诉Project1:Project2中定义了一个返回值为int,传入参数为一个int的加密函数CryptInt,如下:
- #pragma once
- //如果是生成DLL的项目,一定要像下面这样写好接口定义。
- //因为使用你DLL的人的范围很广,他们都不知道源代码,所以要清楚的写明传入参数的意义
- //和返回值的类型
- //函数描述:对一个int变量进行加密后将加密数据赋值给返回值。
- //传参a:需要加密的int变量
- //返回值:加密后的int值
- int CryptInt(int a);
好这样写完以后,这个CryptIn函数只能在当前项目Project2中被其他cpp文件引用,还少了一步把函数接口导出到DLL的导出列表里面。导出到dll的导出列表以后,别的项目才能根据函数名去dll中找到这个函数的位置。这一步的操作就要介绍下如何利用宏定义来完成DLL的函数导出和导入切换:
首先我先给Project2加上一个预处理器定义,也就是一个编译前的宏定义PROJECT2(宏定义全部大写用以区分):
然后修改cryptint.h的代码如下:
- #pragma once
- //下面是本项目的导出接口CRYPT_API的宏定义,所有需要导出的函数标注上CRYPT_API就可以导出到DLL的导出列表里面
- //这样别的项目和exe才能在dll的导出列表里搜索到该函数进行加载
- #ifdef PROJECT2
- #define CRYPT_API __declspec(dllexport)
- #else
- #define CRYPT_API __declspec(dllimport)
- #endif // PROJECT2
- //如果是生成DLL的项目,一定要像下面这样写好接口定义。
- //因为使用你DLL的人的范围很广,他们都不知道源代码,所以要清楚的写明传入参数的意义
- //和返回值的类型
- //函数描述:对一个int变量进行加密后将加密数据赋值给返回值。
- //传参a:需要加密的int变量
- //返回值:加密后的int值
- CRYPT_API int CryptInt(int a);
现在解释下为什么要在Project2里加上一个PROJECT2的项目标识宏定义。首先,dll的导出和导入的关键字区分在于__declspec()的括号里面是dllexport(导出)还是dllimport(导入)。在本项目中我需要告诉VS这个函数是导出的,在其他项目中需要告诉VS这个函数是从别的dll里面导入的。但是不管哪个项目,使用的都是cryptint.h这个头文件,这时宏定义作为编译开关的作用就体现出来了。
在Project2的项目生成中,由于我们定义了PROJECT2的宏定义,所以CRYPT_API是被定义为__declspec(dllexport)。在Project1项目中引用头文件cryptint.h时,Project1中并没有定义PROJECT2的宏定义,所以CRYPT_API被定义为__declspec(dllimport)。这样就做到了同一份header不同的声明。
写好以后直接生成解决方案,会在生成目录下出现一个Project2.dll文件。我们通过下面的方法来检验下cryptint是否导出成功:
可以把Project2.dll用viewdll程序打开,可以查看它的导出函数列表。这里我没有下载,就直接文本打开。这个dll文件的内容是hex形式,不是文本字符,所以有很多乱码不可读。但没关系,因为导出函数的名字一定是字符串形式,所以我们只要在文本中搜索我们的导出函数名“CryptInt”:
搜到了,说明导出成功了。
这时候,就需要在Project1中配置Project2的头文件路径和lib路径,具体见这篇文章VS2017的C++开发心得(八)DLL动态链接——Opencv的使用。
配置完后,在main.cpp中加入Project2的函数调用代码:
- #include "Project1/a.h"
- #include "opencv2/core/types_c.h"
- #include "opencv2/core/core_c.h"
- #include "opencv2/highgui/highgui_c.h"
- #include "Project2/cryptint.h"
- int algorithm(int a, int b)
- {
- a++;
- b++;
- return a - b;
- }
- int main()
- {
- /*IplImage* img = cvCreateImage(CvSize(100, 100), 8, 3);
- cvZero(img);
- cvShowImage("test", img);
- cvWaitKey(0);*/
- int b = CryptInt(3);//b为4 调用成功
- return algorithm(b, 3);
- }
好,以上就是关于如何进行多项目协同开发的流程。最后说明下网上关于dll的导出一般都伴随着extern "c" ,__stdcall,__cdecl等,这些都是和dll的兼容性相关的,如果你们团队都是基于VS2017开发,这些都不用写。感兴趣自己了解下这些也不错,可以看看加上上面的声明后dll中的函数名是否发生变化。
联系客服