什麼是編碼格式
從一個小問題引入
我們在學習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
- 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中,它以首字節的高位作為標識,用來區別當前字節的長度。其規則大致如下:
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等,由於不太常用,此處也就不展開討論了。
GBK/GB2312/GB18030
GB就是”國標“的拼音開頭,顧名思義,以GB開頭的編碼都是中國人專門為支持漢語而設計的編碼格式。但這三者又有區別,最早出現的是GB2312,它收錄了6763個漢字,基本滿足了計算機對漢字的處理需要。
GB2312使用雙字節表示一個漢字。對漢字進行分區處理。每個區含有94個漢字(或符號),這種表示方式稱之為區位碼。
- 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基礎上的擴展。GBK的K就是擴展的”擴“的拼音首字母。因此,GBK向下兼容GB2312。
GBK也使用雙字節表示漢字,其中首字節範圍0x81-0xfe,第二個字節範圍0x40-0xfe,剔除0x7F一條線。因此,GBK所能表示的漢字比GB2312要多得多(能表示21886個漢字)。
GB18030是最新的內碼字集,可以表示70244個漢字。它與UTF-8類似,採用多字節編碼,每個漢字由1、2、4個字節組成。
- 單字節,其值從 0 到 0x7F,與 ASCII 編碼兼容。
- 雙字節,第一個字節的值從 0x81 到 0xFE,第二個字節的值從 0x40 到 0xFE(不包括0x7F),與 GBK 標準兼容。
- 四字節,第一個字節的值從 0x81 到 0xFE,第二個字節的值從 0x30 到 0x39,第三個字節從0x81 到 0xFE,第四個字節從 0x30 到 0x39。
如果你看到這個地方已經覺得很亂了,不要緊。我們只需要知道,在GB打頭的編碼格式下,我們能夠用鍵盤敲出來的,你在電腦上所看見的所有漢字,都是雙字節的(四字節的漢字極少,只有一些極少數不常用的生僻字用到)。
BIG5
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,第二、三字節的範圍為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也是同樣的操作,此處就不做演示了。