天天看點

Qt開發-認清信号槽的本質

作者:QT教程

簡介

這次讨論Qt信号-槽相關的知識點。

信号-槽是Qt架構中最核心的機制,也是每個Qt開發者必須掌握的技能。

網絡上有很多介紹信号-槽的文章,也可以參考。

濤哥的專欄是《Qt進階之路》,如果連信号-槽的文章都沒有,将是沒有靈魂的。

是以這次濤哥就由淺到深地說一說信号-槽。

貓和老鼠的故事

如果一上來就講一大堆概念和定義,讀者很容易讀睡着。是以濤哥從一個故事/場景開始說起。

濤哥小時候喜歡看卡通片《貓和老鼠》, 裡面有湯姆貓(Tom)和傑瑞鼠(Jerry)鬥智鬥勇的故事。。。

Qt開發-認清信号槽的本質

現在做個簡單的設定:Tom有個技能叫”喵”,就是發出貓叫,而正在偷吃東西的Jerry,聽見貓叫聲就會逃跑。

Qt開發-認清信号槽的本質
Qt開發-認清信号槽的本質

我們嘗試用C++面向對象的思想,描述這個設定。

先是定義Tom和Jerry兩種對象

//Tom的定義

class Tom
{
public:
    //貓叫
    void Miaow() 
    {
        cout << "喵!" << endl;
    }
    //省略其它
    ... 
};
//Jerry的定義
class Jerry
{
public:
    //逃跑
    void RunAway()
    {
        cout << "那隻貓又來了,快溜!" << endl;
    }
    //省略其它
    ... 
};
           

接下來模拟場景

int main(int argc, char *argv[])
{
    //執行個體化tom
    Tom tom;

    //執行個體化jerry
    Jerry jerry;

    //tom發出叫聲
    tom.Miaow();

    //jerry逃跑
    jerry.RunAway();

    return 0;
}
           

這個場景看起來很簡單,tom發出叫聲之後手動調用了jerry的逃跑。

我們再看幾種稍微複雜的場景:

場景一:

假如jerry逃跑後過段時間,又回來偷吃東西。Tom再次發出叫聲,jerry再次逃跑。。。

這個場景要重複幾十次。我們能否實作,隻要tom的Miaow被調用了,jerry的RunAway就自動被調用,而不是每次都手動調用?

場景二:

假如jerry是藏在“廚房的櫃子裡的米袋子後面”,無法直接發現它(不能直接擷取到jerry對象,并調用它的函數)。

這種情況下,該怎麼建立 “貓叫-老鼠逃跑” 的模型?

場景三:

假如有多隻jerry,一隻tom發出叫聲時,所有jerry都逃跑。這種模型該怎麼建立?

假如有多隻tom,任意一隻發出叫聲時,所有jerry都逃跑。這種模型又該怎麼建立?

場景四:

假如不知道貓的确切品種或者名字,也不知道老鼠的品種或者名字,隻要 貓 這種動物發出叫聲,老鼠 這種動物就要逃跑。

這樣的模型又該如何建立?

還有很多場景,就不贅述了。

對象之間的通信機制

這裡概括一下要實作的功能:

要提供一種對象之間的通信機制。這種機制,要能夠給兩個不同對象中的函數建立映射關系,前者被調用時後者也能被自動調用。

再深入一些,兩個對象都互相不知道對方的存在,仍然可以建立聯系。甚至一對一的映射可以擴充到多對多,具體對象之間的映射可以擴充到抽象概念之間。

嘗試一:直接調用

應該會有人說, Miaow()的函數中直接調用RunAway()不就行了?

明顯場景二就把這種方案pass掉了。

直接調用的問題是,貓要知道老鼠有個函數/接口叫逃跑,然後主動調用了它。

這就好比Tom叫了一聲,然後Tom主動擰着Jerry的腿讓它跑。這樣是不合理的。(Jerry表示一臉懵逼!)

真實的邏輯是,貓的叫聲在空氣/媒體中傳播,傳到了老鼠的耳朵裡,老鼠就逃跑了。貓和老鼠互相都沒看見呢。

嘗試二:回調函數+映射表

似乎是可行的。

稍微思考一下,我們要做這兩件事情:

1 把RunAway函數取出來存儲在某個地方

2 建立Miaow函數和RunAway的映射關系,能夠在前者被調用時,自動調用後者。

RunAway函數可以用 函數指針|成員函數指針 或者C++11-function 來存儲,都可以稱作 “回調函數”。

(下面的代碼以C++11 function的寫法為主,函數指針的寫法稍微複雜一些,本質一樣)

我們先用一個簡單的Map來存儲映射關系, 就用一個字元串作為映射關系的名字

std::map<std::string, std::function<void()>> callbackMap;
           

我們還要實作 “建立映射關系” 和 “調用”功能,是以這裡封裝一個Connections類

class Connections 
{
public:
    //按名稱“建立映射關系”
    void connect(const std::string &name, const std::function<void()> &callback) 
    {
        m_callbackMap[name] = callback;
    }
    //按名稱“調用”
    void invok(const std::string &name)
    {
        auto it = m_callbackMap.find(name);
        //疊代器判斷
        if (it != m_callbackMap.end()) {
            //疊代器有效的情況,直接調用
            it->second();
        }
    }
private:
    std::map<std::string, std::function<void()>> m_callbackMap;
};
           

那麼這個映射關系存儲在哪裡呢? 顯然是一個Tom和Jerry共有的”上下文環境”中。

我們用一個全局變量來表示,這樣就可以簡單地模拟了:

//全局共享的Connections。
static Connections s_connections;

//Tom的定義
class Tom
{
public:
    //貓叫
    void Miaow() 
    {
        cout << "喵!" << endl;
        //調用一下名字為mouse的回調
        s_connections.invok("mouse");
    }
    //省略其它
    ... 
};
//Jerry的定義
class Jerry
{
public:
    Jerry() 
    {
        //構造函數中,建立映射關系。std::bind屬于基本用法。
        s_connections.connect("mouse", std::bind(&Jerry::RunAway, this));
    }
    //逃跑
    void RunAway()
    {
        cout << "那隻貓又來了,快溜!" << endl;
    }
    //省略其它
    ... 
};
int main(int argc, char *argv[])
{
    //模拟嵌套層級很深的場景,外部不能直接通路到tom
    struct A {
        struct B {
            struct C {
                private:
                    //Tom在很深的結構中
                    Tom tom;
                public:
                    void MiaoMiaoMiao() 
                    {
                        tom.Miaow();
                    }
            }c;
            void MiaoMiao() 
            {
                c.MiaoMiaoMiao();
            }
        }b;
        void Miao() 
        {
            b.MiaoMiao();
        }
    }a;
    //模拟嵌套層級很深的場景,外部不能直接通路到jerry
    struct D {
        struct E {
            struct F {
                private:
                    //jerry在很深的結構中
                    Jerry jerry;
            }f;
        }e;
    }d;

    //A間接調用tom的MiaoW,發出貓叫聲
    a.Miao();
    
    return 0;
}
           

RunAway沒有被直接調用,而是被自動觸發。

分析:這裡是以”mouse”這個字元串作為連接配接tom和jerry的關鍵。這隻是一種簡單、粗糙的示例實作。

觀察者模式

在GOF四人幫的書籍《設計模式》中,有一種觀察者模式,可以比較優雅地實作同樣的功能。

(順便說一下,GOF總結的設計模式一共有23種,濤哥曾經用C++11實作了全套的,github位址是:https://github.com/jaredtao/DesignPattern)

初級的觀察者模式,濤哥就不重複了。這裡濤哥用C++11搭配一點模闆技巧,實作一個更加通用的觀察者模式。

也可以叫釋出-訂閱模式。

//Subject.hpp
#pragma once
#include <vector>
#include <algorithm>

//Subject 事件或消息的主體。模闆參數為觀察者類型
template<typename ObserverType>
class Subject {
public:
    //訂閱
    void subscibe(ObserverType *obs)
    {
        auto itor = std::find(m_observerList.begin(), m_observerList.end(), obs);
        if (m_observerList.end() == itor) {
            m_observerList.push_back(obs);
        }
    }
    //取消訂閱
    void unSubscibe(ObserverType *obs)
    {
        m_observerList.erase(std::remove(m_observerList.begin(), m_observerList.end(), obs));
    }
    //釋出。這裡的模闆參數為函數類型。
    template <typename FuncType>
    void publish(FuncType func)
    {
        for (auto obs: m_observerList)
        {
            //調用回調函數,将obs作為第一個參數傳遞
            func(obs);
        }
    }
private:
    std::vector<ObserverType *> m_observerList;
};
           
//main.cpp
#include "Subject.hpp"
#include <functional>
#include <iostream>

using std::cout;
using std::endl;

//CatObserver 接口 貓的觀察者
class CatObserver {
public:
    //貓叫事件
    virtual void onMiaow() = 0;
public:
    virtual ~CatObserver() {}
};

//Tom 繼承于Subject模闆類,模闆參數為CatObserver。這樣Tom就擁有了訂閱、釋出的功能。
class Tom : public Subject<CatObserver>
{
public:
    void miaoW()
    {
        cout << "喵!" << endl;
        //釋出"貓叫"。
        //這裡取CatObserver類的成員函數指針onMiaow。而成員函數指針調用時,要傳遞一個對象的this指針才行的。
        //是以用std::bind 和 std::placeholders::_1将第一個參數 綁定為 函數被調用時的第一個參數,也就是前面Subject::publish中的obs
        publish(std::bind(&CatObserver::onMiaow, std::placeholders::_1));
    }
};
//Jerry 繼承于 CatObserver
class Jerry: public CatObserver
{
public:
    //重寫“貓叫事件”
    void onMiaow() override
    {
        //發生 “貓叫”時 調用 逃跑
        RunAway();
    }
    void RunAway()
    {
        cout << "那隻貓又來了,快溜!" << endl;
    }
};
int main(int argc, char *argv[])
{
    Tom tom;
    Jerry jerry;

    //拿jerry去訂閱Tom的 貓叫事件
    tom.subscibe(&jerry);

    tom.miaoW();
    return 0;
}
           

任意類隻要繼承Subject模闆類,提供觀察者參數,就擁有了釋出-訂閱功能。

Qt的信号-槽

信号-槽簡介

信号-槽 是Qt自定義的一種通信機制,它不同于标準C/C++ 語言。

信号-槽的使用方法,是在普通的函數聲明之前,加上signal、slot标記,然後通過connect函數把信号與槽 連接配接起來。

後續隻要調用 信号函數,就可以觸發連接配接好的信号或槽函數。

Qt開發-認清信号槽的本質

連接配接的時候,前面的是發送者,後面的是接收者。信号與信号也可以連接配接,這種情況把接收者信号看做槽即可。

信号-槽分兩種

信号-槽要分成兩種來看待,一種是同一個線程内的信号-槽,另一種是跨線程的信号-槽。

同一個線程内的信号-槽,就相當于函數調用,和前面的觀察者模式相似,隻不過信号-槽稍微有些性能損耗(這個後面細說)。

跨線程的信号-槽,在信号觸發時,發送者線程将槽函數的調用轉化成了一次“調用事件”,放入事件循環中。

接收者線程執行到下一次事件處理時,處理“調用事件”,調用相應的函數。

(關于事件循環,可以參考專欄上一篇文章《Qt實用技能3-了解事件循環》)

信号-槽的實作 元對象編譯器moc

信号-槽的實作,借助一個工具:元對象編譯器MOC(Meta Object Compiler)。

這個工具被內建在了Qt的編譯工具鍊qmake中,在開始編譯Qt工程時,會先去執行MOC,從代碼中

解析signals、slot、emit等等這些标準C/C++不存在的關鍵字,以及處理Q_OBJECT、Q_PROPERTY、

Q_INVOKABLE等相關的宏,生成一個moc_xxx.cpp的C++檔案。(使用黑魔法來變現文法糖)

比如信号函數隻要聲明、不需要自己寫實作,就是在這個moc_xxx.cpp檔案中,自動生成的。

MOC之後就是正常的C/C++編譯、連結流程了。

moc的本質-反射

MOC的本質,其實是一個反射器。标準C++沒有反射功能(将來會有),是以Qt用moc實作了反射功能。

什麼叫反射呢? 簡單來說,就是運作過程中,擷取對象的構造函數、成員函數、成員變量。

舉個例子來說明,有下面這樣一個類聲明:

class Tom {
public:
    Tom() {}
    const std::string & getName() const
    {
        return m_name;
    }
    void setName(const std::string &name) 
    {
        m_name = name;
    }
private:
    std::string m_name;
};           

類的使用者,看不到類的聲明,頭檔案都拿不到,不能直接調用類的構造函數、成員函數。

從配置檔案/網絡拿到了一段字元串“Tom”,就要建立一個Tom類的對象執行個體。

然後又拿到一段“setName”的字元串,就要去調用Tom的setName函數。

面對這種需求,就需要把Tom類的構造函數、成員函數等資訊存儲起來,還要能夠被調用到。

這些資訊就是 “元資訊”,使用者通過“元資訊”就可以“使用這個類”。這便是反射了。

設計模式中的“工廠模式”,就是一個典型的反射案例。不過工廠模式隻解決了構造函數的調用,沒有成員函數、成員變量等資訊。

【領QT開發教程學習資料,點選下方連結莬費領取↓↓,先碼住不迷路~】

點選這裡:Qt資料領取(視訊教程+文檔+代碼+項目實戰)