字元編碼與gcc 編譯器的編碼問題
最近在 vscode 中藉助 gcc 編譯器來配置 c
語言開發環境時,發現中文編碼存在亂碼問題。再加上最近學習到多位元組字元與寬字元,攪在一起,搞得很亂,就把自己的理解寫下來,供有需者參考吧。
1. 字元編碼
先來看維基中關於字元編碼的描述
字元編碼
字元編碼(英語:Character
encoding)、字集碼是把字符集中的字元編碼為指定集合中某一物件(例如:位元模式、自然數序列、8位元組或者電脈衝),以便文字在計算機中儲存和通過通訊網路的傳遞。常見的例子包括將拉丁字母表編碼成摩斯電碼和ASCII。其中,ASCII將字母、數字和其它符號編號,並用7位元的二進位制來表示這個整數。通常會額外使用一個擴充的位元,以便於以1個位元組的方式儲存。
在計算機技術發展的早期,如ASCII(1963年)和EBCDIC(1964年)這樣的字符集逐漸成為標準。但這些字符集的侷限很快就變得明顯,於是人們開發了許多方法來擴充套件它們。對於支援包括東亞CJK字元家族在內的寫作系統的要求能支援更大量的字元,並且需要一種系統而不是臨時的方法實現這些字元的編碼
關於字元編碼的詳細介紹,可以參考 字元編碼筆記
Windows 現在預設所用的漢字編碼仍是 GBK,而 字元編碼筆記中沒有提及,, 因此以下對 GBK 編碼進行相應的介紹。
1.1 GBK 編碼
漢字內碼擴充套件規範
漢字內碼擴充套件規範,稱GBK,全名為《漢字內碼擴充套件規範(GBK)》1.0版,由中華人民共和國全國資訊科技標準化技術委員會1995年12月1日製訂,國家技術監督局標準化司和電子工業部科技與質量監督司1995年12月15日聯合以《技術標函[1995]229號》檔案的形式公佈。
GBK共收錄21886個漢字和圖形符號,其中漢字(包括部首和構件)21003個,圖形符號883個。
GBK的K為“擴充套件”的漢語拼音(kuòzhǎn)第一個聲母。英文全稱Chinese Internal Code Extension
Specification。字元有一位元組和雙位元組編碼,00–7F範圍內是第一個位元組,和ASCII保持一致,此範圍內嚴格上說有96個文字和32個控制符號。
之後的雙位元組中,前一位元組是雙位元組的第一位。總體上說第一位元組的範圍是81–FE(也就是不含80和FF),第二位元組的一部分領域在40–7E,其他領域在80–FE
也就是說在 GBK 編碼中
- 對於單位元組的字元,位元組的第一位為 0,後面 7 位為這個符號的 Unicode 碼
- 對於雙位元組的字元,位元組的第一位為1, 後面的第二位要遵循上面提及的規則
2. 多位元組字元與寬字元
由上面關於編碼的介紹可知,一個字元可能佔據一個位元組,也可能佔據兩個位元組。由於字元在實際儲存時,都是二進位制的格式,因此需要藉助額外的資訊才能判斷出字元的實際位元組數。如,對於GBK編碼來說,其首位為 0,說明其只有 1 個位元組;首位為 1,則說明其有兩個位元組。
為了避免需要額外的資訊,才能判斷出字元中實際的位元組數,則就需要引入寬字元(C語言中的定義為 wchar_t)。c 語言中的寬字元佔據 2 個位元組,其優點是能夠加快字元的解析速度(應為不再需要判斷字元的個數),其缺點也顯而易見,就是會增加記憶體空間的佔用(因為能在多位元組字元中用一個字元表示的字元,用寬位元組也必須要用兩個字元來表示)
多位元組字元和寬字元,有點類似於演算法中運算速度和記憶體佔用的問題,魚與熊掌,不可兼得也。
3. gcc 編譯器配置以及相關例項
之所以會對編碼進行深入的學習,是因為最近在 vscode 上藉助 gcc 來配置 c 語言開發環境時,碰到了漢字亂碼的問題,再加了最近在學習寬字元,所以對編碼知識進行了深入的學習
所用的 gcc 編譯環境如下:
gcc version
8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)
gcc 編譯時,預設會按照 c 檔案的編碼進行編碼,所以 c 檔案的編碼要和 cmd 命令視窗的編碼格式相同,不然就會出亂碼。
windows cmd 的預設的編碼為 gbk,如下圖:
因此,c 檔案的編碼也要為 gbk,才能保證漢字不亂碼。當然也可以通過 chcp 65001
來將cmd 的編碼格式改為utf-8,這樣 utf-8 編碼的c 檔案輸出的漢字就不會亂碼。
如果要把 cmd 改為預設的編碼,則使用 chcp936
命令即可。
當然,如果不想修改預設的 cmd 編碼,又想避免由於 c 檔案的編碼與 cmd 預設編碼不匹配所導致的亂碼問題,可以在 gcc 的編譯選項中加入 -fexec-charset=gbk
來避免亂碼。
3.1 多位元組字元例項
#include<stdio.h>
#include<string.h>
void main()
{
char str[10] = "李";
int a, b, c;
printf("%#X %#X %#X\n", (unsigned char)str[0], (unsigned char)str[1], (unsigned char)str[2]);
printf("length: %d\n", strlen(str));
a = printf("%c%c%c", str[0], str[1], str[2]); // 輸出 3 個位元組
putchar('\n');
b = printf("%c%c", str[0], str[1]); // 輸出 2 個位元組
putchar('\n');
printf("a = %d, b = %d", a, b);
}
編譯時採用 utf-8 編碼
gcc -fexec-charset=gbk utf8.c -o utf8.exe
cmd 視窗採用 utf-8 編碼的輸出
從圖中的結果可看出,連續輸出 3 個位元組可以輸出正確的漢字,這是由於 utf-8 中,常用漢字為 3 個位元組。
只輸出兩個位元組時,沒法顯示內容。從 b = 2 也可以看出,輸出成功,說明不能顯示的原因是 cmd 解析出錯。出錯的原因是因為其第一個位元組已經指定了當前字元包含 3 個位元組,而實際只輸出兩個位元組,導致 cmd 視窗解析失敗。
utf-8 的編碼規則
UTF-8 的編碼規則很簡單,只有二條:
1)對於單位元組的符號,位元組的第一位設為
0
,後面7位為這個符號的 Unicode 碼。因此對於英語字母,UTF-8 編碼和 ASCII 碼是相同的。2)對於
n
位元組的符號(n > 1
),第一個位元組的前n
位都設為1
,第n + 1
位設為0
,後面位元組的前兩位一律設為10
。剩下的沒有提及的二進位制位,全部為這個符號的 Unicode 碼。下表總結了編碼規則,字母
x
表示可用編碼的位。Unicode符號範圍 | UTF-8編碼方式 (十六進位制) | (二進位制) ----------------------+--------------------------------------------- 0000 0000-0000 007F | 0xxxxxxx 0000 0080-0000 07FF | 110xxxxx 10xxxxxx 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
cmd 視窗採用 gbk 編碼輸出
- 鏉(shòu 鋒利的意思) 對應的 GBK 編碼是 E6 9D,因此列印 3 個位元組和 2 個位元組時,由於 cmd 的顯示的編碼為 GBK,所以會輸出 鏉,跟實際檔案中的”李“相比,就是亂碼
- 輸出 3 個位元組時,E6 9D 會被解析為 鏉。8E 會解析失敗,因為其首位大於 1,按照 GBK 的編碼規則,其應該有兩個位元組,而其只有一個位元組,因此解析失敗,沒有輸出
3.2 寬字元例項
// 檔名 wide_char.c,編碼 utf-8
#include<stdio.h>
#include<locale.h>
void main()
{
setlocale(LC_CTYPE, ""); // 設定本地化,不然寬字元無法正常顯示
wchar_t wch = L'李'; // 寬字元的定義
wprintf(L"%c\n", wch);
printf("%lc\n", wch);
// 輸出結果為 2,2 說明一個寬字元佔據兩個字元
printf("%d %d\n", sizeof(wch), sizeof(wchar_t));
// 編譯編碼為 utf-8 則輸出為 6,5
// 編譯編碼為 gbk, 則輸出結果為 6, 4
// 以上結果說明一個寬字元固定佔據兩個位元組
printf("%d %d", sizeof(L"1李"), sizeof("1李"));
}
編譯執行過程
從結果中可看出,在執行的過程中,沒有改變 cmd 的編碼格式,但是漢字輸出沒有亂碼。
c 語言的寬字元藉助 Unicode 來實現,因此在使用寬字元時, c 檔案的編碼格式最好是 utf-8。如果編碼格式為 gbk,則編譯時會報錯
相關網站
gbk 編碼查詢
utf-8編碼查