Unicode 输出到 Windows 控制台
在 C++ 中处理 Unicode 字符串并将其打印到 Windows 控制台。
谁应该阅读本文?
如果您创建 Windows 控制台程序并希望能够正确打印宽字符串,那么这个工具就是为您准备的。
比起实际的 C++ 熟练程度,更重要的是您要理解什么是 Unicode,什么是宽字符串。
引言
再怎么强调构建 Unicode 感知的应用程序的重要性都不为过。
C/C++ 的新手从一开始就应该被教导不要使用 main
、strlen
、printf
等函数。应该从一开始就向他们指出,现代 Windows 系统内部使用 16 位 Unicode,也称为宽字符串。因此,应该改用 wmain
、wcslen
、wprintf
等(甚至更好:TCHAR
范式)。
在 Visual Studio 中创建新的 C++ 项目时,它们会遵循 TCHAR
范式。这意味着,将使用 _tmain
、_tcsclen
、_tprintf
等,而不是上面提到的那些。它们是 typedef,根据项目设置中选择的字符集具有不同的含义。这种范式是为了使相同的代码能够为旧版(Windows 95、Windows 98)和新版 Windows(NT、XP 及更高版本)构建。由于现在为这些旧版 Windows 编程已无意义,我们可以直接使用宽版本的函数。然而,遵循 TCHAR
范式仍然有意义,因为它能使代码更容易移植到不使用宽字符串的操作系统,例如 Linux。
这一切都运行良好。问题出现在编写控制台应用程序时。应用程序可以正确读取宽命令行参数。我不知道通过标准输入输入宽字符串是否可行,因为我从未需要过它。但我需要输出它们,而它不起作用。我尝试了 CRT 函数,如 wprintf
,以及 STL 对象,如 wcout
。它们都不起作用。我搜索了一个合适的解决方案,但没有找到。
我将 cmd 窗口设置为使用 Lucida Console 字体(**您也应该这样做,否则任何试图在其中显示 Unicode 字符的尝试都注定失败!**)。我意识到可以使用 conio.h 中的函数(_cputts
、_tcprintf
等)直接将宽字符串打印到控制台。太棒了!
然而……当人们使用控制台应用程序时,他们期望能够重定向其输出。如果输出直接发送到控制台,则重定向不起作用。它必须发送到 stdout
或 stderr
。
微软似乎在这方面不一致。虽然整个系统都使用宽字符串,但控制台输出却不是,而在 .NET 中,默认输出代码页是 UTF-8!但它给了我启发。我还注意到,使用 `type` 命令等,可以正确地将编码为 UTF-8 的文本文件打印到控制台(只要控制台代码页被设置为 UTF-8,使用命令 `chcp 65001`)。现在我想从 C++ 使用 UTF-8。
Using the Code
设置和重置代码页
我们必须为 UTF-8 准备控制台。我们首先将当前控制台输出代码页存储在一个变量中
UINT oldcp = GetConsoleOutputCP();
然后我们将控制台输出代码页更改为 UTF-8,这相当于 `chcp 65001`
SetConsoleOutputCP(CP_UTF8);
在退出程序之前,我们必须礼貌地将控制台恢复到之前的状态。我们必须
SetConsoleOutputCP(oldcp);
当我们想在程序中打印宽字符串时,我们将这样做
假设我们有一个包含 Unicode 字符的宽字符串,例如
wchar_t s[] = L"èéøÞǽлљΣæča";
如果您在 Visual Studio 中这样写,当您尝试保存文件时,会被提示以某种 Unicode 格式保存。“Unicode - Codepage 1200”是可以的。我们将其转换为 UTF-8
首先,我们将 WideCharToMultiByte
的第 6 个参数设置为零。这样,该函数将告诉我们需要多少字节来存储转换后的字符串。
int bufferSize = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL);
我们分配一个缓冲区:
char* m = new char[bufferSize];
第二次调用 WideCharToMultiByte
进行实际转换
WideCharToMultiByte(CP_UTF8, 0, s, -1, m, bufferSize, NULL, NULL);
将其打印到 stdout
。请注意大写的 S
。它告诉 wprint
函数期望一个窄字符串
wprintf(L"%S", m);
释放缓冲区:
delete[] m;
现在输出将发送到 stdout
。如果重定向到文件,该文件将以 UTF-8 编码。
就这样
这不算什么大事,也无法与需要更多工作的文章相提并论。但我希望它能有所帮助,因为它试图解决一个被广泛忽视的问题。我上次检查时,在 Java 中也找不到这个问题的解决方案。
在我提供的示例代码中,我将这里讨论的所有内容都打包在小的 ostream
和 wostream
重载中。它们并不完美,我确信它们可以写得更好。如果我对 iostream
编程了解更多,我会这样做。但它们对于那些想要现成且易于使用的解决方案的人来说可能很有用。但应该指出的是,它们不是线程安全的。代码中有更多注释。
历史
这篇文章经过了完全重写,主要是因为 Member 2901525 的评论让我意识到,如果不做更多解释,这段代码是不够完美的。文章本身非常简短,看起来很粗糙,评价也不高。我忘记了提到必须在 cmd 窗口中使用 Lucida Console 字体。 Member 2901525 注意到了代码中的一个薄弱点,我对此进行了修改。除此之外,代码中没有其他重大改动。