目录
Qt面试
什么是信号(Signal)和槽(Slot)?
什么是Meta-Object系统?
什么是Qt的MVC模式?
1. QT中connect函数的第五个参数是什么?有什么作用?
3. 在QT中,如何使用QSqlDatabase类的transaction()函数来开始一个事务,如何使用commit()函数来提交事务,如何使用rollback()函数来回滚事务
4. QT数据库中的事务 transaction的作用是什么?举例说明
5.QT中可以用子线程操作UI界面吗
6. QT中的事件处理机制的什么?
7.QT setmousetracking的作用
8.QObject类是如何实现了信号与槽机制的?
9.QT的信号与槽的底层实现是什么?原理是什么?
10.Qt 的优点、缺点
11.Qt 的核心机制
12.信号与槽机制原理
13.Qt信号槽机制的优势和不足
14.信号与槽与函数指针的比较
15.Qt 的事件过滤器
16. 为什么 new QWidget 不需要 delete
23.描述QT中的文件流(QTextStream)和数据流(QDataStream)的区别
26.Qt的内存管理
27.描述QT的TCP UDP通讯流程
28.描述UDP 之 UdpSocket通讯UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。在网络质量令人十分不满意的环境下,UDP协议数据包丢失严重。由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。
Qt中的最佳实践和设计原则。
Qt线程同步的方法
moc预编译器(Meta-Object Compiler):
Qt中如何进行多线程编程
模型/视图/代理(Model/View/Delegate)架构
QGraphicsView框架
C++面试
虚函数 虚表 虚指针!
这个是析构函数
这个是构造函数
高级特性
设计模式
实际问题解决
Qt面试
什么是信号(Signal)和槽(Slot)?
答:信号是一个事件,例如按钮单击或文本改变,而槽是响应该事件的函数。信号和槽是Qt中实现事件驱动编程的关键机制。
什么是Meta-Object系统?
答:Meta-Object系统是一个运行时的反射机制,使得Qt能够支持信号和槽、动态属性等特性。它还允许开发人员使用QMetaObject类来查询QObject派生类的元数据。
什么是Qt的MVC模式?
答:MVC(Model-View-Controller)是一种设计模式,用于分离应用程序的数据(Model)、用户界面(View)和控制逻辑(Controller)。在Qt中,可以使用QAbstractItemModel和QAbstractItemView类来实现MVC模式。
1. QT中connect函数的第五个参数是什么?有什么作用?
在 Qt 中使用 connect 连接信号与槽时,通常有以下几个参数:
发送者对象:即发出信号的对象
信号:要连接的信号,例如 clicked()
接收者对象:即接收信号的对象
槽函数:要连接的槽函数,即信号发出时要执行的函数
连接类型:可以是 Qt::ConnectionType 的枚举值,如 Qt::AutoConnection、Qt::DirectConnection、Qt::QueuedConnection 等,默认为 Qt::AutoConnection
connect(sender, SIGNAL(clicked()), receiver, SLOT(handleButtonClicked()));
1
在Qt中,connect函数的第五个参数是connect_mode,它是一个布尔值,用于指定连接模式。
connect_mode参数的作用是决定信号和槽的连接方式。
当connect_mode为false时,信号和槽使用常规的连接方式,即当信号发出时,相应的槽函数将被调用。
当connect_mode为true时,信号和槽使用queued connection模式。在这种模式下,信号将不会立即传递给槽,而是被缓存起来,等待线程调度器将槽函数调度执行。这种模式适用于多线程环境,可以避免不必要的线程间同步和竞争条件。
总结起来,connect函数的第五个参数connect_mode用于指定信号和槽的连接模式,决定信号是否立即传递给槽,以及在多线程环境下的行为。
3. 在QT中,如何使用QSqlDatabase类的transaction()函数来开始一个事务,如何使用commit()函数来提交事务,如何使用rollback()函数来回滚事务
#include <QSqlDatabase>
#include <QSqlQuery> QSqlDatabase db = QSqlDatabase::database(); // 获取数据库连接 if (db.transaction()) { // 开始事务 QSqlQuery query(db); // 在这里执行数据库操作,例如插入、更新或删除数据 // 如果所有操作都成功执行,则继续提交事务 if (query.exec("INSERT INTO table_name (column1, column2) VALUES (value1, value2)")) { // 插入操作成功执行 if (query.exec("UPDATE table_name SET column1 = value1 WHERE column2 = value2")) { // 更新操作成功执行 // 提交事务 if (db.commit()) { // 事务提交成功,所有操作永久保存 qDebug() << "Transaction committed successfully"; } else { // 事务提交失败,回滚所有操作 qDebug() << "Failed to commit transaction"; db.rollback(); // 回滚事务 } } else { // 更新操作失败,回滚所有操作 qDebug() << "Failed to execute update query"; db.rollback(); // 回滚事务 } } else { // 插入操作失败,回滚所有操作 qDebug() << "Failed to execute insert query"; db.rollback(); // 回滚事务 }
} else { // 开始事务失败 qDebug() << "Failed to start transaction";
}
在上面的例子中,首先通过QSqlDatabase::database()获取数据库连接。
然后使用transaction()函数开始一个事务。
在事务中,可以执行多个数据库操作,例如插入、更新或删除数据。
如果所有操作都成功执行,则使用commit()函数提交事务,否则使用rollback()函数回滚事务。
在提交或回滚事务之前,可以使用QSqlQuery对象执行具体的数据库操作。
4. QT数据库中的事务 transaction的作用是什么?举例说明
QT数据库中的事务(Transaction)是一组操作的逻辑单元,它要么全部成功地执行,要么全部失败回滚。事务的主要作用是确保数据库的一致性和完整性。
在数据库和软件开发中,事务是不可分割的最小工作单位。它对数据库中一组相关的操作进行处理,要么全部执行成功并永久保存到数据库中,要么如果有任何操作失败,则撤消所有已执行的操作。这样可以保证数据库中的数据始终保持一致性和完整性。
例如,在银行转账操作中,事务可以起到重要作用。假设一个用户要将自己的账户中的资金转移到另一个用户的账户中,这个过程可以通过一个事务来实现。事务将包括两个操作:从一个账户中减去转账金额,然后在另一个账户中增加相同的金额。如果这两个操作都成功执行,则事务可以提交,转账操作永久保存。但是,如果其中一个操作失败,例如因为账户余额不足,则事务将回滚,所有已执行的操作都将被撤销,转账操作不会生效。
在QT中,可以使用QSqlDatabase类的transaction()函数来开始一个事务,然后使用commit()函数来提交事务,或者使用rollback()函数来回滚事务。这样可以确保对数据库的修改要么全部成功,要么全部失败回滚,保持数据库的一致性和完整性。
总之,QT数据库中的事务是一种非常重要的机制,它可以确保数据库的一致性和完整性,保证一组相关的操作要么全部成功执行,要么全部失败回滚。
5.QT中可以用子线程操作UI界面吗
在QT中,子线程无法直接操作UI界面,否则会出现线程冲突等错误。但可以通过以下两种方法实现子线程与UI线程的交互:
使用信号和槽机制:子线程可以发出信号,UI线程中的槽函数可以接收并处理这些信号,从而实现对UI的更新。这是一种安全且常见的方法,可以避免线程冲突。
使用InvokeMethod方法:InvokeMethod可以在指定的QObject对象上调用指定的方法,同时还可以指定调用方法的参数。使用该方法可以在子线程中调用UI线程中的方法,实现对UI的更新。但需要注意的是,使用该方法时需要确保调用的方法不会阻塞子线程,否则会影响程序的性能。
总之,虽然QT的子线程无法直接操作UI界面,但可以通过信号和槽机制或InvokeMethod方法实现子线程与UI线程的交互,完成UI的更新。
6. QT中的事件处理机制的什么?
QT中的事件处理机制是一种基于事件循环的机制,它通过不断地从事件队列中获取并处理事件来更新应用程序的状态。
事件可以来自不同的源,包括用户交互、系统事件、定时器事件等。当这些事件发生时,它们被添加到事件队列中,等待被处理。
QT的事件处理机制主要通过以下几个步骤实现:
事件循环的启动:应用程序启动后,通过调用QCoreApplication::exec()函数进入事件循环。这个函数会一直运行,直到QCoreApplication::exit()函数被调用。
事件的获取和处理:在事件循环中,QT会不断地从事件队列中获取事件,并根据事件的类型调用相应的处理函数。这些处理函数通常是在QT的组件类中定义的,例如QWidget、QPushButton等。
事件的传播:当一个事件发生时,它首先被发送到最顶层的组件,然后按照组件的层次结构向下传播,直到找到处理该事件的组件。如果一个组件不处理该事件,它会将其传递给父组件或其他组件处理。
事件的处理:当一个组件接收到事件时,它会根据事件的类型调用相应的处理函数。这些处理函数可以是虚函数,例如在QWidget类中的mousePressEvent、mouseMoveEvent等,也可以是自定义的函数。如果事件被成功处理,处理函数会返回true;否则返回false。
事件的结束:当一个事件被处理后,QT会将其从事件队列中移除,并继续处理下一个事件。
需要注意的是,QT的事件处理机制是异步的,这意味着当一个事件正在被处理时,其他事件仍然可以被添加到事件队列中并等待被处理。
这种机制使得QT的应用程序能够响应用户的交互和系统的变化,并保持高效的运行。
7.QT setmousetracking的作用
在QT中,setMouseTracking函数用于设置组件是否跟踪鼠标移动事件。如果一个组件启用了鼠标移动跟踪,它会在鼠标移动时接收mouseMoveEvent事件,并能够响应这些事件。默认情况下,QT中的组件都没有启用鼠标移动跟踪,这意味着它们只有在鼠标按钮按下或释放时才会接收鼠标事件。如果你想在鼠标移动时接收鼠标事件,可以使用setMouseTracking(True)方法来启用鼠标移动跟踪。
8.QObject类是如何实现了信号与槽机制的?
QObject类实现信号与槽机制的方式主要依赖于Qt的元对象系统。元对象系统包括三个重要的组成部分:QObject类,它是所有使用元对象系统的基类;Q_OBJECT宏,它使得类可以使用元对象特性;以及moc(meta-object-compiler,元对象编译器),它预处理包含Q_OBJECT宏的类。
首先,QObject类中的connect函数用于建立信号与槽的连接。这个函数接收四个参数:发送对象、信号、接收对象和槽。当信号被发出时,会调用与之连接的槽函数。
在编译过程中,Qt的moc工具会预处理包含Q_OBJECT宏的类。它会展开Q_OBJECT宏,并自动生成一些变量定义、函数声明等代码。这些代码包括将信号与槽作为可回调的函数,并根据id调用这些函数。这些函数及其id的定义在Q_OBJECT展开的函数qt_static_metacall中。
信号的实现由moc完成,它会生成相应的代码来发出信号。而槽函数的实现则需要程序员自己完成。当信号被发出时,会根据连接的槽函数的id调用相应的槽函数。
总的来说,QObject类通过利用元对象系统中的信息,实现了信号与槽机制的建立、发出和调用的整个过程。
9.QT的信号与槽的底层实现是什么?原理是什么?
QT的信号与槽的底层实现是通过函数间的相互调用实现的。每个信号都可以用函数来表示,称为信号函数;每个槽也可以用函数表示,称为槽函数。信号和槽机制实现的功能其实就是信号函数调用槽函数的效果。
在QT中,每个对象都有一个相应的记录该对象的元对象,元对象类为QMetaObject,记录元对象数据信号与槽的是QMetaData类。QObject类实现了信号与槽机制,它利用元对象记录的信息,实现了信号与槽机制。
信号与槽建立连接的实现是通过QObject类的connect()函数,它的参数包括发送对象、信号、接收对象和槽。连接内部的实现接口是connectInternal()。当信号发生时,会激活操作函数接口QObject::active_signal()。
在实际开发中,可以使用QT提供的信号函数和槽函数,也可以根据需要自定义信号函数和槽函数。QT Creator 提供了很强大的QT GUI开发手册,可以很容易地查到某个控件类中包含哪些信号函数和槽函数。
10.Qt 的优点、缺点
优点:
跨平台,几乎支持所有平台
接口简单,文档详细
开发效率高
缺点:
Qt 作为一个软件平台,比较庞大、臃肿。
11.Qt 的核心机制
元对象系统:
Qt的元对象系统(meta-object)提供了用于内部对象通讯的信号与槽(signals & slots)机制,运行时类型信息,以及动态属性系统(dynamic property system)。 整个元对象系统基于三个东西建立:
QObject类为所有对象提供了一个基类,只要继承此类,那创建出的对象便可以使用元对象系统。
在声明类时,将Q_OBJECT宏放置于类的私有区域就可以在类中使能元对象特性,诸如动态属性,信号,以及槽。一般实际使用中,我们总是把Q_OBJECT宏放置在类声明时的开头位置,除此之外我们的类还需要继承QObject类。
元对象编译器(Meta-Object Compiler,缩写moc),为每个QObject的子类提供必要的代码去实现元对象特性。我们可以认为Qt对C++进行了一些拓展,moc则是负责将这些拓展语法翻译成原生的C++语法,之后交给C++编译器去编译。
12.信号与槽机制原理
信号与槽的具体流程。
moc查找头文件中的signals,slots,标记出信号和槽。
将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
通过active函数找到在map中找到所有与信号对应的槽索引
根据槽索引找到槽函数,执行槽函数
槽:槽函数其实就是普通的C++函数,可以是虚函数、static函数,可以被重载,可以被访问修饰符修饰,也可以被其他C++成员函数调用等,它唯一的特定就是可以和信号连接。当与它连接的信号被emit时候,槽函数会被调用,槽必须直接或者间接继承QObject类型,然后声明Q_OBJECT宏。而且还可以使用Lambda表达式作为槽函数。
13.Qt信号槽机制的优势和不足
优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。
优点:
类型安全:需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,则编译器会报错。
松散耦合:信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的那个槽接收它发出的信号,它只需要在适当的时间发送适当的信号即可,而不需要关心是否被接收和哪个对象接收了。Qt保证适当的槽得到了调用,即使关联的对象在运行时删除,程序也不会崩溃。
灵活性:一个信号可以关联多个槽,多个信号也可以关联同一个槽。
缺点:
速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍左右。
原因:
需要定位接收信号的对象。
安全地遍历所有槽。
编组,解组传递参数。
多线程的时候,信号需要排队等候。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损失,对于实时应用程序是可以忽略的。)
14.信号与槽与函数指针的比较
回调函数使用函数指针来实现的,如果多个类都关注一个类的动态变化,这样就会需要写出一个比较长的列表来管理这些类之间的关系。稍微在编码方面不那么灵活,稍显冗余。
QT使用信号与槽来解决这个连接问题,这种方式比较清晰简单一些,一个类只需要清楚自己有几个槽函数有几个信号,然后将信号与槽进行连接,QT会自己处理函数的调用关系。这样在软件设计角度更加的清晰,灵活,不容易出错。
Qt信号与槽机制降低了Qt对象的耦合度。
发信号的对象不需要知道有几个槽函数,也不需要关系是否收到信号,或者谁收到了,谁没收到。
同样的槽函数也不需要知道谁是信号的发出者。信号只需要在合适的时机发出即可,降低了对象之间的耦合度。
15.Qt 的事件过滤器
父窗口类通过重写eventFilter方法来监听子控件的相关事件进行处理。
使用这种方式的好处是不需要通过重写控件的方式获取某些事件,对于安装了事件过滤器的对象,他们所有的事件都会经过这个事件过滤器,所以就可以直接在父窗口中进行监测。
比如某个窗口中有个QLabel对象,我们想要监听他的鼠标点击事件,那我们就需要继承QLabel类,然后重写mousePressEvent事件,如果有其他类型的控件也需要获取某个事件,那是不是都需要继续控件并重写某个事件了,所以我们通过事件过滤器就可以很方便获取某个对象的某个事件。
专门的事件过滤器类,对特定的对象/特定的事件进行处理。
事件过滤器类只需对当前安装的对象进行处理,无需关心其他操作,且一个事件过滤器类可以被多个对象使用,例如Qt文档中的按键过滤示例,KeyPressEater类中的eventFilter过滤了所有的键盘按下事件,只要安装此事件过滤器的控件,都接收不到键盘按键按下的事件,这种就是对某个通用的事件进行过滤,可以进行多次复用。
给QApplication安装事件过滤器,达到全局事件监听的效果。
在notify方法下发事件的时候,QApplication对象可以拿到第一控制权,对某些事件优先进行处理,比如全局的快捷键操作。
注意点:
事件过滤器可以安装在任何继承QObject的对象上,也可以安装在QApplication对象上(全局事件过滤器);
事件过滤器(eventFilter方法)返回值为true,表示将当前事件进行过滤,不会发送到对象本身;如果返回false,表示对当前事件不做任何处理,会通过event()方法将事件分发给原来的对象。如果不知道怎么处理或者返回什么,那就返回父类的eventFilter方法(类似 return QObject::eventFilter(watched, event));
一个对象可以安装多个事件过滤器(也就是一个对象的事件可以被多个对象进行监控/处理/过滤), 并且最先安装的事件过滤器是最后被调用的,类似于栈的操作,先进后出;
一个事件过滤器可以被多个对象安装,但是如果在事件过滤器(eventFilter方法)中把该对象删除了, 一定要将返回值设为true。否则 Qt会将事件继续分发给这个对象,从而导致程序崩溃。
16. 为什么 new QWidget 不需要 delete
Qt提供了一种机制,能够自动、有效的组织和管理继承自QObject的Qt对象,这种机制就是对象树。
Qt库中的很多类都以QObject作为它们的基类。QObject的对象总是以树状结构组织自己。
当我们创建一个QObject对象时,可以指定其父对象(也被称为父控件),新创建的对象将被加入到父对象的子对象(也被称为子控件)列表中。
当父对象被析构时,这个列表中的所有子对象会被析构。
不但如此,当某个QObject对象被析构时,它会将自己从父对象的列表中删除,以避免父对象被析构时,再次析构自己。
23.描述QT中的文件流(QTextStream)和数据流(QDataStream)的区别
文件流(QTextStream):操作轻量级数据(int,double,QString)数据写入文本件中以后以文本的方式呈现。
数据流(QDataStream):通过数据流可以操作各种数据类型,包括对象,存储到文件中数据为二进制。
文件流,数据流都可以操作磁盘文件,也可以操作内存数据。通过流对象可以将对象打包到内存,进行数据的传输。
26.Qt的内存管理
答:1)QT使用对象父子关系进行内存管理。在创建对象时候,指定父对象指针,当父对象被销毁的时候,父对象会先编译子对象,并逐个销毁子对象,最后在销毁父对象。2)使用引用计数进行对象内存管理,智能指针,QSharedPointer,此类是模板类,可以指向很多数据类型,主要用来管理内存,类似于C++中的shared_ptr。还有QWeakPointer,此类也是模板类,次指针智能从QSharedPointer指针进行初始化,不增加引用计数,只是QSharedPointer指针的观察者,观察而不干预。3)局部指针,是一种超出作用域自动删除、释放堆内存、对象的工具,比如QScopedPointer, QScopedArrayPointer。4)QT中的观察者指针是QPointer,必须指向QObject类型的子对象,因为只有QObject子对象在析构的时候通知QPointer已失效。
27.描述QT的TCP UDP通讯流程
28.描述UDP 之 UdpSocket通讯
UDP(User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的,面向数据报的无连接协议。在网络质量令人十分不满意的环境下,UDP协议数据包丢失严重。由于UDP的特性:它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。所以QQ这种对保密要求并不太高的聊天程序就是使用的UDP协议。
在Qt中提供了QUdpSocket 类来进行UDP数据报(datagrams)的发送和接收。Socket简单地说,就是一个IP地址加一个port端口 。
流程:
①创建QUdpSocket套接字对象
②如果需要接收数据,必须绑定端口
③发送数据用writeDatagram,接收数据用 readDatagram 。
Qt中的最佳实践和设计原则。
1)采用MVC架构,将界面、数据、业务处理分离,以便于进行单独的管理和测试。2)尽量使用Qt提供的标准控件和布局管理器,这样可以减少自定义控件的工作量,同时保证应用程序的一致性。3)将程序分解成小模块,每个模块负责独立的功能,并且模块直接的接口要尽量简单清晰。4)采用良好的命名规范,命名要简单清晰明了释义。5)使用Qt的信号好槽机制,可以简化代码的编写,增加程序的可读性和可维护性。6)采用RAII原则,在构造函数中获取资源,在析构函数中释放资源,保证资源正确释放。7)使用Qt的工具和技术,如Qt Creator等。8)使用版本控制工具,比如Git SVN等,可以方便地管理和追踪代码的变化。9)进行良好的文档注释,方便代码开发和维护。
Qt线程同步的方法
答:1)QMutex类就像一把锁,在互斥量上锁(lock()),使用完之后释放锁(unlock()).
2)QMutextLocker便利类,可以使用其对互斥量进行简化处理,首先,QMutexLocker类接收一个QMutex作为参数并上锁,然后在析构函数中自动对其进行解锁。
3)QReadWriteLock锁,这个锁可以对线程的操作进行分类,读和写。lockforRead()/lockForWrite() unlock()。
4)QReadLocker和QWriteLocker便利类。
5)信号量Qsemaphore。
6)条件触发QWaitCondition。
moc预编译器(Meta-Object Compiler):
moc读取一个C++头文件。如果找到包含Q_OBJECT宏的一个或多个类声明,它会生成一个包含这些类的元对象代码的C++源文件,并且已moc_作为前缀。信号和槽机制、运行时类型信息和动态属性系统需要原对象代码。如下图所示,当定义Q_OBJECT宏时候意味着定义如下的内容。
Qt中如何进行多线程编程
答:1) 继承QThread类:可以创建一个类型,继承QThread类,实现其run方法,在run方法内部实现业务逻辑处理。然后创建该类型的对象,调用其start函数创建子线程并执行。2) 使用线程池QtConcurrent框架:QtConcurrent框架提供了一组更高级的类和函数,用于简化多线程编程,例如可以使用QFture和QFutureWatcher类来管理并发任务的执行和结果获取。线程池中的任务必须是QRunnable类型。
3)使用Qt的信号槽机制。4)随便写一个继承QObject类的类,然后创建一个对象,然后创建一个QThread线程对象,然后调用对象的->moveToThread(子线程指针)这样就这个对象丢到了子线程中去运行,然后再让线程start。5)QThreadPool全局的线程池实例对象。
模型/视图/代理(Model/View/Delegate)架构
在Qt中,模型/视图/代理(Model/View/Delegate)架构是一种设计模式,它提供了一种将数据存储与数据显示分离的方法。这种架构使得数据的表示和数据的处理可以独立开发,提高了代码的可重用性和可维护性。下面是每个部分的简要描述:
模型 (Model):
模型是应用程序的数据结构,它封装了底层数据并提供了访问这些数据的接口。
在Qt中,模型通常继承自QAbstractItemModel或其子类,如QStringListModel、QStandardItemModel等。
模型提供了标准接口,如data()用于获取数据,setData()用于设置数据,rowCount()和columnCount()分别用于获取行数和列数。
视图 (View):
视图负责显示模型中的数据,并允许用户与数据进行交互。
Qt提供了几种现成的视图类,比如QListView、QTableView和QTreeView,它们都继承自QAbstractItemView。
视图通过调用模型提供的接口来获取需要显示的数据,并且能够响应用户的输入(例如点击、选择等),并将这些操作转换为对模型的操作。
代理 (Delegate):
代理主要用于控制如何渲染单个项以及如何编辑这些项。
它充当了视图和模型之间的中介,负责绘制项目、处理键盘和鼠标事件,并且可以在编辑模式下提供一个编辑器。
默认情况下,每个视图都有一个默认的代理(如QStyledItemDelegate),但你可以创建自己的代理类来实现自定义的行为。
代理类通常会重写paint()方法来定制绘图逻辑,或者重写createEditor()、setEditorData()、setModelData()等方法来定制编辑逻辑。
工作流程
当视图需要显示数据时,它会调用模型的data()函数来获取所需的信息。
如果视图需要用户编辑某个项目,它会请求代理创建一个编辑器。
用户完成编辑后,代理会收集编辑结果并通过setModelData()将其传递回模型。
模型更新其内部数据,并可能发出信号通知视图数据已经改变,以便视图可以刷新显示。
优点
分离关注点:模型专注于数据管理,视图专注于展示,代理则处理特定的视觉呈现和编辑细节。
可复用性:相同的模型可以被不同的视图使用,同样的视图也可以展示不同的模型。
灵活性:通过自定义代理,可以很容易地改变单个项目的表现形式而不影响整体架构。
通过使用模型/视图/代理架构,开发者可以构建出更加模块化和易于扩展的应用程序。这种架构特别适合于列表、表格和树形结构的数据展示。
QGraphicsView框架
QGraphicsView 是 Qt 框架中用于显示 QGraphicsScene 内容的窗口部件。它提供了对二维图形和复杂场景的强大支持,适合用来构建复杂的用户界面、游戏、绘图应用等。
从场景坐标到视图坐标:
使用 QGraphicsView::mapFromScene(const QPointF &point) 将场景坐标转换为视图坐标。
例如:QPointF viewPoint = view->mapFromScene(scenePoint);
从视图坐标到场景坐标:
使用 QGraphicsView::mapToScene(const QPointF &point) 将视图坐标转换为场景坐标。
例如:QPointF scenePoint = view->mapToScene(viewPoint);
从项坐标到场景坐标:
使用 QGraphicsItem::mapToScene(const QPointF &point) 将项坐标转换为场景坐标。
例如:QPointF scenePoint = item->mapToScene(localPoint);
从场景坐标到项坐标:
使用 QGraphicsItem::mapFromScene(const QPointF &point) 将场景坐标转换为项坐标。
例如:QPointF localPoint = item->mapFromScene(scenePoint);
C++面试
虚函数 虚表 虚指针!
运行多态,通过虚函数和对象的指针和引用实现,通过虚函数和继承实现多态
虚函数
虚函数是在基类中声明为虚拟的成员函数,它在派生类中可以被重写,用于实现运行时多态性。
虚表和虚函数的关系以及虚表实现原理
虚表(Virtual Table)是一个指针数组,用于存储指向虚函数的指针。它是一种用于实现动态多态性的数据结构
虚表是在编译时静态生成的,与类的定义关联。对于每个包含虚函数的类,编译器会生成一个对应的虚表。
虚表中的每个元素(指针)对应于类中的一个虚函数,按照它们在类中声明的顺序排列。通过这些指针,可以在运行时动态地确定并调用正确的虚函数实现。
虚表的指针数组通常存储在类的元数据中。每个对象在其内存布局中通常会包含一个指向其所属类的虚表的指针(虚指针或vptr),它指向类的虚表。
纯虚函数的作用
1、实现接口定义:纯虚函数用于定义基类的接口,它指定了派生类需要实现的函数接口。基类通过声明纯虚函数,定义了一个规范或契约,要求派生类提供相应的实现。
2、强制派生类实现:通过声明纯虚函数,基类可以要求所有的派生类必须提供对应的实现。如果派生类没有实现纯虚函数,那么该派生类也会成为抽象类,无法实例化。
3、实现多态性:纯虚函数可以作为多态性的一部分。通过基类的指针或引用调用纯虚函数,可以根据实际的对象类型,调用相应的派生类实现,实现运行时多态性。
4、提供默认实现:纯虚函数也可以提供默认实现。在基类中,可以为纯虚函数提供一个默认的实现,派生类可以选择是否覆盖该默认实现。
析构函数和构造函数能否被设置为虚函数
这个是析构函数
- 可以:C++允许将析构函数声明为虚函数。
- 应该:当一个类被设计为基类,并且预期会有派生类时,通常应将其析构函数声明为虚函数,以保证通过基类指针删除派生类对象时能正确调用派生类的析构函数。
- 不需要:如果一个类永远不会作为基类被继承,或者总是直接实例化而不通过基类指针来管理,那么可以不必将析构函数声明为虚函数。虚函数机制会带来一定的运行时开销,因此如果没有必要,避免使用虚析构函数。
这个是构造函数
在C++中,构造函数不能被声明为虚函数。这是由C++语言的设计决定的,有几个原因导致了这一限制:
-
对象构造顺序:当创建一个派生类的对象时,首先调用基类的构造函数,然后是派生类的构造函数。如果允许构造函数为虚函数,那么在基类构造函数执行时,虚函数表(vtable)还未完全初始化,因为派生类的部分还未构造。这会导致运行时行为不明确。
-
内存布局未确定:在构造函数开始执行之前,对象的内存布局尚未完全确定。对于虚函数,需要通过虚函数表来调用正确的函数实现,而这个虚函数表是在对象完全构造后才可用的。
-
设计原则:构造函数的主要任务是初始化对象,而不是提供多态行为。虚函数机制主要用于运行时动态绑定方法,以便在运行时根据实际对象类型来选择正确的方法实现。构造函数不需要这种动态绑定,因为它总是在编译时就已经知道要构造的具体类型。
-
语义问题:如果构造函数可以是虚函数,那么在创建对象时就需要决定调用哪个构造函数。这意味着你需要一个已经存在的对象实例来确定其类型,而这与构造函数的目的相矛盾,因为构造函数是用来创建新对象的。
基础知识
-
什么是引用?引用和指针的区别是什么?
- 引用是一个别名,必须初始化且不能重新绑定。
- 指针可以被重新赋值,可以为空(
nullptr
)。
-
解释 const 关键字的使用。
const
可以用来声明常量变量、常量成员函数等,保证数据不会被修改。
-
什么是静态成员?如何初始化静态成员?
- 静态成员属于类本身而不是类的对象,需要在类外部进行初始化。
-
构造函数和析构函数的作用是什么?
- 构造函数用于初始化对象,析构函数用于清理资源。
-
什么是拷贝构造函数?什么时候会被调用?
- 拷贝构造函数用于创建一个新对象作为现有对象的副本。当对象以值传递的方式传入函数参数或从函数返回时会被调用。
-
浅拷贝和深拷贝的区别是什么?
- 浅拷贝只复制指针,不复制指针指向的内容;深拷贝则会复制指针指向的内容。
-
什么是 RAII(Resource Acquisition Is Initialization)?
- RAII 是一种管理资源的技术,通过将资源的获取与对象的生命周期绑定来确保资源的正确释放。
-
解释 C++ 中的多态性。
- 多态性允许通过基类指针或引用来调用派生类的方法。
-
虚函数和纯虚函数的区别是什么?
- 虚函数可以在基类中有实现,也可以在派生类中重写;纯虚函数在基类中没有实现,必须在派生类中实现。
-
什么是智能指针?列举几种常用的智能指针。
- 智能指针如
std::unique_ptr
、std::shared_ptr
和std::weak_ptr
,用于自动管理内存,防止内存泄漏。
- 智能指针如
高级特性
-
模板是什么?如何定义和使用模板?
- 模板是一种泛型编程技术,允许编写与类型无关的代码。
-
什么是 SFINAE (Substitution Failure Is Not An Error)?
- SFINAE 是编译器处理模板实例化错误的一种机制,使得某些特定的模板实例化失败不会导致整个程序编译失败。
-
解释 move 语义和右值引用。
- Move 语义允许转移资源所有权,而不需要进行深拷贝,提高性能;右值引用是实现 move 语义的关键。
-
什么是 lambda 表达式?如何使用它们?
- Lambda 表达式是一种匿名函数,可以捕获局部变量,并且可以直接在代码中定义和使用。
-
解释标准库中的容器(如 vector, list, map 等)及其适用场景。
- 不同的容器有不同的时间复杂度和空间效率,适用于不同的场景。
设计模式
-
解释单例模式(Singleton Pattern)。
- 单例模式确保一个类只有一个实例,并提供一个全局访问点。
-
工厂模式(Factory Pattern)有什么用途?
- 工厂模式提供了一种创建对象的接口,但由子类决定实例化哪一个类。
-
观察者模式(Observer Pattern)是如何工作的?
- 观察者模式定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。
实际问题解决
-
如何检测内存泄漏?有哪些工具可以使用?
- 使用 Valgrind、Visual Studio 的诊断工具、Sanitizers 等工具。
-
如何优化 C++ 代码的性能?
- 使用内联函数、减少动态内存分配、避免不必要的拷贝等。
-
解释异常安全(Exception Safety)的概念。
- 异常安全是指在发生异常时,程序能够保持一致性和完整性,通常通过 RAII 技术实现。
-
如何处理线程同步问题?
- 使用互斥锁(mutex)、条件变量(condition variable)、原子操作(atomic operations)等。
-
解释死锁(Deadlock)的原因及如何预防。
- 死锁是因为多个线程互相等待对方持有的资源而造成的。可以通过按顺序加锁、使用超时等方法预防。