面向对象是一种游戏规则,它不是游戏。C++只是面向对象的一种开发语言。
很多人在学校里面
1. 学习了C语言,知道这是写“面向结构的”
2. 学习了C++,知道这是面向对象的;
如果这个时候再接触GObject, 就会觉得很怪异,为啥用C语言去实现一个面向对象的机制呢。
老是拿C++ VS GObject,老是觉得GObject浑身是刺,不好用,复杂的很!
如果你学过C语言之后,学校教面向对象,之后叫GObject, 你就不会觉得GObject是个异类了。
C++中用了一个struct, 里面包含了:成员变量,成员函数
GObject用了2个struct, 一个包含成员变量,另外一个包含成员函数;
C和面向对象,这不就是C++么?为什么要搞出另一套东西,而不直接使用C++呢?关于C与C++之争是一个大坑。Linux之父Linus就是力挺C而批判C++的。讨厌C++的人似乎认为C++过于复杂,内部机制陷阱过多等等。自己的经历不多,用C++也很少,达不到大牛们的境界,如果让我给个非要用C而不用C++的理由,我也给不出一个有说服力的。
最原始的动力是,我在使用GTK+进行开发,而GObject是GTK+的基石。如果基础不牢,上层一定不会稳,因此很有必要把GObject给过一遍。知道了它的内部,才知道该如何使用它,明白它的机制与原理,做到心中有数。
但是研究GObject能带来更多。由于C里没有任何面向对象机制,因此GObject把这些机制全部实现了一遍。从中可以看到一些机制的实现原理,从而对面向对象有更多的理性了解。
面向对象的最基本需求就是封装。所谓封装,按我的理解,就是将一系列相关数据,及对这些数据有关的操作,有序的组织在一个结构中。一个圆形有x坐标、y坐标、半径三个参数,我们可以用这三个变量表示一个圆:
1
|
double x, y, radius; |
这没什么问题。现在多了一个圆,我们又要用三个变量:
1
|
double x1, y1, radius1; |
当我们有很多个圆的时候,可能要用到数组:
1
|
double x[100], y[100], radius[100]; |
问题在哪?x、y和radius是相互独立的。我完全可以定义100个x,200个y,150个radius。如果不只有圆,还有矩形,那么矩形的坐标叫什么呢?xx、yy?等你写了一堆代码之后回来看,到底x和y是圆的坐标,还是xx和yy是圆的坐标?
所以有了struct。一个struct对数据进行了很自然的封装:
1
2
3
|
struct Circle { double x, y, radius; }; |
好了,现在我们有了Circle这个类型。这个类型将圆的三个参数封装到了一起,从现在开始它们就是一个整体了。我可以很自然的声明一个圆,而不是它的三个参数:
1
|
struct Circle c; |
我们也不用担心x、y、z的数量不等了,更不用担心坐标和矩形坐标命名冲突——它们定义在Rectangle这个struct里呢:)。
事情还没有完。有了圆这个类型,那么对圆的操作呢?假设一个圆的操作之一为移动(move)。我们可以定义如下函数:
1
2
3
4
|
void circle_move ( struct Circle *self, double x, double y) { self->x = x; self->y = y; } |
我们输入一个圆的指针,以及新的x、y坐标,移动操作帮助我们把指定的圆移动到新的坐标上。注意第一个参数self,是不是有点眼熟?它就是C++里的this。记得学C++时很多同学对this理解相当困难,如果看这个self就不难理解了:self就是我们要操作的那个变量,它是一个指针。C++在对象方法调用时省略了这个参数,它可以被编译器自动设置。在C里面,这个工作要我们自己做。因此移动一个圆要这么调用:
1
2
|
struct Circle cir; circle_move (&cir, 10.0, 5.0); |
注意self是个指针,因为C里没有引用,所以我们只能使用指针来达到传递一个对象,而不是传递它的复制品的效果。
这个方法……不就是普通的函数调用嘛,根本就没把操作给封装呀。好,现给一个看起来像C++中的方法:
1
2
3
4
5
6
7
8
|
struct Circle { double x, y, radius; void (*move) ( struct Circle *self, double x, double y); }; ... struct Circle cir; cir.move = circle_move; cir.move (&cir, 10.0, 5.0); |
通过函数指针,可以让move调用看起来更像C++了。但是,有两个不爽的地方。其一,要显式地将circlemove函数赋值给move函数指针,如果有5个圆,那就要5行指定的代码(除非用数组+循环)。更为严重的是我们可以为不同的变量指定不同move操作。其二,调用时依然要显示地指定self,这带来的一个后果是,我们完全可以调用cir1的move,但是传入的是cir2的指针。
对于第一点,可以使用类结构+初始化函数来解决。对于第二点,C语言是没法避免显示的传入self指针(如果可以的话请告诉我)。因此这种写法只是“像”C++而已,没啥实际的好处。不过在之后我们会看到,GObject会在类结构中使用函数指针来表示对象的操作。
struct _GObject { GTypeInstance g_type_instance; /*< private >*/ volatile guint ref_count; GData *qdata; };
struct _GObjectClass { GTypeClass g_type_class; /*< private >*/ GSList *construct_properties; /*< public >*/ /* seldomly overidden */ GObject* (*constructor) (GType type, guint n_construct_properties, GObjectConstructParam *construct_properties); /* overridable methods */ void (*set_property) (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); void (*get_property) (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); void (*dispose) (GObject *object); void (*finalize) (GObject *object); /* seldomly overidden */ void (*dispatch_properties_changed) (GObject *object, guint n_pspecs, GParamSpec **pspecs); /* signals */ void (*notify) (GObject *object, GParamSpec *pspec); /* called when done constructing */ void (*constructed) (GObject *object); /*< private >*/ gsize flags; /* padding */ gpointer pdummy[6]; };
http://zh.wikipedia.org/wiki/GObject
每個 GObject 類別必須包含至少兩個結構:類別結構與實體結構。
C 結構沒有像 "public", "protected" 或 "private" 等的存取層級修飾,一般的方法是藉著在實體結構裡提供一個指向私有資料的指標,照慣例稱作 _priv。私有的結構可以宣告在公有的表頭檔案裡,然後把實體的定義寫在實作的檔案中,這樣作,對使用者來說,他並不知道私有資料是什麼,但對於實作者來說,卻可以很清楚的知道。如果私有結構也註冊在 GType 裡,那麼物件系統將會自動幫它配置空間。
GObject 框架最主要的不利點在於太過於冗長。像是手動定義的類型轉換巨集和難解的型別註冊咒語等的大量模板代碼都是建立新類別所必要的。GObject Builder 或 GOB2 這些工具試圖以提供樣板語法來解決這個問題。以 GOB2 寫的代碼必須事先處理過才能編譯。另外,Vala 可以將 c# 的語法轉換成 C,並編譯出獨立的二進制檔。
http://blog.csai.cn/user1/265/archives/2006/3301.html
注:此文只许被引用,非经作者同意不得擅自转载
一、概述
gtk是一套跨多种平台的图形工具包,虽然它是采用c语言来编写的,但是gtk显然具有很好的面向对象特性。为了今后自己能更好的使用gtk或扩展gtk系统,在这里俺对gtk对象系统与c++对象系统展开一个综合的比较。
二、支持面向对象的核心引擎
面向对象是一种思想!众所周知,诸如C++、java等语言,我们称它为面向对象的编程语言!那么究竟何谓“面向对象的编程语言”呢?简言之,能够从语言级别上较好的支持“面向对象”特性的,都可以称其为面向对象的编程语言。也即在编译器内部对这种语言予以了面向对象特性的直接支持,如抽象(反映一般和特殊的关系)和继承,聚合(反映局部和整体的关系)和封装等。
但是请注意,对面向对象编程方法的运用,不一定非要求我们必须采用支持“面向对象”的编程语言。因为面向对象编程是一种思想和方法,它本身与某种编程语言并无直接映射关系,它也更不会去依赖某种编程语言。或者换句话说,如果你选择了某种很好地支持了面向对象特性的编程语言,那么你也许能够更轻松地运用到一些面向对象的编程方法,去很好的解决你的业务问题。
因此,gtk虽然是采用了不支持面向对象特性的c语言所编写,但这并不影响gtk也具有了很好的面向对象特性。当然,与C++对象系统相比较,为了使gtk系统具有面向对象的特性,它就必须自己来实现那些由“c++编译器”所实现的对面向对象的支持。总结为一句话:C++对“面向对象”的支持是在“c++编译器”那里给实现的;而gtk系统中对“面向对象”的支持则必须由它自己来亲自实现,也即gtk面向对象的引擎是在gtk的运行库中,例如“类型”的创建、继承关系、“类型”的映射等!
总之,我们需要铭记一点:在gtk系统中,它在运行时刻,会维护一个全局的、大型的、能够反映所有继承关系的“类型关联表”。
三、共同的基类
gtk对象系统中所有的对象都有一个共同的基类,这一点很容易做到;而c++则没有;但java语言中有。
四、如何实现继承,扩展出子类
1、gtk系统的继承是利用struct关键字,它与c++中的class含义如出一辙!下面对重要的几点逐一说明一下:
2、c++中定义一个“类型”时,“成员变量”和“成员函数”都放在class数据结构之中,c++编译器会知道分别该怎么处理它。而gtk对象系统中,定义一个“类型”时,它的“成员变量”和“成员函数”应该显式将它们分开的,原因是“成员变量”的聚合才代表真正的“对象”,它可能是需要被实例化出很多的对象实例;而“对象类”则是“成员函数”的聚合,它表示对该类“对象”中成员变量的处理操作的统一封装,而且“对象类”只需要被实例化一次。示例如下:
//gtkcalendar.h文件中
struct _GtkCalendar
{
//第一个字段表示基类
GtkWidget widget;
//下面是其它数据字段("成员变量"的聚合)
...
}
struct _GtkCalendarClass
{
//第一个字段表示基类
GtkWidgetClass parent_class;
//下面是其它数据字段("成员函数"的聚合)
...
}
3、与c++中不同,由于gtk对象系统中必须自己维护所有继承关系的“类型关联表”,因此除了上面在.h文件中定义了类接口以外,它还必须在.c文件中来显式地实现这种关联。如下示例:
GType
gtk_calendar_get_type (void)
{
static GType calendar_type = 0;
if (!calendar_type)
{
static const GTypeInfo calendar_info =
{
sizeof (GtkCalendarClass),
NULL, /* base_init */
NULL, /* base_finalize */
//指定类型的初始化函数,每种“类型”只被实例化一次。
//在某种“类型”第一次被使用到时,它被实例化,这个函数同时也被调用
(GClassInitFunc) gtk_calendar_class_init,
NULL, /* class_finalize */
NULL, /* class_data */
sizeof (GtkCalendar),
0, /* n_preallocs */
(GInstanceInitFunc) gtk_calendar_init,//注意,这里指定对象的构造函数
};
// 注意!下面这条语句非常关键,尤其是第一个参数的指定,实际上它才代表“类型”的真正继承关系。
// GTK_TYPE_WIDGET代表一个"类型id",每种类型都有唯一对应的一个id,实际上,只有通过这个id才
// 可以访问到gtk系统中"类型”数据结构;而只有访问到了"类型”数据结构,gtk核心才知道该怎么去
// 创建实例化对象(如对象的尺寸大小等信息)
calendar_type = g_type_register_static (GTK_TYPE_WIDGET, "GtkCalendar",
&calendar_info, 0);
}
return calendar_type;
}
3、鉴于习惯的原因,在下文中,我们对gtk系统中相当于c++中“对象”的数据结构称为“构件”,如上面的_GtkCalendar;而相当于c++中“对象类”的gtk数据结构称为“构件类”,如上面的_GtkCalendarClass。
五、“成员变量”的可见性
1、在c++中,“成员变量”的可见性有public、protected、private三种。其实public属性的成员变量基本上不采用,因为我们坚决要杜绝这样做。
2、那么在gtk系统中,“成员变量”的可见性应该怎样控制呢?这里建议你按照如下规则:
(1)public的gtk不支持
(2)protected的,可以直接放在“构件”的数据字段中,它表示可以允许子类访问它
(3)对于需要private属性的,我们应该在“构件”的数据字段中定义一个特殊的字段,如_GtkCalendar构件中的gpointer private_data;再在这个构件的实现文件中定义这个只能被这个构件自己所访问并操作的“私有数据结构”体,如_GtkCalendar构件中的_GtkCalendarPrivateData。
(4)我们可以核对一下现在的gtk系统中每个gtk构件是不是都遵循以上的规则。
六、“成员函数”的可见性
1、同样,在c++中,“成员函数”的可见性也是有public、protected、private三种。public表示外部可见;protected表示子类可见;private表示只有自己可见。
2、在gtk系统中,“成员函数”的可见性又该如何来控制呢?有如下规则或建议:
(1)public属性的表示gtk的外部访问接口。注意!它虽然在.h文件中做声明,但是它并不是(也不能)在“构件类”中进行声明的字段,所以与c++中public属性的“成员函数”相对比,gtk中的这类的外部访问接口函数,它不能被子类所覆盖或重载。也正因为如此,才导致gtk系统的外部访问接口函数简直是忒多忒多了,个人认为这是gtk对象系统中所不可避免的最大缺陷,因为使用者要花很多的精力来学习它和熟悉它。另外,gtk中的这类的外部访问接口函数在声明时,第1个参数都会是“构件”自身,这相当与c++系统中隐式的this指针。示例如下:
//gtkcalendar.h文件中
//gtk_calendar_select_month相当于c++中public属性的“成员函数”,而GtkCalendar *calendar则相当于c++函数中隐式的this指针
gboolean gtk_calendar_select_month (GtkCalendar *calendar,
guint month,
guint year);
(2)请问,c++中protected属性的“成员函数”在gtk系统中如何来体现呢?其实不难理解,那就是那些直接在“构件类”中所声明的数据字段,它们才是来扮演protected属性“成员函数”这个重要角色的功能。当然值得注意的是,这类函数在声明时,都是一个个指向函数的指针(gtk系统中,把它们习惯称为“信号处理函数”,Signal handlers),而不是声明函数本身。其原因就是因为这类函数需要或允许我们能够在子类覆盖或重载它,有时甚至要求某些函数具有“多态性”,而在c++中,语言自身(如virtual所声明的虚函数)就具备了多态性,但在c语言所实现的gtk系统中,我们则可以通过“函数指针”来实现这种“多态性”,在后面的有关内容中,会进一步讨论它。示例如下:
//gtkcalendar.h文件中
struct _GtkCalendarClass
{
GtkWidgetClass parent_class;
/* Signal handlers */
void (* month_changed) (GtkCalendar *calendar);//相当于c++中protected属性的“成员函数”,同样第一个函数参数也是构件自身
void (* day_selected) (GtkCalendar *calendar);
...
};
(3)private属性的成员函数,好象在gtk系统中没有相对应的(似乎也感觉这类函数没多大必要)!其实不然,那些没在.h文件中所声明的,而仅在.c文件中所声明并使用的static函数,我们便可以把它们理解为c++中private属性的“成员函数”,个人认为它们扮演的角色差不多。
七、“成员函数”怎样实现覆盖和多态
1、这里所讨论的“成员函数”都是指在“构件类”中所声明的那些函数指针(从作用和角色上来考量,它相当于c++中protected属性的“成员函数”)
2、gtk系统中的这类函数,我们可以认为它都是多态的虚函数(相当于c++中用virtual关键字所声明的虚函数)
3、gtk系统中,我们如何实现这类函数的覆盖呢?(c++中对成员函数如何实现覆盖,在这里就不浪费笔墨了)
(1)首先在.c文件中声明并实现一个相应的static类型的信号回调函数,如gtkcalendar.c文件中所定义的gtk_calendar_realize函数
(2)接着在我们就可以在“构件类”的初始化函数(如GtkCalendarClass“构件类”中的gtk_calendar_class_init函数,这个在前面提到过,它在gtk_calendar_get_type函数中被指定)内做文章了。也即如果我们想覆盖某个函数,只需简单的给某个“构件类”中的相应“函数指针”进行赋值即可。在gtk系统中,我们把这样的操作通常称为对某类事件信号进行了响应处理,其结果就是对父类的事件响应函数进行了覆盖。
(3)从上面可以看出,对一个所谓的“成员函数”实现覆盖是很容易做到的,但问题是?子类在覆盖父类某个函数之后,子类有时又想调用父类的一些函数!对于这样的要求,在c++中,我们可以用域作用符(“::”)来实现函数的静态绑定;那么请问gtk系统中,这又如何实现呢?其实不难,有如下的步骤:
a. 首先在.c文件中声明一个static类型的父类指针,如GtkCalendar“构件”有如下声明:
//gtkcalendar.c文件中
static GtkWidgetClass *parent_class = NULL;
b. 接着在“构件类”的初始化函数中(gtk_calendar_class_init函数),对上述parent_class进行赋值,如下:
//gtkcalendar.c文件中
static void gtk_calendar_class_init (GtkCalendarClass *class)
{
...
parent_class = g_type_class_peek_parent (class); //获得了父类的“构件类”
...
}
c. 最后,就可以在你自己的“成员函数”中,任意使用父类作用域内的“成员函数”了,示例如下:
//gtkcalendar.c文件中
static void gtk_calendar_finalize (GObject *object)
{
GtkCalendarPrivateData *private_data;
private_data = GTK_CALENDAR_PRIVATE_DATA (object);
g_free (private_data);
//释放自己构件的资源之后,接着调用父类的finalize函数,以便父类构件能释放它曾经所申请并获得的资源
(* G_OBJECT_CLASS (parent_class)->finalize) (object);
}
八、构造函数和析构函数
1、c++面向对象编程一个特出的优点就是每个对象都有相应的构造函数和析构函数。这为我们有效管理“资源”带来了极大的便利,我们通常可以在构造函数中申请资源,而在析构函数中释放资源。
2、在gtk系统中,当然也有构造函数,如_GtkCalendar“构件”的构造函数便是gtk_calendar_init函数,它是在gtk_calendar_get_type函数内定义的GTypeInfo数据结构中被指定。
3、在gtk系统中,析构函数同样也是存在的,但是它不是在GTypeInfo数据结构中被指定的,想想这是为什么?个人分析,其原因有二:首先是没有必要必须在GTypeInfo数据结构中来指定它;其二,就是因为析构函数一般都应该是虚函数,这样系统会更健壮些,也很严谨,且简单。所以gtk系统中,析构函数被声明为“构件类”中的finalize事件信号,请参阅gobject.h文件中的_GObjectClass数据结构的定义
九、对象类型的转换
1、在c++中,对象类型的转换有2种。一种是子类向父类的转换,这是隐式的,也即可以自动完成;还有一类就是父类向子类的转换,这不是隐式的,而必须是显式的,也即它需要RTTI信息,当然你也可以不借助RTTI,而野蛮地进行类型间的强制转换,但这绝对是C++中不提倡的。
2、在gtk对象系统中,“构件”之间类型的转换当然也会有子类向父类的转换,以及父类向子类的转换等2种,但是与c++对象系统不同的是,gtk构件之间类型的转换都必须是显式的,无论是子类向父类的转换,还是父类向子类的转换。这是因为gtk系统中对面向对象的支持都是靠自身来实现的,而没有编译器语言级别上的面向对象的支持。所以它的这种转换都必须借助于gtk运行库中RTTI的支持。
3、每个gtk构件的.h文件中一开始的所声明的几个宏,它们都是用来支持RTTI类型转换的,示例如下:
#define GTK_TYPE_CALENDAR (gtk_calendar_get_type ())
//用的最多的就是下面的这个宏了,它实现构件的类型转换,返回一个指针
#define GTK_CALENDAR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_CALENDAR, GtkCalendar))
#define GTK_CALENDAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_CALENDAR, GtkCalendarClass))
#define GTK_IS_CALENDAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_CALENDAR))
#define GTK_IS_CALENDAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_CALENDAR))
#define GTK_CALENDAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_CALENDAR, GtkCalendarClass))
十、最后简单讨论一下对gtk构件的使用:构件的创建和构件的销毁
1、在c++中,实例化一个对象时,可以用new关键字来从堆上动态地创建出,也可以在栈上创建一个临时的对象实例(局部变量);而在gtk系统中,所有的构件都会是在堆上被创建出的,并且对于每一个构件的创建,它都会声明有一个专门的外部接口函数,如gtk_calendar_new()则用于创建一个_GtkCalendar“构件”
2、在c++中,对象实例如果在栈上,那么被自动销毁;如果在堆上,那么则必须通过delete关键字来显式地销毁某一个对象实例。在对象实例被销毁时,编译器会隐式地插入一个对相应析构函数的调用,来释放该对象所拥有的系统资源。
3、而在gtk系统中,用户一般无需考虑构件的销毁,以及构件资源的释放等,这些琐碎工作会由gtk系统内部来自动地、智能地完成它。它的原理(或者说处理流程)是这个样子的:外部用户调用构件的某个外部接口,如gtk_widget_destroy(...)函数,这会导致构件触发destroy信号事件,这个信号的缺省处理函数会引发gtk系统内部来销毁这个构件,并触发finalize信号事件,之后gtk系统会释放构件在堆上的内存资源;当然gtk_widget_destroy函数也会触发它所有的子构件的销毁,所以说gtk构件的销毁有一定的“自治”能力。最后需要强调的是,gtk1.2版本和gtk2.0版本对构件销毁的原理和流程可能会有比较大的差别,但它们的设计思想是基本保持一致的,上述的流程是针对gtk1.2版本而言的,而gtk2.0以上版本的对构件销毁的流程俺没有深入去研究过它,估计大体差不多,只不过gtk2.0引入了“引用计数”的管理机制,所以对构件销毁的触发过程可能略有不同。
十一、个人总结
C语言是面向过程的编程语言,但是gtk的设计者们却能够用C语言写出如此精妙的gtk对象系统,令俺甚是折服!敬仰之情更是油然而生!深刻体会到什么才是真正的软件系统设计师!同时也告诫自己:语言不是软件的灵魂,它仅仅只是一个工具罢了!俺坚信:一个优秀的程序员用C写出来的东西,将可能会比一个蹩脚的程序员用C++写出来的东西好n多倍。
当然,由于gtk所承载的是一个图形系统,所以gtk它终究会很复杂;但一个如此复杂的系统,却用一个如此简单的C语言来实现它,所以gtk系统必然会烙印上许多不可避免的局限性。例如它的接口实在是太多了,难以学习和使用它;没有异常事件处理系统,所以健壮性很值得担忧;gtk的扩展比较难,且需要自己来实现的东西也较多,代码量大,这无疑都增加了开发新构件的成本。所以俺个人觉得,gtk终究会被淘汰并退出历史的舞台,而能够永恒的、并值得我们永远牢记在心的是,它这其中的许多设计思想和设计理念!