关于c:大话编码格式

4次阅读

共计 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 11100000 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-16UTF-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 根底上的扩大。GBKK 就是扩大的”扩“的拼音首字母。因而,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-8GBK等都反对汉字,然而规范不同,因而,在理论进行开发的过程中,对汉字的解决也不尽相同。

如何判断汉字编码

无论是 UTF-8GBK,还是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-8GBK 对汉字的解决是不一样的。
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);
}

该函数有四个参数,其中 inputinput_len作为原始输出,input_len代表须要截取的地位,outputoutput_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是双字节示意汉字,且第一个字节的值从 0x810xFE,第二个字节的值从 0x400xFE(不包含 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 也是同样的操作,此处就不做演示了。

正文完
 0