Windows API 字符编码转换以及一些解释和心得

我在解决乱码上面实际走了不少弯路,做了很多实验,查了很多资料。在这里做下笔记,希望后来者可以明白,少走些弯路。

从最熟悉的两种字符编码说起

除了一些旧的、没有考虑到兼容性的网页还在用gbk做编码外,大部分的网页都已经用utf-8做编码了。但是最令人头疼的是,windows的控制台是很不好显示utf-8的。有明君为我大C++写了两个函数,是正确的、好用的(除了用std::string做返回值让我等效率党有点觉得不爽之外……还是挺方便的).

#include <string>
#include 
<windows.h>
using std::string;

//gbk 转 utf8
string GBKToUTF8(const string& strGBK)
{
    
string strOutUTF8 = "";
    WCHAR 
* str1;
    
int n = MultiByteToWideChar(CP_ACP, 0, strGBK.c_str(), -1, NULL, 0);
    str1 
= new WCHAR[n];
    MultiByteToWideChar(CP_ACP, 
0, strGBK.c_str(), -1, str1, n);
    n 
= WideCharToMultiByte(CP_UTF8, 0, str1, -1, NULL, 0, NULL, NULL);
    
char * str2 = new char[n];
    WideCharToMultiByte(CP_UTF8, 
0, str1, -1, str2, n, NULL, NULL);
    strOutUTF8 
= str2;
    delete[]str1;
    str1 
= NULL;
    delete[]str2;
    str2 
= NULL;
    
return strOutUTF8;
}

//utf-8 转 gbk
string UTF8ToGBK(const string& strUTF8)
{
    
int len = MultiByteToWideChar(CP_UTF8, 0, strUTF8.c_str(), -1, NULL, 0);
    unsigned 
short * wszGBK = new unsigned short[len + 1];
    memset(wszGBK, 
0, len * 2 + 2);
    MultiByteToWideChar(CP_UTF8, 
0, (LPCTSTR)strUTF8.c_str(), -1, wszGBK, len);

    len 
= WideCharToMultiByte(CP_ACP, 0, wszGBK, -1, NULL, 0, NULL, NULL);
    
char *szGBK = new char[len + 1];
    memset(szGBK, 
0, len + 1);
    WideCharToMultiByte(CP_ACP,
0, wszGBK, -1, szGBK, len, NULL, NULL);
    
//strUTF8 = szGBK;
    std::string strTemp(szGBK);
    delete[]szGBK;
    delete[]wszGBK;
    
return strTemp;
}

这玩意儿不跨平台,因为它用到了windows api。我之所以把它放到跨平台编程上面来,是因为字符编码这东西只有到跨平台的时候才显得坑爹。


接着我是不是要介绍那俩函数一下?

int MultiByteToWideChar(
  _In_       UINT CodePage, 
/*代码页是Windows下字符编码的叫法,gbk是936,utf-8是65001,CP_ACP是ANSI*/
  _In_       DWORD dwFlags, 
/*选项标志,转换类型,设0就行了*/
  _In_       LPCSTR lpMultiByteStr, 
/*多字节字符串*/
  _In_       
int cbMultiByte, /*字符串要处理的长度,如果是-1函数就会处理整个字符串*/
  _Out_opt_  LPWSTR lpWideCharStr, 
/*输出的宽字符串缓存,如果为空就返回需要的宽字符串长度*/
  _In_       
int cchWideChar /*宽字符串缓存的长度,当然如果宽字符串为空,这个设0就可以了*/
);

int WideCharToMultiByte(
  _In_       UINT CodePage,
  _In_       DWORD dwFlags,
  _In_       LPCWSTR lpWideCharStr,
  _In_       
int cchWideChar,
  _Out_opt_  LPSTR lpMultiByteStr,
  _In_       
int cbMultiByte, /*前面的基本与MultiByteToWideChar都相同,就不解释了*/
  _In_opt_   LPCSTR lpDefaultChar, 
/*填0即可*/
  _Out_opt_  LPBOOL lpUsedDefaultChar 
/*填0即可*/
);

这两个函数分别是将多字节字符串转换为宽字符字符串 和 将宽字符字符串转换为多字节字符串(在此处晕倒的童鞋们我没有对不起你们……是M$那家伙对不起你们)。我早就说过Windows API 的界面不友好,这么多不知道干嘛吗用的参数,全部填0就对了。要是iconv(),它貌似只有4个参数,这才是好的榜样。


宽字符?多字节?

这是Windows给它们起的名字,让人摸不着头脑。

  • 宽字符:就是Unicode。它雷打不动地用2个字节(0x0000 - 0xFFFF),表示所有我们平常能见到的字符,具体的表格见:http://unicode-table.com

  • 多字节:就是除了Unicode外其他的。我们熟悉的gbk, utf-8, big5,统统归入多字节。

宽字符之所以叫做宽字符,是因为它是一个宽一点的字符。那什么是短字符……就是ascii了,1个字节1个字符绝对够短,而且只能表示256个西欧字符。宽字符呢,是2个字节1个字符。宽一点,但还是可以识别到一个字符是哪里的。而多字节呢,就是它在计算机里表示成多个字节,但是没有办法识别那里到那里是一个字符。

我不喜欢这两个函数的命名。如果按照Python的命名,MultiByteToWideChar 应该叫 decode(解码),WideCharToMultiByte 应该叫 encode(编码)。


所以呢?

如你所见,多字节无法准确识别字符的长度,处理起来就会很麻烦。而宽字符大多时候虽然比多字节多耗费一点空间,但是处理起来方便。比如正则表达式处理,引擎是基于字符去匹配的,宽字符可以两个字节两个字节跳着匹配,而多字节就会匹配错误。

比如有一个词“程序”=0xB3CCD0F2(gbk),我想匹配“”=0xCCD0(gbk),正则库会替我把中间那两个字节匹配了。用在C里用wchar_t,C++里用std::wstring,我们可以很准确的,无错误地匹配到我们想要的子串,因为引擎在迭代的时候是逐字(而不是逐字节)进行比较的。

1 >>> str1 = ""
2 >>> str2 = "程序"
3 >>> print re.findall(str1, str2)
4 ['\xcc\xd0']
5 >>> print re.findall(str1.decode("gbk"), str2.decode("gbk"))
6 []

所以在处理字符串的时候,但凡要处理中文,要先把用户给的字符串解码成Unicode。处理完之后显示出来或者保存,再编码成需要的charset。


Appendix

在不同的地方用不同的编码:

  • 网络文本(如网页)传输一般用utf-8,因为有少量中文,而大部分是英文。
  • 在保存为本地文件的时候,应该保存为Unicode,因为本地存储资源丰富,且可以节省时间,实时解码毕竟也是O(N^2)啊。
  • 显示出来应该用系统的编码,中文Windows为gbk,繁体Windows为Big5,Linux一律为UTF-8。
  • 源代码里的少量中文串尽量用"\x????\x????"来表示,如果有大量中文建议用gettext或者资源之类的以外挂的方式读入。
  • Qt内部使用Unicode,所以编写Qt应用时显示文字直接传递宽字符串即可。
  • NTFS的文件名、路径都是用GBKUTF16LE编码的,所以如果Windows下用户输入的是路径就无需解码了。


posted on 2013-10-28 22:49 Shihira 阅读(7054) 评论(8)  编辑 收藏 引用 所属分类: 跨平台编程

评论

# re: Windows API 字符编码转换以及一些解释和心得[未登录] 2013-10-29 12:45 jacky

string作为返回值也没什么大问题,现代编译器(返回值优化,右值引用)都能优化掉多余的拷贝。  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2013-10-29 13:52 Shihira

@jacky
谢谢提醒,即将更正。  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得[未登录] 2013-10-30 10:07 春秋十二月

那两个函数和我代码库中的差不多哈  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2013-11-08 10:38 海边泡沫

楼主还是没有理解宽字符和多字符的本质。
它们的本质绝对不是为了储存,而是为了沟通。
从储存的角度讲,我们可以用一个字符表示一个字母,也可以用两个字节表示一个字母和汉字,也可以用三个、四个字符。所以不管是UNICODE也好,还是GBK、BIG5也好,它们没有本质的区别,它们的本质都是固定宽度的。而从沟通的角度讲,只要知道这些固定宽度的字节串的采取的是什么编码,就可以相互转换。
从储存的角度讲,既然都是固定宽度的,我们其实不仅可以用wchar[]来储存UNICODE,也可以用wchar[]来储存BIG5或GBK。而事实上,除了UNICODE,Windows中其它的所有编码储存都是用char[],包括UTF-8。这也是为什么WINDOWS中的转换函数叫MultiByteToWideChar了,这一组函数意义很明确,不存在设计问题。
那么还有用char[]的必要吗?难道这只是历史遗留问题吗?当然不是了。这也是我要说的沟通的本质了。
从沟通的本质讲,我们在计算机中和网络上所传输的、所比较的,一切的本质都是字节流,严格来说,是只用到了最低7位,低8位为0的字节流,也就是unsigned char[]。所以网络上广范使用UTF-8不是因为它省空间,而是因为它是字节流,是最适合传输的表达字符的编码方案。
UTF-8是宽度不固定的。我认为微软的专家早就认识到了这个本质,所以不管一种编码的宽度是否固定,他们都把他们叫MiltiByte,而只有UNICODE叫WideChar。
最后再说一点,从沟通的角度讲,不仅字符是字节流,数字也是字节流,而且在传输数字的时候还要考虑字节序。这也是hton和ntoh这样一组函数存在的原因。  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2013-11-09 16:41 红色代码

windows下的字符转换确实够坑爹的  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2013-11-26 01:12 放屁阿狗

iconv why not ?  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2014-03-04 17:07 huxi

"NTFS的文件名、路径都是用GBK编码的,所以如果Windows下用户输入的是路径就无需解码了。"
貌似NTFS的路径是UTF16LE编码,
http://zh.wikipedia.org/wiki/NTFS

文件系统支持最长 32767 个 Unicode 字符的的路径[57]。但每个路径组成部分(目录或文件名)最多只允许包含 255 个字符[57],同时某些特定的名称需要用于保存 NTFS 将元数据,因此禁止用作普通的文件/目录名。NTFS 的元数据都存放在卷的根目录下,因此上文所述的文件名要求也仅限于根目录。被 NTFS 保留的名称有:$MFT、$MFTMirr、$LogFile、$Volume、$AttrDef、.(点)、$Bitmap、$Boot、$BadClus、$Secure、$Upcase,以及 $Extend[2]。在这些项目中,.(点)和 $Extend 是目录类型,其它项目均为文件类型。  回复  更多评论   

# re: Windows API 字符编码转换以及一些解释和心得 2014-03-27 13:50 Shihira

@huxi
It makes sense. 谢谢提醒,已更正.  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

公告

留言簿(2)

随笔分类

搜索

最新随笔

最新评论

阅读排行榜

评论排行榜