Qt八股-02

知识列表

  1. 如何定义一个信号?
  2. 如何连接信号和槽?
  3. Qt 中的信号和槽是如何处理线程安全的?
  4. Qt 的 QSignalMapper 是什么?它有什么用?
  5. 如何实现一个自定义信号和槽?
  6. 信号与槽的连接方式有哪些?
  7. 如何使用 lambda 表达式连接信号与槽?
  8. 如何断开信号与槽的连接?
  9. 信号与槽的性能影响如何?
  10. 如何处理信号的多重连接?
  11. 如何使用 QMetaObject::invokeMethod 调用槽?
  12. 如何实现延迟信号的发射?
  13. 如何在槽中获取信号的发送者?
  14. 如何使用 QPointer 监控对象的生命周期?

1.如何定义一个信号

在QT中,信号是类中的一部分,通常在类的signals区域声明,信号本质上是函数声明,但他们不需要实现,信号会在特定事件发生时被发射

  • Q_OBJECT 宏必须放在类定义的开头,以启用 Qt 的元对象系统。
  • signals 关键字后面定义了信号 mySignal,它带有一个整型参数。

2.如何连接信号和槽?

连接信号最常用的是使用QObject::connect函数

connect(sender , &SenderClass::siganlName , receiver , &ReceiverClass::slotName);

  • sender 是发射信号的对象。
  • signalName 是信号的名称。
  • receiver 是接收信号的对象。
  • slotName 是接收信号后调用的槽函数。

3.Qt 中的信号和槽是如何处理线程安全的?

Qt的信号与槽机制支持跨线程通信,通过自动管理线程间的数据传输和执行方式来实现线程安全

具体机制

  1. 同一线程连接(Qt::DirectConnection):槽在发射信号的线程中立即执行
  2. 跨线程连接(Qt::QueuedConnection):信号被放入接受者线程的事件处理队列中,由接收者线程处理
  3. 自动连接(Qt::AutoConnection): QT根据发送者和接收者的线程自动选择DirectQueued连接

当信号和槽在不同线程时,Qt 会自动使用 QueuedConnection,确保槽在接收者所在的线程中执行,避免数据竞争和线程安全问题。

4.Qt 的 QSignalMapper 是什么?它有什么用

QSignalMapper是QT提供的一个辅助类,用于将多个相同信号映射到携带不同参数的槽函数,从而简化信号与槽的连接

当多个对象发出相同的信号,但需要在槽中区分来源时,可以使用 QSignalMapper

#include <QSignalMapper>

// 创建信号映射器
QSignalMapper *mapper = new QSignalMapper(this);

// 假设有多个按钮
for(int i = 0; i < 5; ++i){
   QPushButton *button = new QPushButton(QString("Button %1").arg(i), this);
   connect(button, &QPushButton::clicked, mapper, static_cast<void (QSignalMapper::*)()>(&QSignalMapper::map));
   mapper->setMapping(button, i);
}

connect(mapper, SIGNAL(mapped(int)), this, SLOT(handleButton(int)));
  • 每个按钮的 clicked 信号连接到 QSignalMapper::map
  • setMapping 将每个按钮与一个整数值(标识符)关联。
  • 当按钮被点击时,handleButton(int) 槽会接收到相应的标识符,区分是哪个按钮被点击。

在 Qt 5 及以后版本,推荐使用 lambda 表达式替代 QSignalMapper,因为更简洁。

5.如何实现一个自定义信号和槽?

要实现自定义的信号和槽,需要在自定义类中声明信号和槽,并在实现文件中定义槽函数(信号不需要定义)。

  1. 声明信号和槽:#include <QObject>

    class MyObject : public QObject {
       Q_OBJECT

    public:
       MyObject(QObject *parent = nullptr) : QObject(parent) {}

    signals:
       void customSignal(int value);

    public slots:
       void customSlot(int value);
    };
  2. 实现槽函数:#include “MyObject.h”
    #include <QDebug>

    void MyObject::customSlot(int value){
       qDebug() << “Received value:” << value;
    }
  3. 发射信号:// 在类的某个成员函数中
    emit customSignal(42);
  4. 连接信号与槽:MyObject obj1, obj2;
    connect(&obj1, &MyObject::customSignal, &obj2, &MyObject::customSlot);

6.信号与槽的连接方式有哪些

  1. Qt::AutoConnection(自动连接):
    • 默认连接方式。
    • 如果发送者和接收者在同一线程中,使用 Direct 连接;否则使用 Queued 连接。
  2. Qt::DirectConnection(直接连接):
    • 槽在发射信号的线程中立即执行。
    • 适用于在同一线程内的对象。
  3. Qt::QueuedConnection(队列连接):
    • 信号被放入接收者线程的事件队列中,由接收者线程处理。
    • 适用于跨线程通信。
  4. Qt::BlockingQueuedConnection(阻塞队列连接):
    • 类似于 QueuedConnection,但发射信号的线程会阻塞,直到槽函数执行完毕。
    • 仅适用于跨线程连接。
  5. Qt::UniqueConnection(唯一连接):
    • 确保信号与槽只连接一次,防止重复连接。

connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection);

7.如何使用 lambda 表达式连接信号与槽?

从 Qt 5 开始,支持使用 C++11 的 lambda 表达式作为槽,使得代码更简洁且灵活。

connect(button, &QPushButton::clicked, this, [=]() {
   qDebug() << "Button was clicked!";
});
connect(button, &QPushButton::clicked, this, [=](bool checked){    qDebug() << "Button clicked, checked state:" << checked; });
  • 使用 lambda 表达式可以在连接时定义内联的槽逻辑,无需事先定义独立的槽函数。
  • [=] 捕获列表表示按值捕获外部变量,根据需要选择捕获方式(如 [this])。

8.如何断开信号与槽的连接?

可以使用 QObject::disconnect 方法来断开信号与槽的连接。disconnect 有多种重载方式,可以根据需要选择合适的方法。

  • 断开特定信号与槽的连接:disconnect(sender, &Sender::signal, receiver, &Receiver::slot);
  • 断开发送者的所有连接:disconnect(sender, nullptr, nullptr, nullptr);
  • 断开接收者的所有连接:disconnect(nullptr, nullptr, receiver, nullptr);
  • 断开连接后,信号再发射时,槽不会被调用。
  • 也可以在适当的时候自动管理连接,例如通过 QObject 的析构函数自动断开所有由该对象发出的连接。

9.信号与槽的性能影响如何

  1. 函数调用开销:
    • 信号与槽通过 Qt 的元对象系统管理,涉及动态查找和调用,因此比直接函数调用有更多开销。
  2. 跨线程连接:
    • 当信号跨线程连接时,需要使用事件队列传递数据,增加延迟和开销。
  3. 大量信号发射:
    • 如果短时间内频繁发射大量信号,可能会影响性能,尤其是有多个连接的情况下。

优化建议:

  • 减少不必要的连接: 仅在必要时连接信号与槽,避免过多无用的连接。
  • 使用直接连接: 在同一线程内,优先使用 DirectConnection,减少事件队列的开销。
  • 批量更新: 如果需要频繁更新,可以合并多次信号发射为一次,减少开销。
  • 选择合适的数据类型: 避免在信号中传递大型数据,可以使用指针或引用传递。
// 不推荐:频繁发射多次信号
for(int i = 0; i < 1000; ++i){
   emit updateValue(i);
}

// 推荐:合并数据后一次发射
QVector<int> values;
for(int i = 0; i < 1000; ++i){
   values.append(i);
}
emit updateValues(values);

10.如何处理信号的多重连接

多重连接指的是同一个信号连接到多个槽,或者同一个信号多次连接到同一个槽。

  1. 连接到多个槽:
    • 这是合法且常见的用法,一个信号可以触发多个槽。
    connect(sender, &Sender::signal, receiver1, &Receiver1::slotA);
    connect(sender, &Sender::signal, receiver2, &Receiver2::slotB);
  2. 避免重复连接:
    • 使用 Qt::UniqueConnection 确保同一个信号不会重复连接到同一个槽。
    connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::UniqueConnection);
  3. 检查连接状态:
    • QObject::isSignalConnected 可用于检查信号是否已连接到某个槽(需要自定义实现,因为 Qt 没有直接提供该方法)。

注意:

  • 如果不使用 Qt::UniqueConnection,相同的信号与槽多次连接会导致槽被调用多次。
  • 在设计时应确保信号与槽的连接逻辑正确,避免不必要的多重连接。

示例:

// 防止重复连接
bool wasConnected = connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::UniqueConnection);
if(!wasConnected){
   qDebug() << "Connection already exists.";
}

11.如何使用 QMetaObject::invokeMethod 调用槽

QMetaObject::invokeMethod 允许在运行时动态调用对象的槽(或其他 Q_INVOKABLE 方法),不需要在编译时知道具体的方法。

语法:

bool QMetaObject::invokeMethod(QObject *obj, const char *member, 
                              Qt::ConnectionType type = Qt::AutoConnection,
                              QGenericReturnArgument ret = QGenericReturnArgument(nullptr),
                              QGenericArgument val0 = QGenericArgument(),
                              ...);

示例:

// 假设有一个槽函数:void MyObject::processData(int value)
MyObject obj;
int value = 42;
bool success = QMetaObject::invokeMethod(&obj, "processData", Qt::DirectConnection, Q_ARG(int, value));

if(success){
   qDebug() << "Method invoked successfully.";
} else {
   qDebug() << "Failed to invoke method.";
}

说明:

  • member 是方法的名称,必须是槽或标记为 Q_INVOKABLE 的方法。
  • 参数使用 Q_ARG 宏包装,类型和顺序必须匹配。
  • invokeMethod 返回一个布尔值,表示调用是否成功。

高级用法:

  • 可以指定连接类型,如 Qt::QueuedConnection,以实现跨线程调用。
  • 支持返回值,通过 Q_RETURN_ARG 获取。

示例(带返回值):

// 假设有一个 Q_INVOKABLE 方法:int MyObject::compute(int a, int b)
MyObject obj;
int a = 5, b = 10;
int result;
bool success = QMetaObject::invokeMethod(&obj, "compute",
                   Q_RETURN_ARG(int, result),
                   Q_ARG(int, a),
                   Q_ARG(int, b));
if(success){
   qDebug() << "Result:" << result;
}

备注:

  • 使用 invokeMethod 时,方法名称是区分大小写的,且必须正确匹配参数。
  • 适用于动态调用,但性能上不如直接调用。

12.如何实现延迟信号的发射?

可以通过多种方式实现信号的延迟发射,常见的方法包括使用 QTimer 或在事件队列中排队信号。

方法一:使用 QTimer::singleShot

// 在类中
#include <QTimer>

void MyObject::emitSignalLater(int value){
   QTimer::singleShot(1000, [=]() { emit mySignal(value); }); // 延迟1000毫秒
}

说明:

  • singleShot 会在指定的毫秒数后执行给定的 lambda 表达式,进而发射信号。
  • 适用于需要在一定时间后发射信号的场景。

方法二:使用事件队列

// 在槽或函数中
QMetaObject::invokeMethod(this, "emitMySignal", Qt::QueuedConnection, Q_ARG(int, value));

void MyObject::emitMySignal(int value){
   emit mySignal(value);
}

说明:

  • 使用 Qt::QueuedConnection 将信号发射操作放入事件队列,实现一定程度的延迟(具体延迟时间依赖于事件处理速度)。
  • 适用于需要将信号发射推迟到下一轮事件循环的场景。

方法三:自定义延迟计时器

#include <QTimer>

void MyObject::emitSignalWithDelay(int value, int delayMs){
   QTimer::singleShot(delayMs, this, [=]() { emit mySignal(value); });
}

说明:

  • 通过参数化的延迟时间,可灵活控制信号发射的时机。

选择方法:

  • QTimer::singleShot 是实现延迟信号发射最常见且简单的方式,推荐优先使用。

13.如何在槽中获取信号的发送者

Qt 提供了 QObject::sender() 方法,可以在槽函数中获取发射信号的对象指针。

示例:

void MyObject::handleSignal(int value){
   QObject *obj = sender();
   if(obj){
       qDebug() << "Signal sent by:" << obj->objectName();
  }
}

说明:

  • sender() 返回发射信号的 QObject 指针。
  • sender() 在多线程环境下可能不总是可靠,因为信号可能在不同线程中排队执行。

注意事项:

  • 在使用 sender() 时,应确保槽函数是由信号直接调用的,如果槽是通过其他途径调用,sender() 可能返回 nullptr
  • 如果需要更明确的控制,可以在信号中传递发送者的信息或使用 lambda 表达式捕获上下文。

示例(通过信号参数传递发送者信息):

// 定义信号时传递 QObject* 参数
signals:
   void mySignal(int value, QObject* senderObj);

// 发射信号
emit mySignal(42, this);

// 槽函数接收发送者信息
void MyObject::handleSignal(int value, QObject* senderObj){
   qDebug() << "Signal sent by:" << senderObj->objectName();
}

说明:

  • 通过在信号中额外传递发送者信息,可以更可靠地获取发送者,尤其在复杂的连接和多线程环境中。

14.如何使用 QPointer 监控对象的生命周期

QPointer 是 Qt 提供的一个智能指针类,用于监控 QObject 对象的生命周期。当被监控的对象被销毁时,QPointer 会自动设置为 nullptr,避免悬挂指针的问题。

使用场景:

  • 当需要持有一个 QObject* 指针,并希望在对象销毁时自动检测并避免访问无效指针。

示例:

#include <QPointer>

class MyObject : public QObject {
   Q_OBJECT
public:
   MyObject(QObject *parent = nullptr) : QObject(parent) {}
};

void example(){
   QPointer<MyObject> ptr;
  {
       MyObject *obj = new MyObject();
       ptr = obj;
       qDebug() << "Pointer valid?" << (ptr != nullptr); // 输出: true
       delete obj;
  }
   qDebug() << "Pointer valid after deletion?" << (ptr != nullptr); // 输出: false
}

说明:

  • QPointer 会在所指向的对象被销毁时自动置为 nullptr
  • 使用 QPointer 可以安全地检查对象是否仍然存在,避免访问已经删除的对象。

典型用法:

  • 在信号与槽连接时,可以使用 QPointer 存储接收者对象,确保在发射信号时接收者尚存。

示例:

class Receiver : public QObject {
   Q_OBJECT
public slots:
   void handleSignal(int value){
       qDebug() << "Received value:" << value;
  }
};

class Sender : public QObject {
   Q_OBJECT
public:
   void send(){
       emit mySignal(10);
  }
signals:
   void mySignal(int);
};

void connectWithQPointer(){
   Sender sender;
   QPointer<Receiver> receiver = new Receiver();

   QObject::connect(&sender, &Sender::mySignal, receiver, &Receiver::handleSignal);

   sender.send(); // Receiver 处理信号

   delete receiver;
   receiver = nullptr;

   sender.send(); // 信号发出,但接收者已被删除,槽不会调用
}

说明:

  • Receiver 被删除后,QPointer 自动成为 nullptr,防止进一步试图调用已删除对象的槽。
  • QObject::connect 已经处理了对象删除的情况,但使用 QPointer 可以在应用逻辑中进一步确保安全性。