基礎(chǔ)知識 | 字符編碼

編程中經(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一共有2^{11}個碼點可以使用创泄,而[0x80, 0x7ff]一共只有1920個碼點,低位的128個碼點都被浪費了(從11000000 1000000011000001 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)系

由于 GBKUnicode 并沒有直接的對應(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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市附迷,隨后出現(xiàn)的幾起案子惧互,更是在濱河造成了極大的恐慌,老刑警劉巖喇伯,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喊儡,死亡現(xiàn)場離奇詭異,居然都是意外死亡稻据,警方通過查閱死者的電腦和手機艾猜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人箩朴,你說我怎么就攤上這事岗喉。” “怎么了炸庞?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵钱床,是天一觀的道長。 經(jīng)常有香客問我埠居,道長查牌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任滥壕,我火速辦了婚禮纸颜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘绎橘。我一直安慰自己胁孙,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布称鳞。 她就那樣靜靜地躺著涮较,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冈止。 梳的紋絲不亂的頭發(fā)上狂票,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音熙暴,去河邊找鬼闺属。 笑死,一個胖子當(dāng)著我的面吹牛周霉,可吹牛的內(nèi)容都是我干的掂器。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诗眨,長吁一口氣:“原來是場噩夢啊……” “哼唉匾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匠楚,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巍膘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后芋簿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峡懈,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年与斤,在試婚紗的時候發(fā)現(xiàn)自己被綠了肪康。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荚恶。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖磷支,靈堂內(nèi)的尸體忽然破棺而出谒撼,到底是詐尸還是另有隱情,我是刑警寧澤雾狈,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布廓潜,位于F島的核電站,受9級特大地震影響善榛,放射性物質(zhì)發(fā)生泄漏辩蛋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一移盆、第九天 我趴在偏房一處隱蔽的房頂上張望悼院。 院中可真熱鬧,春花似錦咒循、人聲如沸据途。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昨凡。三九已至爽醋,卻和暖如春蚁署,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蚂四。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工光戈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遂赠。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓久妆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親跷睦。 傳聞我的和親對象是個殘疾皇子筷弦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 字符是用戶可以讀寫的最小單位烂琴。計算機所能支持的字符組成的集合,就叫做字符集蜕乡。字符集通常以二維表的形式存在奸绷。二維表的...
    劉惜有閱讀 8,116評論 2 14
  • 又一次的對未來充滿了迷茫,在這里工作感覺不到任何的意義层玲,看不到前途和方向号醉,是我的欲望大于了我的能力了是嗎反症? 身體的...
    東北郭先生閱讀 374評論 0 0
  • “二十來年了,這日子跟他過的畔派,沒有一天讓你省心的铅碍。”灰蒙蒙的朦朧中线椰,我隨著標(biāo)嫂往她家后院的柴禾垛走去该酗。從她們...
    AA皓月蒼穹閱讀 157評論 0 1
  • 昨天有關(guān)這句話問了幾個朋友呜魄,有不同的觀點。 有人認(rèn)為:孩子這是在跟家長講條件莱衩,側(cè)面威脅爵嗅,什么都聽孩子的,依著孩子來...
    燕燕細語閱讀 263評論 1 0
  • 第三個周日被加班了笨蚁,無法拒絕睹晒,唯有接受。校正不良心態(tài)括细,堅忍伪很,堅守,堅持奋单,放棄不切實際的幻想锉试。有句歌詞“除了自己,沒...
    七月紫蘇閱讀 186評論 0 0