在看这篇文章之前,最好先看一下上一篇文章:文字显示1——字符与编码
C++字符串与字符编码
目前大部分编程语言字符串默认使用的是Unicode,比如Java,C#,JavaScript等等,大部分情况不需要考虑字符编码问题。在C/C++中char为一个字节,最初是用于存储ASCII字符,当然也可以存储中文字符,对于中文字符需要两个char才能存储一个字符。C++中的char也可以用来存储其他编码的字符,因为char可存储多种编码的字符,所以我们必须明确知道字符char的编码类型,否则显示时容易出现乱码。
C++中除了char以外,还有wchar_t,char16_t,char32_t,各种字符类型对于的字符如下:
char可以表示ASCII,GBK,ISO-8859,UTF-8等等。
wchar_t用于表示Unicode,基本包括所有的常用字符,也就是Unicode第0平面中的字符。
char16_t用于表示UTF-16,大部分情况下与wchar_t相同。
char32_t 用于表示UTF-32
有些地方会提到UCS编码,UCS和Unicode编码完全一致,可以认为两者为同一个编码。UCS-2和UFT-16类似,UCS-4和UTF-32等价。
C++中可以通过在字符串前面添加一定的标识(字面量)来规定字符的编码,如L”abc”,代表wchar_t类型的字符串。以下是所有字符类型的字符串:
char cs[] = "abc中文";
|
我们可以将上面代码中的字符串字节值打印出来,得到以下结果:
这个是在VS2015上运行的结果,完整程序代码地址。
其中‘中’字的GBK编码是D6D0,Unicode编码是4E2D,UTF-8编码是E4B8AD
其中‘文’字的GBK编码是CEC4,Unicode编码是6587,UTF-8编码是E69687
从结果可以看到wchar_t,char16_t,char32_t中Unicode编码子节顺序是反的。这是因为VS2015中采用小端存储,即底位字节在前面。
当然也有大端存储的方式,对于采用UTF-16/UCS-2编码的文本文件,文件开头会有两个BOM字节如果是FFFE则文件是采用小端存储,如果是FEFF文件则是采用大段存储。如下图,显示了一个只有‘中’字的文本文件的字节。
BOM即byte order mark,有的UTF-8文件也会有BOM,但标准的UTF-8文件是不需要BOM的,UTF-8加BOM只是Windows平台的习惯。UTF-8的字节顺序是固定的,UTF-8的BOM本身并不是来标记大段小端,而是用来标记当前文本是否是UTF-8编码存储。UTF-8的BOM为3个字节EF BB BF,如果读取文件时,文件以EF BB BF3个字节开头,则代表当前文件采用的时UTF-8编码。如下图:
以上文件的字节截图是通过Nodepad++的HexEdit插件显示的,插件下载地址。
控制台显示文字
这里讲的是windows控制台。
在命令行窗口中输入chcp命令回车,可以查看当前命令行窗口的使用的编码表示符号,如下图:
这里的936指的就是GBK编码,中文操作系统中一般默认都是这个编码,其他编码的表示符号可以在这个网站查看。
从以上网站中可以看到UTF-8的编码是65001,我们也可以通过命令修改当前控制台窗口的编码,如下图:
网站中列举的所有代码,windows并不是全部都支持,例如 utf-16的1200就不支持。
控制台默认使用的编码可通过控制面板设置,设置方法。
C++控制显示的编码
C++中的四种字符char、wchar_t、char16_t、char32_t,只有char、wchar_t可以直接输出到控制台,char使用cout输出,wchart_t通过wcout输出。
当char *s= “abc中文”时,s字符串的编码为系统默认的编码,也就是系统设置的默认编码;当char *s= u8”abc中文“时,s字符串的编码为UTF-8。wchar_t的字符串只能表示UTF-16/UCS-2编码。
C++中可以通过函数locale::global设置当前程序要输出到控制台使用的编码,这个函数只接受一个locale对象作为参数。以下是三种locale对象
locale locC("C"); //默认编码
locale locutf8 = locale("zh_cn.utf8");//UTF-8编码
locale locZH("zh"); //中文编码GBK
当设置了Local对象后系统在显示前会将字符串转换为控制台使用的编码。如下代码,完整程序代码地址:
char *str = "abc中文";
char *u8str = u8"abc中文";
wchar_t *wstr = L"abc中文";
locale locC("C"); //默认编码
locale locutf8 = locale("zh_cn.utf8");
locale locZH("zh");
locale::global(locC);
cout << "LocC locale:\n";
cout << "str: " << str << endl;
cout << "u8str: " << u8str << endl;
wcout << L"wstr: " << wstr << endl;
wcout.clear();
cout.clear();
cout << "\n" << endl;
locale::global(locutf8);
cout << "UTF-8 locale:\n";
cout << "str: " << str << endl;
cout << "u8str: " << u8str << endl;
wcout << L"wstr: " << wstr << endl;
wcout.clear();
cout.clear();
cout << endl;
locale::global(locZH);
cout << "locZH locale:\n";
cout << "str: " << str << endl;
cout << "u8str: " << u8str << endl;
wcout << L"wstr: " << wstr << endl;
wcout.clear();
cout.clear();
cout << endl;
输出结果
从结果中可以看出,默认编码locC只能输char默认编码的字符,UTF-8的字符和wchar_t的字符显示不正常。UTF-8编码的情况下UTF-8和wchar_t的字符显示正常。
控制台乱码
控制台乱码一般会有两种情况,1是当前程序输出的字符编码与控制台不符合。2是控制台当前编码中不包含程序的字符,这种情况下字符会以“?”显示。如下:
程序代码
system("chcp 1252");//将编码设为iso 8895-1 西欧字符
cout << "abc中文" << endl;
cout << "abcÖÐÎÄ" << endl; //西欧字符,编译会有warning C4566
输出结果
以上程序是在中文操作系统上编译的,程序默认编码是GBK,以上代码文件在保存时需要选择UTF-8编码,否则无法保存。
虽然代码文件以UTF-8保存,但是程序默认编码还是GBK,因为程序中包含了西欧字符,字符无法转换到程序用的GBK,所以此时会出现warning C4566,说字符串无法转换到GBK编码。
因为程序输出的GBK编码的字符,以上结果中当前控制台编码时1252西欧字符,编码不匹配,所以第二行出现乱码,第三行的乱码是因为当前显示的编码页中不包含相应的字符,这个不包含是因为编译时字符无法转换到GBK,程序直接转换成了“?“。
字符串排序
如果仔细查看GBK和Unicode的编码,可以发现GB2312(GBK)中汉族编码顺序是根据拼音来的,而Unicode编码顺序是按照偏旁部首来的。如下图:
在C++中字符串比较是通过比较字符的编码大小,因为GB2312和Unicode编码顺序不一致,这会导致两者的字符串排序结果也不一致。例如:对于以下字符进行排序排序:
char *gbkstr[] = {"一", "二", "三"};
char *utf8str[] = { u8"一", u8"二", u8"三" };
wchar_t *char16str[] = { L"一", L"二", L"三" };
分别使用strcmp/wcscmp和转成string类用“<”比较,结果如下,完整代码地址:
一、二、三的GBK/GB2312编码分别为D2BB、B6FE、C8FD;
一、二、三的Unicode编码分别为4E00、4E8C、4E09;
编码转换
UTF-8与UCS-2之间的转换,因为两者转换有一定规律,所以转换也比较简单,C++11原生支持两者之间的转换,但C++17中弃用了。
UTF-8、UTF-16、UTF32之间的转换可以使用ConvertUTF库(地址),这个库也很小,只有两个文件几百行代码。
Unicode与其他编码之间的转换
在win32平台中这种转换叫做宽字节与多字节之间的转换,可以通过系统函数WideCharToMultiByte/ MultiByteToWideChar完成。WideCharToMultiByte是Unicode的UTF-16与其他编码转换,MultiByteToWideChar则是相反的转换。
对于其他平台可以通过libiconv库完成,地址,当然这个库也可以支持windows上使用,官网上没有提供VS平台的编译,github上有对于的编译版本,地址。