几个最常见的坑,专门为了处理 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

除此之外,c++提供的std::locale通常也可以保证输出中文正常,例如

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <locale>

int main() {
std::locale::global(std::locale("zh_CN.UTF-8"));

std::cout << "你好\n";
return 0;
}

但是测试发现,它依旧无法保证输入中文正常。

通过直接调用 Windows API 从控制台获取宽字符输入,然后将宽字符串转换为 UTF-8 字符串,可以达到获取 utf-8 输入的目的,示例代码如下

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
std::optional<std::string> utf8_input() {

HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
if (hIn == INVALID_HANDLE_VALUE) { return std::nullopt; }

std::wstring wide_buffer;
std::wstring temp_buf(128, L'\0');

while (true) {
DWORD cnt = 0;
if ((ReadConsoleW(hIn, temp_buf.data(), 128, &cnt, nullptr) == 0)
|| cnt == 0) {
break;
}
wide_buffer.append(temp_buf, 0, cnt);
if (cnt < 128) { break; }
}

if (wide_buffer.empty()) { return std::nullopt; }

int utf8_buf_size = static_cast<int>(wide_buffer.size()) * 4;
std::string utf8_buffer(static_cast<size_t>(utf8_buf_size), '\0');
BOOL use_default_char = 0;
int len = WideCharToMultiByte(CP_UTF8, 0, wide_buffer.data(),
static_cast<int>(wide_buffer.size()),
utf8_buffer.data(), utf8_buf_size,
nullptr, &use_default_char);
if (use_default_char != 0 || len == 0) {
return std::nullopt; // Conversion failed
}

if (len < 2 || utf8_buffer[static_cast<size_t>(len - 2)] != '\r'
|| utf8_buffer[static_cast<size_t>(len - 1)] != '\n') {
return std::nullopt; // Invalid, missing CRLF at the end
}

return utf8_buffer.substr(0,
static_cast<size_t>(len - 2)); // Remove CRLF
}

这部分代码参考一篇博客以及StackOverflow的一个问答

控制台输出颜色

在 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_console.hpp如下

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#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>

#endif

#ifndef _WIN32
#include <iostream>
#endif

#include <optional>
#include <string>

class WindowsConsole {
public:
static int init() noexcept {
#ifdef _WIN32
try {
SetConsoleOutputCP(65001);
SetConsoleCP(65001);

// 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 -1; }

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

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

init_flag = true;
return 0;
}

// Get utf8 input:
// Use Windows API to read wide characters from the console and convert to
// UTF-8 on Windows.
// Use std::getline on Linux, directly.
static std::optional<std::string> utf8_input() {
if (!init_flag) { init(); }

#ifdef _WIN32
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE);
if (hIn == INVALID_HANDLE_VALUE) { return std::nullopt; }

std::wstring wide_buffer;
std::wstring temp_buf(128, L'\0');

while (true) {
DWORD cnt = 0;
if ((ReadConsoleW(hIn, temp_buf.data(), 128, &cnt, nullptr) == 0)
|| cnt == 0) {
break;
}
wide_buffer.append(temp_buf, 0, cnt);
if (cnt < 128) { break; }
}

if (wide_buffer.empty()) { return std::nullopt; }

int utf8_buf_size = static_cast<int>(wide_buffer.size()) * 4;
std::string utf8_buffer(static_cast<size_t>(utf8_buf_size), '\0');
BOOL use_default_char = 0;
int len = WideCharToMultiByte(CP_UTF8, 0, wide_buffer.data(),
static_cast<int>(wide_buffer.size()),
utf8_buffer.data(), utf8_buf_size,
nullptr, &use_default_char);
if (use_default_char != 0 || len == 0) {
return std::nullopt; // Conversion failed
}

if (len < 2 || utf8_buffer[static_cast<size_t>(len - 2)] != '\r'
|| utf8_buffer[static_cast<size_t>(len - 1)] != '\n') {
return std::nullopt; // Invalid, missing CRLF at the end
}

return utf8_buffer.substr(0,
static_cast<size_t>(len - 2)); // Remove CRLF
#else
std::string input;
if (!std::getline(std::cin, input)) { return std::nullopt; }
return input;
#endif
}

private:
inline static bool init_flag = false;
};