好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

字符编码&C语言

字符编码&C语言

祖仙教小凡仙 海鲨数据库架构师

C语言如何实现存储汉字,输入和输出汉字呢? 这个就要说说电脑进化史。。。。 是啊 是历史,电脑就认01 不认ABC。咋办呢?如何让电脑也认ABC呢? 其实简单地这样干,先用1010 来说明后面的东西是啥? 001 是程序 000 是数据 011 是 整数 010 是 浮点数 100 是 字符和字母

这就是分类 TYPE 后面叫VALUE 好了 这下 100 后面是字符和字母。那怎么解释后面的10011 是A 呢? 那我们搞个字符编码就行了,后面1个字节 整好 8位 类似下面这样

前面第一位表示正负 后面7位可以表达 2^7个可以代表128个字符和字母 基本都覆盖了英语地球的字母。这个编码叫ASCII。

ASCII码 我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。 上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。 ASCII码一共规定了128个字符的编码,比如空格"SPACE"是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。

字符编码是解决了,还是不够,那就是如何显示?如何让显卡怎么去显示? 这就需要字库通过阵点来描述。大致如下的图

这样8*8阵列图 来描述哪块该显示,哪块不显示!显示为1,不显示为0 这样用8字节存储如何显示个A。这样就成了128个英文字库。

那么我们汉字中国的电脑工程师照猫画虎,就这样干! 先添加个分类 0101 这后面的是汉字 后面的VALUES 用2个字节来表达一个汉字,可以表达2^16=65535

实际上没存那么多,用7位 大约2^14=16384 不够也可以了. 下面具体如何编码大家一眼带过就可!

GB2312

标准标准编号:GB 2312-1980 GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE GB2312 规定“对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示”

GB 18030

2000年的GB18030是编码采用单字节、双字节和4字节方案。其中单字节、双字节和GBK是完全兼容的。4字节编码的码位就是收录了CJK扩展A的6582个汉字。 GB18030 编码是一二四字节变长编码。一字节部分从 0x0~0x7F 与 ASCII 编码兼容。二字节部分, 首字节从 0x81~0xFE, 尾字节从 0x40~0x7E 以及 0x80~0xFE, 与 GBK 标准基本兼容。四字节部分, 第一字节从 0x81~0xFE, 第二字节从 0x30~0x39, 第三和第四字节的范围和前两个字节分别相同。四字节部分覆盖了从 0x0080 开始, 除去二字节部分已经覆盖的所有 Unicode 3.1 码位。也就是说, GB18030 编码在码位空间上做到了与 Unicode 标准一一对应,这一点与 UTF-8 编码类似。

大陆的国标码发展了好几代,归结如下:

西方也开始搞多字节来表达非英语区的文字,比如德语,法语,俄语 为此定义了个UNICODE编码

Unicode

正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。 可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。 Unicode当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字"严"。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表。

Unicode的问题

需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字"严"的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。 这里就有两个严重的问题,第一个问题是,如何才能区别Unicode和ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。 它们造成的结果是:1)出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示Unicode。2)Unicode在很长一段时间内无法推广,直到互联网的出现。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。 UTF-8的编码规则很简单,只有二条: 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。 下表总结了编码规则,字母x表示可用编码的位。

 
Unicode符号范围 | 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 编码过程

已知"严"的unicode是 16进制 4E25

UTF-8 转 16进制 E4 B8 A5

Unicode 编码系统,可分为编码方式和实现方式两个层次。对于国际组织发布的 Unicode 编码标准,对应的就是编码方式,最常用的是 UCS-2(Universal Character Set 2),采用两字节编码一个字符。当然国际语言文字太多,两字节不够用了,就有四字节编码方式 UCS-4。这个仅仅是标准,而不是实现,在编码实现的过程中,有些考虑兼容旧的单字节 ASCII 编码,有些不考虑兼容性;有些考虑双字节中哪个字节放在前面,哪个字节放在后面的问题,即 BOM(Byte Order Mark,字节顺序标记)的作用。因此诞生了多种国际码的实现方式,统称为 Unicode 转换格式(Unicode Transformation Format,UTF):

虽然我是中国人,虽然支持汉字编码,可惜我还是放弃它!我们现在重点学习上面的UTF-8 UTF-16 UTF-32

我们用WIN7 带的记事文本,NODPAD++ UE三个工具来观看中文编码

先输入中文,然后另存,选择不同的编码。存完后再用UE打看然后用16进制观看

 
UE  win7       =>“中文”
default         =>D6 D0 CE C4
ANSI/ASCII  =>D6 D0 CE C4
UTF-8            =>EF BB BF E4 B8 AD E6 96 87
UTF-8_NO_BOM => E4 B8 AD E6 96 87 

UE工具 就4个编码模式,因为WIN7默认支持汉字编码GBK2013。 后面的两个都是UTF8 一个中文占3个字节,EFBBBF 表示这是UT

 
NOTEPAD++
ANSI             =>D6 D0 CE C4
UTF-8            =>EF BB BF E4 B8 AD E6 96 87
UTF-8 NO_BOM     => E4 B8 AD E6 96 87
UCS-2 BIG_ENDIAN   =>  FE FF  4E 2D 65 87
UCS-2 LITTLE ENDIAN => FF FE 2D 4E 87 65 

NOTEPAD++ 前3个编码跟UE一样,后面两个是UTF-16(UCS-2),唯一区别就是BOM 顺序。好烦,连这个都要争个高下,就是高低字节 谁在前面的问题。 我们的人类习惯是高字节放在前面,可惜 Linux 内存是按低字节放在前面。 UTF-8 NO_BOM 唯一用处就是观看某个中文的实际UTF8编码。

记事本 ANSI =>D6 D0 CE C4 UTF-8 => EF BB BF E4 AD E6 96 87 UNICODE =>FF FE 2D 4E 87 65 UNICODE BIG ENDIAN =>FE FF 4E 2D 65 87

WIN7 自带的记事本 4种编码 前面两个跟UE NODPA++一致。 UNICODE 是UTF-16 小的在前面(UCS-2 LITTLE ENDIAN), 后面的UNICODE BIG ENDIAN就是UTF-16 大端 高字节在前面 UCS-2 BIG_ENDIAN

因此我们常用编码是ANSCII,UTF-8 ,UNICODE (UCS-2 Litter,UTF-16LE) 这3种! 其实我还是挺喜欢UTF-32的, 简单就是浪费空间而已。

C语言 有C99和C11两种标准 为此有两种表示汉字的方法 分别来自<uchar.h>和<wchar.h> 下面 C99实现方法,C11需要GLIB 2.6

 
#include <stdio.h>
#include <stddef.h> //wchar_t 类型定义处
#include <uchar.h> //glib 2.6
#include <locale.h>  //本地化头文件
//#include <wchar.h> 
//#include <string.h>

int main()
{

char utf8_str[]="中文";
wchar_t  wStr[]=L"您好!";
wchar_t    wc_B=L'曾';

setlocale(LC_ALL,"zh_CN.UTF-8");

/*c11 gcc 4.7 up glib 2.6
char16 utf16_char =u'好'; 
char16 utf16_str[]={utf16_char,u'\0'};
printf(u8"字符串:%s\n",utf16_str);
*/

printf("char utf8_str: %s\n",utf8_str);
printf("wchar_t wStr ls:%ls\n",wStr);
printf("wchar_t wc_B lc: %lc\n",wc_B);
printf("wchar_t wStr S:%S\n",wStr); 

printf("wchar_t wc_B:%zu\n",sizeof(wc_B));
printf("Szieof utf8_str[]:%d\n",sizeof(utf8_str));
printf("wchar_t wStr[] sizeof:%d\n",sizeof(wStr));

wprintf(L"wchar_t wStr zs:%zs\n",wStr);
wprintf(L"wchar_t wc_B zc:%zc\n",wc_B);
wprintf(L"wchar_t wStr ls:%ls\n",wStr);
wprintf(L"wchar_t wc_B lc:%lc\n",wc_B);
} 

因为 Linux 是UTF-8 为此需要设置本地化函数 setlocale(LC_ALL,"zh_CN.UTF-8");

我们定义了 3个变量

char utf8_str[]="中文"; wchar_t wStr[]=L"您好!"; wchar_t wc_B=L'曾';

UTF8是 多字节 WCHAR_T 是宽字节。中文多和宽不是一个意思吗?

多字节类似于ORACLE的 NVARCHAR 国际变长字符 宽字节是似于ORACLE的 NCHAR 国际固定字符

字符串要用双引号,字符变量用单引号,宽字符前要用大写L来赋值表示 打印中文 UTF8 正常使用打印函数和格式;打印宽字符可以用打印函数,格式需要%ls或者%S 变量用%lc

 
[root@centos7 c_time]# gcc Unicode.c  -o Unicode.exe
[root@centos7 c_time]# ./Unicode.exe
char utf8_str: 中文
wchar_t wStr ls:您好!
wchar_t wc_B lc: 曾
wchar_t wStr S:您好!
wchar_t wc_B:4
Szieof UTF8 buffer:7       //包含隐藏字符串结束符\0 1个中文占3个字节
wchar_t a[] sizeof:16      //包含隐藏字符串结束符\0 1个中文占4个字节,并且每个字符都占4个字节 

惊奇发现wchar_t 居然是4个字节 UTF-32编码 哈哈。

 
为什么WPRINTF不启作用呢? 
因为WPRINTF 不能跟PRINTF 一起使用在一个源代码里 晕真坑人

wprintf(L"wchar_t a is Str:%zs\n",a);
wprintf(L"wchar_t B is:%zc\n",B);
wprintf(L"wchar_t Str a:%ls\n",a);
wprintf(L"wchar_t B:%lc\n",B);

只有这样才能正常显示出来,不过wprintf显得啰嗦 要大L 还有%l or %z
wprinf 最终要使用下面得函数进行转换成UTF-8 在系统输出。
wctomb(); //宽字符转多字符
mbstowcs() //多字符转宽字符

另外注意坑 宽字符串 用双引号  宽字符用单引号 宽字符串显示用%ls 宽字符用%lc 

char *setlocale(int categoryconst,const *locale);
LC_ALL
LC_COLLATE 字符串比较
LC_CTYPE    字符串分类转换
LC_MONETARY 货币格式
LC_NUMERIC  小数点分隔符
LC_TIME     日期时间格式
LC_MESSAGES  系统消息
[root@centos7 c_time]# rpm -qa|grep glib
spice-glib-0.20-8.el7.x86_64
glibc-2.17-55.el7.x86_64 

如果你是在WIN7下编好的C程序 再传到 Linux 下GCC 就会有点问题

gcc -finput-charset=UTF-8 time_val_zone.c -o Time_val_zone.exe

表示代码文件是UTF8的编码格式

编译:gcc -finput-charset=GBK -fexec-charset=UTF-8 -o main main.c 上面的编译命令你照做就可以了,如果你不带上面的参数编译会报错,如下: main.c:8:24: error: converting to execution character set: Invalid or incomplete multibyte or wide character 这是编码的问题,如果你在windos上编写代码保存的格式是GBK,gcc的编码格式默认是UTF-8。 源文件用不同的编码方式编写,会导致执行结果不一样。

 
[root@localhost ~]# man gcc |grep charset
       -fexec-charset=charset
           Set the execution character set, used for string and character constants.  The default is UTF-8.  charset can be any encoding supported by the system's "iconv" library
       -fwide-exec-charset=charset
           with -fexec-charset, charset can be any encoding supported by the system's "iconv" library routine; however, you will have problems with encodings that do not fit exactly in
       -finput-charset=charset
           option takes precedence if there's a conflict.  charset can be any encoding supported by the system's "iconv" library routine. 

-finput-charset 指定源文件的编码(若不指定,默认是UTF-8) -fexec-charset 指定多字节字符串(const char )常量在编译后的程序里保存的编码集(若不指定,默认是UTF-8) -fwide-exec-charset 指定宽字节字符串(const wchar_t )常量在编译后的程序里的保存的编码集

查看更多关于字符编码&C语言的详细内容...

  阅读:44次