qt面试经验

目录

1.qt底层原理

元对象系统

Qt的元对象系统(meta-object)提供了用于内部对象通讯的信号与槽(signals & slots)机制,运行时类型信息,以及动态属性系统(dynamic property system)。

整个元对象系统基于三个东西建立:

  • QObject 类是所有使用元对象系统的类的基类。 在一个类的 private 部分声明
  • Q_OBJECT宏,使得类可以使用元对象的特性,如动态属性、信号与槽。
  • MOC(元对象编译器)为每个 QObject的子类提供必要的代码来实现元对象系统的特性。

构建项目时,MOC 工具读取 C++ 源文件,当它发现类的定义里有Q_OBJECT 宏时,它就会为这个类生成另外一个包含有元对象支持代码的 C++ 源文件,这个生成的源文件连同类的实现文件一起被编译和连接。

2.connect的第五个参数

  • Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。

  • Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。

  • Qt::QueuedConnection:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。

  • Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。

  • Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。

3.信号槽的原理

本质是回调函数。信号或是传递值,或是传递动作变化;槽函数响应信号或是接收值,或者根据动作变化来做出对应操作。

Qt信号槽机制的优势和不足

优点:类型安全,松散耦合。

缺点:同回调函数相比,运行速度较慢。

优点:

  • 类型安全:需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,则编译器会报错。
  • 松散耦合:信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的那个槽接收它发出的信号,它只需要在适当的时间发送适当的信号即可,而不需要关心是否被接收和哪个对象接收了。Qt保证适当的槽得到了调用,即使关联的对象在运行时删除,程序也不会崩溃。
  • 灵活性:一个信号可以关联多个槽,多个信号也可以关联同一个槽。

缺点:

速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍左右。

原因:

需要定位接收信号的对象。

安全地遍历所有槽。

编组,解组传递参数。

多线程的时候,信号需要排队等候。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损失,对于实时应用程序是可以忽略的。)

自定义信号槽注意事项:

(1)发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);

(2)使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;

(3)槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;

(4)使用 emit 在恰当的位置发送信号;

(5)使用QObject::connect()函数连接信号和槽;

(6)任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。

信号槽的多种用法:

  1. 一个信号可以和多个槽相连

    如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。

  2. 多个信号可以连接到一个槽

    只要任意一个信号发出,这个槽就会被调用。

  3. 一个信号可以连接到另外的一个信号

    当第一个信号发出时,第二个信号被发出。除此之外,这种信号-信号的形式和信号-槽的形式没有什么区别。

  4. 槽可以被取消链接

    这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。

  5. 使用Lambda 表达式

    在使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。

信号与槽的具体流程。

  • moc查找头文件中的signals,slots,标记出信号和槽。
  • 将信号槽信息存储到类静态变量staticMetaObject中,并且按声明顺序进行存放,建立索引。
  • 当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
  • 当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
  • 通过active函数找到在map中找到所有与信号对应的槽索引 根据槽索引找到槽函数,执行槽函数。

注意点

信号与槽机制是比较灵活的,但有些局限性我们必须了解,这样在实际的使用过程中才能够做到有的放矢,避免产生一些错误。下面就介绍一下这方面的情况。

  • 信号与槽的效率是非常高的,但是同真正的回调函数比较起来,由于增加了灵活 性,因此在速度上还是有所损失,当然这种损失相对来说是比较小的,通过在一台 i586- 133 的机器上测试是 10 微秒(运行 Linux),可见这种机制所提供的简洁性、灵活性还是 值得的。但如果我们要追求高效率的话,比如在实时系统中就要尽可能的少用这种机制。
  • 信号与槽机制与普通函数的调用一样,如果使用不当的话,在程序执行时也有可能 产生死循环。因此,在定义槽函数时一定要注意避免间接形成无限循环,即在槽中再次发射 所接收到的同样信号。
  • 如果一个信号与多个槽相关联的话,那么,当这个信号被发射时,与之相关的槽被 激活的顺序将是随机的,并且我们不能指定该顺序。
  • 宏定义不能用在 signal 和 slot 的参数中。
  • 构造函数不能用在 signals 或者 slots 声明区域内。
  • 函数指针不能作为信号或槽的参数。
  • 信号与槽不能有缺省参数。
  • 信号与槽也不能携带模板类参数。
  • 所有的信号声明都是公有的,所以Qt规定不能在signals前面加public,private, protected。
  • 所有的信号都没有返回值,所以返回值都用void。
  • 所有的信号都不需要定义。
  • 必须直接或间接继承自QOBject类,并且开头私有声明包含Q_OBJECT。
  • 在同一个线程中,当一个信号被emit发出时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码,如果一个信号链接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们链接时的顺序执行的。不同线程中(即跨线程时),槽函数的执行顺序是随机的。
  • 在链接信号和槽时,可以设置链接方式为:在发出信号后,不需要等待槽函数执行完,而是直接执行后面的代码,是通过connect的第5个参数。
  • 信号与槽机制要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,信号的参数可以比槽函数的参数多,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。

4.qt的智能指针

QPointer

QPointer是一个被保护的指针,行为类似于普通的c++指针T *,会在被引用的对象被销毁时自动清除(不像普通的C++指针,在这种情况下会成为"悬空指针")。但是,T必须是QObject的子类,否则将导致编译失败或链接错误。

当需要存储一个指向其他地方(可能是类或者他人需要修改的指针)拥有的QObject的指针时,这时使用保护指针非常合适,如果此时程序中仍然持有该内存区域的引用时,该指针区域可能会在其他的地方被销毁。那么使用保护指针可以安全地测试指针的有效性,从而避免使用无效的、非法的或者悬空指针。

QSharedPointer

​ QSharedPointer是一个引用计数的共享指针对象的实现,可以用来维护对单个指针的引用集合。

​ QSharedPointer在c++中是一个自动、共享的指针。它的行为与普通的指针完全一样,包括对const的支持行为。

​ QSharedPointer对象可以从普通指针、另一个QSharedPointer对象创建,也可以通过将QWeakPointer对象提升为强引用创建。

​ QSharedPointer和QWeakPointer是可重入类。通常一个给定的QSharedPointer或QWeakPointer对象不能被多个线程在没有同步的情况下同时访问。

​ 不同的QSharedPointer和QWeakPointer对象可以被多个线程在同一时间安全地访问。这包括它们持有指向同一对象的指针的情况;引用计数机制是原子的,不需要手动同步。

​ QSharedPointer 是线程安全的,因此即使有多个线程同时修改 QSharedPointer 对象也不需要加锁。这里要特别说明一下,虽然QSharedPointer 是线程安全的,但是 QSharedPointer 指向的内存区域可不一定是线程安全的。所以多个线程同时修改 QSharedPointer 指向的数据时还要应该考虑加锁的。

QScopedPointer

​ 类似于 C++ 11 中的 unique_ptr。当内存数据只在一处被使用,用完就可以安全的释放时就可以使用 QScopedPointer。

​ QScopedPointer只是持有一个指向堆分配对象的指针,并在其析构函数中删除它。当一个对象需要分配和删除堆时,这个类很有用,但仅此而已。QScopedPointer是轻量级的,它不使用额外的结构或引用计数。

QWeakPointer

​ 此类使用较少

​ QWeakPointer是C++中对指针的自动弱引用。它不能用于直接解引用指针,但可以用于验证指针是否在另一个上下文中被删除。

​ QWeakPointer对象只能通过QSharedPointer赋值来创建。

​ 值得注意的是,QWeakPointer没有提供自动强制转换操作符来防止错误的发生。即使QWeakPointer跟踪一个指针,它也不应该被视为指针本身,因为它不能保证所指向的对象仍然有效。

​ 因此,要访问QWeakPointer正在跟踪的指针,必须首先将其提升为QSharedPointer,并验证结果对象是否为空。

QSharedDataPointer

​ QSharedDataPointer 这个类帮我们实现数据的隐式共享。Qt 中大量的采用了隐式共享和写时拷贝技术。

QSharedDataPointer持有一个指向共享数据的指针(即,从QSharedData派生的类)。它通过放置在QSharedData基类中的内部引用计数来实现这一点。因此,这个类可以根据对被保护的数据的访问类型进行分离:如果是非const访问,它会自动创建一个副本以完成操作。

QScopedArrayPointer

​ 如果我们指向的内存数据是一个数组,这时可以用 QScopedArrayPointer。QScopedArrayPointer 与 QScopedPointer 类似,用于简单的场景。

5.线程

两种基本方式,一种是QObject继承,将对象MoveToThread(&QThread),另一种是QThread继承,并重写run函数。

QtConcurrent运行一个线程池,它是一个更高级别的API,不适合运行大量的阻塞操作:如果你做了很多阻塞操作,你很快就会耗尽池并让其他请求排队.在那种情况下,QThread(较低级别的构造)可能更适合于操作(每个代表一个线程).

QT 保证多线程安全

  • 互斥量(QMutex) QMutex m_Mutex; m_Mutex.lock(); m_Mutex.unlock();

  • 互斥锁(QMutexLocker) QMutexLocker mutexLocker(&m_Mutex); 从声明处开始(在构造函数中加锁),出了作用域自动解锁(在析构函数中解锁)。

  • 等待条件(QWaitCondition) QWaitCondtion m_WaitCondition; m_WaitConditon.wait(&m_muxtex, time);

    m_WaitCondition.wakeAll();

  • QReadWriteLock类 》一个线程试图对一个加了读锁的互斥量进行上读锁,允许; 》一个线程试图对一个加了读锁的互斥量进行上写锁,阻塞; 》一个线程试图对一个加了写锁的互斥量进行上读锁,阻塞;、 》一个线程试图对一个加了写锁的互斥量进行上写锁,阻塞。 读写锁比较适用的情况是:需要多次对共享的数据进行读操作的阅读线程。 QReadWriterLock 与QMutex相似,除了它对 "read","write"访问进行区别对待。它使得多个读者可以共时访问数据。使用QReadWriteLock而不是QMutex,可以使得多线程程序更具有并发性。

  • 信号量QSemaphore 但是还有些互斥量(资源)的数量并不止一个,比如一个电脑安装了2个打印机,我已经申请了一个,但是我不能霸占这两个,你来访问的时候如果发现还有空闲的仍然可以申请到的。于是这个互斥量可以分为两部分,已使用和未使用。

  • QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁

6.事件

事件主要分为两种:

  • 在与用户交互时发生。比如按下鼠标(mousePressEvent),敲击键盘(keyPressEvent)等。
  • 系统自动发生,比如计时器事件(timerEvent)等。

Qt中所有的事件类都继承于QEvent类

监听全局事件

在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。如上所述,event()函数主要用于事件的分发。所以,如果希望在事件分发之前做一些操作,就可以重写这个event()函数了。

监听某一类控件的事件

在搭建Qt窗口界面的时候,在一个项目中很多窗口,或者是窗口中的某个模块会被经常性的重复使用。一般遇到这种情况我们都会将这个窗口或者模块拿出来做成一个独立的窗口类,以备以后重复使用。在使用Qt的ui文件搭建界面的时候,工具栏栏中只为我们提供了标准的窗口控件,如果我们想单独控制某一类控件的事件就只需要自定义控件然后重写从父类继承的相应的事件虚函数。

监听某一个控件的事件

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,某个EditlLine可能想要拦截鼠标焦点事件,不让别的组件接收到。那么就需要使用事件过滤器(eventFilter ( QObject * watched, QEvent * event ))。
事件过滤器:

QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:

virtual bool QObject::eventFilter ( QObject * watched, QEvent * event);

这个函数正如其名字显示的那样,是一个"事件过滤器"。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。

事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。

Qt的事件循环

Qt作为一个跨平台的UI框架,其事件循环实现原理, 就是把不同平台的事件循环进行了封装,并提供统一的抽象接口。

事件循环首先是一个无限"循环",程序在exec()里面无限循环,能让跟在exec()后面的代码得不到运行机会,直至程序从exec()跳出。从exec()跳出时,事件循环即被终止。QEventLoop::quit()能够终止事件循环。事件循环实际上类似于一个事件队列,对列入的事件依次的进行处理,当时间做完而时间循环没有结束的时候,其实际上比较类似于一个不占用CPU时间的的for(;;)循环。其本质实际上是以队列的方式来重新分配时间片。

事件循环是可以嵌套的,当在子事件循环中的时候,父事件循环中的事件实际上处于中断状态,当子循环跳出exec之后才可以执行父循环中的事件。当然,这不代表在执行子循环的时候,类似父循环中的界面响应会被中断,因为往往子循环中也会有父循环的大部分事件,执行QMessageBox::exec(),QEventLoop::exec()的时候,虽然这些exec()打断了main()中的QApplication::exec(),但是由于GUI界面的响应已经被包含到子循环中了,所以GUI界面依然能够得到响应。如果某个子事件循环仍然有效,但其父循环被强制跳出,此时父循环不会立即执行跳出,而是等待子事件循环跳出后,父循环才会跳出。

事件与信号的区别

使用场合和时机不同 一般情况下,在"使用"窗口部件时,我们经常需要使用信号,并且会遵循信号与槽的机制;而在"实现"窗口部件时,我们就不得不考虑如何处理事件了。举个例子,当使用 QPushButton 时,我们对于它的 clicked()信号往往更为关注,而很少关心促成发射该信 号的底层的鼠标或者键盘事件。但是,如果要实现一个类似于 QPushButton 的类,我们就需要编写一定的处理鼠标和键盘事件的代码,而且在必要的时候,仍然需要发射和接收 clicked()信号。

使用的机制和原理不同 事件类似于 Windows 里的消息,它的发出者一般是窗口系统。相对信号和槽机制,它 比较"底层",它同时支持异步和同步的通信机制,一个事件产生时将被放到事件队列 里,然后我们就可以继续执行该事件 "后面"的代码。事件的机制是非阻塞的。 信号和槽机制相对而言比较"高层",它的发出者一般是对象。从本质上看,它类似 于传统的回调机制,是不支持异步调用的。

7.设计模式

单例模式

单例模式只允许创建一个活动的对象(实例),提供了对唯一实例的受控访问。

比如Windows的任务管理器,就是一个很典型的单例模式实现。

单例实现原理是,将能够创建对象的函数都设置为private,通过静态成员返回一个实例。

有两种方式,一个是懒汉式,一个是饿汉式。懒汉式需要考虑加锁。

那么我们就必须保证:

  • 该类不能被复制。
  • 该类不能被公开的创造。

那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。

懒汉式设计模式实现方式

  • 静态指针 + 用到时初始化

  • 局部静态变量

饿汉式设计模式实现方式

  • 直接定义静态对象

  • 静态指针 + 类外初始化时new空间实现

懒汉模式的特点是延迟加载,比如配置文件,采用懒汉式的方法,配置文件的实例直到用到的时候才会加载,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化。

饿汉模式的特点是单例类定义的时候就进行实例化。因为main函数执行之前,全局作用域的类成员静态变量m_Instance已经初始化,故没有多线程的问题。

优点

实现简单,多线程安全。

缺点

  • 如果存在多个单例对象且这几个单例对象相互依赖,可能会出现程序崩溃的危险。原因:对编译器来说,静态成员变量的初始化顺序和析构顺序是一个未定义的行为;具体分析在懒汉模式中也讲到了。

  • 在程序开始时,就创建类的实例,如果Singleton对象产生很昂贵,而本身有很少使用,这种方式单从资源利用效率的角度来讲,比懒汉式单例类稍差些。但从反应时间角度来讲,则比懒汉式单例类稍好些。

使用条件

  • 当肯定不会有构造和析构依赖关系的情况。

  • 想避免频繁加锁时的性能消耗

工厂模式

就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。

工厂模式属于创建型模式,大致可以分为三类,简单工厂模式、工厂方法模式、抽象工厂模式。

简单工厂模式 它的主要特点是需要在工厂类中做判断,从而创造相应的产品。当增加新的产品时,就需要修改工厂类。
优点 : 简单工厂模式可以根据需求,动态生成使用者所需类的对象,而使用者不用去知道怎么创建对象,使得各个模块各司其职,降低了系统的耦合性。
缺点:就是要增加新的核类型时,就需要修改工厂类。这就违反了开放封闭原则:软件实体(类、模块、函数)可以扩展,但是不可修改。

工厂方法模式 ,是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。
优点 : 扩展性好,符合了开闭原则,新增一种产品时,只需增加改对应的产品类和对应的工厂子类即可。
缺点:每增加一种产品,就需要增加一个对象的工厂。如果这家公司发展迅速,推出了很多新的处理器核,那么就要开设相应的新工厂。在C++实现中,就是要定义一个个的工厂类。显然,相比简单工厂模式,工厂方法模式需要更多的类定义。

抽象工厂模式 是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
优点 : 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。
缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。

观察者模式

观察者模式的作用是:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
优点

(1)降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。

(2)目标与观察者之间建立了一套触发机制。
缺点

(1)目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。

(2)当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。

8.属性系统

属性系统 如同很多编译器厂商提供的编译器一样,Qt也提供了一个精妙的属性系统。然而,作为一个独立于编译器和架构的库,Qt不依赖于诸如__property或[property]这样的非标准的编译器特性。Qt的这套属性系统特性可以用于任何Qt支持的编译器与架构。它基于元对象系统(Meta-Object System),这套系统同时也提供信号与槽机制用于对象间通讯。

属性与成员变量的区别:

  • 成员变量是一个"内"概念,反映的是类的结构构成。属性是一个"外"概念,反映的是类的逻辑意义。
  • 成员变量没有读写权限控制,而属性可以指定为只读或只写,或可读可写。
  • 成员变量不对读出作任何后处理,不对写入作任何预处理,而属性则可以。
  • public成员变量可以视为一个可读可写、没有任何预处理或后处理的属性。 而private成员变量由于外部不可见,与属性"外"的特性不相符,所以不能视为属性。
  • 虽然大多数情况下,属性会由某个或某些成员变量来表示,但属性与成员变量没有必然的对应关系, 比如与非门的 output 属性,就没有一个所谓的 $output 成员变量与之对应。 属性 与成员变量的 区别与联系 及属性修饰词理解。

属性定义

Qt 提供一个 Q_PROPERTY() 宏可以定义属性,它也是基于元对象系统实现的。Qt 的属性系统与 C++ 编译器无关,可以用任何标准的 C++ 编译器编译定义了属性的 Qt C++ 程序。

在 QObject 的子类中,用宏 Q_PROPERTY() 定义属性

相关推荐
Lee川11 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川15 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i17 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有17 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有17 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫18 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫18 小时前
Handler基本概念
面试
Wect19 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼20 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼20 小时前
Next.js 企业级落地
前端·javascript·面试