Windows下c++字符編碼(二)

編譯器對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是三個字節:0xef0xbb0xbf。不過一些由於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,其他編碼源文件都不支持。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章