继承与接口
函数指针、回调函数与 GObject 闭包

GObject 对接口的模拟

Garfileo posted @ 2011年3月16日 20:02 in GObject 笔记 with tags glib GObject , 13520 阅读

在文档 [1] 中谈到接口古已有之,但是类的继承赋予了它一些新的概念。本文结合实例,学习如何使用 GObject 库所提供的接口类型来表达这些概念。

接口声明

下面的代码(文件名 my-iusb.h)声明了一个叫做 MyIUsb 的接口,My 是项目名,I 是 interface 的首字母的大写,Usb 表示接口的名称。MyIUsb 就表示在“My”项目里,Usb 是一个 Interface。

#ifndef MY_IUSB_H
#define MY_IUSB_H
 
#include <glib-object.h>
 
#define MY_TYPE_IUSB (my_iusb_get_type ())
#define MY_IUSB(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj),MY_TYPE_IUSB, MyIUsb))

typedef struct _MyIUsb MyIUsb;
typedef struct _MyIUsbInterface MyIUsbInterface;
 
struct _MyIUsbInterface {
    GTypeInterface parent_interface;
 
    gchar * (*read) (MyIUsb *self);
    void (*write) (MyIUsb *self, const gchar *str);
};


GType my_iusb_get_type (void);

gchar * my_iusb_read (MyIUsb *self);
void my_iusb_write (MyIUsb *self, const gchar *str);
 
#endif

上述代码与文档[5]中 KbBibtex 类的声明代码很相似,但也有所区别。

首先,MyIUsb 接口的实例结构体,它只是个名字,并没有具体实现。这是因为,在 Java 那样的语言里谈到“接口”,那么则意味着它是无法实例化的。这其中是有一定的道理的,因为接口只是协议嘛。

其次,MyIUsbInterface 是 MyIUsb 接口的类结构体,它继承自 GTypeInterface 类结构体。也就是说,当你要声明接口时,那么接口的类结构体便要继承 GTypeInterface 类结构体,而当你声明的是可实例化为对象的类时,其类结构体便要继承 GObjectClass。

再次,在 MyIUsbInterface 结构体中,可包含一组函数指针,它们便是接口的协议。

最后,声明接口。对于本例而言,接口便是 my_iusb_read 与 my_iusb_write。

上述代码的解读过程大致是:

  1. 这是一个 usb 接口。
  2. 这个接口即可以 read,也可以 write,但是 read 只能是返回字符串,而 write 只能是接受字符串
  3. 声明这个接口的具体形式,在现实中则以理解为声明 usb 接口有几根线构成,每根线的功能等等。

接口的定义

建立 my-iusb.c 源文件,内容如下:

#include "my-iusb.h"

G_DEFINE_INTERFACE (MyIUsb, my_iusb, G_TYPE_INVALID);

static void
my_iusb_default_init (MyIUsbInterface *iface)
{
}

gchar *
my_iusb_read (MyIUsb *self)
{
        g_return_if_fail (MY_IS_IUSB (self));

        MY_IUSB_GET_INTERFACE (self)->read (self);
}

void
my_iusb_write (MyIUsb *self, const gchar *str)
{
        g_return_if_fail (MY_IS_IUSB (self));

        MY_IUSB_GET_INTERFACE (self)->write (self, str);
}


其中,my_iusb_default_init 是 G_DEFINE_INTERFACE 宏的展开代码中声明的一个函数,其中可以放置接口的一些初始化代码。如果没有这方面的需求,就让它表现为一个空函数即可,否则编译器会警告你,说你有一个函数声明了但没有实现。

另外,上述代码中出现了三个陌生的宏,其功能如下:

  • G_DEFINE_INTERFACE 宏的功用与 G_DEFINE_TYPE 类似,后者在 GObject 子类化的时候经常使用;
  • MY_IS_IUSB 宏是用来检测对象是否为 MyIUsb 类型,最好要在 my-iusb.h 中进行定义,代码为:
#define MY_IS_IUSB(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_IUSB))
  • 在 GObject 子类化的时候,也可以定义类似的宏,用于识别某个对象对应哪种类型。比如之前我们用过的一个 G_IS_OBJECT 宏,它可以识别对象是否为 GObject 类型的对象。
  • MY_IUSB_GET_INTERFACE 宏,用于从 MyIUsb 接口的实例结构体中取出类结构体指针,然后利用该指针访问接口对应的方法。至于 MyIUsb 接口的实例结构体是怎样与类结构体指针取得关联的,那是 GObject 的内幕,暂且不必关心。MY_IUSB_GET_INTERFACE 宏也需要在 my-iusb.h 中进行定义,如下:
#define MY_IUSB_GET_INTERFACE(obj) (\
                G_TYPE_INSTANCE_GET_INTERFACE ((obj), MY_TYPE_IUSB, MyIUsbInterface))

现在,将 my-iusb.h 文件更新为:

#ifndef MY_IUSB_H
#define MY_IUSB_H
 
#include <glib-object.h>
 
#define MY_TYPE_IUSB (my_iusb_get_type ())
#define MY_IUSB(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj),MY_TYPE_IUSB, MyIUsb))
#define MY_IS_IUSB(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_IUSB))
#define MY_IUSB_GET_INTERFACE(obj) (\
                G_TYPE_INSTANCE_GET_INTERFACE ((obj), MY_TYPE_IUSB, MyIUsbInterface))
 
typedef struct _MyIUsb MyIUsb;
typedef struct _MyIUsbInterface MyIUsbInterface;
 
struct _MyIUsbInterface {
    GTypeInterface parent_interface;
 
    gchar * (*read) (MyIUsb *self);
    void (*write) (MyIUsb *self, const gchar *str);
};


GType my_iusb_get_type (void);

gchar * my_iusb_read (MyIUsb *self);
void my_iusb_write (MyIUsb *self, const gchar *str);
 
#endif

插曲:经常要用到并且需要自己定义的宏

文档 [2-5] 中自定义了多个宏,在此略微进行总结一下,免的后续文档再多费口舌。

对于 GObject 的子类化,那么在声明类的时候,在头文件中直接插入类似下面的一组宏定义:

#define P_TYPE_T (p_t_get_type ())
#define P_T(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), P_TYPE_T, PT))
#define P_IS_T(obj) G_TYPE_CHECK_INSTANCE_TYPE ((obj), P_TYPE_T))
#define P_T_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), P_TYPE_T, PTClass))
#define P_IS_T_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), P_TYPE_T))
#define P_T_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), P_TYPE_T, PTClass))

这些宏的用法总结如下:

  • P_TYPE_T:仅在使用 g_object_new 进行对象实例化的时候使用一次,用于向 GObject 库的类型系统注册 PT 类;
  • P_T (obj):用于将 obj 对象的类型强制转换为 P_T 类的实例结构体类型;
  • P_IS_T (obj):用于判断 obj 对象的类型是否为 P_T 类的实例结构体类型;
  • P_T_CLASS(klass):用于将 klass 类结构体的类型强制转换为 P_T 类的类结构体类型;
  • P_IS_T_CLASS(klass):用于判断 klass 类结构体的类型是否为 P_T 类的类结构体类型;
  • P_T_GET_CLASS(obj):获取 obj 对象对应的类结构体类型。

对于 GTypeInterface 的子类化,在声明类的时候,在头文件中直接插入类似下面的一组宏定义:

#define P_TYPE_IT (p_t_get_type ())
#define P_IT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), P_TYPE_IT, PIt))
#define P_IS_IT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), P_TYPE_IT))
#define P_IT_GET_INTERFACE(obj) \
        (G_TYPE_INSTANCE_GET_INTERFACE ((obj), P_TYPE_IT, PItInterface))

  • P_TYPE_IT:仅在接口实现时使用一次,用于向 GObject 库的类型系统注册 PIT 接口;
  • P_IT (obj):用于将 obj 对象的类型强制转换为 P_IT 接口的实例结构体类型;
  • P_IS_IT (obj):用于判断 obj 对象的类型是否为 P_IT 接口的实例结构体类型;
  • P_IT_GET_INTERFACE(obj):获取 obj 对象对应的 P_IT 接口的类结构体类型。

接口的实现——制造 U 盘

既然已经有了 USB 接口的声明(协议),那就意味着我们可以制造具备这种接口的类与对象了。

首先声明 U 盘类(my-udisk.h):

#ifndef MY_UDISK_H
#define MY_UDISK_H

#include "my-iusb.h"

#define MY_TYPE_UDISK (my_udisk_get_type ())
#define MY_UDISK(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MY_TYPE_UDISK, MyUdisk))
#define MY_IS_UDISK(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MY_TYPE_UDISK))
#define MY_UDISK_CLASS(klass) \
        (G_TYPE_CHECK_CLASS_CAST ((klass), MY_TYPE_UDISK, MyUdiskClass))
#define MY_IS_UDISK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MY_TYPE_UDISK))
#define MY_UDISK_GET_CLASS(obj) \
        (G_TYPE_INSTANCE_GET_CLASS ((obj),MY_TYPE_UDISK,MyUdiskClass))

typedef struct _MyUdisk MyUdisk;
typedef struct _MyUdiskClass MyUdiskClass;

struct _MyUdisk {
        GObject parent;
        GString *data;
};
struct _MyUdiskClass {
        GObjectClass parent_class;
};

GType my_udisk_get_type (void);

#endif

上述代码声明了一个 MyUdisk 类,它是 GObject 的子类。MyUdisk 类的实例结构体中有一个 GString 类型的 data 属性,用于存储数据。也就是说,这个 U 盘设计的有些脑残,因为它只能存储一个字符串!

然后定义 U 盘类(my-udisk.c):

#include "my-udisk.h"

static void my_iusb_interface_init (MyIUsbInterface *iface);

G_DEFINE_TYPE_WITH_CODE (MyUdisk, my_udisk, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (MY_TYPE_IUSB, my_iusb_interface_init));

static gchar *
my_udisk_read (MyIUsb *iusb)
{
        MyUdisk *udisk = MY_UDISK (iusb);
        return udisk->data->str;
}

static void
my_udisk_write (MyIUsb *iusb, const gchar *str)
{
        MyUdisk *udisk = MY_UDISK (iusb);
        g_string_assign (udisk->data, str);
}

static void
my_udisk_init (MyUdisk *self)
{
        self->data = g_string_new (NULL);
}

static void
my_udisk_class_init (MyUdiskClass *self)
{
}

static void
my_iusb_interface_init (MyIUsbInterface *iface)
{
        iface->read = my_udisk_read;
        iface->write = my_udisk_write;
}

上述代码中,有几处需要留意的地方:

  • my_iusb_interface_init 函数声明必须要放在 G_DEFINE_TYPE_WITH_CODE 宏之前,因为这个宏的展开代码中需要使用这个函数;
  • G_DEFINE_TYPE_WITH_CODE 是文档 [2-5] 中出现过的 G_DEFINE_TYPE 宏的“扩展版本”,在本例中可以向 my_udisk_get_type 函数(即 MY_TYPE_UDISK 宏展开的那个函数)中插入 C 代码。在本例中,这个宏所插入的 C 代码是“G_IMPLEMENT_INTERFACE(MY_TYPE_IUSB,my_iusb_interface_init)”,其中 G_IMPLEMENT_INTERFACE 宏的作用是将接口添加到  MyUdisk 类中;
  • my_iusb_interface_init 函数的作用是表明 MyUdisk 类实现了 MyIUsb 所规定的接口。

至此,MyIUsb 接口的一个实现便完成了。下面的 main.c 文件中的代码用于测试这个 U 盘是否可以使用 MyIUsb 接口进行访问,即:

#include "my-udisk.h"

int
main (void)
{
        g_type_init ();

        MyUdisk *udisk = g_object_new (MY_TYPE_UDISK, NULL);

        my_iusb_write (MY_IUSB (udisk), "I am u-disk!");
        gchar *data = my_iusb_read (MY_IUSB (udisk));

        g_printf ("%s\n\n", data);

        g_printf ("Is udisk a MyIUsb object?\n");
        if (MY_IS_IUSB (udisk))
                g_printf ("Yes!\n");
        else
                g_printf ("No!\n");

        g_printf ("\nIs udisk a MyUdisk object?\n");
        if (MY_IS_UDISK (udisk))
                g_printf ("Yes!\n");
        else
                g_printf ("No!\n");

        return 0;
}

这个程序的编译命令及执行结果如下:

$ gcc $(pkg-config --cflags --libs gobject-2.0) my-iusb.c my-udisk.c main.c -o test
$ ./test
I am u-disk!

Is udisk a MyIUsb object?
Yes!

Is udisk a MyUdisk object?
Yes!

一切都在掌握之中啊。

还可以接着造移动硬盘

可以像制造 U 盘那样,便可以炮制移动硬盘了。这项艰巨的任务便交给你了,而懒惰的我只提供下面这个计算机主机程序:

#include "my-udisk.h"

int
main (void)
{
        g_type_init ();

        gchar *data = NULL;

        MyUdisk *udisk = g_object_new (MY_TYPE_UDISK, NULL);
        my_iusb_write (MY_IUSB (udisk), "I am an u-disk!");
        data = my_iusb_read (MY_IUSB (udisk));
        g_printf ("%s\n", data);

        MyPortableHardDisk *phdisk = g_object_new (MY_TYPE_PORTABLE_HARD_DISK, NULL);
        my_iusb_write (MY_IUSB (phdisk), "I am a portable hard disk!");
        data = my_iusb_read (MY_IUSB (phdisk));
        g_printf ("%s\n", data);

        return 0;
}

~ End ~

参考文档

[1] 继承与接口

[2] 使用 GObject 库模拟类的数据封装形式

[3] GObject 子类私有属性模拟

[4] GObject 子类私有属性的外部访问

[5] 温故而知新

转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com

黑暗诗人 说:
2011年10月12日 18:30

您好,这几天每天以你的blog作为学习教材,领悟了不少,在此深深的感谢您把这么优秀的文章分享出来,我前几天买了一本《计算机程序设计艺术》一卷,看了几页基本看不懂,书上赫然写作要是看不懂此书就不要当程序员了,主要是上面高数知识很多都忘了。
我想问一下大拿,您是怎么理解算法的,我上大学时学过java后来自学了c++,对于面向对象也是懂一点,但是要说算法的话我还真不会几种,都是上学时学的,树和表这些都忘了要说写都不写不出,现在就只会点链表了。上次我问来公司讲内核驱动讲座的老师关于算法的问题,他一句话让我很震惊,他说他很久没有在一线编码现在叫他用c写一个二叉树他都写不出来,他说他只是知道它的原理具体怎么写他不会(他很坦诚,但在驱动绝对是大牛,他出过一本关于ARM6410应用开发的书,我们公司人手一本呵呵,尤其是框架他理解的很透彻),所以很迷茫了,到底要怎么学怎么看待算法,必须掌握那些算法,我真的迷茫,现在不会还要去重新学高数吧,Garfileo你有时间帮我讲讲,因为这个问题必须由高手才能解决,麻烦您了

Avatar_small
Garfileo 说:
2011年10月12日 20:14

@黑暗诗人: 我很弱啊,那本书我也没看懂过,所以我只是个业余程序猿 :)

算法是程序的灵魂,自然是很重要的,能挤出时间来认真学习一下定会有收获。最好是能与实际工作相结合,保持学习热情,慢慢积累。

如果没有时间认真学习算法的话,那么即便你的程序没有多少灵魂,只要它能解决实际问题,总比那些纸上谈兵的人要好的多。更何况现在许多有用的算法和数据结构都已经有现成的代码或库函数可供使用。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter