Inside Qt Series (全集,共十六篇,不同版本的Qt有不同的實現)


Inside Qt 系列

QObject這個 class 是 QT 對象模型的核心,絕大部分的 QT 類都是從這個類繼承而來。這個模型的中心特征就是一個叫做信號和槽(signaland slot)的機制來實現對象間的通訊,你可以把一個信號和另一個槽通過 connect(…) 方法連接起來,並可以使用 disconnect(…) 方法來斷開這種連接,你還可以通過調用blockSignal(…) 這個方法來臨時的阻塞信號,QObject 把它們自己組織在對象樹中。當你創建一個 QObject 並使用其它對象作為父對象時,這個對象會自動添加到父對象的children() list 中。父對象擁有這個對象,比如,它將在它的析構函數中自動刪除它所有的 child對象。你可以通過 findChild() 或者findChildren()函數來查找一個對象。每個對象都有一個對象名稱(objectName())和類名稱(class name), 他們都可以通過相應的 metaObject 對象來獲得。你還可以通過 inherits() 方法來判斷一個對象的類是不是從另一個類繼承而來。當對象被刪除時,它發出destroyed()信號。你可以捕獲這個信號來避免對QObject的無效引用。QObject可以通過event()接收事件並且過濾其它對象的事件。詳細情況請參考installEventFilter()和eventFilter()。對於每一個實現了信號、槽和屬性的對象來說,Q_OBJECT 宏都是必須要加上的。QObject 實現了這么多功能,那么,它是如何做到的呢?讓我們通過它的 Source Code 來解開這個秘密吧。

QObject 類的實現文件一共有四個:
* qobject.h,QObject class 的基本定義,也是我們一般定義一個類的頭文件
* qobject.cpp,QObject class 的實現代碼基本上都在這個文件
* qobjectdefs.h,這個文件中最重要的東西就是定義了 QMetaObject class,這個class是為了實現 signal、slot、properties,的核心部分。
* qobject_p.h,這個文件中的 code 是輔助實現QObject class 的,這里面最重要的東西是定義了一個 QObjectPrivate 類來存儲 QOjbect 對象的成員數據。

理解這個 QObjectPrivate class 又是我們理解 QT kernel source code 的基礎,這個對象包含了每一個 QT 對象中的數據成員,好了,讓我們首先從理解 QObject 的數據存儲代碼開始我么的 QT Kernel Source Code 之旅。

我們知道,在C++中,幾乎每一個類(class)中都需要有一些類的成員變量(class member variable),在通常情況下的做法如下:

 

  1.  
    class Person
  2.  
    {
  3.  
    private:
  4.  
    string mszName; // 姓名
  5.  
    bool mbSex; // 性別
  6.  
    int mnAge; // 年齡
  7.  
    };

 

 

就是在類定義的時候,直接把類成員變量定義在這里,甚至於,把這些成員變量的存取范圍直接定義成是 public 的,您是不是這是這樣做的呢?

在QT中,卻幾乎都不是這樣做的,那么,QT是怎么做的呢?

幾乎每一個C++的類中都會保存許多的數據,要想讀懂別人寫的C++代碼,就一定需要知道每一個類的的數據是如何存儲的,是什么含義,否則,我們不可能讀懂別人的C++代碼。在這里也就是說,要想讀懂QT的代碼,第一步就必須先搞清楚QT的類成員數據是如何保存的。

為了更容易理解QT是如何定義類成員變量的,我們先說一下QT 2.x 版本中的類成員變量定義方法,因為在 2.x 中的方法非常容易理解。然后在介紹 QT 4.4 中的類成員變量定義方法。

QT 2.x 中的方法

在定義class的時候(在.h文件中),只包含有一個類成員變量,只是定義一個成員數據指針,然后由這個指針指向一個數據成員對象,這個數據成員對象包含所有這個class的成員數據,然后在class的實現文件(.cpp文件)中,定義這個私有數據成員對象。示例代碼如下:

 

  1.  
    // File name: person.h
  2.  
    struct PersonalDataPrivate; // 聲明私有數據成員類型
  3.  
    class Person
  4.  
    {
  5.  
    public:
  6.  
    Person (); // constructor
  7.  
    virtual ~Person (); // destructor
  8.  
    void setAge(const int);
  9.  
    int getAge();
  10.  
    private:
  11.  
    PersonalDataPrivate* d;
  12.  
    };
  13.  
    //---------------------------------------------------------------------
  14.  
    // File name: person.cpp
  15.  
    struct PersonalDataPrivate // 定義私有數據成員類型
  16.  
    {
  17.  
    string mszName; // 姓名
  18.  
    bool mbSex; // 性別
  19.  
    int mnAge; // 年齡
  20.  
    };
  21.  
     
  22.  
    // constructor
  23.  
    Person::Person ()
  24.  
    {
  25.  
    d = new PersonalDataPrivate;
  26.  
    };
  27.  
     
  28.  
    // destructor
  29.  
    Person::~Person ()
  30.  
    {
  31.  
    delete d;
  32.  
    };
  33.  
     
  34.  
    void Person::setAge(const int age)
  35.  
    {
  36.  
    if (age != d->mnAge)
  37.  
    d->mnAge = age;
  38.  
    }
  39.  
     
  40.  
    int Person::getAge()
  41.  
    {
  42.  
    return d->mnAge;
  43.  
    }

 

 

在最初學習QT的時候,我也覺得這種方法很麻煩,但是隨着使用的增多,我開始很喜歡這個方法了,而且,現在我寫的代碼,基本上都會用這種方法。具體說來,它有如下優點:

* 減少頭文件的依賴性
把具體的數據成員都放到cpp文件中去,這樣,在需要修改數據成員的時候,只需要改cpp文件而不需要頭文件,這樣就可以避免一次因為頭文件的修改而導致所有包含了這個文件的文件全部重新編譯一次,尤其是當這個頭文件是非常底層的頭文件和項目非常龐大的時候,優勢明顯。
同時,也減少了這個頭文件對其它頭文件的依賴性。可以把只在數據成員中需要用到的在cpp文件中include一次就可以,在頭文件中就可以盡可能的減少include語句
* 增強類的封裝性
這種方法增強了類的封裝性,無法再直接存取類成員變量,而必須寫相應的 get/set 成員函數來做這些事情。
關於這個問題,仁者見仁,智者見智,每個人都有不同的觀點。有些人就是喜歡把類成員變量都定義成public的,在使用的時候方便。只是我個人不喜歡這種方法,當項目變得很大的時候,有非常多的人一起在做這個項目的時候,自己所寫的代碼處於底層有非常多的人需要使用(#include)的時候,這個方法的弊端就充分的體現出來了。

還有,我不喜歡 QT 2.x 中把數據成員的變量名都定義成只有一個字母,d,看起來很不直觀,尤其是在search的時候,很不方便。但是,QT kernel 中的確就是這么干的。

QT 4.4.x 中的方法

在 QT 4.4 中,類成員變量定義方法的出發點沒有變化,只是在具體的實現手段上發生了非常大的變化,下面具體來看。

在 QT 4.4 中,使用了非常多的宏來做事,這憑空的增加了理解 QT source code 的難度,不知道他們是不是從MFC學來的。就連在定義類成員數據變量這件事情上,也大量的使用了宏。

在這個版本中,類成員變量不再是給每一個class都定義一個私有的成員,而是把這一項common的工作放到了最基礎的基類 QObject 中,然后定義了一些相關的方法來存取,好了,讓我們進入具體的代碼吧。

 

  1.  
    // file name: qobject.h
  2.  
     
  3.  
    class QObjectData
  4.  
    {
  5.  
    public:
  6.  
    virtual ~QObjectData() = 0;
  7.  
    // 省略
  8.  
    };
  9.  
     
  10.  
    class QObject
  11.  
    {
  12.  
    Q_DECLARE_PRIVATE(QObject)
  13.  
     
  14.  
    public:
  15.  
     
  16.  
    QObject(QObject *parent= 0);
  17.  
     
  18.  
    protected:
  19.  
     
  20.  
    QObject(QObjectPrivate &dd, QObject *parent = 0);
  21.  
    QObjectData *d_ptr;
  22.  
    }

 

這些代碼就是在 qobject.h 這個頭文件中的。在 QObject class 的定義中,我們看到,數據員的定義為:QObjectData*d_ptr; 定義成 protected 類型的就是要讓所有的派生類都可以存取這個變量,而在外部卻不可以直接存取這個變量。而 QObjectData 的定義卻放在了這個頭文件中,其目的就是為了要所有從QObject繼承出來的類的成員變量也都相應的要在QObjectData這個class繼承出 來。而純虛的析構函數又決定了兩件事:

* 這個class不能直接被實例化。換句話說就是,如果你寫了這么一行代碼,new QObjectData, 這行代碼一定會出錯,compile的時候是無法過關的。
* 當 delete 這個指針變量的時候,這個指針變量是指向的任意從QObjectData繼承出來的對象的時候,這個對象都能被正確delete,而不會產生錯誤,諸如,內存泄漏之類的。

我們再來看看這個宏做了什么,Q_DECLARE_PRIVATE(QObject)

 

  1.  
    #define Q_DECLARE_PRIVATE(Class) \
  2.  
    inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr)); } \
  3.  
    inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr)); } \
  4.  
    friend class Class##Private;

 

這個宏主要是定義了兩個重載的函數,d_func(),作用就是把在QObject這個class中定義的數據成員變量d_ptr安全的轉換成為每一個具 體的class的數據成員類型指針。我們看一下在QObject這個class中,這個宏展開之后的情況,就一幕了然了。

Q_DECLARE_PRIVATE(QObject)展開后,就是下面的代碼:

  1.  
    inline QObjectPrivate* d_func() { return reinterpret_cast<QObjectPrivate *>(d_ptr); }
  2.  
    inline const QObjectPrivate* d_func() const
  3.  
    { return reinterpret_cast<const QObjectPrivate *>;(d_ptr); } \
  4.  
    friend class QObjectPrivate;

 

宏展開之后,新的問題又來了,這個QObjectPrivate是從哪里來的?在QObject這個class中,為什么不直接使用QObjectData來數據成員變量的類型?

還記得我們剛才說過嗎,QObjectData這個class的析構函數的純虛函數,這就說明這個class是不能實例化的,所以,QObject這個class的成員變量的實際類型,這是從QObjectData繼承出來的,它就是QObjectPrivate !

這個 class 中保存了許多非常重要而且有趣的東西,其中包括 QT 最核心的 signal 和slot 的數據,屬性數據,等等,我們將會在后面詳細講解,現在我們來看一下它的定義:

下面就是這個class的定義:

  1.  
    class QObjectPrivate : public QObjectData
  2.  
    {
  3.  
    Q_DECLARE_PUBLIC(QObject)
  4.  
     
  5.  
    public:
  6.  
     
  7.  
    QObjectPrivate( int version = QObjectPrivateVersion);
  8.  
    virtual ~QObjectPrivate();
  9.  
    // 省略
  10.  
    }

 

那么,這個 QObjectPrivate 和 QObject 是什么關系呢?他們是如何關聯在一起的呢?

接上節,讓我們來看看這個 QObjectPrivate 和 QObject 是如何關聯在一起的。

  1.  
    // file name: qobject.cpp
  2.  
     
  3.  
    QObject::QObject(QObject *parent)
  4.  
    : d_ptr( new QObjectPrivate)
  5.  
    {
  6.  
    // ………………………
  7.  
    }
  8.  
     
  9.  
    QObject::QObject(QObjectPrivate &dd, QObject *parent)
  10.  
    : d_ptr(&dd)
  11.  
    {
  12.  
    // …………………
  13.  
    }

 

怎么樣,是不是一目了然呀?

從第一個構造函數可以很清楚的看出來,QObject class 中的 d_ptr 指針將指向一個 QObjectPrivate 的對象,而QObjectPrivate這個class是從QObjectData繼承出來的。

這第二個構造函數干什么用的呢?從 QObject class 的定義中,我們可以看到,這第二個構造函數是被定義為protected 類型的,這說明,這個構造函數只能被繼承的class使用,而不能使用這個構造函數來直接構造一個QObject對象,也就是說,如果寫一條下面的語句, 編譯的時候是會失敗的,

 

new QObject(*new QObjectPrivate, NULL);

為了看的更清楚,我們以QWidget這個class為例說明。

QWidget是QT中所有UI控件的基類,它直接從QObject繼承而來,

 

  1.  
    class QWidget : public QObject, public QPaintDevice
  2.  
    {
  3.  
    Q_OBJECT
  4.  
    Q_DECLARE_PRIVATE(QWidget)
  5.  
    // .....................
  6.  
    }
  7.  
     

 

我們看一個這個class的構造函數的代碼:

  1.  
    QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)
  2.  
    : QObject(* new QWidgetPrivate, 0), QPaintDevice()
  3.  
    {
  4.  
    d_func()->init(parent, f);
  5.  
    }

 

非常清楚,它調用了基類QObject的保護類型的構造函數,並且以 *new QWidgetPrivate 作為第一個參數傳遞進去。也就是說,基類(QObject)中的d_ptr指針將會指向一個QWidgetPrivate類型的對象。

再看QWidgetPrivate這個class的定義:

  1.  
    class QWidgetPrivate : public QObjectPrivate
  2.  
    {
  3.  
    Q_DECLARE_PUBLIC(QWidget)
  4.  
    // .....................
  5.  
    };

 

好了,這就把所有的事情都串聯起來了。

關於QWidget構造函數中的唯一的語句 d_func()->init(parent, f) 我們注意到在class的定義中有這么一句話: Q_DECLARE_PRIVATE(QWidget)

我們前面講過這個宏,當把這個宏展開之后,就是這樣的:

  1.  
    inline QWidgetPrivate* d_func() { return reinterpret_cast<QWidgetPrivate *>(d_ptr); }
  2.  
    inline const QWidgetPrivate* d_func() const
  3.  
    { return reinterpret_cast<const QWidgetPrivate *>(d_ptr); } \
  4.  
    friend class QWidgetPrivate;

很清楚,它就是把QObject中定義的d_ptr指針轉換為QWidgetPrivate類型的指針。

小結:

要理解QT Kernel的code,就必須要知道QT中每一個Object內部的數據是如何保存的,而QT沒有象我們平時寫code一樣,把所有的變量直接定義在類 中,所以,不搞清楚這個問題,我們就無法理解一個相應的class。其實,在QT4.4中的類成員數據的保存方法在本質是與QT2.x中的是一樣的,就是 在class中定義一個成員數據的指針,指向成員數據集合對象(這里是一個QObjectData或者是其派生類)。初始化這個成員變量的辦法是定義一個 保護類型的構造函數,然后在派生類的構造函數new一個派生類的數據成員,並將這個新對象賦值給QObject的數據指針。在使用的時候,通過預先定義個宏里面的一個inline函數來把數據指針在安全類 型轉換,就可以使用了。

從本節開始,我們講解 QT Meta-Object System 的功能,以及實現。

在使用 Qt 開發的過程中,大量的使用了 signal 和 slot. 比如,響應一個 button 的 click 事件,我們一般都寫如下的代碼:

  1.  
    class MyWindow : public QWidget
  2.  
    {
  3.  
    Q_OBJECT
  4.  
    public:
  5.  
    MyWindow(QWidget* parent) : QWidget(parent)
  6.  
    {
  7.  
    QPushButton* btnStart = new QPushButton(“start”, this);
  8.  
    connect(btnStart, SIGNAL(clicked()), SLOT(slotStartClicked()));
  9.  
    }
  10.  
     
  11.  
    private slots:
  12.  
    void slotStartClicked();
  13.  
    };
  14.  
     
  15.  
    void MyWindow:: slotStartClicked()
  16.  
    {
  17.  
    // 省略
  18.  
    }

 

在這段代碼中,我們把 btnStart 這個 button 的clicked() 信號和 MyWindow 的 slotStartClicked() 這個槽相連接,當 btnStart 這個 button 被用戶按下(click)的時候,就會發出一個 clicked() 的信號,然后,MyWindow:: slotStartClicked() 這個 slot 函數就會被調用用來響應 button 的 click 事件。

這段代碼是最為典型的 signal/slot 的應用實例,在實際的工作過程中,signal/slot 還有更為廣泛的應用。准確的說,signal/slot 是QT提供的一種在對象間進行通訊的技術,那么,這個技術在QT 中是如何實現的呢?

這就是 QT 中的元對象系統(MetaObject System)的作用,為了更好的理解它,讓我先來對它的功能做一個回顧,讓我們一起來揭開它神秘的面紗。

Meta-Object System 的基本功能

Meta Object System 的設計基於以下幾個基礎設施:

* QObject 類
作為每一個需要利用元對象系統的類的基類
* Q_OBJECT 宏,
定義在每一個類的私有數據段,用來啟用元對象功能,比如,動態屬性,信號和槽
* 元對象編譯器moc (the Meta Object Complier),
moc 分析C++源文件,如果它發現在一個頭文件(headerfile)中包含Q_OBJECT 宏定義,然后動態的生成另外一個C++源文件,這個新的源文件包含 Q_OBJECT 的實現代碼,這個新的 C++ 源文件也會被編譯、鏈接到這個類的二進制代碼中去,因為它也是這個類的完整的一部分。通常,這個新的C++ 源文件會在以前的C++ 源文件名前面加上 moc_ 作為新文件的文件名。其具體過程如下圖所示:

 

除了提供在對象間進行通訊的機制外,元對象系統還包含以下幾種功能:

* QObject::metaObject() 方法
它獲得與一個類相關聯的 meta-object
* QMetaObject::className() 方法
在運行期間返回一個對象的類名,它不需要本地C++編譯器的RTTI(run-timetype information)支持
* QObject::inherits() 方法
它用來判斷生成一個對象類是不是從一個特定的類繼承出來,當然,這必須是在QObject類的直接或者間接派生類當中
* QObject::tr() and QObject::trUtf8()
這兩個方法為軟件的國際化翻譯字符串
* QObject::setProperty() and QObject::property()
這兩個方法根據屬性名動態的設置和獲取屬性值

除了以上這些功能外,它還使用qobject_cast()方法在QObject類之間提供動態轉換,qobject_cast()方法的功能類似於標准 C++的dynamic_cast(),但是qobject_cast()不需要RTTI的支持,在一個QObject類或者它的派生類中,我們可以不定 義Q_OBJECT宏。如果我們在一個類中沒有定義Q_OBJECT宏,那么在這里所提到的相應的功能在這個類中也不能使用,從meta-object的觀點來說,一個沒有定義Q_OBJECT宏的類與它最接近的那個祖先類是相同的,那就是所,QMetaObject::className() 方法所返回的名字並不是這個類的名字,而是與它最接近的那個祖先類的名字。所以,我們強烈建議,任何從QObject繼承出來的類都定義Q_OBJECT 宏。

下一節,我們來了解另一個重要的工具:Meta-Object Compiler

元對象編譯器用來處理QT 的C++擴展,moc 分析C++源文件,如果它發現在一個頭文件(header file)中包含Q_OBJECT 宏定義,然后動態的生成另外一個C++源文件,這個新的源文件包含 Q_OBJECT 的實現代碼,這個新的 C++ 源文件也會被編譯、鏈接到這個類的二進制代碼中去,因為它也是這個類的完整的一部分。通常,這個新的C++ 源文件會在以前的C++ 源文件名前面加上 moc_ 作為新文件的文件名。

如果使用qmake工具來生成Makefile文件,所有需要使用moc的編譯規則都會給自動的包含到Makefile文件中,所以對程序員來說不需要直接的使用moc

除了處理信號和槽之外,moc還處理屬性信息,Q_PROPERTY()宏定義類的屬性信息,而Q_ENUMS()宏則定義在一個類中的枚舉類型列表。 Q_FLAGS()宏定義在一個類中的flag枚舉類型列表,Q_CLASSINFO()宏則允許你在一個類的meta信息中插入name/value 對。

由moc所生成的文件必須被編譯和鏈接,就象你自己寫的另外一個C++文件一樣,否則,在鏈接的過程中就會失敗。

Code example:

  1.  
    class MyClass : public QObject
  2.  
    {
  3.  
    Q_OBJECT
  4.  
    Q_PROPERTY(Priority priority READ priority WRITE setPriority)
  5.  
    Q_ENUMS(Priority)
  6.  
    Q_CLASSINFO("Author", "Oscar Peterson")
  7.  
    Q_CLASSINFO("Status", "Active")
  8.  
     
  9.  
    public:
  10.  
    enum Priority { High, Low, VeryHigh, VeryLow };
  11.  
     
  12.  
    MyClass(QObject *parent = 0);
  13.  
    virtual ~MyClass();
  14.  
     
  15.  
    void setPriority(Priority priority);
  16.  
    Priority priority() const;
  17.  
    };

 

本節介紹Signal和slot的基本知識。

信號和 槽是用來在對象間通訊的方法,當一個特定事件發生的時候,signal會被 emit 出來,slot 調用是用來響應相應的 signal 的。QT 對象已經包含了許多預定義的 signal,但我們總是可以在派生類中添加新的 signal。QT 對象中也已經包含了許多預定義的 slot,但我們可以在派生類中添加新的 slot 來處理我們感興趣的 signal.

signal 和 slot 機制是類型安全的,signal 和 slot必須互相匹配(實際上,一個solt的參數可以比對應的signal的參數少,因為它可以忽略多余的參數)。signal 和 slot是松散的配對關系,發出signal的對象不關心是那個對象鏈接了 這個signal,也不關心是那個或者有多少slot鏈接到了這個 signal。QT的signal 和 slot機制保證了,如果一個signal和slot相鏈接,slot會在正確的時機被調用,並且是使用正確的參數。Signal和slot都可以攜帶任 何數量和類型的參數,他們都是類型安全的。

所有從QObject直接或者間接繼承出來的類都能包含信號和槽,當一個對象的狀態發生變化的時候,信號就可以被emit出來,這可能是某個其它的對象所 關心的。這個對象並不關心有那個對象或者多少個對象鏈接到這個信號了,這是真實的信息封裝,它保證了這個對象可以作為一個軟件組件來被使用。

槽(slot)是用來接收信號的,但同時他們也是一個普通的類成員函數,就象一個對象不關心有多少個槽鏈接到了它的某個信號,一個對象也不關心一個槽鏈接了多少個信號。這保證了用QT創建的對象是一個真實的獨立的軟件組件。

一個信號可以鏈接到多個槽,一個槽也可以鏈接多個信號。同時,一個信號也可以鏈接到另外一個信號。所有使用了信號和槽的類都必須包含 Q_OBJECT 宏,而且這個類必須從QObject類派生(直接或者間接派生)出來,

當一個signal被emit出來的時候,鏈接到這個signal的slot會立刻被調用,就好像是一個函數調用一樣。當這件事情發生的時候,signal和slot機制與GUI的事件循環完全沒有關系,當所有鏈接到這個signal的slot執行完成之后,在 emit 代碼行之后的代碼會立刻被執行。當有多個slot鏈接到一個signal的時候,這些slot會一個接着一個的、以隨機的順序被執行。

Signal 代碼會由 moc 自動生成,開發人員一定不能在自己的C++代碼中實現它,並且,它永遠都不能有返回值。Slot其實就是一個普通的類函數,並且可以被直接調用,唯一特殊的地方是它可以與signal相鏈接。C++的預處理器更改或者刪除 signal, slot, emit 關鍵字,所以,對於C++編譯器來說,它處理的是標准的C++源文件。


 

如下圖所示:假定 QPushButton 的 signal clicked() 已經和 QLineEdit 的 slot clear() 連接成功,那么當 QPushButton 的 clicked() signal 被 emit 出來的時候,QLineEdit 的 clear() slot 就會被調用。

 

前面我們介紹了 Meta Object 的基本功能,和它支持的最重要的特性之一:Signal & Slot的基本功能。現在讓我們來進入 Meta Object 的內部,看看它是如何支持這些能力的。

Meta Object 的所有數據和方法都封裝在一個叫QMetaObject 的類中。它包含並且可以查詢一個Qt類的 meta 信息,meta信息包含以下幾種:
* 信號表(signal table),其中有這個對應的 QT類的所有Signal的名字
* 槽表(slot table),其中有這個對應的QT類中的所有Slot的名字。
* 類信息表(class info table),包含這個QT類的類型信息
* 屬性表(property table),其中有這個對應的QT類中的所有屬性的名字。
* 指向parent meta object的指針(pointersto parent meta object)

請參考下圖, Qt Meta Data Tables:


 

QMetaObject 對象與 QT 類之間的關系:

* 每一個 QMetaObject 對象包含了與之相對應的一個 QT 類的元信息
* 每一個 QT 類(QObject 以及它的派生類) 都有一個與之相關聯的靜態的(static) QMetaObject 對象(注:class的定義中必須有 Q_OBJECT 宏,否則就沒有這個Meta Object)
* 每一個 QMetaObject 對象保存了與它相對應的QT 類的父類的 QMetaObject 對象的指針。  或者,我們可以這樣說:“每一個QMetaObject對象都保存了一個其父親(parent)的指針”.注意:嚴格來說,這種說法是不正確的,最起碼 是不嚴謹的。

請參考下圖,Qt Meta Class 與 Qt class 之間的對應關系:

Q_OBJECT宏

Meta Object 的功能實現,這個宏立下了汗馬功勞。首先,讓我們來看看這個宏是如何定義的:

  1.  
    #define Q_OBJECT \
  2.  
    public: \
  3.  
    Q_OBJECT_CHECK \
  4.  
    static const QMetaObject staticMetaObject; \
  5.  
    virtual const QMetaObject *metaObject() const; \
  6.  
    virtual void *qt_metacast(const char *); \
  7.  
    QT_TR_FUNCTIONS \
  8.  
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
  9.  
    private:

 

這里,我們先忽略Q_OBJECT_CHECK 和QT_TR_FUNCTIONS 這兩個宏。

我們看到,首先定義了一個靜態類型的類變量staticMetaObject,然后有一個獲取這個對象指針的方法metaObject()。這里最重要的 就是類變量staticMetaObject 的定義。這說明所有的 QObject 的對象都會共享這一個staticMetaObject 類變量,靠它來完成所有信號和槽的功能,所以我們就有必要來仔細的看看它是怎么回事了。

我們來看一下QMetaObject的定義,我們先看一下QMetaObject對象中包含的成員數據。

 

 
        
  1.  
    struct Q_CORE_EXPORT QMetaObject
  2.  
    {
  3.  
    // ......
  4.  
    struct { // private data
  5.  
    const QMetaObject *superdata;
  6.  
    const char *stringdata;
  7.  
    const uint *data;
  8.  
    const void *extradata;
  9.  
    } d;
};

 

 

上面的代碼就是QMetaObject類所定義的全部數據成員。就是這些成員記錄了所有signal,slot,property,class information這么多的信息。下面讓我們來逐一解釋這些成員變量:

const QMetaObject*superdata:
這個變量指向與之對應的QObject類的父類,或者是祖先類的QMetaObject對象。

如何理解這一句話呢?我們知道,每一個QMetaObject對象,一定有一個與之相對應的QObject類(或者由其直接或間接派生出的子類),注意:這里是類,不是對象。

那么每一個QObject類(或其派生類)可能有一個父類,或者父類的父類,或者很多的繼承層次之前的祖先類。或者沒有父類(QObject)。那么 superdata 這個變量就是指向與其最接近的祖先類中的QMetaObject對象。對於QObject類QMetaObject對象來說,這是一個NULL指針,因為 QObject沒有父類。

下面,讓我們來舉例說明:

 

  1.  
    class Animal : public QObject
  2.  
    {
  3.  
    Q_OBJECT
  4.  
    //.............
  5.  
    };
  6.  
     
  7.  
    class Cat : public Animal
  8.  
    {
  9.  
    Q_OBJECT
  10.  
    //.............
  11.  
    }


那么,Cat::staticMetaObject.d.superdata這個指針變量指向的對象是Animal::staticMetaObject
而Animal::staticMetaObject.d.superdata 這個指針變量指向的對象是QObject::staticMetaObject.

而 QObject::staticMetaObject.d.superdat 這個指針變量的值為 NULL。

但如果我們把上面class的定義修改為下面的定義,就不一樣了:

 

  1.  
    class Animal : public QObject
  2.  
    {
  3.  
    // Q_OBJECT,這個 class 不定義這個
  4.  
    //.............
  5.  
    };
  6.  
     
  7.  
    class Cat : public Animal
  8.  
    {
  9.  
    Q_OBJECT
  10.  
    //.............
  11.  
    }

 

那么,Cat::staticMetaObject.d.superdata 這個指針變量指向的對象是 QObject::staticMetaObject
因為 Animal::staticMetaObject 這個對象是不存在的。

const char *stringdata:

顧名思義,這是一個指向string data的指針。但它和我們平時所使用的一般的字符串指針卻很不一樣,我們平時使用的字符串指針只是指向一個字符串的指針,而這個指針卻指向的是很多個字符串。那么它不就是字符串數組嗎?哈哈,也不是。因為C++的字符串數組要求數組中的每一個字符串擁有相同的長度,這樣才能組成一個數組。那它是不是一個字符串指針數組呢?也不是,那它到底是什么呢?讓我們來看一看它的具體值,還是讓我們以QObject這個class的QMetaObject為例來說明 吧。

下面是QObject::staticMetaObject.d.stringdata指針所指向的多個字符串數組,其實它就是指向一個連續的內存區,而這個連續的內存區中保存了若干個字符串。

 

  1.  
    static const char qt_meta_stringdata_QObject[] =
  2.  
    {
  3.  
    "QObject\0\0destroyed(QObject*)\0destroyed()\0"
  4.  
    "deleteLater()\0_q_reregisterTimers(void*)\0"
  5.  
    "QString\0objectName\0parent\0QObject(QObject*)\0"
  6.  
    "QObject()\0"
  7.  
    };

這個字符串都是些什么內容呀?有,Class Name, Signal Name,Slot Name, Property Name。看到這些大家是不是覺得很熟悉呀,對啦,他們就是MetaSystem所支持的最核心的功能屬性了。

既然他們都是不等長的字符串,那么Qt是如何來索引這些字符串,以便於在需要的時候能正確的找到他們呢?第三個成員正式登場了。

const uint *data;

這個指針本質上就是指向一個正整數數組,只不過在不同的object中數組的長度都不盡相同,這取決於與之相對應的class中定義了多少signal,slot,property。

這個整數數組的的值,有一部分指出了前一個變量(stringdata)中不同字符串的索引值,但是這里有一點需要注意的是,這里面的數值並不是直接標明了每一個字符串的索引值,這個數值還需要通過一個相應的算法計算之后,才能獲得正確的字符串的索引值。

下面是QObject::staticMetaObject.d.data指針所指向的正整數數組的值。

  1.  
    1. static const uint qt_meta_data_QObject[] =
  2.  
    2. {
  3.  
    3. // content:
  4.  
    4. 2, // revision
  5.  
    5. 0, // classname
  6.  
    6. 0, 0, // classinfo
  7.  
    7. 4, 12, // methods
  8.  
    8. 1, 32, // properties
  9.  
    9. 0, 0, // enums/sets
  10.  
    10. 2, 35, // constructors
  11.  
    11.
  12.  
    12. // signals: signature, parameters, type, tag, flags
  13.  
    13. 9, 8, 8, 8, 0x05,
  14.  
    14. 29, 8, 8, 8, 0x25,
  15.  
    15.
  16.  
    16. // slots: signature, parameters, type, tag, flags
  17.  
    17. 41, 8, 8, 8, 0x0a,
  18.  
    18. 55, 8, 8, 8, 0x08,
  19.  
    19.
  20.  
    20. // properties: name, type, flags
  21.  
    21. 90, 82, 0x0a095103,
  22.  
    22.
  23.  
    23. // constructors: signature, parameters, type, tag, flags
  24.  
    24. 108, 101, 8, 8, 0x0e,
  25.  
    25. 126, 8, 8, 8, 0x2e,
  26.  
    26.
  27.  
    27. 0 // eod
  28.  
    28. };

 

簡單的說明一下,

第一個section,就是 //content 區域的整數值,這一塊區域在每一個QMetaObject的實體對象中數量都是相同的,含義也相同,但具體的值就不同了。專門有一個struct定義了這個section,其含義在上面的注釋中已經說的很清楚了。

  1.  
    1. struct QMetaObjectPrivate
  2.  
    2. {
  3.  
    3. int revision;
  4.  
    4. int className;
  5.  
    5. int classInfoCount, classInfoData;
  6.  
    6. int methodCount, methodData;
  7.  
    7. int propertyCount, propertyData;
  8.  
    8. int enumeratorCount, enumeratorData;
  9.  
    9. int constructorCount, constructorData;
  10.  
    10. };

 

這個 struct 就是定義第一個secton的,和上面的數值對照一下,很清晰,是吧?

第二個section,以 // signals 開頭的這段。這個section中的數值指明了QObject這個class包含了兩個signal,

第三個section,以 // slots 開頭的這段。這個section中的數值指明了QObject這個class包含了兩個slot。

第四個section,以 // properties 開頭的這段。這個section中的數值指明了QObject這個class包含有一個屬性定義。

第五個section,以 // constructors 開頭的這段,指明了QObject這個class有兩個constructor。

const void *extradata;

這是一個指向QMetaObjectExtraData數據結構的指針,關於這個指針,這里先略過。

對於每一個具體的整數值與其所指向的實體數據之間的對應算法,實在是有點兒麻煩,這里就不講解細節了,有興趣的朋友自己去讀一下源代碼,一定會有很多發現。

 

我們都知道,把一個signal和slot連接起來,需要使用QObject類的connect方法,它的作用就是把一個object的signal和另外一個object的slot連接起來,以達到對象間通訊的目的。

connect 在幕后到底都做了些什么事情?為什么emit一個signal后,相應的slot都會被調用?好了,讓我們來逐一解開其中的謎團。

SIGNAL 和 SLOT 宏定義

我們在調用connect方法的時候,一般都會這樣寫:
obj.connect(&obj, SIGNAL(destroyed()), &app, SLOT(aboutQt()));
我們看到,在這里signal和slot的名字都被包含在了兩個大寫的SIGNAL和SLOT中,這兩個是什么呢?原來SIGNAL 和 SLOT 是Qt定義的兩個宏。好了,讓我們先來看看這兩個宏都做了寫什么事情:

這里是這兩個宏的定義:

  1.  
    # define SLOT(a) ”1″#a
  2.  
    # define SIGNAL(a) ”2″#a

 

原來Qt把signal和slot都轉化成了字符串,並且還在這個字符串的前面加上了附加的符號,signal前面加了’2’,slot前面加了’1’。也就是說,我們前面寫了下面的connect調用,在經過moc編譯器轉換之后,就便成了:
obj.connect(&obj, “2destroyed()”, &app, “1aboutQt()”));

當connect函數被調用了之后,都會去檢查這兩個參數是否是使用這兩個宏正確的轉換而來的,它檢查的根據就是這兩個前置數字,是否等於1或者是2,如果不是,connect函數當然就會失敗啦!

然后,會去檢查發送signal的對象是否有這個signal,方法就是查找這個對象的class所對應的staticMetaObject對象中所包含 的d.stringdata所指向的字符串中是否包含這個signal的名字,在這個檢查過程中,就會用到d.data所指向的那一串整數,通過這些整數值來計算每一個具體字符串的起始地址。同理,還會使用同樣的方法去檢查slot,看響應這個signal的對象是否包含有相應的slot。這兩個檢查的任 何一個如果失敗的話,connect函數就失敗了,返回false.

前面的步驟都是在做一些必要的檢查工作,下一步,就是要把發送signal的對象和響應signal的對象關聯起來。在QObject的私有數據類QObjectPrivate中,有下面這些數據結構來保存這些信息:

  1.  
    class QObjectPrivate : public QObjectData
  2.  
    {
  3.  
    struct Connection
  4.  
    {
  5.  
    QObject *receiver;
  6.  
    int method;
  7.  
    uint connectionType : 3; // 0 == auto, 1 == direct, 2 == queued, 4 == blocking
  8.  
    QBasicAtomicPointer< int> argumentTypes;
  9.  
    };
  10.  
     
  11.  
    typedef QList<Connection>; ConnectionList;
  12.  
     
  13.  
    QObjectConnectionListVector *connectionLists;
  14.  
     
  15.  
    struct Sender
  16.  
    {
  17.  
    QObject *sender;
  18.  
    int signal;
  19.  
    int ref;
  20.  
    };
  21.  
     
  22.  
    QList<Sender> senders;
  23.  
    };

在發送signal的對象中,每一個signal和slot的connection,都會創建一個QObjectPrivate::Connection對象,並且把這個對象保存到connectionList這個Vector里面去。

在響應signal的對象中,同樣,也是每一個signal和slot的connection,都會一個創建一個Sender對象,並且把這個對象附加在Senders這個列表中。

以上就是connect的過程,其中,創建QObjectPrivate::Connection對象和Sender對象的過程有一點點復雜,需要仔細思考才可以,有興趣的朋友可以去讀一下源代碼。

當我們寫下一下emit signal代碼的時候,與這個signal相連接的slot就會被調用,那么這個調用是如何發生的呢?讓我們來逐一解開其中的謎團。

讓我們來看一段例子代碼:

 

  1.  
    class ZMytestObj : public QObject
  2.  
    {
  3.  
    Q_OBJECT
  4.  
    signals:
  5.  
    void sigMenuClicked();
  6.  
    void sigBtnClicked();
  7.  
    };
  8.  
    MOC編譯器在做完預處理之后的代碼如下:
  9.  
    // SIGNAL 0
  10.  
    void ZMytestObj::sigMenuClicked()
  11.  
    {
  12.  
    QMetaObject::activate( this, &staticMetaObject, 0, 0);
  13.  
    }
  14.  
     
  15.  
    // SIGNAL 1
  16.  
    void ZMytestObj::sigBtnClicked()
  17.  
    {
  18.  
    QMetaObject::activate( this, &staticMetaObject, 1, 0);
  19.  
    }

哈哈,看到了把,每一個signal都會被轉換為一個與之相對應的成員函數。也就是說,當我們寫下這樣一行代碼:
emit sigBtnClicked();
當程序運行到這里的時候,實際上就是調用了void ZMytestObj::sigBtnClicked() 這個函數。

大家注意比較這兩個函數的函數體,
void ZMytestObj::sigMenuClicked()  void ZMytestObj::sigBtnClicked(),
它們唯一的區別就是調用 QMetaObject::activate 函數時給出的參數不同,一個是0,一個是1,它們的含義是什么呢?它們表示是這個類中的第幾個signal被發送出來了,回頭再去看頭文件就會發現它們就 是在這個類定義中,signal定義出現的順序,這個參數可是非常重要的,它直接決定了進入這個函數體之后所發生的事情。

當執行流程進入到QMetaObject::activate函數中后,會先從connectionLists這個變量中取出與這個signal相對應的 connection list,它根據的就是剛才所傳入進來的signal index。這個connection list中保存了所有和這個signal相鏈接的slot的信息,每一對connection(即:signal 和 slot 的連接)是這個list中的一項。

在每個一具體的鏈接記錄中,還保存了這個鏈接的類型,是自動鏈接類型,還是隊列鏈接類型,或者是阻塞鏈接類型,不同的類型處理方法還不一樣的。這里,我們就只說一下直接調用的類型。

對於直接鏈接的類型,先找到接收這個signal的對象的指針,然后是處理這個signal的slot的index,已經是否有需要處理的參數,然后就使用這些信息去調用receiver的qt_metcall 方法。

在qt_metcall方法中就簡單了,根據slot的index,一個大switch語句,調用相應的slot函數就OK了。

很多C/C++初學者常犯的一個錯誤就是,使用malloc、new分配了一塊內存卻忘記釋放,導致內存泄漏。Qt的對象模型提供了一種Qt對象之間的父 子關系,當很多個對象都按一定次序建立起來這種父子關系的時候,就組織成了一顆樹。當delete一個父對象的時候,Qt的對象模型機制保證了會自動的把 它的所有子對象,以及孫對象,等等,全部delete,從而保證不會有內存泄漏的情況發生。

 

任何事情都有正反兩面作用,這種機制看上去挺好,但是卻會對很多Qt的初學者造成困擾,我經常給別人回答的問題是:1,new了一個Qt對象之后,在什么 情況下應該delete它?2,Qt的析構函數是不是有bug?3,為什么正常delete一個Qt對象卻會產生segment fault?等等諸如此類的問題,這篇文章就是針對這個問題的詳細解釋。

 

在每一個Qt對象中,都有一個鏈表,這個鏈表保存有它所有子對象的指針。當創建一個新的Qt對象的時候,如果把另外一個Qt對象指定為這個對象的父對象, 那么父對象就會在它的子對象鏈表中加入這個子對象的指針。另外,對於任意一個Qt對象而言,在其生命周期的任何時候,都還可以通過setParent函數 重新設置它的父對象。當一個父對象在被delete的時候,它會自動的把它所有的子對象全部delete。當一個子對象在delete的時候,會把它自己 從它的父對象的子對象鏈表中刪除。

 

QWidget是所有在屏幕上顯示出來的界面對象的基類,它擴展了Qt對象的父子關系。一個Widget對象也就自然的成為其父Widget對象的子 Widget,並且顯示在它的父Widget的坐標系統中。例如,一個對話框(dialog)上的按鈕(button)應該是這個對話框的子 Widget。

 

關於Qt對象的new和delete,下面我們舉例說明。

 

例如,下面這一段代碼是正確的:

  1.  
    int main()
  2.  
    {
  3.  
    QObject* objParent = new QObject(NULL);
  4.  
    QObject* objChild = new QObject(objParent);
  5.  
    QObject* objChild2 = new QObject(objParent);
  6.  
    delete objParent;
  7.  
    }

 

在上述代碼片段中,objParent是objChild的父對象,在objParent對象中有一個子對象鏈表,這個鏈表中保存它所有子對象的指針,在 這里,就是保存了objChild和objChild2的指針。在代碼的結束部分,就只有delete了一個對象objParent,在 objParent對象的析構函數會遍歷它的子對象鏈表,並且把它所有的子對象(objChild和objChild2)一一刪除。所以上面這段代碼是安 全的,不會造成內存泄漏。

 

如果我們把上面這段代碼改成這樣,也是正確的:

 

  1.  
    int main()
  2.  
    {
  3.  
    QObject* objParent = new QObject(NULL);
  4.  
    QObject* objChild = new QObject(objParent);
  5.  
    QObject* objChild2 = new QObject(objParent);
  6.  
    delete objChild;
  7.  
    delete objParent;
  8.  
    }

 

在這段代碼中,我們就只看一下和上一段代碼不一樣的地方,就是在delete objParent對象之前,先delete objChild對象。在delete objChild對象的時候,objChild對象會自動的把自己從objParent對象的子對象鏈表中刪除,也就是說,在objChild對象被 delete完成之后,objParent對象就只有一個子對象(objChild2)了。然后在delete objParent對象的時候,會自動把objChild2對象也delete。所以,這段代碼也是安全的。

 

Qt的這種設計對某些調試工具來說卻是不友好的,比如valgrind。比如上面這段代碼,valgrind工具在分析代碼的時候,就會認為objChild2對象沒有被正確的delete,從而會報告說,這段代碼存在內存泄漏。哈哈,我們知道,這個報告是不對的。

 

我們在看一看這一段代碼:

  1.  
    int main()
  2.  
    {
  3.  
    QWidget window;
  4.  
    QPushButton quit("Exit", &window);
  5.  
    }

在這段代碼中,我們創建了兩個widget對象,第一個是window,第二個是quit,他們都是Qt對象,因為QPushButton是從 QWidget派生出來的,而QWidget是從QObject派生出來的。這兩個對象之間的關系是,window對象是quit對象的父對象,由於他們 都會被分配在棧(stack)上面,那么quit對象是不是會被析構兩次呢?我們知道,在一個函數體內部聲明的變量,在這個函數退出的時候就會被析構,那 么在這段代碼中,window和quit兩個對象在函數退出的時候析構函數都會被調用。那么,假設,如果是window的析構函數先被調用的話,它就會去 delete quit對象;然后quit的析構函數再次被調用,程序就出錯了。事實情況不是這樣的,C++標准規定,本地對象的析構函數的調用順序與他們的構造順序相反。那么在這段代碼中,這就是quit對象的析構函數一定會比window對象的析構函數先被調用,所以,在window對象析構的時候,quit對象已 經不存在了,不會被析構兩次。

 

如果我們把代碼改成這個樣子,就會出錯了,對照前面的解釋,請你自己來分析一下吧。

intmain()

{

QPushButtonquit("Exit");

QWidgetwindow;

quit.setParent(&window);

}

 

但是我們自己在寫程序的時候,也必須重點注意一項,千萬不要delete子對象兩次,就像前面這段代碼那樣,程序肯定就crash了。

 

最后,讓我們來結合Qt source code,來看看這parent/child關系是如何實現的。

 

在本專欄文章的第一部分“對象數據存儲”,我們說到過,所有Qt對象的私有數據成員的基類是QObjectData類,這個類的定義如下:

 

  1.  
    typedef QList<QObject*>; QObjectList;
  2.  
    class QObjectData
  3.  
    {
  4.  
    public:
  5.  
    QObject *parent;
  6.  
    QObjectList children;
  7.  
    // 忽略其它成員定義
  8.  
    };

我們可以看到,在這里定義了指向parent的指針,和保存子對象的列表。其實,把一個對象設置成另一個對象的父對象,無非就是在操作這兩個數據。把子對 象中的這個parent變量設置為指向其父對象;而在父對象的children列表中加入子對象的指針。當然,我這里說的非常簡單,在實際的代碼中復雜的多,包含有很多條件判斷,有興趣的朋友可以自己去讀一下Qt的源代碼。

 

https://blog.csdn.net/mznewfacer/article/details/6990790


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM