c++ string/char* 字符编码转换 (utf8,chs,gbk...)

本文用于介绍不同编码格式的string(char*)之间的转换。明确Unicode,UTF-8,string,wstring概念,以及locale name之前, 先简单了解两个概念

  • 字符集: 为每一个字符(asic,中文,日文,俄文等)分配一个唯一的ID(又称码位)。
  • 编码规则:将码位转换为字节序列的规则(编码/解码的过程)

由于UTF-8使用广泛,以utf-8编码为例,介绍其与其它编码方式的流程。

UTF-8

广义的Unicode的一个标准,定义了一个字符集以及一系列的编码规则,即 Unicode 字符集和 UTF-8、UTF-16、UTF-32 等等编码……

UTF-8(8位元,Universal Character Set/Unicode Transformation Format)是针对Unicode的一种可变长度字符编码。它可以用来表示Unicode标准中的任何字符。会将一个码位编码为 1 到 4 个字节(理论最多6个字节):

  • 1字节 一个US-ASCIl字符(Unicode范围由U+0000~U+007F)。
  • 2字节 带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文等字母(Unicode范围由U+0080~U+07FF)。
  • 3字节 其他语言的字符(包括中日韩文字、东南亚文字、中东文字等)包含了大部分常用字,使用3字节编码。
  • 4字节 其他极少使用的语言字符使用4字节编码。
Unicode/UCS-4 bit数 UTF-8 byte数 备注
0000 ~ 007F 0~7 0XXX XXXX 1 ASIC 码
0080 ~ 07FF 8~11 110XXXXX 10XXXXXX 2
0800 ~ FFFF 12~16 1110 XXXX 10XXXXXX 10XXXXXX 3 以上基本定义范围:0~FFFF
1 0000 ~ 1F FFFF 17~21 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX 4 Unicode6.1定义范围:0~10 FFFF

上表是Unicode中任意字符使用utf-8编码的规则:如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的字节数,其余各字节均以10开头。

例如「知」的码位是 30693,记作 U+77E5(30693 的十六进制为 0x77E5),其对应的UTF-8 编码为字节序列 E79FA5。

根据上表中的编码规则,之前的「知」字的码位 U+77E5 属于第三行的范围,使用三个3个字节编码,将0x77E5的二进制位从地位开始放入编码模板的

       7    7    E    5    
    0111 0111 1110 0101    二进制的 77E5
--------------------------
    0111   011111   100101 二进制的 77E5
1110XXXX 10XXXXXX 10XXXXXX 模版(上表第三行)
11100111 10011111 10100101 代入模版
   E   7    9   F    A   5

这就是将 U+77E5 按照 UTF-8 编码为字节序列 E79FA5 的过程。反之亦然。

判断字符串是否为utf-8编码

字符串可能存储在std::string或者char*中,前者实际是 char 内存的一个封装,后续说明全部以std::string为例。

了解utf-8编码规则,首先可以根据每个字节的最高比特位判断其为单字节多字节编码,若为0,则可能是单字节,还需继续判断。 若为1,则需要查看当前是几个字节,且后续字节的前两位必须是10。直到所有字节判断结束。

bool IsTextUTF8(const std::string& str)
{
	char nBytes=0;//UFT8可用1-6个字节编码,ASCII用一个字节
	unsigned char chr;
	bool bAllAscii = true; //如果全部都是ASCII, 说明不是UTF-8
 
	for(int i=0; i < str.length();i++)
	{
		chr = str[i];
		
		// 判断是否ASCII编码,如果不是,说明有可能是UTF-8,ASCII用7位编码,
		// 但用一个字节存,最高位标记为0,o0xxxxxxx
		if( (chr&0x80) != 0 )
			bAllAscii= false;
 
		if(nBytes==0) //如果不是ASCII码,应该是多字节符,计算字节数
		{
			if(chr>=0x80)
			{
				if(chr>=0xFC&&chr<=0xFD)   nBytes=6;
				else if(chr>=0xF8)         nBytes=5;
				else if(chr>=0xF0)         nBytes=4;
				else if(chr>=0xE0)         nBytes=3;
				else if(chr>=0xC0)         nBytes=2;
				else{
					return false;
				}
				nBytes--;
			}
		}
		else //多字节符的非首字节,应为 10xxxxxx
		{
			if( (chr&0xC0) != 0x80 ){
				return false;
			}
			nBytes--;
		}
	}
 
	if( nBytes > 0 ) //违返规则
		return false;
	
	if( bAllAscii ) //如果全部都是ASCII, 说明不是UTF-8
		return false;
	
	return true;
}

不同编码的std::string转换

c/c++修改字符集locale name可能会影响系统正在运行的有关字符编解码的程序,所以使用C++11的std::wstring_convert配合std::codecvt模板类。

  • std::codecvt:编码转换特性类,用在wstring_convert的模板参数中来指定使用哪种编码。
  • std::wstring_convert:转码器,接收一个类似codecvt描述编码转换特性的模板参数,用于将本地化的宽字符wstring和指定编码的字节化string进行互转。

所以编码A和B互转的实现方式就是:借助本地化宽字符串,先将以A编码的string转为本地化的wstring,再将本地化的wstring转为B编码后的string。

示例一:

这里给一个windows下,GBK string转UTF8 string的例子:
首先将GBK string转wstring

const char* GBK_LOCALE_NAME = ".936"; //GBK在windows下的locale name
std::string gbk_str {"\xCC\xCC"};  //0xCCCC,"烫"的GBK码

//构造GBK与wstring间的转码器(wstring_convert在析构时会负责销毁codecvt,
// 所以不用自己delete)
std::wstring_convert<codecvt_byname<wchar_t, char, mbstate_t>> 
// 或 std::wstring_convert>
	cv1(new std::codecvt<wchar_t, char, mbstate_t>(GBK_LOCALE_NAME)); 
wstring tmp_wstr = cv1.from_bytes(gbk_str);

再将wstring转为UTF8 string

std::wstring_convert<codecvt_utf8<wchar_t>> cv2;
std::string utf8_str = cv.to_bytes(tmp_wstr);

转码就完成了。utf8_str里的内容应该是"\xE7\x83\xAB"(烫的UTF8)。

转换的完整代码

std::string StringToUTF8(const std::string& gbkData)
{
    const char* GBK_LOCALE_NAME = "CHS";  //GBK在windows下的locale name(.936, CHS ), linux下的locale名可能是"zh_CN.GBK"

    std::wstring_convert<std::codecvt<wchar_t, char, mbstate_t>>
        conv(new std::codecvt<wchar_t, char, mbstate_t>(GBK_LOCALE_NAME));
    std::wstring wString = conv.from_bytes(gbkData);    // string => wstring

    std::wstring_convert<std::codecvt_utf8<wchar_t>> convert;
    std::string utf8str = convert.to_bytes(wString);     // wstring => utf-8

    return utf8str;
}

示例二:

例如在window下,将utf8编码转换为CHS编码的代码为

std::string UTF8ToString(const std::string& utf8Data)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>> conv;
    std::wstring wString = conv.from_bytes(utf8Data);    // utf-8 => wstring

    std::wstring_convert<std::codecvt< wchar_t, char, std::mbstate_t>>
        convert(new std::codecvt< wchar_t, char, std::mbstate_t>("CHS"));
    std::string str = convert.to_bytes(wString);     // wstring => string

    return str;
}

另外:

由于编码的locale name是操作系统决定的(例如GBK在linux下的locale名可能是"zh_CN.GBK",而windows下是".936"),因此做跨平台的话仍然要给不同的系统做适配。

项目中可以使用iconv开源库。

你可能感兴趣的:(c/c++,unicode,乱码,utf-8,编码)