你所热爱的,就是你的 Windows。
茶 栗
我想要解决方案,gkd
我把这个问题的解决方案和上一篇文章(讲 UTF-8 文本在命令行里对齐的方案)的方案整理到了一个 GitHub Repo 中,可以方便的引用到你的 CMake 项目中(如果不想,也可以手动把 src 和 include 复制到你的项目中,配置好编译)。同时为了说明这一方案的优雅性,我把这个 repo 里给的例子也贴在这里:
#include <iostream>
#include <iomanip>
#include <string>
#include "utf8_console.h"
using namespace std;
int main()
{
initialize_utf8_console();
string test_str1 = "测试 アイウエオ 😅🤣";
string test_str2 = "テスト Россия";
cout << "Test UTF-8 aligned output: " << endl
<< right << setw_u8(30, test_str1) << test_str1 << endl
<< right << setw_u8(30, test_str2) << test_str2 << endl
<< endl;
cout << "Test UTF-8 input: Use EOF as end." << endl;
string str;
while (getline(cin, str))
cout << str << endl;
return 0;
}

细节
还是同一个高程课,有同学在群里问怎么用 cin 读 UTF-8。我以为就 SetConsoleCP 之后就可以用 cin 嘎嘎读,结果自己试了试,并没有那么简单。
直到2022年,微软还没有把Windows上的UTF-8字符处理弄明白。我尝试了以下操作,试图从通过 cin 直接读入 UTF-8 文本,均以失败告终,无论我用的是 Windows Terminal,还是经典的 conhost,还是 ConEmu、MobaXTerm:
system("chcp 65001");
SetConsoleCP
- 修改系统全局 Code Page 到 65001
结果不是中文全部变成“0”就是变成无效的字符。
这个 SO 回答给出了部分解决这个问题的方法——使用 Windows 的 ReadConsoleW
API 直接读取 UTF-16 字符串并用 WideCharToMultiByte
转换为 UTF-8。(原 po 的函数调用没有带“W”,估计是假定了编译开了 UNICODE
,我这里就显式指定了)。因此我们可以写出这样的代码:
unsigned read_console_utf8(char* out, int max_size)
{
auto wstr = new wchar_t[max_size];
unsigned read_len;
ReadConsole(GetStdHandle(STD_INPUT_HANDLE), wstr, max_size, &read_len, NULL);
int size = WideCharToMultiByte(CP_UTF8, 0, wstr, read_len, mb_str, max_size - 1, NULL, NULL);
mb_str[size] = 0;
delete[] wstr;
return size;
}
但是,这样的代码并不足够:
- 我们想用 C++ 风格的
std::cin
来格式化输入 - 这个函数对换行的处理并不好
- 没有处理 EOF
因此,我们需要大大拓展这个方案。不妨从魔改 cin
入手。这需要实现一个自定义的 std::streambuf
,然后通过 std::cin::rdbuf
替换掉 cin
的输入实现。
实现最基础的读取型 streambuf
的还是相对简单的,虽然 C++ 这块的命名只能说灾难来形容,但如果不处理那么 fancy 的功能(putback 等),那我们只需要实现 underflow
函数即可。更具体的介绍可以在 cppreference.com 参阅。
在 underflow
中,实现者要读取新数据,并使用 setg
函数把 streambuf
指向有读取到数据的缓冲区。同时,还要检查 gptr
是否在 egptr
前,如果在的话说明上一次数据还没有 consume 完毕,不一定要读新数据。
然而,如果看我在 utf8-console
项目中的 win_utf8_streambuf
实现,会发现里面有比较复杂的对 EOF 的处理。这是由于 Windows 的控制台下的 EOF(通过 Ctrl+Z
发送)的行为造成的。实际上在使用 ReadConsoleW
读取时,Ctrl+Z 发送的 EOF 会直接作为 0x1A
的一个 ASCII 字符读进来,且不会真正终止输入流。按 Ctrl+Z 后还可以继续读。因此我们需要判定读到的数据中有没有 0x001A
(因为是 UTF-16),如果有的话需要提前结束处理,设置 EOF 标志。
在实现了 streambuf
后,创建一个全局有效的 streambuf
的实例,设置好 SetConsoleCP
、std::cin.rdbuf
后就可以愉快地通过 std::cin
读取控制台下的中文了。至于换行的问题,istream 会帮我们处理好。
值得一提的是,这种方法并不会影响 scanf
、gets
、getchar
、stdin
等通过传统 C 风格 IO 的控制台函数!同时 putback
等函数也是不可用的。
Comments NOTHING