共计 7323 个字符,预计需要花费 19 分钟才能阅读完成。
什么是编码格局
从一个小问题引入
咱们在学习 C 语言的时候,有一道必做的题目是将大写字母转换成小写,置信有点根底的同学都能不加思索的写出上面的代码:
char toLower(char upper){if (upper >= 'A' && upper <= 'Z'){return upper + 32;}else{return upper;}
}
要问为什么是这段代码?咱们往往也能说得出:因为大小写字母在 ASCII
码上正好相差 32(字符 'a'
为 97,字符 'A'
为 65)。
咱们在进行字符初始化的时候,往往会将字符初始化为 '\0'
。因为'\0'
在 ASCII 码中对应的数值是 0。
咱们理所应当地晓得 char
型字符对应的范畴是 0~127
,因为ASCII
码的范畴就是 0~127
。
然而有没有想过,为什么是 ASCII
码?
所谓的 ASCII
码,又到底是什么?
编码格局介绍
要说起 ASCII
码,不得不说起编码格局。
咱们晓得,对于计算机来说,咱们在屏幕上看到的千姿百态的文字、图片、甚至视频是不能间接辨认的,而是要通过某种形式转换为 0 和 1 组成的二进制的机器码,最终被计算机辨认(0 为低电平,1 为高电平)。
对于数字来说,有一套十分成熟的转换计划,就是将十进制的数字转换为二进制,就能间接被计算机辨认(如 5 转换为二进制是 0000 0101
)。然而对于像 ABCD
这样的英文字母,还有 !@#$
这样的特殊符号,计算机是不能间接辨认的,所以就须要有一套通用的规范来进行标准。
这套标准就是 ASCII
码。
ASCII 码应用 127 个字符,示意 A~Z 等 26 个大小写字母,蕴含数字 0~9,所有标点符号以及特殊字符,甚至还有不能在屏幕上间接看到的比方回车、换行、ESC 等。
依照这套 SACII 的编码标准,就很容易的晓得,'\0'
代表的是 0,'A'
代表的是 65,而 'a'
代表的是 97,'A'
和 'a'
之间正好相差了 32。
ASCII 码尽管只有 127 位,但根本实现了对所有英文的反对。所以为什么说 char
类型只占 1 个字节?因为 char
型最大的数字是 127,转成二进制也不过是 0111 1111
, 只须要 1 个字节就能示意所有的char
型字符,因而 char
只占 1 个字节。
然而随着计算机的遍及,计算机岂但要解决英文,还有汉字、甚至希腊文字、韩文、日文等诸多文字,这时,127 个字符必定不够了,这时就引入了 Unicode
的概念。Unicode
是一个编码字符集,它根本涵盖了世界上绝大多数的文字(只有极少数没有蕴含),在 Unicode 中文对照表中能够查看一些汉字的 Unicode
字符集。
比方,汉字”七“在 Unicode
示意为十六进制 0x4e03
,示意成二进制位0100 1110 0000 0011
,占了 15 位,至多须要两个字节能力放得下,有些更简单的生僻字,可能占用的字节数甚至不止两位。
这就面临着一个问题,当一个中英文夹杂的字符串输出到电脑的时候,计算机是如何晓得它到底是什么的?
就像下面的 0100 1110 0000 0011
,它到底是示意的是0100 1110
和0000 0011
两个 ASCII
字符,还是汉字”七“?计算机并不知道。所以就须要一套规定来通知计算机,到底该依照什么来解析。这些规定,就是字符编码格局。
其中就包含以下几种。
- ASCII
- UTF-8
- GBK
- GB2312
- GB18030
- BIG5
- ISO8859
编码格局分类
ASCII
ASCII 编码后面曾经介绍过,此处就不再多说了。它应用 0~127 这 128 位数字代表了所有的英文字母以及数字、标点、特殊符号和键盘上有但屏幕上看不见的非凡按键。
它的长处是仅用 128 个数字就实现了对英文的完满反对,然而毛病也同样显著,不反对中文等除英文以外的其余语言文字。
因而,ASCII 码根本能够看做是其余字符编码格局的一个子集,其余字符编码都是在 ASCII 码的根底上实现了肯定的扩大,但毫无意外地,都实现了对 ASCII 码的兼容。
UTF-8
在汉字环境下,UTF-8
能够说是最常见的编码。它是 Windows
零碎默认的文本编码格局。UTF-8
是一种变长的编码方式,最大能够反对到 6 位。这就意味着他能够无效地节俭空间(在前面介绍 GBK
的时候,会讲 GBK
是固定长度的编码方式)。
那么,UTF8
是如何晓得以后所要表白的字符是几个字节呢?
在 UTF8
中,它以首字节的高位作为标识,用来区别以后字节的长度。其规定大抵如下:
** 1 字节 0xxxxxxx(范畴:0x00-0x7F)
2 字节 110xxxxx 10xxxxxx (范畴:0x80-0x7ff)
3 字节 1110xxxx 10xxxxxx 10xxxxxx (范畴:0x800-0xffff)
4 字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (范畴:0x10000-0x10ffff)
5 字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6 字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx**
如下面的汉字”七“的 unicode
码是 0x4e03
,在0x800-0xffff
区间,所以是 3 字节,用 UTF-8
示意就是 11100100 10111000 10000011
(十六进制示意为0xe4b883
)。
“ 七 ” 的Unicode
码是 0100 1110 0000 0011
,可为什么是这个数呢?
依据 3 字节的填充规定,从右往左,顺次填充 x
的地位:
0100 111000 000011
+
1110xxxx 10xxxxxx 10xxxxxx
=
11100100 10111000 10000011
事实上,utf-8
编码下,汉字都为 3 字节。
实际上 UTF
家族除了 UTF-8
外,还有 UTF-16
、UTF-32
等,因为不太罕用,此处也就不展开讨论了。
GBK/GB2312/GB18030
GB
就是”国标“的拼音结尾,顾名思义,以 GB
结尾的编码都是中国人专门为反对汉语而设计的编码格局。但这三者又有区别,最早呈现的是 GB2312
,它收录了 6763 个汉字,根本满足了计算机对汉字的解决须要。GB2312
应用双字节示意一个汉字。对汉字进行分区解决。每个区含有 94 个汉字(或符号),这种示意形式称之为区位码。
- 01-09 区为特殊符号。
- 16-55 区为一级汉字,按拼音排序。
- 56-87 区为二级汉字,按部首/笔画排序。
- 10-15 区及 88-94 区则未有编码。
GB2312
编码范畴:A1A1-FEFE
,其中汉字编码范畴:B0A1-F7FE
。示意汉字时,第一字节 0xB0-0xF7
(对应区号:16-87),第二个字节0xA1-0xFE
(对应位号:01-94)。GBK
是在 GB2312
根底上的扩大。GBK
的 K
就是扩大的”扩“的拼音首字母。因而,GBK
向下兼容 GB2312
。GBK
也应用双字节示意汉字,其中首字节范畴 0x81-0xfe
,第二个字节范畴0x40-0xfe
,剔除0x7F
一条线。因而,GBK
所能示意的汉字比 GB2312
要多得多(能示意 21886 个汉字)。GB18030
是最新的内码字集,能够示意 70244 个汉字。它与 UTF-8
相似,采纳多字节编码,每个汉字由 1、2、4 个字节组成。
- 单字节,其值从 0 到 0x7F,与 ASCII 编码兼容。
- 双字节,第一个字节的值从 0x81 到 0xFE,第二个字节的值从 0x40 到 0xFE(不包含 0x7F),与 GBK 规范兼容。
- 四字节,第一个字节的值从 0x81 到 0xFE,第二个字节的值从 0x30 到 0x39,第三个字节从 0x81 到 0xFE,第四个字节从 0x30 到 0x39。
如果你看到这个中央曾经感觉很乱了,不要紧。咱们只须要晓得,在 GB
打头的编码格局下,咱们可能用键盘敲进去的,你在电脑上所看见的所有汉字,都是双字节的(四字节的汉字极少,只有一些极少数不罕用的生僻字用到)。
BIG5
BIG5
,从字面翻译来看,叫做”大五码“,它次要用来示意中文繁体字。
它也是用双字节示意一个汉字,其中高位字节应用了 0x81-0xFE,低位字节应用了 0x40-0x7E,及 0xA1-0xFE。。
这种编码格局用的比拟少,此处就不开展说了。
汉字编码
下面介绍的几种编码格局,UTF-8
、GBK
等都反对汉字,然而规范不同,因而,在理论进行开发的过程中,对汉字的解决也不尽相同。
如何判断汉字编码
无论是 UTF-8
、GBK
,还是GB18030
,或者BIG5
,它都是向下兼容ASCII
的,为了辨别 ASCII
码和汉字,在汉字的高位补 0。
这也就是说,如果咱们以 int
的模式取出单个字符的值,汉字都是小于 0 的。
因而,判断是否是汉字也就变得简略了:
enum boolean{true, false};
typedef int boolean;
boolean isChinese(char ch){return (ch < 0) ? true : false;
}
写一段代码验证一下:
void test01(){char str[20];
memset(str, 0, sizeof(str));
strcpy(str, "hello 汉字");
for (int i = 0; i < strlen(str); i++){if (isChinese(str[i]) == true){printf("str[%d]: 汉字 \n", i);
}else{printf("str[%d]: 英文 \n", i);
}
}
}
咱们在 main
函数里调用 test01
函数,失去如下后果:
因为在 utf-8
下,一个汉字占 3 字节,所以前面从 5~10 这 6 个字节正好代表着 2 个汉字。
如果咱们把编码改成 GB2312
,运行能够失去如下后果:
能够看到,只有最初 4 个字节是汉字,充分说明了 GB2312
编码格局下,一个汉字占 2 个字节。
如何解决汉字截断问题
如果咱们把下面的字符串按字符打印进去,失去上面的后果:
能够看到,所有的汉字都乱码了,起因就在于,UTF-8
编码下,每个汉字占 3 个字节,一个字节不足以示意残缺的汉字,所以打印进去都是乱码的。
在理论开发中,比拟常见的须要解决的问题是,截取肯定长度的字符串,然而如果截取的地位正好是个汉字,难免会遇到汉字被截断的问题。
那么,这类问题如何解决呢?
依据汉字的编码规定,咱们晓得,UTF-8
和 GBK
对汉字的解决是不一样的。UFT-8
一个汉字是 3 字节,且规定如下:
1110xxxx 10xxxxxx 10xxxxxx
所以,咱们很容易晓得,汉字的首字节范畴为 11100000~11101111
, 转成十六进制为0xe0~0xef
,第二、三字节的范畴为10000000~10111111
,转成十六进制范畴为0x80~0xbf
。
所以 UTF-8
的汉字截断问题解决能够如下:
void HalfChinese_UTF8(const char *input, size_t input_len, char *output, size_t *output_len)
{char current = *(input + input_len);
if (isChinese(current) == false)
{
*output_len = input_len;
strncpy(output, input, *output_len);
return;
}
// 汉字
*output_len = input_len;
//1110xxxx 10xxxxxx 10xxxxxx
// 第二位和第三位的范畴是 10000000~10ffffff,转成十六进制是 0x80~0xbf,在这个范畴内都阐明是汉字被截断
while ((current&0xff) < 0xc0 && (current&0xff) >= 0x80)
{(*output_len)++;
current = *(input + *output_len);
}
strncpy(output, input, *output_len);
}
该函数有四个参数,其中 input
和input_len
作为原始输出,input_len
代表须要截取的地位,output
和 output_len
作为输入,output
为截断解决后的字符串,output_len
为截断解决后的长度。
咱们应用上面的代码进行测试:
void test02()
{char in[20], out[20];
memset(in, 0, sizeof(in));
memset(out, 0, sizeof(out));
strcpy(in, "hello 汉字");
size_t out_len = 0;
for (int i = 1; i <= strlen(in); i++)
{HalfChinese_UTF8(in, i, out, &out_len);
printf("out: %s\n", out);
}
}
运行后后果如下:
如果是 GBK
编码,要略微麻烦一点。因为咱们晓得,GBK
是双字节示意汉字,且第一个字节的值从 0x81
到 0xFE
,第二个字节的值从 0x40
到 0xFE
(不包含 0x7F
),单从字符的值无奈判断到底是汉字的首字节还是后一个字节(因为二者的值有重复部分)。
如果字符串纯为汉字倒还好办,咱们曾经晓得汉字占 2 个字节,间接依据长度的奇偶来判断就能够,但如果是中英文夹杂就不能采纳这种形式了。
在这里,我应用的是先对字符串进行一道过滤解决,判断字符串中除掉英文字符后纯汉字的长度,如果为奇数,代表汉字被截断,加 1 就能取其残缺的汉字,如果是偶数,阐明正好是一个残缺的汉字,无需解决,间接返回即可。
代码实现如下:
void HalfChinese_GBK(const char *input, size_t input_len, char *output, size_t *output_len){char current = *(input + input_len);
if (isChinese(current) == false)
{
*output_len = input_len;
strncpy(output, input, *output_len);
return;
}
*output_len = input_len;
if (MoveEnglish(input, input_len) %2 != 0){(*output_len)++;
}
strncpy(output, input, *output_len);
}
int MoveEnglish(const char *input, size_t input_len){
int out_len = input_len;
for (int i = 0; i < input_len; i++)
{if (isChinese(input[i]) == false){out_len++;}
}
return (out_len > 0) ? out_len : 0;
}
同样应用下面的测试代码进行测试,失去如下后果:
如何实现编码之间相互转换
既然编码格局这么多,那么怎么进行编码之间的转换呢?
在 C 语言下,次要是利用零碎的 iconv
函数实现。iconv
函数蕴含在头文件 iconv.h
中,其函数原型如下所示:
size_t iconv (iconv_t __cd, char **__restrict __inbuf,
size_t *__restrict __inbytesleft,
char **__restrict __outbuf,
size_t *__restrict __outbytesleft);
第一个参数是转换的一个句柄,由 iconv_open
函数创立,第二个参数是输出的字符串,第三个参数是输出字符串的长度,第四个参数是转换后的输入字符串,第五个参数是输入字符串的长度。在编码转换实现之后,须要调用 iconv_close
函数敞开句柄。所以残缺的调用程序为:
iconv_open
关上iconv
句柄- 调用
iconv
进行编码转换 iconv_close
敞开句柄
还有一点须要留神的是,__inbytesleft
和 __outbytesleft
的长度,因为不同编码对于汉字的解决字节数不同,比方从 UTF-8
转换为 GBK
,同样都是两个汉字,转换前长度为 6,转换后长度为 4。也就是说,在编码转换过程中,字符串可能会变长或缩短,如果长度不正确,很容易造成越界,从而导致谬误。
残缺的编码转换性能封装如下:
boolean convert_encoding(char *in, size_t in_len, char *out, size_t out_len, const char *from, const char *to)
{if (strcasecmp(from, to) == 0){size_t len = (in_len < out_len) ? in_len : out_len;
memcpy(out, in, len);
return true;
}
iconv_t cd = iconv_open(from, to);
if (cd == (iconv_t)-1){printf("iconvopen err\n");
return false;
}
size_t inbytesleft = in_len;
size_t outbytesleft = out_len;
char *src = in;
char *dst = out;
size_t nconv;
nconv = iconv(cd, &src, &inbytesleft, &dst, &outbytesleft);
if (nconv == (size_t)-1){if (errno == EINVAL){printf("EINVAL\n");
} else {printf("error:%d\n", errno);
}
}
iconv_close(cd);
return true;
}
留神,因为应用到了 libiconv
,编译时须要加-liconv
进行链接。
测试代码如下:
void test04()
{char in[20], out[20];
memset(in, 0, sizeof(in));
memset(out, 0, sizeof(out));
strcpy(in, "hello 汉字 world");
if (false == convert_encoding(in, strlen(in), out, 20, "utf-8", "gbk")){printf("failed\n");
return;
}
printf("in: %s\nout:%s\n", in, out);
}
以上代码运行后果如下所示:
将 GBK
转换为 UTF-8
也是同样的操作,此处就不做演示了。