UTF-8编码在Windows程序中的应用

UTF-16编码,有时也被称为Unicode、宽字符或UCS-2,最早在90年代初被引入。当时人们认为其65000个字符足以涵盖所有字符。然而,除了特殊情况外,UTF-16并不比UTF-8更高效或更易用。实际上,在许多情况下,情况正好相反。在UTF-16中,字符同样具有可变宽度编码(两个或四个字节),计数字符的难度与UTF-8相同。

如果希望在Windows中使用UTF-8编码(应该这么做),并且不想让程序崩溃或出现意外,必须遵循以下规则:

  • 在编译程序时定义_UNICODE宏(或在Visual Studio中选择使用Unicode字符集)。
  • 仅在API函数调用的参数中使用wchar_t或std::wstring。在其他地方使用char或std::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); 内部转换使用WideCharToMultiByte和MultiByteToWideChar函数。

还有用于计算UTF-8字符串中字符数量的函数(length()),检查字符串是否有效(valid()),以及在字符字符串中前进指针/迭代器的函数(next())。

几乎所有其他函数都是围绕传统C/C++函数或结构的包装器: 目录操作函数:mkdir、rmdir、chdir、getcwd; 文件操作:fopen、chmod、access、rename、remove; 流:ifstream、ofstream、fstream; 路径操作函数:splitpath和makepath; 环境访问函数:putenv和getenv; 字符分类函数:is...(isalnum、isdigit、isalpha等)。

所有这些函数的参数模仿标准参数。然而,对于某些函数,如access、rename等,返回类型是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(wpath.data ())); GetTempFileName (wpath.c_str(), L"ABC", 1, const_cast(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函数将字符串转换为UTF-8。

有两个函数用于访问和转换UTF-16编码的程序参数:get_argv函数返回一个argv样式的命令行参数指针数组: int argc; char **argv = utf8::get_argv (&argc); 第二个函数直接提供一个string向量: std::vector argv = utf8::argv (); 使用第一个函数时,必须调用utf8::free_argv函数以释放为argv数组分配的内存。

C++20标准增加了一个额外的类型char8_t,旨在保留UTF-8编码的字符,以及一个字符串类型std::u8string。通过将其与char和unsigned char分开,委员会还创建了一些不兼容性。例如,以下片段将产生错误: std::string s {"English text"}; //这是可以的 s = {u8"日本語テキスト"}; //"Japanese text" - 错误 必须将其更改为类似这样的东西: std::u8string s {u8"English text"}; s = {u8"日本語テキスト"}; 在看来,通过引入char8_t类型,委员会违背了UTF-8编码的原则。编码的目的是将char类型的使用扩展到额外的Unicode代码点。它非常成功,现在已经成为整个互联网上使用的事实上的标准。即使是一直使用UTF-16编码的Windows,现在也在慢慢转向UTF-8。

在这种情况下,使用char数据类型来保存字符串编码似乎不合时宜。特别是,与char或unsigned char实体的算术计算只是用例的一小部分。标准应该尝试简化最常见情况的使用,将不常见的情况留给复杂性负担。

按照这个原则,会想这样写: std::string s {"English text"}; s += " and "; s += "日本語テキスト"; 隐含的假设是所有char字符串都是UTF-8编码的字符字符串。

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

从Windows版本1903(2019年3月更新)开始,微软允许将UTF-8设置为系统ANSI代码页(ACP)。这允许使用API函数的-A版本而不是-W版本。设置隐藏得很深,在三个对话框级别下。

  • 转到设置 > 时间和语言 > 语言和区域,然后选择管理语言设置:
  • 在下一个对话框中,选择更改系统区域设置:
  • 最后,选中Beta:使用Unicode 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文本。

智者之言:应该检查Windows是否设置为使用UTF-8代码页,使用GetACP函数: if (GetACP() != 65001) std::cout << "Warning! not using UTF-8 ANSI code page\n"; 否则,如果开发机器设置为使用UTF-8 ANSI代码页,将冒着让应用程序被认证为“在机器上工作”的风险。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485