Qt雜談5.淺談Qt程序亂碼那些事

1 爲啥聊這個?

亂碼問題應該是困擾過每一位Qter,這裏主要從原理出發做個分享,希望大家能掌握其本質,以後遇到類似問題能夠舉一反三,將亂碼問題踩在腳下。

2 從代碼出發

2.1 使用的Qt版本

本人的測試環境:
Qt版本:Desktop_Qt_5_12_3_MSVC2017_64bit
mingw編譯器一般不會遇到亂碼問題,這裏主要講MSVC編譯器。
新建一個工程:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() << "我和你";
}

2.2 代碼頁(936)問題

錯誤如下:

..\AviationPro\mainwindow.cpp: warning C4819: 該文件包含不能在當前代碼頁(936)中表示的字符。請將該文件保存爲Unicode格式以防止數據丟失
..\AviationPro\mainwindow.cpp(16): error C2001: 常量中有換行符
..\AviationPro\mainwindow.cpp(17): error C2143: 語法錯誤: 缺少“;”(在“}”的前面)

首先,需要搞清楚,什麼是當前代碼頁(936)?

當前代碼頁是指操作系統的本地字符編碼設置。代碼頁(Code Page)是一種字符編碼方案,用於表示文本中的字符集合以及字符與數字之間的映射關係。在Windows操作系統中,當前代碼頁指的是系統的本地字符編碼設置。在不同的地區或語言環境中,可能會使用不同的代碼頁作爲默認的字符編碼方式。例如,代碼頁936在中文環境下通常指的是GBK編碼(中文簡體)。它是一種單字節編碼,無法表示Unicode字符。如果源文件中包含了無法在當前的 936 代碼頁中表示的 Unicode 字符,就會出現上述錯誤提示。

這個時候可以這樣處理:

點擊確認,回到源碼編輯頁面,隨便編輯下文件,Ctrl+S保存文件,這個時候文件就變成了UTF-8-BOM編碼的了。此時再次編譯,上面的錯誤消失了,編譯也成功了。
解釋下原因:

當一個源文件添加了BOM之後,在讀取該文件時,編譯器就能正確識別文件的編碼格式,包括Unicode編碼(如UTF-8和UTF-16),進而正確處理其中的Unicode字符,避免了字符丟失或解析錯誤。

2.3 輸出亂碼

經過上面的處理,源文件和編譯過程都沒問題了,但是運行時卻出錯了,輸出如下:

19:35:47: Starting D:\QtPro\build-AviationPro-Desktop_Qt_5_12_3_MSVC2017_64bit-Debug\debug\AviationPro.exe ...
?????
19:35:53: D:/QtPro/build-AviationPro-Desktop_Qt_5_12_3_MSVC2017_64bit-Debug/debug/AviationPro.exe exited with code 0

本來要輸出的‘我和你’變成了‘?????’,什玩意兒這是?
分析下原因:
首先我們使用notepad++打開可執行程序AviationPro.exe,搜索“我和你”,輸出如下:

這是什麼意思呢,MSVC編譯自動將字符串轉爲ANSI編碼了,對於中文來說,ANSI編碼通常指代GBK編碼或GB2312編碼,然而,Qt程序在運行過程中會將字符串認爲是Unicode編碼來進行處理,那肯定是不行的。
那麼問題來了,前面不是把源文件改成UTF-8-BOM編碼了嘛,爲啥編譯生成的exe還是ANSI編碼?

說下原因:對於使用MSVC編譯,使用UTF-8-BOM編碼的源代碼文件在編譯過程中不會直接影響生成的可執行文件的字符集。
MSVC編譯器默認情況下會使用與本地代碼頁相關聯的字符集,通常被稱爲ANSI編碼。這意味着生成的可執行文件將使用本地代碼頁定義的字符集作爲默認字符集。雖然源代碼文件使用 UTF-8-BOM 編碼,但是在編譯過程中,編譯器會將源代碼文件轉換爲內部編碼,然後生成可執行文件。

因此,單純使用UTF-8-BOM編碼的源代碼文件並不能直接改變生成的可執行文件的字符集。
接下來,我們繼續探索。。

2.4 使用QString::fromLocal8Bit轉換

修改代碼如下:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() <<  QString::fromLocal8Bit("我和你");
}

輸出如下:

解釋下原因:

QString::fromLocal8Bit 是 Qt 中的一個字符串轉換函數,它的作用是將一個使用本地(系統默認的)字符編碼表示的字符串轉換爲Unicode編碼的QString。

這應該懂了嘛,MSVC編譯器將字符串轉換爲ANSI編碼了,但是Qt運行時會將字符串轉換爲Unicode編碼,所以需要轉換下才能正常顯示。

2.5 使用QStringLiteral 宏

除了上述方法,還可以這樣:

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() <<  QStringLiteral("我和你");
}

輸出結果如下:

先解釋下QStringLiteral 宏是個啥?

QStringLiteral是Qt框架中的一個宏,用於創建一個指向編譯時常量的QString對象,它會將字符串作爲字面值存儲在內存中,通常被存儲在程序的數據段(.data 或 .rodata)中。當使用QStringLiteral宏創建QString對象時,這些字符串常量會按照原始的UTF-8編碼形式存儲在編譯後的可執行文件中的數據段或只讀數據段。在程序運行時,QString對象會根據需要將UTF-8編碼的數據轉換爲內部的Unicode字符表示。

嗯哼,這下應該清楚了吧,這種情況下就不存在編碼轉換的問題了,順便提一下,QStringLiteral宏帶來了以下優勢:

  1. 字符串優化:QStringLiteral可以在編譯時對字符串進行優化處理。它會將字符串以字面值的形式存儲在內存中,並且可以直接從編譯器的字符串表中獲取。這樣可以減少運行時創建和銷燬QString對象的開銷,提升程序的執行效率;
  2. 避免不必要的複製:使用QStringLiteral可以避免不必要的字符串複製。普通的QString對象會在構造時複製一份字符串數據,而使用QStringLiteral則可以直接共享編譯時的常量字符串數據,減少內存使用和內存拷貝;
  3. 安全性:QStringLiteral在編譯時進行字符串類型檢查,提前發現潛在的問題;
  4. 代碼清晰度:使用QStringLiteral可以使代碼更加簡潔和清晰。由於編譯時優化,字符串的創建和比較操作更加高效,可以提高代碼的可讀性和維護性。

2.6 使用pragma execution_character_set("utf-8")預處理

修改代碼如下:

#pragma execution_character_set("utf-8") 
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    qDebug() <<  "我和你";
}


嗯,一切盡在掌控之中,解釋下原因:

預處理指令#pragma execution_character_set("utf-8"),用於告訴MSVC編譯器將源文件中的字符集設置爲UTF-8。這樣做可以確保源文件中的中文字符和其他非ASCII字符能夠正確地被編譯器解析和處理,避免出現亂碼問題。

這個時候,我們再次使用notepad++打開可執行程序AviationPro.exe,搜索“我和你”,輸出如下:

此時,本地編碼的AviationPro.exe文件已經搜不到“我和你”字符了,因爲中文字符串已經保存爲UTF-8編碼了,Qt運行時也能夠正常識別。
爲了驗證以上描述的正確性,可以將AviationPro.exe文件轉換爲utf-8編碼,此時再次搜索“我和你”,你會神奇般的發現,又能搜索到了,這個大家可以自行驗證。

3 跨平臺編碼方案

通過上述內容,我想大家對原理應該有所瞭解了,瞭解原理後,我們再來尋求一種完美的方案,就很容易了。
大家都知道,linux下源碼是不需要bom的,能不能實現所有平臺都是utf8編碼不帶bom,還不會出現亂碼現象,答案是可以的。
linux就不說了,只要用utf8編碼,不存在亂碼的問題,這裏主要還是針對MSVC編譯器,兩步走:

  1. 統一utf8編碼,去除bom

    此時,編輯下文件再保存,QtCreator會自動去除bom
  2. 針對msvc編譯器,設置編譯器選項
    在pro文件中添加:
win32-msvc* {
    QMAKE_CXXFLAGS += /source-charset:utf-8 /execution-charset:utf-8
}

/source-charset:utf-8 用於指定源代碼文件的字符集爲UTF-8,這將告訴編譯器源代碼文件的編碼方式是UTF-8,此時,不會報‘該文件包含不能在當前代碼頁(936)中表示的字符’錯誤。
/execution-charset:utf-8 用於指定生成的可執行文件的字符集爲UTF-8,這將告訴編譯器生成的可執行文件使用UTF-8編碼,此時,會將字符使用UTF-8編碼方式保存在可執行文件中。
到此,一切都正常了,並實現了統一。

4 總結

說了這麼多,不知道大家有沒有收穫點什麼,其實就兩個要點,一個是編譯時處理爲的編碼,一個是運行(執行)時需要的編碼,一般來說MSVC編譯器會將字符處理爲本地編碼(gbk),而Qt運行時默認使用Unicode編碼,這就不一致了,所以會亂碼。解決辦法就是轉換一下,或者一開始就避免轉換,再或者告訴編譯器怎麼做。
補充一點,嚴格來講,Unicode編碼這種叫法不嚴謹,簡單說明下:

Unicode是一個字符集,它爲每個字符分配了一個唯一的編號,這些編號稱爲碼點。Unicode字符集包含了世界上幾乎所有的字符,包括各種語言的字符、符號、標點等。
UTF-8是一種編碼方式,它定義瞭如何將Unicode字符集中的字符編碼爲字節序列。UTF-8使用可變長度的編碼方式,根據字符的碼點大小來確定所需的字節數。較小的字符使用較少的字節來編碼,較大的字符使用更多的字節。UTF-8編碼是一種向後兼容ASCII編碼的方式,ASCII字符與UTF-8編碼是完全一致的。
簡而言之,Unicode是一個字符集,而UTF-8是一種將Unicode字符編碼爲字節序列的編碼方式。Unicode是一種標準,而UTF-8是一種具體的實現方式。

好了,一切豁然開朗,你開朗了嗎?本人水平有限,如果有不對的地方,還希望大家不吝賜教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章