2. 嵌入式系统与Qt基础
好,咱们进入第二章。这一章我把它叫做「地基加固」。为什么?因为串口编程里,内存泄漏、野指针、对象提前销毁,这些坑我几乎每个项目都踩过。你想想看,一个串口设备连着,程序跑着跑着突然崩溃了,查半天发现是父对象被删了,子对象还在发信号——这种问题,光靠肉眼很难看出来。
所以,咱们先把Qt的内存管理机制彻底搞明白。说白了,就是搞清楚三个问题:谁创建、谁销毁、什么时候销毁。
2.1 Qt对象树与父子对象机制
Qt最核心的设计之一,就是对象树。我刚开始学Qt时,觉得这东西不就是个链表嘛。后来做嵌入式项目,发现它远比想象中重要。
每个QObject都可以有一个父对象。父对象销毁时,会自动销毁所有子对象。这就是Qt的内存管理基础。
核心规则: 给QObject或其子类指定父对象,Qt会自动管理其生命周期。
// 正确做法:指定父对象
QPushButton *btn = new QPushButton("发送", this); // this是父窗口
// 错误做法:不指定父对象,需要手动delete
QPushButton *btn2 = new QPushButton("发送"); // 需要手动管理
我在项目中见过一个典型案例:有人new了一个QTimer,没指定父对象,也没手动delete。结果定时器一直在后台跑,内存泄漏不说,还导致CPU占用居高不下。嗯,这种问题排查起来特别头疼。
| 场景 | 父对象设置 | 内存管理方式 | 风险等级 |
|---|---|---|---|
| 窗口部件 | 必须设置 | Qt自动管理 | 低 |
| QTimer | 建议设置 | Qt自动管理 | 中 |
| QThread | 谨慎设置 | 需注意线程安全 | 高 |
| 自定义QObject | 建议设置 | Qt自动管理 | 中 |
注意: 对象树不是线程安全的。跨线程操作父子关系时,一定要加锁或使用信号槽。
2.2 QObject内存管理基础
Qt的内存管理,说白了就是谁创建,谁负责。但有了对象树,这个「谁负责」就变成了「父对象负责」。
我个人的习惯是:能用父对象管理的,绝不手动delete。为什么?因为手动delete容易出两个问题:
- 重复删除:父对象销毁时,会再次delete子对象,导致崩溃。
- 遗漏删除:忘记delete,内存泄漏。
// 错误示例:手动delete导致重复删除
QWidget *widget = new QWidget();
QLabel *label = new QLabel("Hello", widget); // label的父对象是widget
delete label; // 手动删除label
delete widget; // widget销毁时,会再次尝试删除label → 崩溃!
我曾经在一个串口调试工具里犯过这个错。程序跑着跑着就崩,查了两天才发现是手动delete了一个子对象。从那以后,我再也不敢手动delete有父对象的QObject了。
经验之谈: 如果你确实需要提前销毁一个子对象,调用deleteLater()而不是直接delete。它会等事件循环处理完再安全删除。
2.3 QPointer与QSharedPointer使用入门
对象树解决了大部分内存管理问题,但有一个场景它搞不定:悬空指针。
你想想看,如果A对象持有一个指向B对象的指针,B被销毁了,A还不知道。这时候A去访问B,程序就崩了。
Qt提供了QPointer来解决这个问题。它就像一个「智能观察者」,被指向的对象销毁时,它会自动变成nullptr。
QPointer<QLabel> ptr = new QLabel("Hello");
delete ptr; // 销毁对象
if (ptr.isNull()) {
qDebug() << "对象已被销毁,安全!";
}
但QPointer有个限制:它只能用于QObject及其子类。对于普通C++对象,我们需要QSharedPointer。
| 智能指针 | 适用对象 | 引用计数 | 自动置空 | 线程安全 |
|---|---|---|---|---|
| QPointer | QObject及其子类 | 否 | 是 | 是 |
| QSharedPointer | 任意类型 | 是 | 否 | 是(原子操作) |
| QWeakPointer | 任意类型 | 否(弱引用) | 是 | 是 |
| std::shared_ptr | 任意类型 | 是 | 否 | 是(原子操作) |
// QSharedPointer使用示例
QSharedPointer<MyClass> sp1 = QSharedPointer<MyClass>::create();
QSharedPointer<MyClass> sp2 = sp1; // 引用计数+1
// 自动管理生命周期,最后一个引用销毁时释放对象
避坑指南: 我曾经在串口数据解析模块里混用了QSharedPointer和原始指针,导致引用计数混乱,对象被提前释放。记住:要么全用智能指针,要么全用原始指针,不要混用。
2.4 RAII原则在Qt中的应用
RAII(Resource Acquisition Is Initialization),说白了就是资源在构造函数中获取,在析构函数中释放。这是C++最强大的设计模式之一。
在Qt中,RAII的应用无处不在:
- QFile:打开文件时获取资源,析构时自动关闭。
- QMutexLocker:构造时加锁,析构时解锁。
- QScopedPointer:构造时接管指针,析构时自动delete。
// RAII在Qt中的典型应用
void readFile(const QString &path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
return; // 文件打开失败,file析构时自动关闭
}
QByteArray data = file.readAll();
// file析构时自动关闭,无需手动close()
}
我个人特别喜欢QMutexLocker。以前写多线程串口程序时,经常忘记解锁,导致死锁。用了QMutexLocker之后,再也没出过这种问题。
// 使用QMutexLocker避免忘记解锁
QMutex mutex;
void sendData(const QByteArray &data) {
QMutexLocker locker(&mutex); // 自动加锁
// 发送数据...
// locker析构时自动解锁,即使发生异常也会解锁
}
核心思想: RAII让资源管理与对象生命周期绑定。对象活着,资源就在;对象死了,资源自动释放。这比手动管理安全得多。
2.5 本章知识体系
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个「内存管理决策树」来用。
我的建议: 刚开始用Qt时,先记住一条规则——所有QObject子类都指定父对象。等用熟了,再根据场景选择智能指针。别一上来就想用最复杂的方式,简单可靠才是王道。
好了,这一章的内容就到这里。记住:内存管理不是Qt的负担,而是它的优势。用好对象树和智能指针,你的串口程序会稳定得多。