QT面经(含相关知识)
1、QT中connect函数的第五个参数是什么?有什么作用?
第五个参数是连接类型(Qt::ConnectionType),用于指定信号与槽之间的连接方式,决定槽函数被调用的时机和线程环境。
该参数主要用于控制信号发出后槽函数的执行方式,例如
:Qt::DirectConnection
表示立即在信号发送线程中执行槽函数;Qt::QueuedConnection
表示将槽函数调用延迟到事件循环中执行,常用于跨线程通信,确保线程安全;Qt::AutoConnection
为默认值,程序会根据信号和槽所处线程自动选择上述两种方式之一。正确使用该参数对多线程应用的稳定性和性能至关重要。
好的,我来帮你整理 Qt 信号和槽机制的优点,保持面试可用的简洁+分点形式:
2、Qt 信号和槽机制的优点
Qt 的信号槽机制优点在于 **解耦合、类型安全、跨线程安全、灵活性高、内存安全**,比传统回调函数更适合大型 C++ 程序。
- 解耦合
- 信号的发送者和接收者互不依赖,不需要提前知道对方的存在。
- 对比回调函数,减少强耦合,提高代码可维护性。
- 类型安全
- 编译期会检查信号和槽的参数是否匹配,避免函数签名不一致导致的运行时错误。
- 支持跨线程通信
- 利用
Qt::QueuedConnection
等机制,可以安全地在不同线程之间传递消息,而不用自己写锁或条件变量。
- 利用
- 灵活性高
- 一个信号可以连接多个槽,一个槽也可以接收多个信号,支持多对多关系。
- 可以随时
connect
/disconnect
,比传统回调更灵活。
- 内存安全
- Qt 内部使用对象树(parent-child)和
QMetaObject
管理,当对象销毁时会自动断开相关信号槽,避免野指针调用。
- Qt 内部使用对象树(parent-child)和
- 统一事件处理
- 信号槽机制是基于 Qt 事件系统的,天然与 GUI、事件循环集成,简化消息传递与异步编程。
3、在QT中,如何使用QSqlDatabase类的transaction()函数来开始一个事务,如何使用commit()函数来提交事务,如何使用rollback()函数来回滚事务
**总结**:在Qt中,
QSqlDatabase::transaction()开始事务,
commit()提交事务,
rollback()回滚事务,确保数据库操作的原子性。
展开:
-
开始事务:调用
QSqlDatabase::transaction()
启动事务,返回true
表示成功。例如:QSqlDatabase db = QSqlDatabase::database(); if (!db.transaction()) {qDebug() << "Failed to start transaction"; }
-
执行操作:使用
QSqlQuery
执行SQL语句,所有操作在事务中暂存。例如:QSqlQuery query(db); query.exec("INSERT INTO table_name (column1) VALUES ('value1')");
-
提交事务:调用
commit()
保存更改,若失败则处理错误:if (db.commit()) {qDebug() << "Transaction committed"; } else {qDebug() << "Commit failed";db.rollback(); }
-
回滚事务:若操作失败,调用
rollback()
撤销更改:if (!query.exec()) {qDebug() << "Query failed";db.rollback(); }
注意:确保数据库连接有效且驱动支持事务(如SQLite支持,MySQL的MyISAM不支持)。使用lastError()
检查错误。
说明:回答以一句总结开头,简洁展开核心步骤,代码示例精炼,适合面试快速表达。如果需要更具体的内容,请告诉我!
5、QT数据库中的事务 transaction的作用是什么?举例说明
**总结**:Qt数据库中的事务(Transaction)是一组操作的逻辑单元,确保数据库操作的原子性、一致性、隔离性和持久性(ACID),保证数据完整性。
事务的作用
事务的主要作用是保证一组数据库操作要么全部成功执行并保存,要么全部失败并回滚,从而维护数据库的一致性和完整性。它遵循ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全执行,要么全不执行。
- 一致性(Consistency):事务完成后,数据库保持一致状态。
- 隔离性(Isolation):事务之间相互隔离,防止并发操作干扰。
- 持久性(Durability):事务提交后,数据永久保存。
举例说明
以银行转账为例,假设从账户A转100元到账户B:
- 启动事务:
QSqlDatabase::transaction()
。 - 执行操作:
- 从账户A减去100元。
- 向账户B增加100元。
- 如果两个操作都成功,调用
commit()
提交事务,保存更改。 - 如果任一操作失败(例如账户A余额不足),调用
rollback()
回滚,撤销所有操作。
代码示例:
QSqlDatabase db = QSqlDatabase::database();
if (db.transaction()) {QSqlQuery query(db);// 操作1:从账户A减100if (query.exec("UPDATE accounts SET balance = balance - 100 WHERE id = 'A'")) {// 操作2:向账户B加100if (query.exec("UPDATE accounts SET balance = balance + 100 WHERE id = 'B'")) {db.commit(); // 提交事务qDebug() << "Transfer succeeded";} else {db.rollback(); // 回滚事务qDebug() << "Failed to credit account B";}} else {db.rollback(); // 回滚事务qDebug() << "Failed to debit account A";}
} else {qDebug() << "Failed to start transaction";
}
注意:事务需确保数据库驱动支持(如SQLite、PostgreSQL支持,MySQL的MyISAM不支持)。使用QSqlDatabase::lastError()
可排查错误。
6、connect函数的第五个参数是什么?怎么使用?
总结:connect函数的第五个参数是Qt::ConnectionType,用于指定信号和槽的连接方式。
说明: 第五个参数Qt::ConnectionType控制槽函数的触发方式,如Qt::AutoConnection(默认,自动选择)、Qt::DirectConnection(同步执行)、Qt::QueuedConnection(异步执行)。
使用示例:
cpp
QObject::connect(&sender, &Sender::mySignal, &receiver, &Receiver::mySlot, Qt::QueuedConnection);
在多线程场景中,Qt::QueuedConnection确保槽函数在接收者线程中异步执行,保障线程安全。
7、QT中可以在子线程操作UI界面吗?
总结:Qt中子线程不能直接操作UI界面,需通过信号和槽或QMetaObject::invokeMethod
与主线程交互以更新UI。
说明:
Qt的UI组件运行在主线程,直接在子线程操作UI会导致崩溃。安全方法包括:
- 信号和槽:子线程发出信号,主线程槽函数更新UI。
- QMetaObject::invokeMethod:子线程调用主线程槽函数,异步执行。
使用示例:
// 信号和槽
connect(&worker, &Worker::updateUISignal, &window, &MainWindow::updateUI);
worker.moveToThread(&thread);
thread.start();// invokeMethod
QMetaObject::invokeMethod(ui->label, "setText", Qt::QueuedConnection, Q_ARG(QString, "Data"));
注意:使用Qt::QueuedConnection
确保线程安全,信号和槽是首选方法。
7、QT中的事件处理机制是什么?
总结:Qt的事件处理机制基于事件循环,通过从事件队列获取并处理用户交互、系统事件等,异步更新应用程序状态。
说明:
Qt的事件处理机制以事件循环为核心,主要步骤如下:
- 事件循环:通过
QCoreApplication::exec()
启动,持续运行,处理事件队列中的事件。 - 事件获取:事件(如鼠标点击、键盘输入、定时器)从队列中取出,发送给目标组件(如
QWidget
)。 - 事件传播:事件从顶层组件向下传递,若未处理则传给父组件。
- 事件处理:组件通过虚函数(如
mousePressEvent
)或自定义槽函数处理事件,返回true
表示已处理,false
则继续传递。 - 事件移除:处理后事件从队列移除,继续处理下一事件。
使用示例:
class MyWidget : public QWidget {
protected:void mousePressEvent(QMouseEvent *event) override {qDebug() << "Mouse pressed at" << event->pos();}
};
需要注意的是,QT的事件处理机制是异步的
,这意味着当一个事件正在被处理时,其他事件仍然可以被添加到事件队列中并等待被处理。这种机制使得QT的应用程序能够响应用户的交互和系统的变化,并保持高效的运行。
8、QT中setMouseTracking的作用
总结:setMouseTracking
函数用于启用或禁用组件的鼠标移动跟踪,使其在鼠标移动时接收mouseMoveEvent
事件。
说明:
默认情况下,Qt组件(如QWidget
)仅在鼠标按下或释放时接收事件,鼠标移动事件(mouseMoveEvent
)不会触发。调用setMouseTracking(true)
启用鼠标跟踪后,组件会在鼠标移动时(即使未按下按钮)持续接收mouseMoveEvent
,可用于实现如鼠标悬停效果或实时坐标跟踪。设置为false
则禁用跟踪,恢复默认行为。
使用示例:
class MyWidget : public QWidget {
public:MyWidget() {setMouseTracking(true); // 启用鼠标跟踪}
protected:void mouseMoveEvent(QMouseEvent *event) override {qDebug() << "Mouse at:" << event->pos();}
};
注意:
- 启用鼠标跟踪可能增加性能开销,建议仅在需要时使用。
- 子组件和父组件的鼠标跟踪独立设置,需根据需求分别配置。
9、QObject类是如何实现信号与槽机制的?
总结:QObject通过Qt的元对象系统(Meta-Object System)、Q_OBJECT宏和moc工具实现信号与槽机制,自动生成代码以管理信号发射和槽函数调用。
说明:
Qt的信号与槽机制依赖以下核心组件:
- QObject基类:提供信号与槽的运行时支持,
connect
函数用于建立信号与槽的关联。 - Q_OBJECT宏:标记类以启用元对象特性,通知moc工具处理该类。
- moc工具:在编译时解析Q_OBJECT宏,生成信号的实现代码和元数据(如
qt_static_metacall
函数),包括信号和槽的索引及调用逻辑。
实现过程:
- 连接:
QObject::connect
记录发送者、信号、接收者和槽的关联,存储在QObject的内部连接表中。 - 信号发射:moc为信号生成代码,调用
QObject::activate
触发所有连接的槽函数。 - 槽调用:根据信号的元数据索引,调用对应的槽函数,支持跨线程(通过
Qt::ConnectionType
)。
使用示例:
class MyClass : public QObject {Q_OBJECT
public:void trigger() { emit mySignal(); }
signals:void mySignal();
slots:void mySlot() { qDebug() << "Slot called"; }
};
QObject::connect(&obj, &MyClass::mySignal, &obj, &MyClass::mySlot);
注意:
- 信号是moc自动实现的空函数,仅负责触发;槽由开发者实现。
- 跨线程连接需使用
Qt::QueuedConnection
确保线程安全。
8、QT的信号与槽的底层实现是什么?原理是什么?
QT的信号与槽的底层实现是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为信号函数;每个槽也可以用函数表示,称为槽函数。信号和槽机制实现的功能其实就是信号函数调用槽函数的效果。
在QT中,每个对象都有一个相应的记录该对象的元对象,元对象类为QMetaObject,记录元对象数据信号与槽的是QMetaData类。QObject类实现了信号与槽机制,它利用元对象记录的信息,实现了信号与槽机制。
信号与槽建立连接的实现是通过QObject类的connect()函数,它的参数包括发送对象、信号、接收对象和槽。连接内部的实现接口是connectInternal()。当信号发生时,会激活操作函数接口QObject::active_signal()。
在实际开发中,可以使用QT提供的信号函数和槽函数,也可以根据需要自定义信号函数和槽函数。QT Creator 提供了很强大的QT GUI开发手册,可以很容易地查到某个控件类中包含哪些信号函数和槽函数。
QT面经
1、Qt 的优点、缺点
优点:
跨平台,几乎支持所有平台
接口简单,文档详细
开发效率高
缺点: Qt 作为一个软件平台,比较庞大、臃肿。
2、Qt 的核心机制
Qt核心机制就是 **元对象系统 → 信号与槽 → 对象树内存管理 → 事件系统**。
面试精简版:
- 元对象系统:
moc
工具扩展C++,实现信号槽/动态属性(需QObject
+Q_OBJECT
)。 - 信号与槽:类型安全的通信机制,替代回调函数,支持跨线程。
- 对象树:父子对象自动内存管理(父删子亡),避免泄漏。
- 事件系统:事件循环驱动UI交互,耗时操作需移出主线程。
一句话总结:元对象是地基,信号槽管通信,对象树管内存,事件系统管交互——四者缺一不可。
ps:moc工具会将宏展开生成额外的c++代码,包含调用信号与槽的调用逻辑,相关 的元信息
3、信号与槽机制原理
Qt 信号槽的原理是通过 moc 生成的元对象系统,把信号和槽的信息存储在类的 staticMetaObject
中,在 connect
时建立映射,在 emit
时通过索引查找并调用槽函数。
详细流程
- moc 扫描类定义
- 解析
signals
和slots
关键字。 - 在生成的
xxx.moc
文件里,创建类的 元对象信息(staticMetaObject
)。
- 解析
- 元对象存储信息
- 信号、槽的名字和参数列表会存放在
staticMetaObject
里。 - 每个信号、槽都有一个唯一 索引号,按声明顺序排列。
- 信号、槽的名字和参数列表会存放在
- connect 建立映射
- 调用
QObject::connect()
时,Qt 在staticMetaObject
中查找信号和槽的索引。 - 把 “信号索引 → 槽索引” 存放在一个内部的连接表(map)里。
- 调用
- emit 发射信号
- 调用
emit signal()
时,本质就是执行信号对应的函数。 - 这个函数会调用 Qt 的内部方法
QMetaObject::activate()
。
- 调用
- 激活机制
activate()
根据当前对象、信号索引,在 map 里找到所有对应的槽函数。- 遍历这些槽,并调用它们。
- 执行槽函数
- 槽函数可以是:成员函数、静态函数、lambda,甚至另一个信号。
- Qt 内部通过函数指针或事件循环把参数传递过去。
面试回答示例
Qt 的信号槽机制是基于元对象系统实现的。
moc
工具会扫描类定义,把信号和槽存到类的staticMetaObject
中并建立索引。connect
时将信号索引和槽索引记录到连接表中;当emit
一个信号时,实际上调用QMetaObject::activate()
,它会根据索引找到所有绑定的槽函数并依次调用,从而实现对象间的解耦通信。
4、Qt信号槽机制的优势和不足
优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。
**优势
- 类型安全:信号和槽的参数必须匹配,编译器能帮忙检查,避免类型错误。
- 松散耦合:发送信号的一方不用关心谁接收,只负责发出信号即可,降低了对象间的耦合度。
- 灵活性高:一个信号可以连接多个槽,一个槽也可以接收多个信号,方便扩展。
不足
4. 性能开销:相比直接函数调用,信号槽要经过索引查找和参数打包解包,大概会慢一个数量级。
5. 多线程下有额外开销:跨线程时,信号需要进入事件队列,调度更复杂。
面试回答示例
Qt 的信号槽机制优点是类型安全、解耦性好,而且很灵活,一个信号可以连接多个槽。缺点是性能开销比直接函数调用要大一些,尤其是跨线程时还需要排队调度,但一般应用中影响不大。
5、Qt信号和槽的本质是什么
Qt 信号槽的本质就是 一种更安全、更灵活的回调机制。
说明
- 信号:就是一个特殊的函数调用,本质是事件通知(值变化、动作变化)。
- 槽:就是对应的回调函数,接收信号后执行相应逻辑。
- 本质:Qt 用元对象系统(moc)封装了回调机制,保证了类型安全、解耦和灵活性。
面试回答示例
Qt 的信号和槽本质上就是回调机制。信号负责发出事件,槽函数负责响应。Qt 通过元对象系统做了封装,比传统回调更安全和灵活。
6、信号与槽与函数指针的比较
一句话总结:Qt 信号槽机制比函数指针回调更简洁、解耦、可维护性强。
- 函数指针回调需要显式维护调用关系,类之间耦合度高,代码冗长且灵活性差。
- Qt 信号槽机制只需声明并连接,调用关系由框架自动处理,写法清晰直观。
- 信号与槽实现完全解耦,发射端无需关心接收端,接收端也不依赖发射端,从而降低了出错率并提升了可维护性。
参考: 信号槽机制与回调函数的区别
7、Qt 的事件过滤器
一句话总结:Qt 事件过滤器提供了一种灵活机制,可以在事件分发前拦截并处理事件,实现局部或全局的事件监听。
- 父窗口类重写
eventFilter
,即可监听子控件的事件,避免必须继承子控件并重写事件处理函数。 - 可以创建专门的事件过滤器类,对某类事件进行统一处理,并可复用在多个对象上。
- 在
QApplication
上安装事件过滤器,可以实现全局事件监听,如全局快捷键。 - 返回值
true
表示事件被拦截,不再传递给目标对象;false
表示继续分发给目标对象。 - 一个对象可以安装多个事件过滤器,按安装顺序倒序调用(先进后出)。
- 一个过滤器可安装在多个对象上,但若在过滤器中删除了对象,必须返回
true
,否则可能导致崩溃。
详见: Qt之事件过滤器(eventFilter)详解
8、为什么 new QWidget 不需要 delete
Qt 通过 **对象树(Object Tree)机制** 自动管理继承自 QObject的对象生命周期。
QObject
支持父子关系,每个对象都可以指定一个父对象(parent)。- 当子对象创建时,会被自动加入父对象的子对象列表。
- 当父对象析构时,会自动析构其所有子对象。
- 当某个子对象被析构时,它也会主动从父对象的子对象列表中移除,避免重复析构。
因此,大多数情况下我们不需要手动 delete
QWidget,只要确保对象的父子关系正确即可。
详见: Qt编程中new出来的控件为什么没有delete
9、信号与槽的多种用法
一句话总结:信号和槽非常灵活,可以一对多、多对一、信号连信号,并支持动态管理和Lambda写法。
- 一信号多槽:一个信号可以连接多个槽;同线程时按声明顺序执行,跨线程时顺序不确定。
- 多信号一槽:多个信号可以连接到同一个槽,任意信号发出都会触发槽。
- 信号连接信号:一个信号可以触发另一个信号,逻辑上等同于信号-槽连接。
- 断开槽连接:可以用
disconnect()
取消连接,一般对象销毁时会自动取消。 - Lambda表达式:Qt5 以上支持直接用 Lambda 作为槽,写法简洁、灵活。
参考: qt之信号与槽的原理
10、Qt connect 函数的连接方式
一句话总结:Qt 提供多种连接方式,决定槽函数何时以及如何被调用,并可避免重复连接。
- Qt::AutoConnection(默认):同线程直接调用(同步),跨线程放入事件队列(异步)。
- Qt::DirectConnection:槽函数在发射信号时立即执行,同步调用。
- Qt::QueuedConnection:信号放入接收对象线程的事件队列,异步执行,emit 后代码立即执行。
- Qt::BlockingQueuedConnection:类似 QueuedConnection,但发送者线程会阻塞,直到槽执行完,注意不能同线程使用,否则死锁。
- Qt::UniqueConnection:避免重复连接,可与其他连接类型组合使用。
11、事件与信号的区别
一句话总结:事件是底层消息机制,信号是高层对象间通信机制。
- 使用场合不同:
- 信号用于“使用”控件,比如关注 QPushButton 的
clicked()
信号。 - 事件用于“实现”控件,需要处理鼠标、键盘等底层动作,然后可能再发射信号。
- 信号用于“使用”控件,比如关注 QPushButton 的
- 机制和原理不同:
- 事件类似 Windows 消息,发出者一般是系统,放入事件队列,非阻塞,支持异步。
- 信号是对象间通信机制,发出者是对象,本质是回调,通常同步调用,不依赖事件队列。
12、信号与槽机制需要注意的问题
一句话总结:灵活好用,但有性能和使用限制,需要注意避免死循环和不支持的用法。
- 性能开销:信号与槽比普通回调稍慢,但通常影响很小(如 10 微秒级),实时系统要慎用。
- 避免死循环:槽中不要再次发射自己接收的信号,否则可能无限循环。
- 多个槽执行顺序不定:一个信号关联多个槽时,槽执行顺序随机,无法指定。
- 宏不能作为参数:signal/slot 参数中不能使用宏。
- 构造函数位置限制:不能在 signals 或 slots 区域声明构造函数。
- 函数指针限制:信号与槽不能以函数指针作为参数。
- 不支持缺省参数:signal 和 slot 不允许有默认参数。
- 不支持模板参数:信号和槽不能携带模板类参数。
13、信号的注意点
一句话总结:信号声明简单但有严格规则,参数、继承和线程行为都要注意。
- 声明规则:信号都是
public
,不能在signals
前加修饰符。 - 返回值:信号没有返回值,统一用
void
。 - 定义:信号只声明,不需要自己定义。
- 继承要求:类必须直接或间接继承自
QObject
,并包含Q_OBJECT
宏。 - 线程行为:
- 同线程:
emit
发出信号后,会立即执行槽函数,等所有槽执行完才继续执行后续代码;多个槽按连接顺序执行。 - 跨线程:槽函数执行顺序随机,
emit
后代码立即执行,不等待槽完成。
- 同线程:
- 连接方式:可通过
connect
第五个参数设置连接类型,如异步执行槽函数而不阻塞。 - 参数一致性:信号参数类型必须和槽函数匹配;槽函数可以少于信号参数,但不能多于信号参数。槽函数可以忽略信号多余的数据,但不能依赖未传入的数据。
14、Qt 实现多线程
一句话总结:Qt 提供高层和低层两种多线程方案,根据任务类型选择使用。
- QtConcurrent
- 高级 API,基于线程池实现。
- 适合 CPU 密集型任务或短时任务。
- 不适合大量阻塞操作,否则线程池容易被耗尽,任务排队等待。
- QThread
- 低级 API,每个
QThread
对应一个独立线程。 - 适合长时间或阻塞操作。
- 可以灵活控制线程生命周期和任务执行。
面试表述可说:“QtConcurrent 用线程池适合短任务,高并发阻塞任务用 QThread 更合适。”
- 低级 API,每个
15、描述QT中的文件流(QTextStream)和数据流(QDataStream)的区别
一句话总结:QTextStream用于文本数据,QDataStream 用于二进制数据。
- QTextStream(文件流)
- 面向文本,操作轻量级数据类型:
int
、double
、QString
等。 - 写入文件后以可读文本形式存储。
- 可操作磁盘文件或内存数据。
- 面向文本,操作轻量级数据类型:
- QDataStream(数据流)
- 面向二进制,支持多种数据类型,包括对象。
- 写入文件或内存后以二进制形式存储。
- 可以将对象打包到内存中,实现数据传输或保存。
面试可说:“文本用 QTextStream
,二进制用 QDataStream
,两者都能操作文件和内存。”
16、Qt 保证多线程安全的方法
**一句话总结**:Qt 提供丰富的线程同步机制,包括互斥锁、条件变量、读写锁和信号量,确保多线程程序安全访问共享资源。
-
QMutex(互斥量)
- 用于保护共享资源,保证同一时间只有一个线程访问。
- 示例:
QMutex mtx; mtx.lock(); // 访问共享资源 mtx.unlock();
-
QMutexLocker(互斥锁封装类)
- RAII 封装,进入作用域自动加锁,离开作用域自动解锁。
- 防止忘记 unlock 或异常导致死锁。
- 示例:
QMutexLocker locker(&mtx); // 自动加锁,作用域结束自动解锁
-
QWaitCondition(等待条件)
- 结合
QMutex
使用,让线程等待某个条件发生。 - 支持
wait()
、wakeOne()
、wakeAll()
,常用于生产者-消费者模型。 - 示例:
QWaitCondition cond; QMutex mtx; cond.wait(&mtx); // 等待条件 cond.wakeAll(); // 唤醒所有等待线程
- 结合
-
QReadWriteLock(读写锁)
- 支持多个线程同时读,但写操作互斥。
- 适合读多写少的共享资源,提高并发性能。
- 示例:
QReadWriteLock lock; lock.lockForRead(); // 读锁 lock.lockForWrite(); // 写锁 lock.unlock();
-
QSemaphore(信号量)
- 控制访问某资源的线程数量(如资源池管理)。
- 示例:
QSemaphore sem(2); // 最多允许两个线程访问 sem.acquire(); // 访问资源 sem.release();
-
QReadLocker / QWriteLocker(读写锁便利类)
- 自动管理加解锁,减少出错。
- 示例:
QReadLocker rlocker(&lock); // 读锁 QWriteLocker wlocker(&lock); // 写锁
面试口语化总结:
“Qt 提供 QMutex 保证互斥访问,QReadWriteLock 多读共享少写互斥,QWaitCondition 用于线程等待唤醒,QSemaphore 控制资源并发数量。使用 Locker 类可以自动管理锁,避免忘记解锁导致死锁。”
17、详解Qt中的内存管理机制
**一句话总结**:Qt通过对象树(父子关系)和智能指针管理内存,减少手动 delete,提高安全性。
- 对象树管理
- 所有继承自
QObject
的对象可以指定父对象。 - 父对象析构时,会自动删除所有子对象,形成层级式清理。
- 顶层对象(如通过
setCentralWidget()
设置的控件)通常由QApplication
管理,无需手动 delete。
- 所有继承自
- delete 与 deleteLater()
- 不建议直接
delete
一个 QObject,可能导致事件队列中的对象被提前销毁,引发崩溃。 - 使用
deleteLater()
可以安全删除对象,Qt 会等事件处理完再删除。 - 多次调用
deleteLater()
也不会出问题。
- 不建议直接
- 外部指针管理
- 不建议在对象树外持有 QObject 指针,否则可能在对象被删除后成为悬空指针。
- 如果必须持有,可连接
destroyed()
信号,或使用 Qt 提供的智能指针QPointer
,会在对象被销毁时自动置空。
- 智能指针 QPointer
- 专门用于 QObject 子类,类似普通指针,安全监控对象生命周期。
- 可避免悬空指针和重复删除的问题。
- 注意点
- Qt 内存管理机制有时会被工具误报为内存泄露,但实际上对象会在父对象析构或 deleteLater() 时被释放。
面试口语总结:
“Qt 用对象树管理内存,父对象会自动删除子对象,顶层对象由 QApplication 管理。deleteLater() 保证事件处理完再删除对象,避免崩溃。外部指针用 QPointer 或 destroyed() 信号监控,保证多线程安全。”
【精华】详解Qt中的内存管理机制_qt内存管理机制_撬动未来的支点的博客-CSDN博客
QT中的内存管理
参考自:
【精华】详解Qt中的内存管理机制_qt内存管理机制_撬动未来的支点的博客-CSDN博客
QT中使用对象父子关系进行内存管理
使用对象父子关系进行内存管理的原理,简述为:
在创建类的对象时,为对象指定父对象指针。当父对象在某一时刻被销毁释放时,父对象会先遍历其所有的子对象,并逐个将子对象销毁释放。(对象树)
使用引用计数对内存进行管理
引用计数
引用计数可以说是软件开发人员必知必会的知识点,它在内存管理领域的地位是数一数二的。
引用计数需要从三个方面来全面理解:
使用场景:一个资源,多处使用(使用即引用)。
问题:到底谁来释放资源。
原理:使用一个整形变量来统计,此资源在多少个地方被使用,此变量称为引用计数。当某处使用完资源以后,将引用计数减1。当引用计数为0时,即没有任何地方再使用此资源时,真正释放此资源。这里的资源,在动态内存管理中就是指堆内存。
用一句话描述就是:谁最后使用资源,谁负责释放资源。
显式共享
显式共享,是仅仅使用引用计数控制资源的生命周期的一种共享管理机制。这种机制下,无论资源在何处被引用,自始至终所有引用指向资源都是同一个。
之所以叫显式共享,是因为这种共享方式很直接,没有隐含的操作,如:Copy on Write写时拷贝(见隐式共享的相关说明)。如果想要拷贝并建立新的引用计数,必须手动调用detach()函数。
隐式共享
隐式共享,也是一种基于引用计数的控制资源的生命周期的共享管理机制。
隐式共享,对不同的操作有不同的处理:
读取时,在所有引用的地方使用同一个资源;
在写入、修改时自动复制一份资源出来做修改,自动脱离原始的引用计数,因为是新的资源,所以要建立新的引用计数。这种操作叫Copy on Write写时复制技术,是自动隐含进行的。
从使用者的角度看,每个使用者都像是拥有独立的一份资源。在一个地方修改,修改的只是原始资源的拷贝,不会影响原始资源的内容,自然就不会影响到其他使用者。所以这种共享方式称为隐式共享。
相关Qt类有QString、QByteArray、QImage、QList、QMap、QHash等。
智能指针
智能指针是对C/C++指针的扩展,同样基于引用计数。
智能指针和显示共享和隐式共享有何区别?它们区别是:智能指针是轻量级的引用计数,它将显式共享、隐式共享中的引用计数实现部分单独提取了出来,制作成模板类,形成了多种特性各异的指针。
例如,QString除了实现引用计数,还实现了字符串相关的丰富的操作接口。QList也实现了引用计数,还实现了列表这种数据结构的各种操作。可以说,显式共享和隐式共享一般是封装在功能类中的,不需要开发者来管理。
智能指针将引用计数功能剥离出来,为Qt开发者提供了便捷的引用计数基础设施。
强(智能)指针
Qt中的强指针实现类是:QSharedPointer,此类是模板类,可以指向多种类型的数据,主要用来管理堆内存。关于QSharedPointer在Qt Assistant中有详细描述。
它的原理和显式共享一样:最后使用的地方负责释放删除资源,如类对象、内存块。
强指针中的“强”,是指每多一个使用者,引用计数都会老老实实地**+1**。而弱指针就不同,下面就接着讲解弱指针。
弱(智能)指针
Qt中的弱指针实现类是QWeakPointer,此类亦为模板类,可以指向多种类型的数据,同样主要用来管理堆内存。关于QWeakPointer在Qt Assistant中有详细描述。
弱指针只能从强指针QSharedPointer转化而来,获取弱指针,不增加引用计数,它只是一个强指针的观察者,观察而不干预。只要强指针存在,弱指针也可以转换成强指针。可见弱指针和强指针是一对形影不离的组合,通常结合起来使用。
局部指针
局部指针,是一种超出作用域自动删除、释放堆内存、对象的工具。它结合了栈内存管理和堆内存管理的优点。
Qt中的实现类有:QScopedPointer,QScopedArrayPointer,具体可以参考Qt Assistant。
观察者指针
上面说弱指针的时候,讲到过观察者。观察者是指仅仅做查询作用的指针,不会影响到引用计数。
Qt中的观察者指针是QPointer,它必须指向QObject的子类对象,才能对对象生命周期进行观察。因为只有QObject子类才会在析构的时候通知QPointer已失效。
QPointer是防止悬挂指针(即野指针)的有效手段,因为所指对象一旦被删除,QPointer会自动置空,在使用时,判断指针是否为空即可,不为空说明对象可以使用,不会产生内存访问错误的问题。
总结
本篇文章讲解了Qt中的各种内存管理机制,算是做了一个比较全面的描述。
之所以说是必读,是因为笔者在工作中发现,内存管理确实非常重要。Qt内存管理机制是贯穿整个Qt中所有类的核心线索之一,搞懂了内存管理
能在脑海中形成内存中对象的布局图,写代码的时候才能下笔如有神,管理起项目中众多的对象才能游刃有余,提高开发效率;
能够减少bug的产生。有经验的开发者应该知道,内存问题很难调试定位到具体的位置,往往导致奇怪的bug出现。
能够帮助理解Qt众多类的底层不变的逻辑,学起来更容易。
QT其他未整理题目
【精华】详解Qt中的内存管理机制_qt内存管理机制_撬动未来的支点的博客-CSDN博客
QT 面试题汇总_qt面试题_BC菜鸟的博客-CSDN博客
字节跳动C++/Qt PC客户端面试题精选 - 知乎 (zhihu.com)
C++ Qt常用面试题整理(不定时更新)_qt 面试题_倚栏|听风的博客-CSDN博客
C++/QT PC客户端面试题 | Skykey’s Home (skykeyjoker.github.io)
4. 互斥锁、可重入锁、读写锁与自旋锁
mutex 互斥量
mutex是睡眠等待类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠。优点就是节省CPU资源,缺点就是休眠唤醒会消耗一点时间。
依据同一线程是否能多次加锁,把互斥量又分为如下两类:
- 是:递归互斥量recursive mutex,也称可重入锁,reentrant lock
- 否:非递归互斥量non-recursive mutex,也称不可重入锁,non-reentrant mutex
read-write lock 读写锁
又称“共享-独占锁”,对于临界区区分读和写,读共享,写独占。
读写锁的特性:
- 当读写锁被加了写锁时,其他线程对该锁加读锁或者写锁都会阻塞。
- 当读写锁被加了读锁时,其他线程对该锁加写锁会阻塞,加读锁会成功。
适用于多读少写的场景。
spinlock 自旋锁
自旋,更通俗的一个词时“忙等待”(busy waiting)。最通俗的一个理解,其实就是死循环。
自旋锁不会引起线程休眠。当共享资源的状态不满足时,自旋锁会不停地循环检测状态(循环检测状态利用了CPU提供的原语Compare&Exchange来保证原子性)。因为不会陷入休眠,而是忙等待的方式也就不需要条件变量。不休眠就不会引起上下文切换,但是会比较浪费CPU。
题目
- 讲一下可重入锁?
- 讲一下自旋锁?自旋锁循环检测状态的时候如何保证原子性?
5. C++类对象的内存分布
C++类初始化为一个对象后,该对象实例在内存中的分布情况:
空类
实例化一个空类,会在内存中占用1个字节,表示为类实例。
只含基本数据,不含函数
#include<iostream>using namespace std;class A{char a;int b;char c;};class B{char a;char c;int b;};int main(){ cout<<sizeof(A)<<endl; cout<<sizeof(B)<<endl; }
答案分别是:
12
8
原因在于C++类成员变量的内存分布式从上到下,按照内存对齐原则进行分布的。
内存对齐原则:
- 分配内存的顺序是按照声明的顺序。
- 每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍位置。
- 最后整个类的大小必须是变量类型最大值的整数倍。
为什么要进行内存对齐:
- 平台原因(移植原因):某些硬件平台只能在某些地址处取某些特定类型的数据,不能访问任意地址。
- 性能原因:访问未对其的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
带成员函数的类
函数不占实例内存,一个类的函数时公共的,一个类的函数只有一份。
类的成员函数存放与具体编译器有关,有的放在只读区,有的存放在代码区。
带虚函数的类
class E{virtual int func1(){cout<<"虚函数"<<endl;}char a;int b;char c;int func(){cout<<"成员函数"<<endl;}};
24
虚函数表指针占用了前8位。
参考链接:C++类对象的内存分布
题目:
- 计算一下某个类对象的内存占用?
6. C++内存配分相关
C++程序运行时进程的内存分布情况
内存分为5部分,从高地址到低地址为:
- 栈:空间向下
- 堆:空间向上
- 未初始化的数据段(bss):该段数据在程序开始之前由操作系统内核初始化为0,包含所有初始化为0和没有显式初始化的全局变量和静态变量
- 初始化的数据段(data):初始化的全局变量和静态变量
- 代码段(text):存放程序的二进制代码
C++变量的内存分布
C的储存区分为:
- 栈:编译器自动分配释放
- 堆:程序员分配释放
- 全局区(静态区):全局变量与静态变量存放在一起,初始化与未初始化的全局变量和静态变量分别存放在两块相邻的区域。-程序结束释放
- 常量区:程序结束释放
C++的储存区分为:
- 栈:由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变脸、函数参数等。
- 堆:new分配的内存块,他们的释放由程序员负责。若程序员没有释放掉,程序结束后操作系统会自动回收。
- 自由存储区:malloc分配的内存块,他和堆是十分相似的,区别是用free来结束自己的声明。
- 全局**/**静态存储区:全局变量和静态变量被分配到同一块内存中,在C语言中,全局变量和静态变量分为初始化的和未初始化的,在C++中无区分,共同占用同一块内存区
- 常量存储区:里面存放常量
判断规则:
- 函数体中定义的变量通常是在栈上
- 用malloc,new等分配内存的函数分配得到的在堆上
- 全局变量存在全局区
- 所有静态变量存在全局区
- "abcd"字符串常量存放在常量区
char s[] = "hello"
,s为全局变量,存放在数据段(简称“数据段”)的读写区域;
char *ss = "world"
,ss为全局变量,存放在数据段的只读部分
题目
- 在堆上分配内存快还是栈上分配内存更快?
在栈上分配释放内存更快。栈是程序启动时,系统分配好了的。堆是用的时候向系统申请,用了还回去,申请和交还的过程开销就比较大了。 - C++变量的内存分布?
- C++程序运行时进程的内存分布情况?
7. Qt的D指针(d_ptr
)与Q指针(q_ptr
)
D指针
PIMPL模式,指向一个包含所有数据的私有数据结构体。
- 私有的结构体可以随意改变,而不需要重新编译整个工程项目
- 隐藏实现细节
- 头文件中没有任何实现细节,可以作为API使用
- 原本在头文件的实现部分转移到乐源文件,所以编译速度有所提高
Q指针
私有的结构体中储存一个指向公有类的Q指针。
总结
- Qt中的一个类常用一个PrivateXXX类来处理内部逻辑,使得内部逻辑与外部接口分开,这个PrivateXXX对象通过D指针来访问;在PrivateXXX中有需要引用Owner的内容,通过Q指针来访问。
- 由于D和Q指针是从基类继承下来的,子类中由于继承导致类型发生变化,需要通过
static_cast
类型转化,所以DPTR()
与QPTR()
宏定义实现了转换。
题目
- 讲一下Qt的D指针和Q指针?
8. Qt信号槽(反射机制)相关
Qt信号槽的调用流程
- MOC查找头文件中的signal与slots,标记出信号槽。将信号槽信息储存到类静态变量staticMetaObject中,并按照声明的顺序进行存放,建立索引。
- connect链接,将信号槽的索引信息放到一个双向链表中,彼此配对。
- emit被调用,调用信号函数,且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数。
- active函数在双向链表中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数。
信号槽的实现:元对象编译器MOC
元对象编译器MOC负责解析signals、slot、emit等标准C++不存在的关键字,以及处理Q_OBJECT、Q_PROPERTY、Q_INVOKABLE等相关的宏,生成moc_xxx.cpp的C++文件(使用黑魔法来变现语法糖)。比如信号函数只要声明、不需要自己写实现,就是在这个moc_xxx.cpp文件中自动生成的。
moc****的本质就是反射器。
Qt信号槽的链接方式(connect的第五个参数)
Qt::AutoConnection
: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。Qt::DirectConnection
:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。Qt::QueuedConnection
:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。Qt::BlockingQueuedConnection
:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。Qt::UniqueConnection
:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
题目
- Qt connect的第五个参数(信号槽链接方式)?
- Qt信号槽的调用流程?
9. Qt智能指针相关
Qt的智能指针包括:
- QSharedPointer
- QScopedPointer
- QScopedArrayPointer
- QWeakPointer
- QPointer
- QSharedDataPointer
QSharedPointer
相当于std::shared_ptr
,内部维持着对拥有的内存资源的引用计数,引用计数下降到0时,这个内存资源就被释放了。
QSharedPointer是线程安全的,多个线程同时修改QSharedPointer对象也不需要加锁,但是QSharedPointer指向的内存区域不一定是线程安全的,所以多个线程同时修改QSharedPointer指向的数据时还要考虑加锁。
QWeakPointer
类似于std::weak_ptr
。
QScopedPointer
相当于std::unique_ptr
,内存数据只在一处被使用。
QScopedArrayPointer
类似于QScopedPointer,用于指向的内存数据是一个数组时的场景。
QPointer
QPointer只能用于指向QObject及派生类的对象。当一个QObject或派生类对象被删除后,QPointer能自动将其内部的指针设置为0,这样在使用QPointer之前就可以判断一下是否有效乐。
QPointer****对象超出作用域时,并不会删除它指向的内存对象。
QSharedPointer
用于实现数据的隐式共享。Qt中大量使用了隐式共享与写时拷贝技术,例如:
QString str1="abc";QString str2=str1;str2[2]="X";
第二行执行完后,str2和str1指向同一片内存数据。第三句执行时,Qt会为str2的内部数据重新分配内存。这样做的好处是可以有效地减少大片数据拷贝的次数,提高程序的运行效率。
Qt中隐式共享和写时拷贝就是利用QSharedDataPointer和QSharedData这两个类实现的。
题目
- 讲一下Qt的智能指针?
- 了解Qt的QSharedPointer吗?
- 了解Qt的QPointer吗?