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

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

由于 GBKUnicode 并沒有直接的對(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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 字符是用戶可以讀寫的最小單位。計(jì)算機(jī)所能支持的字符組成的集合,就叫做字符集。字符集通常以二維表的形式存在。二維表的...
    劉惜有閱讀 8,380評(píng)論 2 14
  • 又一次的對(duì)未來充滿了迷茫,在這里工作感覺不到任何的意義,看不到前途和方向,是我的欲望大于了我的能力了是嗎? 身體的...
    東北郭先生閱讀 435評(píng)論 0 0
  • “二十來年了,這日子跟他過的,沒有一天讓你省心的?!被颐擅傻碾鼥V中,我隨著標(biāo)嫂往她家后院的柴禾垛走去。從她們...
    AA皓月蒼穹閱讀 257評(píng)論 0 1
  • 昨天有關(guān)這句話問了幾個(gè)朋友,有不同的觀點(diǎn)。 有人認(rèn)為:孩子這是在跟家長(zhǎng)講條件,側(cè)面威脅,什么都聽孩子的,依著孩子來...
    燕燕細(xì)語閱讀 324評(píng)論 1 0
  • 第三個(gè)周日被加班了,無法拒絕,唯有接受。校正不良心態(tài),堅(jiān)忍,堅(jiān)守,堅(jiān)持,放棄不切實(shí)際的幻想。有句歌詞“除了自己,沒...
    七月紫蘇閱讀 218評(píng)論 0 0

友情鏈接更多精彩內(nèi)容