欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

c++转码基础(1):各种编码类型及unicode和uft-8互转

程序员文章站 2022-05-03 18:05:42
什么是ascii编码? 单字节编码,适用于所有拉丁文字字母. ascii 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符. 标准ascii 码也叫基础ascii码,使用7...

什么是ascii编码?

单字节编码,适用于所有拉丁文字字母.

ascii 码使用指定的7 位或8 位二进制数组合来表示128 或256 种可能的字符.

标准ascii 码也叫基础ascii码,使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号, 以及在美式英语中使用的特殊控制字符。其中:

0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:lf(换行)、cr(回车)、ff(换页)、del(删除)、bs(退格)、bel(响铃)等;

通信专用字符:soh(文头)、eot(文尾)、ack(确认)等;ascii值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。

它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响

32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字。65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

扩展ascii 字符是从128 到255(0x80-0xff)的字符.扩展ascii不再是国际标准

什么是dbcs字符集?unicode的由来

中文常用的字符集有gb2312-80,gbk(大陆的中文字符集),big5(*的倚天汉字),unicode.

为了解决中国、日本和韩国的象形文字符和ascii的某种兼容性,开发出双字节字符集(dbcs:double-byte character set)。

dbcs从256代码开始,就像ascii一样。较高的128个代码中的某些总是跟随着第二个字节。这两个字节一起(称作首字节和跟随字节)定义一个字符,通常是一个复杂的象形文字。

为了解决中文字符的应用,中国定义了一套gb2312的字符集:

字符集规定:127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,

前面的一个字节(他称之为高字节)从0xa1用到 0xf7,后面一个字节(低字节)从0xa1到0xfe,这样我们就可以组合出大约7000多个简体汉字了。

在这些编码里,我们还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在 ascii 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,

这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。 中国人民看到这样很不错,于是就把这种汉字方案叫做 “gb2312“。gb2312 是对 ascii 的中文扩展。

但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家*。于是我们不得不继续把 gb2312 没有用到的码位找出来老实不客气地用上。

后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字 符集里的内容。

结果扩展之后的编码方案被称为 gbk 标准,gbk包括了gb2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。 后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,gbk扩成了 gb18030。

从此之后,中华民族的文化就可以在计算机时代中传承了。 中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “dbcs“(double byte charecter set 双字节字符集)

后来因为很多地区都定义自己的编码字符集合,而且都是重叠的,所以在跨区域通讯向出现了问题. iso (国际标谁化组织)为了解决这个问题,废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号 的编码!他们打算叫它”universal multiple-octet coded character set”,简称 ucs, 俗称 “unicode“。

unicode编码的形式?utf-8的由来?

unicode开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 iso 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,

对于ascii里的那些“半角”字符,unicode包持其原编码不变,只是将其长度由原 来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。

由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在 保存英文文本时会多浪费一倍的空间。

如以下字符编码的unicode 编码(ucs-2):

字符 十六进制编码 二进制编码

i 0049 00000000 01001001

t 0074 00000000 01110100

' 0027 00000000 00100111

s 0073 00000000 01110011

0020 00000000 00100000

知 77e5 01110111 11100101

乎 4e4e 01001110 01001110

日 65e5 01100101 11100101

报 62a5 01100010 10100101

unicode很长一段时间无法的原因有两个:

1. unicode 是双字符编码,在网络传输和不同机器上存在大小端的问题;相同的字符集在不同的位置存在几码顺序不同。

2. unicode 1~127的文字符浪费一个字节的空间

为了解决unicode在网络上传输的问题,出现了面向传输的utf(ucs transfer format)标准,utf-8就是每次8位传输数据,utf-16就是每次16位数据(先有的utf-16,但是和unicode一样的问题)。

utf-8就是在互联网上使用最广的一种unicode的实现方式,这是为传输而设计的编码,并使编码无国界。

utf-8编码格式?

utf-8最大的一个特点,就是它是一种变长的编码方式.

它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,当字符在ascii 码的范围时,就用一个字节表示,保留了ascii字符一个字节的编码做为它的一部分,

注意的是unicode一个中文字符占2个字节,而utf-8一个中 文字符占3个字节)。从unicode到uft-8并不是直接的对应,而是要过一些算法和规则来转换。

utf-8是这样做的:

1. 单字节的字符,字节的第一位设为0,对于英语文本,utf-8码只占用一个字节,和ascii码完全相同;

2. n个字节的字符(n>1),第一个字节的前n位设为1,第n+1位设为0,后面字节的前两位都设为10,这n个字节的其余空位填充该字符unicode码,高位用0补足。

这样就形成了如下的utf-8标记位:

(十六进制) | (二进制)

—————————————————————–

0000 0000-0000 007f | 0xxxxxxx

0000 0080-0000 07ff | 110xxxxx 10xxxxxx

0000 0800-0000 ffff | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 ffff | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

如以下字符编码utf-8:

字符 十六进制编码 二进制编码

i 49 01001001

t 74 01110100

' 27 00100111

s 73 01110011

20 00100000

知 e79fa5 11100111 10011111 10100101

乎 e4b98e 11100100 10111001 10001110

日 e697a5 11100110 10010111 10100101

报 e68aa5 11100110 10001010 10100101

utf-8和utf-16的区别?

区分utf-8还是utf-16的文件:文件头包含

ef bb bf    utf-8

fe ff     utf-16/ucs-2, little endian

ff fe     utf-16/ucs-2, big endian

ff fe 00 00  utf-32/ucs-4, little endian.

00 00 fe ff  utf-32/ucs-4, big-endian.

utf-16不需要用啥字符来做标志,所以两字节也就是2的16次能表示65536个字符.

表示一个"汉"中文字(27721(十进制)), utf-16 是 01101100 01001001; 十六进制是 6c49

而utf-8由于里面有额外的标志信息,所有一个字节只能表示2的7次方128个字符,两个字节只能表示2的11次方2048个字符.而三个字节能表示2的16次方,65536个字符.

所以utf-8对"汉"的编码是 11100110 10110001 10001001(这里不关注大小端的问题,大小端不同,编码顺序不同)

由上面我们可以看出utf-8需要判断每个字节中的开头标志信息,所以如果一当某个字节在传送过程中出错了,就会导致后面的字节也会解析出错.而utf-16不会判断开头标志,即使错也只会错一个字符,所以容错能力强.

大小端问题:

计算机是以字节为寻址单位的,这就涉及到字(2个字节), 双字(4个字节)及其他多字节单位 在计算机内如何排布的问题, 这里无非就是2种:低字节在低地址的little-endian和高字节在低地址的big-endian.

如何区分当前系统是哪种类型的大小端? 曾经看到有经验的程序员也以当前的操作系统类型来判断, 实际上系统的大小端和你的cpu架构体系相关联, 比如说x86是小端, powpc是大端,arm则是可控制(默认也是小端)。

要判断当前环境是大小端实际上很简单: bool islittleendian() { int i=1; return (*(char *)&i == 1); }

感觉现在大端的应用主要是网络字节序, java内部全都是大端。

ucs2和ucs4因为都是用多字节表示一个字符,所以实际上都有大小端的问题,比如分别对应ucs2-le和ucs2-be,windows上的unicode实际上是ucs2-le;

utf8因为是字节流,所以没有大小端的问题。

base64 编码:

base64是网络上最常见的用于传输8bit字节代码的编码方式之一,大家可以查看rfc2045~rfc2049,上面有mime的详细规范。base64编码可用于在http环境下传递较长的标识信息。

因为,base64将三个字节转化成四个字节,因此base64编码后的文本,会比原文本大出三分之一左右。

举一个具体的实例,演示英语单词man如何转成base64编码。

text content

m

a

n

ascii

77

97

110

bit pattern

0

1

0

0

1

1

0

1

0

1

1

0

0

0

0

1

0

1

1

0

1

1

1

0

index

19

22

5

46

base64-encoded

t

w

f

u

第一步,"m"、"a"、"n"的ascii值分别是77、97、110,对应的二进制值是01001101、01100001、01101110,将它们连成一个24位的二进制字符串010011010110000101101110。

第二步,将这个24位的二进制字符串分成4组,每组6个二进制位:010011、010110、000101、101110。

第三步,在每组前面加两个00,扩展成32个二进制位,即四个字节:00010011、00010110、00000101、00101110。它们的十进制值分别是19、22、5、46。

第四步,根据上表,得到每个值对应base64编码,即t、w、f、u。

如果字节数不足三,则这样处理:

a)二个字节的情况:将这二个字节的一共16个二进制位,按照上面的规则,转成三组,最后一组除了前面加两个0以外,后面也要加两个0。这样得到一个三位的base64编码,再在末尾补上一个"="号。

比如,"ma"这个字符串是两个字节,可以转化成三组00010011、00010110、00010000以后,对应base64值分别为t、w、e,再补上一个"="号,因此"ma"的base64编码就是twe=。

b)一个字节的情况:将这一个字节的8个二进制位,按照上面的规则转成二组,最后一组除了前面加二个0以外,后面再加4个0。这样得到一个二位的base64编码,再在末尾补上两个"="号。

比如,"m"这个字母是一个字节,可以转化为二组00010011、00010000,对应的base64值分别为t、q,再补上二个"="号,因此"m"的base64编码就是tq==。

实例函数:

static const uint8_t *kbase64encodetable = (const uint8_t *)

"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789+/";

void base64_encode(const uint8_t *in, uint32_t len, uint8_t *buf) {

buf[0] = kbase64encodetable[(in[0] >> 2) & 0x3f];

if (len == 3) {

buf[1] = kbase64encodetable[((in[0] << 4) + (in[1] >> 4)) & 0x3f];

buf[2] = kbase64encodetable[((in[1] << 2) + (in[2] >> 6)) & 0x3f];

buf[3] = kbase64encodetable[in[2] & 0x3f];

} else if (len == 2) {

buf[1] = kbase64encodetable[((in[0] << 4) + (in[1] >> 4)) & 0x3f];

buf[2] = kbase64encodetable[(in[1] << 2) & 0x3f];

} else { // len == 1

buf[1] = kbase64encodetable[(in[0] << 4) & 0x3f];

}

}

static const uint8_t kbase64decodetable[256] ={

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,

52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,

-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,

15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,

-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,

41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,

};

void base64_decode(uint8_t *buf, uint32_t len) {

buf[0] = (kbase64decodetable[buf[0]] << 2) |

(kbase64decodetable[buf[1]] >> 4);

if (len > 2) {

buf[1] = ((kbase64decodetable[buf[1]] << 4) & 0xf0) |

(kbase64decodetable[buf[2]] >> 2);

if (len > 3) {

buf[2] = ((kbase64decodetable[buf[2]] << 6) & 0xc0) |

(kbase64decodetable[buf[3]]);

}

}

}

多字符与双字符转码函数:

在windows上不同编码方式的相互转换可以通过widechartomultibyte和mutibytetowidechar进行转码。

linux下可以用mbstowcs() ,wcstombs() 或者使用iconv库函数;

需要注意的问题:

size_t mbstowcs(wchar_t *wcstr,const char *mbstr,size_t count);

这个函数的第三个参数count,大小一定要是mbstr长度的2倍,否则出来的中文也会是乱码。

iconv库函数的使用

#include

iconv_t iconv_open(const char *tocode, const char *fromcode);

size_t iconv(iconv_t cd,char **inbuf,size_t *inbytesleft,char **outbuf,size_t *outbytesleft);

int iconv_close(iconv_t cd);

iconv函数的使用需要setlocale() 这个函数设置环境:如:setlocale(lc_all,"zh_cn.gb18030");

linux下的库函数使用麻烦而且易出错,可以使用下列函数

// 检查字符串中是不是有需要转换的宽字符(utf-8编码中的宽字符),当然该函数不能识别是不是unicode编码,只能识别中文等等多字节字符的特征

bool checkisutf8(const char *put_in, int size)

{

if (put_in != null && size-- > 0) // size减1预先去掉结尾位置,所有size因为是实际长度+1

{

while (*put_in != 0 && size > 0) // 遍历

{

unsigned char ch = *put_in;

if (ch > 0x7fu) // 检查是不是utf8编码,是的话一次取完,(utf-8没有大小端的问题)

{

int cwch = 0; // 记录个数

while (ch & 0x80u)

{

ch <<= 1;

cwch++;

}

if (cwch > 6 || cwch < 1) // 超过了最大识别边界

{

put_in++;

size--;

continue;

}

unsigned char ch_g = *put_in++;

size--;

bool istrue = true;

while (--cwch > 0 && *put_in != 0 && size > 0) // 递减验证

{

if (0x80u != ch_g & 0x80u) // 表示没有

{

istrue = false;

}

ch_g = *put_in++;

size--;

}

if (istrue)

{

return true; // 表示包含宽字符编码

}

}

else

{

put_in++;

size--;

}

}

}

int utf8toutf16(const char *pmbs, wchar_t *pwcs, int size) // pmbs需要有边界以0结尾,size表示字符个数

{

int cnt = 0;

if (pmbs != null && pwcs != null && size-- > 0) // size减1预先去掉结尾位置,所有size因为是实际长度+1

{

while (*pmbs != 0 && size > 0) // 遍历

{

unsigned char ch = *pmbs;

//printf("%c %x\n", ch,ch);

if (ch > 0x7fu) // 检查是不是utf8编码,是的话一次取完,(utf-8没有大小端的问题)

{

int cwch = 0; // 记录个数

while (ch & 0x80u) // 检查字符头是不是宽字符标记

{

ch <<= 1; // 左移一位,检查有几个字符

cwch++;

}

if (cwch > size) // 检查是不是最后几个字符

{

*pwcs = *pmbs++;

//printf("1\n");

//printf("1: %ls \n", pwcs);

}

else

{

*pwcs = *pmbs++ & (0xffu >> cwch); // 付给第一个字符的数据值

size--;

while (--cwch > 0) // 递减,(没有做严格验证)

{

if (size <= 0)

{

*pwcs = *pmbs; // 重复复制,保证单值正确

}

else

{

*pwcs <<= 6; // 向大端平移6位(因为跟随字符都是6位有效)

*pwcs |= (*pmbs++ & 0x3fu); // 添加地位字符

size--;

}

}

//printf("2 \n");

//printf("2:%ls \n", pwcs);

}

}

else

{

*pwcs = *pmbs++; // 表示是单字节编码,直接赋值给一个宽字符(自动适应大小端)

size--;

}

//printf("3 \n");

pwcs++;

cnt++;

}

*pwcs = 0; // 以0x00

cnt++;

}

return cnt;

}

int unicodetoutf8(const wchar_t *pwcs, char *pmbs, int size)

{

int cnt = 0;

// 这里 size-- 是预先除去尾零所需位置

if (pwcs != null && pmbs != null && size-- > 0) {

while (*pwcs != 0 && size > 0) {

if (*pwcs < 0x00000080u) {

*pmbs++ = (char)*pwcs;

size -= 1;

cnt += 1;

}

else if (*pwcs < 0x00000800u) {

// 剩余空间不够存放该字符

if (size < 2) {

break;

}

*pmbs++ = (0xffu << 6) | (*pwcs >> 6);

*pmbs++ = 0x80u | (*pwcs & 0x3fu);

size -= 2;

cnt += 2;

}

else if (*pwcs < 0x00010000u) {

// 剩余空间不够存放该字符

if (size < 3) {

break;

}

*pmbs++ = (0xffu << 5) | (*pwcs >> 12);

*pmbs++ = 0x80u | ((*pwcs >> 6) & 0x3fu);

*pmbs++ = 0x80u | (*pwcs & 0x3fu);

size -= 3;

cnt += 3;

}

else if (*pwcs < 0x00200000u) {

// 剩余空间不够存放该字符

if (size < 4) {

break;

}

*pmbs++ = (0xffu << 4) | (*pwcs >> 18);

*pmbs++ = 0x80u | ((*pwcs >> 12) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 6) & 0x3fu);

*pmbs++ = 0x80u | (*pwcs & 0x3fu);

size -= 4;

cnt += 4;

}

else if (*pwcs < 0x04000000u) {

// 剩余空间不够存放该字符

if (size < 5) {

break;

}

*pmbs++ = (0xffu << 3) | (*pwcs >> 24);

*pmbs++ = 0x80u | ((*pwcs >> 18) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 12) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 6) & 0x3fu);

*pmbs++ = 0x80u | (*pwcs & 0x3fu);

size -= 5;

cnt += 5;

}

else if (*pwcs < 0x80000000u) {

// 剩余空间不够存放该字符

if (size < 6) {

break;

}

*pmbs++ = (0xffu << 2) | (*pwcs >> 30);

*pmbs++ = 0x80u | ((*pwcs >> 24) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 18) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 12) & 0x3fu);

*pmbs++ = 0x80u | ((*pwcs >> 6) & 0x3fu);

*pmbs++ = 0x80u | (*pwcs & 0x3fu);

size -= 6;

cnt += 6;

}

else {

// 无法识别的 unicode 字符

break;

}

pwcs++;

}

*pmbs = 0;

cnt++;

}

return cnt;

}