編碼格式介紹及C語言處理漢字編碼

什麼是編碼格式

從一個小問題引入

咱們在學習C語言的時候,有一道必作的題目是將大寫字母轉換成小寫,相信有點基礎的同窗都能不加思索的寫出下面的代碼:函數

char toLower(char upper){
    if (upper >= 'A' && upper <= 'Z'){
        return upper + 32;
    }else{
        return upper;
    }
}

要問爲何是這段代碼?咱們每每也能說得出:由於大小寫字母在ASCII碼上正好相差32(字符'a'爲97, 字符'A'爲65)。
咱們在進行字符初始化的時候,每每會將字符初始化爲'\0'。由於'\0'在ASCII碼中對應的數值是0。
咱們理所應當地知道char型字符對應的範圍是0~127,由於ASCII碼的範圍就是0~127
可是有沒有想過,爲何是ASCII碼?
所謂的ASCII碼,又究竟是什麼?學習

編碼格式介紹

要提及ASCII碼,不得不提及編碼格式。
咱們知道,對於計算機來講,咱們在屏幕上看到的千姿百態的文字、圖片、甚至視頻是不能直接識別的,而是要經過某種方式轉換爲0和1組成的二進制的機器碼,最終被計算機識別(0爲低電平,1爲高電平)。
對於數字來講,有一套很是成熟的轉換方案,就是將十進制的數字轉換爲二進制,就能直接被計算機識別(如5轉換爲二進制是 0000 0101)。可是對於像ABCD這樣的英文字母,還有!@#$這樣的特殊符號,計算機是不能直接識別的,因此就須要有一套通用的標準來進行規範。
這套規範就是ASCII碼。
ASCII碼使用127個字符,表示A~Z等26個大小寫字母,包含數字0~9,全部標點符號以及特殊字符,甚至還有不能在屏幕上直接看到的好比回車、換行、ESC等。
ASCII碼錶.png
按照這套SACII的編碼標準,就很容易的知道,'\0'表明的是0, 'A'表明的是65,而'a'表明的是97,'A''a'之間正好相差了32。
ASCII碼雖然只有127位,但基本實現了對全部英文的支持。因此爲何說char類型只佔1個字節?由於char型最大的數字是127,轉成二進制也不過是0111 1111,只須要1個字節就能表示全部的char型字符,所以char只佔1個字節。
可是隨着計算機的普及,計算機不但要處理英文,還有漢字、甚至希臘文字、韓文、日文等諸多文字,這時,127個字符確定不夠了,這時就引入了Unicode的概念。
Unicode是一個編碼字符集,它基本涵蓋了世界上絕大多數的文字(只有極少數沒有包含),在Unicode中文對照表中能夠查看一些漢字的Unicode字符集。
好比,漢字」七「在Unicode表示爲十六進制0x4e03,表示成二進制位0100 1110 0000 0011,佔了15位,至少須要兩個字節才能放得下,有些更復雜的生僻字,可能佔用的字節數甚至不止兩位。
這就面臨着一個問題,當一箇中英文夾雜的字符串輸入到電腦的時候,計算機是如何知道它究竟是什麼的?
就像上面的0100 1110 0000 0011,它究竟是表示的是0100 11100000 0011兩個ASCII字符,仍是漢字」七「?計算機並不知道。因此就須要一套規則來告訴計算機,到底該按照什麼來解析。這些規則,就是字符編碼格式。
其中就包括如下幾種。測試

  • ASCII
  • UTF-8
  • GBK
  • GB2312
  • GB18030
  • BIG5
  • ISO8859

編碼格式分類

ASCII

ASCII 編碼前面已經介紹過,此處就再也不多說了。它使用0~127這128位數字表明瞭全部的英文字母以及數字、標點、特殊符號和鍵盤上有但屏幕上看不見的特殊按鍵。
它的優勢是僅用128個數字就實現了對英文的完美支持,可是缺點也一樣明顯,不支持中文等除英文之外的其餘語言文字。
所以,ASCII碼基本能夠看作是其餘字符編碼格式的一個子集,其餘字符編碼都是在ASCII碼的基礎上實現了必定的擴展,但毫無心外地,都實現了對ASCII碼的兼容。編碼

UTF-8

在漢字環境下,UTF-8能夠說是最多見的編碼。它是Windows系統默認的文本編碼格式。
UTF-8是一種變長的編碼方式,最大能夠支持到6位。這就意味着他能夠有效地節省空間(在後面介紹GBK的時候,會講GBK是固定長度的編碼方式)。
那麼,UTF8是如何知道當前所要表達的字符是幾個字節呢?
UTF8中,它以首字節的高位做爲標識,用來區別當前字節的長度。其規則大體以下:spa

1字節 0xxxxxxx (範圍:0x00-0x7F)
2字節 110xxxxx 10xxxxxx (範圍:0x80-0x7ff)
3字節 1110xxxx 10xxxxxx 10xxxxxx (範圍:0x800-0xffff)
4字節 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (範圍:0x10000-0x10ffff)
5字節 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6字節 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

如上面的漢字」七「的unicode碼是0x4e03,在0x800-0xffff區間,因此是3字節,用UTF-8表示就是11100100 10111000 10000011(十六進制表示爲0xe4b883)。
"七"的Unicode碼是0100 1110 0000 0011,可爲何是這個數呢?
根據3字節的填充規則,從右往左,依次填充x的位置:翻譯

       0100     111000     000011
+
1110xxxx 10xxxxxx 10xxxxxx
=
11100100 10111000 10000011

事實上,utf-8編碼下,漢字都爲3字節。
實際上UTF家族除了UTF-8外,還有UTF-16UTF-32等,因爲不太經常使用,此處也就不展開討論了。設計

GBK/GB2312/GB18030

GB就是」國標「的拼音開頭,顧名思義,以GB開頭的編碼都是中國人專門爲支持漢語而設計的編碼格式。但這三者又有區別,最先出現的是GB2312,它收錄了6763個漢字,基本知足了計算機對漢字的處理須要。
GB2312使用雙字節表示一個漢字。對漢字進行分區處理。每一個區含有94個漢字(或符號),這種表示方式稱之爲區位碼。3d

  • 01-09 區爲特殊符號。
  • 16-55 區爲一級漢字,按拼音排序。
  • 56-87 區爲二級漢字,按部首/筆畫排序。
  • 10-15 區及 88-94 區則未有編碼。

GB2312編碼範圍:A1A1-FEFE,其中漢字編碼範圍:B0A1-F7FE。表示漢字時,第一字節0xB0-0xF7(對應區號:16-87),第二個字節0xA1-0xFE(對應位號:01-94)。
GBK是在GB2312基礎上的擴展。GBKK就是擴展的」擴「的拼音首字母。所以,GBK向下兼容GB2312
GBK也使用雙字節表示漢字,其中首字節範圍0x81-0xfe,第二個字節範圍0x40-0xfe,剔除0x7F一條線。所以,GBK所能表示的漢字比GB2312要多得多(能表示21886個漢字)。
GB18030是最新的內碼字集,能夠表示70244個漢字。它與UTF-8相似,採用多字節編碼,每一個漢字由一、二、4個字節組成。rest

  • 單字節,其值從 0 到 0x7F,與 ASCII 編碼兼容。
  • 雙字節,第一個字節的值從 0x81 到 0xFE,第二個字節的值從 0x40 到 0xFE(不包括0x7F),與 GBK 標準兼容。
  • 四字節,第一個字節的值從 0x81 到 0xFE,第二個字節的值從 0x30 到 0x39,第三個字節從0x81 到 0xFE,第四個字節從 0x30 到 0x39。

若是你看到這個地方已經以爲很亂了,沒關係。咱們只須要知道,在GB打頭的編碼格式下,咱們可以用鍵盤敲出來的,你在電腦上所看見的全部漢字,都是雙字節的(四字節的漢字極少,只有一些極少數不經常使用的生僻字用到)。code

BIG5

BIG5,從字面翻譯來看,叫作」大五碼「,它主要用來表示中文繁體字。
它也是用雙字節表示一個漢字,其中高位字節使用了0x81-0xFE,低位字節使用了0x40-0x7E,及0xA1-0xFE。。
這種編碼格式用的比較少,此處就不展開說了。

漢字編碼

上面介紹的幾種編碼格式,UTF-8GBK等都支持漢字,可是標準不一樣,所以,在實際進行開發的過程當中,對漢字的處理也不盡相同。

如何判斷漢字編碼

不管是UTF-8GBK,仍是GB18030,或者BIG5,它都是向下兼容ASCII的,爲了區分ASCII碼和漢字,在漢字的高位補1。
這也就是說,若是咱們以int的形式取出單個字符的值,漢字都是小於0的。
所以,判斷是不是漢字也就變得簡單了:

enum boolean{true, false};
typedef int boolean;

boolean isChinese(char ch){
    return (ch < 0) ? true : false;
}

寫一段代碼驗證一下:

void test01(){
    char str[20];
    memset(str, 0, sizeof(str));
    strcpy(str, "hello漢字");
    for (int i = 0; i < strlen(str); i++){
        if (isChinese(str[i]) == true){
            printf("str[%d]: Chinese\n", i);
        }else{
            printf("str[%d]: English\n", i);
        }
    }
}

咱們在main函數裏調用test01函數,獲得以下結果:
image.png

由於在utf-8下,一個漢字佔3字節,因此後面從5~10這6個字節正好表明着2個漢字。
若是咱們把編碼改爲GB2312,運行能夠獲得以下結果:
image.png
能夠看到,只有最後4個字節是漢字,充分說明了GB2312編碼格式下,一個漢字佔2個字節。

如何處理漢字截斷問題

若是咱們把上面的字符串按字符打印出來,獲得下面的結果:
image.png
能夠看到,全部的漢字都亂碼了,緣由就在於,UTF-8編碼下,每一個漢字佔3個字節,一個字節不足以表示完整的漢字,因此打印出來都是亂碼的。
在實際開發中,比較常見的須要處理的問題是,截取必定長度的字符串,可是若是截取的位置正好是個漢字,不免會遇到漢字被截斷的問題。
那麼,這類問題如何處理呢?
根據漢字的編碼規則,咱們知道,UTF-8GBK對漢字的處理是不同的。
UFT-8一個漢字是3字節,且規則以下:

1110xxxx 10xxxxxx 10xxxxxx

因此,咱們很容易知道,漢字的首字節範圍爲11100000~11101111,轉成十六進制爲0xe0~0xef,第2、三字節的範圍爲10000000~10111111,轉成十六進制範圍爲0x80~0xbf
因此UTF-8的漢字截斷問題處理能夠以下:

void HalfChinese_UTF8(const char *input, size_t input_len, char *output, size_t *output_len)
{
    char current = *(input + input_len);
    if (isChinese(current) == false)
    {
        *output_len = input_len;
        strncpy(output, input, *output_len);
        return;
    }
    //漢字
    *output_len = input_len;
    //1110xxxx 10xxxxxx 10xxxxxx
    //第二位和第三位的範圍是10000000~10ffffff,轉成十六進制是0x80~0xbf,在這個範圍內都說明是漢字被截斷
    while ((current&0xff) < 0xc0 && (current&0xff) >= 0x80)
    {
        (*output_len)++;
        current = *(input + *output_len);
    }
    strncpy(output, input, *output_len);
}

該函數有四個參數,其中inputinput_len做爲原始輸入,input_len表明須要截取的位置,outputoutput_len做爲輸出,output爲截斷處理後的字符串,output_len爲截斷處理後的長度。
咱們使用下面的代碼進行測試:

void test02()
{
    char in[20], out[20];
    memset(in, 0, sizeof(in));
    memset(out, 0, sizeof(out));
    strcpy(in, "hello漢字");
    size_t out_len = 0;
    for (int i = 1; i <= strlen(in); i++)
    {
        HalfChinese_UTF8(in, i, out, &out_len);
        printf("out: %s\n", out);
    }
}

運行後結果以下:
image.png

若是是GBK編碼,要稍微麻煩一點。由於咱們知道,GBK是雙字節表示漢字,且第一個字節的值從 0x810xFE,第二個字節的值從 0x400xFE(不包括0x7F),單從字符的值沒法判斷究竟是漢字的首字節仍是後一個字節(由於兩者的值有重複部分)。
若是字符串純爲漢字倒還好辦,咱們已經知道漢字佔2個字節,直接根據長度的奇偶來判斷就能夠,但若是是中英文夾雜就不能採用這種方式了。
在這裏,我使用的是先對字符串進行一道過濾處理,判斷字符串中除掉英文字符後純漢字的長度,若是爲奇數,表明漢字被截斷,加1就能取其完整的漢字,若是是偶數,說明正好是一個完整的漢字,無需處理,直接返回便可。
代碼實現以下:

void HalfChinese_GBK(const char *input, size_t input_len, char *output, size_t *output_len){
    char current = *(input + input_len);
    if (isChinese(current) == false)
    {
        *output_len = input_len;
        strncpy(output, input, *output_len);
        return;
    }
    *output_len = input_len;
    if (MoveEnglish(input, input_len) %2 != 0){
        (*output_len)++;
    }
    strncpy(output, input, *output_len);
}

int MoveEnglish(const char *input, size_t input_len){
    int out_len = input_len;
    for (int i = 0; i < input_len; i++)
    {
        if (isChinese(input[i]) == false){
            out_len++;
        }
    }
    return (out_len > 0) ? out_len : 0;
}

一樣使用上面的測試代碼進行測試,獲得以下結果:
image.png

如何實現編碼之間互相轉換

既然編碼格式這麼多,那麼怎麼進行編碼之間的轉換呢?
在C語言下,主要是利用系統的iconv函數完成。
iconv函數包含在頭文件iconv.h中,其函數原型以下所示:

size_t iconv (iconv_t __cd, char **__restrict __inbuf,
                size_t *__restrict __inbytesleft,
                char **__restrict __outbuf,
                size_t *__restrict __outbytesleft);

第一個參數是轉換的一個句柄,由iconv_open函數建立,第二個參數是輸入的字符串,第三個參數是輸入字符串的長度,第四個參數是轉換後的輸出字符串,第五個參數是輸出字符串的長度。在編碼轉換完成以後,須要調用iconv_close函數關閉句柄。因此完整的調用順序爲:

  • iconv_open打開iconv句柄
  • 調用iconv進行編碼轉換
  • iconv_close關閉句柄

還有一點須要注意的是,__inbytesleft__outbytesleft的長度,由於不一樣編碼對於漢字的處理字節數不一樣,好比從UTF-8轉換爲GBK,一樣都是兩個漢字,轉換前長度爲6,轉換後長度爲4。也就是說,在編碼轉換過程當中,字符串可能會變長或縮短,若是長度不正確,很容易形成越界,從而致使錯誤。
完整的編碼轉換功能封裝以下:

boolean convert_encoding(char *in, size_t in_len, char *out, size_t out_len, const char *from, const char *to)
{
    if (strcasecmp(from, to) == 0){
        size_t len = (in_len < out_len) ? in_len : out_len;
        memcpy(out, in, len);
        return true;
    }

    iconv_t cd = iconv_open(from, to);
    if (cd == (iconv_t)-1){
        printf("iconvopen err\n");
        return false;
    }
    size_t inbytesleft = in_len;
    size_t outbytesleft = out_len;

    char *src = in;
    char *dst = out;

    size_t nconv;
    nconv = iconv(cd, &src, &inbytesleft, &dst, &outbytesleft);
    if (nconv == (size_t)-1){
        if (errno == EINVAL){
            printf("EINVAL\n");
        } else {
            printf("error:%d\n", errno);
        }
    }
    iconv_close(cd);
    return true;
}

注意,因爲使用到了libiconv,編譯時須要加-liconv進行連接。
測試代碼以下:

void test04()
{
    char in[20], out[20];
    memset(in, 0, sizeof(in));
    memset(out, 0, sizeof(out));
    strcpy(in, "hello漢字world");
    if (false == convert_encoding(in, strlen(in), out, 20, "utf-8", "gbk")){
        printf("failed\n");
        return;
    }
    printf("in: %s\nout:%s\n", in, out);
}

以上代碼運行結果以下所示:
image.png

GBK轉換爲UTF-8也是一樣的操做,此處就不作演示了。

相關文章
相關標籤/搜索