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是一种具体的实现方式。

好了,一切豁然开朗,你开朗了吗?本人水平有限,如果有不对的地方,还希望大家不吝赐教。

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