写在前面:本文仅对vs的项目开发做一个简单的整理,只求可以使用,不深入探讨内部机制。如果想要深入了解,可以查看下文给出的博客连接。
最后一个章节,“一个规范的工程目录”,记录了创建一个多项目工程的完整操作。
参考博客VS2017的C++开发心得(四)VS的文件夹路径以及项目生成步骤。
一个vs解决方案下,可以有多个项目。总会有一个同名项目。例如,下图中,解决方案SolutionTest下,目前只有一个项目SolutionTest。
一个vs解决方案下有以下若干文件夹/文件夹:
参考C++编译与链接(1)-编译与链接过程
vs的项目生成大致分为三步:预处理、编译、链接和生成
一 、预处理
C++的预处理是指在C++程序源代码被编译之前,由预处理器对C++程序源代码进行的处理。这个过程并不对程序的源代码进行解析。
这里的预处理器(preprocessor)是指真正的编译开始之前由编译器调用的一个独立程序。
预处理器主要负责以下几处
二、编译
当点击vs菜单的 生成-编译,或者ctrl+f7后,则会对当前单个文件进行编译操作。当生成解决方案时,vs会先对每个cpp进行编译。
编译时,首先对单个源码文本文件进行语法检查有没有语法错误-error 或者警告-warning。写代码时,编译器给出的标红也就是这种情况。
vs把每个cpp生成一个对应的.obj文件,默认放在每个项目(注意,是项目而非解决方案)目录下的,对应的debug或者release文件夹下。
这里借用上文VS2017的C++开发心得(四)VS的文件夹路径以及项目生成步骤中的一张图
三、链接和生成
可以理解为,以main函数所在的cpp文件为入口,进行搜索,将相关的*.obj文件链接成可执行文件*.exe,存储在解决方案文件夹下的相关文件夹中(如Debug)。多个.obj文件下有同名变量的问题,则会在这一步暴露出来。
仍然参考VS2017的C++开发心得(四)VS的文件夹路径以及项目生成步骤。
路径宏很大的作用是可以根据你的配置名去选择不同的文件夹下面的资源。而且重要的是,可以选择相对路径而非绝对路径,从而便于整个项目进行迁移。
常用到的宏,有$(SolutionDir)解决方案目录,和$(Configuration) Debug或Release
。
主要参考VS2017的C++开发心得(六)头文件的路径问题与属性管理器
当引用头文件时,有两种写法
#include
#include "iostream"
两种写法都是合法的,不同之处在于,两种写法优先从不同的路径搜索"iostream"这个文件。
项目右键-属性管理器-VC++目录-包含目录,这儿就是使用#include
<>`时的默认搜索路径。
对于#include ""
这种写法,其默认搜索路径则是在对应的同一个项目文件夹下。
主要参考[VS2017的C++开发心得(九)DLL动态链接——多项目开发(https://blog.csdn.net/luoyu510183/article/details/83999548)
多项目开发的主旨是尽可能的实现代码的模块化,减少对其它项目的依赖性,最好能够独立实现它设计的功能。
例如,要开发一个Windows窗体程序,实现医学影像数据的采集、处理和显示,那么,可以在解决方案下创建三个项目。一个项目用于,连接医疗影像设备,采集图像数据;一个项目是图像处理操作;还有一个项目用于窗体的UI。这样将几类功能鲜明地分割开,减少项目之间的耦合性,也更容易实现代码复用。
在原来的解决方案里,添加一个新C++项目Project2。
右键解决方案,添加》新建项目》空项目,在Project2的项目属性中把配置类型改成动态库
如上图所示,现在解决方案SolutionTest下,有两个项目。SolutionTest项目处于高亮状态,这是它作为启动项目的标识。为了了解启动项目,可以查看解决方案属性页:
单启动项目:
按f5,或者按ctrl+f10的时候调用的项目。
多个启动项目:
适用于多进程间(不是多线程间)的调试,即多个exe之间的通信调试。一般开发很少见,服务器程序比较常用,一个ex用于监听网络请求,一个exe发送请求。此处不做深究。
要注意的是,调试的时候只会生成启动项目,也就是项目SolutionTest,Project2是默认不生成的。生成-生成解决方案,这种方法固然会生成所有项目,但是vs进行生成的顺序,是按照项目添加的先后顺序的。为了保证SolutionTest调用的Project2项目是最新的,需要手动设置项目的生成顺序。
设定项目SolutionTest依赖于项目Project2,则Project2会先于SolutionTest生成。生成顺序错误的话,可能会导致无法找到xx.lib的错误,因为需要用到的lib并没有在这个项目之前生成出来。
项目之间的循环依赖 如a依赖b,b依赖c,c依赖a,是不被允许的,vs中也无法进行这种设置。
在Project2当中,新建了一个类DataProcessor,用于数据处理。
将Project2设置为配置类型 动态库(.dll),上文已经提到过。
写完以后,这个DataProcessor类只能在当前项目Project2中被其他cpp文件引用,还少了一步把函数接口导出到dll的导出列表里面。导出到dll的导出列表以后,别的项目才能根据函数名去dll中找到这个函数的位置。这一步的操作就要介绍下如何利用宏定义来完成DLL的函数导出和导入切换:
首先给Project2加上一个预处理器定义,也就是一个编译前的宏定义PROJECT2(宏定义全部大写)
然后修改DataProcessor类的代码如下
//DataProcessor.h
#pragma once
#ifdef PROJECT2
#define PROCESS_API _declspec(dllexport)
#else
#define PROCESS_API _declspec(dllimport)
#endif
class PROCESS_API DataProcessor
{
public:
double ProcessDouble(double d);
private:
};
//DataProcessor.cpp
#include "DataProcessor.h"
double DataProcessor::ProcessDouble(double d)
{
return d * 2;
}
现在解释下为什么要在Project2里加上一个PROJECT2的项目标识宏定义。首先,dll的导出和导入的关键字区分在于__declspec()的括号里面是dllexport(导出)还是dllimport(导入)。在本项目中我需要告诉VS这个函数是导出的,在其他项目中需要告诉VS这个函数是从别的dll里面导入的。但是不管哪个项目,使用的都是DataProcessor.h这个头文件,这时宏定义作为编译开关的作用就体现出来了。
在Project2的项目生成中,由于我们定义了PROJECT2的宏定义,所以CRYPT_API是被定义为__declspec(dllexport)。在SolutionTest项目中引用头文件DataProcessor.h时,SolutionTest中并没有定义PROJECT2的宏定义,所以CRYPT_API被定义为__declspec(dllimport)。这样就做到了同一份header不同的声明。
在此之后,需要在SolutionTest项目中配置Project2的头文件路径和lib路径。
首先,SolutionTest项目右键——属性——C/C++——常规——附加包含目录,添加头文件所在的路径,这里我用宏 $(SolutionDIr),指定到了解决方案目录
也就是这个位置:
然后要设置lib文件的路径。因为生成时选择64位 x64,所以生成的.lib文件会出现在解决方案目录下的x64\Debug,或者x64\Release下。
SolutionTest项目右键——属性——链接器——常规——附加库目录,设置lib文件所在的路径,同样用到了宏$(SolutionDir)x64$(Configuration)
此后,还要告诉vs,SolutionTest这个项目用到了哪些库 .lib,SolutionTest项目右键——属性——链接器——输入——附加依赖项,这里添加Project2.lib即可
在main.cpp中测试一下,运行成功
//main.cpp
#include "Project2/DataProcessor.h"
int main()
{
double d1, d2;
DataProcessor p;
d1 = 1;
d2 = p.ProcessDouble(d1); //d2=2
return 0;
}
注意:在vs的解决方案资源管理器界面,是解决方案的逻辑层级,不代表具体存储在硬盘中的物理位置
首先 创建一个SolutionTest2解决方案,如下图所示:
创建完成后,解决方案文件夹如下图:
为了便于代码维护,在服务器上存储,所以在解决方案目录下新建几个文件夹Include,Int,Lib,Output,Source,分别用来存储头文件、.obj文件,.lib文件,exe和dll文件,以及.cpp文件。
在文件资源管理器中,将原来的SolutionTest2文件夹拖到Source目录下。为了在vs中更新这个项目的信息,所以,先在解决方案资源管理器中1.项目SolutionTest右键-移除;2 解决方案SolutionTest右键-添加-现有项目,选择刚才迁移到硬盘中的文件SolutionTest2.vcxproj。如下图:
最终解决方案目录如下图所示:
为此项目添加main.cpp。默认路径已经改成了解决方案文件夹\Source\SolutionTest,直接添加即可,如下图所示:
在vs里新建一个项目DataProcessor。解决方案"SolutionTest"右键-添加-新建项目,路径选择解决方案目录下的source文件夹,如下图所示:
如上文提到的,DataProcessor项目右键-属性-配置属性-常规-配置类型-选择动态库(.dll)
添加一个类NumProcessor。项目DataProcessor右键-添加-新建项-头文件,目录选择include文件夹下的DataProcessor项目文件夹(如果没有,就自己创建),如下图所示:
类似的,添加NumProcess.cpp文件,在Source文件夹下的DataProceeor项目文件夹下
SolutionTest是main函数的入口,将会依赖于其他项目。
解决方案"SolutionTest"右键-属性-启动项目-选择单启动项目SolutionTest2
解决方案"SolutionTest"右键-属性-项目依赖项-选择项目SolutionTest2-勾选依赖于DataProcessor
规范起见,分别对于Debug模式下的每一个项目,设置:项目右键-属性-常规-目标文件名,设置宏 ( P r o j e c t N a m e ) D 对 于 R e l e a s e 模 式 , 默 认 的 已 经 是 (ProjectName)D 对于Release模式,默认的已经是 (ProjectName)D对于Release模式,默认的已经是(ProjectName),所以不作修改
Include文件夹。它的下级目录为各个项目的名字。每个项目文件夹存放对应项目的头文件。
要为每个项目指定这个头文件的目录。举个例子:项目SolutionTest2右键-属性-C/C+±附加包含目录,在此处添加路径 用宏来表示:$(SolutionDir)Include\。对于现有的两个项目,要分别进行这个操作。在Debug和Release模式下,也要分别设置一次(以64位 x64为例)。以下各个目录的设置同理,也需要这样分别设置
Int文件夹,intermedia,中间过程,存储
.obj格式的目标文件等。文件夹的组织形式位,int文件夹下有debug,release两个文件夹,每一个目录又下辖以各个项目名命名的子文件夹。类似的,对于每个项目,分别对于Debug/Release,设置 项目右键-属性-常规-中间目录,添加宏 $(SolutionDir)Int\$(Configuration)\$(ProjectName)\
lib文件夹,存储.lib、.exp文件。
如下图所示,分为Commonlib通用库,Debug模式下的lib,Release模式下的lib三个子文件夹:
类似的,对于每个项目,分别对于Debug/Release,设置 项目右键-属性-链接器-高级-导入库,设置宏 $(SolutionDir)lib\$(Configuration)\$(TargetName).lib
Source文件夹,各个项目所在的位置。新建项目时也要选择位置在这个文件夹。
Output文件夹,存储最终生成的.exe,.dll,.pdb,.itk文件。
由于此项目只考虑x64,所以Output文件夹下仅有x64一个文件夹,x64下有Debug,Release两个文件夹
类似的,对于每个项目,分别对于Debug/Release,设置 项目右键-属性-常规-输出目录,设置宏 $(SolutionDir)Output\$(Platform)\$(Configuration)\
对于除了启动项目之外的每个项目,对于本项目来说也就是DataProcessor项目。分别对于Debug/Release,设置 项目右键-属性-C/C+±预处理器,添加预处理器定义 为[大写项目名称]_EXPORT,如下图
然后在对应项目的头文件中,添加如下代码,从而使得其对于本项目来说是导出(dllexport),对于其他项目来说是导入(dllimport)
#ifdef DATAPROCESSOR_EXPORT
#define DATAPROCESSOR_API _declspec(dllexport)
#else
#define DATAPROCESSOR_API _declspec(dllimport)
#endif
需要为启动项目设置库(lib)目录。
对于启动项目,也就是本工程中的SolutionTest2,分别对于Debug,Release,设置 项目右键-属性-链接器-常规-附加库目录,添加宏 $(SolutionDir)lib\$(Configuration)\,和$(SolutionDir)lib\Commonlib\,如下图:
还要告诉vs,SolutionTest2这个项目用到了哪些库 .lib。分别对于Debug,Release模式,设置:SolutionTest2项目右键——属性——链接器——输入——附加依赖项,添加对应的DataProcessorD.lib(Debug模式下)或者DataProcessor.lib(Release模式下),如下图:
#pragma once
//DataProcessor.h
#pragma once
#ifdef DATAPROCESSOR_EXPORT
#define DATAPROCESSOR_API _declspec(dllexport)
#else
#define DATAPROCESSOR_API _declspec(dllimport)
#endif
namespace DATAPROCESSOR //每个项目内,类、变量、函数的声明,都用命名空间包含起来,避免重名
{
class DATAPROCESSOR_API NumProcessor
{
public:
double ProcessDouble(double d);
private:
};
}
//DataProcessor.cpp
#include "DataProcessor/NumProcessor.h"
using namespace DATAPROCESSOR; //注意 使用本项目的命名空间
double NumProcessor::ProcessDouble(double d)
{
return d * 2;
}
//main.cpp
#include "DataProcessor/NumProcessor.h"
using namespace DATAPROCESSOR;
int main()
{
double d1, d2;
NumProcessor np;
d1 = 1;
d2 = np.ProcessDouble(d1); //d2=2
return 0;
}
经测试运行通过,代码见百度云:
链接:https://pan.baidu.com/s/1IWPIAB6qjMMRrX0NMsSVNw
提取码:nzf2