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容易出两个问题:

  1. 重复删除:父对象销毁时,会再次delete子对象,导致崩溃。
  2. 遗漏删除:忘记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内存管理知识体系 Qt内存管理知识体系 QObject内存管理 对象树与父子机制 智能指针家族 RAII原则 自动销毁 deleteLater() QPointer QSharedPointer QWeakPointer QMutexLocker QScopedPointer 核心原则:谁创建,谁负责;父对象管理子对象生命周期 智能指针用于非QObject对象或需要共享所有权的场景 安全、高效、可维护的内存管理

我的建议: 刚开始用Qt时,先记住一条规则——所有QObject子类都指定父对象。等用熟了,再根据场景选择智能指针。别一上来就想用最复杂的方式,简单可靠才是王道。

好了,这一章的内容就到这里。记住:内存管理不是Qt的负担,而是它的优势。用好对象树和智能指针,你的串口程序会稳定得多。