目的
本文档可用于两个目的:一是作为一篇学习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”。
/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GTypeInstance gtype;
gint m_a;
gchar* m_b;
gfloat m_c;
};
/* “类结构体”定义所有的方法函数,类对象将是共享的 */
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"结尾,并且不被导出到头文件中),虚函数将被实现为指向这些函数。
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”宏来从类型系统中获取到类对象,用以调用虚函数表中的虚方法。非虚方法时,直接写实现代码即可。
/* 公有方法 */
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. 实现初始化/销毁方法。在这两个方法中,系统传入的参数是指向该对象的泛型指针(我们相信这个指针的确指向一个合适的对象),所以我们在使用它之前必须将其转型为合适的类型。
/* 该函数将在类对象创建时被调用 */
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将被保存在一个静态变量中,以后该函数再被调用时就无须注册可以直接返回该数值了。虽然可以使用一个独立的函数来注册该类型,但这样的实现可以保证类在使用前是注册过的,该函数通常在实例化第一个对象时被调用。
/* 因为该类没有父类,所以父类函数是空的 */
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;
}
/* 让我们来编写一个测试用例吧! */
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())
注意:实际展开后的代码将随着系统版本不同而不同。你应该总是检查一下展开后的结果而不是凭主观臆断。
展开后的代码(清理了空格):
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",它是一个指针,指向我们打算创建对象的父类。当我们要找到虚方法继承自哪里时它会派上用场,可以用于链式触发处理/销毁函数(译者:下面会介绍)。这些处理/销毁函数几乎总是虚函数。我们接下来的代码将不再使用这个结构,因为有其它的函数能够不使用静态变量而做到这一点。
你应该注意到了,这个宏定义没有定义父类的初始化、销毁函数以及类对象的销毁函数。那么如果你需要这些函数,就得自己动手了。
3.创建一个继承自GObject的对象
设计
尽管我们现在能够生成一个基本的对象,但事实上我们故意略过了本类型系统的上下文:作为一个复杂的开发库套件的基础 -那就是图形库GTK+。GTK+的设计要求所有的类应该继承自一个根类。这样就至少能允许一些公共的基础功能能够被共享:如支持信号(让消息可以很容易的从一个对象传递到另一个),使用引用计数来管理对象生命期,支持属性(针对对象的数据成员生成简单的setting和getting函数),支持构造和析构函数(用来设置信号、引用计数器、属性)。当我们让对象继承自GObject时,我们就获得了上述的一切,并且当与其它基于GObject的库交互时会很容易。然而,本章节我们不讨论信号、引用计数和属性,或者任何其它专门的特性,这里我们将详细描述类型系统中继承是如何工作的。
我们都知道,如果高档轿车继承自轿车,那么高档轿车就是轿车加上一些新的特性。那如何让系统去实现这样的功能呢?其实可以使用结构体的一个特性来实现:结构体里的第一个成员一定是在内存的最前面。只要我们要求所有的对象将它们的基类声明为它们自己结构体的第一个成员,那么我们就能迅速的将指向某个对象的指针转型为指向它基类的指针!尽管这个技巧很好用,并且语法上非常干净,但这种转型的方式只适用于指针 - 你不能这样来转型一个普通的结构体。
注意:这种转型技巧是类型不安全的。把一个对象转型为它的基类对象虽然合法但不明智。这将要求程序员自己来保障此次转型是安全的。
创建类型的实例
了解了这个技术后,那么究竟类型系统是如何实例化出对象的呢?第一次我们使用g_type_create_instance让系统创建一个*实例对象时,它必须要先创建一个*类对象供实例来使用。如果该类结构体继承自其它类,系统则需要先创建和初始化这些父类。系统依靠我们指定的结构体(*_get_type函数中的!GTypeInfo结构体)来完成这个工作,这个结构体描述了每个对象的实例对象大小,类对象大小,初始化函数和销毁函数。
- 要用g_type_create_instance来实例化一个对象
如果它没有相关联的类对象
创建类对象并且将其加入到类的层次中
创建实例对象并且返回指向它的指针
当系统创建一个新的类对象时,它先会分配足够的内存来放置这个最终的类对象(译者:“最终的”意指这个新的类对象,相对于其继承的父类们)。然后在继承链上从最顶端的父类开始到最末端的子类对象,用父类的成员域覆写掉这个最终类对象的成员域。这就是子类如何继承自父类的。当把父类的数据复制完后,系统将会在当前状态的类对象中执行父类的“base_init“函数。这个覆写和执行“base_init”的工作将循环多次,直到这个继承链上的每个父类都被处理过后才结束。接下来系统将在这个最终的类对象上执行最终子类的“base_init”和“class_init”函数。函数“class_init”有一个参数,即上文所提到的“class_data”,该参数会是构造函数的参数。
细心的读者可能会问,为什么我们已经有了一个完整的父类对象的拷贝还需要它的base_init函数?因为当完整拷贝无法为每个类重新创建出某些数据时,我们就需要base_init函数。例如,某个类对象成员指向了另外一个对象,拷贝后我们希望每个类对象的成员都指向它自己的对象,而不是只拷贝对象的指针(内存的拷贝只是“浅拷贝”,这时我们需要一次“深拷贝”)。但事实上有经验的GObject程序员告诉我base_init函数会很少用到。
当系统创建一个新的实例对象时,它会先分配足够的内存来将这个实例对象放进去。从继承链的最顶端的父类开始调用它的“instance_init”函数,直到最终的子类。最后,系统在最终类对象上调用最终子类的“instance_init”函数。
我来总结一下上面所描述到的算法:
- 实例化一个类对象
为最终对象分配内存
从父类到子类开始循环
复制对象内容以覆盖掉最终对象的内容
在最终对象上运行对象自己的base_init函数
在最终对象上运行最终对象的base_init函数
在最终对象上运行最终对象的class_init(附带上类数据)
- 实例化一个实例对象
为最终对象分配内存
从父类到子类开始循环
在最终对象上运行instance_init函数
在最终对象上运行最终对象的instance_init函数
此时创建的类对象和实例对象都已经被初始化,系统将实例对象的类指针指向到类对象,这样实例对象就能找到类对象所包含的虚函数表。这就是系统实例化已注册类型的过程,其实GObject实现的构造函数和析构函数语义与上述的方法也是相同的。
创建GObject实例
前面我们使用g_type_create_instance来创建一个实例对象。然而事实上GObject给我们提供了一个新的API来创建gobjects,可以完成我们上述所有的工作。这个API调用三个新的方法来创建和销毁新的GObject对象:构造函数(constructor),部署函数(dispose)以及析构函数(finalize)。
因为C语言缺少真正面向对象的语言所具备的多态特性,特别是认出多个构造函数的能力,所以GObject的构造函数需要一些更复杂的实现:
我们怎样才能灵活的传递不同种类的初始化信息到对象中,使得构造对象更容易呢?也许我们会想到限制只使用拷贝构造函数,然后用初始化数据填充一个静态的”初始化对象“,再将这个”初始化对象“传递到这个拷贝构造函数中。方法虽然简单,但是不太灵活。
事实上GObject的作者们提供了一种更加通用的解决方案,同时还提供了方便的getting和setting方法来操作对象成员数据,这种机制被称作”属性“。在系统中我们的属性用字符串来命名,并对它进行边界和类型检查。属性还可以被声明为仅构造时可写,就像C++中的const变量一样。
属性使用了一种多态的类型(GValue),这种类型允许程序员在不了解它实际类型的前提下安全的复制一个值。GValue会记录下它的值所持有的GType,使用类型系统来保证它总是具有一个虚函数,该函数可以处理将其自身复制到另一个GValue或转换为另一种GType。我们将在下一章详细讨论GValues和属性。
要为一个GObject创建一个新的属性,我们要定义它的类型、名字,以及默认值,然后创建一个封装这些信息的“属性规格”对象。在GObject的类初始化函数中,我们可以通过g_object_class_install_property来将属性规格绑定到GObject的类对象上。
注意:任何子对象要添加一个新的属性必须覆盖它从GObject继承下来的set_property和get_property虚方法。将在下一节中介绍这两个方法。
使用属性我们可以向构造函数传递一组属性规格,附上我们希望的初始值,然后简单调用GObject的set_property,这样就能获得属性带给我们的神奇功效。但是事实上,构造函数是不会被我们直接调用的。
GObject构造函数另一个不是那么明显的特性是,每个构造函数需要接受一个GType作为其参数之一,并且当它向上转型为其父类时,需要将这个GType传递给它父类的构造函数。这是因为 GObject的构造函数使用子类的GType来调用g_type_create_instance,这样GObject的构造函数必须要知道它的最终子类对象的GType。
注意:如果我们自己定义构造函数,我们则必须覆盖继承自父类的构造函数。自定义的构造函数必须得沿着“继承链”向上,在做任何其他的工作前,先调用完父类的构造函数。然而,因为我们使用了属性,所以事实上我们从来不用覆盖掉默认的构造函数。
我要为上面的离题而道歉,但是这是为了理解系统是如何工作的所必须要克服的困难。如上所述,我们现在能理解GObject的构造函数了-g_object_new。这个函数接受一个子类的GType类型,一系列属性名(字符串)和GValue对作为参数。
这一系列属性对被转换为键值对列表和相关的属性规格,这些属性规格将被在类初始化函数里被安装到系统中。调用类对象的构造函数时系统传入GType和构造属性。从最底端的子类构造函数到最顶端的基类构造函数,这条链会一直被触发直到GObject的构造函数被执行 - 这实际上才是第一个真正执行的初始化程序。GObject的构造函数现调用g_type_create_instance,并传下我们通过g_object_new一路带上的GType,这样我们上面所描述的细节将会发生,最终创建出实例。然后它将获得最终对象的类,并对传入所有构造属性调用set_property方法。这就是为什么我们加入一个新属性时必须要覆盖get_/set_property方法的原因。当这一串构造函数返回后,包含在其中的代码将从基类执行到子类。
当父类构造函数返回后,就轮到子类来执行它自己的初始化代码了。这样执行代码的顺序就成为:
* 从GObject到ChildObject运行实例初始化函数
* 从GObject到ChildObject运行构造函数
最后任何剩余的没有传递到构造函数的属性将使用set_property方法一次设置完毕。
读者也许会考虑在什么情况下需要覆盖默认构造函数,将自己的代码放到他们自己的构造函数里。因为我们所有的属性都可以使用虚方法set_property来设置,所以基本上没有覆盖GObject的默认构造函数的必要。
我仍尝试使用伪码的方式来总结一下GObject的构造函数过程:
- 使用属性键值对列表创建合适的GObject对象:
在键值对列表中查找对应的属性规格
调用最终对象的构造函数并传入规格列表和类型
递归的向下调用直到GObject的构造函数
对传入的类型调用g_type_create_instance
对属性规格列表调用虚方法set_property
对剩下的属性,调用set_property
注意:GObject将属性区分为两类,构造属性和“常规”属性。
销毁GObject实例
该做的工作完成后,我们可以看看要清理这个对象时会发生些什么。GObject实现面向对象中的析构时,将其分解成了两步:处理(dispose)和销毁(finalize)。
"处理"方法在对象知道自己将要被销毁时调用。实现该方法时,应该将指向资源的引用释放掉,这样可以避免循环引用或资源稀缺。“处理”方法应该允许被调用多次。要实现这一点,一般的做法是使用一个静态变量来保护”处理“方法。在“处理”方法调用后,对象本身应该依然能够使用,除非产生了不可恢复的错误(如段错误)。因此,“处理”方法不能释放或者改动某些对象成员。对于可恢复的错误,例如返回错误码或者空指针,则不应该受影响。
“销毁”方法会在从内存中清理掉对象之前被调用,用于释放剩余的资源引用。因此它只能被调用一次。析构过程被分成两个步骤降低了引用计数策略中循环引用发生的可能。
注意:如果我们自定义“处理”和“销毁”方法,就必须要覆盖掉继承自父类的相同方法。这两个方法从子类开始调用,沿着继承链向上直到最顶端的父类。
与构造函数不同的是,只要新的对象分配了资源,我们就需要覆盖掉继承自父类的相同方法,自己实现“处理”和“销毁”方法。
判断销毁代码放置到哪个函数不是件容易的事。一般来说,当与实现了引用计数的库(如GTK+)打交道时,我们应该在“处理”方法中解除对其它资源对象的引用,而在“销毁”方法中释放掉所有的内存或者关闭所有的文件描述字。
上面我们讨论过g_object_new,但是我们什么时候来销毁这些对象呢?其实上面也有提示过,GObject使用了引用计数的技术,它保存了一个整型的数据,该数据描述了有多少个对象或函数现在正在使用或者引用这个对象。当你在使用GObject时,如果你希望新创建的对象不在使用时被销毁掉,你就必须及早调用g_object_ref,将对象作为参数传递给它,这样就为引用计数器增加了1。如果你没有做这件事就意味着对象允许被自动销毁,这也许会导致你的程序崩溃。
同样的,当对象完成了它的任务后,你必须要调用g_object_unref。这样会使引用计数器减1,并且系统会检查它是否为0.当计数器为0时,对象将被先调用“处理”方法,最终被“销毁”掉。如果你没有解除到该对象的引用,则会导致内存泄漏,因为计数器永远不会回到0。
现在我们已经准备好了来写一些代码了!不要让上面冗长复杂的描述吓到您。如果你没有完全理解上面所提到的,别紧张 - GObject的确是很复杂的!继续读下去,你会看到许多细节,试试一些例子程序,或者去睡觉吧,明天再来接着读。
下面的程序与第一个例子很相似,事实上我去掉了更多的不合逻辑的、冗余的代码。
代码(头文件)
a. 我们仍然按照上面的方式继续,但是这次将把父类对象放到结构体的第一个成员位置上。事实上就是GObject。
/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GObject parent_obj;
/* 下面是一些数据 */
};
/* “类结构体”定义所有的方法函数,类对象将是共享的 */
typedef struct _SomeObjectClass SomeObjectClass;
struct _SomeObjectClass
{
GTypeClass parent_class;
/* 下面是一些方法 */
};
b. 头文件剩下的部分与第一个例子相同。
代码(源文件)
注意:我们需要增加一些对被覆盖的GObject方法的声明。
/* 这些是GObject的构造和析构方法,它们的用法说明在gobject.h中 */
void some_object_constructor(GType this_type,
guint n_properties,
GObjectConstructParam *properties)
{
/* 如果有子类要继承我们的对象,那么this_type将不是SOME_OBJECT_TYPE,
g_type_peek_parent再是SOME_OBJECT_TYPE的话,将会造成无穷循环 */
GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent (SOME_OBJECT_TYPE()));
parent_class-> constructor (self_type, n_properties, properties);
/* 很少需要再做其它工作 */
}
void some_object_dispose (GObject *self)
{
GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent(SOME_OBJECT_TYPE()));
static gboolean first_run = TRUE;
if (first_run)
{
first_run = FALSE;
/* 对引用的所有GObject调用g_object_unref,但是不要破坏这个对象 */
parent_class-> dispose (self);
}
}
void some_object_finalize (GObject *self)
{
GObjectClass *parent_class = g_type_class_peek (g_type_peek_parent(SOME_OBJECT_TYPE()));
/* 释放内存和关闭文件 */
parent_class-> finalize (self);
}
注意:GObjectConstructParam是一个有两个成员的结构体,一个是一组!GParamSpec类型,用来描述参数定义,另一个是一组GValue类型,是对应参数的值。
/* 这是GObject的Get和Set方法,它们的用法说明在gobject.h中 */
void some_object_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
}
void some_object_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
}
/* 这里是我们覆盖函数的地方,因为我们没有定义属性或者任何域,下面都是不需要的 */
void some_object_class_init (gpointer g_class,
gpointer class_data)
{
GObjectClass*this_class = G_OBJECT_CLASS (g_class);
this_class-> constructor = &some_object_constructor;
this_class-> dispose = &some_object_dispose;
this_class-> finalize = &some_object_finalize;
this_class-> set_property = &some_object_set_property;
this_class-> get_property = &some_object_get_property;
}
要想讨论关于创建和销毁GObject,我们就必须要了解属性和其它特性。我将把操作属性的示例放到下一节来叙述。以避免过于复杂而使得你灰心丧气。在你对这些概念有些实作经验后,它们将开始显现出来存在的意义。如上面所言,我们现在只是将自己限制在创建一个基础的GObject类,在下一节我们将真正的编写一些函数。 重要的是我们获得了让下面的学习更轻松的工具。
4.属性
上面已经提到属性是个很奇妙的东西,也简单介绍了如何使用它。在进一步深入介绍属性之前,我们又得先离一会儿题。
GValues
C 是一门强类型语言,也就是说变量声明的类型必须和它被使用的方式保持一致,否则编译器就会报错。这是一件好事,它使得程序编写起来更迅速,帮助我们发现可能会导致系统崩溃或者不安全的因素。但这又是件坏事,因为实际上程序员活在一个很难什么事都保持严格的世界上,而且我们也希望声明的类型能够具备多态的能力 -也就是说类型能够根据上下文来改变它们自己的特性。通过C语言的转型我们可以获得一些多态的能力,如上面所讨论过的继承。然而,当使用无类型指针作为参数传递给函数时,可能问题会比较多。幸运的是,类型系统给了我们另外一个C语言没有的工具:GType。
让我们更清楚的描述一下问题吧。我需要一种数据类型,可以实现一个可以容纳多类型元素的链表,我想为这个链表编写一些接口,可以不依赖于任何特定的类型,并且不需要我为每种数据类型声明一个多余的函数。这种接口必然能涵盖多种类型,所以我们称它为GValue(Generic Value,泛型)。该如何实现这样一个类型呢?
我们创建了封装这种类型的结构体,它具有两个成员域:所有基础类型的联合(union),和表示保存在这个union中的值的GType。这样我们就可以将值的类型隐藏在GValue中,并且通过检查对 GValue的操作来保证类型是安全的。这样还减少了多余的以类型为基础的操作接口(如get_int,set_float,...),统一换成了g_value_*的形式。
细心的读者会发现每个GValue都占据了最大的基础类型的内存大小(通常是8字节),再加上GType自己的大小。是的,GValues在空间上不是最优的,包含了不小的浪费,因此不应该被大量的使用它。它最常被用在定义一些泛型的API上。
属性是如何工作的这一点稍稍超出了我们要讨论的范围,但是这对于理解属性本身还是很有帮助的。
/* 让我们使用GValue来复制整型数据! */
#define g_value_new(type) g_value_init (g_new (GValue, 1), type)
GValue *a = g_value_new (G_TYPE_UCHAR);
GValue *b = g_value_new (G_TYPE_INT);
int c = 0;
g_value_set_uchar (a, ''a'');
g_value_copy (a, b);
c = g_value_get (b);
g_print ("w00t: %d/n", c);
g_free (a);
g_free (b);
设计
我们已经在上面接触过属性了,对它们有了初步的认识,现在我们将继续来了解一下设计它们的最初动机。 要编写一个泛型的属性设置机制,我们需要一个将其参数化的方法,以及与实例结构体中的成员变量名查重的机制。从外部上看,我们希望使用C字符串来区分属性和公有API,但是内部上来说,这样做会严重的影响效率。因此我们枚举化了属性,使用索引来标识它们。
上面提过属性规格,在Glib中被称作!GParamSpec,它保存了对象的gtype,对象的属性名称,属性枚举ID,属性默认值,边界值等,类型系统用!GParamSpec来将属性的字符串名转换为枚举的属性 ID,GParamSpec也是一个能把所有东西都粘在一起的大胶水。
当我们需要设置或者获取一个属性的值时,传入属性的名字,并且带上GValue用来保存我们要设置的值,调用g_object_set/get_property。g_object_set_property函数将在GParamSpec中查找我们要设置的属性名称,查找我们对象的类,并且调用对象的set_property方法。这意味着如果我们要增加一个新的属性,就必须要覆盖默认的set/get_property方法。而且基类包含的属性将被它自己的set/get_property方法所正常处理,因为!GParamSpec就是从基类传递下来的。最后,应该记住,我们必须事先通过对象的class_init方法来传入GParamSpec参数,用于安装上属性!
假设我们已经有了如上一节所描述的那样一个可用的框架,那么现在让我们来为SomeObject加入处理属性的代码吧!
代码(头文件)
a. 除了我们增加了两个属性外,其余同上面的一样。
/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */
typedef struct _SomeObject SomeObject;
struct _SomeObject
{
GObject parent_obj;
/* 新增加的属性 */
int a;
float b;
/* 下面是一些数据 */
};
代码(源文件)
a. 创建一个枚举类型用来内部记录属性。
enum
{
OBJECT_PROPERTY_A = 1 << 1;
OBJECT_PROPERTY_B = 1 << 2;
};
b. 实现新增的处理属性的函数。
void some_object_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
{
SomeObject *self = SOME_OBJECT (object);
switch (property_id)
{
case OBJECT_PROPERTY_A:
g_value_set_int (value, self-> a);
break;
case OBJECT_PROPERTY_B:
g_value_set_float (value, self-> b);
break;
default: /* 没有属性用到这个ID!! */
}
}
void some_object_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
{
SomeObject *self = SOME_OBJECT (object);
switch (property_id)
{
case OBJECT_PROPERTY_A:
self-> a = g_value_get_int (value);
break;
case OBJECT_PROPERTY_B:
self-> b = g_value_get_float (value);
break;
default: /* 没有属性用到这个ID!! */
}
}
c. 覆盖继承自基类的set/get_property方法,并且传入GParamSpecs。
/* 这里是我们覆盖函数的地方 */
void some_object_class_init (gpointer g_class, gpointer class_data)
{
GObjectClass *this_class = G_OBJECT_CLASS (g_class);
GParamSpec *spec;
this_class-> constructor = &some_object_constructor;
this_class-> dispose = &some_object_dispose;
this_class-> finalize = &some_object_finalize;
this_class-> set_property= &some_object_set_property;
this_class-> get_property = &some_object_get_property;
spec = g_param_spec_int
(
"property-a", /* 属性名称 */
"a", /* 属性昵称 */
"Mysterty value 1", /* 属性描述 */
5, /* 属性最大值 */
10, /* 属性最小值 */
5, /* 属性默认值 */
G_PARAM_READABLE |G_PARAM_WRITABLE /* GParamSpecFlags */
);
g_object_class_install_property(this_class,OBJECT_PROPERTY_A, spec);
spec = g_param_spec_float
(
"property-b", /* 属性名称 */
"b", /* 属性昵称 */
"Mysterty value 2" /* 属性描述 */
0.0, /* 属性最大值 */
1.0, /* 属性最小值 */
0.5, /* 属性默认值 */
G_PARAM_READABLE |G_PARAM_WRITABLE /* GParamSpecFlags */
);
g_object_class_install_property (this_class, OBJECT_PROPERTY_B, spec);
}
链接地址:http://blog.mcuol.com/User/AT91RM9200/Article/9619_1.htm