在C++程序中,尤其是新手,在涉及到打印/输出字符串的时候,经常莫名其妙遇到乱码或者不输出的问题。
如果是在windows系统上编程,更是如此。不管是CFile还是CStdioFile,莫名其妙就输出不了或者输出的是乱码。
如果只是想要解决输出问题,很简单,很多文章直接让setlocale(LC_ALL, "")、setlocale(LC_CTYPE, "")、setlocale(LC_ALL, "chs")、setlocale(LC_ALL, "zh_CN")等等,反正就是setlocale,最后发现真的可以。可是真的了解具体含义吗?如果软件是跨区域/国家的,一个setlocale可能确实解决了你的这个乱码问题,但是也许会给别人带来灾难。
首先,setlocale属于C语言的函数,在C++中尽量少用,C++针对国际化有专门的解决方案,且更加灵活。
C++的国际化是通过std名称空间下的locale类实现的,其对外接口比较简单,如下:
所有输入输出流,都有其对应的locale,可以通过getloc获取,再调用locale的成员函数name,就可以获取locale的名字。请看代码(vs2019):
#include <iostream>
int main()
{
using namespace std;
cout << "cout:" << cout.getloc().name() << "\n";
cout << "wcout:" << wcout.getloc().name() << "\n";
system("pause");
return 0;
}
执行结果如下:
可以看到,不管是cout,还是wcout,在vs2019编译器里面,其默认的locale都是"C",即与C语言的设置保持一致。但是,上一篇文章中有介绍,wcout输出中文的时候默认是无法输出的,但是cout可以。
不知道是wcout的支持的问题,还是C语言在处理wchar_t的中文时的问题,对C语言了解得不多,不敢乱下结论。
1、locale
看locale的构造函数介绍:
形式有很多种,其中的默认构造函数,取得是当前的全局locale,我们可以通过两行代码获取当前的locale设定:
#include <iostream>
int main()
{
using namespace std;
locale lc_defalut;
cout << lc_defalut.name() << "\n";
system("pause");
return 0;
}
执行结果:
C会根据当前的系统区域设定来判断,如果操作系统是中文的,那么确实可以正确显式中文;但如果不是,直接设置成默认的,肯定会有问题;另外,用系统默认的locale设置到wcout上,也无法正确输出中文。
其他构造函数,还有一个接受"char*"/"string"参数的构造函数,这两个是用得最多的。参数的形式如下:language[_area[.code]]。
_area和.code都是可选的,简单列几个(包含世界所有的国家和地区的来源没找到,此处参考的是另一篇文章:https://blog.csdn.net/templar1000/article/details/20369271):
c Default: ANSI-C conventions (English, 7 bit)
en_US English in the United States
en_AU English in Australia
en_CA English in Canada
fr_FR French in France
fr_CH French in Switzerland
fr_CA French in Canada
ja_JP.jis Japanese in Japan with Japanese Industrial Standard (JIT) encoding
ja_JP.sjis Japanese in Japan with Shift JIS encoding
ja_JP.ujis Japanese in Japan with UNIXized JIS encoding
ja_JP.EUC Japanese in Japan with Extended UNIX Code encoding
zh_CN Chinese in China
zh_TW Chinese in Taiwan
lt_LN.bit7 ISO Latin, 7 bit
lt_LN.bit8 ISO Latin, 8 bit
可以看出来,对于中文,有两种,我们用的一般是"zh_CN",所以想要定一个支持中文的locale,别用系统默认的,而应该直接指定。
如果传递的字符串为空,则返回的也是环境变量中默认的locale;如果传的字符串非法,即不是约定的,则会抛出异常:
总结一下,对于locale来说,有两种方式获取当前默认的区域环境:
(1)用默认的构造函数构造一个lcoale对象;
(2)传入空字符串构造一个locale("")对象。
2、设置locale
C++的locale设置很灵活,可以设置全局的,也可以设置到具体的流对象上。这里建议设置到具体的流对象上,除非你有必须设置到全局的充分理由。
(1)设置全局的
如果有充分的理由需要设置全局的locale,在不使用C语言的setlocle函数的前提下,如何设置呢?
locale类有一个静态的成员函数:globale,如下:
可以看到,调用global,传入一个locale,就可以设置想要的区域环境;但是需要注意,如果参数loc有name,还会设置C的global locale。
因此,在调用global前,一定要记住当前的全局locale是什么,操作结束后再重新设置回来。
看global的返回值,返回的是之前的全局locale,这就简单了,示例代码如下:
#include <iostream>
void PrintWchar()
{
std::wcout << L"output English\n";
std::wcout << L"输出一句中文\n";
std::cout << "now-wcout: " << std::wcout.getloc().name() << std::endl;
std::locale lc_global;
std::cout << "now-global: " << lc_global.name() << std::endl;
}
int main()
{
using namespace std;
std::cout << "before: "<< std::wcout.getloc().name() << std::endl;
locale lc_old = locale::global(locale("zh_cn")); //设置全局locale为中文,并记录返回值,即原来的全局locale
PrintWchar();
locale::global(lc_old); //设回原来的全局locale
std::cout << "after: " << std::wcout.getloc().name() << std::endl;
system("pause");
return 0;
}
执行结果非常有意思,如下:
程序其实非常简单:
先打印wcout里的locale,发现是"C";
调用locale::global函数,传入一个参数为"zh_cn"的locale的临时变量,并定义lc_old接收返回值;
调用PrintWchar函数,函数内部打印一句英文,一句中文,另外,还打印了此刻wcout流中locale值以及此时全局的locale值;结果非常有意思,wcout依然是C,而全局的"zh_cn";
再次调用locale::global,传入参数为刚才接收的返回值lc_old;
打印此时的wcout的locale值,依然是C。
程序从始至终,都没有改变wcout的locale值,但是在设置了全局的locale为"zh_cn"后,wcout就能正常打印中文。
具体的原理,我也不太清楚,似乎很有意思,这里不强行解释。
再次强调一下,设置全局的locale后,一定要记得重新设置回来!
(2)设置到具体的流对象上
标准输出
设置到具体的流对象上,都是调用流的"imbue"函数,接受一个locale的const引用。如下:
#include <iostream>
int main()
{
using namespace std;
locale lc_zh("zh_CN");
wcout.imbue(lc_zh);
wcout << L"中文" << endl;
cout << "wcout:" << wcout.getloc().name() << endl;
cout << "cout:" << cout.getloc().name() << endl;
system("pause");
return 0;
}
执行结果如下:
可以看到,imbue的设置,仅仅影响到了wcout,对cout没有影响。
试了一下,这里locale的参数,可以传:"zh_cn"、"chs"、"chinese"中的任何一个,且不区分大小写。
文件流
如果想要操作文件,用imbue设置到具体的文件流对象上更合适。如下:
#include <iostream>
#include <fstream>
void WriteFile(std::wfstream& fOut)
{
if (!fOut.is_open())
return;
fOut << L"A line English\n";
fOut << L"窗前明月光\n";
fOut << L"疑似地上霜\n";
fOut << L"举头望明月\n";
fOut << L"低头思故乡";
fOut.close();
}
int main()
{
using namespace std;
wfstream wf_not;
wf_not.open(R"(L:\locale_not.txt)", ios_base::out);
WriteFile(wf_not);
wfstream wf_yes;
wf_yes.imbue(locale("zh_cn"));
wf_yes.open(R"(L:\locale_yes.txt)", ios_base::out);
WriteFile(wf_yes);
system("pause");
return 0;
}
执行完程序后,会在L盘下生成连个文件:
打开文件,会发现locale_not里的中文没有输出:
原因很简单,wf_not没有设置locale,wf_yes设置了。
可以看到,这里不需要记录全局的lcoale再重新设置回去。因为这里locale的设置是针对流对象而言的,仅影响这一个对象。
(3)涉及中文输出(包括文件),尽量设置locale
尽管我们开发的软件大部分情况都用于中国,但不能百分百确定一定会是中文操作系统,换句话说,不能百分百确定用户操作系统中的默认环境是中文的;或者,其他人在某个地方把全局的locale设置成了英文等其他环境,忘了设回系统默认的。
如果不是,会出现什么情况呢?请看下面代码:
#include <iostream>
void OtherOp()
{
std::locale::global(std::locale("en_US"));
}
int main()
{
using namespace std;
OtherOp();
cout << "C++ nice! \n";
cout << "C++ 很棒!\n";
system("pause");
return 0;
}
运行结果如下:
如此结果的原因很简单,OtherOp里面把全局的locale设置成了英文环境,但没有设回原来的,导致cout无法识别中文。如果是和别人一起合作项目,而且该项目涉及多个国家和区域,这种情况极有可能发生。
所以,在涉及文件的读写时,尽量设置一下流的locale,以防其他人在别的地方修改了全局locale忘记设回之前的状态。