Qt基础(C++11)

Qt核心模块

  1. QtCore:提供了 Qt 的核心功能,例如基本的非 GUI 类、线程和事件处理等。

  2. QtGui:提供用户界面(UI)类,例如窗口部件、按钮、标签等。此外,它还包含 QPainter 和 QPalette 等绘图和调色板类。

  3. QtWidgets:是 QtGui 模块的子集,提供了一套完整的可视化 UI 控件库,例如按钮、文本编辑器、表格等,用于构建跨平台的桌面应用程序。

  4. QtNetwork:提供网络编程类,用于创建 TCP 和 UDP 客户端和服务器,以及处理套接字和 HTTP 请求。

  5. QtSql:提供简单易用的数据库访问 API,用于在 Qt 中连接、查询和操作数据源中的数据。

ladmbda表达式

lambda表达式是一种匿名函数,其基本语法结构为:

[capture](parameters) -> return_type { body }
// capture: 用于指定 lambda 表达式可以访问哪些外部变量(即在 lambda 之外声明的变量)。
// parameters: 参数列表,类似于普通函数的参数列表。
// return_type: 返回类型(可选的,如果能自动推导出则可省略)。
// body: 函数体,lambda 表达式的实现。

其中重点需要注意的时候lambda表达式的外部变量捕获,c++11支持以下几种捕获方式:

  1. 值捕获:直接复制变量值,外部修改不会影响表达式内部的值。

  2. 引用捕获:捕获变量的引用,不复制数据,在表达式内部可以修改外部变量,需要保证在表达式执行时外部引用依然有效。

  3. 隐式捕获:即使用=(捕获作用域范围内所有变量的值)、&(捕获作用域范围内所有变量的引用)进行捕获,也可混合使用,如使用&x捕获x的引用,在使用=捕获其余变量的值,反之也可以。

  4. this指针捕获,直接将this传递到表达式中,需注意变量的生命周期。

捕获方式

语法

修改外部变量

是否需要mutable

生命周期关注

Qt中使用建议

值捕获

[var]

否(修改副本)

是(如果修改副本)

安全,推荐

引用捕获

[&var]

小心使用

隐式值捕获

[=]

否(成员变量除外)

是(如果修改)

注意this捕获

隐式引用捕获

[&]

非常高

避免使用

this捕获

[this]

是(修改对象)

注意对象生命周期

在Qt中的使用建议和注意事项:

  1. 链接信号时,使用值捕获,或者this捕获

  2. 使用对象时检查对象是否可用或存在。

  3. 避免在表达式中循环引用,如无法避免,建议使用std::weak_ptr。

  4. 注意对象生命周期;同时避免隐式捕获,尽可能小的进行捕获。

自动类型推导:

即关键字 auto,编译器根据初始化的内容判断变量类型,简化声明操作。

范围循环

range-based for,简化容器迭代,

std::vector<int> vec = {1, 2, 3, 4};
  for (int i : vec) {
      std::cout << i << std::endl;
  }

智能指针

通过引用计数等方式,提供了自动管理对象生命周期的功能,避免手动管理时的内存泄露。

如果是纯Qt项目、需要管理QObject派生类(对于QObject派生类,优先使用父子关系进行管理)、或使用信号槽进行传递的情况下,建议使用Qt提供的智能指针,

如果需要使用跨平台标准库,需要标准C++兼容,或者管理和使用STL算法与容器,则建议使用STL的智能指针。

以下是常用的智能指针简介与使用场景说明:

  1. std::unique_ptr:独占所有权,支持自定义删除器,可转换为std::shared_ptr,常用于工厂模式的返回值、独占的类成员等。

  2. std::shared_ptr:共享所有权,内部包含引用计数,支持自定义删除器,配合std::weak_ptr可解决循环引用问题。需注意初始化时要使用std::make_shared。如果该指针管理的对象中包含需要调用一个指向该对象的智能指针作为参数(如链式调用、方法链或工厂类返回自己本身的shard_ptr等),则可以使用std::enable_shared_from_this来构造该对象,避免在对象内部使用this指针重复构建,导致重复释放。

  3. std::weak_ptr:弱引用,解决循环调用问题,使用weak_ptr不增加引用计数,在使用前执行lock()获取shard_ptr。

  4. QScopedPointer:作用域指针,使用方式和场景同std::unique_ptr,Qt风格的API。

  5. QSharedPointer:共享指针,使用方式和场景同std::shared_ptr,附带支持QObject和QSharedData。

  6. QWeakPointer :弱指针,使用方式和场景同std::weak_ptr,支持信号槽。

特性

STL

Qt

说明

独占指针

std::unique_ptr

QScopedPointer

Qt版缺少移动语义

共享指针

std::shared_ptr

QSharedPointer

Qt版有更好的Qt集成

弱指针

std::weak_ptr

QWeakPointer

功能类似

数组支持

unique_ptr<T[]>

QScopedArrayPointer

Qt有专门数组版

自定义删除器

支持

支持

语法略有不同

线程安全

引用计数原子操作

引用计数原子操作

两者都线程安全

性能

通常更快

与Qt集成更好

STL更标准

与Qt对象集成

需要小心

无缝集成

Qt智能指针知道QObject

移动语义

C++11完全支持

Qt5开始有限支持

STL更现代

类型转换

static_cast:

  • 用于在编译期执行静态类型转换,无运行开销,无运行时类型检查。可以进行常见的类型转换,例如基本类型之间的转换、向上转型(继承关系中子类转为基类)、向下转型(将基类指针或引用转换为派生类指针或引用)等。

dynamic_cast:

  • 用于执行动态类型转换,在运行时检查类型的兼容性,确保安全的向下转型。

  • 用于多态类型的转换,需要基类中至少有一个虚函数

  • 如果转换不安全,返回空指针(对于指针转换)或抛出 std::bad_cast 异常(对于引用转换)。

reinterpret_cast

  • reinterpret_cast 用于执行低级别的类型转换,将一个指针或引用转换为不同类型的指针或引用,没有类型检查。

  • 可以执行诸如将指针抓换为数字,将数字转换为指针,转换指针类型,将char*转为std::string,将float直接转为char*等。由于不执行类型检查,直接执行低级转换,所以使用时需要格外注意。

const_cast:

  • const_cast 用于去除 const 或 volatile 修饰符,允许对 const 或 volatile 对象进行非 const 或非 volatile 操作。

qobject_cast:

  • 是 Qt 框架提供的类型转换操作符,用于在 Qt 对象体系中进行类型转换。

  • 主要用于在 Qt 的对象之间进行安全的转换,例如在信号槽连接、查找子对象等场景。

  • 在运行时进行类型检查,如果转换失败,返回空指针或 null。

  • 与 dynamic_cast 相似,但 qobject_cast 需要类继承自 QObject,且需要启用元对象系统 (MOC)。

qstatic_cast 和 qdynamic_cast:

  • qstatic_cast 是 Qt 框架提供的静态类型转换操作符,类似于 static_cast,用于常见的类型转换,如基本类型之间的转换、向上转型等。

  • qdynamic_cast 是 Qt 框架提供的动态类型转换操作符,类似于 dynamic_cast,用于多态类型的转换,进行向下转型,并在运行时检查类型的兼容性。

  • qstatic_cast 和 qdynamic_cast 与 static_cast 和 dynamic_cast 的差异主要是在 Qt 对象体系中的使用,允许与 Qt 特性(如信号槽系统)一起使用。

QPainter简述

QPainter 是 Qt 的绘图引擎,用于绘制各种图形元素和渲染文本。它提供了很多常用的函数,例如画矩形、画圆等可以绘制基本图形。它还包含设置画笔宽度、颜色和样式、字体,渐变、图案、透明度的功能等,使用户可以使用相对简单的步骤呈现出复杂的效果。

QPainter 在 GUI 应用程序中特别有用。通过使用 QPainter,您可以在窗口或其他部件上实现自定义的渲染,并创建自己的图标、图表和数据可视化工具,增强应用程序的用户体验。

当时做悬浮球窗口的时候,就是重写了QWidget的paintEvent来实现的,在paintEvent中拿到QPainter对象,然后通过设置其属性、设置画笔、绘制形状,控制其将窗口绘制成异形,并在根据传递来的数据实时绘制界面内容。

如果需要绘制大量过于复杂的图形,会产生性能问题,建议使用OpenGL或者是QtQuick的QML Canvas来实现。

QString与STL字符串简述

QString:最常用字符串类,支持Unicode和本地化字符、支持格式化操作和常用字符串操作。

std::string:纯字节序列,不关心编码,提供简单高效的字符串操作。

Qt的内存管理机制简述

Qt中内存管理机制类似于智能指针,它可以为对象设置一个父对象,当父对象释放的时候,子对象会跟随父对象一起释放,注意这里的父子对象与C++继承无关,是对象之间的关系。

原理很简单,在设置父对象的时候,让父对象保存子对象的地址,然后在父对象的析构函数中释放子对象。即可完成内存管理,Qt中的内存管理规则为一个父对象可以管理多个子对象,而子对象最多只会有一个父对象,并且由于父对象需要管理多个子对象,导致必须使用容器存储,且所有子对象和父对象必须直接或间接继承同一个类。

用法为在子对象构造时传递父对象的指针即可,或者子对象调用setParent()函数设置或更换父对象。Qt中基本所有带有Parent与Children单词的函数基本都与内存管理相关。

窗口对象(QWidget)的父子关系可由子对象的构造函数中传递父对象的指针完成,或者子对象调用setParent()函数,传递父对象的指针。

作用为子窗口默认会成为父窗口的子窗口,并嵌套进父窗口中,在父窗口显示时同时显示,父窗口隐藏时同时隐藏。如果不希望嵌套可以使用setWindowFlag(Qt::Window)设置子窗口的属性为Window。需要注意的是设置完Window后就不会在随着父对象的显示而显示,隐藏而隐藏了。 但是在父窗口销毁时还是会带着子窗口一起销毁,子对象无需再调用delete。

QxxWidget和QxxView的区别

在Qt内部可以见到好多以此规则命名的组件,这里以QTableView和QTableWidget为例,列举出这两种控件之间区别和最佳实践。

特性

QTableWidget

QTableView

架构

一体式(模型+视图)

分离式(MVC)

适用数据量

小数据(< 1000行)

大数据(支持虚拟化)

API复杂度

简单直观

相对复杂

内存占用

较高(每个单元格都是对象)

较低(可优化)

功能扩展

有限

强大(排序、过滤、自定义模型)

多视图支持

不支持

支持(同一模型多个视图)

数据库集成

需要手动处理

直接支持(QSqlTableModel)

自定义显示

通过QTableWidgetItem属性

通过委托(Delegate)

性能优化

有限

支持虚拟化、懒加载

使用场景

简单表单、配置表格、原型开发

数据库应用、实时监控、复杂数据展示

由此可见,当数据量较大、需要多视图显示同一数据、自定义数据源以及显示特异界面的时候,使用QTableView更容易实现需求。

国际化

待补充。。。。

Qt多线程与线程同步

多线程的实现方式有两种:

  1. 继承QThread并重写run函数。

  2. 使用Qt的线程池,QThreadPool结合QRunnable类。QRunnable是一个抽象基类,用于定义可执行的任务,并将任务添加到线程池中运行。

线程之间的通信,可以使用信号槽机制。由于跨线程传递,需要将连接类型改为QueuedConnection。

线程同步则有以下几种方式:

QMutexLocker

访问共享资源时,可以使用QMutex互斥量对共享资源进行加锁和解锁操作,保证同一时间只有一个线程可以访问共享资源。

为避免死锁,需要与QMutexLocker配合使用。

QReadWriteLock读写锁

针对需要多个线程对共享资源进行读操作,同时只有一个线程进行写操作,这种情况可以使用QReadWriteLocker。该锁主要实现多个线程读取资源,一个线程写。写操作执行时会阻断所有读线程,读线程之间则不会进行同步。

同QMutex一样,Qt同样为其提供了QReadLocker和QWriteLocker类方便使用。

QSemaphore信号量

信号量库用于实现线程之间的互斥和同步以及控制线程的并发数量。它用来保护一定数量的相同资源,且具有更好的并发性,一般常用信号量实现生产者-消费者模式。

QAtomicInteger

对于简单的标记位、整数类型等共享资源,可以使用原子操作来保证操作的原子性,避免使用锁而带来额外的系统开销。

QThread 和 QtConcurrent 的区别

QThread 是 Qt 中的一个基础类,用于在应用程序中建立新的线程。使用 QThread,可以创建一个新线程并将特定任务放在该线程中执行,从而使主线程不会被阻塞

两者的设计思路区别

QtConcurrent 则是一个高级别 API,提供了许多方便加载和管理线程的函数。通过使用 QtConcurrent,您可以轻松地编写并行代码,并使用 map-reduce 模式执行算法。它更易于使用和管理,但灵活性较低。

QThread将线程当作一个可操作对象,需要手动显示的创建和管理线程,而QtConcurrent则更像是一个并行执行的任务,使用全局的线程池,无需创建线程对象,自动复用线程。

通信上的区别

QThread是使用信号槽来进行外部通信的。

QtConcurrent 是基于返回值进行通信的,其返回QFuture,该类中包含任务的执行结果和返回数据,是阻塞式的,如需非阻塞式,需要使用QFutureWatcher来进行监视。

异常处理区别

QThread需要配合QMetaObject::invokeMethod来向主线程传递异常,不能直接emit信号。

QtConcurrent可以直接throw异常。

最佳实践

QThread更适合需要长时间运行的后台服务、需要事件循环的后台任务(如网络监听、串口控制)、更精细控制线程行为的场景。

QtConcurrent 更适合较大的数据处理与转换(如导出数据库数据到表格中)、并行计算、批量操作文件、简单的一次性任务等场景。

模型视图

传统的MVC是Model模型+Controller控制器+View视图,在Qt中则是实现为Model数据、逻辑控制+Delegate委托、渲染+View视图显示。

其核心优点有:

  1. 做到数据与显示分离,同一个数据模型可以显示在不同的视图中,修改模型,视图自动更新。

  2. 处理大数据时性能较好。

  3. 通过代理可简单实现过滤和排序。

常见的内置控件如QListView、QListWidget、QTableView、QTableWidget、QTreeView、QTreeWidget等均支持。

常用数据结构

  1. vector:向量,连续存储,可随机访问。

  2. deque:双向队列,连续存储,随机访问。

  3. list:链表,内存不连续,不支持随机访问。

  4. stack:栈,不可随机访问,只允许再开头增加/删除元素。

  5. queue:单向队列,尾部增加,开头删除。

  6. set:集合,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。

  7. map:图,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。

  8. hash_set:哈希表,随机访问。查找、插入、删除时间复杂读为O(1)。

信号槽机制

  • 信号与槽是 QT 中用于对象间通信的机制。当一个对象的状态发生变化时,它会发出一个信号(signal);而另一个对象可以连接这个信号到一个槽(slot)函数上,当信号发出时,与之连接的槽函数会被自动调用。信号和槽都是普通的 C++ 函数,但信号只需声明,无需实现;槽函数则需要实现具体的逻辑。信号与槽机制是类型安全的,编译器会检查信号和槽的参数类型是否匹配。

  • 优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。可用回调函数进行替代。当然槽函数也可以时虚函数。

  • 信号槽是同步的还是异步,主要取决于最后一个参数的设置,其中最常用的是前三种:

    Qt::AutoConnection(自动方式)Qt的默认连接方式,如果信号的发出和接收这个信号的对象同属一个线程,那个工作方式与直连方式相同;否则工作方式与排队方式相同。

    Qt::DirectConnection(直连方式)信号与槽函数关系类似于函数调用,同步执行,即当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。

    Qt::QueuedConnection(排队方式)此时信号被塞到信号队列里了,信号与槽函数关系类似于消息通信,异步执行,当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。

    Qt::BlockingQueuedConnection(阻塞队列方式)要求信号和槽必须在不同的线程中,否则就产生死锁,这个是完全同步队列只有槽线程执行完成才会返回,否则发送线程也会一直等待,相当于是不同的线程可以同步起来执行。

    Qt::UniqueConnection(唯一链接)与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。

  • disconnect函数的使用场景,以及再次emit一个已经disconnect的信号会有什么问题:

    使用场景:希望信号再某些时间段不触发槽函数的情况、避免重复链接、接收对象需要被销毁时、改变信号链接关系时

    emit断开链接的信号,不会发生问题,QT内部会检查链接的列表。

    信号链接管理的最佳实践建议,不要频繁的断开信号链接,如需要临时断开,可使用QSignalBlocker类,且QT会再对象销毁时自动断开相关的链接。

  • 关于信号槽参数传递时候的拷贝问题,请参考这篇文章 数据是否发生拷贝?参数在信号-槽中的传递 - Larpx的站点|一个什么都记录的小站

QT的事件机制

QT事件机制本身是一个无限循环的exce(),在这个死循环中不断检测事件队列有无新的事件,并将其封装成QEvent,发送到对应的接收者进行处理。其特点是可以设置一个QObject去监测、拦截另外一个QObject的事件。

对于系统事件,由于不同的操作系统对事件循环的实现不同,Windows系统的事件循环由WinAPI提供的消息循环机制来实现事件处理,Qt会将Windows的消息转换为Qt事件进行处理,而在Linux系统中,则由epoll多路复用机制完成。不同操作系统不同版本的Qt具体实现方式可能不同,版本迁移时需要额外注意。

处理方式

作用范围

执行时机

典型使用场景

性能影响

notify()

全局

最早

全局事件监控、调试、拦截

最高(每个事件都会经过)

事件过滤器

对象/全局

notify之后

监控多个对象、扩展第三方类

中等(需要遍历过滤器链)

event()

单个对象

过滤器之后

统一处理多种事件、事件预处理

低(每个对象一次调用)

特定处理器

单个对象

event()之后

具体事件处理、自定义行为

最低(直接调用)

  • 事件循环的流程:

    1. 进入 QCoreApplication::exec(),主事件循环

    2. 等待事件到来(如鼠标点击、键盘输入、绘图事件、定时器事件等),或者处理定时器、网络等异步事件。

    3. 当事件到来,将其放入事件队列中。

    4. 从事件队列中取出,找到事件的接收者,再将其发送给该对象进行处理。

    5. 当事件处理函数执行完成,则开始继续监听事件队列,循环继续。

    6. 当调用QCoreApplication::quit()exit() 时,退出事件循环,程序终止。

  • 事件的传递顺序:事件发生 -> notify() -> 事件过滤器链 -> event() -> 特定的事件处理器。

    1. QCoreApplication::notify() - 全局事件入口

    2. 目标对象的事件过滤器链(由近及远)

      1. 对象自身安装的过滤器

      2. 父对象安装的过滤器

      3. QApplication安装的全局过滤器

    3. 目标对象的event()函数

    4. 目标对象的特定事件处理器(如mousePressEvent()等各种xxxEvent())

    5. 如果事件未被处理,可能传递给父对象(某些事件类型)

  • 最佳实践:

    • 可以通过调用QEvent对象的accept()和 ignote()方法来表示事件已经处理和允许继续传递。

    • installEventFilter的安装顺序决定了事件的执行顺序,先安装的最后执行,最后安装的先执行,如果任何一个过滤器返回了true,则后续过滤器将不在执行。

    • 注意在事件处理过程中不要执行过于耗时的操作。

    • 注意在事件中避免触发无限递归操作,如在paintEvent中调用update()。

    • 避免不必要的事件拦截,保持事件传递的透明性。

  • 临时的事件循环QEventLoop,如果需要等待一个耗时操作,可使用QEventLoop来创建并启动一个局部的事件循环。

元对象于QObject

Qt 的元对象系统(Meta-Object System)是 Qt 框架的核心组件之一,它为 C++ 提供了额外的功能,如信号与槽机制、运行时类型识别(RTTI)、属性系统等。这些功能在标准的 C++ 中是没有的,但 Qt 通过元对象系统扩展了这些能力。

元对象系统依赖于Meta-Object Compiler工具,在编译带QWidget界面的程序时,经常能见到编译结果目录中会生成好多以moc_为前缀的临时cpp文件,这就是QT使用MOC工具执行预编译所生成的,MOC负责解析类中的特殊宏,如常用的Q_OBJECT,生成额外的代码,使得信号槽机制与其他元对象功能可以正常工作。

此外,QT还提供了一套动态属性系统,可以通过setProperty() 和 property() 可以动态地为对象设置和获取属性,这在标准 C++ 中是无法做到的。还支持运行时类型识别 (RTTI),通过 inherits() 或 qobject_cast<>() 可以检测对象类型,方便编码时可以安全的执行类型转换。

主要组成部分如下:

  1. 元对象系统(QMetaObject)

    • 每个继承于QObject的类都会自动包含一个QMetaObject对象,该对象存储有类的元信息,其中包括:类名、父类信息、信号槽、枚举类型和属性。

  2. 信号和槽

  3. 宏和MOC

    • Q_OBJECT宏:每个需要元对象系统支持的类都必须包含该宏,这样 MOC 才能生成必要的元信息代码。

    • Q_PROPERTY宏:定义类属性,支持 Qt 的属性系统,如可用于绑定或在 Qt 的编辑器中显示。

    • Q_ENUMS和Q_FLAGS宏:为枚举和标志类型提供运行时支持。

  4. 动态属性于运行时类型识别(RTTI)

  5. 国际化,即使用tr()函数方便的进行字符串的翻译处理。

  6. 计时器功能,可以直接通过startTimer()和timerEvent()等函数添加定时器。

  7. 反射机制

    • Qt 的元对象系统还提供了基本的反射机制。通过 QMetaObject 可以查询类的信号、槽、方法、属性等信息,甚至可以在运行时调用方法(如 QMetaObject::invokeMethod())。

隐式共享

简单来说,隐式共享是一种高效的内存管理技术。它用于优化对象的复制行为,使得对象的复制不实际进行,而是通过引用计数来共享数据,只有在修改数据时才执行真实的复制操作。

其核心思想是当多个对象共享相同的数据时,它们引用同一块内存区域,而不复制数据。只有当某个对象试图修改共享的数据时,才会创建一份副本,以避免对其他共享该数据的对象产生影响。这种方式结合了浅拷贝深拷贝的优点,既保证了内存使用的高效,又能确保对象的独立性

相比移动语义,隐式共享仅仅共享所有权,并在最后修改数据时执行复制,而移动语义则是转移数据的所有权,被转移的源对象变为空。

最佳实践:

  1. 使用const &,使用链式调用或者中间临时变量去操作,避免不必要的分离。

  2. 利用返回值优化,系统会自动使用移动语义或者是隐式共享,更加高效。

  3. 隐式共享不完全线程安全,避免在多线程间共享可修改的对象,应该让每个线程都使用独立副本。

  4. 避免在循环中反复修改共享对象。

使用流程:

  1. 创建对象:内部分配一个数据结构,保存实际的数据内容。

  2. 拷贝对象:当执行拷贝的时候,新对象不会创建副本,而是直接指向源对象的数,并增加引用计数,这时候多个对象共享同一份数据。

  3. 发生修改:当某个对象需要修改共享的数据时候,QT首先检测该数据的引用计数,如果是多个,则为该对象创建一个副本并执行修改,其他的对象仍指向原始数据,这就是所谓的写时复制。

  4. 分离:当某个对象被销毁时,共享数据的引用计数减少,当引用计数为0时,系统才会释放该数据的内存。

支持隐式共享的QT类如下:

// 字符串类

QString

QByteArray

QChar

QLatin1String

// 容器类

QList<T>

QVector<T>

QLinkedList<T>

QMap<K, V>

QHash<K, V>

QSet<T>

QStack<T>

QQueue<T>

// 图像和图形

QImage

QPixmap

QBitmap

QBrush

QPen

QFont

QRegion

QPolygon

QPainterPath

// 其他

QUrl

QVariant

QDateTime

QDir

QFileInfo

数据库操作

数据库操作