只要你和计算机打交道,这些问题可以说是天天会遇到,但是很多人是似懂非懂, 能真正完全理解的人却不多, 下面是个人的一些理解,有错欢迎指正.

最早的计算机只支持ASCII码, 具体来说就是用1个字节(最高位为0, 没有用)表示0到127,总共128个字符, 这样就可以完全满足英语应用的要求了。

后来扩展到欧洲语系,理论上一个字节可以表示256个字符, 欧洲人就把剩余的128个字符(最高位为1)按照自己语言(法语,德语...)的要求扩充应用了起来, 好像也能满足需要。

然后又到了亚洲国家,比如中国,中文汉字有十多万,这剩余的128个字符根本不够我用啊, 怎么办? 于是就有了两个字节的编码,如中文的GBK, GB2312, BIG5等,当然日语,韩语等其他亚洲国家也有自己编码方式。

这就是所谓的多字节编码(MBCS)方式, Win95/98时代只支持这种方式, 那时候处理字符串非常痛苦, 因为它里面有些字符是一个字节表示的,也有一些是多个字节表示的, 比如字符串"你好abc", 里面明明是5个字符,strlen返回长度却是7, 你要正确识别字符个数,可以使用类似_mbslen的API, 但是实际上该API内部会绑定当前的字符集, 不然神仙也识别不了。

要统一解决上面的问题, 需要有一个世界通用的统一编码格式, 那就是UNICODE。
 UNICODE个人感觉分广义和狭义, 广义的UNICODE包括UTF8, UCS2, UCS4, 而狭义的UNICODE(主要是Windows平台)就是指UCS2。
先说UCS2, Windows平台上常说的UNICODE实际上就是指UCS2, 简单来说就是统一用2个字节的编码,表示实际上所有语言的常用字符。
再说UTF8, 有了上面的UCS2,为甚么还有要UTF8? UCS2把任何字符全都编码成2个字节(包括我们常用的英文字符), 这样极大地增加了网络传输和数据存储的开销,于是就有了UTF8。UTF8对英文字符还是1个字节存储,只对其他语言字符用多个字节存储(2-6个字节)。UTF8和UNICODE可以完全对应的相互转换, 具体可以参考这里
为什么还要有 UCS4? UCS2用2个字节,最多也只能表示0xFFFF+1 = 65536个字符, 但是我们仅汉字就有十多万,所以UCS2/UTF8收录的也只是我们一些常用的汉字, 所以我们需要UCS4, 用4个字节表示一个字符,可以表示0xFFFFFFFF+1=4294967296个字符, 它才是我们以后的终极解决方案。

在Windows上不同编码方式的相互转换可以通过WideCharToMultiByteMutiByteToWideChar进行, 它里面WideChar就是指UCS2, 可以看到它这里把UTF8也命名成MultiByte, 是不是有点误导...

下面再谈小大小端(little-endian, big-endian).

计算机是以字节为寻址单位的,这就涉及到字(2个字节), 双字(4个字节)及其他多字节单位 在计算机内如何排布的问题, 这里无非就是2种:低字节在低地址的little-endian和高字节在低地址的big-endian.
如何区分当前系统是哪种类型的大小端? 曾经看到有经验的程序员也以当前的操作系统类型来判断, 实际上系统的大小端和你的CPU架构体系相关联, 比如说X86是小端, PowPC是大端,ARM则是可控制(默认也是小端)。
要判断当前环境是大小端实际上很简单: bool IsLittleEndian()  {  int i=1;  return (*(char *)&i == 1); }
曾经看到公司跨平台的代码没有通过大小端转换,直接通过memcpy某个表示长度的int在客户端之间传送,却没有发生问题, 感觉很奇怪, 最后发现原来公司的所有native客户端(Win, Mac, ios, Android,BlackBerry,Linux)全都是小端。感觉现在大端的应用主要是网络字节序, Java内部全都是大端。
上面的UCS2和UCS4因为都是用多字节表示一个字符, 所以实际上都有大小端的问题,比如分别对应UCS2-LE和UCS2-BE,Windows上的UNICODE实际上是UCS2-LE, UTF8因为是字节流,所以没有大小端的问题。

下面再说一下BOM (Byte Order Mark), 上面说了各种编码方式以及大小端的问题, 那么我们怎么知道某个文本或者数据流是何种编码方式? 
一般来说有3种方法:一种是分本显示指定, 比如web里html头一般会有这么一段"content="text/html;charset=utf-8"; 要不就是大家默认约定,比如自定义的网络数据流内的字符串一般都会用UTF8编码; 还有一种就是用BOM,通过在文件头里填入BOM规定的字节,从而区分文件是何种编码类型: 
UTF-8 0xEF 0xBB 0xBF
UCS2 BE 0xFE 0xFF
UCS2 LE 0xFF 0xFE
UCS4 BE 0x00 0x00 0xFE 0xFF
UCS4 LE 0xFF 0xFE 0x00 0x00
有兴趣的同学可以用notepad保存,试下各种效果, 然后用UltraEdit的16进制方式查看验证。

上面关于各种字符编码方式的集合构成了所谓的字符集/代码页相关的概念, 下面是是一些常用的代码页及其字符集: 
代码页(CodePage)字符集(CharSet)
Description
936
GB2312
简体中文(GB2312)
950
BIG5
繁体中文(BIG5)
1200
UTF-16
Unicode (UCS2-LE)
1201
unicodeFFFE
Unicode (UCS2-BE)
65000
UTF-7
Unicode (UTF-7)
65001
UTF-8
Unicode (UTF-8)
65005
UTF-32
Unicode (UCS4-LE)
65006
UTF-32 (BE)
Unicode (UCS4-BE)

最后讨论下C++编程中常见的关于字符编码方式相关的问题。

在C++编程中, 我们常打交道的无非是编辑器和编译器, 对编辑器起来说,我们常遇到就是乱码问题, 比如中文注释显示或是保存不了等, 解决办法就是把你的文件保存成Unicode(UTF8)。
对于编译器来说, 编码方式取决于它对C++标准的支持程度, 比如C++ 11以前,字符串我们只能指定成2种:一种是MBCS,如char* p="abc哈哈"; 还有一种是UCS2, 比如wchar_t*p = L"abc哈哈", 这样编译器就知道你要表示的字符串类型。C++11之后,标准增加了UTF8和UCS4的支持, 比如char* p=u8"abc哈哈"表示UTF8,wchar_t* p=u"abc哈哈"表示UCS2(实际上和L"xxxx"一样), char32_t* p=U"abc哈哈"表示UCS4。这里要区分编译期和运行期, 尽管C++11之前在编译期我们没法告诉编译器我们这个常量串是UTF8格式的,但是程序运行期我们还是可以使用所有的编码式(MBCS/UTF8/UCS2/UCS4), 因为这些最终在内存里都是二进制流。
另外C++11还增加了UTF8, UCS2, UCS4相互转码的支持:
std::codecvt_utf8 封装了UTF8相关的编码转换
std::codecvt_utf16 封装了UCS2相关的编码转换
std::codecvt_utf8_utf16 封装了UTF8与UCS2的编码转换

对于C++跨平台开发, 我们经常遇到的就是默认用那种编码方式的问题,我们会发现Windows 的UCS2解决方案对其他平台来说是个异类, 一般来说有2种解决方法:
一种是统一用UTF8 , 但是这样对Windows来说有点麻烦, 因为Windows的API都是UCS2的,所以这种方式意味着任何字符串在传给Windows API 之前都要从UTF8转成UCS2; 还有一种就是用#define宏了, Windows上将字符串相关宏全都定义成UCS2, 其他平台则全都定义成UTF8, 该方式要求就你在写代码时头脑要比较清醒,因为同样的代码在不同平台上的编码格式是不一样的。

一直很好奇,谁知道Windows为什么不用UTF8,非要搞得和其他平台不一样?
posted on 2015-07-25 01:11 Richard Wei 阅读(4603) 评论(2)  编辑 收藏 引用 所属分类: C++

FeedBack:
# re: 关于字符集,编码格式,大小端的简单总结
2015-07-25 01:40 | test
因为nt内核用的是ucs2,那是89年,utf8到了92年才发明出来。  回复  更多评论
  
# re: 关于字符集,编码格式,大小端的简单总结
2015-07-25 19:55 | Richard Wei
@test
看来确实是历史原因, 不过对于运行期的程序来说,个人倒是更赞同Windows的做法,忽略内存因素,Windows的UCS2编码相对于其他平台的UTF8更统一和高效。  回复  更多评论
  

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