之前的 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
2
3
4
5
6
|- .vs
Demo1.cpp
Demo1.sln
Demo1.vcxproj
Demo1.vcxproj.filters
Demo1.vcxproj.user

这只是建议的一对一情形下的设置,但是我们仍然可以在这个基础上再向解决方案中添加新项目,例如添加 Demo2 项目,此时默认推荐的项目位置是F:/,最终在F:/得到如下的内容

1
2
3
4
5
6
7
8
9
10
11
12
|- Demo1
|- .vs
Demo1.cpp
Demo1.sln
Demo1.vcxproj
Demo1.vcxproj.filters
Demo1.vcxproj.user
|- Demo2
Demo2.cpp
Demo2.vcxproj
Demo2.vcxproj.filters
Demo2.vcxproj.user

此时 Demo1 项目和 Demo2 项目的文件夹仍然是并列的,而属于 Demo1 解决方案的部分(包括Demo1.sln文件和.vs文件夹)则位在 Demo1 项目的文件夹下。

1.2 示例二

如果我们创建名为 Demo1 的项目,对应不同名的 Demo 解决方案,项目位置放置在E:/。我们会得到什么?

查看文件系统可知,VS 会在F:/创建 Demo 目录,这个目录对应的是 Demo 解决方案, 下面是在F:/Demo/默认生成的内容

1
2
3
|- .vs
|- Demo1
Demo.sln

Demo1 这个子目录对应的就是 Demo1 这个项目,里面的内容包括

1
2
3
4
5
Demo1.cpp
Demo1.sln
Demo1.vcxproj
Demo1.vcxproj.filters
Demo1.vcxproj.user

对比可以发现,.vs这个隐藏文件夹也是和解决方案对应的。

我们可以在 Demo 解决方案中再添加新的项目(对着解决方案右键,添加,新建项目),例如再添加一个 Demo2 项目,此时提示设置两个信息:

  • 项目名称
  • 项目位置(默认是E:/Demo,即 Demo 解决方案的目录下,也可以修改到其它位置)

此时文件系统中就变成

1
2
3
4
5
6
7
8
9
10
11
12
|- .vs
|- Demo1
Demo1.cpp
Demo1.vcxproj
Demo1.vcxproj.filters
Demo1.vcxproj.user
|- Demo2
Demo2.cpp
Demo2.vcxproj
Demo2.vcxproj.filters
Demo2.vcxproj.user
Demo.sln

即 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
2
3
# pragma once

void say_hello();

在 hello.cpp 实现

1
2
3
4
5
6
#include "hello.h"
#include <iostream>

void say_hello(){
std::cout << "hello,world!\n";
}

在 Demo1.cpp 引用 hello.h 并调用该函数

1
2
3
4
5
6
#include "hello.h"

int main(){
say_hello();
return 0;
}

在主界面的生成选项,有两大部分,第一部分是若干针对整个解决方案的生成/清理/分析,第二部分是针对当前项目的生成/清理/分析。

不需要任何配置信息的修改,直接执行当前项目的生成,我们就可以得到一个麻雀虽小五脏俱全的 helloworld 可执行程序。

注意:VS 对于每一个项目都会根据构建模式(例如 Debug)和平台(例如 x86)的不同,选择执行对应的编译/调试等行为,默认情形下总是使用 Debug-x86 的配置。在下文修改信息时也要注意,有时候配置信息的修改可能都只限于当前配置,改成 Debug-x64 或者 Release-x86,这些修改又都没了。

我们关注一下在文件系统中主要发生了什么?这里顺便对不同的配置进行对比

  1. 如果在 Debug-x86 配置下进行生成,最终会多出这些内容
    • 在解决方案目录E:/DemoDebug子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb
    • 在项目目录E:/Demo/Demo1Debug子目录下,生成若干的杂项文件
  2. 如果在 Debug-x64 配置下进行生成,最终会多出这些内容
    • 在解决方案目录E:/Demox64/Debug子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb
    • 在项目目录E:/Demo/Demo1x64/Debug子目录下,生成若干的杂项文件
  3. 如果在 Release-x86 配置下进行生成,最终会多出这些内容
    • 在解决方案目录E:/DemoRelease子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb
    • 在项目目录E:/Demo/Demo1Release子目录下,生成若干的杂项文件
  4. 如果在 Release-x64 配置下进行生成,最终会多出这些内容
    • 在解决方案目录E:/Demox64/Release子目录下,生成 Demo1.exe 以及附带的文件 Demo1.pdb
    • 在项目目录E:/Demo/Demo1x64/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
2
3
#pragma once

void say_hello_static();

将 Demo2_Static.cpp 的文件内容替换为

1
2
3
4
5
6
7
#include "framework.h"
#include "pch.h"

#include "Demo2_Static.h"
#include <iostream>

void say_hello_static() { std::cout << "hello, my staticlib!\n"; }

这里我们保持 Debug-x86 的配置,直接生成当前项目。我们关注一下在文件系统中主要发生了什么?

  • 在解决方案目录的Debug子目录中,产生了
    • Demo2_Static.lib(重要)
    • Demo2_Static.idb(调试相关)
    • Demo2_Static.pdb(调试相关)
  • 在项目目录的Debug子目录下,生成若干的杂项文件

3.2 使用静态库

创建一个可执行文件项目 Demo2_Exe 的过程略,我们关注如何使用解决方案内部的静态库。(对于解决方案之外的静态库导入,就像 CMake 那样可能很复杂,这里不考虑)

将默认的 Demo2_Exe.cpp 的文件内容替换为

1
2
3
4
5
6
#include "Demo2_Static.h"

int main(){
say_hello_static();
return 0;
}

接下来是需要的配置过程,参考官方教程

  • 在项目层面,通过右键菜单,对 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

我们首先添加头文件 Demo3_Shared.h,内容为

1
2
3
4
5
6
7
8
9
#pragma once

#ifdef MY_EXPORTS
#define MY_API __declspec(dllexport)
#else
#define MY_API __declspec(dllimport)
#endif

MY_API void say_hello_shared();

这里极具 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
2
3
4
5
6
7
#include "framework.h"
#include "pch.h"

#include "Demo3_Shared.h"
#include <iostream>

void say_hello_shared() { std::cout << "hello, my sharedlib!\n"; }

然后我们需要进行如下设置,这里仍然把 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
2
3
4
5
6
#include "Demo3_Shared.h"

int main(){
say_hello_shared();
return 0;
}

接下来是需要的配置过程,参考官方教程

  • 在项目层面,通过右键菜单,对 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
2
3
4
5
6
7
8
9
10
11
12
|-bin
Demo3_Shared.dll
|-include
|-Demo2_Static
Demo2_Static.h
|-Demo3_Shared
Demo3_Shared.h
|-lib
|-Demo2_Static
Demo2_Static.lib
|-Demo3_Shared
Demo3_Shared.lib

现在的情景是:新建一个简单项目 Simple(以及同名解决方案),生成一个可执行文件 Simple_Exe,它需要调用上面的两个库。 这里我们还明确:第三方库都是 MSVC 通过 Debug-x86 配置得到的,接下来的使用也在 Debug-x86 配置下进行。

源文件 Simple_Exe.cpp 的内容为

1
2
3
4
5
6
7
8
#include "Demo2_Static.h"
#include "Demo3_Shared.h"

int main(){
say_hello_static();
say_hello_shared();
return 0;
}

配置过程如下:(把 Debug-x86 改成所有配置-所有平台,重要)

  • 解决找不到库的头文件的问题(相当于 gcc 的-I选项),在下面的配置中加入头文件所在目录(E:\include\Demo3_SharedE:\include\Demo2_Static
1
项目 -> 属性 -> C/C++ -> 常规 -> 附加包含目录(第一个选项)
  • 找到库文件(.lib),在下面添加库文件的位置(E:\lib\Demo2_StaticE:\lib\Demo3_Shared
1
项目 -> 属性 -> 链接器 -> 常规 -> 附加库目录
  • 添加库文件(.lib),在下面添加库文件的名称(Demo2_Static.libDemo3_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
2
3
4
5
6
7
8
#if defined(_MSC_VER)
#pragma warning(disable : 4996)

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif

#endif

这里我们进行了冗余的设置,#pragma选项和下面的_CRT_SECURE_NO_WARNINGS宏定义都可以用来关闭这些警告,相当于加了两道保险。

输出中文乱码

如果源文件采用 utf-8,在 Windows 尝试进行中文输出,那么我们很可能遇到中文乱码的情况,原因是活动代码页是默认的 GBK 编码而非 utf-8。我们可以手动执行chcp 65001更改活动代码页为 utf-8,但这个做法不仅麻烦,而且程序执行时有时会打开一个新的界面,导致我们来不急在程序启动之前修改活动代码页。

我们可以利用全局变量的初始化,在 main 函数之前自动先将活动代码页改为 UTF-8:

1
2
3
4
5
6
7
8
9
#include "windows.h"

inline static int set_chcp_utf8 = []() {
// chcp utf-8
SetConsoleOutputCP(65001);
SetConsoleCP(65001);

return 0;
}();

至于 C++程序输入中文有乱码或者压根无法输入,这个暂时还不会。在 shell/cmd 使用中文输入总有种不太合适的感觉,可能需要使用 GUI 程序,在图形化界面上处理非 ASCII 编码的输入才更加自然,例如 QT 对这些编码问题可以很方便地处理。

对于 Windows 的控制台输出,还有一种做法(参考 fmt 库)是首先判定是否是控制台,然后对于 Windows 控制台,将 utf8 代码转换为 utf16,通过 Windows 提供的WriteConsoleW接口直接输出,绕过了std::cout

控制台输出颜色

在 Linux 的 shell,我们可以很方便地使用\x1b[94m等控制字符去调整输出内容的颜色,例如下面的代码只有中间一行会输出蓝色。

1
2
3
std::cout << "no color\n"
<< "\x1b[94m" << "color is blue!\n" << "\x1b[0m"
<< "no color again!\n";

但是这些在 Windows 上默认不支持(测试发现 Windows terminal 会支持,原始的 cmd 和 powershell 不支持)

Windows 环境下可以手动开启一个选项:支持控制台虚拟终端序列,也就是\x1b[94m等字符,此时就可以正常显示颜色,但是 Windows 为了历史兼容性,并没有默认开启这个选项!(参考微软官方文档

我们可以利用全局变量的初始化,在 main 函数之前自动开启这个选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "windows.h"

inline static int enable_virtual_terminal_mode = []() {
// Set output mode to handle virtual terminal sequences
// To support ansi colorful output
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE) { return false; }

DWORD dwMode = 0;
if (!GetConsoleMode(hOut, &dwMode)) { return false; }

dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (!SetConsoleMode(hOut, dwMode)) { return false; }
return true;
}();

关于 windows.h 的坑

一个著名的关于 windows.h 这个重要头文件的坑,它自己定义了 min 和 max 这两个宏!这直接与 std::min 和 std::max 冲突了,例如下面的代码可能会先进行宏展开,从而导致编译报错。

1
a = std::max(b,c);

一个做法是关闭这两个宏,在导入 windows.h 时使用下面的形式

1
2
3
#define NOMINMAX
#include <windows.h>
#undef NOMINMAX

另一个做法是加括号,括号也会阻止宏的展开

1
a = (std::max)(b,c);

头文件参考

以上的几个小技巧可以更完整的封装到一个头文件中,注意:首先要判断一下是否处于 Windows 平台,其次这里初始化调用的函数可能抛异常,因此需要重新包装一下,把可能的异常都捕获但直接忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#pragma once

#if defined(_MSC_VER)
#pragma warning(disable : 4996)

#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif

#endif

#ifdef _WIN32

#ifndef NOMINMAX
#define NOMINMAX
#endif

#include <windows.h>


class WindowsInit {
private:
static int set_chcp_utf8() noexcept {
try {
SetConsoleOutputCP(65001);
SetConsoleCP(65001);
}
catch (...) {
return -1;
}
return 0;
}

static int enable_virtual_terminal_mode() noexcept {
try {
// Set output mode to handle virtual terminal sequences
// To support ansi colorful output
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE) { return false; }

DWORD dwMode = 0;
if (GetConsoleMode(hOut, &dwMode) == 0) { return false; }

dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
catch (...) {
return -1;
}
return 0;
}

inline static int tmp = enable_virtual_terminal_mode() + set_chcp_utf8();
};
#endif