字符编码、字符存储、字符转换及工程中字符的使用
版本控制
版本 |
时间(北京时间) |
作者 |
备注 |
V1.0 |
2016-05-13 |
施小丰 |
创建本文、第七章工程总结尚未完成 |
|
|
|
|
一、 前言
1. 目的
本文主要用于整理字符相关知识,包括字符编码、字符存储、行业标准、文件读写、工程注意事项等涉及字符相关的内容,
从而在实际工程中更好地设计和使用字符、更快地解决字符问题。
2. 适用范围
本文标题是“Windows C++字符编码、存储、转换大全”,
但“第三段.行业标准“属于概念总结,不涉及具体的编程语言和平台,是通用的知识,读者无需编程基础,但需要一定的计算机基础。
第四段到第七段仅适用于Windows C++ VC环境,需要读者具备一定的C++开发基础。
3. 开发环境及辅助工具
操作系统: Windows7 64bit中文版
IDE: VS2008+Sp1
WinHex, 下载地址http://www.winhex.com/winhex/index-m.html
Notepad++:下载地址https://notepad-plus-plus.org/
4. 声明
本文由施小丰发表于http://www.smallgui.com/,任何人以任何形式转载时,请确保本文完整,包括本节权利声明。
二、 常见问题
在使用Windows、C++、VC编程时,经常遇到以下个问题
1. 函数调用时参数字符编码不匹配
比如提示cannot convertparameter 3 from 'LPCWSTR' to 'const char *'
2. 存储或解析字符后是乱码
比如程序存储数据至文本文件后,使用Notepad打开发现是乱码。
3. 网络请求时由于字符问题导致乱码
比如发送http请求时,由于编码格式不对,对方服务器无法正确解析数据导致没有服务器正确执行预期操作
三、 字符理论
面对字符编码,我们不禁要问,怎么会产生字符编码问题,为什么不能统一字符编码?那就得先看下字符编码的历史了。
1. 标准ASCII码
计算机内的信息本质上都是二进制信息,即只有0和1两种状态,则此时字符本身,比如'A'需要用多个二进制位表示。
所以为统一各种字符的二进制值,美国国家标准委员会(American National Standard Institute,ANSI)于上个世纪60年代制定了叫做ASCII码(AmericanStandard Code for Information Interchange,美国标准信息交换码。)的字符编码。
标准ASCII码表使用8个二进制,最高位统一为0,所以实际使用7个低二进制位,从而规定了128个字符(2的7次方)的编码,见本文附件1.ASCII码表。
(这里其实来到了计算机行业一个恒久不变的坑:“够了”,比如这里的ASCII码表,比如当时的IP地址,比如比尔盖茨先生的“640K ought to be enough for anyone”,比如千年虫问题。。。)
2. 非标准ASCII码
纯英文使用ASCII码或许够了,但到非英语国家时,显然128个字符不够用,于是很多国家都充分利用起最高位来引入新的符号,
但每个国家对于扩展位并没有统一,于是出现”130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。”。
到目前为止ASCII已经不够用了,必须设计新的编码方式,但新编码方式至少需要解决六个问题
(1)0-127各国都一样,都是标准ASCII码,新编码方式需要兼容
(2)如何统一现有的所有字符
(3)如何在兼容原有字符的基础上,能在未来扩展新字符
(4)新编码是否能够不显著增加小子集字符国家的复杂度,比如对纯英语国家,128种字符够用,完全用全新的多个字节的编码方式,太浪费了。
(5)如何知道一个文件的编码格式和存储方式(大端小端),从而使用正确解析文件内容
(6)如何确保在网络传输过程中双方的编码和解码一致
3. Unicode、UCS2、UCS4
为了解决上述六个问题,有两个国际组织试图设计Unicode,分别是ISO(国际标准化组织)和Unicode.org(软件制造商协会),ISO开发ISO 10646项目,Unicode开发Unicode项目。
从1999年的Unicode3.0开始,Unicode项目和ISO10646项目的字库和字符已经一样,所以我们可以认为他们是一样的。
Unicode的目的是给符号进行编码(注意不是存储,也不是传输),目前常用的实现是UCS2,使用两个字节(16位)来表示一个字符,如汉字“施”的Unicode编码为\u65bd(可用在线工具http://tool.chinaz.com/tools/unicode.aspx转换)
,其中\u表示这是一个unicode编码(实际可忽略这个\u,仅仅为了区分而标记),其编码为十六进制的65bd。
了解了其形式,我们看看针对上面提出的六个问题,Unicode是如何解决的:
(1)标准ASCII码在Unicode编码中没变,比如字母A在ASCII中为0x41,在Unicode中的UCS2(实际上我们一般直接称UCS2为Unicode)实现中为\u0041,只是高位被用0填充了,所以第一个问题解决了
(2)Unicode为每个字符定义了一个唯一的编号,且字符对应的编码确定好以后,不再更改(至少低位不更改,高位可能由于后续扩展会统一填充),第二个问题也解决了
(3)ASCII码因为只有8个二进制位,所以不够用,而Unicode标准本身并没有规定具体多少个字节,
但真正实现Unicode则必须考虑这个问题,现在实际用的最多的是用两个字节(即16位,共有65536钟表示)表示一个符号,这种实现方式称做UCS2。UCS2实际上是Unicode的一个子集,
也有用四个字节表示一个字符的称作UCS4,所以未来如果忽然增加了很多新字符,则可以设计UCS8、UCS16等实现,且低位兼容UCS2和UCS4即可,第三个问题也解决了
(4)(5)(6)三个问题涉及到编码的具体存储和协商方式,详见第四小节4.UTF8、UTF16, 第五小节5.大端小端, 第六小节6.codepage和charset
4. UTF8、UTF16
UTF8或者UTF16的出现,本质上是解决上面6个问题中的第4个,即如果所有的字符都用相同字节的编码来表示,那么对于小字符集国家而言,这个存储太浪费空间了(可能当年的存储成本很高导致)。
这里以UTF-8为例,UTF-8是对Unicode编码格式(一般平时所说的Unicode等同于UCS2)的一种存储实现。UTF-8和UCS2的转换关系如下所示
UCS2编码(16进制) UTF8字节流(二进制) 备注
0000-007F 0xxxxxxx 其实就是标准ASCII码
0080-07FF 110xxxxx 10xxxxxx
0800-FFFF 1110xxxx 10xxxxxx 10xxxxxx
以汉字“施”为例,其unicode编码为65bd,也即Unicode二进制代码为
110010110111101
则转换成UTF8编码时,使用上述二进制代码从右往左依次填充1110xxxx 10xxxxxx 10xxxxxx中的x,左边不足部分使用0填充,得到其UTF8编码二进制值为
11100110 10010110 10111101
转换成16进制,即为E6 96 BD,我们在记事本中写入施,然后另存为UTF8,格式,再使用WinHex观察,前三个字节EF BB BF属于BOM,后面三个字节就是汉字“施”的UTF8编码,好了,新的问题又出现了,BOM是什么?
5. 大端、小端、BOM、零宽度非换行空格
汉字“施”的Unicode编码(再次提醒,其实一般所说的Unicode编码就是指UCS2编码)是65bd,占用两个字节,
于是就存在两种存储方式,
大端:65在高位的存储方式就是大端
小端:65在低位的存储方式叫做小端
为了区分当前存储或传输方式到底是大端还是小端,Unicode
BOM:字节顺序标记,FF FE
零宽度非换行空格:我们可以理解成BOM的一个组成部分
通过在windows中的Notepad++中写入字符保存为不同的格式,然后用WinHex打开可以得到不同编码的字节顺序标记(本表仅在Windows平台下测试过)
编码 |
BOM(十六进制) |
备注 |
ASCII |
无 |
相当于txt另存为ASCII |
UTF-8 |
EF BB BF |
相当于txt另存为utf8 |
UTF-8无BOM |
无 |
相当于UTF-8去掉文件头BOM |
小端 |
FF FE |
相当于txt另存为Unicode |
大端 |
FE FF |
相当于txt另存为Unicode big endian |
上节中提到的我们存储的汉字“施”就是使用UTF8编码,其中前面三个字节就是BOM。
但是这个BOM其实是Windows的“特产”,所以存储UTF8时,最好不要带BOM。网上这篇文章http://www.cnblogs.com/findumars/p/3620078.html对于UTF8应不应该带BOM讲的比较清楚。结论是UTF-8尽量不要带BOM。
那么新问题又来了,假如我们在文件中只是存储字符的UTF8码,而没有BOM信息的话,我们在解析文件时,如何确定编码方式,尤其在网络上传输信息,比如html的时候,浏览器如何确定编码方式?
6. 文件编码和charset
(1)文件编码
第一种情况是纯文本文件,且没有BOM信息,这种情况下从纯软件的角度来讲其实只能猜测尝试编码方式;可以按照http://blog.csdn.net/turingo/article/details/8136644这篇博文实现。
第二种情况是纯文本文件,有BOM信息,这种情况下从通过BOM信息判断文件的编码格式。
第三种情况是文件本身已经描述了其编码方式,比如标准xml在声明中需要表明其本身的编码方式,一般形式如下:<?xml version="1.0" encoding="utf-8"?>,其中encoding表明文件本身是用utf-8编码的。
(2)网络传输
第一种情况是自定义的socket的话,需要双方自己协商确定编码格式。
另外一种是标准协议或语言,比如html。Html标记语言有一个属性charset,这个属性在html中一般是head节点的第一个子节点,这样浏览器解析html时,会先用默认的编码格式读取一部分html数据(比如GB2312),如果读取到当前html的charset后,与当前默认的编码格式不同,则浏览器使用charset中指定的编码重新读取html并显示。下面我们来验证一下。
使用Chrome浏览器打开百度首页后,右击查看网页源代码后获取的百度首页的html内容,
我们右击另存到本地计算机上后,使用Notepad++打开该文件,然后再使用Chrome打开,字符显示正常(样式会由于css样式表确实导致走样)
然后将其中的utf-8修改为GB2312
再刷新刚打开的页面,发现字符显示已经是乱码
所以对于网页或者传输而言,你要知道你传输的字符或者文件的编码格式,然后由你自己告诉对方你的编码格式,如果你告诉别人的编码方式和实际文件的编码格式不一致,那么结果很可能是两个字:乱码。
四、 常用字符编码标准
ID |
标准名称 |
作用 |
备注 |
1 |
标准ASCII |
特指0-127共128个字符 |
只适用于纯英语环境 |
2 |
Unicode |
为世界上每个字符分配一个编码 |
只规定了编号,未规定存储和传输方式 |
3 |
UCS2 |
Unicode编码的一种实现 使用两个字节表示一个字符 |
实际是Unicode的一个子集 通常所说的Unicode就指UCS2 |
4 |
UTF8 |
Unicode编码存储的一种实现 字节可变 |
|
5 |
GB2312 |
|
又称GB2312-80 |
6 |
GBK |
|
|
五、 字符定义总结
在C++体系中,字符类型本质上只有char和wchar_t两种,其他的要么只是封装,比如string封装了char数组,wstring封装了wchar_t数组,要么是为了兼容不同的语言或组件,比如CString是MFC框架下的字符串,_bstr_t是COM组件中的字符串。下表列出了常用的字符类型。
分类 |
ID |
名称 |
备注 |
标准C++ |
|
char |
单字节 |
|
wchar_t |
宽字节,在string.h中定义 实际是unsigned short 需使用wcs前缀的函数处理wchar_t wchar_t* myStr=L"测试"; |
|
|
string |
|
|
|
wstring |
|
|
VC |
|
CHAR |
等同于char |
|
WCHAR |
等同于wchar_t |
|
|
TCHAR |
根据_UNICODE宏,确定当前表示的是ANSI还是Unicode 所以应配合_T使用初始化字符串 且应使用以_tcs为前缀的函数 |
|
|
_T TEXT _TEXT |
三个作用一样, 根据环境决定是ASCII还是Unicode |
|
|
L |
将字符串变成Unicode |
|
MFC |
|
CString |
MFC字符串 |
COM相关 |
|
OLECHAR |
不同环境下自动为 WCHAR或CHAR |
|
BSTR |
是一个有长度有前缀和null结束符的OLECHAR数组 是COM中默认的字符串格式 |
|
|
_bstr_t |
是对BSTR的封装的类 实际是一个智能指针 为实现和LPCSTR和BSTR才有的 |
|
|
CComBSTR |
ATL中的类 是对BSTR类型的分装 |
|
|
VARIANT |
可变类型 |
|
|
_variant_t |
是对VARIAN的封装,类似于_bstr_t对于BSTR的封装 |
|
|
COleVariant |
是对VARIANT的封装 |
其它类型见WinNT.h头文件:
typedef WCHAR *PWCHAR,*LPWCH, *PWCH;
typedef CONST WCHAR *LPCWCH, *PCWCH;
typedef _Null_terminated_WCHAR *NWPSTR, *LPWSTR, *PWSTR;
typedef _Null_terminated_ PWSTR *PZPWSTR;
typedef _Null_terminated_ CON