編程中經(jīng)常會涉及到字符編碼的知識础拨,容易混淆氮块,在這里總結(jié)一下。
編碼的作用
計算機處理都是使用二進制編碼進行處理的诡宗,所以在處理字符的時候需要將字符進行編碼滔蝉,映射成二進制序列,然后才能被計算機處理和傳輸塔沃。下面介紹一些常用的字符編碼蝠引。
ASCII
ASCII(美國信息交換標(biāo)準(zhǔn)碼)是我們接觸最多的字符編碼。它是隨著計算機誕生而產(chǎn)生的蛀柴,所有只用十進制的0-128表示一些字符螃概,其中也包括大小寫字母。這種編碼一直用到現(xiàn)在鸽疾,之后新產(chǎn)生的編碼都兼容ASCII碼吊洼。
打印出所有 ASCII 字符的C程序:
#include <stdio.h>
int main()
{
int i = 0;
for(i = 0; i < 128; i++)
{
printf("%d. %c\n", i, i);
}
return 0;
}
在打印出來之后,有些字符會無法顯示制肮,是因為 ASCII 包含了一些控制符等無法顯示的字符冒窍,例如退格等。ASCII 包含的所有字符可以查看維基百科豺鼻。
Unicode
隨著計算機的發(fā)展和普及综液,ASCII 編碼已經(jīng)不能滿足表示所有字符的需求,Unicode 這時候就誕生了儒飒,其作用就是用一套編碼來表示所有文字谬莹,使計算機能夠支持多語言環(huán)境。Unicode 說是編碼其實是一種字符集,包含了所有的字符附帽。
Unicode 一共定義了1114112個碼位(code point)(從0x000000到0x10FFFF)埠戳,表示方法為用“U+”或者"\u"后跟一個十六進制數(shù)。這么多字符基本上可以包含世界上所有的字符了士葫。但是它并沒有規(guī)定計算機如何存儲這些字符乞而,并且還存在很多問題送悔,比如:
這里就有兩個嚴(yán)重的問題慢显,第一個問題是,如何才能區(qū)別 Unicode 和 ASCII 欠啤?計算機怎么知道三個字節(jié)表示一個符號荚藻,而不是分別表示三個符號呢?第二個問題是洁段,我們已經(jīng)知道,英文字母只用一個字節(jié)表示就夠了,如果 Unicode 統(tǒng)一規(guī)定稽荧,每個符號用三個或四個字節(jié)表示嘶伟,那么每個英文字母前都必然有二到三個字節(jié)是
0
,這對于存儲來說是極大的浪費写半,文本文件的大小會因此大出二三倍岸蜗,這是無法接受的。
因此Unicode 定義了兩種映射方式叠蝇,其中一種叫做 Unicode Transformation Format璃岳,即 UTF,衍生出來的編碼方式就是我們常見的 UTF-8悔捶、UTF-16铃慷、UTF-32 等等,這些編碼名稱里面的數(shù)字代表用多少位表示 Unicode 中的碼位蜕该。
大小端模式
關(guān)于 Unicode 編碼的直接存儲犁柜,有兩種模式,一種是小端模式(Little Endian) 堂淡,一種是大端模式(Big Endian)赁温,例如漢字李
的 Unicode 碼是U+673E
,使用小端模式(字節(jié)的高位存儲在內(nèi)存的高位)存儲為3E 67
淤齐,使用大端模式(字節(jié)的高位存儲在內(nèi)存的低位)為67 3E
股囊。
那如何知道文件是使用大端模式還是小端模式呢,Unicode 規(guī)定每個文件的第一個字符用來表示編碼順序更啄,如果是 FE FF
稚疹,表示使用大端模式,如果是FF FE
,表示使用小端模式内狗。
UTF-8
上面提到怪嫌,UTF-8 是用8位(即一個字節(jié))表示 Unicode 的碼位,但是很明顯8位是不夠的柳沙,所以 UTF-8 是一種變長編碼(最長為4個字節(jié))岩灭,編碼規(guī)則如下:
字節(jié)數(shù) | 第一個碼點 | 最后一個碼點 | 字節(jié)1 | 字節(jié)2 | 字節(jié)3 | 字節(jié)4 |
---|---|---|---|---|---|---|
1 | U+0000 | U+007F | 0xxxxxxx | |||
2 | U+0080 | U+07FF | 110xxxxx | 10xxxxxx | ||
3 | U+0800 | U+FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
4 | U+10000 | U+1FFFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
可以看到 UTF-8 將 Unicode 的所有碼點劃分為了四塊,并用不同的字節(jié)長度來表示赂鲤。其規(guī)定:當(dāng)字節(jié)的開頭是0時噪径,表示U+0000到U+007F的字符,即 ASCII 碼對應(yīng)的字符数初;當(dāng)字節(jié)的開頭是110的時候找爱,其表示加上后面的字節(jié),兩個字節(jié)一起表示一個字符泡孩。
舉例:A
在 Unicode 里為U+0041
车摄,二進制為 00000000 01000001
,根據(jù)上表得知使用一個字節(jié)來表示仑鸥,然后從二進制的最后一位開始吮播,替換上表的x,替換完成為 01000001
眼俊,即A
的 UTF-8 編碼為0x41
意狠;
舉例:漢字李
在 Unicode 里的碼位為U+673E
,二進制為0110 0111 0011 1110
泵琳,根據(jù)上表得知使用三個字節(jié)來表示(所有的漢字基本上都是用三個字節(jié)來表示)摄职,然后從二進制的最后一位開始,替換上表的x获列,替換完成為11100110 10011100 10111110
谷市,即李
的 UTF-8 編碼為 0xE6 0x9C 0xBE
Unicode 碼轉(zhuǎn)換成 UTF-8 的 C 代碼如下:
// Unicode 轉(zhuǎn) UTF8
// 需要保證char* utf8c至少有4字節(jié)的空間
// 返回值:返回編號后所占的字節(jié)數(shù),如果出錯返回-1
// 在此使用的是小端排序
int unicodeToUTF8(unsigned long unicode, char* utf8c)
{
if (unicode <= 0x007F)
{ // 10xxxxxx
*utf8c = (char)(unicode & 0x7F);
return 1;
}
if (unicode <= 0x07FF)
{ // 110xxxxx 10xxxxxx
*utf8c = (char)((unicode >> 6 & 0x1F) | 0xC0);
*(utf8c + 1) = (char)((unicode & 0x3F) | 0x80);
return 2;
}
if (unicode <= 0xFFFF)
{ // 1110xxxx 10xxxxxx 10xxxxxx
*utf8c = (char)((unicode >> 12 & 0x000F) | 0x00E0);
*(utf8c + 1) = (char)((unicode >> 6 & 0x003F) | 0x0080);
*(utf8c + 2) = (char)((unicode & 0x003F) | 0x0080);
return 3;
}
if (unicode <= 0x1FFFFF)
{
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
*utf8c = (char)((unicode >> 18 & 0x07) | 0xF0 );
*(utf8c + 1) = (char)((unicode >> 12 & 0x3F) | 0x80);
*(utf8c + 2) = (char)((unicode >> 6 & 0x3F) | 0x80);
*(utf8c + 3) = (char)((unicode & 0x3F) | 0x80);
return 4;
}
return -1;
}
UTF-8 轉(zhuǎn)換成 Unicode 的 C 代碼如下:
// 將 UTF-8 編碼轉(zhuǎn)換成 Unicode
// @utf8c: 需要轉(zhuǎn)換的utf8編碼的字符指針
// @Return: 返回轉(zhuǎn)換后的 Unicode 碼位
//
long utf8ToUnicode(unsigned char* utf8c)
{
// 判斷 utf8 編碼的長度
assert(utf8c != NULL);
int size = 0;
if ((*utf8c & 0x80) == 0x00) size = 1;
else if ((*utf8c & 0xE0) == 0xC0 && (*(utf8c + 1) & 0xC0) == 0x80)
size = 2;
else if ((*utf8c & 0xF0) == 0xE0 && (*(utf8c + 1) & 0xC0) == 0x80
&& (*(utf8c + 2) & 0xC0) == 0x80)
size = 3;
else if ((*utf8c & 0xF8) == 0xF0 && (*(utf8c + 1) & 0xC0) == 0x80
&& (*(utf8c + 2) & 0xC0) == 0x80 && (*(utf8c + 3) & 0xC0) == 0x80)
size = 4;
else return -1;
if (size == 1) return *utf8c & 0x7F;
if (size == 2) return ((*utf8c & 0x1F) << 6 )| (*(utf8c + 1) & 0x3F);
if (size == 3)
return (*utf8c & 0x0F) << 12 | ((*(utf8c + 1) & 0x3F) << 6) | (*(utf8c + 2) & 0x3F);
return (*utf8c & 0x07) << 18 | ((*(utf8c + 1) & 0x3F) << 12) | ((*(utf8c + 2) & 0x3F) << 6) | (*(utf8c + 3) & 0x3F);
}
------------------- 2018.12.16 更新------------------------
在V2EX上看到一個帖子击孩,是在說為什么UTF-8編碼不利用一個區(qū)間的所有碼點迫悠。例如,在雙字節(jié)表示中巩梢,110xxxxx 10xxxxxx
一共有個碼點可以使用创泄,而[0x80, 0x7ff]一共只有1920個碼點,低位的128個碼點都被浪費了(從
11000000 10000000
到 11000001 10111111
)括蝠。
在下面的回復(fù)中我覺得比較對的是說 如果使用11000001 10111111
, 其對應(yīng)的Unicode碼點為U+007F鞠抑,且11000000 10000000
對應(yīng)的Unicode碼點為U+0000,也就是ASCII碼的范圍忌警,表示范圍重復(fù)(用單字節(jié)就可以表示搁拙,所以雙字節(jié)從11000010 10000000
開始)
GB2312
GB2312 是由中國發(fā)布的一個簡體中文字符集,基本滿足了漢字的計算機處理需求,但是一些罕用字和繁體字還沒有包含在里面箕速。GB2312 把漢字進行了分區(qū)處理酪碘,每個區(qū)含有 94 個漢字/符號,一共有 94 個區(qū)盐茎,每個字符用其所在的區(qū)和位來表示兴垦。
GB2312 的編碼方法如下:
每個漢字及符號通過兩個字節(jié)來表示,第一個字節(jié)(稱為高位字節(jié))范圍為 0xA1-0xF7字柠,即字符的區(qū)號加上 0xA0探越,第二個字節(jié)(稱為低位字節(jié))范圍為 0xA1-0xFE,即 1-94 加上 0xA0募谎, 由于一級漢字從 16 區(qū)開始扶关,到87區(qū)結(jié)束(包括87區(qū))阴汇,所以漢字區(qū)的“高位字節(jié)”范圍為 0xB0-0xF7, 低位字節(jié)的范圍為 0xA1-0xFE数冬。
GBK
GBK
是 Windows 系統(tǒng)使用的漢字編碼符,其起源是因為 GB2312
含有一些未收錄的字符搀庶,因此 GBK
利用 GB2312
未使用的編碼區(qū)間拐纱,對 GB2312
進行了擴展。
GBK
的編碼方式包括一字節(jié)和雙字節(jié)兩種:
- 一字節(jié)范圍為
00-7F
哥倔,與 ASCII 保持一致 - 雙字節(jié)的第一字節(jié)范圍為
81-FE
秸架,第二字節(jié)一部分在40-7E
,另一部分在80-FE
GBK
完全兼容 GB2312
咆蒿,維基百科鏈接
GBK 與 Unicode 的映射關(guān)系
由于 GBK
和 Unicode
并沒有直接的對應(yīng)關(guān)系东抹,我們在轉(zhuǎn)換的時候需要使用映射表來進行轉(zhuǎn)換。我們可以在網(wǎng)上找到對應(yīng)的映射表來進行轉(zhuǎn)換沃测,也可以使用 libiconv
庫來進行轉(zhuǎn)換缭黔。
libiconv 是一個專門用于字符編碼轉(zhuǎn)換的一個庫,其支持很多種編碼方式(具體請查看官方文檔)蒂破。在 Ubuntu 上默認(rèn)就已經(jīng)安裝了這個庫馏谨,下面是一個示例 C 程序:
#include <iconv.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
iconv_t fd = iconv_open("UTF-8", "GBK");
if (fd == 0) return -1;
size_t inLen = 10;
size_t outLen = 255;
char* inbuf = (char*)malloc(sizeof(char)* inLen);
char* outbuf = (char*)malloc(sizeof(char) * outLen);
bzero(outbuf, outLen * sizeof(char));
// iconv函數(shù)的第二個參數(shù)和第四個參數(shù)需要傳入指向輸入緩存和輸出緩存的指針(二級指針)
char *in = inbuf;
char *out = outbuf;
scanf("%s", inbuf);
iconv(fd, &in, &inLen, &out, &outLen);
printf("%s\n", outbuf);
free(inbuf);
free(outbuf);
iconv_close(fd);
}