本篇介绍了这个问题的原理分析以及解决策略,需要耐心阅读并理解透彻。
切忌没有耐心,对 string 和 编码 略知一二,日后将导致巨大麻烦!
问题引入
我有一份 unicode(utf8) 的 txt 文稿
hello你好مرحباHallķဟယ်လိုΓεια σαςЗдравствыйте안녕하세요
这包含了许多国家的文字,并通过下列代码将所有内容读入 string
ifstream ifs(L"input.txt", ios::binary); // ios::binary 是为了保证编码不变
string content((istreambuf_iterator<char>(ifs)), istreambuf_iterator<char>());
// 上面一行代码表示将 txt 的所有内容都读入到 string 中
// 或者书写 ifs >> content; 肯定也会出现乱码问题
ifs.close();
cout << content << endl;
如果你此时打印 content 的值,你会发现都变为了所谓的乱码
其中只有英文保持正确的显示,其余都变掉了。
阶段提示
在中文互联网平台上查询该问题解决方案,你会发现大多数文章都会让你进行转码到 GBK / ANSI。
这个方法是局限的,并会引发很多潜在问题。
明明可以显示正常了,为什么不能这样操作?在下方会有详细分析。
解决问题
首先我们需要知道控制台打印出来的文字会以 ANSI 的方式呈现,ANSI 是你本地的字符集,对于中文操作系统那么就是 GBK(CP-936) 等。
而我们创建的 txt 不是 utf8 类型的吗?那么读入到 string 中的也就是 utf8 编码。
仔细想想,将 utf8 打印到 ANSI 环境的窗口中会发生什么?当然是乱码。
所以读入的 utf8 并没有乱码,只是打印导致了乱码。
那我们该如何正确显示 utf8 文本呢?
方案一:改变控制台编码环境
我们可以将控制台的编码环境改为 utf8 就可以正确显示了!需要在 main 函数开头加上
SetConsoleOutputCP(CP_UTF8);
现在就正常了
并且这样做在任何编码环境的电脑中都可以得到一样的显示效果。
弊端? 现在只是显示正确了,但是 string 本身依旧是 utf8 编码的。如果与 Win32 API 交互或使用其他需要 ANSI 编码的函数,比如修改窗口标题等等,依然是乱码。因为 Win32 API 接受的是 ANSI 编码,所以需要按照方案二进行转码。
相关问题
现在通过这个方案是可以正确的显示 utf8 文本了,但是如果这样打印出来的依旧是乱码
cout << "你好" << endl;
为什么?因为代码文件一般是使用 ANSI 进行存储的,那么这里的 string 就是 ANSI 编码的,将 ANSI 编码的文字打印到 utf8 中肯定会乱码。
cout << u8"你好" << endl;
你可以尝试这样做,在前面加上一个 u8
让编译器帮你转码成 utf8 编码,就可以正确显示了。(注:C++20 及以后已经改变了这个写法,需要去网络搜索相关内容)
方案二:转码到 ANSI
我们可以通过下面的函数来解决
string Utf8ToAnsi(string str)
{
// CP_UTF8 就表示 UTF8 编码
size_t siz = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, NULL, 0);
wchar_t* strSrc = new wchar_t[siz + 1];
MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, strSrc, siz);
// CP_ACP 就表示运行这个程序的计算机的 ANSI 编码
siz = WideCharToMultiByte(CP_ACP, 0, strSrc, -1, NULL, 0, NULL, NULL);
char* szRes = new char[siz + 1];
WideCharToMultiByte(CP_ACP, 0, strSrc, -1, szRes, siz, NULL, NULL);
string res(szRes);
delete[] strSrc;
delete[] szRes;
return string(res);
}
我们可以得到下面的结果
这不是还是乱码吗?不,仔细观察,中文是不是正确了,但是其他国家的文字依然是乱码。这也就是上面我所提到的局限性。
分析
我们前面说过,转换到的 ANSI 是当前运行这个程序的计算机的本地字符集编码,在我的电脑上就是中文 GBK(CP-936),所以说我的 ANSI 只能正确的显示中文字符和国家通用字符,并不能显示其他国家的文字。
那么如果避免这个问题呢?请继续往下看。
警示
一些人可能会认为,我做的程序只会在中文的操作系统上运行,还不如直接转为 GBK 得了?
你有没有想过,中文的操作系统中也可能出现其他国家的文字。比如刚才 txt 的内容,就像方案二所展示的问题。
或者用户可能会将文件夹的名称写为俄语,比如 Папка
。那么你的程序即使在中文环境下,通过 Win32 API 依然打不开这个俄语文件夹中的 txt,别忘了你的 ANSI 是 GBK(CP-936),这个标准之外的字符都会发生乱码。也就是说你提供的路径乱码了。
UTF8 string 不能用于 Win32 API
还是一个原因,Win32 API 和控制台一样,接受的是 ANSI,必须转码。但是也回出现像方案二一样的问题。具体编码取决于系统的代码页设置。例如,在中文操作系统上是 CP-936,在俄语操作系统上是 CP-1251。
编码不能混用 必须对应
Win32 API 和一些第三方库会要求你传入 ANSI 编码,我们必须进行转码到它要求的,或参阅建议中的解决办法。
建议
我们会发现,这里面有一个巨大的矛盾,就是 utf8 虽然是 unicode,支持国际化语言,但是多字符的程序依赖于本地化的 ANSI。也就是说在 Win32API 下使用 string 进行相关交互,比如修改标题,查询文件等等,都会需要使用 ANSI 编码。
这对程序国际化来讲是个巨大的隐患,会引发诸多的问题。
最典型的就是明明在你的电脑上都一切正常,但是到了俄罗斯朋友中,中文变乱码。
原因:你的 ANSI 不是他的 ANSI。
你的是 中文 ANSI(CP-936)
他的是 俄文 ANSI(CP-1251)
那就没有改进过后的更好的办法了吗?当然有!
Welcome wchar_t
and wstring
!
我们不妨可以将上面所以的 string 改为 wstring,而 Win32 API 也提供 utf16 版本的,刚好就是宽字符,则我们可以使用 wchar_t
和 wstring
等,这样不用考虑乱码问题了。
还是读入刚刚那个 utf8 txt,这次我们转而使用下面的代码
_setmode(_fileno(stdout), _O_U16TEXT); // 开启控制台 UTF16 支持
wifstream wif(L"input.txt", ios::binary);
wif.imbue(locale(wif.getloc(), new codecvt_utf8<wchar_t>)); // 这行我们将 UTF8 内容转为 UTF16 并存入 wstring
wstring content((istreambuf_iterator<wchar_t>(wif)), istreambuf_iterator<wchar_t>());
wif.close();
wcout << content << endl;
轻轻松松就正确了,并且在支持任何 Windows 电脑上都不会发生乱码了(前提是你安装了正确的字体资源)
同时你还可以将 txt 改为 utf16 LE
_setmode(_fileno(stdout), _O_U16TEXT);
wifstream wif(L"input.json", ios::binary);
wif.imbue(locale(wif.getloc(), new codecvt_utf16<wchar_t, 0x10ffff, little_endian>));
wstring content((istreambuf_iterator<wchar_t>(wif)), istreambuf_iterator<wchar_t>());
wif.close();
同样也可以得到正确的结果
如果不知道 txt 的编码该怎么办?
这就不是这篇文章所介绍的范围了,你的程序需要有识别 txt 编码的能力。可以使用 International Components for Unicode 库,来达到更好的国际化本地化支持。
最后,非常感谢能与你分享本篇文章的相关知识。