关于字符编码这个展开来说有太多东西了,这里主要是想说一说最常说的ASCLL和Unicode字符编码的问题,这样至少你在用相关函数的时候,可以搞明白参数的真正含义。
ASCLL编码
计算机就是0和1的世界,所有的东西都是0和1演变而来,对字符来说,也是如此,每个字符在计算机内存中便是就是数字转换成的二进制0、1组合,这个数字就称为该字符的编码。
最常见的就是ACSLL编码了,整个码表对应如下:
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
可以看到0-127对应的英文中0-9,a-z,A-Z及控制字符(如换行字符 等),这些是最基本的英文表达,有了他们我们基本上可以组合出所有的英文表达了。
扩展ASCLL编码
但是这是远远不够的,一些其他欧美国家语言(如德文、法文)有多音字符,还有尽管在最开始ASCLL编码出现的时候还是命令行,但是人们还是想在这上面加些其他的玩意儿,这就有了如下的扩展ACSLL码表了:
可以看到128-255都是一些德文、法文多音字符(上面带点的字符...)和一些希腊符号及其他的符号。
但是注意ACSLL只有0-127字符是标准定义的,扩展ACSLL(128-255)编码表示的字符都是OEM厂商自己定义的,最常用的是IBM的。
再扩展ASCLL
但是这样的扩展在遇到中东文字尤其是东亚如中国的文字后又不行了,那么多汉字,如何用最开始的char(8 bit)编码来表示呢?
如下程序:
- #include <stdio.h>
- int main()
- {
- char a1[] = "a";
- char b1[] = "b";
- char c1[] = "c";
- char a2[] = "我";
- char b2[] = "你";
- char c2[] = "他";
- printf("a1:%d——b1:%d——c1:%d ", a1[0],b1[0],c1[0]);
- printf("a2:%x——b2:%x——c2:%x ", a2[0],b2[0],c2[0]);
- printf("a2:%x——b2:%x——c2:%x ", a2[1],b2[1],c2[1]);
- return 0;
- }
#include <stdio.h>
int main()
{
char a1[] = "a";
char b1[] = "b";
char c1[] = "c";
char a2[] = "我";
char b2[] = "你";
char c2[] = "他";
printf("a1:%d——b1:%d——c1:%d
", a1[0],b1[0],c1[0]);
printf("a2:%x——b2:%x——c2:%x
", a2[0],b2[0],c2[0]);
printf("a2:%x——b2:%x——c2:%x
", a2[1],b2[1],c2[1]);
return 0;
}
查看内存:
命令行输出结果为:
- a1:97——b1:98——c1:99
- a2:ffffffce——b2:ffffffc4——c2:ffffffcb
- a2:ffffffd2——b2:ffffffe3——c2:fffffffb
a1:97——b1:98——c1:99
a2:ffffffce——b2:ffffffc4——c2:ffffffcb
a2:ffffffd2——b2:ffffffe3——c2:fffffffb
哈哈,看到了吧,英文a、b、c在内存是一个字节,但是中文你、我、他在内存中就是两个字节了。
我们可以直接使用printf打印字符,但是一次只能打印一个字节,中文要如上一样分两次打印。
那么中文是如何编码的呢,我们知道传统的ASCLL一个字节最多也只能表示2^8=256个字符,这对于中文是远远不够的,那么就如上所说,中文编码在传统的ASCLL扩展编码的基础上将在第一个字节值是128-255时,在每个字节后附加一个字节,这样就可以表示的字符数为128*256=32768个,一般称为ANSI编码,这对于一般中文表达来说是够了的。
如上的我、你、他编码分别如下(注意低位在前):
我:0xd2ce
你:0xe3c4
他:0xfbcb
代码页
但是这对于一般的表达是够的,问题是中文还有繁体字,还有东亚其他国家的文字如韩国、日本以及中亚等象形字国家的文字,这么多文字也不是32768个编码能够全部表达的。怎么办呢?
针对不同国家不同地区,把常用字符组合成一个ASCLL扩展表并将它们编号,每一个编号表示的扩展表称为一个代码页,现在常见的代码页都有自己的名称,如936代码页就是我们常用的GBK编码,如
950 繁体中文
949 朝鲜语
936 简体中文
932 日语
都是各个国家和地区的代码页。
注意相同的编号在不同的代码页下表示的是不同的字符,
如0xd2ce在中文代码页936对应中文字符我,但是在日语代码页下就不是了。如果代码页设置不正确,很可能显示为乱码,下面我就会演示这一情况。
Unicode编码
上面我们看到了,不同的代码页整的太麻烦了,能不能有一种统一的编码方式来完成编码和字符对应呢?
Unicode就是干这个的。
不同于上面说的同一ANSI编码在不同的代码页下表示不同的字符,同一Unicode编码在任何时候都表示唯一的字符,这你就要问了这得多少个字节才能表示世界上所有的字符啊,哈哈,最简单的就是UTF32编码了,4个字节表示一个字符,总共2^32=4294967296,这下总算够用了。
但是问题是,对于网络传送等场合这样同一个字符要占4个字节,带宽内存什么的浪费了,人们就想到了各式各样的压缩算法:常用的UTF16一般使用两个字节表示常用字符,对于不能表示的或不常用的字符才使用32位编码,这是Windows程序默认的Unicode编码方式;UTF8编码更过分,按照不同的国家文字的多少分别使用1个字节、2个字节、3个字节和4个字节表示,常用于网络传速。
对于如下程序:
- #include <Windows.h>
- #include <stdio.h>
- #include <locale.h>
- int main()
- {
- wchar_t a1[] = L"a";
- wchar_t b1[] = L"b";
- wchar_t c1[] = L"c";
- wchar_t a2[] = L"我";
- wchar_t b2[] = L"你";
- wchar_t c2[] = L"他";
- wprintf_s(L"a1:%x——b1:%x——c1:%x ", a1[0], b1[0], c1[0]);
- wprintf_s(L"我:%x——你:%x——他:%x ", a2[0], b2[0], c2[0]);
- _locale_t lt = _get_current_locale();
- printf_s(" 之前代码页:%s ", (lt->locinfo)->lc_category[0].locale);
- _wsetlocale(LC_ALL, L"chs");
- lt = _get_current_locale();
- wprintf_s(L"现在代码页:%s ", (lt->locinfo)->lc_category[0].wlocale);
- wprintf_s(L" a1:%x——b1:%x——c1:%x ", a1[0], b1[0], c1[0]);
- wprintf_s(L"我:%x——你:%x——他:%x ", a2[0], b2[0], c2[0]);
- return 0;
- }
#include <Windows.h>
#include <stdio.h>
#include <locale.h>
int main()
{
wchar_t a1[] = L"a";
wchar_t b1[] = L"b";
wchar_t c1[] = L"c";
wchar_t a2[] = L"我";
wchar_t b2[] = L"你";
wchar_t c2[] = L"他";
wprintf_s(L"a1:%x——b1:%x——c1:%x
", a1[0], b1[0], c1[0]);
wprintf_s(L"我:%x——你:%x——他:%x
", a2[0], b2[0], c2[0]);
_locale_t lt = _get_current_locale();
printf_s("
之前代码页:%s
", (lt->locinfo)->lc_category[0].locale);
_wsetlocale(LC_ALL, L"chs");
lt = _get_current_locale();
wprintf_s(L"现在代码页:%s
", (lt->locinfo)->lc_category[0].wlocale);
wprintf_s(L"
a1:%x——b1:%x——c1:%x
", a1[0], b1[0], c1[0]);
wprintf_s(L"我:%x——你:%x——他:%x
", a2[0], b2[0], c2[0]);
return 0;
}
查看内存:
程序运行结果为:
- a1:61
- 之前代码页:(null)
- 现在代码页:Chinese (Simplified)_People's Republic of China.936
- a1:61——b1:62——c1:63
- 我:6211——你:4f60——他:4ed6
a1:61
之前代码页:(null)
现在代码页:Chinese (Simplified)_People's Republic of China.936
a1:61——b1:62——c1:63
我:6211——你:4f60——他:4ed6
现在看到了乱码现象吧?第一次输出a1,b1,c1,a2,b2,c2时只能输出一部分,转换不正确出不来。
这是因为控制台程序显示只能使用ANSI方式,可以右键查看属性,如下:
即显示默认的是使用简体中文代码页936,即GBK编码。
我们在内存中的字符编码是Unicode方式,要显示到GBK编码的控制台窗口上,必须经过编码转换,那么编码转换依据什么呢,就是依靠当前的代码页来完成转换的。
如对于字符"我"的转换过程:
Unicode编码:0x6211->对应字符:"我"->查询代码页936->ANSI编码:0xd2ce
反之亦然。
最开始没有正确设置转换用的代码页,所以转换不正确,也就不能正确显示,设置转换用的代码页后就可以正常显示了。
在windows中的编码转换函数MultiByteToWideChar和WideCharToMultiByte的第一个参数都是codepage,即设定用于编码转换的表(代码页)。
现在你应该明白为什么很多时候和场合要设置locale了吧,设置不同的locale就是对应不同的代码页,这样显示转换才能成功,对应的常见场合有网页显示和数据库数据读取。
那关于编码的问题就这么多了,弄懂整个编码演变历史和过程,我们使用相关函数的时候就没有什么大问题了。
演示源代码下载链接
原创,转载请注明来自http://blog.csdn.net/wenzhou1219