[software development] GObject简介和入门指引(四)简单案例
Tofloor
poster avatar
enforcee
deepin
2023-08-11 06:22
Author

纸上得来终觉浅,绝知此事要躬行。我们现在通过一个简单的案例来体会一下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内容:

#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内容:

#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内容:

#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-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内容:

#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的,其他有趣的事。

Reply Favorite View the author
All Replies

No replies yet