只要你和计算机打交道,这些问题可以说是天天会遇到,但是很多人是似懂非懂, 能真正完全理解的人却不多, 下面是个人的一些理解,有错欢迎指正.
最早的计算机只支持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上不同编码方式的相互转换可以通过WideCharToMultiByte和MutiByteToWideChar进行, 它里面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在客户端之间传送,却没有发生问题, 感觉很奇怪, 最后发现原来当前公司的所有客户端(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 |
UTF-16 BE | 0xFE 0xFF |
UTF-16 LE | 0xFF 0xFE |
UTF-32 BE | 0x00 0x00 0xFE 0xFF |
UTF-32 LE | 0xFF 0xFE 0x00 0x00 |
有兴趣的同学可以用notepad保存,试下各种效果, 然后用UltraEdit的16进制方式查看验证。
最后讨论下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,非要搞得和其他平台不一样?