字符编码及宽字符类型(wchar_t)的跨平台处理

这篇文章的目的是希望你能在看完后对字符的编码和子节相关的东西,以及宽字符类型在不同平台之间的处理能有一个清晰的认识,有出入的地方,感谢指正。


字符编码


“电脑只有二进制,人脑才有乱码”,凡是我们看到的乱码都是由字符编码引起的。如果对于字符编码没有一个清晰的认识,那么各种各样的编码格式在你的脑海里肯定是混乱的。首先,我们知道一个字节是由八个二进制位组成的,用十六进制表示就是0x00,这八个二进制位可以组合成256种不同的状态。最开始计算机只是在美国用的,为了在计算机的终端显示可见的东西,他们就把这8个二进制位所组成的状态约定成一些指定的字符,0x20以下的称为控制字符,如换行,反白之类的。然后他们又把空格啊,标点符号啊,数字字母之类的也指定成了特定的状态表示。这样一直约定到了127号,就算把英文字符编码完了,它们全都是用的一个字节来表示,这些编码就被称为“ascii”。但是当时发明计算机的人也没想到计算机能发展到除了他们美帝以外其他一些国家居然也可以用了,但是各个国家之间的语言字符多种多样,除了“ascii”编码的那些外,还有很多的字符没能在终端显示出来。所以他们继续把127号以后的进行编码,从128一直编到了255,这些又称为“扩展字符”。

 


但是,当计算机进入中国后,由于我们的汉子博大精深,那区区的一两百个状态怎么够啊,所以我们当然也开始了自己的编码指定。因为最开始的考虑不周,如没有考虑到少数名族语言之类的。单汉字编码就有了好几种。“GB2312”:127号之前的字符保持不变,直接废掉了127之后的那些扩展字符,约定两个127号之后的子节组合在一起编码成一个汉字,前面一个字节称为高子节,后面一个字节称为低字节,都是127号之后的子节。“GBK”:前127号保持不变,不再要求两个字节都是127号之后的子节了,只要第一个字节,也就是高子节,是127号之后的子节就行,第二个字节(低字节)不管是不是127号之后的子节都可以。“GB18030”:对“GBK”又进行了扩展,增加了更多的字符表示。这一系列的中文编码又总称位“DBCS“。

 

单中文就出现了这么多的编码格式,在世界范围内来看,其他的国家的情况也差不多。这些各种各样的编码都是你认你的我认我的,根本都没有一个统一的共识,这样的编码信息就无法实现信息的传输和共享了,得,要想认别国的字符,那你得装一套别国的编码系统。在这样的情况下,国际标准化组织(iso)就开始着手解决这个问题了,他们废除了国家地区性的编码,统一制定了一个编码格式,”Universal Multiple-Octet Coded Character Set”,简称UCS, 俗称 “unicode“。

 

unicode有两种格式,UCS2和UCS4,它们采用定长编码,UCS2指定2个字节编码一个字符,UCS4指定4个字节编码一个字符,在这样的约定下,所有国家的字符都采用这样的约定格式来进行重新编码,原有的ascii保持编码格式不变,只是将它扩展成了2个字节或者是4个字节。当然,你可能也发现了,这样的编码指定也存在一些问题,UCS2根本就不能编码出所有的字符,UCS4却可能是文本的长度成倍的增加,因为一些字符本可以用一个字节或者是两个字节就可以编码的。所以基于这样一些原因,而且随着互联网的出现,就有了后来的 UTF-8,UTF-16,UTF-32,它们都是Unicode编码格式的具体实现方式。UTF-8和UTF-16采用变长的编码方式,utf-8约定可以用1-4个字节来表示一个字符,utf-16可以用2个或者是4个字节来编码一个字符。utf-16可以说是ucs2的扩展,而utf-32和ucs4基本相同。关于utf-8的具体编码方式,我建议你可以看一下后面第二个链接阮一峰老师的文章,讲得很清楚。

 

字节序

上面我们说了字符编码,在计算机中还有字节序这个概念了,你肯定也听过大端序列和小端序列这个说法。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节序列中最小的地址,因为不同机器之间处理器和系统的不同,字节序有大端序列(big-endian)和小端序列(little-endian)之分。大端序列指的是在对象的起始地址存储高序序列,小端序列指的是在对象的起始地址存储低序字节。如一个int类型的值0x01234567,用小端序列表示为:67 45 23 01,用大端序列表示为:01 23 45 67. 当然,在上面我们说的字符编码中,utf-8时没有大小端序列之分的。 经测试在Intel处理器上的win7 64位系统,Ubuntu 32位系统,OS X系统均为小端字节序。当然在我们常用的Intel处理器都是采用的小端字节序。可以用下面的代码来测试字节序:

#include <iostream>
union {
short s; //2 bytes
char c[sizeof(short)];
}endian;

int main()
{
endian.s = 0x0102;

if(endian.c[0] == 1 && endian.c[1] == 2)
std::cout << "big endian" << std::endl;
else if (endian.c[0] == 2 && endian.c[1] == 1)
std::cout << "little endian" << std::endl;
else
std::cout << "unknow" << std::endl;

return 0;
}

宽字符类型(wchar_t)的跨平台处理
当我们需要把以前写的Windows程序进行跨平台处理时,如果原来的工程采用的Unicode编码,我想你肯定首先想到了,wchar_t类型在Windows和Linux平台下的大小时不一样的,Windows下采用的是2字节编码一个字符,基于utf-16,Linux下采用的是4个字节编码一个字符,基于utf-32。两个平台下的wchar_t类型sizeof出来的大小不同,那同一份代码进行跨平台处理的时候,会不会出问题呢,这个或许你就有点犯难了。

 

在这里,我们首先应该明确一点,wchar_t类型在Windows和Linux平台下字节大小的不同,对我们程序本身的跨平台性没有任何影响,你Windows下是怎么处理wchar_t的,那么在Linux下就怎么处理,相应的接口和操作都不用改变。不会对数据的读取产生错误。但是有一点,这些是基于这样一个事实的,就是你没有在两个平台之间对不同平台下产生的文件进行读取。如我目前所做的项目中,在代码实现上,需要把wchar_t类型的一些数据输出到文件保存,然后在后续的代码中进行读写。在不同平台下产生的这个文件,wchar_t字符的编码方式肯定是不一样的,所以不能把Windows下生成的文件,直接拿到Linux下面来进行读写,如果这样做,那么读写错误是肯定会发生的。还有一点,不能贸然的添加gcc 编译项 -fshort-wchar,强制将Linux平台下的wchar_t指定成两个字节,因为这样做,只会改变你在代码中自己实现的部分,而内部库或者是第三方库中用到的接口和函数都是没有变的,仍然采用的是4字节编码。如,std::wstring, QT中的QString等。

 

对于这点,在项目中我拟定了两个方案,方案一是在代码中读写文件的部分,写入文件的时候,把wchar_t类型的数据转成utf-8的编码格式来保存,读取的时候把utf-8编码的数据读出来后再转成平台对应的wchar_t字符,两个平台下都采用同样的解决办法。在windows下可以采用系统函数WideCharToMultiByte()和MultiByteToWideChar()来进行转换,如下面把宽字符转成UTF-8的列子:

#include < windows.h >

std::string to_utf8(const wchar_t* buffer, int len)
{
int nChars = ::WideCharToMultiByte(
CP_UTF8,
0,
buffer,
len,
NULL,
0,
NULL,
NULL);
if (nChars == 0) return "";

string newbuffer;
newbuffer.resize(nChars) ;
::WideCharToMultiByte(
CP_UTF8,
0,
buffer,
len,
const_cast< char* >(newbuffer.c_str()),
nChars,
NULL,
NULL);

return newbuffer;
}

std::string to_utf8(const std::wstring& str)
{
return to_utf8(str.c_str(), (int)str.size());
}

而在Linux下同样采用系统函数iconv()来转换,具体用法下面会提到,这里先省略。当然,如果你现在项目中的编译器支持C++11那就好办了,而我现在windows上的编辑器还是vs2008,C++11在语言上提供了对utf-8的支持,提供了一些新的类型和函数来处理,如下:
// convert UTF-8 string to wstring
std::wstring utf8_to_wstring (const std::string& str)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
return myconv.from_bytes(str);
}

// convert wstring to UTF-8 string
std::string wstring_to_utf8 (const std::wstring& str)
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> myconv;
return myconv.to_bytes(str);
}

因为目前项目需要兼容以前Windows下生成的数据文件,所以最后采用了方案二来做,方案二的办法是保持Windows下wchar_t类型的数据读写方式和编码都不变。只在Linux下面做文章,Linux下的数据读写需要根据Windows下wchar_t的编码来进行,也就是,在Linux下写入wchar_t类型的数据的时候,需要先把Linux下的wchar_t的编码格式转成Windows下的wchar_t编码格式,读的时候同理,需要先把读出来的Windows下的wchar_t数据,转成Linux下的wchar_t数据,这样就能实现两个平台下的数据读写,而且夜兼容了以前的数据文件。在Linux下的转换函数也采用的是iconv(),在头文件<iconv.h>中。iconv()的用法如下:
int encodingConvert(const char *tocode, const char *fromcode,
char *inbuf, size_t inlength, char *outbuf, size_t outlength)
{
#ifndef _WIN32

char **inbuffer = &inbuf;
char **outbuffer = &outbuf;

iconv_t cd;
size_t ret;
cd = iconv_open(tocode, fromcode);
if((size_t)cd == -1)
return -1;
ret = iconv(cd, inbuffer, &inlength, outbuffer, &outlength);
if(ret == -1)
return -1;
iconv_close(cd);
#endif

return 0;
}

注意iconv()函数在处理的时候,传进来的inbuffer应该传一个副本,因为iconv()函数在处理的时候会改变inbuffer,iconv()是对inbuffer中的字符一个一个的进行转换,然后保存到outbuffer中。可以用类似下面的代码来做具体的处理,主要传进去的编码需要指定具体的大端序列还是小端序列,因为Linux下默认的好像是大端的,所以指定详细点总没有错的。
//把wchar_t数据写到文件中, 文件中的数据都是以windows下的格式来存放。
int WriteWstringToBuffer(const std::wstring &wstr, void *buffer)
{
int inLength = (wstr.size() + 1) * sizeof(wchar_t);
int outLength = inLength / 2; //从Linux下写入wchar_t的数据到文件的长度
char *inBuffer = new char[inLength]();
memcpy(inBuffer, wstr.c_str(), inLength);
char *outBuffer = new char[outLength]();

int ret = EncodingCovert("UTF-16LE", "UTF-32LE", inBuffer, inLength, outBuffer, outLength);

memcpy(buffer, outBuffer, outLength);

delete[] inBuffer;
delete[] outBuffer;

return (ret == -1)?ret:0;
}

//从文件中读取wchar_t数据, 文件中的数据都是以windows下的格式来存放。
int ReadWstringFromBuffer(std::wstring &wstr, void *buffer, int bufLength)
{
int inLength = bufLength;
int outLength = inLength * 2;
char *inBuffer = new char[inLength]();
memcpy(inBuffer, buffer, inLength);
char *outBuffer = new char[outLength]();

int ret = EncodingCovert("UTF-32LE", "UTF-LE", inBuffer, inLength, outBuffer, outLength);

wstr = outBuffer;

delete[] inBuffer;
delete[] outBuffer;

return (ret == -1)?ret:0;
}

在进行这样的转换处理后,Linux和Windows下就能互相交换和读写wchar_t类型的数据了,而不用担心数据的读写错误。


字符编码部分参考文章:

ASCII、Unicode、GBK和UTF-8字符编码的区别联系

字符编码笔记:ASCII,Unicode和UTF-8
————————————————
版权声明:本文为CSDN博主「Willib」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/willib/article/details/51583044

posted @ 2019-09-20 09:23  aij  阅读(1471)  评论(0编辑  收藏  举报