Visual Studio 简单使用记录
之前的 C++编程都是在 Linux 或者 VScode+MinGW 进行的,但有时难免需要使用宇宙第一 IDE,简单记录一些 VS 的基本使用吧,尤其了解一下分布在各个菜单栏各个按钮下的常用配置。(直接命令行参数多省事,省的到处找配置目录)
本文全部在 VS2019 完成,并且不考虑 MSVC 的纯命令行使用。
1. 解决方案与项目
项目是 VS 中的基本概念,例如某个库的开发就是一个项目,解决方案是项目的上层概念,一个解决方案可以包含多个项目。简单情况下一个项目会对应一个同名的解决方案。
解决方案在文件系统中直接对应一个xxx.sln
解决方案文件,它记录了解决方案层面的配置信息。(可以直接点击.sln
文件在
VS 中启动解决方案)
项目在文件系统中会对应.vcxproj
以及.vcxproj.*
文件,它们记录了项目层面的配置信息。如果点击.vcxproj
文件,似乎还是启动对应的整个解决方案。例如.vcxproj.filters
记录了一个虚拟的目录结构,.vcxproj.user
记录了用户的
IDE 设置(包括 IDE
布局,工具栏等设置,通常不影响项目编译,可以删除)
在创建新项目界面,选择控制台应用(C++,Windows,控制台)进行创建。(这不是一个空项目,main 函数会输出 helloworld) 提示需要设置几个信息:
- 项目名称
- 项目位置
- 解决方案名称(默认与项目同名)
- 勾选框:将解决方案和项目放在同一目录中,默认不勾选。
如果勾选这个选项,会强制让解决方案名称与项目名称保持一致,这适合于简单的情形:一个解决方案对应一个同名的项目。
只有在不勾选时,我们才可以对项目和解决方案设置不同的名字,此时通常我们会在解决方案下再添加几个项目,适合复杂情形:一个解决方案包含多个项目。
1.1 示例一
例如我们创建名为 Demo1
的项目,勾选将解决方案和项目放在同一目录中
,对应会创建同名的
Demo1 解决方案,项目位置放置在F:/
。 我们会得到什么?
查看文件系统可知,VS 会在F:/
创建 Demo1
目录,下面是在F:/Demo1/
默认生成的内容
1 | |- .vs |
这只是建议的一对一情形下的设置,但是我们仍然可以在这个基础上再向解决方案中添加新项目,例如添加
Demo2
项目,此时默认推荐的项目位置是F:/
,最终在F:/
得到如下的内容
1 | |- Demo1 |
此时 Demo1 项目和 Demo2 项目的文件夹仍然是并列的,而属于 Demo1
解决方案的部分(包括Demo1.sln
文件和.vs
文件夹)则位在
Demo1 项目的文件夹下。
1.2 示例二
如果我们创建名为 Demo1 的项目,对应不同名的 Demo
解决方案,项目位置放置在E:/
。我们会得到什么?
查看文件系统可知,VS 会在F:/
创建 Demo
目录,这个目录对应的是 Demo 解决方案,
下面是在F:/Demo/
默认生成的内容
1 | |- .vs |
Demo1 这个子目录对应的就是 Demo1 这个项目,里面的内容包括
1 | Demo1.cpp |
对比可以发现,.vs
这个隐藏文件夹也是和解决方案对应的。
我们可以在 Demo 解决方案中再添加新的项目(对着解决方案右键,添加,新建项目),例如再添加一个 Demo2 项目,此时提示设置两个信息:
- 项目名称
- 项目位置(默认是
E:/Demo
,即 Demo 解决方案的目录下,也可以修改到其它位置)
此时文件系统中就变成
1 | |- .vs |
即 Demo
这个解决项目对应的文件夹下,放置着属于解决方案的.vs
文件夹和.sln
文件,还包括子文件夹
Demo1 和 Demo2 分别对应两个项目。
接下来进入正篇,我们采用的是一个 Demo 解决方案,包括三组共五个项目,依次为
- Demo1 创建可执行文件
- Demo2_Static 生成静态库
- Demo2_Exe 创建可执行文件调用静态库
- Demo3_Shared 生成动态库
- Demo3_Exe 创建可执行文件调用动态库
2. 创建可执行文件
自动生成控制台项目 Demo1 的内容很简单,只有一个直接 helloworld 的 Demo1.cpp。
我们做点改造:(必须通过 VS 的对应结构的的右键菜单添加,而不是直接在文件系统新建,因为在 sln 没有记录,VS 就不会识别新文件,无论这个新文件创建在哪)
- 头文件 hello.h
- 源文件 hello.cpp Demo1.cpp
在 hello.h 中声明如下函数
1 |
|
在 hello.cpp 实现
1 |
|
在 Demo1.cpp 引用 hello.h 并调用该函数
1 |
|
在主界面的生成
选项,有两大部分,第一部分是若干针对整个解决方案的生成/清理/分析,第二部分是针对当前项目的生成/清理/分析。
不需要任何配置信息的修改,直接执行当前项目的生成,我们就可以得到一个麻雀虽小五脏俱全的 helloworld 可执行程序。
注意:VS 对于每一个项目都会根据构建模式(例如 Debug)和平台(例如 x86)的不同,选择执行对应的编译/调试等行为,默认情形下总是使用 Debug-x86 的配置。在下文修改信息时也要注意,有时候配置信息的修改可能都只限于当前配置,改成 Debug-x64 或者 Release-x86,这些修改又都没了。
我们关注一下在文件系统中主要发生了什么?这里顺便对不同的配置进行对比
- 如果在 Debug-x86 配置下进行生成,最终会多出这些内容
- 在解决方案目录
E:/Demo
的Debug
子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb - 在项目目录
E:/Demo/Demo1
的Debug
子目录下,生成若干的杂项文件
- 在解决方案目录
- 如果在 Debug-x64 配置下进行生成,最终会多出这些内容
- 在解决方案目录
E:/Demo
的x64/Debug
子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb - 在项目目录
E:/Demo/Demo1
的x64/Debug
子目录下,生成若干的杂项文件
- 在解决方案目录
- 如果在 Release-x86 配置下进行生成,最终会多出这些内容
- 在解决方案目录
E:/Demo
的Release
子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb - 在项目目录
E:/Demo/Demo1
的Release
子目录下,生成若干的杂项文件
- 在解决方案目录
- 如果在 Release-x64 配置下进行生成,最终会多出这些内容
- 在解决方案目录
E:/Demo
的x64/Release
子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb - 在项目目录
E:/Demo/Demo1
的x64/Release
子目录下,生成若干的杂项文件
- 在解决方案目录
注:
- 几种模式下生成的可执行文件都是不一样的,尤其是文件大小,Debug 模式下的文件更大(包含更多调试信息),x64 和 x86 得到的结果也不一样大(一个是 32 位程序,一个是 64 位程序)
- 一个小技巧,判断 exe 文件是 32 位还是 64 位:右键查看属性,兼容性,简化的颜色模式,如果 32 位这个勾选框可以选择,如果是 64 位这个勾选框被禁用
- 从目录组织也可以看出,VS 默认是针对 x86 设计的文件夹组织,如果选择 x64,还会额外增加一级 x64 子目录。虽然 Windows 系统普遍是 64 位了,但是 VS2019 从根上还是 32 位的
3. 创建静态库并使用
3.1 创建静态库
在创建新项目界面,选择静态库(C++,Windows,库),创建 Demo2_Static 项目。
自动生成的内容包括如下几个文件:
- 头文件 framework.h
- 头文件 pch.h (include framework.h)
- 源文件 pch.cpp (include pch.h)
- 源文件 Demo2_Static.cpp(include pch.h framework.h)
这里主要是 VS 自动提供的机制:把不常变动的头文件都扔给 pch.h,这个部分只编译一次,加速编译。(不去管这些,完全删掉其实也可以,这里选择保留)
我们首先添加头文件 Demo2_Static.h,内容为
1 |
|
将 Demo2_Static.cpp 的文件内容替换为
1 |
|
这里我们保持 Debug-x86 的配置,直接生成当前项目。我们关注一下在文件系统中主要发生了什么?
- 在解决方案目录的
Debug
子目录中,产生了- Demo2_Static.lib(重要)
- Demo2_Static.idb(调试相关)
- Demo2_Static.pdb(调试相关)
- 在项目目录的
Debug
子目录下,生成若干的杂项文件
3.2 使用静态库
创建一个可执行文件项目 Demo2_Exe 的过程略,我们关注如何使用解决方案内部的静态库。(对于解决方案之外的静态库导入,就像 CMake 那样可能很复杂,这里不考虑)
将默认的 Demo2_Exe.cpp 的文件内容替换为
1 |
|
接下来是需要的配置过程,参考官方教程。
- 在项目层面,通过右键菜单,对 Demo2_Exe 项目添加引用,添加 Demo2_Static 项目;
- 解决找不到库的头文件的问题(相当于 gcc 的
-I
选项),把 Debug-x86 改成所有配置-所有平台(重要!!!),在下面的配置中加入头文件所在目录
1 | 项目 -> 属性 -> C/C++ -> 常规 -> 附加包含目录(第一个选项) |
注意:这里库的头文件目录的配置建议在所有配置-所有平台下进行,否则不仅代码提示找不到头文件,编译也是找不到头文件。
在 Debug-x86 配置下生成,我们关注一下在文件系统中主要发生了什么?
- 在解决方案目录的
Debug
子目录中,产生了- Demo2_Exe.exe
- 附带的文件 Demo2_Exe.pdb
- 在项目目录的
Debug
子目录下,生成若干的杂项文件
4. 创建动态库并使用
4.1 创建动态库
在创建新项目界面,选择动态链接库 DLL(C++,Windows,库),创建 Demo3_Shared 项目。
自动生成的内容包括如下几个文件:
- 头文件 framework.h
- 头文件 pch.h (include framework.h)
- 源文件 pch.cpp (include pch.h)
- 源文件 dllmain.cpp(include pch.h)
留意一下 dllmain.cpp 的默认内容,是在 DLL 添加的加载时的启动函数。(似乎非必要?不去理会,保留这个文件)
1 | // dllmain.cpp : 定义 DLL 应用程序的入口点。 |
我们首先添加头文件 Demo3_Shared.h,内容为
1 |
|
这里极具 Windows 特色,动态库不会默认导出任何内容,我们需要手动控制函数和类的导出,在编译时需要的头文件应当为
1 | __declspec(dllexport) void say_hello_shared(); |
在使用时需要的头文件应当为
1 | __declspec(dllimport) void say_hello_shared(); |
这里通过宏MY_EXPORTS
是否被定义区分这两个状态,避免分别提供两个版本的头文件。(关于这个宏,见下文的配置)
注:
- 这里只演示了函数符号的导入导出,其实还有类符号的导入导出,不过类的
__declspec()
需要放置在class
和类名之间。 - 关于库文件中符号的导出,Windows和Linux采用了不同的做法,其实各有利弊:
- Windows默认所有的符号都不导出,这导致库的编写非常繁琐,而且拆分了多个库文件不利于库的使用;
- Linux默认所有的符号都导出,这虽然对库的编写和使用通常是便利的,但是如果在使用第三方库时出现菱形依赖的情况,相应的处理会非常麻烦。
添加源文件 Demo2_Shared.cpp,内容为
1 |
|
然后我们需要进行如下设置,这里仍然把 Debug-x86
改成所有配置-所有平台,在下面的位置添加MY_EXPORTS
宏
1 | 项目 -> 属性 -> C/C++ -> 预处理器 -> 预处理器定义(第一个选项) |
事实上,这个静态库模板会自动给我们添加<PROJECTNAME>_EXPORTS
这个宏,我们使用的是MY_EXPORTS
,和官方推荐的命名略有不同,并不影响。
在 Debug-x86 配置下生成,我们关注一下在文件系统中主要发生了什么?
- 在解决方案目录的
Debug
子目录中,产生了- Demo3_Shared.dll(重要)
- Demo3_Shared.exp
- Demo3_Shared.lib(重要)
- Demo2_Static.pdb(调试相关)
- 在项目目录的
Debug
子目录下,生成若干的杂项文件
其中.lib
在 DLL
被链接时必须(也可以用.exp
替代,主要为了处理相互依赖的问题),.dll
在
DLL 被使用时必须,是动态库的主要部分。
4.2 使用动态库
创建一个可执行文件项目 Demo3_Exe 的过程略,我们关注如何使用解决方案内部的动态库。(对于解决方案之外的动态库导入,就像 CMake 那样可能很复杂,这里不考虑)
将默认的 Demo3_Exe.cpp 的文件内容替换为
1 |
|
接下来是需要的配置过程,参考官方教程。
- 在项目层面,通过右键菜单,对 Demo3_Exe 项目添加引用,添加 Demo3_Shared 项目;
- 解决找不到库的头文件的问题(相当于 gcc 的
-I
选项),把 Debug-x86 改成所有配置-所有平台(重要!!!),在下面的配置中加入头文件所在目录
1 | 项目 -> 属性 -> C/C++ -> 常规 -> 附加包含目录(第一个选项) |
上面的配置过程和静态库一致,但是此时如果尝试生成,编译环节没问题,但是链接环节会报 LINK2019 链接错误。我们还需要继续进行配置
- 解决链接错误 LINK2019 问题,把 Debug-x86 改成所有配置-所有平台(重要!!!),在下面的位置添加 Demo3_Shared.lib (注意是.lib,不是.dll)
1 | 项目 -> 属性 -> 链接器 -> 输入 -> 附加依赖项(第一个选项,通常非空) |
- 继续,在下面的位置添加 Demo3_Shared.lib 所在的路径(注意这里的路径通常是输出位置,例如 Debug 或 x64/Debug 文件夹下)
1 | 项目 -> 属性 -> 链接器 -> 常规 -> 附加库目录 |
注,.lib
文件的具体位置与配置有关,可以使用相对路径例如$(IntDir)
,这可以兼容不同的配置情形,并且不会因为解决方案的整体移动,导致所有路径发生错误。
在 Debug-x86 配置下生成,我们关注一下在文件系统中主要发生了什么?
- 在解决方案目录的
Debug
子目录中,产生了- Demo3_Exe.exe
- 附带的文件 Demo3_Exe.pdb
- 在项目目录的
Debug
子目录下,生成若干的杂项文件
这里的可执行文件 Demo3_Exe.exe 能不能正确运行,还得看天时地利——能不能找得到动态库:Windows 下只能在可执行文件同一路径下或者 PATH 中查找动态库,不支持在调用可执行文件的当前目录下查找动态库。
5. 使用外部库
前面我们只考虑了静态库和动态库都在同一个解决方案内部,如果 Demo 库是第三方开发的,直接给我们提供了:
- 第三方静态库:头文件 Demo2_Static.h 库文件 Demo2_Static.lib
- 第三方动态库:头文件 Demo3_Shared.h 库文件 Demo3_Shared.dll Demo3_Shared.lib
它们分别存放在E:/
下的如下位置:(模仿 Linux 风格)
1 | |-bin |
现在的情景是:新建一个简单项目 Simple(以及同名解决方案),生成一个可执行文件 Simple_Exe,它需要调用上面的两个库。 这里我们还明确:第三方库都是 MSVC 通过 Debug-x86 配置得到的,接下来的使用也在 Debug-x86 配置下进行。
源文件 Simple_Exe.cpp 的内容为
1 |
|
配置过程如下:(把 Debug-x86 改成所有配置-所有平台,重要)
- 解决找不到库的头文件的问题(相当于 gcc
的
-I
选项),在下面的配置中加入头文件所在目录(E:\include\Demo3_Shared
,E:\include\Demo2_Static
)
1 | 项目 -> 属性 -> C/C++ -> 常规 -> 附加包含目录(第一个选项) |
- 找到库文件(.lib),在下面添加库文件的位置(
E:\lib\Demo2_Static
,E:\lib\Demo3_Shared
)
1 | 项目 -> 属性 -> 链接器 -> 常规 -> 附加库目录 |
- 添加库文件(.lib),在下面添加库文件的名称(
Demo2_Static.lib
,Demo3_Shared.lib
)
1 | 项目 -> 属性 -> 链接器 -> 输入 -> 附加依赖项 |
这样就可以完成编译和链接了。
6. 小结与补充
综上,我们在 VS 中,完成了 Demo 解决方案,包括三组共五个项目,依次为
- Demo1 创建可执行文件
- Demo2_Static 生成静态库
- Demo2_Exe 创建可执行文件调用静态库
- Demo3_Shared 生成动态库
- Demo3_Exe 创建可执行文件调用动态库
然后将 Demo 视作第三方库,将其中的库文件和头文件存放到指定位置,在一个新的 Simple 项目中,可执行文件直接调用第三方的静态库和动态库。
Windows 对库的处理逻辑:
- 对于静态库,编译时需要
.lib
文件;运行时不需要 - 对于动态库,编译时只需要动态库的
.lib
文件;运行时只需要动态库的.dll
文件
与 Linux 不同,Windows 的动态库在运行时需要的
.dll
部分,更像一个可执行文件,需要和可执行文件放在一起,而非和静态库放在一起。
Windows 下的可执行文件查找动态库的逻辑:
- 首先查找可执行文件同一目录下是否有需要的
.dll
文件; - 然后根据 PATH 等环境变量,或者系统固定路径进行查找
尤其注意的是,不支持在执行命令的当前目录进行查找。最终的可执行文件为了正常运行,仍然需要找到动态库:要么移动.dll
文件到可执行文件存放的目录下,要么移动.dll
文件到
PATH 路径下。
对于一个 VS 解决方案文件夹,哪些内容是不需要的呢?(不会影响再次编译生成)包括但不限于:
- 输出文件夹 Debug,Release,x64/Debug,x64/Release
- .vs 文件夹
7. Visual Studio 文本配置
对于 Visual Studio 2022 的文本配置,顺便记录一下吧。
- VS
是直接支持
.editconfig
,.clang-tidy
和.clang-format
这些的,只需要放在 VS 可以找到的位置(VS 会自动沿着项目路径往上查找) - 关于编码:下载一个
FileEncoding
插件,这个插件会在编辑区的右下角显示文件编码,并支持在 UTF-8 和几个常用编码之间直接切换 - 关于回车:编辑区的右下角默认是显示回车符号类型的,注意它是否正确识别
.editconfig
文件即可 - 关于 tab:编辑器的右下角默认是显示 tab 或空格选项的,注意即可
- 关于空白字符:编辑->高级->显示空白
8. Windows 编程的 tips
几个最常见的坑,专门为了处理 Windows 的几个麻烦问题。
关于 scanf 警告不安全
scanf 这一类标准库的函数被微软的 MSVC 编译器视作不安全的,可能有缓冲区溢出的危险,它总是不厌其烦地建议替换为 scanf_s。通常我们不想理会这个建议,可以使用下面的选项
1 |
这里我们进行了冗余的设置,#pragma
选项和下面的_CRT_SECURE_NO_WARNINGS
宏定义都可以用来关闭这些警告,相当于加了两道保险。
输出中文乱码
如果源文件采用 utf-8,在 Windows
尝试进行中文输出,那么我们很可能遇到中文乱码的情况,原因是活动代码页是默认的
GBK 编码而非
utf-8。我们可以手动执行chcp 65001
更改活动代码页为
utf-8,但这个做法不仅麻烦,而且程序执行时有时会打开一个新的界面,导致我们来不急在程序启动之前修改活动代码页。
我们可以利用全局变量的初始化,在 main 函数之前自动先将活动代码页改为 UTF-8:
1 |
|
至于 C++程序输入中文有乱码或者压根无法输入,这个暂时还不会。在 shell/cmd 使用中文输入总有种不太合适的感觉,可能需要使用 GUI 程序,在图形化界面上处理非 ASCII 编码的输入才更加自然,例如 QT 对这些编码问题可以很方便地处理。
对于 Windows 的控制台输出,还有一种做法(参考 fmt 库)是首先判定是否是控制台,然后对于 Windows 控制台,将 utf8 代码转换为 utf16,通过 Windows 提供的
WriteConsoleW
接口直接输出,绕过了std::cout
。
控制台输出颜色
在 Linux 的
shell,我们可以很方便地使用\x1b[94m
等控制字符去调整输出内容的颜色,例如下面的代码只有中间一行会输出蓝色。
1 | std::cout << "no color\n" |
但是这些在 Windows 上默认不支持(测试发现 Windows terminal 会支持,原始的 cmd 和 powershell 不支持)
Windows
环境下可以手动开启一个选项:支持控制台虚拟终端序列,也就是\x1b[94m
等字符,此时就可以正常显示颜色,但是
Windows 为了历史兼容性,并没有默认开启这个选项!(参考微软官方文档)
我们可以利用全局变量的初始化,在 main 函数之前自动开启这个选项:
1 |
|
关于 windows.h 的坑
一个著名的关于 windows.h 这个重要头文件的坑,它自己定义了 min 和 max 这两个宏!这直接与 std::min 和 std::max 冲突了,例如下面的代码可能会先进行宏展开,从而导致编译报错。
1 | a = std::max(b,c); |
一个做法是关闭这两个宏,在导入 windows.h 时使用下面的形式
1 |
另一个做法是加括号,括号也会阻止宏的展开
1 | a = (std::max)(b,c); |
头文件参考
以上的几个小技巧可以更完整的封装到一个头文件中,注意:首先要判断一下是否处于 Windows 平台,其次这里初始化调用的函数可能抛异常,因此需要重新包装一下,把可能的异常都捕获但直接忽略。
1 |
|