2.信号与槽
信号槽是 Qt 框架引以为豪的机制之一。熟练使用和理解信号槽,能够设计出解耦的非常漂亮的程序,有利于增强我们的软件设计能力。
所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,用自己的一个函数(成为槽(slot))来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)
为了体验一下信号槽的使用,我们以一段简单的代码说明:
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit);
button.show();
return a.exec();
}
//quit() 函数是 static 的,因此不需要 receiver,只需要传递槽函数(函数指针)就可以了。
在 Qt 5 中,QObject::connect()
有多个重载,每个重载的返回值都是QMetaObject::Connection
,现在我们不去关心这个返回值。下面我们先来看看connect()
函数最常用的一般形式:
connect(sender, signal,
receiver, slot);
这是我们最常用的形式。connect()
一般会使用前面四个参数,第一个是发出信号的对象,第二个是发送对象发出的信号,第三个是接收信号的对象,第四个是接收对象在接收到信号之后所需要调用的函数。也就是说,当 sender 发出了 signal 信号之后,会自动调用 receiver 的 slot 函数。
connect()
函数,sender 和 receiver 没有什么区别,都是QObject
指针;主要是 signal 和 slot 形式的区别。具体到我们的示例,我们的connect()
函数是使用的是如下重载版本:
[static] QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
//PointerToMemberFunction,看这个名字就应该知道,这是指向成员函数的指针
最后一个参数是QApplication
的 static 函数quit()
。也就是说,当我们的 button 发出了clicked()
信号时,会调用QApplication
的quit()
函数,使程序退出。
这个quit()函数在这里定义:
//qcoreapplication.h
public Q_SLOTS:
static void quit();
//QApplication继承自QGuiApplication,QGuiApplication继承自QcoreApplication
看到这里也就能发现:c++中类继承时是可以继承父类的非私有的静态成员函数的。
搜了一下博客:c++中 static 变量和函数能否被子类继承_m345376054的博客-CSDN博客_c++ static 继承
1. 父类的static变量和函数在派生类中依然可用,但是受访问性控制(比如,父类的private域中的就不可访问),而且对static变量来说,派生类和父类中的static变量是共用空间的,这点在利用static变量进行引用计数的时候要特别注意。 2. static函数没有“虚函数”一说。因为static函数实际上是“加上了访问控制的全局函数”,全局函数哪来的什么虚函数? 3. 派生类的friend函数可以访问派生类本身的一切变量,包括从父类继承下来的protected域中的变量。但是对父类来说,他并不是friend的。
继而又引出一个问题:为什么虚函数(virtual)不能是static函数。搜索到:为什么虚函数(virtual)不能是static函数_做自己喜欢的事是多么幸福的一件事呀!-CSDN博客
简而言之,成员函数实例相关,静态函数类相关。
虚函数,是一种特殊的成员函数,用来实现运行时多态。
-
静态成员函数,可以不通过对象来调用,没有隐藏的this指针。
-
virtual函数一定要通过对象来调用,有隐藏的this指针。
所以,关键问题是static成员没有this指针。
static function 是静态决议(编译的时候就绑定了)
而virtual function 是动态决议的(运行时才绑定)
引用stackoverflow网友@Kerrek SB 的回答:
That would make no sense. The point of virtual member functions is that they are dispatched based on the dynamic type of the object instance on which they are called. On the other hand, static functions are not related to any instances and are rather a property of the class. Thus it makes no sense for them to be virtual. If you must, you can use a non-static dispatcher.
即是说:virtual成员函数的关键是动态类型绑定的实例调用。然而,静态函数和任何类的实例都不相关,它是class的属性。
信号槽要求信号和槽的参数一致,所谓一致,是参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。这是因为,你可以在槽函数中选择忽略信号传来的数据(也就是槽函数的参数比信号的少),但是不能说信号根本没有这个数据,你就要在槽函数中使用(就是槽函数的参数比信号的多,这是不允许的)。
如果信号槽不符合,或者根本找不到这个信号或者槽函数的话,比如我们改成:
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit2);
由于 QApplication 没有 quit2 这样的函数的,因此在编译时,会有编译错误:
'quit2' is not a member of QApplication
这样,使用成员函数指针,我们就不会担心在编写信号槽的时候会出现函数错误。(备注:运算符的优先级中域解析符::是最高级)
借助 Qt 5 的信号槽语法,我们可以将一个对象的信号连接到 Lambda 表达式,例如:
#include <QApplication>
#include <QPushButton>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked, [](bool) {
qDebug() << "You clicked me!";
});
button.show();
return a.exec();
}
注意这里的 Lambda 表达式接收一个 bool 参数,这是因为QPushButton
的clicked()
信号实际上是有一个参数的。找了一下在这个里面:
//qabstractbutton.h
Q_SIGNALS:
void pressed();
void released();
void clicked(bool checked = false);
void toggled(bool checked);
//QPushButton : public QAbstractButton
小知识tip:
为什么给指向对象的函数成员的指针赋值要用&取址符呢?
函数名可以作为函数的地址,但这是有前提条件的,从函数到指针的隐式转换是函数名在表达式中的行为,这个转换仅在表达式中才会发生,这只是函数名众多性质中的一个,而非本质,函数名的本质是函数实体的代表。
对于C++,规定非静态成员函数的左值不可获得,因此非静态成员函数不存在隐式左值转换,即不存在像常规函数那样的从函数到指针的隐式转换,所以必须在非静态成员函数前使用&操作符才能获得地址。
这就是为啥当你不写取地址符&时,编译器会提示你:call to non-static member function without an object argument.
结束语
该系列主要参考豆子大佬的博客:《Qt 学习之路 2》目录 - DevBean Tech World。