字符编码详解

第一部分:首先讲下自己遇到的问题

        两边系统都是win7。

用c语言搭建了一个用ftp服务器跟前端来传输文件,默认只能设置为二进制文件。前端文本文件编码格式为utf-8, 前端吧文本文件跟dll等文件压缩成包,然后访问ftp服务器,把文件上传到服务器上。

我这边接收到压缩文件,解压。问题出来了,对于包含中文的文本文件,如果解压成写字板形式,乱码。解压成记事本,不乱码。但是如果把不乱码的记事本内容用vs2015(设置的是多字符集)读到内存,再打印出来又是乱码的了。对于字符编码不清晰的我很是头痛。下面在网上找到了很好的解释。


第二部分:编码详解

最近在写网络数据传输的程序,被各种编码搞的一塌糊涂,在这里简单记录如下:

1. ASCII和Ansi编码

字符内码(charcter code)指的是用来代表字符的内码.读者在输入和存储文档时都要使用内码,内码分为 
a.单字节内码 -- Single-Byte character sets (SBCS),可以支持256个字符编码. 
b.双字节内码 -- Double-Byte character sets (DBCS),可以支持65000个字符编码. 
前者即为ASCII编码,后者对应ANSI。在简体中文的操作系统中ANSI就指的是GB2312,代码页936(ANSI下不同语言有不同的代码页)。

2.GB2312和GBK编码

GB2312是对 ANSI 的简体中文扩展。GB2312共收录了七千个字符,由于GB2312支持的汉字太少而且不支持繁体中文,所以GBK对GB2312进行了扩展,以支持繁体中文和更多的字符,GBK共支持大概22000个字符,GB18030是在GBK的基础上又增加了藏文、蒙文、维吾尔文等主要的少数民族文字。 
代码页(codepage) 就是各国的文字编码和Unicode之间的映射表。例如GBK和Unicode的映射表就是CP936,所以也常用cp936 来指代GBK。

3.Unicode

ANSI有很多代码页,使用不同代码页的内码无法在其他代码页平台上正常显示。由于各国之间的编码不同造成的交流传输不便,ISO 打算废除所有的地区性编码方案,重新建立一个全球性的编码方案把所有字母和符号都统一编码进去,称之为 "Universal Multiple-Octet Coded Character Set",简称为 UCS(ISO10646)。同时又有unicode.org这个组织也制定了自己的全球性编码 unicode,自从unicode2.0开始,unicode采用了与USC相同的字库和字码,阶段主要采用的是 UCS-2/unicode 16 位的编码。

4.UTF编码

UTF(Unicode/UCS Transfer Format),UCS 变长存储的编码方式,主要用来解决 UCS 编码的传输问题的。分为 UTF-7,UTF-8,UTF-16,UTF-32 等。UTF-8是一次传输8位(一个字节)的UTF编码方式,一个字符可能会经过1-6次传输,具体的跟 unicode/UCS 之间的转换关系如下:

unicode(U+) utf-8
U+00000000 - U+0000007F: 0xxxxxxx
U+00000080 - U+000007FF: 110xxxxx10xxxxxx
U+00000800 - U+0000FFFF: 1110xxxx10xxxxxx10xxxxxx
U+00010000 - U+001FFFFF: 11110xxx10xxxxxx10xxxxxx10xxxxxx
U+00200000 - U+03FFFFFF: 111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
U+04000000 - U+7FFFFFFF: 1111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

比如: "我" 的unicode/UCS编码为 "U+6211"(01100010 00010001),在U+00000800 - U+0000FFFF之间,所以采用三字节编码,按规则分段为:0110 001000 010001,再分别替换上表中的x,得到11100110 10001000 10010001,即为 "E6 88 91",这就是 "我" 的UTF-8编码。 
举个有趣的例子: 
在 Windows 的记事本里新建一个文本文件,输入"联通"两个字,保存,关闭,再次打开,会发现文本已经不是"联通"了,而是几个乱码。 
当使用记事本新建文件时,默认的编码是 ANSI,输入中文就是 GB 系列的编码,"联通" 两字的编码为: 
c1 1100 0001 
aa 1010 1010 
cd 1100 1101 
a8 1010 1000 
注意到了吗?第一二个字节、第三四个字节的起始部分的都是 "110" 和 "10",正好与 UTF-8 规则里的两字节模板是一致的,于是再次打开记事本时,记事本就误认为这是一个UTF-8编码的文件,让我们把第一个字节的110和第二个字节的10去掉,我们就得到了"00001 101010",再把各位对齐,补上前导的0,就得到了 "0000 0000 0110 1010",这是 UNICODE 的 006A,也就是小写的字母 "j",而之后的两字节用 UTF-8 解码之后是0368,这个字符什么也不是。这就是只有 "联通" 两个字的文件没有办法在记事本里正常显示的原因。 
而如果你在 "联通" 之后多输入几个其他字,其他的字的编码不见得又恰好是 110 和 10 开始的字节,这样再次打开时,记事本就不会坚持这是一个 UTF-8 编码的文件,而会用 ANSI 的方式解读之,这时乱码又不出现了。

5.UTF-16

UTF-16是一次传输两个字节的UTF编码方式,现如今Unicode/UCS也主要采用16位编码,所以UTF-16的存储方式和Unicode/UCS的编码方式也相同。确切的说是和UCS-2/unicode 16的编码方式相同

6.big endian 和 little endian

在UTF-16或者UCS的编码中经常遇到这两个选项,big endian 和little endian 是CPU处理多字节数的不同方式。例如“汉”字的 Unicode/UCS 编码是 6C49。那么写到文件里时,究竟是将 6C 写在前面,还是将 49 写在前面?如果将 6C 写在前面,就是big endian。还是将 49 写在前面,就是little endian。 
BOM 称为 "Byte Order Mark"。UTF-8 以字节为编码单元,没有字节序的问题。而 UTF-16 以两个字节为编码单元,在解释一个 UTF-16 文本前,首先要弄清楚每个编码单元的字节序。例如收到一个 "奎" 的 Unicode/UCS 编码是 594E,"乙" 的 Unicode/UCS 编码是 4E59。如果我们收到 UTF-16 字节流 "594E",那么这是 "奎" 还是 "乙"? 
在Unicode/UCS编码中有一个叫做 "ZERO WIDTH NO-BREAK SPACE" 的字符,它的编码是FEFF。而FFFE在Unicode/UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符 "ZERO WIDTH NO-BREAK SPACE"。这样如果接收者收到 FEFF,就表明这个字节流是Big-Endian 的;如果收到FFFE,就表明这个字节流是Little-Endian 的。因此字符 "ZERO WIDTH NO-BREAK SPACE" 又被称作 BOM。 
UTF-8 不需要 BOM 来表明字节顺序,但可以用 BOM 来表明编码方式。字符 "ZERO WIDTH NO-BREAK SPACE" 的 UTF-8 编码是 EF BB BF。所以如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8 编码了。Windows 就是使用 BOM 来标记文本文件的编码方式的。

1.三种编码的回顾

Ansi字符串我们最熟悉,英文占一个字节,汉字2个字节,以一个\0结尾,常用于txt文本文件。 
Unicode字符串,每个字符(汉字、英文字母)都占2个字节;在VC++的世界里,Microsoft比较鼓励使用Unicode,如wchar_t。 
UTF8是Unicode一种压缩形式,英文A在unicode中表示为0x0041,英语中这种存储方式太浪费,因为浪费了50%的空间,于是就把英文压缩成1个字节,成了utf8编码;但是汉字在utf8中占3个字节,显然用做中文不如ansi合算,这就是中国的网页用作ansi编码而国外的网页常用utf8的原因。程序中把15.7M大小UTF8格式的txt文件转化为ANSI后,大小仅为10.8M。

2.转换函数

一般情况下,可以通过Windows头文件下的两个函数实现各个类型之间的转换。头文件添加:

#include <Windows.h>

多字节字符集 -> Unicode字符集

复制代码
int MultiByteToWideChar(
  __in   UINT CodePage, // 标识了与多字节关联的一个代码页值
  __in   DWORD dwFlags, // 允许我们进行额外的控制,它会影响带变音符号(比如重音)的字符。但是一般情况下不适用,赋为 0 即可。
  __in   LPCSTR lpMultiByteStr, // 参数指定要转换的字符串
  __in   int cbMultiByte, // 指定要转换串长度的长度(字节数),如果参数值是-1,函数便可自动判断源字符串的长度
  __out  LPWSTR lpWideCharStr, // 指定转换后Unicode版本的字符串内存地址
  __in   int cchWideChar        // 指定 lpWideCharStr 缓冲区的最大长度。
                                // 如果传入0,函数不会进行转换,而是返回一个宽字符数(包括终止字符'\0'),
                // 只有当缓冲区能够容纳该数量的宽字符时,转换才会成功。
);
复制代码

Unicode字符集 –> 多字节字符集

复制代码
int WideCharToMultiByte(
  __in   UINT CodePage,   // 标志了要与新转换的字符串关联的代码页
  __in   DWORD dwFlags,   // 制定额外的转换控制,一般不需要进行这种程度的控制,而为 dwFlag 传入 0
  __in   LPCWSTR lpWideCharStr, // 指定要转换的字符串的内存地址
  __in   int cchWideChar,       // 指出该字符串的长度,如果传入 -1 ,则由函数来判断字符串的长度
  __out  LPSTR lpMultiByteStr,  // 转换后的缓冲区
  __in   int cbMultiByte,       // 指定 lpMultiByteStr 缓冲区的最大大小(字节数),如果传入 0 ,函数返回该目标缓冲区需要的大小
  __in   LPCSTR lpDefaultChar,  
  __out  LPBOOL lpUsedDefaultChar // 宽字符字符串中,如果至少有一个字符不能转换为对应的多字节形式,函数就会把这个变量设为 TRUE 。如果所有字符都能成功转换,就会把这个变量设为 FALSE。 通常将此函数传入 NULL 值。
);
复制代码

只有一个字符在 CodePage 制定的代码页中没有对应的表示时,WideCharToMultiByte 才会使用后两个参数。在遇到一个不能转换的字符时,函数便使用 lpDefaultChar 参数指向的字符。如果这个参数指向为 NULL ,函数就会使用一个默认的字符。这个默认的值通常是一个问号。这对文件操作是非常危险的,因为问号是一个通配符。

3.程序实现

程序的头文件:

复制代码
/*
 *作者:侯凯
 *说明:utf8、unicode、utf8相互转化
 *日期:2013-6-4
*/
#include <iostream>
#include <string>
#include <fstream>
#include <Windows.h> //Windows头文件
using std::string;
using namespace std;
复制代码

ANSI转Unicode

复制代码
void AnsiToUnicode() 
{
    char* sAnsi = "ANSI to Unicode, ANSI 转换到 Unicode";

    //ansi to unicode
    int sLen = MultiByteToWideChar(CP_ACP, NULL, sAnsi, -1, NULL, 0); 
    wchar_t* sUnicode = new wchar_t[sLen];
    //wchar_t* sUnicode = (wchar_t*)malloc(sLen*sizeof(wchar_t));
    MultiByteToWideChar(CP_ACP, NULL, sAnsi, -1, sUnicode, sLen); 

    ofstream rtxt("ansitouni.txt");
    rtxt.write("\xff\xfe",2);//原因参见上一篇——"小尾"字节序方式存储
    rtxt.write((char*)sUnicode, sLen*sizeof(wchar_t));
    rtxt.close();

    delete[] sUnicode; 
    sUnicode =NULL; 
    //free(sUnicode);
}
复制代码

Unicode转ANSI

复制代码
void UnicodeToAnsi() 
{
    wchar_t *sUnicode = L"Convert Unicode to ANSI, Unicode 转换为 ANSI";

    //unicode to ansi
    int sLen = WideCharToMultiByte(CP_ACP, NULL, sUnicode, -1, NULL, 0, NULL, NULL); 
    char* sAnsi = new char[sLen];
    //char* sAnsi = (char*)malloc(sLen);
    WideCharToMultiByte(CP_ACP, NULL, sUnicode, -1, sAnsi, sLen, NULL, NULL); 

    ofstream rtxt("unitoansi.txt");
    rtxt.write(sAnsi, sLen);
    rtxt.close();

    delete[] sAnsi; 
    sAnsi =NULL; 
    //free(sAnsi);
}
复制代码

Unicode转UTF8

复制代码
void UnicodeToUtf8()
{
    wchar_t *sUnicode = L"Convert Unicode to UTF8, Unicode 转换为 UTF8"; 
    // unicode to UTF8 
    int sLen = WideCharToMultiByte(CP_UTF8, NULL, sUnicode, -1, NULL, 0, NULL, NULL); 
    //UTF8虽然是Unicode的压缩形式,但也是多字节字符串,所以可以以char的形式保存 
    char* sUtf8 = new char[sLen];  
    //unicode版对应的strlen是wcslen 
    WideCharToMultiByte(CP_UTF8, NULL, sUnicode, -1, sUtf8, sLen, NULL, NULL); 

    ofstream rtxt("unitoutf8.txt");
    rtxt.write("\xef\xbb\xbf", 3);//原因参见上一篇
    rtxt.write(sUtf8, sLen);
    rtxt.close();

    delete[] sUtf8; 
    sUtf8 =NULL; 
}
复制代码

UTF8转Unicode

复制代码
void Utf8ToUnicode()
{    
    //UTF8 Convert to Unicode, UTF8 转换为 Unicode,用UE十六进制打开“转化为”直接复制过来乱码,用16进制表示
    char* sUtf8 = "UTF8 Convert to Unicode, UTF8 \xe8\xbd\xac\xe6\x8d\xa2\xe4\xb8\xba Unicode"; 
    //UTF8 to Unicode 
    int sLen = MultiByteToWideChar(CP_UTF8, NULL, sUtf8, -1, NULL, 0); 
    wchar_t* sUnicode = new wchar_t[sLen]; 
    MultiByteToWideChar(CP_UTF8, NULL, sUtf8, -1, sUnicode, sLen);

    ofstream rtxt("utf8touni.txt");
    rtxt.write("\xff\xfe",2);
    rtxt.write((char*)sUnicode, sLen*sizeof(wchar_t));
    rtxt.close();

    delete[] sUnicode; 
    sUnicode =NULL;  
}
复制代码

Ansi转换utf8和utf8转换Ansi就是上面2个的结合,把unicode作为中间量,进行2次转换即可。

4.UTF8转ANSI

在网络传输中,我们常常使用UTF8编码,但在程序处理时,我们习惯于ANSI编码,至少目前的VS2010对UTF8码的显示是乱码的。以下函数综合上述程序,实现了txt文件UTF8编码向ANSI编码的转化。

复制代码
//changeTxtEncoding修改字符串的编码  

char* changeTxtEncoding(char* szU8)
{  
    int wcsLen = ::MultiByteToWideChar(CP_UTF8, NULL, szU8, -1, NULL, 0);  
    wchar_t* wszString = new wchar_t[wcsLen];
    ::MultiByteToWideChar(CP_UTF8, NULL, szU8, -1, wszString, wcsLen);
    cout<<wszString<<endl;

    int ansiLen = ::WideCharToMultiByte(CP_ACP, NULL, wszString, -1, NULL, 0, NULL, NULL);  //wcslen(wszString)
    char* szAnsi = new char[ansiLen];  
    ::WideCharToMultiByte(CP_ACP, NULL, wszString, -1, szAnsi, ansiLen, NULL, NULL); 
    delete[] wszString;
    return szAnsi;  
}

void changeTextFromUtf8ToAnsi(const char* filename)  
{  
    ifstream infile;
    string strLine="";
    string strResult="";  
    infile.open(filename);
    infile.seekg(3, ios::beg);
    if (infile)  
    {  
        while(!infile.eof())
        {  
            getline(infile,strLine);  
            strResult+=strLine+"\n";
        }  
    }
    infile.close();
    char* changeTemp=new char[strResult.length()+1];
    changeTemp[strResult.length()]='\0'; //问题记录
    strcpy(changeTemp, strResult.c_str()); //const char*转化char*的方法
    char* changeResult=changeTxtEncoding(changeTemp); 
    strResult=changeResult;  

    ofstream outfile;  
    outfile.open("ANSI.txt");  
    outfile.write(strResult.c_str(), strResult.length());  
    outfile.flush();  
    outfile.close();
    delete[] changeResult;
    delete[] changeTemp;
}
复制代码

问题记录: 
关于字符串的长度a.String类型的length()和size()函数都返回字符串的真实大小,不包括'\0‘ ; 
b.char*类型的strlen()函数也是返回字符串的真实大小,不包括'\0‘ ; 
c.注意,sizeof()函数包含'\0‘ ,如char str[] = “Hello” ;则sizeof (str ) = 6。

第三部分:回答问题

    看到上面的这些知识应该很清晰了。用ftp二进制模式传输utf-8的文本压缩文件。解压成写字板形式,默认是ANsi的,所以会乱码;解压成记事本,解压默认为utf-8的,所以不会乱码。
但是用vs2015设置的多字符集来读取(rb读取)记事本时,又把utf-8变成了ansi,所以乱码。这里就要读取后要把utf-8转成ANsi,也就是对应中文的GBK.
收获,ANsi---->通过cp936也代码--->GB2312(简体)---->GBK(再加上繁体)------>GB18030(再加上少数名族字体),真是层层递进。
  怎么判断一个文本文件是utf-8,前三个字节为定值。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值