Qt開發中文顯示亂碼
來源 https://www.jianshu.com/p/ed269df8104d
參考 https://blog.csdn.net/J_H_C/article/details/93882284
為什么會出現亂碼
首先,我們需要有的概念是亂碼的問題是由編碼和解碼方式引起的。涉及到編碼方式的地方有3個:
- 源碼字符集
- 執行字符集
- 運行環境字符集
源碼字符集(the source character set):源碼文件時使用何種編碼格式保存的。
執行字符集(the execution character):可執行程序內保存的是何種編碼(程序執行時內存中字符串編碼)
gcc 運行字符集設置參數
-finput-charset=charset //設置源碼字符集為charset
-fexec-charset=charset //設置執行字符集為charset
-fwide-exec-charset=charset //設置寬字符串的執行字符集為charset
msvc 運行字符集設置參數
-execution-charset:utf-8 // 指明程序執行時使用UTF-8字符集
-source-charset:utf-8 // 指明源碼文件的編碼為UTF-8字符集
源碼字符集確切的說是編譯器認為源碼文件的編碼方式,執行字符集是可執行程序采用的編碼方式,而運行環境字符集則是環境支持的編碼方式。編譯程序處理字符串的過程,實際上是首先讀入字符的二進制數,根據編碼格式到另一種編碼格式轉換策略得到另外一串二進制數,所以1->2可能有二進制數的變化,而3則是通過既定的編碼方式來解讀2中的二進制數為字符(這里為什么說可能呢,因為1和2如果是相同的編碼是不需要變化的)。
那么具體是哪些地方引起錯誤呢?在解答之前先介紹理解該問題的先驗知識(由於我的運行環境是window簡體中文版,所以以下的locale編碼就是指GBK編碼):
- msvc2013編譯程序時,處理源碼字符集時,有BOM標識符的則正確識別(實際上目前就是有無BOM的utf-8),無BOM則使用本地Locale字符集(隨系統設置而變),執行字符集默認用本地Locale字符集(其他msvc版本在看完本文甚至可以根據自己實驗猜測處理)。
- gcc編譯程序時,默認兩者都是uft-8,有finput-charset源碼字符集和fexec-charset執行字符集則按照設置。
那么亂碼的原因有:
①編譯器解讀源碼字符集錯誤。如我是utf-8的源碼,因為不帶bom你當成locale,執行字符集也是locale所以不需要轉換,而本來utf-8到locale是需要轉換的。
②源碼字符集到執行字符集的轉換錯誤。如本來把識別正確的源碼字符集locale轉成執行字符集中的utf-8,結果你給我指定了錯誤了轉換方式,說讓我通過xxx編碼轉utf-8的策略轉(Note:這是錯誤的表述,看到下面你就明白,實際上這里的錯誤只能是應為轉換算法的錯誤)。
③字符解析錯誤。如果現在程序中的字符串二進制是utf-8的,結果你非要說執行字符集是loacle,那么解析肯定會出錯。
還需要理解的包括下面的知識:
- windows console控制台代碼頁為locale,即把程序中的字符串二進制表示當locale執行字符集來解讀
- 字符串二進制的表示形式不需要編譯,直接拷貝到執行程序的二進制中
亂碼情況解析
接下來內容的實例基於csdn作者“在水一方”博文中舉的“我是中文”的例子(文末有引用),他的博文在我理解這個問題的本質過程中幫助很大。這里就套用他的例子的,一方面我比較懶,不想舉其他例子,另一方面通過驗證他的例子,也佐證了我自己的想法。
直接上例子(這里說的都是源碼字符集):
const char * str = "我是漢字" //用GBK編碼等價於 const char * str = "\xce\xd2\xca\xc7\xba\xba\xd7\xd6"; //用utf-8編碼等價於 const char * str = "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97"; //note:這里的等價於就是說當計算機到內存中來處理的時候,中文讀入的就是等價於的二進制數
翻譯一下就是,“我是漢字”這幾個字,在GBK編碼下就是保存的“\xce\xd2\xca\xc7\xba\xba\xd7\xd6”這樣一串二進制,而utf-8則是保存的“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”。這里可以使用Notepad++進行驗證。
字符解析錯誤亂碼
- 編譯環境:vs2013(msvc2013編譯器),源碼文件字符集GBK
- 運行環境:Windows簡體中文下的Console命令行
下面看一段代碼:
char * cc = "\xce\xd2\xca\xc7\xba\xba\xd7\xd6"; std::cout << cc << std::endl; char * cc1 = "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97"; std::cout << cc1 << std::endl; char * cc2 = "我是漢字"; std::cout << cc2 << std::endl;
運行程序得到下圖結果:

根據結果我們可以看到2是亂碼的,而漢字表現出了和GBK下二進制數據一樣的結果。有了前面的先驗知識按照前面先驗的亂碼原因①②③來理解:
①對於不帶bom源碼的文件,msvc2013當成locale處理,而源碼字符集恰巧是locale,讀入源碼字符集沒問題。這里需要“我是漢字”字符串變為二進制數,並記錄源碼字符集。
②源碼字符集和執行字符集都是locale,不需要轉換,沒轉換自然轉換沒問題。到此,字符串的二進制表示的直接拷貝到了執行程序中。
③2把執行程序中“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”——“我是中文”uft-8編碼下的二進制,當成了GBK編碼來解析,所以出現了類型③亂碼。
Note:請用notepad++檢驗,以便理解。
在上面程序的基礎上,我們添加測試函數的函數體前添加一段預定義,這是c++11對執行字符集的支持:
//讓編譯器編譯生成程序的執行字符集為utf-8 #if _MSC_VER >= 1600 #pragma execution_character_set("utf-8") #endif
再次運行程序,得到如下的結果:

首先看到12和上面結果一樣,有人這里就有疑問了。你說的字符串的二進制表示直接拷貝我也理解,但是現在我的執行字符集是utf-8啊,那我解讀第一個和第二個的結果不應該是這個啊。那你可能忘掉了我之前的一個先驗知識了,console不認識utf-8,它仍然會把這串二進制當成locale來解讀,所以這里和上面的表現結果是一樣的。
下面來看3是怎么回事,①②流程下來:
①源碼為locale,編譯器也默認認為源碼字符集是locale(編譯器這是瞎貓碰到死耗子,蒙對了!),解讀正確。
②編譯器正確知道源碼字符集的情況下,需要轉化成指定的字符集,自然是會給出正確的轉化策略。
最終,編譯通過編碼轉換策略做了一次從 “\xce\xd2\xca\xc7\xba\xba\xd7\xd6”到“\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97”的轉換,所以程序中又是“我是中文”uft-8編碼下的二進制了,最終又回到了2的情況——類型③亂碼。
轉換錯誤亂碼(反證)
- 編譯環境:QtCreator(MinGW gcc編譯器),源碼文件字符集utf-8
- 運行環境:Windows簡體中文下圖形界面
下面看一段代碼:
MainWindow w; QLabel *lb1 = new QLabel(&w); QLabel *lb2 = new QLabel(&w); QLabel *lb3 = new QLabel(&w); lb1->setText(QString("我是漢字")); lb1->resize(100, 20); lb1->move(120, 120); lb2->setText(QString::fromUtf8("我是漢字")); lb2->resize(100, 20); lb2->move(120,160); //由於源碼是utf-8編碼,下面代碼等價於: //lb3->setText(QString::fromLocal8Bit("我是漢字")); lb3->setText(QString::fromLocal8Bit( "\xe6\x88\x91\xe6\x98\xaf\xe6\xb1\x89\xe5\xad\x97")); lb3->resize(100, 20); lb3->move(120,200); w.show();
運行程序得到下圖結果:

這里我不給出詳細的分析了,通過第1個標簽和第2個標簽結果都正常,可以驗證出gcc編譯的默認規則——默認源碼字符集和執行字符集都是uft-8,且知道了Qt中QString::fromxxx()函數的作用了。而標簽2和標簽3的對比可以知道,當環節②出錯,就出現亂碼了。過程是編譯器把讀入的utf-8編碼下的二進制當成了loacle來解析,這時就解析成了所謂的那串“亂碼”,然后正確轉換成了uft-8編碼下的該“亂碼”(Note:這里兩次亂碼實際的二進制是不一樣的哦,只是編碼形式不同才有的相同結果,你明白我的意思嘛?)。有人又要疑惑了,不對啊,你明明說這是個類型②的錯誤,怎么我看着像是類型①亂碼呢。其實如果你能這么疑惑,說明你是真的懂了,這里確實是一個類型①的亂碼。實際上這里的源碼字符集到執行字符集的算法是api內部實現的,所以我們面對這種情況的時候②都不會出問題的。當然了,像你這種亂碼都沒有理解的人來說,去實現這個算法,那我是不敢用,說不定就會產生類型②亂碼了,哈哈。
總結
由於Qt的出現就是為了跨平台,所以QString中統一采用utf-16存儲字符串。所有源碼中的字符串存放到QString中時,都需要經過一次到utf-16的正確轉換。在qt5之前,有兩種解決方式解決亂碼:
QString::fromxxx(); QTextCodec::setCodecForxxx();
相信大家看了前面已經明白這兩個函數是意思,這里要提醒一句的就是,兩種方式最終在QString中存放的,都是字符串在unicode編碼形式下的二進制。
寫在最后
這系列的文章將會以自己學習后理解的知識點分享為主,希望吾之所得亦可為汝所得。在2017年3月18日重新更新文章時,我刪掉了與知識點無關的表述。只是希望讓正努力從“不求甚解”到“先去理解清楚一些以釋重負”轉變的你,不會因為篇幅過長望而卻步。如有疑問,歡迎提問,如有高見,煩請指點。
參考:
- qt中文亂碼問題
- QString亂談(2)
- 從此亂碼是路人
- 以及n多的內容。上面三篇博文尤其前兩篇帶我真正理解了qt中中文亂碼的原因
msvc:QMAKE_CXXFLAGS += -execution-charset:utf-8
msvc:QMAKE_CXXFLAGS += -source-charset:utf-8
msvc:QMAKE_CXXFLAGS_WARN_ON += -wd4819
gcc:QMAKE_CXXFLAGS += -finput-charset=charset:utf-8
gcc:QMAKE_CXXFLAGS += -fexec-charset=charset:utf-8
gcc:QMAKE_CXXFLAGS += -fwide-exec-charset=:utf-8
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8")); QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8"));
網上很多人一碰到編碼問題就無腦的Copy上面3行……
從Qt5開始只剩下setCodecForLocale這一個了,只是影響Qt對toLocal8Bit相關函數的編碼方式
{ // Qt默認會使用本機編碼,所以對於中文系統,下面這句設置是多余的 QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK")); QString str1("你好Hello"); QByteArray bLocal = str1.toLocal8Bit(); // 受setCodecForLocale影響,會轉換為設定的編碼。如果本機不支持指定編碼,則會按toLatin1處理 QByteArray baLatin1 = str1.toLatin1(); // 不受setCodecForLocale影響,強制轉換為ISO-8859-1編碼 QByteArray bUtf8 = str1.toUtf8(); // 不受setCodecForLocale影響,強制轉換為UTF-8編碼 qDebug() << str1; // 正常,Qt會將UTF-16轉換為UTF-8輸出 qDebug() << baLatin1; // 亂碼,用UTF-8編碼輸出Latin1字節流 qDebug() << bLocal; // 亂碼,用UTF-8編碼輸出GBK字節流 qDebug() << bUtf8; // 正常,用UTF-8編碼輸出UTF-8字節流 QString str2 = QString::fromLocal8Bit(bLocal); qDebug() << str2; // 正常,因為上面顯式指定字節流來自本機編碼,而bLocal正是本機編碼GBK str2 = QString::fromLatin1(bLocal); qDebug() << str2; // 亂碼,bLocal是GBK編碼,但卻指定了以Latin1方式去讀取,肯定會亂碼 // 字節流來自UTF-8 str2 = QString::fromUtf8("\xE4\xBD\xA0\xE5\xA5\xBD\x48\x65\x6C\x6C\x6F"); qDebug() << str2; // 正常 // Qt默認采用UTF-8處理字符串,所以不用顯式地去調用fromUtf8 str2 = QString("\xE4\xBD\xA0\xE5\xA5\xBD\x48\x65\x6C\x6C\x6F"); qDebug() << str2; // 正常 }
在Qt中,QString會用UTF-16編碼存儲,而qDebug()等I/O函數會以UTF-8編碼處理。
其實轉換后的字節流是正確的,只是顯示時用了和字節流不同的編碼方式處理導致亂碼
所以當要在Qt中輸入輸出非UTF-8字符串時應該先轉換一下
在源碼中要寫入非英文字符的話建議使用轉義的方式,也就是上面“\xE4\xBD...”這種,這也是官方推薦的方式
為了方便將字符串轉換為UTF8轉義字符,寫了一個小工具
http://download.csdn.net/detail/aqtata/5596247
2014-5-6補充:
從vs2010sp1和vs2013開始就已經支持UTF-8的源碼文件了,只用在工程里加入一句"#pragma execution_character_set("UTF-8")"即可。不用再做上面的轉義了。