咱们在学习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 编码前面已经介绍过,此处就再也不多说了。它使用0~127这128位数字表明了全部的英文字母以及数字、标点、特殊符号和键盘上有但屏幕上看不见的特殊按键。
它的优势是仅用128个数字就实现了对英文的完美支持,可是缺点也一样明显,不支持中文等除英文之外的其余语言文字。
所以,ASCII码基本能够看作是其余字符编码格式的一个子集,其余字符编码都是在ASCII码的基础上实现了必定的扩展,但毫无心外地,都实现了对ASCII码的兼容。编码
在汉字环境下,UTF-8
能够说是最多见的编码。它是Windows
系统默认的文本编码格式。UTF-8
是一种变长的编码方式,最大能够支持到6位。这就意味着他能够有效地节省空间(在后面介绍GBK
的时候,会讲GBK
是固定长度的编码方式)。
那么,UTF8
是如何知道当前所要表达的字符是几个字节呢?
在UTF8
中,它以首字节的高位做为标识,用来区别当前字节的长度。其规则大体以下:spa
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
等,因为不太经常使用,此处也就不展开讨论了。设计
GB
就是”国标“的拼音开头,顾名思义,以GB
开头的编码都是中国人专门为支持汉语而设计的编码格式。但这三者又有区别,最先出现的是GB2312
,它收录了6763个汉字,基本知足了计算机对汉字的处理须要。GB2312
使用双字节表示一个汉字。对汉字进行分区处理。每一个区含有94个汉字(或符号),这种表示方式称之为区位码。3d
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
相似,采用多字节编码,每一个汉字由一、二、4个字节组成。rest
若是你看到这个地方已经以为很乱了,没关系。咱们只须要知道,在GB
打头的编码格式下,咱们可以用键盘敲出来的,你在电脑上所看见的全部汉字,都是双字节的(四字节的汉字极少,只有一些极少数不经常使用的生僻字用到)。code
BIG5
,从字面翻译来看,叫作”大五码“,它主要用来表示中文繁体字。
它也是用双字节表示一个汉字,其中高位字节使用了0x81-0xFE,低位字节使用了0x40-0x7E,及0xA1-0xFE。。
这种编码格式用的比较少,此处就不展开说了。
上面介绍的几种编码格式,UTF-8
、GBK
等都支持汉字,可是标准不一样,所以,在实际进行开发的过程当中,对汉字的处理也不尽相同。
不管是UTF-8
、GBK
,仍是GB18030
,或者BIG5
,它都是向下兼容ASCII
的,为了区分ASCII
码和汉字,在汉字的高位补1。
这也就是说,若是咱们以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]: Chinese\n", i); }else{ printf("str[%d]: English\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
,第2、三字节的范围为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
也是一样的操做,此处就不作演示了。