編程中經(jīng)常會(huì)涉及到字符編碼的知識(shí),容易混淆,在這里總結(jié)一下。
編碼的作用
計(jì)算機(jī)處理都是使用二進(jìn)制編碼進(jìn)行處理的,所以在處理字符的時(shí)候需要將字符進(jìn)行編碼,映射成二進(jìn)制序列,然后才能被計(jì)算機(jī)處理和傳輸。下面介紹一些常用的字符編碼。
ASCII
ASCII(美國(guó)信息交換標(biāo)準(zhǔn)碼)是我們接觸最多的字符編碼。它是隨著計(jì)算機(jī)誕生而產(chǎn)生的,所有只用十進(jì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;
}
在打印出來之后,有些字符會(huì)無法顯示,是因?yàn)?ASCII 包含了一些控制符等無法顯示的字符,例如退格等。ASCII 包含的所有字符可以查看維基百科。
Unicode
隨著計(jì)算機(jī)的發(fā)展和普及,ASCII 編碼已經(jīng)不能滿足表示所有字符的需求,Unicode 這時(shí)候就誕生了,其作用就是用一套編碼來表示所有文字,使計(jì)算機(jī)能夠支持多語言環(huán)境。Unicode 說是編碼其實(shí)是一種字符集,包含了所有的字符。
Unicode 一共定義了1114112個(gè)碼位(code point)(從0x000000到0x10FFFF),表示方法為用“U+”或者"\u"后跟一個(gè)十六進(jìn)制數(shù)。這么多字符基本上可以包含世界上所有的字符了。但是它并沒有規(guī)定計(jì)算機(jī)如何存儲(chǔ)這些字符,并且還存在很多問題,比如:
這里就有兩個(gè)嚴(yán)重的問題,第一個(gè)問題是,如何才能區(qū)別 Unicode 和 ASCII ?計(jì)算機(jī)怎么知道三個(gè)字節(jié)表示一個(gè)符號(hào),而不是分別表示三個(gè)符號(hào)呢?第二個(gè)問題是,我們已經(jīng)知道,英文字母只用一個(gè)字節(jié)表示就夠了,如果 Unicode 統(tǒng)一規(guī)定,每個(gè)符號(hào)用三個(gè)或四個(gè)字節(jié)表示,那么每個(gè)英文字母前都必然有二到三個(gè)字節(jié)是
0,這對(duì)于存儲(chǔ)來說是極大的浪費(fèi),文本文件的大小會(huì)因此大出二三倍,這是無法接受的。
因此Unicode 定義了兩種映射方式,其中一種叫做 Unicode Transformation Format,即 UTF,衍生出來的編碼方式就是我們常見的 UTF-8、UTF-16、UTF-32 等等,這些編碼名稱里面的數(shù)字代表用多少位表示 Unicode 中的碼位。
大小端模式
關(guān)于 Unicode 編碼的直接存儲(chǔ),有兩種模式,一種是小端模式(Little Endian) ,一種是大端模式(Big Endian),例如漢字李的 Unicode 碼是U+673E,使用小端模式(字節(jié)的高位存儲(chǔ)在內(nèi)存的高位)存儲(chǔ)為3E 67,使用大端模式(字節(jié)的高位存儲(chǔ)在內(nèi)存的低位)為67 3E。
那如何知道文件是使用大端模式還是小端模式呢,Unicode 規(guī)定每個(gè)文件的第一個(gè)字符用來表示編碼順序,如果是 FE FF,表示使用大端模式,如果是FF FE,表示使用小端模式。
UTF-8
上面提到,UTF-8 是用8位(即一個(gè)字節(jié))表示 Unicode 的碼位,但是很明顯8位是不夠的,所以 UTF-8 是一種變長(zhǎng)編碼(最長(zhǎng)為4個(gè)字節(jié)),編碼規(guī)則如下:
| 字節(jié)數(shù) | 第一個(gè)碼點(diǎn) | 最后一個(gè)碼點(diǎn) | 字節(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 的所有碼點(diǎn)劃分為了四塊,并用不同的字節(jié)長(zhǎng)度來表示。其規(guī)定:當(dāng)字節(jié)的開頭是0時(shí),表示U+0000到U+007F的字符,即 ASCII 碼對(duì)應(yīng)的字符;當(dāng)字節(jié)的開頭是110的時(shí)候,其表示加上后面的字節(jié),兩個(gè)字節(jié)一起表示一個(gè)字符。
舉例:A在 Unicode 里為U+0041,二進(jìn)制為 00000000 01000001,根據(jù)上表得知使用一個(gè)字節(jié)來表示,然后從二進(jìn)制的最后一位開始,替換上表的x,替換完成為 01000001,即A的 UTF-8 編碼為0x41;
舉例:漢字李在 Unicode 里的碼位為U+673E,二進(jìn)制為0110 0111 0011 1110,根據(jù)上表得知使用三個(gè)字節(jié)來表示(所有的漢字基本上都是用三個(gè)字節(jié)來表示),然后從二進(jìn)制的最后一位開始,替換上表的x,替換完成為11100110 10011100 10111110,即李的 UTF-8 編碼為 0xE6 0x9C 0xBE
Unicode 碼轉(zhuǎn)換成 UTF-8 的 C 代碼如下:
// Unicode 轉(zhuǎn) UTF8
// 需要保證char* utf8c至少有4字節(jié)的空間
// 返回值:返回編號(hào)后所占的字節(jié)數(shù),如果出錯(cuò)返回-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 編碼的長(zhǎng)度
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上看到一個(gè)帖子,是在說為什么UTF-8編碼不利用一個(gè)區(qū)間的所有碼點(diǎn)。例如,在雙字節(jié)表示中,110xxxxx 10xxxxxx一共有個(gè)碼點(diǎn)可以使用,而[0x80, 0x7ff]一共只有1920個(gè)碼點(diǎn),低位的128個(gè)碼點(diǎn)都被浪費(fèi)了(從
11000000 10000000 到 11000001 10111111)。
在下面的回復(fù)中我覺得比較對(duì)的是說 如果使用11000001 10111111, 其對(duì)應(yīng)的Unicode碼點(diǎn)為U+007F,且11000000 10000000對(duì)應(yīng)的Unicode碼點(diǎn)為U+0000,也就是ASCII碼的范圍,表示范圍重復(fù)(用單字節(jié)就可以表示,所以雙字節(jié)從11000010 10000000開始)
GB2312
GB2312 是由中國(guó)發(fā)布的一個(gè)簡(jiǎn)體中文字符集,基本滿足了漢字的計(jì)算機(jī)處理需求,但是一些罕用字和繁體字還沒有包含在里面。GB2312 把漢字進(jìn)行了分區(qū)處理,每個(gè)區(qū)含有 94 個(gè)漢字/符號(hào),一共有 94 個(gè)區(qū),每個(gè)字符用其所在的區(qū)和位來表示。
GB2312 的編碼方法如下:
每個(gè)漢字及符號(hào)通過兩個(gè)字節(jié)來表示,第一個(gè)字節(jié)(稱為高位字節(jié))范圍為 0xA1-0xF7,即字符的區(qū)號(hào)加上 0xA0,第二個(gè)字節(jié)(稱為低位字節(jié))范圍為 0xA1-0xFE,即 1-94 加上 0xA0, 由于一級(jí)漢字從 16 區(qū)開始,到87區(qū)結(jié)束(包括87區(qū)),所以漢字區(qū)的“高位字節(jié)”范圍為 0xB0-0xF7, 低位字節(jié)的范圍為 0xA1-0xFE。
GBK
GBK 是 Windows 系統(tǒng)使用的漢字編碼符,其起源是因?yàn)?GB2312 含有一些未收錄的字符,因此 GBK 利用 GB2312 未使用的編碼區(qū)間,對(duì) GB2312 進(jìn)行了擴(kuò)展。
GBK 的編碼方式包括一字節(jié)和雙字節(jié)兩種:
- 一字節(jié)范圍為
00-7F,與 ASCII 保持一致 - 雙字節(jié)的第一字節(jié)范圍為
81-FE,第二字節(jié)一部分在40-7E,另一部分在80-FE
GBK 完全兼容 GB2312,維基百科鏈接
GBK 與 Unicode 的映射關(guān)系
由于 GBK 和 Unicode 并沒有直接的對(duì)應(yīng)關(guān)系,我們?cè)谵D(zhuǎn)換的時(shí)候需要使用映射表來進(jìn)行轉(zhuǎn)換。我們可以在網(wǎng)上找到對(duì)應(yīng)的映射表來進(jìn)行轉(zhuǎn)換,也可以使用 libiconv 庫(kù)來進(jìn)行轉(zhuǎn)換。
libiconv 是一個(gè)專門用于字符編碼轉(zhuǎn)換的一個(gè)庫(kù),其支持很多種編碼方式(具體請(qǐng)查看官方文檔)。在 Ubuntu 上默認(rèn)就已經(jīng)安裝了這個(gè)庫(kù),下面是一個(gè)示例 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ù)的第二個(gè)參數(shù)和第四個(gè)參數(shù)需要傳入指向輸入緩存和輸出緩存的指針(二級(jí)指針)
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);
}