你需要知道的编码(乱码)知识

 

你需要知道的编码(乱码)知识

中文在风靡全球的路上如果一定有阻碍,那就是乱码啊。引无数大神尽折腰的编码转换问题,这篇文章就记录下这个问题。

大家知道,计算机是只认识二进制的,如果一个字符变成了我们肉眼可见的乱码时,一定是因为我们给了计算机错误的编码格式导致的。

文件编码

文章开始,我们先说说编程时,我们的保存代码的文件的编码,以VS2008为例,在中文操作系统下,我们的编程文件会被保存为GB2312编码,但凡文件中使用了中文,为了正常显示中文,我们必须使用扩展的编码方式来显示这个文件。GB2312算是最早的中文编码方式了,后面依次出了GBK和GB18030,这些暂且不提,编码参考可以看这里。GB2312使用两个字节表示一个中文字符,而UTF-8则使用3个字节表示一个中文。如果不想看文献,我们可以走一个实验:

#include <iostream>

//this file for coder test
#include <string>
using std::cout ;
using std::string;

int main()
{
string ansiByte = "this";
string chineseByte = "这是中文";
cout << "english lenght:" << ansiByte.length() <<std::endl;
cout << "chinese lenght:" << chineseByte.length() <<std::endl;

return 0;
}

 

分别把以上代码,以GB2312和Utf-8保存后,生成的结果分别为:

english lenght:4
chinese lenght:8

  

english lenght:4
chinese lenght:12

  

以上结果很清楚的证明了以上说法。当然了,英文字符是编码界的一等公民的,所以编码方式变化基本不会影响代码最后的结果。而对于中文,我们就很尴尬了,毕竟string的length函数不可用的情况下,对应的c语言里的strlen函数也是不准确的。这个问题 ,我们在后面说说如何解决。现在,我们还是说回文件的编码问题,因为WINDOWS的文本文件换行格式与LINUX不同的原因,WINDOWS行尾使用\r\n来换行,LINUX行尾使用\n来换行,\r在LINUX就会显示成奇怪的符号。这样的系统级的差异,我们不会扩展,只以windows系统说明后面的问题。

不同的文件编码在转换时,必然会引起乱码问题,这里的乱码是因为错误的字符映射关系导致的。类似的说明网文也是很多的。

这里只说明一点,为使计算机支持更多语言,兼容ASCII码表,在0x00~0x7F之间的字符,依旧是1个字节代表1个字符。通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个非英语字符。像GB2312, BIG5, JIS 等使用ANSI码表的0x80~0xFF范围的 2 个字节来代表一个字符的各种汉字延伸编码方式,统称为ANSI 编码。因此,在简体中文系统中,ANSI编码实际上是指GBK(GB2312或者GB18030).

字符编码

开发者除了需要处理文件的编码导致的乱码外,还需要处理编码过程中,网络传输时的乱码问题。在开发过程中,我们经常遇到TCHAR来表示一个字符字节,众所周知的原因,这里其实隐藏了char和wchar_t类型,即所谓的窄字节 和 宽字节,一个char对应1个8bit,而wchar_t又存在平台差异,windows下是2个8bits,对应一个UTF-16编码字符(Linux下是4个8bit,对应一个UTF-32字符),Windows下的wchar_t定义为 typedef unsigned short wchar_t; /* 16 bits */ 在编码时,我们使用L来表示我们使用的是Unicode编码,但是wchar_t也可以存储其他编码的字符,即如果我们不加L标识的话,这个字符就是我们的编程文件的编码格式,但是在linux下我们无法使用不加L的字符串给wchar_t类型赋值了。来,show me the code:

wchar_t width_t1 = ''; 
wchar_t width_t2 = L''; 
printf( "%0x %0x\n",width_t1,width_t2);

 

文件编码为GB2312时的输出

d6d0 4e2d

文件编码为UTF-8时的输出

e4b8ad 4e2d

由此可见,当使用L修饰的中文后, 因为它已经表明使用了特定的编码方式,所以它在内存中的数值是一致的,不会随着文件的编码而改变,但是不加入L则就与文件的编码格式相关了。现在,我们已经积累了2个随着文件编码方式不同,导致程序输出不同的问题了,这些都是乱码的根源。

乱码回归

为了弄清楚乱码的问题,我们从文件的编码方式,编程方式来说明了潜在的问题,现在,我们来看看可以从哪些方面解决这个问题。

  • 文件编码

为了正常显示中文,我们可以将文件编码设置为GB2312的,但是现在很多IDE把字符编码默认为UTF-8了比如Qt的IDE ,QtCreator。如前所述,UTF-8存储中文时占用的磁盘空间比GB2312更大些,嗯,其实吧,存储空间并不是什么大问题。不同的文件编码其实影响的是我们在做网络数据的传输,我们需要根据不同的编码来转码为UTF-8了。

备注说明下:在linux下,含中文的文件最好保存为UTF-8格式的,使用ANSI编码在编辑时可能会报"converting to execution character set: Invalid or incomplete multibyte or wide character";这需要做很多额外的工作来消除。

  • 使用严格的宽字节

现在,我们假设,文件的编码格式为GB2312,而我们需要使用中文字符串了,使用string char等类型来存储中文字符是完全可以的,那么这里就会涉及到1个中文2个char字节的口诀了,古时候(嗯,那时候),即时现在,很多人都还没玩坏这个口诀。这自然不是武功秘籍,所以,我们需要改变下编码思路,因为,直接使用string存储中文,字符长度等函数都是不准确的。于是,我们使用了严格的宽字节来规避这些问题,VS中可以把字符设置为Unicode,这个设置不是为文件编码准备的,是为编程中的编码准备的。设置完毕后,我们将合理的使用TCHAT等字符类型。当字符设置为Unicode后,这里的TCHAR会被解析为wchar_t,个人其实并不喜欢微软的字符封装,所以更喜欢直接使用C++标准库中的wchar_t及其标准操作函数。show me the code:

wchar_t width_t1[] = "中文1测试起来"; //error C2440: “初始化”: 无法从“const char[14]”转换为“wchar_t []”

 

以上代码说明,vs下没有加L的中文串默认为chat类型,

wchar_t width_t2[] = L"中文2测试起来"; 
printf( "%s \n",width_t2);

 

结果(powershell下显示)

-N噀2

又见乱码,喜不胜收啊,分析下,加了L后,我们的wchar中加入的为UTF-16的字符编码,而我们的显示界面为powershell的字符集为GBK的,把utf-16的字符解析为GBK自然就是乱码了。于是,就涉及到解码了,这里,我们的目标就是把wchar_t类型的字符转为ANSI编码,如果不知道为什么是这样的转换,可以多查看资料,其实,本文前面也略有提及,具体的转换在下节。

wstring wchar1(L"this");
wstring wchar2(L"只有中文");
wstring wchar3(L"中e混合");

std::cout << "e文长度 :" << wchar1.length()<<std::endl;
std::cout << "中文长度 :" << wchar2.length()<<std::endl;
std::cout << "混合长度 :" << wchar3.length()<<std::endl;

 

e文长度 :4
中文长度 :4
混合长度 :4

可见,在宽字节加持下,中英文的长度等总算统一了,所有的字符都是按一个计算,让一个中文是两个英文字符见鬼去吧,我们完全不需要关系在磁盘或者内存中字符的存储方式,因为我们更关心编程的字符串本身。

  • 编码格式转换

在Windows下,编码转换主要是WideCharToMultiByte和MultiByteToWideChar函数在此之前,我们先来了解下代码页的概念,参考这里,知道了codepage的存在,我们就很好理解这两个函数了,这两个函数就是在查表,找到宽字节 到 多字节(ANSI)的对应关系;

show me the code

//函数名中的Unicode实际就是UTF-16,
//需要加头文件 Windows.h
CHAR * UnicodeToAnsi(const WCHAR * lpszStr)
{
CHAR * lpAnsi;
int nLen;

if (NULL == lpszStr)
return NULL;
//查找代码页,先获取宽字节到多字节时需要的byte长度
nLen = ::WideCharToMultiByte(CP_ACP, 0, lpszStr, -1, NULL, 0, NULL, NULL);
if (0 == nLen)
return NULL;

lpAnsi = new CHAR[nLen + 1];
if (NULL == lpAnsi)
return NULL;

memset(lpAnsi, 0, nLen + 1);
//执行映射转换,两次调用同样的函数,获取不同的值,总觉得 哪里不对,不过任务算是完成了
nLen = ::WideCharToMultiByte(CP_ACP, 0, lpszStr, -1, lpAnsi, nLen, NULL, NULL);
if (0 == nLen)
{
delete []lpAnsi;
return NULL;
}

return lpAnsi;
}
char * charansi = UnicodeToAnsi(width_t2);
printf( "%s \n",charansi);
delete []charansi;

 

执行后OK,这一次总算显示正常了。

以上,告诉了你从wchar_t向ANSI转换的过程,那么同理,你可以写出其他的转换函数了,只是,知道为什么要转其实比知道怎么转更重要。

  • 网络通讯使用UTF-8编码

UTF-8的地位是互联网给与的,而Unicode的推广是因为UTF-8,其中缘由自己百度咯。同样的道理,在联网通信时,为了让对方正确识别,我们需要把字符全部转成UTF-8,

前文说过,我们需要使用宽字节来使用中文字符串,所以,这里我们只需要把宽字节的UTF-16转换为UTF-8即可。只有英文的可以不做转换。

另外,本地化编程也是一个大坑,以后继续再填吧。

posted @ 2018-11-18 21:28  Lckfa  阅读(1755)  评论(0编辑  收藏  举报