咱們在學習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等。
按照這套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 1110
和0000 0011
兩個ASCII
字符,仍是漢字」七「?計算機並不知道。因此就須要一套規則來告訴計算機,到底該按照什麼來解析。這些規則,就是字符編碼格式。
其中就包括如下幾種。測試
ASCII 編碼前面已經介紹過,此處就再也不多說了。它使用0~127這128位數字表明瞭全部的英文字母以及數字、標點、特殊符號和鍵盤上有但屏幕上看不見的特殊按鍵。
它的優勢是僅用128個數字就實現了對英文的完美支持,可是缺點也一樣明顯,不支持中文等除英文之外的其餘語言文字。
所以,ASCII碼基本能夠看作是其餘字符編碼格式的一個子集,其餘字符編碼都是在ASCII碼的基礎上實現了必定的擴展,但毫無心外地,都實現了對ASCII碼的兼容。編碼
在漢字環境下,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-16
、UTF-32
等,因爲不太經常使用,此處也就不展開討論了。設計
GB
就是」國標「的拼音開頭,顧名思義,以GB
開頭的編碼都是中國人專門爲支持漢語而設計的編碼格式。但這三者又有區別,最先出現的是GB2312
,它收錄了6763個漢字,基本知足了計算機對漢字的處理須要。GB2312
使用雙字節表示一個漢字。對漢字進行分區處理。每一個區含有94個漢字(或符號),這種表示方式稱之爲區位碼。3d
GB2312
編碼範圍:A1A1-FEFE
,其中漢字編碼範圍:B0A1-F7FE
。表示漢字時,第一字節0xB0-0xF7
(對應區號:16-87),第二個字節0xA1-0xFE
(對應位號:01-94)。GBK
是在GB2312
基礎上的擴展。GBK
的K
就是擴展的」擴「的拼音首字母。所以,GBK
向下兼容GB2312
。GBK
也使用雙字節表示漢字,其中首字節範圍0x81-0xfe
,第二個字節範圍0x40-0xfe
,剔除0x7F
一條線。所以,GBK
所能表示的漢字比GB2312
要多得多(能表示21886個漢字)。GB18030
是最新的內碼字集,能夠表示70244個漢字。它與UTF-8
相似,採用多字節編碼,每一個漢字由一、二、4個字節組成。rest
若是你看到這個地方已經以爲很亂了,沒關係。咱們只須要知道,在GB
打頭的編碼格式下,咱們可以用鍵盤敲出來的,你在電腦上所看見的全部漢字,都是雙字節的(四字節的漢字極少,只有一些極少數不經常使用的生僻字用到)。code
BIG5
,從字面翻譯來看,叫作」大五碼「,它主要用來表示中文繁體字。
它也是用雙字節表示一個漢字,其中高位字節使用了0x81-0xFE,低位字節使用了0x40-0x7E,及0xA1-0xFE。。
這種編碼格式用的比較少,此處就不展開說了。
上面介紹的幾種編碼格式,UTF-8
、GBK
等都支持漢字,可是標準不一樣,所以,在實際進行開發的過程當中,對漢字的處理也不盡相同。
不管是UTF-8
、GBK
,仍是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
函數,獲得以下結果:
由於在utf-8
下,一個漢字佔3字節,因此後面從5~10這6個字節正好表明着2個漢字。
若是咱們把編碼改爲GB2312
,運行能夠獲得以下結果:
能夠看到,只有最後4個字節是漢字,充分說明了GB2312
編碼格式下,一個漢字佔2個字節。
若是咱們把上面的字符串按字符打印出來,獲得下面的結果:
能夠看到,全部的漢字都亂碼了,緣由就在於,UTF-8
編碼下,每一個漢字佔3個字節,一個字節不足以表示完整的漢字,因此打印出來都是亂碼的。
在實際開發中,比較常見的須要處理的問題是,截取必定長度的字符串,可是若是截取的位置正好是個漢字,不免會遇到漢字被截斷的問題。
那麼,這類問題如何處理呢?
根據漢字的編碼規則,咱們知道,UTF-8
和GBK
對漢字的處理是不同的。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); }
該函數有四個參數,其中input
和input_len
做爲原始輸入,input_len
表明須要截取的位置,output
和output_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); } }
運行後結果以下:
若是是GBK
編碼,要稍微麻煩一點。由於咱們知道,GBK
是雙字節表示漢字,且第一個字節的值從 0x81
到 0xFE
,第二個字節的值從 0x40
到 0xFE
(不包括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; }
一樣使用上面的測試代碼進行測試,獲得以下結果:
既然編碼格式這麼多,那麼怎麼進行編碼之間的轉換呢?
在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); }
以上代碼運行結果以下所示:
將GBK
轉換爲UTF-8
也是一樣的操做,此處就不作演示了。