纸上得来终觉浅,绝知此事要躬行。我们现在通过一个简单的案例来体会一下GObject中类的定义与使用。如果大家遇到了麻烦的话,可以回过头查看之前的几章。这个案例不会很难,代码也不是很严谨,纯粹当作之前理论的补充。
想要自己动手编写运行的话,请自己安装一下glib的开发库,C语言的编译器,和你最喜欢的代码编辑器。(都可以在系统软件源中找到,这些我就不示范了。)
这也是一个比较经典的编程课作业,就是做一个「学生管理系统」。我们不要弄那么复杂,只做一个学生档案的类,能存储数据和互动就可以了。在这个案例中所完善的是:
声明和定义一个可派生的类student,命名空间是manage。根据模板,我们需要在头文件中做一个获取对象的型的宏(MANAGE_TYPE_STUDENT),类的声明(G_DECLARE_DERIVABLE_TYPE ()),类的结构体(_ManageStudentClass),一个必须的函数(manage_student_new ())。在C源代码文件中做隐私结构体(ManageStudentPrivate),带隐私结构体的定义(G_DEFINE_TYPE_WITH_PRIVATE ()),类的初始函数(manage_student_class_init ()),对象的初始函数(manage_student_init ())。
MANAGE_TYPE_STUDENT
G_DECLARE_DERIVABLE_TYPE ()
_ManageStudentClass
manage_student_new ()
ManageStudentPrivate
G_DEFINE_TYPE_WITH_PRIVATE ()
manage_student_class_init ()
manage_student_init ()
不要怕多,他们都是模板,不需要自由发挥,只要抄写上去,然后把命名空间和类名都改成你自己的就可以了。
几个隐私变量,姓名、性别、年级、入学年份。其中性别是一个枚举。把这些变量放到隐私结构体中。然后枚举和常量在头文件中定义。
几个方法,存储数据、自我介绍、介绍性别的三个公开方法(在头文件中声明,源代码文件中定义)。另外,还有一个计算毕业还有多少年的隐私方法(只定义不声明),这个方法会在自我介绍的公开方法中利用。
隐私变量的初始化,放在对象的初始函数中。另外是几个可以覆盖的,在对象声明周期中有影响的函数指针(比如解构方法),赋值在类的初始函数中。(这部分在这个案例中其实是没有作用的,只是用来演示。)
代码我已经写好了,放在下面。你可以试着找一找这几个部分分别对应着哪段代码。也可以自己试着写一遍。我在很多地方都留了注释,方便大家理解。
manage-student.h内容:
manage-student.h
#pragma once /* 你也可以用ifndef define endif来保证头文件只被引入一次。这个是C语言很基础的了。 */ #include /* 引入头文件。也可以选择glib.h gdk.h gtk.h等,因为他们都已经包含了gobject, 引入任何一个都能使用。 */ G_BEGIN_DECLS /* 可能引入C++源文件的头文件,需要G_BEGIN_DECLS和G_END_DECLS一前一后圈起来。 和GObject本身关系不大。参见:https://docs.gtk.org/glib/macros.html */ typedef enum { MANAGE_STUDENT_GENDER_MALE = 0, MANAGE_STUDENT_GENDER_FEMALE = 1 } ManageStudentGender; /* 枚举。不要嫌长,就算不用gobject,其他C语言也是这样的规范。 如果不用C语言的枚举,也可以用GLib的枚举类。 */ #define MANAGE_STUDENT_YEAR_NOW 2023 /* 设置常量。 */ #define MANAGE_TYPE_STUDENT manage_student_get_type() /* 这个是必须的。 */ G_DECLARE_DERIVABLE_TYPE (ManageStudent, manage_student, MANAGE, STUDENT, GObject); /* 如果需要一个类是不可继承的,就用G_DECLARE_FINAL_TYPE, 否则就是G_DECLARE_DERIVABLE_TYPE。 G_DECLARE_FINAL_TYPE的话,成员都算是私有的,在C源代码文件里用G_DEFINE_TYPE, 需要把一个实例的结构体定义在C源代码文件,之后放G_DEFINE_TYPE。 G_DECLARE_DERIVABLE_TYPE的话,可以在C源代码文件用G_DEFINE_TYPE, 或者需要私有成员的话,做一个实例的私有结构体,定义到C源代码文件, 之后放G_DEFINE_TYPE_WITH_PRIVATE。 此外在用G_DECLARE_DERIVABLE_TYPE时,你还需要做一个类的结构体,如下。 */ struct _ManageStudentClass { GObjectClass parent_class; /* 这里可以用来定义拟方法。 */ gpointer padding[12]; /* 给将来的拟方法保留的空间。 如果使用G_DECLARE_DERIVABLE_TYPE的话,要保证API和ABI不要改变, 你就不能调整各个拟方法的顺序,也不能改变结构体的大小。 但是如果预留了空间后,在将来版本就可以把他利用起来, 每多用一个拟方法,就减小一个padding的长度。 要是不愿意保证ABI的话,那就随便了。 */ }; ManageStudent manage_student_new (void); /* 这个是必须的。 */ /* 之后你可以在这里声明公共方法。然后在C源文件中定义。 */ void manage_student_update_info (ManageStudent * self, gchar * name, ManageStudentGender gender, guchar grade, gshort admission_year); /*获取入学年份*/ void manage_student_self_introduction (ManageStudent * self); /*介绍*/ void manage_student_describe_gender (ManageStudent * self); /*介绍性别*/ G_END_DECLS /* 见上G_BEGIN_DECLS。 */
manage-student.c内容:
manage-student.c
#include "manage-student.h" /* 我们要给这个头文件做定义。 */ #include /* 后面我们用到了printf。所以引入stdio.h。 别忘了,系统中的头文件,我们用小于号和大于号包括, 我们项目中的头文件,用双引号包括。 */ /* 为什么隐私成员是隐私的?因为他们在C源代码里声明的, 自然其他源代码就没法使用了。 */ typedef struct { /* gobject是没有公开变量的,只有隐私变量。 如果想公共访问的话,可以通过方法或者属性。 */ gchar * name; /*姓名*/ ManageStudentGender gender; /*性别*/ guchar grade; /*年级*/ gshort admission_year; /*入学年份*/ } ManageStudentPrivate; G_DEFINE_TYPE_WITH_PRIVATE (ManageStudent, manage_student, G_TYPE_OBJECT); /* 如果不需要隐私成员,用G_DEFINE_TYPE替代。 */ static void manage_student_constructed (GObject *obj) { G_OBJECT_CLASS (manage_student_parent_class)->constructed (obj); } /* 见下,这些函数不是必须的。 */ static void manage_student_finalize (GObject *obj) { ManageStudent *self = MANAGE_STUDENT (obj); G_OBJECT_CLASS (manage_student_parent_class)->finalize(obj); } /* 见下,这些函数不是必须的。 */ static void manage_student_class_init (ManageStudentClass *klass) { /* 在产生第一个实例之时,这个类也同时被建立。一个类在建立时就会执行这个函数的内容。 可以在这里重新指定一些函数,这些并不是必须的。 */ GObjectClass *object_class = G_OBJECT_CLASS (klass); /* 每次产生一个实例之「后」,执行constructed函数。这是一个指针,需要手动指定。 */ object_class->constructed = manage_student_constructed; /* 实例的解构函数。这是一个指针,需要手动指定。 */ object_class->finalize = manage_student_finalize; } static void manage_student_init (ManageStudent *self) { /* 一个类的对象在被实例化时,执行这部分的内容。 一般在这里为对象的成员变量赋初始值。 */ ManageStudentPrivate *priv = manage_student_get_instance_private (self); /* 如果需要私有成员,需要这样获取。 */ priv -> name = "吴名氏"; priv -> gender = MANAGE_STUDENT_GENDER_MALE; priv -> grade = 1; priv -> admission_year = 2023; /* 如果不给变量赋予初始值,有可能导致意外发生。 */ } /* 公共方法的声明在头文件。 每个方法的第一个参数都应该是自己。gobject没法像其他面向对象语言那样, 可以直接用「对象.方法」来调用。所以要用第一个参数传递对象。 写过python的应该有体会。 */ void manage_student_update_info (ManageStudent * self, gchar * name, ManageStudentGender gender, guchar grade, gshort admission_year) { ManageStudentPrivate *priv = manage_student_get_instance_private (self); priv -> name = name; /* 如果用复制的方式会更安全。所以说,程序还有很多优化的空间呢。 */ priv -> gender = gender; if (grade > 4) { grade = 4; } priv -> grade = grade; priv -> admission_year = admission_year; } /* 如果一个方法只在C源文件里声明和定义,那他就是私有方法。 如果声明在头文件,那么就是公共方法。 */ gshort manage_student_years_to_graduate (ManageStudent * self) { ManageStudentPrivate *priv = manage_student_get_instance_private (self); return (5 - priv -> grade + MANAGE_STUDENT_YEAR_NOW - priv -> admission_year); } void manage_student_self_introduction (ManageStudent * self) { ManageStudentPrivate *priv = manage_student_get_instance_private (self); printf("我的名字是%s,%d年入学,", priv -> name, priv -> admission_year ); if (manage_student_years_to_graduate (self) > 4) { printf ("已经毕业了。"); } else { printf ("还有%d年毕业。", manage_student_years_to_graduate (self)); } } void manage_student_describe_gender (ManageStudent * self) { ManageStudentPrivate *priv = manage_student_get_instance_private (self); if (priv -> gender == MANAGE_STUDENT_GENDER_MALE) { printf ("我是男生。"); } else { printf ("我是女生。"); } }
在对象设计完成后,我们就可以去互动一下了。我们把代码写在主函数中,然后你可以编译一下看看结果,自己改一改内容,感觉一下。
main.c内容:
main.c
#include #include "manage-student.h" int main() { /* 创建一个全新的对象。 */ ManageStudent *student_a = g_object_new (MANAGE_TYPE_STUDENT, NULL); manage_student_self_introduction (student_a); manage_student_describe_gender (student_a); printf ("\n"); /* 初始值是这样的,下面我们用之前做的方法给各个成员赋值。 */ manage_student_update_info (student_a, "小明", MANAGE_STUDENT_GENDER_FEMALE, 3, 2023); manage_student_self_introduction (student_a); manage_student_describe_gender (student_a); printf ("\n"); /* 重新查看一下。 */ /* 对象不再使用后,你可以给他们清除,来减少内存消耗。 在这个程序中,由于已经执行完成了,所以是否清除也不重要了。 但是这个方法迟早会有用的。 */ g_clear_object (&student_a); /* 清除了对象以后,就不要再使用了。 否则你就是在使用子虚乌有的东西。 */ return 0; }
你可以使用gcc来编译(如果你用的是其他编译器,请自行研究),使用如下命令:
gcc ./main.c ./manage-student.c -I /usr/include/glib-2.0/ -I /usr/lib/glib-2.0/include -lgobject-2.0 -lglib-2.0 -o main
或者:
gcc ./main.c ./manage-student.c `pkg-config --cflags gobject-2.0` `pkg-config --libs gobject-2.0` -o main
上面我们已经完成了第一个「类的创造」的案例,接下来我们做一个「类的继承」。
设想一个情景,你的学校来了些美国留学生。这位对你说:「你这个系统挺好,但是我们美国人用不了。」怎么回事呢?原来在美国,性别可以随便填写,但是我们设计的这个系统,性别是用枚举做的,如果要支持美国人的性别,就要做好多好多种,太麻烦了。所以我们索性专门给美国单独做一个类,给他们的性别用字符串变量就行了。幸亏我们刚才做的是一个可继承的类,只需要派生出一个新类给美国人用就可以了。
在这个案例我们完善的是:
一个不可被继承的类usa student,命名空间还是manage。根据模板,我们需要在头文件中做一个获取对象的型的宏(MANAGE_TYPE_USA_STUDENT),类的声明(G_DECLARE_FINAL_TYPE ()),(这里注意,我们要继承ManageStudent),一个必须的函数(manage_usa_student_new ())。在C源代码文件中做对象结构体(ManageUsaStudent),类的定义(G_DEFINE_TYPE()),类的初始函数(manage_student_class_init ()),对象的初始函数(manage_student_init ())。
MANAGE_TYPE_USA_STUDENT
G_DECLARE_FINAL_TYPE ()
manage_usa_student_new ()
ManageUsaStudent
G_DEFINE_TYPE()
一个隐私变量,美国专用性别。隐私变量的初始化,放在对象的初始函数中。
存储数据、介绍性别的两个个公开方法(在头文件中声明,源代码文件中定义)。我们之后可以重复利用基类的自我介绍方法。
代码我已经写好了,放在下面。你可以试着找一找这几个部分分别对应着哪段代码。也可以自己试着写一遍。一些重复的注释我就不留了。
manage-usa-student.h内容:
manage-usa-student.h
#pragma once #include "manage-student.h" G_BEGIN_DECLS #define MANAGE_TYPE_USA_STUDENT manage_usa_student_get_type() G_DECLARE_FINAL_TYPE (ManageUsaStudent, manage_usa_student, MANAGE, USA_STUDENT, ManageStudent); /* 这里我们继承ManageStudent。 */ ManageUsaStudent * manage_usa_student_new (void); void manage_usa_student_update_info (ManageUsaStudent * self, gchar * name, gchar * gender, guchar grade, gshort admission_year); void manage_usa_student_describe_gender (ManageUsaStudent * self); G_END_DECLS
manage-usa-student.c内容:
manage-usa-student.c
#include "manage-usa-student.h" #include struct _ManageUsaStudent { ManageStudent parent_instance; gchar * usa_gender; }; G_DEFINE_TYPE (ManageUsaStudent, manage_usa_student, MANAGE_TYPE_STUDENT); static void manage_usa_student_class_init (ManageUsaStudentClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); } static void manage_usa_student_init (ManageUsaStudent *self) { self -> usa_gender = "未知"; } void manage_usa_student_update_info (ManageUsaStudent * self, gchar * name, gchar * gender, guchar grade, gshort admission_year) { manage_student_update_info (MANAGE_STUDENT (self) /*把类型转换成父类型*/ , name, MANAGE_STUDENT_GENDER_MALE, grade, admission_year); /* gobject不能自动转换类型,需要手动转换才能使用父类型的方法。 */ self -> usa_gender = gender; } void manage_usa_student_describe_gender (ManageUsaStudent * self) { printf("我的性别是%s。", self -> usa_gender); }
我们改写一下main.c,试试与新类的实例互动。
main.c 内容:
#include #include "manage-student.h" #include "manage-usa-student.h" int main() { ManageStudent *student_a = g_object_new (MANAGE_TYPE_STUDENT, NULL); manage_student_self_introduction (student_a); manage_student_describe_gender (student_a); printf ("\n"); manage_student_update_info (student_a, "小明", MANAGE_STUDENT_GENDER_FEMALE, 3, 2023); manage_student_self_introduction (student_a); manage_student_describe_gender (student_a); printf ("\n"); g_clear_object (&student_a); /* 在上一个案例中讲解的,不再赘述。 */ ManageUsaStudent *student_b = g_object_new (MANAGE_TYPE_USA_STUDENT, NULL); manage_usa_student_update_info (student_b, "John", "你猜", 1, 2020); manage_student_self_introduction (MANAGE_STUDENT (student_b)); /* 进行类型转换,就能使用父类的方法。 */ manage_usa_student_describe_gender (student_b); printf ("\n"); g_clear_object (&student_b); return 0; }
编译的命令和之前一样,你只需要再添加一个源代码文件manage-usa-student.c到命令里面就可以了。
那么,我们的案例就演示完了。当然这只是GObject最基础的部分,想更深入的话可以读一读官网文章,各种网页书籍资料,当然还有许多开源软件的源代码。在下一篇文章中,我将讲一讲有关GObject的,其他有趣的事。
No replies yet
Rankings
Popular Events
纸上得来终觉浅,绝知此事要躬行。我们现在通过一个简单的案例来体会一下GObject中类的定义与使用。如果大家遇到了麻烦的话,可以回过头查看之前的几章。这个案例不会很难,代码也不是很严谨,纯粹当作之前理论的补充。
想要自己动手编写运行的话,请自己安装一下glib的开发库,C语言的编译器,和你最喜欢的代码编辑器。(都可以在系统软件源中找到,这些我就不示范了。)
这也是一个比较经典的编程课作业,就是做一个「学生管理系统」。我们不要弄那么复杂,只做一个学生档案的类,能存储数据和互动就可以了。在这个案例中所完善的是:
声明和定义一个可派生的类student,命名空间是manage。根据模板,我们需要在头文件中做一个获取对象的型的宏(
MANAGE_TYPE_STUDENT
),类的声明(G_DECLARE_DERIVABLE_TYPE ()
),类的结构体(_ManageStudentClass
),一个必须的函数(manage_student_new ()
)。在C源代码文件中做隐私结构体(ManageStudentPrivate
),带隐私结构体的定义(G_DEFINE_TYPE_WITH_PRIVATE ()
),类的初始函数(manage_student_class_init ()
),对象的初始函数(manage_student_init ()
)。不要怕多,他们都是模板,不需要自由发挥,只要抄写上去,然后把命名空间和类名都改成你自己的就可以了。
几个隐私变量,姓名、性别、年级、入学年份。其中性别是一个枚举。把这些变量放到隐私结构体中。然后枚举和常量在头文件中定义。
几个方法,存储数据、自我介绍、介绍性别的三个公开方法(在头文件中声明,源代码文件中定义)。另外,还有一个计算毕业还有多少年的隐私方法(只定义不声明),这个方法会在自我介绍的公开方法中利用。
隐私变量的初始化,放在对象的初始函数中。另外是几个可以覆盖的,在对象声明周期中有影响的函数指针(比如解构方法),赋值在类的初始函数中。(这部分在这个案例中其实是没有作用的,只是用来演示。)
代码我已经写好了,放在下面。你可以试着找一找这几个部分分别对应着哪段代码。也可以自己试着写一遍。我在很多地方都留了注释,方便大家理解。
manage-student.h
内容:manage-student.c
内容:在对象设计完成后,我们就可以去互动一下了。我们把代码写在主函数中,然后你可以编译一下看看结果,自己改一改内容,感觉一下。
main.c
内容:你可以使用gcc来编译(如果你用的是其他编译器,请自行研究),使用如下命令:
或者:
上面我们已经完成了第一个「类的创造」的案例,接下来我们做一个「类的继承」。
设想一个情景,你的学校来了些美国留学生。这位对你说:「你这个系统挺好,但是我们美国人用不了。」怎么回事呢?原来在美国,性别可以随便填写,但是我们设计的这个系统,性别是用枚举做的,如果要支持美国人的性别,就要做好多好多种,太麻烦了。所以我们索性专门给美国单独做一个类,给他们的性别用字符串变量就行了。幸亏我们刚才做的是一个可继承的类,只需要派生出一个新类给美国人用就可以了。
在这个案例我们完善的是:
一个不可被继承的类usa student,命名空间还是manage。根据模板,我们需要在头文件中做一个获取对象的型的宏(
MANAGE_TYPE_USA_STUDENT
),类的声明(G_DECLARE_FINAL_TYPE ()
),(这里注意,我们要继承ManageStudent),一个必须的函数(manage_usa_student_new ()
)。在C源代码文件中做对象结构体(ManageUsaStudent
),类的定义(G_DEFINE_TYPE()
),类的初始函数(manage_student_class_init ()
),对象的初始函数(manage_student_init ()
)。一个隐私变量,美国专用性别。隐私变量的初始化,放在对象的初始函数中。
存储数据、介绍性别的两个个公开方法(在头文件中声明,源代码文件中定义)。我们之后可以重复利用基类的自我介绍方法。
代码我已经写好了,放在下面。你可以试着找一找这几个部分分别对应着哪段代码。也可以自己试着写一遍。一些重复的注释我就不留了。
manage-usa-student.h
内容:manage-usa-student.c
内容:我们改写一下main.c,试试与新类的实例互动。
main.c
内容:编译的命令和之前一样,你只需要再添加一个源代码文件
manage-usa-student.c
到命令里面就可以了。那么,我们的案例就演示完了。当然这只是GObject最基础的部分,想更深入的话可以读一读官网文章,各种网页书籍资料,当然还有许多开源软件的源代码。在下一篇文章中,我将讲一讲有关GObject的,其他有趣的事。