再战 GObject · 前言 <=
对于双向链表这种数据结构,即便是 C 语言的初学者也能够创建一份名为 double-list.h 的头文件并写出以下代码:
/* file name: double-list.h */ #ifndef DOUBLE_LIST_H #define DOUBLE_LIST_H struct double_list_node { struct double_list_node *prev; struct double_list_node *next; void *data; }; struct double_list { struct double_list_node *head; struct double_list_node *tail; }; #endif
较为熟悉 C 语言的人自然不屑于写出上面那种新手级别的代码,他们通常使用 typedef
去为一些数据类型起一些自己觉得好听一点的名字,并将 double_list
简写为 DList
:
/* file name: dlist.h(版本 2)*/ #ifndef DLIST_H #define DLIST_H typedef struct _DListNode DListNode; struct _DListNode { DListNode *prev; DListNode *next; void *data; }; typedef struct _DList DList; struct _DList { DListNode *head; DListNode *tail; }; #endif
这样可以避免反复去写 struct double_list_xxxxx
这样的代码,而且还遵循 Pascal 命名惯例,即单词首字母大写[2]。
现在,代码看上去稍微『专业』了一点。但是,由于 C 语言没有为数据类型提供自定义命名空间的功能,程序中所有的数据类型(包括函数)均处于同一个命名空间,这样数据类型便存在因为同名而撞车的可能性。
为了避免这一问题,更专业一点的程序员会为数据类型名称添加一些前缀,并且通常会选择项目名称的缩写。我们可以为这种命名方式取一个名字,叫做 PT 约定 , P
是项目名称的缩写, T
是数据类型的名称。例如,对于一个多面体建模(Polyhedron Modeling)的项目,如果要为这个项目定义一个双向链表的数据类型,可将 dlist.h 文件名修改为 pm-dlist.h,然后将其内容改为:
/* file name: pm-dlist.h*/ #ifndef PM_DLIST_H #define PM_DLIST_H typedef struct _PMDListNode PMDListNode; struct _PMDListNode { PMDListNode *prev; PMDListNode *next; void *data; }; typedef struct _PMDList PMDList; struct _PMDList { PMDListNode *head; PMDListNode *tail; }; #endif
在以上一波三折的过程中,我们所做的工作就是仅仅是定义了两个结构体而已,一个是双向链表结点的结构体,另一个是双向链表的结构体,并且这两个结构体中分别包含了一组指针成员。此类工作,用面向对象编程方法中的术语来将,就是”数据封装“。如《C++ Primer》所言, 数据封装,就是一种将低层次的元素组合起来,形成新的、高层次实体的技术 。对于上述代码而言,指针类型的变量属于低层次的元素,而它们所构成的结构体,则是高层次的实体。
在支持面向对象的编程语言中,有一种比 C 结构体更高层次的实体—— 类 。然而在 C 语言中,结构体已经是最高层次的数据类型了,因此 GObject 只能用 C 结构体对『 类 』这种概念进行模拟。
下面是基于 GObject 对 类 的数据封装形式的模拟:
/* file name: pm-dlist.h*/ #ifndef PM_DLIST_H #define PM_DLIST_H #include <glib-object.h> typedef struct _PMDListNode PMDListNode; struct _PMDListNode { PMDListNode *prev; PMDListNode *next; void *data; }; typedef struct _PMDList PMDList; struct _PMDList { GObject parent_instance; PMDListNode *head; PMDListNode *tail; }; typedef struct _PMDListClass PMDListClass; struct _PMDListClass { GObjectClass parent_class; }; #endif
上述代码与 dlist.h 版本 2 中的代码相比,除去空行,多出 6 行代码,它们的作用是实现一个双向链表类。也许你会感觉这样很滑稽,特别当你特别熟悉 C++、Java、C# 之类的语言之时。
在 GObject 世界里, 类 是两个结构体的组合,一个叫 实例结构体 ,另一个叫 类结构体 。上面的示例代码中, PMDList
是实例结构体, PMDListClass
是类结构体,它们合起来就是 PMDList
类 (此处的 PMDList
类只是一个称谓,并非是指 PMDList
实例结构体)。
也许你会注意到,PMDList 类的实例结构体的第一个成员是 GObject
结构体,其类结构体的第一个成员是 GObjectClass
结构体。 GObject
结构体与 GObjectClass
结构体分别是 GObject 类的实例结构体与类结构体,当它们分别作为 PMDList 类的实例结构体与类结构体的第一个成员时,这意味着 PMDList 类继承了 GObject 类。
也许你并不明白 GObject 为什么要将类拆解为实例结构体与类结构体,也不明白为什么将某个类的实例结构体与类结构体,分别置于另一个类的实例结构体与类结构体的成员之首便可实现类的继承……一开始无需去理解这些,只需像数学公式那样将它们记住。以后每当需要使用 C 语言来模拟类的封装形式时,只需使其基类为 GObject。
这就像初学 C++ 时,对于下面的代码:
class PMDListNode { public: PMDListNode *prev; PMDListNode *next; void *data; } class PMDList : public GObject { public: PMDListNode *head; PMDListNode *tail; };
也许除了 C++ 编译器的开发者之外,没人会对为什么使用 class
关键字便可以将一个数据结构变成类,为什么使用一个冒号便可以让 DList
类继承自 GObject
类,以及为什么使用 public
即可将 DList
类的 head
与 tail
属性对外开放之类的问题感兴趣。
为何要将 GObject 类作为父类?这主要是因为 GObject 类具有以下功能:
基于引用计数的内存管理;
对象的构造函数与析构函数;
可设置对象属性的 set/get 函数;
易于使用的信号机制。
虽然现在你不是很清楚继承 GObject 类的好处,但是不继承 GObject 类的坏处是显而易见的。在 cloverprince 所写的 11 篇文章[3]中,从第 2 篇到第 5 篇展示了不使用 GObject 的坏处,那就是需要使用很多代码方能构造出上述的 DList 类。
我不是很明白面向对象程序设计中的 类 、 对象 以及 实例 这三者之间的关系。看到还有人同我一样没有搞明白[4],心里便略微有些安慰,甚至觉得 C 语言不内建支持面向对象程序设计是一件值得庆幸的事情。暂时就按照文档[4]那样理解吧,对于上面所设计的 PMDList 类,可以用下面的代码模拟类的实例化与对象的实例化:
PMDList *list; /* 类的实例化 */ list = g_object_new(PM_TYPE_DLIST, NULL); /* 对象的实例化 */
也就是说,对于 PMDList 类,它的实例是一个对象,例如上述代码中的 list
,而对象的实例化则是让这个对象成为存在于计算机存储器中的实体。显然 list
作为指针类型,只有当它指向一块存储器空间才会形成『实体』。
也许,对象的实例化比较令人费解,幸好,C 语言的表示会更加直观,例如:
PMDList *dlist; /* 类的实例化,产生对象 */ dlist = g_object_new(PM_TYPE_DLIST, NULL); /* 创建对象的一个实例 */ g_object_unref(dlist); /* 销毁对象的一个实例 */ dlist = g_object_new(PM_TYPE_DLIST, NULL); /* 再创建对象的一个实例 */
这里需要暂停一下。请回溯到上一节所说的让一个类继承 GObject 类的好处的前两条,即 GObject 类提供的“ 基于引用计数的内存管理 ”与“ 对象的构造函数与析构函数 ”,此时体现于上例中的 g_object_new
与 g_object_unref
函数。
g_object_new
用于构建对象的实例,虽然其内部实现非常繁琐和复杂,但这是 GObject 库的开发者所要考虑的事情。我们只需要知道 g_object_new
可以为对象的实例分配内存与初始化,并且将实例的引用计数设为 1。
g_object_unref
用于将对象的实例的引用计数减 1,并检测对象的实例的引用计数是否为 0,若为 0,便释放对象的实例的存储空间。
现在,再回到上述代码,它真正要表述的概念是: 类的实例是一个对象,一个对象可能存在多个实例 。
继续概念游戏,看下面的代码:
PMDList *list = g_object_new(PM_TYPE_DLIST, NULL);
这是类的实例化还是对象的实例化?自然是 类的实例化的实例化 ,感觉有点恶心,事实上这只不过是为某个数据类型动态分配了内存空间,然后用指针指向它!
我们最好是不再对 对象
和 实例
进行区分,对象是实例,实例也是对象,只需要知道它们所指代的事物在同一时刻是相同的,除非有了量子计算机。
PM_TYPE_DLIST
的面具 上一节所使用的 g_object_new
函数的参数是 PM_TYPE_DLIST
和 NULL
。对于 NULL
,虽然我们不知道它的真实意义,但是至少知道它表示一个空指针,而那个 PM_TYPE_DLIST
是什么?
PM_TYPE_DLIST
是一个宏,需要在 pm-dlist.h
头文件中进行定义,于是最新版的 pm-dlist.h
内容如下:
#ifndef PM_DLIST_H #define PM_DLIST_H #include <glib-object.h> #define PM_TYPE_DLIST (pm_dlist_get_type()) typedef struct _PMDListNode PMDListNode; struct _PMDListNode { PMDListNode *prev; PMDListNode *next; void *data; }; typedef struct _PMDList PMDList; struct _PMDList { GObject parent_instance; PMDListNode *head; PMDListNode *tail; }; typedef struct _PMDListClass PMDListClass; struct _PMDListClass { GObjectClass parent_class; }; GType pm_dlist_get_type(void); #endif
与之前的 pm-dlist.h 相比,现在又多了两行代码:
#define PM_TYPE_DLIST (pm_dlist_get_type ()) GType pm_dlist_get_type(void);
显然, PM_TYPE_DLIST
这个宏是用来替代函数 pm_dlist_get_type
的,该函数的返回值是 GType 类型。
我们将 PM_TYPE_DLIST
宏作为 g_object_new
函数第一个参数,这就意味着向 g_object_new
函数传递了一个看上去像是在获取数据类型的函数。不难想到, g_object_new
之所以能够为实现对象的实例化,那么它必然要知道对象对应的 类的数据结构 , pm_dlist_get_type
函数的作用就是告诉它有 PMDList
类的具体结构。
那么 pm_dlist_get_type
函数应该如何实现?很简单,只需要再建立一个 pm-dlist.c 文件,内容如下:
#include "pm-dlist.h" G_DEFINE_TYPE(PMDList, pm_dlist, G_TYPE_OBJECT); static void pm_dlist_init(PMDList *self) { g_printf ("/t实例结构体初始化!/n"); self->head = NULL; self->tail = NULL; } static void pm_dlist_class_init(PMDListClass *klass) { g_printf ("类结构体初始化!/n"); }
这样,在源代码文件的层次上,pm-dlist.h 文件存放 PMDList 类的声明,而 pm-dlist.c 文件存放的是 PMDList 类的具体实现。
在上述的 pm-dlist.c 中,我们并没有看到 pm_dlist_get_type
函数的具体实现,这是因为 GObject 库所提供的 G_DEFINE_TYPE
宏可以为我们生成 pm_dlist_get_type
函数的实现代码。
G_DEFINE_TYPE
宏,顾名思义,它可以帮助我们最终实现 类 类型的定义。对于上例:
G_DEFINE_TYPE(PMDList, pm_dlist, G_TYPE_OBJECT);
G_DEFINE_TYPE
可以让 GObject 库的数据类型系统能够识别我们所定义的 PMDList 类 类型,它接受三个参数,第一个参数是类名,即 PMDList
;第二个参数则是类的成员函数(面向对象术语称之为”方法“或”行为“)名称的前缀,例如 pm_dlist_get_type
函数即为 PMDList 类的一个成员函数, pm_dlist
是它的前缀;第三个参数则指明 PMDList 类 类型的父类型为 G_TYPE_OBJECT
……嗯,这又是一个该死的宏!
也许你会注意到, G_TYPE_OBJECT
与我们自定义的宏 PM_TYPE_DLIST
非常相像。的确是这样, G_TYPE_OBJECT
指代 g_object_get_type
函数。
为了便于描述,我们可以将 PMDList 类和 GObject 类这种形式的 类 类型统称为 PT 类 类型,将 pm_dlist_get_type
和 g_object_get_type
这种形式的函数统称为 p_t_get_type
函数,并将 PM_TYPE_DLIST
和 G_TYPE_OBJECT
这样的宏统称为 P_TYPE_T
宏。这些只是一种约定。
若想让 GObject 库能够识别你所定义的数据类型,那么必须要提供一个 p_t_get_type
这样的函数。虽然你不见得非要使用 p_t_get_type
这样的函数命名形式,但是必须提供一个具备同样功能的函数。 p_t_get_type
函数的作用是向 GObject 库的类型管理系统提供 PT 类 类型的相关信息,其中包含 PT 类 类型的 实例结构体初始化函数 p_t_init
与 类结构体初始化函数 p_t_class_init
,例如上例中的 pm_list_init
与 pm_list_class_init
。
因为 p_t_get_type
函数是 g_object_new
函数的参数,当我们首次调用 g_object_new
函数进行对象实例化时, p_t_get_type
函数便会被 g_object_new
调用,从而引发 GObject 库的类型管理系统去接受 PT 类 类型(例如 PMDList 类型)的申请并为其分配一个类型标识码作。当 g_object_new
从 p_t_get_type
那里获取 PT 类 类型标识码之后,此时意味着 PT 类 的类结构体已经初始化完毕,然后便调用 p_t_init
进行对象的实例化。
这篇文章原本没打算涉及任何细节,结果有些忍不住。不过,倘若你顺利的阅读到这里,便已经掌握了如何使用 GObject 库在 C 语言程序中模拟面向对象程序设计中基于类的数据封装,并且我们完成了一个双向链表类 PMDList 的数据封装。
为了验证 PMDList 类是否可用,可以再建立一份 main.c 文件进行测试,内容如下:
#include "pm-dlist.h" int main(void) { int i; PMDList *list; /* 进行三次对象实例化 */ for (i = 0; i < 3; i++) { list = g_object_new(PM_TYPE_DLIST, NULL); g_object_unref(list); } /* 检查实例是否为 GObject 对象 */ list = g_object_new(PM_TYPE_DLIST, NULL); if (G_IS_OBJECT(list)) g_printf("/t这个实例是一个 GObject 对象!/n"); return 0; }
编译上述测试程序的命令为:
$ gcc $(pkg-config --cflags --libs gobject-2.0) pmd-dlist.c main.c -o test
测试程序的运行结果如下:
$ ./test 类结构体初始化! 实例结构体初始化! 实例结构体初始化! 实例结构体初始化! 实例结构体初始化! 这个实例是一个 GObject 对象!
从输出结果可以看出,PMDList 类的类结构体初始化函数只被调用了一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。这意味着, 所有实例共享的数据,可保存在类结构体中,而所有对象私有的数据,则保存在实例结构体中 。
main 函数中还使用了 G_IS_OBJECT
宏,来检测 list
是否为 G_TYPE_OBJECT
类型:
G_IS_OBJECT(list)
因为 PMDList 类继承自 GObject 类,那么一个 PMDList 对象自然是 G_TYPE_OBJECT
类型。
也许我讲的很明白,也许我一点都没有讲明白。但是使用 GObject 库模拟基于类的数据封装,或者用专业术语来说,即 GObject 类 类型的 子类化 ,念起来比较拗口,便干脆简称 GObject 子类化 ,其过程只需要以下四步:
在 .h 文件中包含 glib-object.h;
在 .h 文件中构建实例结构体与类结构体,并分别将 GObject 类的实例结构体与类结构体置于成员之首;
在 .h 文件中定义 P_TYPE_T
宏,并声明 p_t_get_type
函数;
在 .c 文件中调用 G_DEFINE_TYPE
宏产生类型注册代码。
也许让我们感到晕眩的,是那些命名约定。但是,从工程的角度来说,GObject 库所使用的命名风格是合理的,值得 C 程序员借鉴。
[1]
有关 GObject 的几份文档
[2]
Pascal 命名法
[3]
cloverprince 的 GObject 学习笔记
[4]
对象与实例的区别