字符集和字符编码[订正]

原文地址:

http://www.cppblog.com/pansunyou/archive/2010/12/31/charset_encoding.html

这个主题已经被N多人讨论过了,这里仅仅是个人总结,不是教程。

字符集和字符编码

潘孙友 2010-12-31 于遵义

目录
一、字符集
二、字符编码
三、Windows平台
  3.1 Codepage代码页
  3.2 编码转换(API)
  3.3 编码转换(CRT) [感谢@loop指出错误]
四、Linux/unix平台
  4.1 iconv
  4.2 ICU

一、字符集

字符集是一个集合,描述并定义了这个集合中可以出现哪些字符,常见的字符有GB2312、GBK、GB18030、UNICODE等。字符集仅仅是一种规范,一种约定,我们也可以定义自己的字符集。

举例来说,银行IT系统为了字段合法性校验的方便,常内部定义一些小字符集,比如X字符集,N字符集。

x-字符集由以下86个字符组成
a b c d e f g h I j k l m n o p q r s t u v w x y z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9
. , - _ ( ) / = ’+ : ? ! ” % & * < > ; @ #
(cr)(lf) (space)

不同的字符集之间很可能是有交集的,并且包含越多字符的字符集越通用。中国的BG2312,GBK,GB18030是在不同时期,逐步扩展而来的,所以GB18030是前者的超集。现今最大的字符集是UNICODE,几乎包含了世界上所有语言的字符。

二、字符编码

先有字符集才有字符编码,编码是字符集的具体表示方式,一个字符集可以有多种编码方式,只要这种方式可以涵盖字符集中的所有字符。

比如UNICODE字符集的具体编码方式有很多种,utf-8/utf-16/utf-32;而gb2312就只有常见的一种编码方式GB2312(所以有时候人们常将编码与字符集弄混)。可能有人会说,utf-8中包含了gb2312的所有字符,那不就是以utf-8编码表示了gb2312字符集么,事实是这样,但问题人家utf-8编码方式又不是为你gb2312定制的,人家只是顺手把你给表示了。

不同的编码方式,根据其特性应用在不同场合。以UNICODE字符集的编码方式为例,其中utf-8以尽可能少的存储空间存放字符,自动纠错性能好(由于编码的特殊性,其一个字符出错只会影响其后几个字节,而utf16/32等等长字节的就连错一大片了),适合传输及存储;utf-16/32以等字节表示所有字符,在程序内部更容易处理,一般用于系统内部字符格式。

三、Windows平台

3.1 Codepage代码页

Windows平台有所谓的代码页,用于表示字符编码方式,至少支持哪些,要看你的系统里安装了哪些。通过GetACP我们可以获取当前系统的编码方式。

WINBASEAPI UINT WINAPI GetACP(void);

通过以下小段代码,可以获举系统支持的编码方式。
 for(int i=0; i<=65001; i++)
 {
  CPINFOEXA cpinfo;
  if (IsValidCodePage(i)){
   if (GetCPInfoExA(i, 0, &cpinfo))
    printf("%d=[%s]\n", cpinfo.CodePage, cpinfo.CodePageName);
  }
 }

详细的列表见: http://msdn.microsoft.com/en-us/library/dd317756(v=VS.85).aspx
3.2 编码转换(API)

Windows为我们提供了两个函数用于字符编码的转换。

WINBASEAPI
int
WINAPI
MultiByteToWideChar(
    __in UINT     CodePage,
    __in DWORD    dwFlags,
    __in_bcount(cbMultiByte) LPCSTR   lpMultiByteStr,
    __in int      cbMultiByte,
    __out_ecount_opt(cchWideChar) __transfer(lpMultiByteStr) LPWSTR  lpWideCharStr,
    __in int      cchWideChar);

WINBASEAPI
int
WINAPI
WideCharToMultiByte(
    __in UINT     CodePage,
    __in DWORD    dwFlags,
    __in_ecount(cchWideChar) LPCWSTR  lpWideCharStr,
    __in int      cchWideChar,
    __out_bcount_opt(cbMultiByte) __transfer(lpWideCharStr) LPSTR   lpMultiByteStr,
    __in int      cbMultiByte,
    __in_opt LPCSTR   lpDefaultChar,
    __out_opt LPBOOL  lpUsedDefaultChar);

Windows内部应该是完全使用Unicode编码的,系统提供的W后缀的API都接受wchar_t(unsigned short)的字符串,而A系列的API实则是先内部转换为Unicode编码再调用W系统函数。

在windows上做不同编码间的转换,一般是先将原始编码转换为Unicode编码,再将Unicode编码的字符串转换为目标编码。以gbk到utf-8为例:

std::string ctk_gbk2utf8(const char*s)
{
 s = s?s:"";
 std::wstring unicodestr;
 std::string utf8str;
 //gbk转换成utf16
 int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);

 unicodestr.resize(n);
 MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());
 
 //从utf16转utf8
 n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
 utf8str.resize(n);
 WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 );
 
 return utf8str;
}

std::string ctk_utf82gbk(const char*s)
{
 s = s?s:"";
 std::wstring unicodestr;
 std::string gbkstr;
 //utf8转换成utf16
 int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);

 unicodestr.resize(n);
 MultiByteToWideChar(CP_UTF8, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());

 //从utf16转gbk
 n = WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
 gbkstr.resize(n);
 WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, (char*)gbkstr.c_str(), (int)gbkstr.length(), 0, 0 );

 return gbkstr;
}

测试代码:
 const char*gbk  = "这是中文. hello world!";
 printf("gbk=%s\n", gbk);

 string utf8str = ctk_gbk2utf8(gbk);
 printf("utf8str=%s\n", utf8str.c_str());

 string gbkstr = ctk_utf82gbk(utf8str.c_str());
 printf("gbkstr=%s\n", gbkstr.c_str());

如果要处理像日本或者韩国语言编码,只要换一个那个936为相应的代码就可以了。上面的CP_UTF8可以换为65501。一般情况下,使用系统API转换编码就差不多了。

在未经实践的情况下我听轻信了网上其它文章的言论,实际上是错误的:另外,还有c/c++标准库提供的mbstocws, cwstombs函数,但由于在windows上无法处理当前代码页以外的字符集而变得不是那么好用(即便setlocale,也是没用的)。

3.3 编码转换(CRT)

另外我们可以通过c/c++标准库中的mbstocws, cwstombs函数来转码,我们可通过两组函数来测试,一组为Windows API实际的,一组为 mbstocws, cwstombs实现,效果是一样的。

std::string ctk_gbk2utf8(const char*s)
{
 s = s?s:"";
 std::wstring unicodestr;
 std::string utf8str;
 int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);
 unicodestr.resize(n);
 MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());
 n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
 utf8str.resize(n);
 WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 );
 return utf8str;
}

std::string ctk_gbk2big5(const char*s)
{
 s = s?s:"";
 std::wstring unicodestr;
 std::string dststr;
 int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0);
 unicodestr.resize(n);
 MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length());
 n = WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 );
 dststr.resize(n);
 WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, (char*)dststr.c_str(), (int)dststr.length(), 0, 0 );
 return dststr;
}

std::string ctk_gbk2big5_crt(const char*s)
{
 s = s?s:"";
 std::string srcstr = s;
 std::string curLocale = setlocale(LC_ALL, NULL);     
 setlocale(LC_ALL, ".936");
 size_t newSize = srcstr.length() + 1;
 wstring unicodestr;
 unicodestr.resize(newSize);
 wmemset((wchar_t*)unicodestr.c_str(), 0, newSize);
 mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize);
 string newstr;
 newSize = newSize*2 + 1;
 setlocale(LC_ALL, ".950");
 newstr.resize(newSize);
 memset((char*)newstr.c_str(), 0, newSize);
 wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize);
 setlocale(LC_ALL, curLocale.c_str());
 return newstr;
}

std::string ctk_big52gbk_crt(const char*s)
{
 s = s?s:"";
 std::string srcstr = s;
 std::string curLocale = setlocale(LC_ALL, NULL);     
 setlocale(LC_ALL, ".950");

 size_t newSize = srcstr.length() + 1;
 wstring unicodestr;
 unicodestr.resize(newSize);
 wmemset((wchar_t*)unicodestr.c_str(), 0, newSize);
 mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize);
 string newstr;
 newSize = newSize*2 + 1;
 setlocale(LC_ALL, ".936");
 newstr.resize(newSize);
 memset((char*)newstr.c_str(), 0, newSize);
 wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize);
 setlocale(LC_ALL, curLocale.c_str());
 return newstr;
}

四、Linux/unix平台

4.1 iconv

这些平台上,字符编码的转换一般使用iconv,可能是独立的iconv库也可能是glibc自带的版本。在linux/unix上字符编码的问题比windows更容易碰到,所以经常有人喊搭了个ftp传上来的文件名乱码什么的(顺便提一句,包括IE7在内的之前的IE版本直接访问utf8编码的ftp时会有乱码,通过网络抓包工具会发现是IE发送的utf-8字符串是部分错误的)。

暂时只谈程序中的编码问题,先不管locale之类的(虽然也相关)。

自己“封装”的转码函数。

char *ctk_iconv(const char *fromStr, const int fromLen, char**toStr,  const char *fromCode, const char *toCode)
{
 char *buffer;
 iconv_t cd;
 const char *inbuf = NULL;
 size_t inbytesleft = 0;
 char *outbuf = NULL;
 size_t outbytesleft = 0;
 int errorCode = 0;
 int bufferSize=0;
 size_t ret = 0;
 int done = 0;

 if (fromStr==NULL || fromStr[0]=='\0' || fromLen <=0 ) return NULL;
 if (fromCode==NULL || fromCode[0]=='\0' ) return NULL;
 if (toCode==NULL || toCode[0]=='\0' ) return NULL;

 memset(&cd, 0x00, sizeof(iconv_t));
 inbuf = fromStr;
 inbytesleft = fromLen;

 errorCode = 0;
 bufferSize = fromLen*4+1;
 buffer = (char*)malloc(sizeof(char)*bufferSize);
 memset(buffer, 0x00, bufferSize);

 outbuf = buffer;
 outbytesleft = bufferSize;

 if ( (iconv_t)-1  == ( cd = iconv_open(toCode, fromCode) ) ) {
  return NULL;
 } 

 while ( inbytesleft >0 && done !=1 ) {
  ret = iconv(cd, (char**)&inbuf, &inbytesleft, &outbuf, &outbytesleft);
  if ( (size_t)-1  == ret ) {
   errorCode = errno;
   switch(errorCode)
   {
   case EILSEQ:
   {
    if((outbuf<buffer+bufferSize)&&(outbuf>=buffer))
    {
     memcpy(outbuf, inbuf, 1);
     outbuf += 1;
     outbytesleft -= 1;
     inbuf += 1;
     inbytesleft -= 1;
     if ( inbytesleft <= 0 ) break;
    }
   }
   break;
   case EINVAL:
   {
    done = 1;
   }
   break;
   case E2BIG:
   {
    done = 1;
    break;
   }
   break;
   default:
    done = 1;
   }
  }
 }
 if ( NULL != toStr)
  *toStr = buffer;
 iconv_close(cd);
 return buffer;
}

std::string ctk_iconv_gbk2utf8(const char*s)
{
 s = s ? s:"";
 char *utf8str = NULL;
 ctk_iconv(s, strlen(s), &utf8str,  "gbk", "utf-8");
 std::string result("");
 if (utf8str!=NULL)
 {
  result = utf8str;
  free(utf8str);
 }
 return result;
}

std::string ctk_iconv_utf82gbk(const char*s)
{
 s = s ? s:"";
 char *gbkstr = NULL;
 ctk_iconv(s, strlen(s), &gbkstr, "utf-8", "gbk");
 std::string result("");
 if (gbkstr!=NULL)
 {
  result = gbkstr;
  free(gbkstr);
 }
 return result;
}

Iconv在windows上也非常好用。

4.2 ICU

IBM出品的ICU也是编码转换的好手,到处有它的身影,php6就使用它来做内码。由于没有亲身使用经历,就不多说了。

http://www-01.ibm.com/software/globalization/icu/index.html

猜你喜欢

转载自blog.csdn.net/scy1028/article/details/7655485