文章目录
一、C++ lambda函数详解
详细内容可参考:https://liujie.blog.csdn.net/article/details/139213292
C++ lambda表达式的本质就是重载了operator(),lambda表达式会被编译器翻译成类进行处理,在调用时会进行编译展开,因此lambda表达式对象其实就是一个匿名的函数对象,所以lambda表达式也叫做匿名函数对象。Qt槽函数可以使用lambda函数来写。
C++ lambda表达式的构成:
[caputer list] (parameters) mutable throw() -> return type{ statement }
参数意义:
-
caputer list
是捕获列表用于捕获外部变量,捕获的变量可以在函数体中使用,可以省略,即不捕获外部变量。一共有三种捕获方式:值捕获、引用捕获和隐式捕获。
-
parameters
是参数列表与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略,即无参数列表
-
mutable
关键字默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。加上mutable关键字后,可以修改传递进来的拷贝,在使用该修饰符时,参数列表不可省略(即使参数为空)。
-
throw
是异常说明用于Lamdba表达式内部函数抛出异常。
-
return type
是返回值类型。追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
-
statement
是Lambda表达式的函数体内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
注意:可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体
下面介绍三种捕获方式并举例
-
值捕获:不能在lambda表达式中修改捕获变量的值
值捕获意味着捕获的变量通过值传递给lambda表达式,即在lambda表达式中创建了这些变量的副本。因此,lambda表达式中使用的是这些变量的副本,而不是原始变量本身。由于被捕获变量的值是在创建时拷贝,因此随后对其求改不会影响到 lambda 内对应的值。
-
引用捕获:使用引用捕获一个外部变量,需要在捕获列表变量前面加上一个引用说明符&
引用捕获意味着捕获的变量通过引用传递给lambda表达式,即lambda表达式中使用的是原始变量的引用,这意味着可以在lambda内部修改这些变量的值。
-
隐式捕获
- =为值捕获
- &为引用捕获
- 当我们混合使用了隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 & 或 =。此符号指定了默认捕获方式为引用或值。
示例:
// ch1_9_lambda.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
using namespace std;
int main()
{
//值捕获
int value = 100;
auto f = [value](int a, int b)->int {
return a + b + value;
};
cout << f(1, 2) << endl; //103
//引用捕获
auto f2 = [&value](int a, int b)->int {
value++;
return a + b;
};
cout << f2(1, 3) << endl;
cout << "value =" << value << endl;
//隐式捕获,=值捕获,&引用捕获
int age = 123;
auto f3 = [&](int a, int b)->int {
value++;
age++;
return a + b;
};
cout << "age =" << age << endl;
return 0;
}
运行结果:
二、信号槽函数的五种写法
要使用信号槽,必须在类的定义中包含Q_OBJECT
宏。Q_OBJECT
宏使得类能够使用信号和槽机制。在编译过程中,Qt的元对象编译器(moc)会读取带有Q_OBJECT
宏的类的定义,并生成额外的代码来支持信号和槽的连接和调用。
元对象编译器(moc):moc是一个工具,它读取带有
Q_OBJECT
宏的源文件,并生成一个额外的源文件,这个文件包含了元对象代码,包括信号和槽的实现。
-
第一种写法:Qt4写法
connect(ui->btnOpnen, SIGNAL(clicked), this, SLOT(open()));
不推荐这种写法,如果SIGNAL写错了,或者信号名字、槽函数名字写错了,编译器检查不出来,导致程序无响应,引起不必要的误解。
-
第二种写法:Qt5写法
connect(ui.btnOpnen, &QPushButton::clicked, this, &Widget::open);
推荐使用这种写法,信号名字、槽函数名字写错了,编辑器会直接报错。
-
第三种写法:lambda表达式写法
connect(ui.btnOpnen, &QPushButton::clicked, [=](){ //具体代码 });
适用于slot代码(槽函数代码)比较少的情况
-
第四种写法:牵线法
需要在代码中实现槽函数slot2()
不推荐使用这种写法,非常不建议使用,如果空间太多,而且跨很多层界面,这种基本处理不了。
-
第五种写法:槽函数名称为
on_控件名字_信号名()
这种写法不用connect,Qt自动连接。这种用法挺常见,推荐这样使用。
三、自定义信号、信号emit、信号参数注册
1.如何自定义信号
-
使用signals声明
-
返回值是void
-
在需要发送的地方使用
emit 信号名字(参数);
进行发送
-
在需要链接的地方使用
connect
进行链接
2.跨UI发送信号
以“跨UI发送信号”为例,使用自定义信号。
.pro文件:
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
setdialog.cpp \
widget.cpp
HEADERS += \
setdialog.h \
widget.h
FORMS += \
setdialog.ui \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
setdialog.h文件:
#ifndef SETDIALOG_H
#define SETDIALOG_H
#include <QDialog>
namespace Ui {
class SetDialog;
}
class SetDialog : public QDialog
{
Q_OBJECT
public:
explicit SetDialog(QWidget *parent = nullptr);
~SetDialog();
private slots:
void on_btnAdd_clicked();
signals:
void signal_addOne(int value);
private:
Ui::SetDialog *ui;
};
#endif // SETDIALOG_H
widget.h文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_btnOpen_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
setdialog.cpp文件:
#include "setdialog.h"
#include "ui_setdialog.h"
SetDialog::SetDialog(QWidget *parent)
: QDialog(parent)
, ui(new Ui::SetDialog)
{
ui->setupUi(this);
}
SetDialog::~SetDialog()
{
delete ui;
}
void SetDialog::on_btnAdd_clicked()
{
static int score = 90;
emit signal_addOne(score++);
}
widget.cpp文件:
#include "widget.h"
#include "ui_widget.h"
#include "setdialog.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btnOpen_clicked()
{
SetDialog dlg;
//使用connect进行链接
connect(&dlg,&SetDialog::signal_addOne, [=](int value){
ui->lineEdit->setText(QString::number(value));
});
dlg.exec(); //事件循环会阻塞UI
}
main.cpp文件:
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
3.跨线程发送信号
以“跨线程发送信号”为例,使用自定义信号,在子线程上向UI线程上发送信号。
注意:Qt的子线程无法直接修改UI,需要发送信号到UI线程进行修改
.pro文件:
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
childthred.cpp \
main.cpp \
widget.cpp
HEADERS += \
childthred.h \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
childthread.h文件:
#ifndef CHILDTHRED_H
#define CHILDTHRED_H
#include <QThread>
#include <string>
using namespace std;
struct Score
{
string name;
int id;
int age;
};
class ChildThred : public QThread
{
Q_OBJECT
public:
explicit ChildThred(QObject *parent = nullptr);
protected:
void run() override;
signals:
void signal_SendToUI(Score score);
};
#endif // CHILDTHRED_H
widget.h文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "childthred.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_btnUpdate_clicked();
void showInfo(Score score);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
childthread.cpp文件:
#include "childthred.h"
#include <QDebug>
ChildThred::ChildThred(QObject *parent)
: QThread{parent}
{
//非基础类型参数注册
qRegisterMetaType<Score>("Score");
}
void ChildThred::run()
{
// qDebug() << "run thread id = " << QThread::currentThreadId();
while(1)
{
Score s;
s.name = "wanfeng";
s.id = 1001;
s.age = 27;
//发射信号
emit signal_SendToUI(s);
}
}
widget.cpp文件:
#include "widget.h"
#include "ui_widget.h"
#include "childthred.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_btnUpdate_clicked()
{
ChildThred *ch = new ChildThred();
//链接子线程的信号与UI线程设置文本框
//lamda槽函数的函数体实际运行在子线程中
// connect(ch, &ChildThred::signal_SendToUI, [=](Score s){
// string info = s.name + " id =" + to_string(s.id) + " age = " + to_string(s.age);
// ui->lineEdit->setText(QString::fromStdString(info));
// qDebug() << "slot thread id = " << QThread::currentThreadId();
// });
//槽函数的函数体实际运行在UI线程中,
//出现问题:QObject::connect: Cannot queue arguments of type 'Score'(Make sure 'Score' is registered using qRegisterMetaType().)
//涉及到非基础类型参数注册
connect(ch, &ChildThred::signal_SendToUI, this, &Widget::showInfo);
ch->start();
// qDebug() << "ui thread id = " << QThread::currentThreadId();
}
void Widget::showInfo(Score score)
{
string info = score.name + " id =" + to_string(score.id) + " age = " + to_string(score.age);
ui->lineEdit->setText(QString::fromStdString(info));
}
main.cpp文件:
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
4.非基础类型参数注册
当子线程中发射信号的参数类型为非基础类型时,需要进行非基础类型参数注册。
会出现的问题:
QObject::connect: Cannot queue arguments of type 'Score'
(Make sure 'Score' is registered using qRegisterMetaType().)
解决办法:
ChildThred::ChildThred(QObject *parent)
: QThread{parent}
{
//非基础类型参数注册
qRegisterMetaType<Score>("Score");
}
5.槽函数的参数和信号参数的关系
Qt中的槽函数与信号参数之间的关系需要遵循以下规则:
- 参数数量:槽函数的参数数量可以比它所连接的信号的参数数量少(即忽略部分信号参数),但是不能比信号的参数多。
- 参数类型:槽函数的参数类型必须与信号的参数类型完全匹配。参数类型包括基本数据类型、自定义类型、指针、引用等。
- 参数顺序:槽函数的参数顺序必须与信号的参数顺序一致。Qt的信号和槽连接是基于参数的类型和顺序的。
- 参数名称:参数的名称在信号和槽之间不需要匹配。Qt在连接信号和槽时,只关心参数的类型和顺序,不关心参数的名称。
- 可变参数:信号可以有可变参数(使用
...
语法),但是槽函数不能有可变参数。 - 默认参数:信号和槽都不能使用默认参数。这是因为信号的调用是动态的,而默认参数是编译时的概念。
- 重载的槽函数:如果一个类中有多个重载的槽函数,只有参数列表与信号完全匹配的那个槽会被连接。如果信号连接到了一个参数不匹配的槽,编译时会报错。
- 信号的重载:信号可以被重载,即同一个信号名可以对应多个具有不同参数列表的信号。但是,每个信号都必须有唯一的参数列表。
- const修饰符:信号可以是
const
的,这意味着它们可以在const
成员函数中被调用。但是,槽函数不能是const
的,因为槽可能会修改对象的状态。 - 指针和引用:信号可以传递指针或引用作为参数,槽函数也必须相应地使用指针或引用。例如,如果信号有一个
int*
参数,槽函数也必须有一个int*
参数。 - 值传递:信号和槽都支持值传递。如果信号传递一个值(例如
int
),槽函数也必须接收一个int
类型的参数。 - 信号的连接:在连接信号和槽时,Qt会检查参数的匹配性。如果参数不匹配,连接会失败,并且编译器会报错。
6.信号重名(重载)如何处理
例如:QComboBox的信号
Q_SIGNALS:
void currentIndexChanged(int index);
void currentIndexChanged(const QString &);
对于这种重载的信号,用Qt4的connect来写是没问题的,但如果使用Qt5的写法那就无法编译通过,即
connect(ui->comboBox, &QComboBox::currentIndexChanged, this, &MainWindow::on_IndexChanged);
需要使用泛型解决信号重载问题。
connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::on_IndexChanged);
示例:
.pro文件:
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++17
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
widget.h文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void onIndex(int index);
void onIndexStr(const QString& index);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp文件:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
ui->comboBox->addItem("10");
ui->comboBox->addItem("11");
ui->comboBox->addItem("12");
ui->comboBox->addItem("13");
ui->comboBox->addItem("14");
ui->comboBox->addItem("15");
ui->comboBox->addItem("16");
// connect(ui->comboBox, &QComboBox::currentTextChanged, this, &Widget::onIndex);
//使用泛型解决信号重载问题
connect(ui->comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &Widget::onIndex);
connect(ui->comboBox, QOverload<const QString&>::of(&QComboBox::currentIndexChanged),
this, &Widget::onIndexStr);
}
Widget::~Widget()
{
delete ui;
}
void Widget::onIndex(int index)
{
qDebug() << "int = " << index;
}
void Widget::onIndexStr(const QString& index)
{
qDebug() << "const QString = " << index;
}
main.cpp文件:
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
四、connect函数详解
connect函数的原型为
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(
const typename QtPrivate::FunctionPointer<Func1>::Object *sender,
Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>::Object *receiver,
Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
{
//...
}
-
sender:发送者
-
signal:信号函数
-
receiver:接收者
-
slot:槽函数
-
type:连接类型
enum ConnectionType { AutoConnection, DirectConnection, QueuedConnection, BlockingQueuedConnection, UniqueConnection = 0x80 };
-
AutoConnection:
默认的连接方式。在Qt中默认是用Qt::AutoConnection,所以平时写信号槽都是4个参数。
如果接收方在发出信号的线程中,使用Qt::DirectConnection;否则使用Qt::QueuedConnection。在发出信号时确定连接类型。
-
DirectConnection:
当发出信号时,插槽立即被调用。槽在发送信号的线程中执行。
-
QueuedConnection:
当控制返回到接收方线程的事件循环时调用槽,槽在接收方的线程中执行。
-
BlockingQueuedConnection:
与Qt::QueuedConnection相同,只是发送信号的线程会阻塞,直到槽返回。如果接收方存在于发送信号的线程中,则不能使用此连接,否则应用程序将死锁。
-
UniqueConnection:
这是一个可以使用按位OR与上述任何一种连接类型组合的标志,当Qt::UniqueConnection被设置时,如果连接已经存在,QObject::connect()将失败(例如:如果相同的信号已经连接到相同的对象对的插槽)
-
五、Qt信号槽与moc
moc全称是Meta-Object Compiler,也就是“元对象编译器”。Qt程序在交由标准编译器编译之前,先要使用moc分析C++源文件。如果它发现在一个头文件中包含了宏Q_OBJECT,则会生成另一个C++源文件,这个源文件中包含了Q_OBJECT宏的实现代码,这个新的文件名字将会是原文件名前面加上moc_构成,这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT宏就不存在了。可以这么理解,moc把Qt中一些不是C++的关键字做了解析,让C++编译器可以认识,例如:slots、signals、emit等,moc会把这些重新编译解析。
注意:这一段介绍需要熟悉,面试时可能会问到
运行moc指令,可以将带有Q_OBJECT宏的头文件进行代码翻译
D:\Project\QtProject\ch1_13_moc> moc widget.h -o moc_widget.cpp
会生成moc_widget.cpp文件。
六、C++模版技术实现Qt信号槽机制
问题:只有Qt有信号槽吗?其他项目不能使用信号槽吗?
信号槽(Signals and Slots)是Qt框架中特有的一种对象通信机制,它使得对象之间的通信更加灵活和动态。然而,信号槽机制并不是Qt独有的,其他编程语言和框架也可能有类似的模式或机制,尽管它们可能有不同的名称和实现方式
例如:sigslot-C++ Signal/Slot Libary是一个纯C++信号槽,链接为:https://sigslot.sourceforge.net/
七、Qt内存管理机制
C++派生类的构造顺序与析构顺序为
- 构造顺序:先执行基类的构造函数,再执行派生类的构造函数
- 析构顺序:先执行派生类的析构函数,在执行基类的析构函数
Qt框架使用半自动内存管理机制,这种机制结合了手动内存管理的灵活性和自动内存管理的便利性。
- QObject及其派生类的对象,如果其parent非0,那么其parent析构时会析构该对象。
- QWidget及其派生类的对象,可以设置Qt::WA_DeleteOnClose标志位,当close时会调用QWidgetPrivate::close_helper,进而调用deleteLater析构该对象
关键特点:
- 智能指针:Qt提供了智能指针类,如
QPointer<T>
和QScopedPointer<T>
,用于自动管理对象的生命周期。QPointer<T>
在对象被删除时自动将指针设置为nullptr
,而QScopedPointer<T>
则确保在其作用域结束时自动删除所指向的对象。 - 所有权系统:Qt使用所有权概念来管理内存。
QSharedPointer<T>
是一个引用计数的智能指针,当多个QSharedPointer<T>
指向同一个对象时,对象的生命周期会根据引用计数来管理。当最后一个引用被销毁时,对象也会被自动删除。 - 内存分配器:Qt提供了一个高效的内存分配器
QMalloc
,它用于分配和释放内存。这个分配器优化了内存分配和释放的性能,尤其是在频繁分配和释放小块内存时。 - 垃圾收集:Qt没有使用传统的垃圾收集机制,而是依赖于C++的析构函数来释放资源。当对象超出作用域或其引用计数降到零时,析构函数会被调用。
- 手动内存管理:虽然Qt提供了智能指针和内存分配器,但开发者仍然可以选择手动管理内存,使用
new
和delete
操作符来分配和释放内存。 - 内存泄漏检测:Qt提供了内存泄漏检测工具,如Valgrind和Qt Creator的内存检查工具,帮助开发者发现和修复内存泄漏问题。
- 对象复制和赋值:Qt的类通常使用复制构造函数和赋值运算符来管理对象的复制。开发者需要注意深拷贝和浅拷贝的区别,并适当地重载这些函数。
- 析构函数:Qt的类通常需要定义析构函数来释放资源,如关闭文件、释放动态分配的内存等。
- 资源管理:Qt鼓励使用RAII(Resource Acquisition Is Initialization)原则来管理资源,即在对象构造时获取资源,在析构时释放资源。
八、Qt中文乱码如何解决
Qt对中文的支持不是很好,使用QtCreator会出现各种乱七八糟的中文乱码问题,如何处理这种问题?
-
粘贴别人的代码时,先在记事本里“过一遍”,再贴到QtCreator;
-
使用
u8
ui->pushButton->setText(u8"你好");
-
不使用QtCreator开发,直接使用vs2019
其他设置:
-
QtCreator — 选项 — 文本编辑器 — UTF8 BOM 总是删除
-
#pragma execution_character_set("utf-8")
预处理器指令,用于指定源文件的执行字符集为UTF-8
上述方法不一定解决中文乱码问题。最好的解决办法是使用qt翻译文件,才不会出现中文乱码问题。
具体参考:https://blog.csdn.net/zwcslj/article/details/140335946?spm=1001.2014.3001.5501