容器

QString

QString 是 Qt 的 Unicode 字符串类型,以 UTF-16 保存文字,并提供查找、截取、替换和编码转换等操作。

QString 与 std::string 的不同

存储编码、操作单位和配套功能。

对比 std::string QString
编码 本身不规定编码,常约定为 UTF-8 UTF-16
size() char 数量,存 UTF-8 时就是字节数 UTF-16 码元数
截取单位 char 字节 UTF-16 码元
文本工具 基本拼接、查找、截取等 另有 Unicode 大小写、字符分类、规范化和编码转换等
普通复制 建立独立字符存储 通常先共享存储,修改时分离
接口配合 标准 C++ 及许多第三方库 Qt 控件、QML、模型、网络等

Unicode 给字符规定编码标识;UTF-8、UTF-16 则规定这些字符怎样存成字节或码元。

1
2
3
4
5
6
7
8
9
// 假设源文件和窄字符串字面量使用 UTF-8 编码。
std::string bytes = "中国";
bytes.size(); // 6 字节。
bytes.substr(0, 1); // 只取一个字节,不是完整的“中”。

// 两个常见汉字各占一个 UTF-16 码元。
QString text = QStringLiteral("中国");
text.size(); // 2 个码元。
text.left(1); // “中”。

QStringLiteral

QStringLiteral("文字") 是 Qt 提供的宏,用来从编译期已知的字符串字面量构造 QString。它不是新的字符串类型,得到的结果仍然是 QString。

1
2
3
4
5
// 普通窄字符串转为 QString:Qt 按 UTF-8 解释并准备 UTF-16 数据。
QString a = "直播间";

// 编译时准备 UTF-16 字面量数据,运行时构造 QString 值来引用它。
QString b = QStringLiteral("直播间");

二者最终表达同一段文字,但构造路径不同:

1
2
普通窄字面量 → 运行期转换为 UTF-16,并准备字符缓冲 → QString
QStringLiteral → 编译期生成静态 UTF-16 数据 → QString 引用这些数据

QStringLiteral 可以避免这一步字面量构造中的运行期编码转换和字符堆缓冲分配。

误区

它只能包装字面量,不能转换运行时变量。

1
2
3
4
5
6
// 网络返回的文字在运行时才知道,明确按 UTF-8 转换。
QByteArray response = "example";
QString text = QString::fromUtf8(response);

// 错误示意:response 不是字面量,不能这样使用。
// QString text = QStringLiteral(response);

在项目中的用法

房间号、主播名、直播标题、弹幕、日志等都使用 QString。例如 RoomService.cpp 用它处理房间信息和请求提示。

1
2
3
4
5
6
// 用户输入可能带空格,先清理首尾空白。
QString input = QStringLiteral(" 13308358 ");
QString roomId = input.trimmed();

// 把变量填入提示文字,得到“正在加载房间 13308358”。
QString message = QStringLiteral("正在加载房间 %1").arg(roomId);

常用操作:

1
2
3
4
5
6
7
8
9
// 查找文字,不修改原字符串。
QString title = QStringLiteral("游戏直播间");
bool found = title.contains(QStringLiteral("游戏")); // true。

// replace() 修改 title 本身。
title.replace(QStringLiteral("游戏"), QStringLiteral("音乐"));

// 拼接得到一个新字符串。
QString result = QStringLiteral("当前:") + title;

项目里的典型用途是固定日志模板、固定字段名和资源路径:

1
2
3
4
5
6
// 源码使用的形式:固定 shader 资源路径在编译期就已确定。
QString shaderPath = QStringLiteral(":/video/shaders/video.frag.qsb");

// 模板部分固定;arg() 仍然在运行时生成包含房间号的新文本。
QString roomId = QStringLiteral("13308358");
QString log = QStringLiteral("正在加载房间 %1").arg(roomId);

QByteArray

QByteArray 保存原始字节,不要求内容是文字,可以包含二进制包头、压缩数据和零字节。

1
2
3
4
5
6
// 显式指定长度,三个字节分别是 A、零字节、B。
QByteArray packet("A\0B", 3);
packet.size(); // 3。

// 提取其中两个字节。
QByteArray part = packet.mid(1, 2);

文字与字节互转:

1
2
3
4
5
// 将 Unicode 文字编码成 UTF-8 字节。
QByteArray bytes = QStringLiteral("直播").toUtf8();

// 确定字节采用 UTF-8 后,再解码成 QString。
QString text = QString::fromUtf8(bytes);

与 STL 相比,它的用途接近 std::vector<char>、std::vector<std::byte> 或用于二进制存储的 std::string,并方便接入 Qt 文件、网络和 Base64 接口。

QByteArray 内容中允许 \0,不要用 strlen() 判断这种二进制数据的长度,应使用 size()。

交给 C 接口时还要注意拥有者寿命:

1
2
3
4
5
6
// bytes 保持存活且不进行导致指针失效的修改时,pointer 可用于读取。
QByteArray bytes = QStringLiteral("直播").toUtf8();
const char* pointer = bytes.constData();

// 错误示意:临时 QByteArray 在分号处销毁,留下悬空指针。
// const char* bad = QStringLiteral("直播").toUtf8().constData();

在项目中的用法

DanmakuPacket.cpp 用它接收和拆解弹幕协议包;VideoMaterial.cpp 也通过字节数组写 shader uniform。

QList / QVector

Qt 6 中 QVector 是 QList 的别名,QList 使用连续存储,概念上接近 std::vector;它不是 std::list 双向链表。

两者都适合按下标访问和顺序遍历。中间插入、删除通常需要移动后续元素。Qt 容器支持隐式共享,std::vector 的普通复制则建立独立元素存储,详见隐式共享与写时复制。

容量不等于元素数量:

1
2
3
4
5
6
7
8
// reserve() 仅预留空间,还没有实际元素。
QList<int> ids;
ids.reserve(16);
ids.size(); // 0,此时不能访问 ids[0]。

// 添加元素后,size 才增加。
ids.append(7);
ids.size(); // 1。

在项目中的用法

1
2
3
4
5
6
7
8
// 保存逻辑位置对应的物理身份;下标从 0 开始。
QList<int> mapping{0, 1, 2, 3};
int first = mapping.at(0); // 0。

// 修改、追加和删除元素。
mapping[0] = 8;
mapping.append(4);
mapping.removeAt(1); // 删除下标 1,后续元素的位置随之调整。

项目使用它保存布局矩形、播放列表条目和热点时间桶:

1
2
3
4
5
// 源码中的类型别名:一组布局矩形。
using LayoutRects = QList<LayoutRect>;

// 源码中的成员:房间条目列表。
QVector<PlaylistEntry> m_rooms;

对应 LayoutTree.hpp、PlaylistEntries.hpp 和 LiveHotspotDetector.hpp

QStringList

QStringList 基于 QList<QString> 是字符串列表,增加了一组字符串常用的处理方法。std::vector<std::string> 可以保存同类数据,但没有作为该容器成员的 join()。QStringList 集中了文本列表的便利操作。

在项目中的用法

项目的画质名称、线路名称、Cookie 拆分都使用它。

1
2
3
4
5
6
7
// 拆分得到“原画”“高清”“流畅”三个元素。
QString text = QStringLiteral("原画,高清,流畅");
QStringList qualities = text.split(',');
QString first = qualities.at(0);

// 把列表重新连接成“原画 / 高清 / 流畅”。
QString display = qualities.join(QStringLiteral(" / "));

项目 Cookie 处理的形式:

1
2
3
4
5
6
7
8
// 一段 Cookie 拆成若干键值项,跳过空项。
QString cookie = QStringLiteral("a=1; b=2; c=3");
const QStringList items = cookie.split(';', Qt::SkipEmptyParts);

// 各项先去掉首尾空格,再交给后续键值解析。
for (const QString& item : items) {
QString cleaned = item.trimmed();
}

QMap

QMap 接近 std::map:按键排序,查找通常为 O(log n);额外提供 Qt 接口及隐式共享。

在项目中的用法

项目用 QMap<QString, QString> 将 Cookie 拆成名称和值。

1
2
3
4
5
6
7
8
9
// Cookie 名称对应 Cookie 值。
QMap<QString, QString> cookies;
cookies.insert("uid", "12345");
cookies.insert("buvid3", "example-device-id");

// 按名称查找;相同键再次插入会替换旧值。
QString uid = cookies.value("uid");
cookies.insert("uid", "67890");
bool exists = cookies.contains("buvid3");

遍历时分别读取键和值:

1
2
3
4
5
// 只读遍历,按键顺序访问。
for (auto it = cookies.cbegin(); it != cookies.cend(); ++it) {
QString name = it.key();
QString value = it.value();
}

源码见 CredentialStore.cpp 的 parseCookieMap()、buildCookieString()。

读取时注意:

1
2
3
4
5
// value() 找不到键时返回默认值,不会插入新项。
QString missing = cookies.value("missing");

// 对非 const QMap 使用 [],找不到键时会插入默认值。
QString inserted = cookies["missing"];

QHash

QHash 接近 std::unordered_map:查找平均 O(1),不保证按键排序。哈希冲突、扩容等仍会影响成本。

需要按键排序通常选 QMap;主要按键查值而不关心顺序可以选 QHash。

在项目中的用法

项目的典型用途是模型角色编号与 QML 字段名之间的映射。

1
2
3
4
5
6
7
8
// 教学示意:编号是键,不要求连续,也不等于列表下标。
QHash<int, QByteArray> roles;
roles.insert(257, "roomId");
roles.insert(258, "title");
roles.insert(259, "playing");

// 查找角色 258 对应的名字。
QByteArray name = roles.value(258); // title。

实际入口是 PlaylistEntries.cpp 的 roleNames()。

QSet

QSet 接近 std::unordered_set。

在项目中的用法

项目用它记录已弹出的格子、已直接关闭的格子,以及检查身份映射是否重复。

1
2
3
4
5
6
7
8
9
10
// 记录已弹出的逻辑格子。
QSet<int> poppedOut;
poppedOut.insert(3);
poppedOut.insert(8);
poppedOut.insert(3); // 重复元素不会再添加一次。

// 成员检查和移除。
poppedOut.size(); // 2。
poppedOut.contains(3); // true。
poppedOut.remove(3);

LayoutEditor.hpp 的实际成员为 QSet<int> m_poppedOut, m_discarded。

QQueue

QQueue 基于 QList。QQueue 本身不是线程安全的阻塞队列,也不会自动限制积压。

在项目中的用法

项目用它保存播放列表刷新请求。

1
2
3
4
5
6
7
8
9
// 两个房间依次等待刷新。
QQueue<QString> requests;
requests.enqueue("13308358");
requests.enqueue("12345");

// dequeue() 同时返回并删除队头;调用前必须确认非空。
if (!requests.isEmpty()) {
QString nextRoom = requests.dequeue(); // 13308358。
}

Playlist.cpp 使用 enqueueRoom() 入队,再由调度逻辑取出 PlaylistRefreshRequest 发起请求。

项目媒体包队列使用 PacketQueue.hpp 中的 std::deque、mutex 和 condition_variable,另外实现容量、背压、等待和唤醒。

QVariantMap / QVariantList

两者的类型关系如下

1
2
3
4
5
// 类型关系示意:字符串键对应一个 QVariant 值。
using QVariantMap = QMap<QString, QVariant>;

// 类型关系示意:每个列表元素都是 QVariant。
using QVariantList = QList<QVariant>;

可以把 QVariantMap 理解为“一条带字段的数据”,QVariantList 理解为“一批数据”。它们便于通用 Qt 数据交换,但字符串键的拼写错误通常不会被编译器发现。

例如把 "title" 写成 "titel" 仍可能编译通过。因此项目内部字段明确的数据继续使用 PlaylistEntry、TileAssignment 等结构体,而不是把所有业务对象改为 QVariantMap。

与 STL 组合相比,可以用 std::map<std::string, std::variant<...>> 和 std::vector<std::variant<...>> 表达类似结构;Qt 这组别名的优势是可以直接配合 QVariant、属性与 QML 接口。

在项目中的用法

布局预览可以这样表达:

1
2
3
4
5
6
7
8
9
10
11
12
// 一个格子的字段中同时包含文字和浮点数。
QVariantMap tile{
{"label", QStringLiteral("窗口 1")},
{"x", 0.0},
{"y", 0.0},
{"w", 0.5},
{"h", 1.0}
};

// 多个格子组成一个布局。
QVariantList layout;
layout.append(tile);

项目的 LayoutEditor.cpp 用这种结构提供布局卡片与预览;热点模块也使用 QVariantMap 传递元信息。

隐式共享、写时复制与 STL 的区别

先共享,写入时再分离

QString、QByteArray、QList、QMap 等容器采用隐式共享,只传递指向数据的指针,并且只有当函数写入数据时才会复制数据,即写时复制。共享类由一个指向共享数据块的指针组成,该数据块包含引用计数和数据。

创建共享对象时,它会将引用计数设置为 1。每当有新对象引用共享数据时,引用计数就会递增;当对象取消引用共享数据时,引用计数就会递减。当引用计数变为零时,共享数据将被删除。在处理共享对象时,有两种复制对象的方式。我们通常称之为深拷贝和浅拷贝。深拷贝意味着复制一个对象。浅拷贝是引用拷贝,即仅指向共享数据块的指针。进行深拷贝在内存和 CPU 方面可能代价高昂。进行浅拷贝非常快,因为它只涉及设置指针和增加引用计数。

1
2
3
4
5
6
7
8
// 原始布局身份列表。
QList<int> original{0, 1, 2};

// 复制时通常只共享底层数据,不立刻复制全部元素。
QList<int> preview = original;

// 需要修改时分离存储,original 保持不变。
preview[0] = 8;
1
2
3
4
5
6
7
8
复制后:
original ─┐
├── [0, 1, 2]
preview ──┘

修改后:
original ─── [0, 1, 2]
preview ─── [8, 1, 2]

std::vector 的普通复制通常立即建立独立元素存储。Qt 将可能的大复制延后到需要写入的时候,因此第一次修改共享副本可能比预想的昂贵。

非 const 访问也可能触发分离

容器不知道你取得可写引用后是否真的会改,所以非 const 的 begin()、operator[] 等调用也可能触发分离。

1
2
3
4
// 只读遍历:把容器转为 const 访问,选择只读迭代器。
for (const auto& room : std::as_const(rooms)) {
qDebug() << room.title;
}

仅把循环变量写成 const auto&,不保证容器使用 const begin();std::as_const() 才是将访问对象变为 const。它声明在标准头文件 <utility> 中。

分离、扩容和修改可能使已有迭代器、引用或裸指针失效。不要拿着一个元素引用,再随意复制或修改相关容器。

复制容器,不等于复制指针指向的对象

1
2
3
4
5
6
// 假设 object 是一个仍然有效的 QObject;两个容器保存同一对象的地址。
QList<QObject*> a{object};
QList<QObject*> b = a;

// 修改的是 QObject,而不是容器中的指针值,不会自动克隆 QObject。
b.at(0)->setObjectName("changed");

这时通过 a 中的指针也能观察到名字变化。STL 容器复制裸指针或 shared_ptr 元素也存在类似的“指向同一个对象”语义。

QObject 本身不能像 QString 一样进行值复制。不要把隐式共享与 QObject 父子所有权、shared_ptr 共享所有权混为一谈。

隐式共享不是自动线程安全

不同线程操作各自的值副本,与多个线程同时读写同一个容器对象,是不同情况。引用计数机制不能代替后者所需的同步。

在项目中的用法

项目布局算法经常复制一组矩形,再计算调整后的结果。只读时共享可以减少复制;真正修改后,两份布局仍互不影响。

项目媒体层仍需要 mutex、condition_variable 和明确的只读帧契约,不能因为外面用了 Qt 类型就省掉线程协调。

Qt 容器与 STL 在项目中分别承担以下任务:

任务 项目例子 原因
文本和 Qt 接口 QString、QStringList 与网络、QML、日志接口配合直接
列表业务与展示 QVector、QAbstractListModel 保存结构化条目,并通知视图
通用字段交换 QVariantMap、QJsonObject 分别负责 Qt 值传递与 JSON 序列化
原始 Y/UV 像素 std::vector 连续字节缓冲,方便媒体数据访问
多线程包队列 std::deque<AVPacket*> 加锁和条件变量 明确队列、包寿命与等待策略
BSP 子节点 std::unique_ptr 表达唯一所有权
跨线程帧持有 std::shared_ptr 延长帧寿命并限制消费者写入

Qt 容器也能使用许多 STL 算法:例如布局代码直接对 QList 的迭代器调用 std::sort。

类型

QVariant

QVariant 是携带类型信息的通用值,可以保存整数、字符串、布尔值,以及符合 Qt 元类型要求的其他类型。

1
2
3
4
5
6
7
// 同一个 QVariant 可以先保存整数,再保存字符串。
QVariant value = 10;
int count = value.toInt();

// 赋入字符串后,其当前保存的值和类型随之改变。
value = QStringLiteral("直播间");
QString title = value.toString();

转换时要检查结果:

1
2
3
4
// 字符串不能解析为整数,不能把返回的零误认为正常业务值。
QVariant value = QStringLiteral("abc");
bool ok = false;
int number = value.toInt(&ok); // ok 为 false。

QVariant 不是所有类型间都能自动正确转换。

STL 中的 std::variant 在编译期明确列出候选类型:

1
2
// 只能保存这些候选类型中的一种,不是任意类型盒子。
std::variant<int, QString, bool> value = 10;

QVariant 更方便接 Qt 属性、模型和 QML;std::variant 适合候选类型明确、希望编译期检查的业务数据。也不要把 QVariant 与无类型裸内存混淆,它保留了类型信息。

两者底层都使用了模板,区别在于模板参数有没有成为对象类型的一部分:

1
2
3
4
5
6
7
// 两个不同的类模板实例,是两种不同的 C++ 类型。
std::variant<int, QString> a;
std::variant<int, bool> b;

// 包装的内容不同,但两个对象的 C++ 类型都是 QVariant。
QVariant c = QVariant::fromValue<int>(10);
QVariant d = QVariant::fromValue<QString>(QStringLiteral("直播间"));

QVariant::fromValue<T>() 内部利用模板获知 T,取得对应的类型信息,并把对象放入存储。之后统一用 QVariant 表示,通过保存的类型信息处理它。这个过程叫类型擦除:对外隐藏具体类型,但内部仍保留管理它所需的信息。

std::variant QVariant
底层使用模板 是 是
类本身是类模板 是 否
具体类型如何表达 候选类型属于模板参数 当前类型记录在对象内部的类型信息中
对外是什么类型 variant<int, QString> 等不同类型 统一为 QVariant

在项目中的用法

项目模型的 data() 只有一种返回类型 QVariant,但不同角色要返回不同数据:

1
2
3
4
5
6
7
8
9
10
11
// 模型内部教学片段:返回值被统一包装成 QVariant。
switch (role) {
case TitleRole:
return room.title; // QString。
case LiveStatusRole:
return room.liveStatus; // int。
case LoadingRole:
return room.loading; // bool。
default:
return {}; // 无效 QVariant,表示没有对应数据。
}

QJsonObject / QJsonArray / QJsonDocument

类型 负责什么
QJsonObject 表示 JSON 的花括号对象 {}
QJsonArray 表示 JSON 的方括号数组 []
QJsonDocument 将对象或数组解析自 JSON 字节,或序列化为 JSON 字节

QVariantMap 与 QJsonObject 不同:前者可以保存许多 Qt 或注册的 C++ 类型,后者受 JSON 的字符串、数字、布尔、数组、对象、null 等格式约束。QVariant 是程序内部的通用值,JSON 是序列化格式。

标准库本身没有对应的完整 JSON 解析器,使用 STL 的项目通常另外引入 JSON 库。

在项目中的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个带多个字段的 JSON 对象。
QJsonObject room{
{"roomId", "13308358"},
{"title", QStringLiteral("直播间")},
{"playing", true}
};

// 多个房间对象放入数组。
QJsonArray rooms;
rooms.append(room);

// 序列化为紧凑 JSON 字节,可交给文件或网络接口。
QByteArray bytes = QJsonDocument(rooms).toJson(QJsonDocument::Compact);

反过来解析:

1
2
3
4
5
6
7
8
9
// 先检查 JSON 语法,再确认顶层类型是否为数组。
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(bytes, &error);
if (error.error == QJsonParseError::NoError && doc.isArray()) {
QJsonArray array = doc.array();
if (!array.isEmpty()) {
QString title = array.at(0).toObject().value("title").toString();
}
}

项目用它们解析平台接口响应,并保存布局树和身份映射。上面省略了业务字段逐项校验。

QAbstractListModel

它是数据与视图之间的协议。三个基本接口分别回答:

  • rowCount():一共有多少行?
  • data():某行的某个字段是什么?
  • roleNames():QML 使用什么名称访问字段?

STL 容器只管理数据,不提供这种视图通知;QAbstractListModel 负责把数据变化解释给 Qt 的视图系统。

在项目中的用法

项目播放列表有两层:

1
2
3
4
5
QVector<PlaylistEntry>:保存房间数据
↓
PlaylistEntries : QAbstractListModel:提供行、角色和变化通知
↓
QML 列表界面:根据通知读取并展示
1
2
3
4
5
6
7
// 模型内部简化片段:把角色编号映射为 QML 字段名。
QHash<int, QByteArray> PlaylistEntries::roleNames() const {
return {
{RoomIdRole, "roomId"},
{TitleRole, "title"}
};
}

项目添加房间时,先声明插入、再改数据、最后结束插入:

1
2
3
4
5
6
7
// 模型内部教学片段:通知视图第 0 行即将插入。
beginInsertRows(QModelIndex(), 0, 0);
m_rooms.insert(0, room);
endInsertRows();

// 已有行内容变化时,通知视图重新读取相应数据。
emit dataChanged(index(row), index(row));

对应 PlaylistEntries.cpp。只执行 m_rooms[0].title = ... 会改变内存,但 QML 不会自动获知普通 C++ 容器元素的变化。

QRectF / QSize

QRectF 表示浮点矩形,包含位置 x、y 和尺寸 width、height。QSize 只表示整数宽高,不含位置;QSizeF 表示浮点宽高。

标准库没有 GUI 几何类型;std::pair 虽然能放两个数,但没有宽高语义和几何方法。

在项目中的用法

项目布局通常先使用 0~1 的归一化坐标,显示时再映射为像素。

1
2
3
4
5
6
7
8
// 位于左半边、铺满高度的归一化矩形。
QRectF leftTile(0.0, 0.0, 0.5, 1.0);
leftTile.width(); // 0.5。
leftTile.height(); // 1.0。
leftTile.right(); // 0.5。

// 判断转换到同一坐标系的鼠标点是否位于格子内。
bool inside = leftTile.contains(QPointF(0.25, 0.5)); // true。

包围盒用于合并或联动计算:

1
2
3
4
// 两块区域的最小外接矩形。
QRectF left(0.0, 0.0, 0.5, 1.0);
QRectF right(0.5, 0.0, 0.5, 1.0);
QRectF whole = left.united(right);

united() 得到包围盒,并不保证输入区域无空洞,所以项目还需要面积等合法性检查。

1
2
3
4
// 以像素表示视频或纹理的尺寸。
QSize videoSize(1920, 1080);
int width = videoSize.width();
int height = videoSize.height();

源码见 LayoutTree.cpp、VideoGeometry.cpp

QUrl / QUrlQuery

结构化处理地址与参数
URL 包含协议、主机、路径、查询参数。

QUrlQuery 便于按查询参数结构维护和编码,减少手工处理问号、连接符和特殊字符的错误。

在项目中的用法

1
2
3
4
5
6
7
8
// 地址主体与查询参数分开组织。
QUrl url("https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo");
QUrlQuery query;
query.addQueryItem("room_id", "13308358");
query.addQueryItem("qn", "10000");
query.addQueryItem("protocol", "0");
query.addQueryItem("format", "0");
url.setQuery(query);

项目入口是 RoomService.cpp 的 requestStream()。

QDateTime / QElapsedTimer

QDateTime 回答“什么时候”

用途大致对应 std::chrono::system_clock 的时间点,但 QDateTime 还提供日期、格式化、时区等 Qt 接口。涉及跨时区或 UTC 数据时,要确保双方时间含义一致。

QElapsedTimer 回答“经过多久”

1
2
3
4
5
6
// 启动间隔测量,不表示某年某月某日。
QElapsedTimer timer;
timer.start();

// 做完待测操作后,查询从起点到现在经过的毫秒数。
qint64 elapsedMs = timer.elapsed();

Qt 6.8 的 QElapsedTimer 使用单调时钟,接近 std::chrono::steady_clock,不会因手动调整系统日历时间而跳变

QElapsedTimer 不会自己周期触发回调,周期事件是 QTimer 的职责。这里 QTimer 决定何时统计,QElapsedTimer 测量真实经过的时长。

在项目中的用法

项目用 QDateTime 记录开播时间、最近刷新时间等。

1
2
3
4
5
// 记录这次刷新发生的日历时间。
QDateTime lastChecked = QDateTime::currentDateTime();

// 之后计算两次时间点间隔,用于“几秒前刷新”的显示。
qint64 seconds = lastChecked.secsTo(QDateTime::currentDateTime());

项目 FPS 统计的核心片段:

1
2
3
4
5
6
// 源码摘录:获取实际间隔,同时将起点重置为现在。
const auto elapsed = m_elapsed.restart();

// 定时回调未必恰好每秒到达,因此按实际时长计算 FPS。
if (elapsed > 0)
setRenderFps((frames - m_lastFrames) * 1000.0 / elapsed);

见 VideoSurface.cpp

qint64 / quint64 / qsizetype

Qt 类型 标准类型的近似对应 关键点
qint64 std::int64_t 有符号,64 位
quint64 std::uint64_t 无符号,64 位
qsizetype 用途接近 std::ptrdiff_t 有符号尺寸类型,位宽随平台

qsizetype 与 size_t 不完全一样,size_t 是无符号的。混合有符号和无符号值进行比较、减法时,要留意转换结果。

在项目中的用法

1
2
3
4
5
6
7
8
9
// 有符号 64 位整数,保存毫秒时间戳。
qint64 timestampMs = QDateTime::currentMSecsSinceEpoch();

// 无符号 64 位整数,保存递增帧编号。
quint64 frameId = 1;

// Qt 容器的有符号尺寸类型,保存字节数。
QByteArray packet;
qsizetype length = packet.size();

项目弹幕解析使用 qsizetype offset,与 QByteArray::size() 配合,避免把较宽的长度放进 int 发生截断。

1
2
3
// 长度和偏移采用相同类型,计算剩余数据量。
qsizetype offset = 0;
qsizetype remaining = packet.size() - offset;

QPointer

观察 QObject,对象销毁后自动清空
QPointer 不拥有对象,只观察 QObject 派生对象是否仍然存在。

1
2
3
4
5
6
7
// 独立教学示例:对象由其他代码负责销毁。
QObject* object = new QObject;
QPointer<QObject> observer = object;

// QObject 销毁后,observer 自动清空。
delete object;
bool gone = observer.isNull(); // true。

普通裸指针在 delete 后通常仍保留旧地址,成为悬空指针;QPointer 会在 QObject 销毁时被清空。

类型 是否拥有对象 失效/释放方式
T* 类型本身不表达所有权 对象销毁后可能悬空
QPointer 不拥有,只适用于 QObject 派生类 QObject 销毁后自动清空
std::unique_ptr 独占所有权 拥有者释放对象
std::shared_ptr 共享所有权 最后一个拥有者释放对象
std::weak_ptr 不拥有,观察 shared_ptr 管理的对象 用 lock() 尝试取得临时共享所有权

QPointer 不是跨线程寿命锁,不能认为“检查非空后,对象就绝不会在另一线程销毁”。它也不同于 shared_ptr,不提供临时保活的 lock()。

在项目中的用法

项目 VideoSurface.hpp 中的 QPointer<PlaybackSession> m_session 表示显示组件观察会话,不靠这个指针延长会话寿命。

1
2
// 源码中的读取形式:会话不存在时不再调用取帧方法。
const auto frame = m_session ? m_session->latestFrame() : nullptr;