GObject Tutorial Part 1

GObject Tutorial Part 1

本文轉自:http://blog.mcuol.com/User/AT91RM9200/Article/9405_1.htm

    Ryan McDougall(2004) 

    translated by neowillis 

    from http://www.mail-archive.com/[email protected]/msg17190.html 

目的

    本文檔可用於兩個目的:一是作爲一篇學習Glib的GObject類型系統的教程,二是用作一篇按步驟使用GObject類型系統的入門文章。本文從如何用C語言來設計一個面向對象的類型系統着手,將GObject作爲假設的解決方案。這種介紹的方式可以更好的解釋這個開發庫爲何採用這種形式來設計,以及使用它爲什麼需要這些步驟。入門文章被安排在教程之後,使用了一種按步驟的、實際的、簡潔的組織形式,這樣對於某些更實際的程序員會更有用些。 

讀者

    本文假想的讀者是那些熟悉面向對象概念,但是剛開始接觸GObject或者GTK+的開發人員。 我會認爲您已經瞭解一門面向對象的語言,和一些C語言的基本命令。 

動機

    使用一種根本不支持面向對象的語言來編寫一個面向的系統,這讓人聽上去有些瘋狂。然而我們的確有一些很好的理由來做這樣的事情。但在這裏我不會試着去證明作者決定的正確性,並且我認爲讀者自己就有一些使用GLib的好理由。 這裏我將指出這個系統的一些重要特性:

        C是一門可移植性很強的語言
        一個完全動態的系統,新的類型可以在運行時被添加上 

    這樣系統的可擴展性要遠強於一門標準的語言,所以新的特性也可以被很快的加入進來。 

    對面嚮對象語言來說,面向對象的特性和能力是用語法來定義的。然而,因爲C並不支持面向對象,所以GObject系統必須手動的將面向對象的能力引入進來。一般來說,要實現這個目標需要做一些乏味的工作,甚至偶爾使用某些奇妙的手段。而我需要做的只是枚舉出所有必要的步驟或“咒語”,使得程序執行起來,當然也希望能說明這些步驟對您的程序意味着什麼。 

1. 創建一個非繼承的對象
設計

    在面向對象領域,對象包含兩種成員類型:數據和方法,它們處於同一個對象引用之下。有一種辦法可以使用C來實現對象,那就是C的結構體(struct)。這樣,普通公用成員可以是數據,方法則可以被實現爲指向函數的指針。然而這樣的實現卻存在着一些嚴重的缺陷:彆扭的語法,類型安全問題,缺少封裝。而更實際的問題是-空間浪費嚴重。每個實例化後的對象需要一個4字節的指針來指向其每一個成員方法,而這些方法在同樣的類封裝範圍裏則是完全相同的,是冗餘的。例如我們有一個類需要有4個成員方法,一個程序實例化了1000個這個類的對象,這樣我們就浪費了接近16KB的空間。顯然我們只需要保留一張包含這些指針的表,供這個類實例出的對象調用,這樣就會節省下不少內存資源。 

    這張表就被稱作虛方法表(vtable),GObject系統爲每個類在內存中都保存了一份這張表。當你想調用一個虛方法時,必須先向系統請求查找這個對象所對應的虛方法表,而如上所述這張表包含了一個由函數指針組成的結構體。這樣你就能復引用這個指針,通過它來調用方法了。 

    我們稱這兩種成員類型(數據和方法)爲“實例結構體”和“類結構體”,並且將這兩種結構體的實例分別稱爲“實例對象”和“類對象“。這兩種結構體合併在一起形成了一個概念上的單元,我們稱之爲“類”,對這個“類”的實例則稱作“對象”。 

    將這樣的函數稱作“虛函數”的原因是,調用它需要在運行時查找合適的函數指針,這樣就能允許繼承自它的類覆蓋這個方法(只要更改虛函數表中的函數指針指向相應函數入口即可)。這樣子類在向上轉型(upcast)爲父類時就會正常工作,就像我們所瞭解的C++裏的虛方法一樣。 

    儘管這樣做可以節省內存和實現虛方法,但從語法上來看,將成員方法與對象用“點操作符”關聯起來的能力就不具備了。(譯者:因爲點操作符關聯的將是struct裏的方法,而不是vtable裏的)。因此我們將使用如下的命名約定來聲明類的成員方法:NAMESPACE_TYPE_METHOD (OBJECT*, PARAMETERS) 

    非虛方法將被實現在一個普通的C函數裏。虛方法其實也是實現在普通的C函數中,但不同的是這個函數實現時將調用虛函數表中某個合適的方法。私有成員將被實現爲只存活在源文件中,而不被導出聲明在頭文件中。 

    注意:面向對象通常使用信息隱藏來作爲封裝的一部分,但在C語言中卻沒有簡單的辦法來隱藏私有成員。一種辦法是將私有成員放到一個獨立的結構體中,該結構體只定義在源文件中,再向你的公有對象結構體中添加一個指向這個私有類的指針。然而,在開放源代碼的世界裏,如果用戶執意要做錯誤的事,這種保護也是毫無意義的。大部分開發者也只是簡單的寫上幾句註釋,標明這些成員他們應該被保護爲私有的,希望用戶能尊重這種封裝上的區別。 

    現在爲止我們有了兩種不同的結構體,但我們沒有好辦法能通過一個實例化後的對象直接找到其虛方法表。但如我們在上面提到的,這應該是系統的職責,我們只要按要求向系統註冊上新聲明的類型,就應該能夠處理這個問題。系統也要求我們去向它註冊(對象的和類的)結構體初始化和銷燬函數(以及其他的重要信息),這樣我們的對象才能被正確的實例化出來。系統將通過枚舉化所有的向它註冊的類型來記錄新的對象類型,要求所有實例化對象的第一個成員是一個指向它自己類的虛函數表的指針,每個虛函數表的第一個成員是它在系統中保存的枚舉類型的數字表示。 

    注意:類型系統要求所有類型的對象結構體和類結構體的第一個成員是一個特殊結構體。在對象結構體中,該特殊結構體是一個指向其類型的對象。因爲C語言保證在結構體中聲明的第一個成員是在內存的最前面,因此這個類型對象可以通過將這個原對象的結構體轉型而獲得到。又因爲類型系統要求我們將被繼承的父結構體指針聲明爲子結構體的第一個成員,這樣我們只需要在父類中聲明一次這個類型對象,以後就能夠通過一次轉型而找到虛函數表了。 

    最後,我們還需要定義一些管理對象生命期的函數:創建類對象的函數,創建實例對象的函數,銷燬類對象的函數,但不需要銷燬實例對象的函數,因爲實例對象的內存管理是一個比較複雜的問題,我們將把這個工作留給更高層的代碼來做。 

代碼(頭文件)

    a. 用struct來創建實例對象和類對象,實現“C風格”的對象 

    注意:對結構體命名一般要在名字前添加下劃線,然後使用前置類型定義typedef。這是因爲C的語法不允許你在SomeObject中聲明SomeObject指針(這對定義鏈表之類的數據結構很方便)(譯者:如果非要這樣用,則需要在類型前加上struct)。按上面的命名約定,我們還創建了一個命名域,叫做“Some”。

    /* “實例結構體”定義所有的數據域,實例對象將是唯一的 */
  1.     typedef struct _SomeObject SomeObject;
        struct _SomeObject
        {
                GTypeInstance   gtype;

                gint            m_a;
                gchar*          m_b;
                gfloat          m_c;
        };




    /* “類結構體”定義所有的方法函數,類對象將是共享的 */
  1.     typedef struct _SomeObjectClass SomeObjectClass;
        struct _SomeObjectClass
        {
                GTypeClass  gtypeclass;

                void  (*method1) (SomeObject *self, gint);
                void  (*method2) (SomeObject *self, gchar*);
        };




    b. 聲明一個"get_type"函數,第一次調用該函數時,函數負責向系統註冊上對象的類型,並返回系統返回的一個GType類型值,在此後的調用就會直接返回該GType值。該值實際上是一個系統用來區別已註冊類型的整型數字。由於函數是SomeObject類型特有的,我們在它前面加上“some_object_"。

    /* 該方法將返回我們新聲明的對象類型所關聯的GType類型 */
    GType   some_object_get_type (void);

    c. 聲明一些用來管理對象生命期的函數:初始化時創建對象的函數,結束時銷燬對象的函數。

    /* 類/實例的初始化/銷燬函數。它們的標記在gtype.h中定義。 */
    void some_object_class_init     (gpointer g_class, gpointer class_data);
    void some_object_class_final    (gpointer g_class, gpointer class_data);
    void some_object_instance_init  (GTypeInstance *instance, gpointer g_class);

    d. 用上面我們約定的方式來命名成員方法函數。

    /* 所有這些函數都是SomeObject的方法. */
    void  some_object_method1 (SomeObject *self, gint);   /* virtual */
    void  some_object_method2 (SomeObject *self, gchar*); /* virtual */
    void  some_object_method3 (SomeObject *self, gfloat); /* non-virtual */

    e. 創建一些樣板式代碼(boiler-plate code),符合規則的同時也讓事情更簡單一些

    /* 方便的宏定義 */
    #define SOME_OBJECT_TYPE          (some_object_get_type ())
    #define SOME_OBJECT(obj)          (G_TYPE_CHECK_INSTANCE_CAST ((obj), SOME_OBJECT_TYPE, SomeObject))
    #define SOME_OBJECT_CLASS(c)      (G_TYPE_CHECK_CLASS_CAST ((c), SOME_OBJECT_TYPE, SomeObjectClass))
    #define SOME_IS_OBJECT(obj)       (G_TYPE_CHECK_TYPE ((obj), SOME_OBJECT_TYPE))
    #define SOME_IS_OBJECT_CLASS(c)   (G_TYPE_CHECK_CLASS_TYPE ((c), SOME_OBJECT_TYPE))
    #define SOME_OBJECT_GET_CLASS(obj)(G_TYPE_INSTANCE_GET_CLASS ((obj), SOME_OBJECT_TYPE, SomeObjectClass))

代碼(源程序)

    現在可以實現那些剛剛聲明過的函數了。 

    注意:由於虛函數是一些函數指針,我們還要創建一些可被尋址的普通C函數(命名以"impl"結尾,並且不被導出到頭文件中),虛函數將被實現爲指向這些函數。 

  1.     a. 實現虛方法。

        /* 虛函數中指向的普通函數 */
        void some_object_method1_impl (SomeObject *self, gint a)
        {
                self->m_a = a;
                g_print ("Method1: %i\n", self->m_a);
        }

        void some_object_method2_impl (SomeObject *self, gchar* b)
        {
                self->m_b = b;
                g_print ("Method2: %s\n", self->m_b);
        }




    b. 實現所有公有方法。實現虛方法時,我們必須使用“GET_CLASS”宏來從類型系統中獲取到類對象,用以調用虛函數表中的虛方法。非虛方法時,直接寫實現代碼即可。

  1.     /* 公有方法 */
        void some_object_method1 (SomeObject *self, gint a)
        {
            SOME_OBJECT_GET_CLASS (self)->method1 (self, a);
        }

        void some_object_method2 (SomeObject *self, gchar* b)
        { 
           SOME_OBJECT_GET_CLASS (self)->method2 (self, b);
        }

        void some_object_method3 (SomeObject *self, gfloat c)
        {
                self->m_c = c;
                g_print ("Method3: %f\n", self->m_c);
        }




    c. 實現初始化/銷燬方法。在這兩個方法中,系統傳入的參數是指向該對象的泛型指針(我們相信這個指針的確指向一個合適的對象),所以我們在使用它之前必須將其轉型爲合適的類型。

    /* 該函數將在類對象創建時被調用 */
  1.     void  some_object_class_init(gpointer g_class, gpointer class_data)
        {
                SomeObjectClass *this_class = SOME_OBJECT_CLASS (g_class);

                /* 填寫類結構體的方法成員 (本例只存在一個虛函數表) */
                this_class->method1 = &some_object_method1_impl;
                this_class->method2 = &some_object_method2_impl;
        }

        /* 該函數在類對象不再被使用時調用 */
        void some_object_class_final (gpointer g_class, gpointer class_data)
        {
             /* 該對象被銷燬時不需要做任何動作,因爲它不存在任何指向動態分配的
               資源的指針或者引用。 */
        }

        /* 該函數在實例對象被創建時調用。系統通過g_class實例的類來傳遞該實例的類。 */
        void some_object_instance_init (GTypeInstance *instance, gpointer g_class)
        {
                SomeObject *this_object = SOME_OBJECT (instance);

                /* 填寫實例結構體中的成員變量 */
                this_object->m_a = 42;
                this_object->m_b = 3.14;
                this_object->m_c = NULL;
        }




    d. 實現能夠返回給調用者SomeObject的GType的函數。該函數在第一次運行時,它通過向系統註冊SomeObject來獲取到GType。該 GType將被保存在一個靜態變量中,以後該函數再被調用時就無須註冊可以直接返回該數值了。雖然可以使用一個獨立的函數來註冊該類型,但這樣的實現可以保證類在使用前是註冊過的,該函數通常在實例化第一個對象時被調用。

  1.     /* 因爲該類沒有父類,所以父類函數是空的 */
        GType some_object_get_type (void)
        {
           static GType type = 0;

          if (type == 0) 
          {
          /* 這是系統用來完整描述要註冊的類型是如何被創建、初始化和銷燬的結構體。 */
            static const GTypeInfo type_info = 
            {
               sizeof (SomeObjectClass),
               NULL,                           /* 父類初始化函數 */
               NULL,                           /* 父類銷燬函數 */
               some_object_class_init,         /* 類對象初始化函數 */
               some_object_class_final,        /* 類對象銷燬函數 */
               NULL,                           /* 類數據 */
               sizeof (SomeObject),
               0,                              /* 預分配的字節數 */
               some_object_instance_init       /* 實例對象初始化函數 */
             };

          /* 因爲我們的類沒有父類,所以它將被認爲是“基礎類(fundamental)”,
             因此我們必須要告訴系統,該類既是一個複合結構的類(與浮點型,整型,
             或者指針不同),而且是可以被實例化的(系統可以創建實例對象,相反如接口
             或者抽象類則不能被實例化) */
           static const GTypeFundamentalInfo fundamental_info =
           {
              G_TYPE_FLAG_CLASSED | G_TYPE_FLAG_INSTANTIATABLE
           };      

            type = g_type_register_fundamental
           (
              g_type_fundamental_next (),     /* 下一個可用的GType */
              "SomeObjectType",               /* 類型的名稱 */
               &type_info,                     /* 上面定義的type_info */
               &fundamental_info,              /* 上面定義的fundamental_info */
               0                               /* 類型不是抽象的 */
              );
         }

                return  type;
        }




    /* 讓我們來編寫一個測試用例吧! */

  1.     int     main()
        {
           SomeObject      *testobj = NULL;

           /* 類型系統初始化 */
           g_type_init ();

           /* 讓系統創建實例對象 */
          testobj = SOME_OBJECT (g_type_create_instance (some_object_get_type()));

           /* 調用我們定義了的方法 */
          if (testobj)
           {
                g_print ("%d\n", testobj->m_a);
                some_object_method1 (testobj, 32);
                g_print ("%s\n", testobj->m_b);
                some_object_method2 (testobj, "New string.");
                g_print ("%f\n", testobj->m_c);
                some_object_method3 (testobj, 6.9);
            }

           return  0;
        }





還需要考慮的

    我們已經用C實現了第一個對象,但是做了很多工作,而且這並不算是真正的面向對象,因爲我們故意沒有提及任何關於“繼承”的方法。在下一節我們將看到如何利用別人的代碼,使SomeObject繼承於內建的類GObject。 

    儘管在下文中我們將重用上面討論的思想和模型,但是創建一個基礎類使得它能夠像其它的GTK+代碼一樣,是一件非常困難和深入的事情。因此強烈建議您創建新的類時總是繼承於GObject,它會幫您做大量背後的工作,使得您的類能符合GTK+的要求。 

2.使用內建的宏定義來自動生成代碼
設計

    您可能已經注意到了,我們上面所做的大部分工作基本上都是機械的、模板化的工作。大多數的函數都不併是通用的,每創建一次類我們就需要重寫一遍。很顯然這就是爲什麼我們發明了計算機的原因 - 讓工作自動化,讓我們的生活更簡單! 

    OK,其實我們很幸運,C的預處理器將允許我們編寫宏定義,這些宏定義在編譯時會展開成爲合適的C代碼,來生成我們需要的類型定義。其實使用宏定義還能幫助我們減少一些低級錯誤。 

    然而,自動化將使得我們失去對定義處理的靈活性。在上面描述的步驟中,我們能有許多可能的變化,但一個宏定義卻只能實現一種展開。如果這個宏定義提供了輕量級的展開,但我們想要的是一個完整的類型,這樣我們仍然需要手寫一大堆代碼。如果宏定義提供了完整的展開,但我們需要的卻是一種輕量級的類型,我們將得到許多冗餘的代碼,花許多時間來填寫這些用不上的樁代碼,甚至是一些錯誤的代碼。不幸的是C預處理器並沒有設計成能夠自動發現我們感興趣的代碼生成方式,它只包含了最有限的功能。 

代碼

    創建一個新類型的代碼非常簡單:

    G_DEFINE_TYPE_EXTENDED (TypeName, function_prefix, PARENT_TYPE, GTypeFlags, CODE)。

    第一個參數是類的名稱。第二個是函數名稱的前綴,這使得我們的命名規則能保持一致。第三個是父類的GType。第四個是會被添加到!GTypeInfo結構體裏的!GTypeFlag。第五個是在類型被註冊後應該立刻被執行的代碼。 

    看看下面的代碼將被展開成爲什麼樣將會給我們更多的啓發。

    G_DEFINE_TYPE_EXTENDED (SomeObject, some_object, 0, some_function())

    注意:實際展開後的代碼將隨着系統版本不同而不同。你應該總是檢查一下展開後的結果而不是憑主觀臆斷。 

    展開後的代碼(清理了空格):

  1. static void some_object_init (SomeObject *self);
        static void some_object_class_init (SomeObjectClass *klass);
        static gpointer some_object_parent_class = ((void *)0);

        static void some_object_class_intern_init (gpointer klass) 
        {
          some_object_parent_class = g_type_class_peek_parent (klass); 
          some_object_class_init ((SomeObjectClass*) klass);
        } 

        GType some_object_get_type (void) 
        {
          static GType g_define_type_id = 0; 
          if ((g_define_type_id == 0)) 
          { 
            static const GTypeInfo g_define_type_info = 
            { 
             sizeof (SomeObjectClass), 
             (GBaseInitFunc) ((void *)0), 
             (GBaseFinalizeFunc) ((void *)0), 
             (GClassInitFunc) some_object_class_intern_init, 
             (GClassFinalizeFunc) ((void *)0), 
             ((void *)0), 
              sizeof (SomeObject), 
              0, 
             (GInstanceInitFunc) some_object_init, 
            }; 

          g_define_type_id = g_type_register_static 
          (
             G_TYPE_OBJECT, 
             "SomeObject", 
             &g_define_type_info, 
             (GTypeFlags) 0
          );
                        
           { some_function(); } 
          
        } 

           return g_define_type_id; 
        }



    注意:該宏定義聲明瞭一個靜態變量“_parent_class",它是一個指針,指向我們打算創建對象的父類。當我們要找到虛方法繼承自哪裏時它會派上用場,可以用於鏈式觸發處理/銷燬函數(譯者:下面會介紹)。這些處理/銷燬函數幾乎總是虛函數。我們接下來的代碼將不再使用這個結構,因爲有其它的函數能夠不使用靜態變量而做到這一點。 

    你應該注意到了,這個宏定義沒有定義父類的初始化、銷燬函數以及類對象的銷燬函數。那麼如果你需要這些函數,就得自己動手了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章