[software development] GObject简介和入门指引(三)GObject基础知识
Tofloor
poster avatar
enforcee
deepin
2023-08-10 22:43
Author

就算GObject的目标再广阔,他毕竟不是空中楼阁。由于他是在C语言上建设的,因此一方面他能发挥C语言的特性,一方面也会受到C语言的限制。

C语言给GObject可以利用的特性有两个:头文件和源代码文件的差别,以及预处理宏命令。

首先我们需要了解C语言为什么要分头文件(*.h)和源代码文件(*.c)。实际上,C语言的编译过程,本来就是用一整个源代码文件来编译成目标文件的。其实是在编译器预处理阶段,编译器会把头文件插入到源代码文件中的#include位置,把你所写的程序转化成实际编译所需的文件(#include其实是一种预处理命令)。所以说其实头文件和源代码文件都是一样的C语言,只不过我们人为给他们分成了用来被包含的头文件,和用来嵌入头文件的源代码。

但是,为什么要这样做呢?我们在课堂上学过,C语言的编译过程可以分为四个阶段:预处理、编译、汇编和连接。每一个C源代码都会转换成一个目标文件,而必须将所有目标文件连接到一起,才能生成一个我们想要的程序文件。我们在不同C源代码中编写(称为定义define)的函数,就会被编译到对应的目标文件中去,而从一个C源代码中调用另一个源代码中定义的函数,编译的结果就是一个目标文件调用另一个目标文件的函数。因此必须把两个目标文件中的函数对接起来,程序才能正常工作,这就是「连接」阶段的意义。

我们想在一个源代码中调用另一个源代码的函数,要怎么做?如果我们直接把另一个源代码嵌入我们的源代码中,也是一种办法。但是这样就造成了代码体积的不断扩张,难以维护。在C语言中,我们得到了另一种办法,即:声明(declare)。一个被声明的函数,我们就可以直接使用他,尽管编译器并不知道被声明的函数到底是什么内容。这个函数其实是存在于另一个目标文件中,直到编译过程进入最后的连接阶段,包含这个函数的目标文件与你的目标文件连接,函数的定义就会被补齐。为了防止一次次重复在各种源代码文件中写上相同的声明,我们把这些声明单独分成一个文件,在预处理阶段让预处理器去自动嵌入,这就是我们说的头文件。

使用头文件也带来了一些有趣的特点:第一,我们不再需要把自己的源代码给别人看了,只需要把头文件和目标文件给他们,他们编译以后连接就行了。这就是我们所谓的链接库
。而库也无需开源就可以让其他开发者使用。其二、我们只需要阅读头文件中的函数声明,就可以知道我们可以调用哪些函数。因此很多开发者也习惯把注释也写到头文件中,帮助大家理解。这就是最原始的API文档。而后来也出现了gtkdoc这种直接把头文件转化成文档的工具。第三、如果我们想隐藏一个函数只供内部使用,不希望其他源代码使用,很简单,不在头文件中声明,只在源代码中定义即可。这就是我们之前提到的,公开函数和隐私函数的雏形。

C语言的另一个特色则是预处理宏命令。所谓宏就是把一种原文本,用一个另外的名称所替代。在C语言中的宏可以做到:把常量用助记变量替代,防止被各种独立出现的数字弄糊涂;把一段经常使用的代码缩写,看上去像是定义了一个新的函数;逻辑判断,比如说防止重复引入头文件的ifdef define endif三连等等操作。实际上宏只不过单纯的文本替换,但是也能做出想当有趣的成果。甚至有一种专门的宏处理语言m4 ,我的另一篇文章提到的autoconf就是用m4写的。

宏和函数的最大不同是,宏是在预处理期间被扩展,因此宏可以动态地定义函数,减少了手动处理的麻烦(虽然看上去还是很麻烦)。这种看上去像函数的宏,在GObject文档中被称为宏函数(Function Macro)。


有关C语言的特性就暂时谈到这里了。下面谈一谈GObject的约定,这也是GObject让初学者迷惑的一个原因。其他编程语言的语法通常是编译器所检查的,如果不能「通过编译」,就告知开发者代码出错的原因,这样开发着就会在试错的过程中获得经验。但是GObject是建筑在C语言之上的,因此编译器只会提供C语言相关的错误,不会告诉你有关GObject的建议。如果想写出符合GObject规则的代码,除了遵守C语言的语法规则,还需要符合GObject的约定。如果不符合这个约定,代码也有可能通过编译,也没准能正常运行,但是这就破坏了GObject通用、统一的初衷。以下规则是GObject开发者需要了解的:

你需要给命名空间和类名取名。这个名称应该是拉丁字母组成的。比如说我给我的命名空间起名:kong jian,然后有一个类叫:lei。

首字母大写,表示这个类(其实是一个结构体),比如说我的类名应该是:KongJianLei

全是小写,中间用下划线分隔,表示方法。对于通常方法,你要用你的类名开头。比如说我的类里面有个方法fang fa,他的名字应该是:kong_jian_lei_fang_fa()

全是大写,中间用下划线分隔,表示宏、常量、枚举。比如说我的类里有个常量chang liang,他的名字是 KONG_JIAN_LEI_CHANG_LIANG

你的类要写到头文件和源代码文件中,这个类和这两个文件是对应的关系。你需要给你的文件取一个和类名有关的文件名。比如你可以选择如下几种:
kong-jian-lei.hkong-jian-lei.c
kong_jian_lei.hkong_jian_lei.c
kongjianlei.hkongjianlei.c

类需要在头文件(*.h)中声明,在源代码文件(*.c)中定义。声明是用的 G_DECLARE_DERIVABLE_TYPE宏或者 G_DECLARE_FINAL_TYPE宏。区别是前者声明的类是可以被继承的,而后者不允许被继承。要注意的是,这两种宏并非是只写上就完事大吉,他都对应着一种模板,你需要对着这个模板,在头文件和源代码文件中把所有部分补全,不要忘记把里面的命名空间和类名都改成你自己的,这样才能完成类的声明和定义。如果写得不对的话,就有可能出现各种编译错误和ld连接错误。

你的类名是一个特殊的宏函数,作用是转换类型。比如说有一个对象a是派生类的实例,你想把他转换成基类的对象,那么就用 KONG_JIAN_LEI (a) 。GObject不会像其他语言一样,当你想调用基类的成员时,派生类会自动转换为基类。GObject任何时候都需要手动转换类型。

还有一个宏函数是在命名空间和类名之间加上is:比如 KONG_JIAN_IS_LEI () ,作用是检查对象的型。这些宏函数均是上面的类型声明宏所声明的。如果用的是 G_DECLARE_DERIVABLE_TYPE,还能得到 KONG_JIAN_LEI_CLASS ()对于类的型转换,KONG_JIAN_IS_LEI_CLASS ()检查类的型和 KONG_JIAN_LEI_GET_CLASS ()获取类这三个宏。

GObject其实是没有「公开变量」的,对象中的变量都是「隐私变量」。你可以用公开方法或者属性去访问他们。

如果一个方法在头文件中有声明,在源代码文件中有定义,那他就是「公开方法」。如果只在源代码中有定义,没有头文件中的声明,那就是「隐私方法」。

任何类中的方法,第一个参数必须是这个类的结构体的指针。比如:kong_jian_lei_fang_fa (KongJianLei * self, 其他参数)

GObject的「类的成员」和「对象的成员」是不一样的,「类的成员」是类的所有对象都可以使用的,声明在类结构体(_KongJianLeiClass,之后会被typedef成 KongJianLeiClass)中,「对象的成员」是每个对象自己持有的,如果用的是不可继承类,就声明在对象结构体中(_KongJianLei,之后会被typedef成 KongJianLei);如果用的是可继承类,就是对象隐私结构体(KongJianLeiPrivate)。其实「类的成员」只有一种,就是「拟方法」(其实是函数指针),「对象的成员」也只有一种,就是「隐私变量」。通常方法哪去了呢?其实他名义上是属于类的,但是毕竟我们用的是C语言,方法都是写在结构体外面的,只是我们把他用面向对象的思维,当作类的成员而已。

GObject有类的初始函数(kong_jian_lei_class_init (KongJianLeiClass *klass)),也有对象的初始函数(kong_jian_lei_init (KongJianLei *self))。具体是这样的,一个类的第一个对象被实例化时,这个类也随着诞生;当这个类的最后一个实例清除后,这个类也随着消亡。类的初始函数主要是用来为各种函数指针赋值(比如拟方法,以及GObject自带的解构函数等在对象和类的生命周期中有影响的函数,以及属性处理函数。),对象的初始函数作用主要是给隐私变量赋初始值。(另外虽然他和其他语言的构造器是一个作用,但是由于这个名字被做另一个作用了,所以这里叫初始函数。)

实例化一个新的对象要这么做:KongJianLei * a = g_object_new (KONG_JIAN_TYPE_LEI, NULL) 。KONG_JIAN_TYPE_LEI这个宏是我们之前在模板中自己定义的,作用是获取类的型。

GObject有垃圾回收机制,但是是「半自动」的。每次增加一个指针指向对象时,你都要调用 g_object_ref (G_OBJECT (a))增加一个引用计数,每次删除一个指向对象的指针时,都要调用 g_object_unref (G_OBJECT (a))减少一个引用计数。当引用数为0时,这个对象就会被删除。也可以用 g_clear_object (&a),这个和 g_object_unref (G_OBJECT (a))大体一样,只是他也会同时把指针置0(NULL)。


关于接口、属性和信号等就不细讲了,只要增加对GObject规则的了解,就会能清楚地看透他们的本质。本文主要目的是为新手解惑,不可能一口气让大家成为传说级的开发者。这些规则看上去很繁琐,但是每条都有他们的原因。如果已经接触过其他面向对象编程的开发者,读过这些也会很轻松地掌握了。

在下一篇文章中,让我们来尝试自己创建类和对象吧。


推荐大家阅读官方文章,其实讲解得很详细,只要破除一些思维定势,就能明白其中的奥秘了:
GObject设计观念(一定要看):https://docs.gtk.org/gobject/concepts.html
GObject教程(一定要看):https://docs.gtk.org/gobject/tutorial.html
G_DECLARE_FINAL_TYPE:https://docs.gtk.org/gobject/func.DECLARE_FINAL_TYPE.html
G_DECLARE_DERIVABLE_TYPE:https://docs.gtk.org/gobject/func.DECLARE_DERIVABLE_TYPE.html

Reply Favorite View the author
All Replies
阿尼樱奈奈
Moderator
2023-08-10 23:15
#1

like

Reply View the author
yanjuner
Super Moderator
2023-08-10 23:31
#2

优秀!

Reply View the author