1. D指针和Q指针
D指针
WidgetPrivate *d_ptr;
PIMPL模式,指向一个包含所有数据的私有数据结构体。
Pimpl 意思为“具体实现的指针”(Pointer to Implementation), 它通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏,是隐藏实现,降低耦合性和分离接口实现的一个现代 C++ 技术,并有着“编译防火墙(compilation firewall)”的名头。
- 私有的结构体可以随意改变,而不需要重新编译整个工程项目
- 隐藏实现细节
- 头文件中没有任何实现细节,可以作为API使用
- 原本在头文件的实现部分转移到了源文件,所以编译速度有所提高
为了二进制兼容性,只要版本已发布,除非重新编译工程,否则就不能更改类的结构和大小。那么,为了能够为原有类方便的引入新的功能,这就是Qt引入D指针的目的。私有的结构体可以随意更改,而不需要重新编译整个工程项目。
Q指针
Widget *q_ptr;// q-ptr指向基类API
私有的结构体中储存一个指向公有类的Q指针
总结
1. Qt 中的一个类常用一个PrivateXXX类来处理内部逻辑,使得内部逻辑与外部接口分开,这个PrivateXXX对象通过D指针来访问;在PrivateXXX中有需要引用Owner的内容,通过Q指针来访问。
2. 由于D和Q指针是从基类继承下来的,子类中由于继承导致类型发生变化,需要通过static_cast类型转化,所以DPTR() 与QPTR()宏定义实现了转换。
例子:
/* widget.h */
class Widget {
public:
Widget();
protected:
// 只有子类会访问以下构造函数
Widget(WidgetPrivate &d);// 允许子类通过它们自己的私有结构体来初始化
WidgetPrivate *d_ptr;
};
/* widget_p.h */
struct WidgetPrivate {
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr;
Rect geometry;
String stylesheet;
};
/* widget.cpp */
Widget::Widget()
: d_ptr(new WidgetPrivate(this)) {
}
Widget::Widget(WidgetPrivate &d)
: d_ptr(&d) {
}
/* label.h */
class Label :public Widget {
public:
Label();
protected:
Label(LabelPrivate &d);// 允许Label的子类通过它们自己的私有结构体来初始化
// 注意Label在这已经不需要d_ptr指针,它用了其基类的d_ptr
};
/* label.cpp */
#include "widget_p.h"
class LabelPrivate :public WidgetPrivate {
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate)//用其自身的私有结构体来初始化d指针
}
Label::Label(LabelPrivate &d)
: Widget(d) {
}
副作用是q-ptr和d-ptr分别是Widget和WidgetPrivate类型,所以为了在子类能够使用d指针,我们用static_cast来做强制转换。
// global.h (macros)
#define DPTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
#define QPTR(Class) Class *q = static_cast<Class *>(q_ptr)
// label.cpp
void Label::setText(constString &text) {
DPTR(Label);
d->text = text;
}
void LabelPrivate::someHelperFunction() {
QPTR(label);
q->selectAll();// 我们现在可以通过此函数来访问所有Label类中的方法
}
2. QT 信号槽
调用流程
1. MOC查找头文件中的signal与slots,标记出信号槽。将信号槽信息储存到类静态变量staticMetaObject中,并按照声明的顺序进行存放,建立索引
2. connect 链接,将信号槽的索引信息放到一个双向链表中,彼此配对
3. emit被调用,调用信号函数,且传递发送信号的对象指针,元对象指针,信号索引,参数列表到active函数
4. active函数在双向链表中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数。
信号槽的实现:元对象编译器MOC负责解析signals、slot、emit等标准C++不存在的关键字,以及处理Q_OBJECT、Q_PROPERTY、Q_INVOKABLE等相关的宏,生成moc_xxx.cpp文件。信号函数只有声明,不需要自己实现,在moc_xxx.cpp文件中自动生成。moc本质上是反射器
连接方式,connect第五个参数
1. Qt::AutoConnection: 默认值。 接受、发送在同一个线程 –> Qt::DirectConnection, 否则 –> Qt::QueuedConnection
2. Qt::DirectConnection。 槽函数在信号发送位置被直接调用,槽函数运行于信号发送者所在线程。多线程情况下比较危险
3. Qt::QueuedConnection。 槽函数在控制回到接受者所在的线程的事件循环时被调用,槽函数运行于信号接受者所在的线程。发送信号之后,槽函数不会立刻被调用, 等到接受者的当前函数执行完后,进入事件循环,多线程环境下一般用这个
4. Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5. Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
3. Qt 只运行一个exe实例
#include "mainwindow.h"
#include <QApplication>
#include <QSharedMemory> //
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QSharedMemory shared("onlyOne"); //
if (shared.attach()) return 0; //
shared.create(1); //
MainWindow w;
w.show();
return a.exec();
}
4. QT下如何使用多线程
- 子类化QThread的方法,就是重写了QThread中的run()函数,在run()函数中定义了需要的工作。这样的结果是,我们自定义的子线程调用start()函数后,便开始执行run()函数。如果在自定义的线程类中定义相关槽函数,那么这些槽函数不会由子类化的QThread自身事件循环所执行,而是由该子线程的拥有者所在线程(一般都是主线程)来执行。如果你不明白的话,请看,第二个例子中,子类化的线程的槽函数中输出当前线程的ID,而这个ID居然是主线程的ID!!事实的确是如此,子类化的QThread只能执行run()函数中的任务直到run()函数退出,而它的槽函数根本不会被自己的线程执行。
- moveToThread方法,是把我们需要的工作全部封装在一个类中,将每个任务定义为一个的槽函数,再建立触发这些槽的信号,然后把信号和槽连接起来,最后将这个类调用moveToThread方法交给一个QThread对象,再调用QThread的start()函数使其全权处理事件循环。于是,任何时候我们需要让线程执行某个任务,只需要发出对应的信号就可以。其优点是我们可以在一个worker类中定义很多个需要做的工作,然后发出触发的信号线程就可以执行。相比于子类化的QThread只能执行run()函数中的任务,moveToThread的方法中一个线程可以做很多不同的工作(只要发出任务的对应的信号即可)。
1、线程开启
Qt中,开启子线程,一般有两种方法:
a, 定义工作类worker:
worker继承 QThread, 重写run函数,在主线程中实例化worker,把耗时工作放进worker的run函数中完成,结束后,往主线程中发信号,传递参数即可。
注意:此worker的实例,只有run函数在子线程中执行,worker的其他函数,均在主线程中执行。
如果子线程已经start开启,run函数尚未运行完时,再次start,此时子线程不会有任何操作,run函数不会被重新调用,会继续执行run函数。
b, 定义工作类worker:
worker继承Qobject,在worker中完成耗时操作,并在主线程中 #include “worker.h”进来,随后,在主线程中New出几个子线程QThread,使用moveToThread()函数,把worker转移进入子线程,例如:
pWorker = new Worker;
pThread1 = new QThread;
pWorker->moveToThread(pThread1)
之后,再根据需求,看何时开启子线程:pThread1->start();
如果线程已经运行,你重复调用start其实是不会进行任何处理。
对于上面a类,在run中开启的子线程,如果run中没有调用exec(),使用quit(),exit(),是无法跳出run中的循环,终止子线程的。不会发生任何效果,QThread不会因为你调用quit()函数而退出正在运行到一半的run。
但使用QThread的terminate()方法,可以立刻结束子线程,但这个函数存在非常不安定因素,不推荐使用。那么如何安全的终止一个线程呢?
最简单的方法是添加一个bool变量,通过主线程修改这个bool变量来进行终止,但这样有可能引起访问冲突,需要对其进行加锁。
void myThread::run()
{
int count = 0;
m_isCanRun = true;//标记可以运行
QString str = QString(“%1->%2,thread id:%3”).arg(FILE).arg(FUNCTION)
.arg((unsigned int)QThread::currentThreadId());
emit message(str);
while(1)
{
sleep(1);
++count;
doSomething();
if(m_runCount == count)
{
break;
}
{
QMutexLocker locker(&m_lock);// 此处加锁,防止访问冲突
if(!m_isCanRun)//在每次循环判断是否可以运行,如果不行就退出循环
{
return;
}
}
}
}
5. QT 内存泄露
Qt在内部能够维护对象的层次结构。对于可视元素,这种层次结构就是子组件与父组件的关系;对于非可视元素,则是一个对象与另一个对象的从属关系。在Qt中,在Qt中,删除父对象会将其子对象一起删除。C++中delete 和 new 必须配对使用。delete少了内存泄露多了会crash。Qt中使用了new却很少delete,因为QObject的类及其继承的类,设置了parent(也可在构造时使用setParent函数或parent的addChild)故parent被delete时,这个parent的相关所有child都会自动delete,不用用户手动处理。但parent是不区分它的child是new出来的还是在栈上分配的。这体现delete的强大,可以释放掉任何的对象,而delete栈上对象就会导致内存出错,这需要了解Qt的半自动的内存管理。另一个问题:child不知道它自己是否被delete掉了,故可能会出现野指针。那就要了解Qt的智能指针QPointer。
在Qt中,最基础和核心的类是:QObject,QObject内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。
https://www.cnblogs.com/billbyte/p/14184865.html
Qt 半自动化的内存管理
1. QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象。
2. QWidget及其派生类的对象,可以设置Qt::WA_DeleteOnClose 标志位(当close时会析构该对象)
3. QAbstractAnimation派生类的对象,可以设置QAbstractAnimation::DeleteWhenStopped.
4. QRunnable::setAutoDelete()、MediaSource::setAutoDelete().
5. 父子关系:父对象、子对象、父子关系。Qt中所特有的,与类的继承关系无关,传递参数是与parent有关(基类、派生类、或父类、子类,这是对于派生体系来说的,与parent无关)。
内存泄露案例
使用new 分配,未指定parent且没有delete 改进:
1. 分配到栈上
2. 设置标志位 close() 后会delete label->setAttribute(Qt::WA_DeleteOnClose);
3. new 后手动delete
删除栈上内存 改为堆
Object内部有一个list,会保存children,还有一个指针保存parent,当自己析构时,会自己从parent列表中删除并且析构所有的children。w比label先被析构,当w被析构时,会删除chilren列表中的对象label,但label是分配到栈上的,因delete栈上的对象而出错。
1. 修改顺序。确保label先于其parent被析构,label析构时将自己从父对象的列表中移除自己,w析构时,children列表中就不会有分配在stack中的对象了。
2. 将label分配到堆上或者设this为parent
野指针。 改进方式:QPointer智能指针
deleteLater 当一个QObject正在接受事件队列时如果中途被你销毁掉了,就是出现问题了,所以QT中建议大家不要直接Delete掉一个QObject,如果一定要这样做,要使用QObject的deleteLater()函数,它会让所有事件都发送完一切处理好后马上清除这片内存,而且就算调用多次的deleteLater也不会有问题。
项目经历
- 字幕轨道,最开始我们只有标题文字,我的工作是在标题文字的基础上单独实现了字幕轨道与视频和音频轨道做对应, 这样可以和视频轨道分离,并且方便字幕文件单独的导入和导出,目前支持srt webvtt 和TTML三种字幕文件格式
- Qt样式表的优化。 qt样式表使用qss格式,它是一个css的子集,用来快速实现UI的skinning,并且当spec 要求改变的时候可以快速的更新,然而当UI比较复杂的时候,qss代码也会比较复杂,用来处理和应用skining的时间也会相应增加。因为Qt需要解析widget使用类名,object名,父类名还有它放在哪里,需要不断地获取widget的性质。再者QT使用widget 作为skining的单位,对于每个widget 实例,qt不得不去过滤所有的规则来得到实例需要的样式表,即使之后同样的widget 被创建,qt也会做同样的过程。
- 仍然基于stylesheet,快速实现更新修改
- 打破类之间的继承和容器之间的关系使得查询和过滤加快
- 不再基于widget而是改为基于class 的skining,类似于QStyle的方法,一旦skining被处理过,它将会被缓存用到其他实例
- “PseudoStyleClassName” 引入一个虚拟的类使得不同widget划分到一个class style按需要继承父类,减少不同的性质处理
- 见解了QT6 中的QProxyStyle 子类 实现自定义UI 并且能够快速的创建widget