几个最常见的坑,专门为了处理 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宏定义都可以用来关闭这些警告,相当于加了两道保险。
关于 windows.h 的坑
一个著名的关于 windows.h 这个重要头文件的坑,它自己定义了 min 和 max 这两个宏!这直接与 std::min 和 std::max 冲突了,例如下面的代码可能会先进行宏展开,从而导致编译报错。
一个做法是关闭这两个宏,在导入 windows.h 时使用下面的形式
1 2 3
| #define NOMINMAX #include <windows.h> #undef NOMINMAX
|
另一个做法是加括号,括号也会阻止宏的展开
更好的做法是:只要有得选,尽量不要使用 windows.h 这类与平台高度绑定的头文件,这会破坏代码的可移植性,而且 windows.h 会引入一大堆乱七八糟的东西。
控制台的 UTF-8 交互
C++ 对 UTF-8 的支持真是一塌糊涂,存在一堆历史包袱,尤其在 Windows 上的使用简直到处都是问题,下面提供的做法也不是万能的,只能保证某些情况下可以勉强使用,并且需要假定对外部环境的正确配置,以及对编译选项的正确使用。
输出 UTF-8
如果源文件采用 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 = []() { 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; }
|
测试发现,这种做法只能保证输出正常,无法保证输入中文正常。
输入 UTF-8
如果存在从控制台获取用户输入的 UTF-8 字符串的需求,一种可行的做法是:通过直接调用 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #include <windows.h> #include <iostream> #include <string> #include <optional>
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; }
if (len < 2 || utf8_buffer[static_cast<size_t>(len - 2)] != '\r' || utf8_buffer[static_cast<size_t>(len - 1)] != '\n') { return std::nullopt; }
return utf8_buffer.substr(0, static_cast<size_t>(len - 2)); }
int main() { SetConsoleOutputCP(65001); SetConsoleCP(65001);
std::cout << "test utf8 output:\n"; std::string str1 = "テスト Россия"; std::cout << "测试 アイウエオ 😅🤣" << " | " << str1 << '\n';
std::cout << "test utf8 input:\n"; auto input = utf8_input(); if (input.has_value()) { std::cout << "input = \"" << input->c_str() << "\"\n"; std::cout << "len = " << input->size() << '\n'; } else { std::cout << "fail\n"; }
return 0; }
|
这部分代码参考 通过 cin 在 Windows 上正确读入 UTF-8 字符串 和 Reading UTF-8 characters from console。
控制台输出颜色
在 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 = []() { 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; }();
|