編譯器對c++源文件編碼的識別
注意,這裏說的,是對於源文件編碼的識別,而不是用c++來讀寫文件。實際上,不同編譯器對於源文件編碼的識別時有差異的,這是你需要特別注意的一點。如果你在 Code Blocks 裏邊在源文件中寫了中文,用 sublime 打開卻發現出現亂碼,本文可以徹底解決這個問題。
基本知識和基本工具
本篇是整理時加上的一節,我覺得最好將一些基礎知識提前說清楚,便於之後的陳述。
文件編碼基礎知識
前面說完了基本的編碼知識,具體在文件中是這麼實現的呢。
- 文本文件
通常意義上理解的文本文件,就是隻保存字符的編碼信息,而不保存字符的字體、大小、顏色等信息的文件。事實上,所有的編程源文件都是文本文件。
在windows下我們新建一個文本文件,一般默認是使用Notepad這個軟件,默認編碼是ANSI。不過如果你採用其他軟件,例如Notepad++,vim,或者VS Code,結果可能是不盡相同。但是無論如何,字符在文件中保存,一定還是0101這樣的編碼(計算機上什麼東西不是這樣編碼的呢)。
- ANSI
這個基本上沒有什麼好說的,windows下使用記事本(Notepad)新建一個文本文件,就是ANSI編碼的。其在硬盤上保存的,就是這些字符的ANSI編碼。
- utf-8
現在有一個問題,同樣一個.txt文件,編輯器如何判斷它是什麼編碼呢。所以很多文件都有一些打頭的一些字節來表示自己的文件類型。一些utf-8文件就會有文件頭,稱爲BOM,utf-8文件的BOM是三個字節:0xef
,0xbb
,0xbf
。不過一些由於utf-8編碼的普及,現在無BOM的utf-8文件更多。
作爲utf-8擁護者,我們一般推崇無BOM的utf-8文件
比如 sublime 識別文件默認就是utf-8,如果文件編碼是utf-8,就會正確顯示,如果文件實際上是ANSI編碼,顯然不會有BOM,sublime 還是把他當成utf-8文件來讀,就會產生亂碼。
- utf-16文件
前面說過,utf-16實際上有兩種存儲方式,大端序和小端序,這關係到一個utf-16字符兩個字節哪個在硬盤中位置靠前。這個信息要告訴編輯器,就依靠文件頭了:
- 大端序utf-16BE,文件頭爲0xfe,0xff。
- 小端序utf-16LE,文件頭文0xff,0xfe。
- utf-32文件
目前我的工具無法查看utf-32文件,就不管了,一般來說utf-32文件不會用到。
工具
- Notepad++
我的主要工具就是Notepad++了,它會判斷文件編碼,可以很方便地轉換編碼,雖然我現在常用VS Code,不過寫此文時,Notepad++幫了大忙,如果你碰到亂碼文本文件,可以用他來查看。
- cmd(命令提示符)
要特別注意的是,命令提示符的輸入輸出默認編碼是ANSI,所以任何輸出字節都會按照ANSI解碼,然後再打印出來。所以,通常只有ANSI編碼字符串才能正確輸出,如何輸出其他編碼字符,是後面的課題。
python3的str
的默認編碼是utf-8,其可以正確輸出是python自己實現的。
c++字符串的基本知識
- 字符初始化
類型名 | 單元 | 初始化(注意前綴) | 編碼 |
---|---|---|---|
std::string |
char |
std::string s = "你好世界"; |
系統默認 |
std::string |
char |
std::string s = u8"你好世界"; |
utf-8 |
std::u16string |
char16_t |
std::u16string s = u"你好世界"; |
utf-16 |
std::u32string |
char32_t |
std::u32string s = U"你好世界"; |
utf-32 |
std::wstring |
wchar_t |
std::wstring s = L"你好世界"; |
系統依賴 |
wchar_t
的長度
sizeof(wchar_t) == 2; //Winodws下
sizeof(wchar_t) == 4; //Unix/Linux下
編譯器對源文件的識別
實驗使用的編譯器有visual studio 2017的cl.exe(x86_x64),clang 7.0.0(自己編譯的release版本),以及gcc 8.1.0 (MinGW-W64),全部都在命令行下編譯,實驗時會具體說明。所有實驗源文件編碼也會標註。
#include <iostream>
#include <string>
// encoding = utf-8 (no BOM)
int main(){
std::string sa = "你好世界";
std::string sb = u8"你好世界";
std::cout << "len(sa)=" << sa.size() << ' ' << sa << std::endl;
std::cout << "len(sb)=" << sb.size() << ' ' << sb << std::endl;
}
輸出:
- g++ or clang-cl
len(sa)=12 浣犲ソ涓栫晫
len(sb)=12 浣犲ソ涓栫晫
- cl.exe
len(sa)=12 浣犲ソ涓栫晫
len(sb)=18 嫺g姴銈芥稉鏍櫕
如果上述源文件改成ANSI編碼,結果是:
- g++
len(sa)=8 你好世界
len(sb)=8 你好世界
- clang-cl
test.cpp(6,23): warning: illegal character encoding in string literal [-Winvalid-source-encoding]
std::string sa = "<C4><E3><BA><C3><CA><C0><BD><E7>";
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.cpp(7,25): error: illegal character encoding in string literal
std::string sb = u8"<C4><E3><BA><C3><CA><C0><BD><E7>";
- cl.exe
len(sa)=8 你好世界
len(sb)=12 浣犲ソ涓栫晫
分析:從上訴結果可以看出,
- gcc和clang都是默認utf-8編碼的,不過,g++沒有編碼檢查,會把任何文件都當作utf-8文件來讀取。
- gcc和clang的
u8
前綴是不起作用的,畢竟本來就默認是utf-8編碼。 - cl 默認編碼是ANSI,所以
u8
前綴是起作用的,但是對於無BOM的utf-8文件,它會當成ANSI來讀取,所以它把utf-8的"你好世界"
含有的12字節當成了6個ANSI字符,並對其進行的轉碼,試圖轉爲6個utf-8字符,也就是18字節,這顯然是越搞越亂。
不過還是可以誇誇cl的,如果你用其他編碼實驗,對於cl,它都會給出:
len(sa)=8 你好世界
len(sb)=12 浣犲ソ涓栫晫
這是由於它會自動把他轉換爲ANSI編碼,然後再編譯,包括有BOM的utf-8文件也是這樣的(顯然,有BOM就是告訴了它這是一個utf-8文件,無BOM它就當是ANSI了)。
而對於gcc和clang,其中ANSI和utf-8都會按照utf-8文件來讀取,但是對於clang,string中的非utf-8字符會報錯。而其他編碼,gcc和calng都不支持,不過,gcc會報出一系列很難看懂的錯誤,但是clang會貼心地告訴你:
fatal error: UTF-16 (LE) byte order mark detected in 'test.cpp', but encoding is not supported
1 error generated.
總結如下
- cl.exe默認編碼是ANSI,它會將其他文件先轉化成ANSI編碼,再編譯。但是對於無BOM的utf-8文件,會當成ANSI從而可能出錯。
- gcc和clang都默認utf-8,clang拒絕ANSI的string(不過註釋可以),gcc會把ANSI也當成utf-8,其他編碼源文件都不支持。