乱码 - c++字符串的编码?
怪我咯
怪我咯 2017-04-17 13:41:13
0
1
665

c++ 字符串加载到内存里面是什么编码格式的?

win7中文系统下,控制台默认是GBK编码的,用GBK格式保存的源文件,中文字符串在vs2010下编译输出到控制台会正常输出

但是vs2010里面采用utf-8无BOM的源文件 输出中文字符串到终端就出现乱码了

所以 是不是c++把字符串的值加载到内存中时 是按照cpp文件的编码保存的? 也就是说utf-8编码的cpp文件,编译后字符串加载到内存时是utf-8编码的?

怪我咯
怪我咯

走同样的路,发现不同的人生

全部回覆(1)
迷茫

這個問題想說清楚還是挺複雜的,題主可以參考 New Options for Managing Character Sets in the Microsoft C/C++ Compiler 這篇文章。具體來說,原始碼檔案中的字串常數能否正確的顯示在控制台視窗中與以下幾個因素有關:

  • 原始碼檔案(.cpp)保存時所使用的字元集(下文以 C1 取代)

  • 編譯器在讀取原始碼檔案時所使用的內部字元集(source character set)(下文以 C2 取代)

  • 編譯器在生編譯時所使用的字元集(execution character set)(下文以 C3 取代)

  • 執行可執行程式的控制台視窗所使用的字元集(下文以 C4 取代)

第一,「原始碼檔案保存時所使用的字元集」決定了原始碼檔案中的抽象字元儲存到硬碟中是什麼樣的位元組。這裡所說的抽象字符,就是指人所能辨識的文字(如「張」)。而位元組則是由抽象字符按照字符集的規定映射的,不同的字符集映射的結果不一樣(如“張”在UTF-8 下映射的字節是E5 BC A0三個字節,而在GBK 下映射的位元組是D5 C5兩個位元組)。例如下面一行程式碼:

char *s = "张三";

其中的字串常數"张三"在使用不同的字元集(C1)保存時,映射的位元組是不同的。

第二,「編譯器在讀取原始碼檔案時所使用的內部字元集」決定了編譯器如何將讀入的原始碼檔案位元組流進行轉換。我所謂的轉換就是指把位元組流從一種編碼轉到另一種編碼。舉例來說,對於抽象字元“張”,如果採用 GBK 編碼,映射的位元組流就是D5 C5,而轉換到 UTF-8 編碼,就是把這個位元組流轉換成E5 BC A0

這個字元集(C2)是由編譯器決定的,標準中並未規定。不同的編譯器可能採用不同的內部字元集,同樣的編譯器不同版本也可能採用不同的內部字元集。在Visual C++ 的某些版本中,內部字元集是UTF-8,編譯器會嘗試判斷原始檔案所使用的字元集(C1),並將其轉換成內部字元集(C2),如果編譯器無法判斷檔案所使用的字元集,則會預設其(C1)為目前作業系統的程式碼頁(default code page)這裡如果判斷錯誤,就會造成亂碼或編譯出錯)。

例如:原始檔案如果是UTF-8 with BOM,則能夠正確的被Visual C++ 識別,其中的抽象字元「張」映射的位元組流E5 BC A0就會被正確的轉換成E5 BC A0(沒變)。而原始檔如果是UTF-8 without BOM,則不能正確的被Visual C++ 識別,編譯器會採用目前的程式碼頁來進行轉換,其中的抽象字元「張」所映射的位元組流E5 BC A0就會被當作GBK 編碼,錯誤的轉換成其它位元組流E5 AF AE ...(寮)。

第三,「編譯器在編譯時所使用的字元集」決定了編譯器如何把原始碼中使用內部字元集(C2)編碼的字元/字串常數轉還到編譯時所使用的字元集(C3)。這個字元集(C3)也是由編譯器決定的,標準中並未規定。 C++ 中的字元常數和字串常數有不同的類型,它們對應於不同的 C3。

在 Visual C++ 中,參考 String and Character Literals 和前文提到的博客,可以推知不同類型的字元/字串常數對應的 C3:

// Character literals
auto c0 =   'A'; // char, encoded as default code page
auto c1 = u8'A'; // char, encoded as UTF-8
auto c2 =  L'A'; // wchar_t, encoded as UTF-16LE
auto c3 =  u'A'; // char16_t, encoded as UTF-16LE
auto c4 =  U'A'; // char32_t, encoded as UTF-32LE

// String literals
auto s0 =   "hello"; // const char*, encoded as default code page
auto s1 = u8"hello"; // const char*, encoded as UTF-8
auto s2 =  L"hello"; // const wchar_t*, encoded as UTF-16LE
auto s3 =  u"hello"; // const char16_t*, encoded as UTF-16LE
auto s4 =  U"hello"; // const char32_t*, encoded as UTF-32LE

編譯器根據字串常數的型別把其從 C2 轉換到 C3(前面如果判斷錯誤,這裡就會繼續保留錯誤)。

例如:auto s1 = "张";,抽象字元「張」在C2 中(UTF-8)映射的位元組流E5 BC A0就會轉換成在C3 中(CP936,GBK)映射的位元組流D5 C5

auto s2 = u8"张";,抽象字元「張」在C2 中(UTF-8)映射的位元組流E5 BC A0就會被轉換成在C3 中(UTF-8)映射的位元組流E5 BC A0(不變)。

第四,「執行執行程式的控制台視窗所使用的字元集」決定如何把編譯好的執行程式中的位元組流轉換成抽象字元顯示在控制台中。例如,在上一步驟中的s1映射的位元組流就會透過 C4(CP 936)映射回抽象字元“張”,在我們看來就是正確的。而上一步中的s2映射的位元組流就會透過 C4(CP 936)映射回抽像字元“寮”,在我們看來就是亂碼。

以上,就是我理解的 C++ 中字元/字串的編碼處理方式,如果有誤還請指出:-)

題主可以試著在 Visual C++ 中把以​​下程式碼分別儲存成 CP936、UTF-8 with BOM、UTF-8 without BOM 的格式,看看輸出結果是什麼。

#include <iostream>
#include <fstream>
using namespace std;

int main() {
  char *s1 = u8"张";
  char *s2 = "张";
  cout << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;  // Error in console
  cout << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;  // OK in console

  ofstream os("s1.txt");
  if (os.is_open()) {
    os << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;  
    os.close();
  }
  ofstream os2("s2.txt");
  if (os2.is_open()) {
    os2 << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;  
    os2.close();
  }
  ofstream os3("s3.txt");
  if (os3.is_open()) {
    os3 << "s1 " << sizeof(s1) << " " << strlen(s1) << " -> " << s1 << endl;
    os3 << "s2 " << sizeof(s2) << " " << strlen(s2) << " -> " << s2 << endl;
    os3.close();
  }

  cin.get();
  return 0;
}

輸出的三個檔案中,前兩個檔案s1.txts2.txt都能夠被正常的文字編輯器猜出其編碼格式,從而正確顯示內容,但是第三個檔案s3.txt會顯示成部分或全部亂碼,因為其中既包含了UTF-8 編碼的字節流又包含了GBK 編碼的字節流,所以文本編輯器就不知道該用什麼編碼來把字節流映射回抽象文本了。


Character Encoding Model


標準引用與參考文件

來自ISO C++11 § 2.3/1
基本原始字元集由96 個字元組成:空格字元、代表水平製表符、垂直製表符、換頁符的控製字元和換行符,加上以下91 個圖形字元:

雷雷

來自 ISO C++11 § 2.3/2
universal-character-name 構造提供了一種命名其他字元的方法。

雷雷

universal-character-name UNNNNNNNN 指定的字元是ISO/IEC 10646 中字元短名為NNNNNNNN 的字元;通用字元名稱uNNNN 指定的字元是ISO/IEC 10646 中字元短名稱為0000NNNN 的字元。 ...

來自ISO C++11 § 2.3/3
基本執行字符集基本執行寬字符集 應各自包含所有成員基本源字符集的字符集,加上表示警報、退格鍵和回車符的控製字符,再加上一個空字符(分別為空寬字符),其表示形式全部為零。
...
執行字符集執行寬字符集分別是基本執行字符集和基本執行寬字符集的實現定義的超集。執行字元集成員的值和附加成員集是特定於語言環境的。

來自 ISO C++11 § 2.2/1
翻譯語法規則的優先順序由以下階段指定。

  1. 如果需要,實體原始檔字元會以實作定義的方式對應到基本來源字元集(引入換行符作為行尾指示符)。接受的物理來源檔案字元集是實現定義的。 ....不在基本來源字元集 (2.3) 中的任何原始檔案字元都將替換為指定該字元的通用字元名稱。 ...

  2. ...

  3. ...

  4. ...

  5. 字元文字或字串文字中的每個原始字元集成員,以及字元文字或非原始字串文字中的每個轉義序列和通用字元名稱,都會轉換為對應的成員執行字符集(2.14.3、2.14.5);如果沒有相應的成員,則將其轉換為除空(寬)字元之外的實現定義的成員。

  6. ...

來自 ISO C++11 § 2.14.3/5
通用字元名稱被轉換為指定字元在適當的執行字元集中的編碼。如果沒有這樣的編碼,則通用字元名稱將轉換為實現定義的編碼。 [ 注意: 在翻譯階段 1 中,只要在源文本中遇到實際的擴展字符,就會引入通用字符名稱。因此,所有擴展字元都按照通用字元名進行描述。然而,實際的編譯器實作可能會使用自己的本機字元集,只要獲得相同的結果即可。 — 尾註 ]

來自ISO C++11 § 2.14.5/6 7 15
6 在翻譯階段6 之後,不以編碼前綴開頭的字串文字是普通字串文字,並被初始化
7 以u8 開頭的字串文字,例如u8"asdf",是UTF-8 字串文字,並使用以UTF-8 編碼的給定字元進行初始化。
...
15 非原始字串文字中的轉義序列和通用字元名稱與字元文字(2.14.3) 中的意思相同,但單引號 可以由其本身或轉義序列表示,雙引號" 前面應有。在窄字串文字中,由於多位元組編碼,通用字元名稱可能會對應到多個 char 元素。 ...

  • 字符集

  • 字串和字元文字

  • 在 Microsoft C/C++ 編譯器中管理字元集的新選項

熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板