004-QString

004-QString
abinng😶🌫️auther: abinng date: 2026-03-18 211:03
createDate:2026-03-18 21:03
从字符编码讲起
计算机字符编码起源于英语系国家,初始是 ASCII 编码,实现了 128 个符号的映射关系
但是后来非英语国家也对字符编码有需求,显然 ASCII 的 8bit 不够表示那么多状态,不够那么多映射关系,此时依旧沿用低 7 位标识英语系符号,使用多字节来进行编码,有了国标码,例如中国是 GBK
而后肯定有一个问题是,在中国使用 GBK 编码的文件,发送到了阿拉伯国家,编码格式变了,数据翻译成了另外的含义,不便于传输信息
所以有了 Unicode 编码,2Byte 表示所有符号,是一个统一符号编码。当然 2Byte 还是不太够,因为还有 emoji 表情等符号,后面还有其他编码,这里暂且不论
but, 英语系国家原本只用到了低 7 位的二进制,现在要使用 Unicode 编码就得数据长度翻倍且效果一致,这是定长编码的缺点。那就用变长编码(梦回哈夫曼编码…),用字符出现的概率对不同的字符进行长度适应,出现概率大的字符,编码较短,出现概率小的字符编码较长,这就是 UTF-8 编码
接下来去 VSCode 中进行一下实验,先下载一个插件
Hex Editor 用于查看文件的十六进制表示
创建一个文件输入 abc ,之后
Ctrl + Shift + P 输入 Hex
回车,就可以看到当前文件内容的十六进制表示,这里是
61 62 63
选中 a 对应的 61
可以看到前 128 个字符,基本在各大编码里面都一样
当我们输入一个中文,如 abc中 ,发现:
因为 VSCode 默认编码是 UTF-8 ,中文占三个字节
这里我们切成 GBK 编码,右下角找到 UTF-8
代表当前的编码,点一下,有两个选项
- 通过编码重新打开:不改变文件底层数据内容,只改变翻译规则
- 通过编码保存:改变文件底层数据内容,且改变翻译规则
这里点击通过编码保存,搜索 GBK ,当然
GB18030 和 GB2312
也行,之后再去看十六进制编码,就是两字符了
这次换一下,通过编码重新打开,随便换一个国家的编码,假设文件不做修改,直接用其他编码的翻译规则来看:
这符号都看不懂,但 abc 依旧没变
对于解析不出来的符号,会用一个实心菱形嵌套问号来表示
不同国家的机器上,终端的编码都不同,可见编码的问题比较难解决,但是 Qt 是怎么做的呢??
存为 Unicode 编码,两个字节代表一个编码,在不同的机器上再进行编码的转换,达到跨平台的效果。相当于加了一层
终端乱码?为什么
可执行程序中的字符到终端
可执行程序,将字符流送到终端的解码器,最后显示出来
这里有一个疑问,终端的解码器是依据什么编码呢?
拿 Windows 举例,安装完之后会让选择时区/地区,会根据选择的结果来给终端定编码
例如:现在源码文件是 UTF-8 编码,之后送到终端的时候,是一个一个原封不动地传过去的,而终端的解码器是 GBK ,那么就会使用 GBK 的映射表进行解码,最后显示在终端上大概率是有乱码的
试验一下:创建文件,写入
1 |
|
在该文件所在文件夹打开终端,g++ -o build hello.cpp,build.exe
发现输出是:hello和乱码组成的串,hello浣犲ソ
但是如果源文件就是GBK编码的格式,那么再往后的操作就会正常输出,并不会出现乱码
也就是说有很多时候出现的乱码,是因为源文件的编码和终端的编码不一致导致的
由此也可见,源文件的编码和终端的编码都在影响着跨平台,Qt实现跨平台就是通过无论什么源文件,当存入可执行程序的时候,存的是 Unicode 编码集,编译的时候就会结合我当前操作系统中的 Qt SDK 来处理编码的问题
Qt 中的字符,含义和传统的字符不一样了
操作一下
Qt creator 的默认编码是什么?—>最上面一行->编辑->preferences->文本编辑器->行为->往下翻就可以看到默认编码了,是 UTF-8
如果要改变本文件的编码,最上面一行->编辑->选择编码,之后选择目标编码
创建一个 Qt Console Application
测试一:编码问题
1 | void test01() { |
也可以打断点,看看实际存入的情况
我们发现 QChar 和普通的字符好像确实不太一样,他是以 UTF16 两个字节来作为一个单位
现在源文件是UTF8编码,我们保存为GBK编码,再次运行,发现前两个是正常中文,最后一个又成了乱码,这是因为 QChar 默认认为传入的字符串是 UTF8 编码,我们可以通过构造函数来适配一下
1 | QString str3 = QString::fromLocal8Bit("hello你好"); |
这时候我们的字符串 hello你好 是 GBK 编码,通过构造函数
fromLocal8Bit 的转换就可以了
这个 Local 就意味着我们现在操作系统终端默认的编码,也就是 GBK
而当我们又把源文件保存为 UTF8 编码时,用 fromLocal8Bit
来构造,就又会出问题了
其他构造函数可以去帮助手册中看
测试二:QString 底层与
COW
说一下 COW
Qt
中,由于控件众多,还有很多信号和槽的机制,要是用指针指来指去,对于开发者来说,维护比较困难,采用了
COW 的机制
当我们写类似如下代码时
1 | QString s1 = "hello你好"; |
此时 s2 是和 s1 指向同一个空间的,除非我们修改了 s2,才会新拷贝出来一个空间并修改
QString 底层
QString 对外表现的,是 QChar 组成的流。但实际上它的内部结构不止包含 QChar ,还包含元属性
测试代码
1 | /* 查看QString的内部结构,以及COW效果 */ |
constData():这个是数据的起始地址data_prt():这个是整个QString的起始地址
两个值差的绝对值,正好是 0x10 ,十六字节,说明 QString
前面的元属性占十六字节
第一段输出,s2 和 s1 的地址还相同,说明是指向同一块内存
第二段输出,发现 s2 和 s1 的地址不同了,说明触发了 COW
所以 Qt
中函数传参时,如果只是用来读,即使是值传递,代价也不大,因为有
COW ,按值传递传参的代价就仅仅是 “复制一个指针 +
增减一次引用计数”。如果用 const QString &
就会是复制一个指针,连引用计数都不用操作
COW 扩展
By Gemini
Qt 中大部分的数据类型,都是 COW
机制,而控件(如:QWidget, QPushButton,
QLabel 等)以及像 QTcpSocket,
QTimer, QThread
这样的核心功能类,它们都有一个共同的祖宗:QObject。
QObject 及其所有的子类,不仅没有 COW
机制,而且根本不允许被拷贝
为什么这么设计? 因为这类对象代表的是一个独一无二的实体(身份)。
状态与连接: 一个按钮可能有特定的父窗口,绑定了一堆信号和槽(Signals & Slots)。如果你“复制”了一个按钮,新按钮应该继承那些信号和槽吗?如果继承了,点击新按钮是不是要触发老按钮的逻辑?这会引发巨大的混乱。
对象树(Object Tree): Qt 通过父子树来管理内存。复制一个树节点在逻辑上是极其复杂的。
如果你去翻看 QObject 的源码,你会发现 Qt
直接把拷贝构造函数和赋值操作符给禁用了(使用了
Q_DISABLE_COPY 宏)。
1 | // 这样的代码在编译时就会直接报错 |
一个高频踩坑点
隐式共享的“分离(Detach)陷阱”
既然容器类(比如 QList)都支持
COW,那很多新手会遇到一个经典的性能陷阱。
当两个变量共享同一块数据时,只要其中一个执行了非 const 的操作,Qt 就会被迫进行深拷贝(这在 Qt 中称为 Detach 分离)。
1 | QList<int> list1 = {1, 2, 3}; |
在上面这段代码中,list2[0] 调用的是非 const 的
operator[]。Qt
不知道你拿到这个引用之后是要“读”还是要“写”,为了安全起见,它会立刻触发
COW 进行深拷贝!这会导致极大的性能浪费。
正确的做法是使用 at(),它是 const
的,不会触发拷贝:
1 | int a = list2.at(0); // 引用计数依然为 2,非常快! |
QString 的使用
详细的 API 列表建议直接查阅 QtCreator 帮助文档:
QString Class。这里主要总结高频核心操作和避坑指南。
1. 基础状态与查阅
容量与长度
size()和length():两者等价,返回的是 字符的个数(准确地说是 UTF-16 编码的QChar个数),不是字节数。例如QString("哈哈哈aaa").size()返回的是 6。
安全访问
at(qsizetype i):只读访问。极力推荐使用,因为它是const的,不会触发隐式共享的 COW(写时复制)机制带来的深拷贝。operator[]:如果非常量字符串使用[]读取,可能会意外触发 COW,造成性能浪费。
区分
isNull():对象是否未被初始化。QString().isNull()为true。isEmpty():内容长度是否为 0。QString("").isEmpty()为true,但它的isNull()为false。- 最佳实践:
日常开发中判断字符串有没有内容,一律使用
isEmpty(),涵盖了isNull的情况。
2. 拼接与插入 (增)
除了传统的 + 和 +=
操作符外,QString 提供了更语义化的链式调用:
1 | void test_append() { |
3. 截断、截取与分割 (改)
这部分是处理文本数据时最常用的功能。
头尾修剪 (Chop & Truncate)
chop(n):原地修改,从末尾砍掉 n 个字符。chopped(n):非原地修改,返回一个砍掉尾部 n 个字符的新字符串(原字符串不变)。truncate(n):原地修改,从索引 n 处截断(保留前 n 个字符,后面的全扔掉)。
提取子串 (Mid, Left, Right)
left(n)/right(n):提取最左边 / 最右边的 n 个字符。mid(position, n):极其常用! 从position处开始,提取n个字符。如果不传n,则一直提取到末尾。
字符串分割 (Split)
split(分隔符):将字符串按规则切分成字符串列表QStringList(本质上是QList<QString>)。
1 | void test_slice() { |
4. 查找与删除 (查、删)
包含与检索
contains(str):是否包含某子串(可设置是否区分大小写)。startsWith(str)/endsWith(str):判断开头或结尾。indexOf(str):查找子串第一次出现的位置索引,找不到返回 -1。
删除与替换
remove(position, n):从某位置开始删除 n 个字符。replace(position, n, after):按位置替换。replace(before, after):全局替换,将所有的before替换为after。
1 | void test_replace() { |
常用案例
提取路径信息中的文件名,后缀名,所在目录信息
1 | // 提取路径信息中的文件名,后缀名,所在目录信息 |
提取一段K-V对的value值,区间截取
1 | // 提取一段K-V对的value值,区间截取 |
编码转换类
这是 Static Public Members
如上面我们已经提到的一个 fromLocal8Bit ,还有很多类似的
from... 方法,用于匹配编码格式
1 | void test01() { |
数字转换类
就是 to... ,实际写代码的时候可以直接写一个
to
然后会自动提示很多方法,根据名字基本可以看出来是其作用,不行的话还是可以去文档中直接找或者上网/问ai的
还有从数字转换到 QString ,通过 QString::number
静态方法来实现
1 | void test02() { |
格式化类
C风格的是 sprintf ,QString 中是 asprintf
,可以进行格式化字符串
结合 arg 方法,可以格式化填充
1 | void test03() { |
字符串拼接优化
在 Qt 中拼接多段字符串,主要有三种方式:+
操作符、arg() 函数、以及 Qt 特有的 %
操作符。它们在底层的性能表现有着天壤之别。
+ 操作符
该操作符有隐形开销
我们在直觉上觉得 + 号很自然,比如:
1 | QString a = "Hello"; |
底层发生了什么?
C++ 的 + 操作符是从左到右结合的。
- 先计算
a + b:Qt 必须分配一块新内存,把 “Hello “ 放进去,生成一个临时的 QString 对象。 - 再计算
临时对象 + c:Qt 再次分配一块更大的新内存,把 “Hello World” 放进去。 - 最后把生成的字符串赋给
res,并销毁那个中间的临时对象。
结论: 如果你拼接 N 个字符串,底层会发生 N-1 次内存分配和拷贝,产生大量的临时对象。这在循环拼接巨大字符串时,简直是性能灾难。
arg()
该方法效率比较低
1 | QString res = QString("%1 %2").arg(a).arg(c); |
arg() 的好处是语义清晰,非常适合做多语言翻译。
底层发生了什么?
它的缺点是运行时解析开销。Qt
需要在运行时扫描整个字符串,寻找 %1、%2
等占位符的位置,然后再进行替换和内存调整。它比 +
号稍微好一点点。
优化:QStringBuilder
与 % 操作符
为了解决多段拼接的性能问题,Qt 引入了一个基于 C++ 模板元编程(Expression Templates)的机制。
1 |
|
底层发生了什么?
当你使用 % 时,a % b % c
并不会立刻生成任何字符串,也不会分配内存!
- 它在编译期生成了一个复杂的模板类(记录了参与拼接的所有对象)。
- 当它被赋值给
res的那一瞬间,Qt 内部会先一次性计算出总长度(a.size() + b.size() + c.size())。 - Qt 直接在堆上分配一次足够大的内存。
- 将 a, b, c 的内容直接拷贝进这块内存中。
结论: 无论你拼接多少个字符串,使用 %
操作符永远只有一次内存分配,没有中间临时对象。这是真正的零开销抽象。
实战最佳实践
虽然 % 性能无敌,但把老代码里的 + 全改成
% 太累了。Qt 提供了一个宏,可以直接把所有的 +
号“劫持”成 % 的行为
在现代 Qt 工程中,你应该直接在构建脚本中全局开启这个宏:
- 如果是 qmake (
.pro文件):
1 | DEFINES += QT_USE_QSTRINGBUILDER |
- 如果是 CMake (
CMakeLists.txt):
1 | add_compile_definitions(QT_USE_QSTRINGBUILDER) |
只要加了这一行代码,你工程里所有的 QString a + b + c
都会自动在底层替换为 QStringBuilder 机制














