GObject学习笔记(三)可派生抽象类型

paw5zx Lv4

可派生类型

根据派生性,GObject中可分为两种类型:最终类型(final type)和可派生类型(derivable type)。最终类型没有任何子对象,而可派生类型可以有子对象。

这两种对象的主要区别在于它们的类结构:

  • 可派生类型的类结构体可以在父类型类结构体之后继续定义新的虚函数或其他类级成员,因此其类结构体对子类是开放的,可以被进一步继承和扩展。
  • 最终类型也有自己的类结构体,但在G_DECLARE_FINAL_TYPE()生成的默认形式下,该类结构体通常只包含父类的类结构体,不额外公开新的类级扩展字段。

G_DECLARE_DERIVABLE_TYPE可以用来在头文件中声明一个可派生类型。

抽象类型

抽象类型不能被直接实例化。它通常作为基类存在,其子类可以继承并使用抽象类型提供的函数、信号以及其他公共接口。

本节将使用三个类型PawNumberPawIntPawDouble

PawDouble在前一节中已定义,PawInt的定义类似PawDoublePawIntPawDouble分别表示整数和浮点数。

PawNumber代表数字,数字是比整数和浮点数更抽象的概念。所以PawNumber被设计为PawIntPawDouble的父类。它作为抽象类型不能被直接实例化。

PawIntPawDouble都支持三个算术操作:乘法、除法和一元取反。为了方便展示,这里省略了加法和减法。除此以外,还定义了一个to_s操作,用于将PawNumber表示的数值转换为字符串。这些操作统一定义在PawNumber这一抽象类型上,再由具体子类分别实现。

下面将依次给出这些类型的定义与实现。

G_BEGIN_DECLS和G_END_DECLS

G_BEGIN_DECLSG_END_DECLS是GLib提供的宏,用于在C++环境中保持C语言链接约定。这两个宏主要用于确保当GObject库的头文件被C++编译器包含时,内部声明的C函数不会被当作C++函数进行名称修饰,从而可以在C++代码中正常调用C库函数。

等价于:

1
2
3
4
5
6
7
#ifdef __cplusplus
#define G_BEGIN_DECLS extern "C" {
#define G_END_DECLS }
#else
#define G_BEGIN_DECLS
#define G_END_DECLS
#endif

G_DECLARE_DERIVABLE_TYPE

1
#define G_DECLARE_DERIVABLE_TYPE(ModuleObjName, module_obj_name, MODULE, OBJ_NAME, ParentName)

其中:

  • ModuleObjName:新类型的名称,大驼峰命名(如PawDouble
  • module_obj_name:新类型名称,蛇形命名(如paw_double
  • MODULE:模块的名称,全大写(如PAW
  • OBJ_NAME:新类型的名称,去除修饰,全大写如(DOUBLE
  • ParentName:父类型的名称,大驼峰命名(如GObject

G_DECLARE_DERIVABLE_TYPE内部完成以下操作:

  • 声明<module>_<name>_get_type()函数:声明一个返回类型为GType<module>_<name>_get_type()函数。这里仅生成函数声明,需要用户在某处提供定义。实际定义通常由源文件中的G_DEFINE_TYPE展开提供,因此一般不需要用户手写该函数的实现。
  • 定义实例结构体_<Module><Name>并定义类型别名<Module><Name>:生成实例结构体struct _<Module><Name>,其中默认只包含父类型的实例结构体作为唯一成员;同时生成typedef struct _<Module><Name> <Module><Name>,用于简化实例结构体类型的书写。由于该结构体默认不公开额外的实例成员,实际项目中通常会在源文件中配合私有结构体保存实例数据,以便封装实现细节。
  • 定义类结构体类型别名<Module><Name>Class:生成typedef struct _<Module><Name>Class <Module><Name>Class>,用于简化类结构体类型的书写。不过,_<Module><Name>Class类结构体本身并不会在该宏中定义,仍需由用户在头文件中于宏调用之后自行定义。
  • 定义实例转换函数<MODULE>_<NAME>():生成一个名为<MODULE>_<NAME>()的静态内联函数,用于将传入参数转换为当前类型的实例指针。
  • 定义类转换函数<MODULE>_<NAME>_CLASS():生成一个名为<MODULE>_<NAME>_CLASS()的静态内联函数,用于将传入参数转换为当前类型的类结构体指针。
  • 定义实例类型检查函数<MODULE>_IS_<NAME>():生成一个名为<MODULE>_IS_<NAME>()的静态内联函数,用于检查参数是否为指向当前类型实例的指针;如果参数指向<Module><Name>或其后代类型的实例,则返回真。
  • 定义类类型检查函数<MODULE>_IS_<NAME>_CLASS():生成一个名为<MODULE>_IS_<NAME>_CLASS()的静态内联函数,用于检查参数是否为指向当前类型类结构体的指针;如果参数指向当前类型或后代类型的类结构体,则返回真。
  • 定义类结构体获取函数<MODULE>_<NAME>_GET_CLASS():生成一个名为<MODULE>_<NAME>_GET_CLASS()的静态内联函数。它接受一个指向当前类型实例(或其子类型实例)的指针,并返回对应的类结构体指针<Module><Name>Class*
  • 定义当前类型的g_autoptr()支持:基于父类型为当前类型补充g_autoptr()相关支持,从而可以使用GLib的自动清理机制管理该类型对象。

另外,在使用G_DECLARE_DERIVABLE_TYPE之前,我们还要手动定义<MODULE>_TYPE_<NAME>

对于一个类型的声明,从使用G_DECLARE_FINAL_TYPE()变成使用G_DECLARE_DERIVABLE_TYPE(),通常不会破坏已有API或ABI。因为最终类型没有暴露类结构体和实例结构体,外部代码无法依赖这些结构体布局。因此,在编写库时,如果一开始不确定某个类型是否需要被继承,官方建议先使用G_DECLARE_FINAL_TYPE(),等确定有派生需求时,再切换为G_DECLARE_DERIVABLE_TYPE()

但一旦使用G_DECLARE_DERIVABLE_TYPE()并公开了类结构体,类结构体的大小、字段顺序和已有成员就会成为ABI约束,后续不能随意改变。因此可派生类型的类结构体底部通常应预留padding,以便未来增加虚函数。

G_DECLARE_FINAL_TYPE

详见上一节

G_DEFINE_ABSTRACT_TYPE

G_DEFINE_ABSTRACT_TYPEG_DEFINE_TYPE的用法类似,不同之处在于它定义的是抽象类型。抽象类型可以作为父类提供公共接口和虚函数,但不能被直接实例化。

示例代码

PawNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// file: pawnumber.h
#ifndef PAW_NUMBER_H
#define PAW_NUMBER_H

#include <glib-object.h>

G_BEGIN_DECLS

#define PAW_TYPE_NUMBER (paw_number_get_type())
G_DECLARE_DERIVABLE_TYPE(PawNumber, paw_number, PAW, NUMBER, GObject)

// 公开类结构
struct _PawNumberClass
{
GObjectClass parent_class;
// 虚函数槽位
PawNumber* (*mul)(PawNumber* self, PawNumber* other);
PawNumber* (*div)(PawNumber* self, PawNumber* other);
PawNumber* (*uminus)(PawNumber* self);
char* (*to_s)(PawNumber* self);

// padding
gpointer padding[12];
};

/* 算术运算接口 */
/* 这些函数会创建新实例,并返回新实例指针。 */
PawNumber* paw_number_mul(PawNumber* self, PawNumber* other);

PawNumber* paw_number_div(PawNumber* self, PawNumber* other);

PawNumber* paw_number_uminus(PawNumber* self);

char* paw_number_to_s(PawNumber* self);

G_END_DECLS

#endif // PAW_NUMBER_H
  • G_BEGIN_DECLS/G_END_DECLS用于在C++环境下保持C语言链接约定,确保C函数不会被当作C++函数进行名称修饰;
  • G_DECLARE_DERIVABLE_TYPE用于声明可派生类型PawNumber
  • _PawNumberClass为类结构体,其中预留四个虚函数槽位;
  • 类结构体末尾的padding用于为未来扩展预留空间。对于库设计者,可派生类型会公开类结构体,结构体布局很容易成为ABI约束,后续若新增虚函数,没有padding的话可能破坏ABI,因此官方建议预留padding;
  • paw_number_mulpaw_number_divpaw_number_uminuspaw_number_to_s是对外公开的运算接口,它们作为对虚函数的统一包装,供外部代码直接调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// file: pawnumber.c
#include "pawnumber.h"

G_DEFINE_ABSTRACT_TYPE(PawNumber, paw_number, G_TYPE_OBJECT)

PawNumber* paw_number_mul(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_NUMBER(self), NULL);
g_return_val_if_fail(PAW_IS_NUMBER(other), NULL);

PawNumberClass* class = PAW_NUMBER_GET_CLASS(self);

return class->mul ? class->mul(self, other) : NULL;
}

PawNumber* paw_number_div(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_NUMBER(self), NULL);
g_return_val_if_fail(PAW_IS_NUMBER(other), NULL);

PawNumberClass* class = PAW_NUMBER_GET_CLASS(self);

return class->div ? class->div(self, other) : NULL;
}

PawNumber* paw_number_uminus(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_NUMBER(self), NULL);

PawNumberClass* class = PAW_NUMBER_GET_CLASS(self);

return class->uminus ? class->uminus(self) : NULL;
}

char* paw_number_to_s(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_NUMBER(self), NULL);

PawNumberClass* class = PAW_NUMBER_GET_CLASS(self);

return class->to_s ? class->to_s(self) : NULL;
}

static void paw_number_class_init(PawNumberClass* class)
{
// 默认虚函数实现
class->mul = NULL;
class->div = NULL;
class->uminus = NULL;
class->to_s = NULL;
}

static void paw_number_init(PawNumber* self)
{
}
  • G_DEFINE_ABSTRACT_TYPE用于将PawNumber定义为抽象类型。因此PawNumber只能作为父类使用,不能被直接实例化;
  • paw_number_mulpaw_number_divpaw_number_uminuspaw_number_to_s这几个公共函数都会先取得对象对应的运行时类结构体,再调用其中保存的虚函数实现。如果子类没有提供相应实现,则返回NULL,调用者需要自行处理这种情况。
  • paw_number_class_init类初始化函数,将四个虚函数槽位都初始化为NULL,表示抽象父类本身只定义接口,不提供默认实现。
  • paw_number_init实例初始化函数,这里没有需要初始化的实例数据,因此函数体为空,不过此函数的定义不能省略。因为在创建具体子类对象时,父类的实例初始化函数依然会参与初始化过程。

PawInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// file: pawint.h
#ifndef PAW_INT_H
#define PAW_INT_H
#include <glib-object.h>
#include "pawnumber.h"

G_BEGIN_DECLS

#define PAW_TYPE_INT (paw_int_get_type())
G_DECLARE_FINAL_TYPE(PawInt, paw_int, PAW, INT, PawNumber)

PawInt* paw_int_new(int value);
gboolean paw_int_get_value(PawInt* self, int* value);
void paw_int_set_value(PawInt* self, int value);

G_END_DECLS

#endif // PAW_INT_H
  • G_BEGIN_DECLS/G_END_DECLS用于在C++环境下保持C语言链接约定,确保C函数不会被当作C++函数进行名称修饰;
  • G_DECLARE_FINAL_TYPE用于声明最终类型PawInt
  • paw_int_newpaw_int_get_valuepaw_int_set_valuePawInt自身的公共接口,分别负责对象创建、读值和写值;
  • 算术运算接口以及to_s接口已经由父类PawNumber统一声明,因此这里不需要重复声明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// file: pawint.c
#include "pawnumber.h"
#include "pawint.h"
#include "pawdouble.h"

struct _PawInt
{
PawNumber parent_instance;
int value;
};

G_DEFINE_TYPE(PawInt, paw_int, PAW_TYPE_NUMBER)

// 按当前示例的封装规则,跨类型取值通过公开getter完成;本类型内部仍直接访问value成员。
// 混合运算时先按double进行计算,避免过早截断右操作数;但最终结果仍会转换回int,因此仍可能损失精度。
#define paw_int_binary_op(op)\
int i;\
double d;\
if(PAW_IS_INT(other))\
{\
if(!paw_int_get_value(PAW_INT(other), &i))\
return NULL;\
return PAW_NUMBER(paw_int_new(PAW_INT(self)->value op i));\
}\
else\
{\
if(!paw_double_get_value(PAW_DOUBLE(other), &d))\
return NULL;\
return PAW_NUMBER(paw_int_new((int)(((double)PAW_INT(self)->value) op d)));\
}

static PawNumber* paw_int_mul(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_INT(self), NULL);

paw_int_binary_op(*);
}

static PawNumber* paw_int_div(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_INT(self), NULL);
// 这里没有处理除数为0的情况,后续会在“信号”一节继续优化。
paw_int_binary_op(/);
}

static PawNumber* paw_int_uminus(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_INT(self), NULL);

return PAW_NUMBER(paw_int_new(-PAW_INT(self)->value));
}

static char* paw_int_to_s(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_INT(self), NULL);

PawInt* int_self = PAW_INT(self);
return g_strdup_printf("%d", int_self->value);
}

PawInt* paw_int_new(int value)
{
PawInt* i;
i = g_object_new(PAW_TYPE_INT, NULL);
paw_int_set_value(i, value);
return i;
}

gboolean paw_int_get_value(PawInt* self, int* value)
{
g_return_val_if_fail(PAW_IS_INT(self), FALSE);
g_return_val_if_fail(value != NULL, FALSE);

*value = self->value;
return TRUE;
}

void paw_int_set_value(PawInt* self, int value)
{
g_return_if_fail(PAW_IS_INT(self));

self->value = value;
}

static void paw_int_class_init(PawIntClass* class)
{
PawNumberClass* number_class = PAW_NUMBER_CLASS(class);

/* 覆盖父类虚函数 */
number_class->mul = paw_int_mul;
number_class->div = paw_int_div;
number_class->uminus = paw_int_uminus;
number_class->to_s = paw_int_to_s;
}

static void paw_int_init(PawInt* self)
{
}
  • G_DEFINE_TYPE用于定义PawInt
  • struct _PawInt为实例结构体,其中value字段作为实例的内部数据保存整数值。由于结构体没有暴露到头文件,外部代码无法直接依赖其内部布局。
  • paw_int_binary_op宏统一实现了PawInt参与的二元运算逻辑。它会先判断另一个操作数的实际类型,再分别读取其数值。当otherPawInt时,直接通过paw_int_get_value()取得整数值;当otherPawDouble时,则通过paw_double_get_value()取得浮点值,并先将当前PawInt的值提升为double参与运算,最后再把结果转换回int构造新的PawInt对象。这种写法避免了过早将浮点操作数截断为整数,因此比直接把double转成int后再运算更合理;不过由于最终结果仍然会转换回int,所以依然可能发生截断并损失精度。
  • paw_int_mulpaw_int_divpaw_int_uminuspaw_int_to_s并不是对外统一调用的接口,而是PawInt为父类PawNumber的虚函数槽位提供的具体实现;外部通常调用的是paw_number_mul等父类包装函数,再由运行时分派到这些子类实现。
  • paw_int_newpaw_int_get_valuepaw_int_set_value分别提供对象创建、读值和写值的公共接口。
  • paw_int_class_init类初始化函数,将PawInt自己的实现绑定到PawNumberClass的虚函数槽位上,这样外部通过PawNumber接口调用时就会发生正确的动态分派。
  • paw_int_init实例初始化函数,这里函数体为空,因为这个示例没有额外的实例初始化逻辑。

PawDouble

PawDouble的实现思路与PawInt基本类似,这里只给出代码,不再逐段展开说明。值得一提的是,与PawInt不同,PawDouble在混合运算时会把整数转换为浮点数参与计算,因此结果能够保留浮点精度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// file: pawdouble.h
#ifndef PAW_DOUBLE_H
#define PAW_DOUBLE_H
#include <glib-object.h>
#include "pawnumber.h"

G_BEGIN_DECLS

#define PAW_TYPE_DOUBLE (paw_double_get_type())
G_DECLARE_FINAL_TYPE(PawDouble, paw_double, PAW, DOUBLE, PawNumber)

PawDouble* paw_double_new(double value);
gboolean paw_double_get_value(PawDouble* self, double* value);
void paw_double_set_value(PawDouble* self, double value);

G_END_DECLS

#endif // PAW_DOUBLE_H
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// file: pawdouble.c
#include "pawnumber.h"
#include "pawdouble.h"
#include "pawint.h"

struct _PawDouble
{
PawNumber parent_instance;
double value;
};

G_DEFINE_TYPE(PawDouble, paw_double, PAW_TYPE_NUMBER)

#define paw_double_binary_op(op)\
int i;\
double d;\
if(PAW_IS_INT(other))\
{\
if(!paw_int_get_value(PAW_INT(other), &i))\
return NULL;\
return PAW_NUMBER(paw_double_new(PAW_DOUBLE(self)->value op (double)i));\
}\
else\
{\
if(!paw_double_get_value(PAW_DOUBLE(other), &d))\
return NULL;\
return PAW_NUMBER(paw_double_new(PAW_DOUBLE(self)->value op d));\
}

static PawNumber* paw_double_mul(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_DOUBLE(self), NULL);

paw_double_binary_op(*);
}

static PawNumber* paw_double_div(PawNumber* self, PawNumber* other)
{
g_return_val_if_fail(PAW_IS_DOUBLE(self), NULL);

// 这里没有处理除数为0的情况,后续会在“信号”一节继续优化。
paw_double_binary_op(/);
}

static PawNumber* paw_double_uminus(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_DOUBLE(self), NULL);

return PAW_NUMBER(paw_double_new(- PAW_DOUBLE(self)->value));
}

static char* paw_double_to_s(PawNumber* self)
{
g_return_val_if_fail(PAW_IS_DOUBLE(self), NULL);

PawDouble* double_self = PAW_DOUBLE(self);
return g_strdup_printf("%lf", double_self->value);
}

PawDouble* paw_double_new(double value)
{
PawDouble* d;
d = g_object_new(PAW_TYPE_DOUBLE, NULL);
paw_double_set_value(d, value);
return d;
}

gboolean paw_double_get_value(PawDouble* self, double* value)
{
g_return_val_if_fail(PAW_IS_DOUBLE(self), FALSE);
g_return_val_if_fail(value != NULL, FALSE);

*value = self->value;
return TRUE;
}

void paw_double_set_value(PawDouble* self, double value)
{
g_return_if_fail(PAW_IS_DOUBLE(self));

self->value = value;
}

static void paw_double_class_init(PawDoubleClass* class)
{
PawNumberClass* number_class = PAW_NUMBER_CLASS(class);

// 覆盖父类虚函数
number_class->mul = paw_double_mul;
number_class->div = paw_double_div;
number_class->uminus = paw_double_uminus;
number_class->to_s = paw_double_to_s;
}

static void paw_double_init(PawDouble* self)
{
}

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// file: main.c
#include <glib-object.h>
#include "pawnumber.h"
#include "pawint.h"
#include "pawdouble.h"

int main(int argc, char **argv)
{
PawNumber *i, *d, *num;
char *si, *sd, *snum;

i = PAW_NUMBER(paw_int_new(23));
d = PAW_NUMBER(paw_double_new(10.3));

num = paw_number_mul(i, d);

si = paw_number_to_s(i);
sd = paw_number_to_s(d);
snum = paw_number_to_s(num);

g_print("%s * %s is %s\n", si, sd, snum);

g_object_unref(num);
g_free(snum);

num = paw_number_mul(d, i);
snum = paw_number_to_s(num);

g_print("%s * %s is %s\n", sd, si, snum);

g_object_unref(num);
g_free(snum);

g_free(sd);
g_free(si);

g_object_unref(d);
g_object_unref(i);

return 0;
}

主函数的功能较简单,主要用于验证PawNumber这一抽象接口上的动态分派行为:

  • 先分别创建一个PawInt对象和一个PawDouble对象。
  • 接着调用paw_number_mul(i, d),验证当左操作数实际类型为PawInt时,会分派到PawInt提供的乘法实现。
  • 然后调用paw_number_mul(d, i),验证当左操作数实际类型为PawDouble时,会分派到PawDouble提供的乘法实现。
  • 最后通过paw_number_to_s把结果转换为字符串输出,并释放创建过程中分配的对象和字符串资源。

总结

上述代码为了突出“抽象类型与可派生类型”的主题,暂时省略了一些更完整的设计,主要包括:

  • 除法运算没有处理除数为0的情况。这将在《信号章节》中进行优化。
  • 当前的混合运算采用了显式类型判断加数值转换的简化实现。例如,PawInt参与与PawDouble的运算时,会先按浮点数进行计算,但最终仍会把结果转换回int,因此依然可能发生截断和精度损失。(这里的精度损失是当前示例设计下的必然结果:为了保持PawInt运算结果仍返回PawInt,就必须在某个阶段把浮点结果转换回整数。后续无相关优化)
  • PawIntPawDouble的实例结构体都保留在各自的源文件中,外部通过getter/setter访问内部数值。这种写法已经避免了直接暴露实例数据布局,但在更完整的GObject设计中,可以使用属性系统代替实现。这将在《属性章节》中进行优化。

参考文章

1.GObject Tutorial for beginners

  • 标题: GObject学习笔记(三)可派生抽象类型
  • 作者: paw5zx
  • 创建于 : 2024-11-13 22:14:13
  • 更新于 : 2026-05-02 18:23:21
  • 链接: https://paw5zx.github.io/GObject-tutorial-beginner-03/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论