65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Windows 中处理 UTF-8

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (17投票s)

2019年11月25日

MIT

6分钟阅读

viewsIcon

41223

downloadIcon

574

这是(又一篇!)关于如何在仍然鼓励使用 UTF-16 编码的平台上处理 UTF-8 编码的文章。

背景

让我重申一下上面提到的宣言中的一些观点

  • UTF-16(也称为 Unicode、widechar 或 UCS-2)早在 20 世纪 90 年代初就已推出,当时人们认为其 65000 个字符足以满足所有字符的需求,
  • 除特殊情况外,UTF-16 的效率或易用性并不比 UTF-8 高。事实上,在许多情况下,情况恰恰相反。
  • 在 UTF-16 中,字符也具有可变宽度编码(两个或四个字节),并且计算字符数量与 UTF-8 一样困难。

如果您想在 Windows 中使用 UTF-8 编码(并且您应该这样做),并且不想发疯或让程序意外崩溃,您必须遵循以下规则

  • 编译程序时定义 _UNICODE(或在 Visual Studio 中选择使用 Unicode 字符集)。
  • 仅在 API 函数调用的参数中使用 wchar_tstd::wstring。其他地方请使用 charstd::string
  • 使用 widen()narrow() 函数在 UTF-8 和 UTF-16 之间进行转换。

此程序包中提供的函数将使您的工作更加轻松。

调用库函数

所有函数都位于 utf8 命名空间中,我建议您不要为此命名空间使用 using 指令。这是因为许多/大多数函数与传统的 C 函数同名。例如,如果您有一个函数调用

	mkdir (folder_name);

并且您想开始使用 UTF-8 字符,您只需将其更改为

	utf8::mkdir (folder_name);

在函数前加上命名空间可以清楚地表明您正在使用哪个函数。

基本转换函数

遵循相同的宣言,基本转换函数是 narrow()(从 UTF-16 到 UTF-8)和 widen()(反之)。它们的签名如下:

    std::string narrow (const wchar_t* s);
	std::string narrow (const std::wstring& s);
    
    std::wstring widen (const char* s);
    std::wstring widen (const std::string& s);    

此外,还有两个函数用于与 UTF-32 进行相互转换

    std::string narrow (const std::u32string& s);
	std::u32string runes (const std::string& s);

内部转换是通过 WideCharToMultiByteMultiByteToWideChar 函数完成的。

还有用于计算 UTF-8 字符串中的字符数(length())、检查 string 是否有效(valid())以及将指针/迭代器移到字符字符串中的下一个字符(next())的函数。

包装器

几乎所有其他函数都是对传统 C/C++ 函数或结构的封装。

  • 目录操作函数:mkdirrmdirchdirgetcwd
  • 文件操作:fopenchmodaccessrenameremove
  • 流:ifstreamofstreamfstream
  • 路径操作函数:splitpathmakepath
  • 环境变量访问函数 putenvgetenv
  • 字符分类函数 is...isalnumisdigitisalpha 等)

所有这些函数的参数都模仿了标准参数。但是,对于某些函数,例如 accessrename 等,返回类型是 bool,其中 true 表示成功,false 表示失败。这与返回 0 表示成功的标准 C 函数相反。请注意!

返回值

对于返回字符的 API 函数,您需要设置一个 wchar_t 缓冲区来接收值,使用 narrow 函数将其转换为 UTF-8,并最终释放缓冲区。以下是一个示例,说明这将如何显示。代码检索临时文件名。

  wstring wpath (_MAX_PATH, L'\0');
  wstring wfname (_MAX_PATH, L'\0');

  GetTempPath (wpath.size (), const_cast<wchar_t*>(wpath.data ()));
  GetTempFileName (wpath.c_str(), L"ABC", 1, const_cast<wchar_t*>(wfname.data ()));

  string result = narrow(wfname);

这似乎过于繁琐且容易出错,所以我创建了一个用于保存返回值的对象。它具有转换为 wchar_t 缓冲区,然后转换为 UTF-8 字符串的运算符。由于缺乏更好的名称,我称之为 buffer。使用此对象,相同的代码片段变成:

  utf8::buffer path (_MAX_PATH);
  utf8::buffer fname (_MAX_PATH);

  GetTempPath (path.size (), path);
  GetTempFileName (path, L"ABC", 1, fname);

  string result = fname;

内部,buffer 对象包含 UTF-16 字符,但 string 转换运算符会调用 utf8::narrow 函数将 string 转换为 UTF-8。

程序参数

有两个函数用于访问和转换 UTF-16 编码的程序参数:get_argv 函数返回一个类似 argv 的指向命令行参数的指针数组。

  int argc;
 char **argv = utf8::get_argv (&argc);

第二个函数直接提供一个 string 向量。

std::vector<std::string> argv = utf8::argv ();

使用第一个函数时,必须调用 utf8::free_argv 函数来释放为 argv 数组分配的内存。

意外转折 #

C++20 标准添加了一个额外的类型 char8_t,用于保存 UTF-8 编码的字符,以及一个 string 类型 std::u8string。通过将其与 charunsigned char 区分开来,委员会也造成了许多不兼容性。例如,以下片段将产生错误:

std::string s {"English text"}; //this is ok
s = {u8"日本語テキスト"}; //"Japanese text" - error

您必须将其更改为类似以下内容:

std::u8string s {u8"English text"};
s = {u8"日本語テキスト"}; 

依我看,委员会引入 char8_t 类型违背了 UTF-8 编码的初衷。该编码的目的是将 char 类型的用法扩展到额外的 Unicode 代码点。它取得了巨大成功,已成为互联网上事实上的标准。即使是 UTF-16 编码的坚定支持者 Windows,现在也在缓慢转向 UTF-8

在此背景下,char 数据类型除了用于保存 string 的编码之外,似乎不合适。特别是,使用 charunsigned char 进行算术计算只是使用场景的一小部分。标准应该尝试简化最常见用例的使用,而将不常见的用例承担复杂性的负担。

遵循这一原则,您将希望编写

std::string s {"English text"};
s += " and ";
s += "日本語テキスト";

并隐含假设所有 char string 都是 UTF-8 编码的字符 string

最近(2022 年 6 月),委员会似乎改变了立场,并引入了一个兼容性和可移植性修复 - DR2513R3,允许使用 UTF-8 字符串字面量初始化 charunsigned char 的数组。在缺陷报告进入下一个标准版本之前,Visual C++ 用户在 C++20 标准规则下编译的解决方案是使用/Zc:char8_t- 编译器选项。

双重保险 #

从 Windows 版本 1903(2019 年 3 月更新)开始,Microsoft 允许您将 UTF-8 设置为系统 ANSI 代码页(ACP)。这允许您使用 API 函数的 -A 版本而不是 -W 版本。该设置深埋在三级对话框中。

转到设置 > 时间和语言 > 语言和区域,然后选择区域设置

在下一个对话框中,选择更改系统区域设置

最后,勾选Beta 版:对全球语言支持使用 Unicode UTF-8复选框。

更多信息可以在这里找到:在 Windows 应用中使用 UTF-8 代码页

“有什么好处?”您可能会问。“您之前说过我应该定义 _UNICODE 来确保只调用 -W API 函数。”是的,这对于 Windows API 函数是正确的,但对于 C 函数,没有 -A 和 -W 变体。如果您像下面的程序一样调用 fopen

int main()
{
  const char* greeting = u8"Γειά σου Κόσμε!\n"; //Hello world
  const char* filename = u8"όνομααρχείου.txt";

  FILE* f = fopen(filename, "w");
  fprintf(f, greeting);
  fclose(f);
  return 0;
}

您可能不会注意到您调用了 fopen 函数而不是 utf8::fopen。在默认 ANSI 代码页(通常是 1252)的 Windows 机器上运行此程序将产生以下结果:

但是,如果 Windows 设置为使用 UTF-8 ACP,即使调用标准 fopen 也能生成正确的文件名:

此外,所有输出到 std::cout 的内容都将正确格式化为 UTF-8 文本。

明智之举:您应该使用 GetACP 函数检查 Windows 是否设置为使用 UTF-8 代码页。

if (GetACP() != 65001)
    std::cout << "Warning! not using UTF-8 ANSI code page\n";

否则,如果您的开发机器设置为使用 UTF-8 ANSI 代码页,您将面临应用程序被认证为 “在我机器上可用” 的风险。

结论

我希望这篇文章以及随附的代码能表明,在 Windows 程序中使用 UTF-8 编码并不一定非常痛苦。

本系列的下一章是:

  • Tolower or not to Lower(转小写还是不转小写)展示了如何在 UTF-8 中解决大小写转换问题。
  • INI 文件展示了在 Windows INI 文件中使用 UTF-8 的 Windows API 实现。

历史

  • 2023 年 10 月 9 日 - 添加了关于 C++20 和设置 ACP 的部分。
  • 2020 年 8 月 2 日 - 链接到系列中的其他文章,代码已更新。
  • 2019 年 11 月 22 日 - 初始版本。
© . All rights reserved.