C++ ifstream 读入 utf8 txt 变为乱码如何解决?为什么会变为乱码?

本篇介绍了这个问题的原理分析以及解决策略,需要耐心阅读并理解透彻。
切忌没有耐心,对 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 的值,你会发现都变为了所谓的乱码
终端中显示这些字符ANSI
其中只有英文保持正确的显示,其余都变掉了。

阶段提示

在中文互联网平台上查询该问题解决方案,你会发现大多数文章都会让你进行转码到 GBK / ANSI。

这个方法是局限的,并会引发很多潜在问题。

明明可以显示正常了,为什么不能这样操作?在下方会有详细分析。


解决问题

首先我们需要知道控制台打印出来的文字会以 ANSI 的方式呈现,ANSI 是你本地的字符集,对于中文操作系统那么就是 GBK(CP-936) 等。

而我们创建的 txt 不是 utf8 类型的吗?那么读入到 string 中的也就是 utf8 编码。
仔细想想,将 utf8 打印到 ANSI 环境的窗口中会发生什么?当然是乱码。
所以读入的 utf8 并没有乱码,只是打印导致了乱码。

那我们该如何正确显示 utf8 文本呢?


方案一:改变控制台编码环境

我们可以将控制台的编码环境改为 utf8 就可以正确显示了!需要在 main 函数开头加上

SetConsoleOutputCP(CP_UTF8);

现在就正常了
终端中显示这些字符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);
}

我们可以得到下面的结果
终端中显示这些字符GBK
这不是还是乱码吗?不,仔细观察,中文是不是正确了,但是其他国家的文字依然是乱码。这也就是上面我所提到的局限性。

分析

我们前面说过,转换到的 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_twstring 等,这样不用考虑乱码问题了。

还是读入刚刚那个 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;

终端中显示这些字符UTF16
轻轻松松就正确了,并且在支持任何 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 库,来达到更好的国际化本地化支持。


最后,非常感谢能与你分享本篇文章的相关知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值