子系统的配置方式为 属性页->链接器->系统->子系统
点击子系统后,下方会出现微软的官方解释:当双击exe文件后,子系统会告知操作系统运行的入口函数,默认是int main(),可以在 属性页->链接器->高级->入口点 进行设置。
补充:链接器->常规 和 链接器->输入 也很重要,当我们使用外部一些封装好的库函数的接口(ddl或者lib文件),就需要进行配置。链接器->调试可以设置调试的pdb文件,因为编译是单向的,于是VS通过将cpp文件和obj文件建立字典,即pdb文件,由此实现调试。
可以右键项目->生成解决方案,也可以点击“本地Windows调试器”,直接生成并运行调试我们的代码,后者更加常用。
VS的项目生成大概需要四步:分析、编译、链接和生成。
分析,主要检查语法错误,在"错误列表"中查看
编译,将解决方案资源管理器中的cpp文件都编译为obj文件,这里执行了gcc
中的预处理、编译、汇编三个步骤
链接,以main.cpp为根节点(可右击项目->属性->链接器->高级,设置入口点),将所有与之相关的obj文件链接起来,一方面将没有用到的编译内容进行了删减,另一方面对外部变量和函数进行了匹配(例如定义在其他文件中或者ddl文件中的变量,诸如extern int a
),"连接错误"往往就是这里的问题;
注意:所有在cpp文件下的变量定义和函数定义都是全局的,整个项目都共享(只是项目,不是解决方案),所以同一个项目的全局变量的命名不能冲突。
生成,在Debug文件下生成了可执行文件,但是这是一个调试程序,里面有大量对VS的依赖,在没有安装对应版本的VS的电脑上无法运行,需要进行独立打包才能在其他电脑上运行。
VS中目录的设置都是宏的形式,和C++ define一样,只是所有的路径都是相对路径,更加灵活。
VS配置属性中的常规->输出目录,是生成exe文件的Debug文件夹;配置属性中的调试->工作目录,是调试时候的目录,两者一般不同。在程序中如果用相对路径寻找一个文件,在调试的时候,是以后者为出发点查找,但是如果双击exe文件,则是以前者为出发点查找,这就会出现调试时没有问题,但是双击exe会显示文件找不到的问题——此时,就需要修改两个路径宏,使之相同。
点击属性页的右上角的"配置管理器",可以看到不同项目的运行配置方式:
在配置属性的常规目录下,我们可以看到
VS将这些配置字段和值保存成xml文件的格式,这个思路在开发中经常会用到,保存的文件后缀名为.vcxproj
如下所示
一般来讲,写代码和调试代码使用Debug模式,发布代码使用release模式,前者编译时可以放弃优化,保持代码的原始性,后者则会对代码做较大的优化,不利于调试查看。
两者在变量的初始化上也有较大不同,例如int a
,release模式下初始值为0,debug模式下初始值为0xcccccccc
或者0xcdcdcdcd
。
为了保证代码的可移植性,一般使用win32即可,如果代码中涉及到较多的int64和double类型,使用win64会在速度上有优势。
VS默认的项目文件夹目录并不适合大型项目的代码管理,特别是需要用到云开发的时候,典型的就是Github 和 Azure。首先要明确,服务器只保存用户数据,不保存VS的生成文件,直白的说,就是如何上传最小的文件让用户下载下来可以编译运行,因此只需要上传:
项目名.vcxproj
,项目名.vcxproj.filters
,项目名.vcxproj.user
,后者就是.sln文件所以重新调整了之前的解决方案路径:
分为四个大文件:
其实就是将原来的Debug,Release文件夹都不要,只需要将配置文件.vcxproj、头文件.h和源文件.cpp整理昊即可,之后创建新的cpp文件和头文件也要放在对应的目录下面,而不要使用默认路径。
VS通过intelllisense功能自动补全(编辑->Intellisense)。
我们知道C++有两种引用头文件的方法:
#include "iostream"
#include
但是VS的intellisense在前者不会自动补全,后者却能够自动补全,管理后者的搜索路径的设置就在 项目属性->VC++目录->包含目录
中,如下图所示,可见搜索路径包括自定义的搜索路径和从项目属性管理器中继承的值,例如下面的项目就自动继承了$(VC_IncludePath)
和$(WindowsSDK_IncludePath)
这两个值:
可以通过打开“属性管理器”查看第二种方法链接的一部分lib文件,如下图所示:
该项目显示有三个属性:Core Windows Libraries,Multi-byte Character Support,Microsoft.Cpp.Win32.user,分别代表链接的C++ lib文件、多字节 lib文件、win32 lib文件,以**.props**的格式存储,其中,以Core Windows Libraries的props文件如下所示:
而debug所作的就是将这三个属性表(.props文件)拼接成一份属性页。
但是,Toolset.props并没有通过属性管理器中,而是在.vcxproj中加载的,.vcxproj加载这个属性表的代码片段 如下图所示:
Toolset.props中导入 $(VC_IncludePath)
和$(WindowsSDK_IncludePath)
的代码如下图所示:
总结:#include <>
通过属性管理器中的props和.vcxproj中加载的Toolset.props管理链接的lib文件地址,底层都是通过加载.props文件实现的。
#include " "
的搜索路径在属性页->C/C++ ->常规->附加包含目录
,而#include < >
的搜索路径保存在属性页->VC++ -> 包含目录
,因此如果不想头文件和Windows标准文件混在一起,可以使用#include ""
,并在属性页中配置搜索路径。
通过任务管理器的资源监视器,我们在CPU中找到运行程序的进程,选定VSDebugConsole以后,在下面的“关联的模块中”会出现这个程序所依赖的dll文件,如下图所示:
外部依赖模块的链接分为静态链接和动态链接。
动态链接,诸如dll文件,当程序启动的时候在PATH路径下查找所需要的dll文件并加载到内存中。
静态链接,诸如lib文件,在链接期就被编译进当前的项目文件中了,但是静态链接编译出来的文件比较大,而且不够灵活,所以用的比较少。
所以如果出现dll文件无法找到的问题,往往需要检查PATH路径是否正确,或者将dll文件和调用dll的exe文件放在一起。
首先,下载opencv,需要说明的是,流行的第三方库往往是Cmake项目,基于源码使用Cmake生成的VS项目,然后再编译出所需要的dll、lib和pdb文件。下载解压之后的opencv文件夹包括build和source两个文件夹,前者是我们会用到的,后者是debug源码时会用到的。
打开build文件夹,其中的include放置头文件,而x64放置生成的对应于64位系统的lib、pdb和dll文件,分别对应链接、调试和运行,如果是x86则是32位系统的。
然后设置头文件include的搜索目录,将opencv的include文件夹添加到C/C++的附加包含目录中:
之后,设置链接的lib文件路径,添加opencv对应VS版本的lib文件的文件夹路径到属性页->链接器->常规->附件库目录
下,再将lib文件添加到属性页->链接器->输入->附加依赖项
中,这两步操作如下图所示:
注意:这里的附加库目录要根据VS版本选择合适的include文件夹,附加依赖项则要分清release和debug版本——其实如果设置好了附加库目录,此处的附加依赖项似乎并不是必须的,因为代码中还能够直接加载,之所以这么做,是为了让项目的依赖项更加明了,而不用去cpp文件里找加载的依赖项。
之后就可以愉快的写代码了。
如果调试代码的时候,如果出现**“无法解析的外部符号”错误**,可以按照上面的方法检查一遍链接器设置,如果报“找不到opencv_world4000d.dll文件”错误(如果你前面配置合适这个问题并不会出现,但是有时候必须自己下载一些丢失的dll文件),此时本地确实没有该文件,可以按照debug和release两种途径有两种方法:
debug
将附加依赖库文件夹lib的同级bin文件夹设置为环境变量
release
在属性页->生成事件->生成后事件->命令行
添加诸如xcopy "F:\SDK\opencv_400alpha\opencv\build\x64\vc14\bin\opencv_world400d.dll" "$(SolutionDir)$(Platform)\$(Configuration)" /y
,即将找不到的dll文件夹复制一份到exe的目录下面,当然debug模式也可以使用该方法,这样的好处是,程序的依赖项目都集中在生成目录下,方便最后打包。
多项目开发的主旨是尽可能的实现代码的模块化,减少对其它项目的依赖性,最好能够独立实现它设计的功能。
右键解决方案-》添加-》新建项目-》空项目,将新建项目的项目属性页修改为动态库(.dll)。
此时,我们会发现解决方案资源管理器中的启动项目Project1为高亮显示,而其他项目是普通的。右击解决方案,可以设置启动项目是单项目还是多项目:
这里要注意一点:调试的时候只会生成启动项目,如本文中的例子,Project1是启动项目,虽然Project1会使用Project2的生成DLL,但是我们进行本地Windows调试器调试的时候Project2是默认不生成的。虽然解决方案的“生成解决方案”会生成其他非启动项目Project2,但是这是不可行的。因为生成解决方案的生成顺序是和项目添加顺序相关,即不能完全保证Project1在Project2生成之后开始生成。为什么要保证这个顺序?因为Project1需要在链接期间使用Project2的.lib文件。(总结:调试只能生成启动项目,而生成解决方案并不能保证生成项目的顺序)
如何让VS能够保证这种生成顺序关系呢?就是添加项目依赖项,也在解决方案属性页里面:
应该注意两点:Project依赖于Project2,表明Project2会先于Project1生成,这样Project1使用的Project2的lib文件就是最新的,否则会出现找不到lib文件的错误;千万不能出现项目相互依赖的情况。
总之,多项目开发一定要注意启动项目的设置和项目依赖关系的设置。
在Project2中添加文件Cryptint.cpp
,并实现一个对整数的加密函数CryptInt
:
#include "Project2/cryptint.h" //去掉试试
int CryptInt(int a)
{
return (a-1)*2%100;
}
因此还需要添加一个头文件CryptInt.h
,声明函数:
#pragma once
//函数描述:对一个int变量进行加密后,将加密后的数据返回
//传参a: 需要加密的int变量
//返回值: 加密后的int数值
int CryptInt(int a);
但是这样只能在当前项目Project2中被引用,要想跨项目调用,必须将函数接口导出到dll文件的导出列表中,因此首先在Project2的 属性页》C/C++》预处理器》预处理器定义
中添加本项目的项目标识符PROJECT2
,然后修改CryptInt.h
:
#pragma once
// 导出接口的宏定义,根据预处理器定义不同,做到了同一份header不同的声明
#ifdef PROJECT2
#define CRYPT_API __declspec(dllexport) // 如果是project2引用,则导出dll
#else
#define CRYPT_API __declspec(dllimport) // 如果是其他文件引用,则导入dll
#endif // PROJECT2
CRYPT_API int CryptInt(int a);
此时生成解决方案,在生成目录下会出现一个Project2.dll文件。接下来就需要在Project1中设置头文件路径(引用目录)和lib路径(附加库目录和附加依赖项)了,此处的步骤和配置opencv相似,然后在Project1中可以直接#include "Project2/cryptint.h"
,然后使用CryptInt函数了。
有时候我们会遇到头文件相互包含的问题,例如a.cpp中导入了b1.h和b2.h,但是b1.h和b2.h同时导入了c.h,此时一个cpp文件包含了多个相同的头文件,就会报编译错误。为了避免头文件重复包含,需要在头文件开头加上#pragma once
。
但是,上述方法仅能检测一个cpp文件的重复定义,对于多个文件并不管用,假设同一个项目下的a.cpp和b.cpp分别引用了b1.h,而且b1.h中同时定义了int变量m
,此时就会报错:重复定义了变量m
,这是初学者经常容易犯的错误,即在头文件中定义变量和函数,此时的错误属于链接错误。
对于头文件造成的同一个项目下不同cpp文件重复定义变量和函数的解决方法——头文件中只能写入声明,而不是定义,需要将头文件写成以下形式:
#pragma once
extern int testint;
extern int testfunc(); //对于函数,不强制加extern
VS调试分为主动调试和被动调试——主动调试:即编译并运行,就是“本地Windows调试器”;被动调试:这个需求比较灵活,一般是程序已经发布了release版本,直接双击exe运行而不依赖于VS了,而exe运行过程中可能会报错,可能状态异常,这时候我们需要通过VS去链接到这个exe进程进行调试。
VS的调试页面如下所示:
其提供了切换线程、堆栈、输入地址查看连续内存对应的数据、诊断工具查看CPU占用率和每行代码的内存创建情况、调用堆栈查看函数崩溃时所在的行号、监视器查看变量的值等功能,这些都可以在调试菜单》窗口中打开。
内存溢出:也就是内存访问越界,一般出现于数组和指针的使用中。这是一个比较致命的bug。一般情况下,程序都会立即崩溃,提示“0x???内存读取访问权限冲突”,也可能不会崩溃,但是你的程序总是会有时候运行异常。
这时候我们可以使用VS的条件断点找到该变量被修改的时刻, 有两种方法:
或者 新建数据断点
第二种方法, 还可以设置条件表达式断点, 但是会严重拖慢程序的运行速度, 因此不建议使用.
内存泄漏:相较于内存溢出,它的危害要小一些。泄漏就是申请的内存没有合理回收,导致出现不可使用的内存片段,大量的泄漏最终还是会导致程序无法申请到新的内存而崩溃。(分区的概念,可以参考csapp)
例如下面的程序,new了新的数组后并没有及时delete,在test结束运行后,堆区里的数据仍然没有被释放,这块区域也就处于完全不可用的状态,内存就会越用越少:
#include
double test(){
double *t = new double[2];
return t[1];
}
int main(){
double k = test();
return (int)k;
}
我们可以通过内存占用率》堆分析 快速定位堆上没有及时释放的内存:
这里原作者首先介绍了Windows蓝屏时的处理机制, 并以此为例子, 说明了如何基于Windows标准库上报和分析崩溃信息(dump文件)。
原作者并没有将该系列的文章整理成专栏,所以翻阅有些麻烦,为了方便查找,整理如下:
(一) VS项目创建
(二) VS项目介绍
(三) VS常用的项目属性介绍
(四) VS项目文件夹路径和生成步骤
(五) 项目开发的文件夹管理
(六) 头文件的路径问题和属性管理器
(七) DLL动态链接
(八) DLL动态链接实战 opencv
(九) DLL动态链接实战 多项目开发
(十) 头文件与重复定义符号
(十一) VS调试内存泄漏
(十二) VS调试内存溢出
.net/luoyu510183/article/details/83962476)
(九) DLL动态链接实战 多项目开发
(十) 头文件与重复定义符号
(十一) VS调试内存泄漏
(十二) VS调试内存溢出
(十三) 远程调试,分析崩溃信息