UTF-16编码,有时也被称为Unicode、宽字符或UCS-2,最早在90年代初被引入。当时人们认为其65000个字符足以涵盖所有字符。然而,除了特殊情况外,UTF-16并不比UTF-8更高效或更易用。实际上,在许多情况下,情况正好相反。在UTF-16中,字符同样具有可变宽度编码(两个或四个字节),计数字符的难度与UTF-8相同。
如果希望在Windows中使用UTF-8编码(应该这么做),并且不想让程序崩溃或出现意外,必须遵循以下规则:
这个包中提供的函数将使生活更加轻松。所有函数都位于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
这似乎有点繁琐且容易出错,所以制作了一个小对象,旨在保存返回值。它具有将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
使用第一个函数时,必须调用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版本。设置隐藏得很深,在三个对话框级别下。
更多信息可以在此处找到:
“可能会问,‘有什么优势?’之前说过应该定义_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代码页,将冒着让应用程序被认证为“在机器上工作”的风险。