TA的每日心情 | 奋斗 2016-10-11 09:20 |
---|
签到天数: 271 天 连续签到: 1 天 [LV.8]以坛为家I
|
本文转自:http://blog.mcuol.com/User/AT91RM9200/Article/9405_1.htm
Ryan McDougall(2004)
translated by neowillis
from http://www.mail-archive.com/gtk-list@gnome.org/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”。
/* “实例结构体”定义所有的数据域,实例对象将是唯一的 */- 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;
- }
复制代码- [/code]
-
- 还需要考虑的
-
- 我们已经用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())
-
- 注意:实际展开后的代码将随着系统版本不同而不同。你应该总是检查一下展开后的结果而不是凭主观臆断。
-
- 展开后的代码(清理了空格):
-
- [code]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",它是一个指针,指向我们打算创建对象的父类。当我们要找到虚方法继承自哪里时它会派上用场,可以用于链式触发处理/销毁函数(译者:下面会介绍)。这些处理/销毁函数几乎总是虚函数。我们接下来的代码将不再使用这个结构,因为有其它的函数能够不使用静态变量而做到这一点。
你应该注意到了,这个宏定义没有定义父类的初始化、销毁函数以及类对象的销毁函数。那么如果你需要这些函数,就得自己动手了。 |
|