pentadactyl使用笔记

pentadactyl是firefox下的一款vim按键的插件。对于习惯了vim的人来说这个是非常方便的。

pentadactyl的默认按键跟vim差不多,而且也可以自行修改。 pentadactyl的配置文件在windows下是_pentadactylrc,在linux下是.pentadactylrc。如果不存在可以在firefox直接执行命令:mkp,就会生成~/.pentadactyl目录和一个空的~/.pentadactylrc文件。

然后就可以直接编辑这个文件了,编辑完成后再重启一下firefox即可。以下是我在网上找到的一些配置内容,仅供参考。

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
"1.0
loadplugins '\.(js|penta)$'
group user

" vim: set ft=pentadactyl:

map J <C-d>
map K <C-u>
" 修改Ctrl快捷键
noremap <C-c> <C-v><C-c>
noremap <C-a> <C-v><C-a>
cnoremap <C-c> <C-v><C-c>
cnoremap <C-v> <C-v><C-v>
cnoremap <C-x> <C-v><C-x>
inoremap <C-a> <C-v><C-a>
inoremap <C-c> <C-v><C-c>
inoremap <C-v> <C-v><C-v>
inoremap <C-x> <C-v><C-x>
inoremap <C-z> <C-v><C-z>
inoremap <C-y> <C-v><C-y>

" 在Gmail和Greader中屏蔽Vimperator
autocmd LocationChange .* :js modes.passAllKeys = /mail\.google\.com/.test(buffer.URL) || /google\.com\/reader\//.test(buffer.URL)

" 在有无菜单、地址栏之间切换:
map <C-Up> :se go=mnT<CR>
map <C-Down> :se go=s<CR>:

使用VIM管理git

之前看到在emacs下有管理git的插件,于是就想在vim下也有类似的没。网上搜了下,找到了Git-Vim,可以在VIM下管理git。
项目地址为: http://github.com/motemen/git-vim

安装了之后就可以直接在vim里输入命令管理git了,如:GitStatus就是git status,快捷键是<Leader>gs。(<Leader>默认为\,你也可以自己修改)

更多的用法可以看它的README

Bash常用快捷键

编辑命令

Ctrl + a :移到命令行首
Ctrl + e :移到命令行尾
Ctrl + f :按字符前移(右向)
Ctrl + b :按字符后移(左向)
Alt + f :按单词前移(右向)
Alt + b :按单词后移(左向)
Ctrl + xx:在命令行首和光标之间移动
Ctrl + u :从光标处删除至命令行首
Ctrl + k :从光标处删除至命令行尾
Ctrl + w :从光标处删除至字首
Alt + d :从光标处删除至字尾
Ctrl + d :删除光标处的字符
Ctrl + h :删除光标前的字符
Ctrl + y :粘贴至光标后
Alt + c :从光标处更改为首字母大写的单词
Alt + u :从光标处更改为全部大写的单词
Alt + l :从光标处更改为全部小写的单词
Ctrl + t :交换光标处和之前的字符
Alt + t :交换光标处和之前的单词
Alt + Backspace:与 Ctrl + w 相同类似,分隔符有些差别 [感谢 rezilla 指正]

重新执行命令

Ctrl + r:逆向搜索命令历史
Ctrl + g:从历史搜索模式退出
Ctrl + p:历史中的上一条命令
Ctrl + n:历史中的下一条命令
Alt + .:使用上一条命令的最后一个参数

控制命令

Ctrl + l:清屏
Ctrl + o:执行当前命令,并选择上一条命令
Ctrl + s:阻止屏幕输出
Ctrl + q:允许屏幕输出
Ctrl + c:终止命令
Ctrl + z:挂起命令

Bang (!) 命令

!!:执行上一条命令
!blah:执行最近的以 blah 开头的命令,如 !ls
!blah:p:仅打印输出,而不执行
!$:上一条命令的最后一个参数,与 Alt + . 相同
!$:p:打印输出 !$ 的内容
!*:上一条命令的所有参数
!*:p:打印输出 !* 的内容
^blah:删除上一条命令中的 blah
^blah^foo:将上一条命令中的 blah 替换为 foo
^blah^foo^:将上一条命令中所有的 blah 都替换为 foo

以上介绍的大多数 Bash 快捷键仅当在 emacs 编辑模式时有效,若你将 Bash 配置为 vi 编辑模式,那将遵循 vi 的按键绑定。通过 set -o emacs 将Bash设置为emacs模式,通过set -o vi 奖Bash设置为vi模式。Bash 默认为 emacs 编辑模式。
更多可以参考以下网址:
http://www.bigsmoke.us/readline/shortcuts
http://www.huangwei.me/bash_emacs.html

firefox全键盘操作:Pentadactyl

今天在网上淘到了一个很好的软件,Pentadactyl。

Pentadactyl是Vimperator的一个分支 。Vimperator 是让 Firefox 拥有模式、纯键盘操作、等 Vim 风格的强大扩展。

插件下载地址:https://addons.mozilla.org/en-US/firefox/addon/pentadactyl/

安装完后就可以像操作vim一样使用firefox了,这对于熟悉vim的人来说很方便,基本上不用再使用鼠标了。

基本的操作有
open:打开网页
tabopen:在新标签中打开网页
d:关闭当前标签
u:撤消关闭标签
y:复制当前网址
p:打开复制的网址
gt:切换标签

…………

更多的操作可以使用F1来看帮助。

[转载]GObject 学习笔记:GObject 的子类继承

在文档 [1] 中,我们构造了一个 KbBibtex 类,其构造过程看似挺复杂,但实际上只需要动手尝试一下,即可明白 GObject 子类化的各项步骤的意义与作用。许多事物之所以被认为复杂,是因为大家在观望。

本文沿用文档 [1] 中的那个 KbBibtex 示例,学习如何对其进行子类化,构造新类,即面向对象程序设计方法中类的继承。

文献有很多种类

写过论文的人都知道,参考文献的形式有许多种类,例如期刊论文、技术报告、专著等,并非所有的文献格式都能表示文档 [1] 所给出的 KbBibtex 对象属性,即:

1
2
3
4
5
6
7
typedef struct _KbBibtexPrivate KbBibtexPrivate;
struct _KbBibtexPrivate {
GString *title;
GString *author;
GString *publisher;
guint year;
};

对于期刊论文而言,也许我们期望的数据结构是:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _KbBibtexPrivate KbBibtexPrivate;
struct _KbBibtexPrivate {
GString *title;
GString *author;
GString *journal;
GString *volume;
GString *number;
GString *pages;
guint year;
GString *publisher;
};

对于技术报告,需求又要变成:

1
2
3
4
5
6
7
typedef struct _KbBibtexPrivate KbBibtexPrivate;
struct _KbBibtexPrivate {
GString *title;
GString *author;
GString *institution;
guint year;
};

这样的变化非常之多。因此,设计一个“万能”的 KbBibtex 类,使之可以表示任何文献类型,看上去会很美好。

类的继承

因为期刊论文这种对象只比文档 [1] 中的 KbBibtex 对象多出 4 个属性,即 journal、volume、number、pages,其他属性皆与 KbBibtex 对象相同。

在程序猿的职业生涯中也许不断听到这样的警告:Don’t Repeat Yourself(不要重复)!所以面向对象程序设计方法会告诉我们,既然 KbBibtex 对象已经拥有了一部分期刊论文对象所需要的属性,后者与前者都属于 KbBibtex 类(因为它们都是文献类型),那么只需设计一个期刊论文类,让它可以继承 KbBibtex 类的所以所拥有的一切,那么就可以不用 DRY 了。

那么怎么来实现?和 GObject 子类化过程相似,只需建立一个 kb-article.h 头文件,与继承相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
#include "kb-bibtex.h"

typedef struct _KbArticle KbArticle;
struct _KbArticle {
KbBibtex parent;
};

typedef struct _KbArticleClass KbArticleClass;
struct _KbArticleClass {
KbBibtexClass parent_class;
};

然后,再建立一个 kb-article.c 源文件,其中与继承相关的代码如下:

1
G_DEFINE_TYPE (KbArticle, kb_article, KB_TYPE_BIBTEX);

另外,KbBibtex 对象的 kb_bibtex_printf 方法也需要被 KbArticle 对象继承,这只需在 kb_article_printf 函数中调用 kb_bibtex_printf 即可实现,例如:

1
2
3
4
5
6
7
8
void
kb_article_printf (KbArticle *self)
{
kb_bibtex_printf (&self->parent);

/* 剩下代码是 KbArticle 对象的 kb_article_printf 方法的具体实现 */
... ...
}

当然,kb-article.h 和 kb-article.c 中剩余代码需要类似文档 [1] 中实现 KbBibtex 类那样去实现 KbArticle 类。这部分代码,我希望你能动手去尝试一下。下面,我仅给出测试 KbArticle 类的 main.c 源文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "kb-article.h"

int
main (void)
{
g_type_init ();

KbArticle *entry = g_object_new (KB_TYPE_ARTICLE,
"title", "Breaking paragraphs into lines",
"author", "Knuth, D.E. and Plass, M.F.",
"publisher", "Wiley Online Library",
"journal", "Software: Practice and Experience",
"volume", "11",
"number", "11",
"year", 1981,
"pages", "1119-1184",
NULL);
kb_article_printf (entry);

g_object_unref (entry);
return 0;
}

测试结果表明,一切尽在掌握之中:

1
2
3
4
5
6
7
8
9
$ ./test
Title: Breaking paragraphs into lines
Author: Knuth, D.E. and Plass, M.F.
Publisher: Wiley Online Library
Year: 1981
Journal: Software: Practice and Experience
Volume: 11
Number: 11
Pages: 1119-1184

继承真的很美好?

通过类的继承来实现一部分的代码复用真的是很惬意。但是,《C 专家编程》一书的作者却不这么认为,为了说明类的继承通常很难解决现实问题,他运用了一个比喻,将程序比作一本书,并将程序库比作一个图书馆。当你基于一个程序库去写你个人的程序之时,好比你在利用图书馆中的藏书去写你个人的书,显然你不可能很轻松的在那些藏书中复印一部分拼凑出一本书。

就本文开头所举的例子而言,就无法通过继承文档 [1] 所设计的 KbBibtex 类来建立技术报告类,因为技术报告对象是没有 publisher 属性的。

也许你会说,那是 KbBibtex 类设计的太烂了。嗯,我承认这一点,但是你不可能要求别人在设计程序库时候知道你想要什么,就像你不能去抱怨为什么图书馆里的藏书怎么不是为你写书而准备的一样。

基类设计的失误,对于它的所有子类是一场巨大的灾难。要避免这种灾难,还是认真考虑自己所要解决的问题吧。其实很多问题都可以不需要使用继承便可以很好的解决,还有许多问题不需要继承很多的层次也可以很好的解决。

对于本文的问题,不采用继承的实现会更好。比如我们可以这样来改造 KbBibtex 类:

将 Bibtex 文献所有格式的属性都设为 KbBibtex 的类属性
为 Bibtex 对象拥有一个链表或数组之类的线性表容器,或者效率更高的 Hash 表容器(GLib 库均已提供),容器的第一个单元存储 Bibtex 对象对应的文献类型,其他单元则存储文献的属性。
在 kb_bibtex_set_property 与 kb_bibtex_get_property 函数中,实现 Bibtex 类属性与 Bibtex 对象属性的数据交换。

仅此而已。

如果说继承是美好的,那么平坦一些会更美好。如果你不愿意选择平坦,那么可以选择接口(Interface),这是下一篇文档的主题。

~ End ~

原文出处:http://garfileo.is-programmer.com

[转载]GObject 学习笔记:温故而知新

也许很多人将孔子所说的“温故而知新”理解为:温习学过的知识,从中获得新的领悟。但是,我更倾向于另一种解释:温习学过的知识,继续学习新的知识。本文借助一个更加平易和现实的示例用以温习文档 [1-3] 中的 GObject 知识,并进一步学习类与对象的方法。

Bibtex 文献类的设计,代号Kill Bill

这个才是本文的主题。我们将此次行动(或者称为项目也可)命名为“Kill Bill”,缩写“Kb”,其中并无深意,只是因为我正在看这部电影。

1. 类的声明

建立 kb-bibtex.h 头文件,以 GObject 类作为基类,声明 KbBibtex 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef KB_BIBTEX_H
#define KB_BIBTEX_H

#include <glib-object.h>

#define KB_TYPE_BIBTEX (kb_bibtex_get_type ())

#define KB_BIBTEX(object) \
G_TYPE_CHECK_INSTANCE_CAST ((object), KB_TYPE_BIBTEX, KbBibtex)

typedef struct _KbBibtex KbBibtex;
struct _KbBibtex {
GObject parent;
};

typedef struct _KbBibtexClass KbBibtexClass;
struct _KbBibtexClass {
GObjectClass parent_class;
};

GType kb_bibtex_get_type (void);

#endif

2. 类型注册

建立 kb-bibtex.c 源文件,对 KbBibtex 类进行定义,首先向 GObject 库的类型系统注册 KbBibtex 类型,使之成为 GObject 库认可的一类公民:

1
2
3
#include "kb-bibtex.h"

G_DEFINE_TYPE (KbBibtex, kb_bibtex, G_TYPE_OBJECT);

3. 对象的私有属性

在此,必须要澄清两个概念,它们在文档 [1-3] 中的描述有些混淆:

首先是对象属性,它指代隶属于实例结构体的数据,文档 [2] 主要讲述的是对象属性的隐藏。
其次是类属性,它指代隶属于类结构体的数据,可被所有的对象所共享,在文档 [3] 中我们利用了这一点,实现了通过类属性来访问对象属性。

下面定义 KbBibtex 对象的私有属性:

1
2
3
4
5
6
7
8
9
10
#define KB_BIBTEX_GET_PRIVATE(object) (\
G_TYPE_INSTANCE_GET_PRIVATE ((object), KB_TYPE_BIBTEX, KbBibtexPrivate))

typedef struct _KbBibtexPrivate KbBibtexPrivate;
struct _KbBibtexPrivate {
GString *title;
GString *author;
GString *publisher;
guint year;
};

4. 类结构体与实例结构体的构造函数(初始化函数)

在 KbBibtex 类结构体构造函数中安装对象的私有属性:

1
2
3
4
5
6
static void
kb_bibtex_class_init (KbBibtexClass *klass)
{
g_type_class_add_private (klass,
sizeof (KbBibtexPrivate));
}

kb_bibtex_class_init 参数名之所以使用 klass 而不是 class,是因为 class 是 c++ 语言的关键字,如果使用 class,那么 KbBibtex 类如是被 C++ 程序调用,那么程序编译时就杯具了。

KbBibtex 实例结构体的构造函数则什么也不做:

1
2
3
4
static void
kb_bibtex_init (KbBibtex *self)
{
}

因为我们打算采用文档 [3] 中的方式,通过类属性来访问对象属性。
5. 对象私有属性的外部访问

首先要实现类属性与对象属性之间的映射,即 kb_bibtex_set_property 与 kb_bibtex_get_property 函数:

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
static void
kb_bibtex_set_property (GObject *object, guint property_id,
const GValue *value, GParamSpec *pspec)
{
KbBibtex *self = KB_BIBTEX (object);
KbBibtexPrivate *priv = KB_BIBTEX_GET_PRIVATE (self);

switch (property_id) {
case PROPERTY_TITLE:
if (priv->title)
g_string_free (priv->title, TRUE);
priv->title = g_string_new (g_value_get_string (value));
break;
case PROPERTY_AUTHOR:
if (priv->author)
g_string_free (priv->author, TRUE);
priv->author = g_string_new (g_value_get_string (value));
break;
case PROPERTY_PUBLISHER:
if (priv->publisher)
g_string_free (priv->publisher, TRUE);
priv->publisher = g_string_new (g_value_get_string (value));
break;
case PROPERTY_YEAR:
priv->year = g_value_get_uint (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}

static void
kb_bibtex_get_property (GObject *object, guint property_id,
GValue *value, GParamSpec *pspec)
{
KbBibtex *self = KB_BIBTEX (object);
KbBibtexPrivate *priv = KB_BIBTEX_GET_PRIVATE (self);
GString *similar = NULL;

switch (property_id) {
case PROPERTY_TITLE:
g_value_set_string (value, priv->title->str);
break;
case PROPERTY_AUTHOR:
g_value_set_string (value, priv->author->str);
break;
case PROPERTY_PUBLISHER:
g_value_set_string (value, priv->publisher->str);
break;
case PROPERTY_YEAR:
g_value_set_uint (value, priv->year);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}

为了是上述代码能够工作,需要在 kb-bibtex.c 文件中定义 KbBibtex 各个类属性的 ID:

1
2
3
4
5
6
7
8
enum PROPERTY_BIBTEX {
PROPERTY_0,
PROPERTY_TITLE,
PROPERTY_AUTHOR,
PROPERTY_PUBLISHER,
PROPERTY_YEAR,
N_PROPERTIES
};

注意,enum 类型中,第一个成员 PROPERTY_0 仅仅是个占位符,它不会被使用,而最后一个成员 N_PROPERTIES 则用于表示类属性的个数,它在向类中安装属性的时候可以用到,详见下文。

6. 类属性的构造与安装

一旦完成了 KbBibtex 类属性与 KbBibtex 对象属性之间的映射,即 kb_bibtex_set_property 与 kb_bibtex_get_property 函数,那么便可以在 KbBibtex 类结构体构造函数中使用 GObject 类的 setter 与 getter 指针指向它们:

1
2
3
4
5
6
7
8
9
static void
kb_bibtex_class_init (KbBibtexClass *klass)
{
g_type_class_add_private (klass, sizeof (KbBibtexPrivate));

GObjectClass *base_class = G_OBJECT_CLASS (klass);
base_class->set_property = kb_bibtex_set_property;
base_class->get_property = kb_bibtex_get_property;
/* 未完 */

然后,构造类属性,与文档 [3] 有所区别,本文采用的是 ParamSpec 类型的指针数组,一遍在类属性构造完毕后,使用 g_object_class_install_properties 函数一并安装到类结构体中,而不是像文档 [3] 那样使用 g_object_class_install_property 函数一个一个的安装。

类属性的构造代码如下:

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
/* 接上面的代码 */
GParamSpec *properties[N_PROPERTIES] = {NULL,};
properties[PROPERTY_TITLE] =
g_param_spec_string ("title",
"Title",
"Bibliography title",
NULL,
G_PARAM_READWRITE);
properties[PROPERTY_AUTHOR] =
g_param_spec_string ("author",
"Author",
"Bibliography author",
NULL,
G_PARAM_READWRITE);
properties[PROPERTY_PUBLISHER] =
g_param_spec_string ("publisher",
"Publisher",
"Bibliography Publisher",
NULL,
G_PARAM_READWRITE);
properties[PROPERTY_YEAR] =
g_param_spec_uint ("year",
"Year",
"Bibliography year",
0,
G_MAXUINT,
0,
G_PARAM_READWRITE);
/* 未完 */

最后,安装类属性:

1
2
3
/* 接上面代码 */
g_object_class_install_properties (base_class, N_PROPERTIES, properties);
}

使用 KbBibtex 类

建立 main.c 源文件,内容为:

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
#include "kb-bibtex.h"

int
main (void)
{
g_type_init ();

KbBibtex *entry = g_object_new (KB_TYPE_BIBTEX,
"title", "The {\\TeX}Book",
"author", "Knuth, D. E.",
"publisher", "Addison-Wesley Professional",
"year", 1984,
NULL);

gchar *title, *author, *publisher;
guint year;

g_object_get (G_OBJECT (entry),
"title", &title,
"author", &author,
"publisher", &publisher,
"year", &year,
NULL);

g_printf (" Title: %s\n"
" Author: %s\n"
"Publisher: %s\n"
" Year: %d\n", title, author, publisher, year);

g_free (title);
g_free (author);
g_free (publisher);

g_object_unref (entry);
return 0;
}

编译这个程序的命令为:
$ gcc $(pkg-config –cflags –libs gobject-2.0) kb-bibtex.c main.c -o test
程序运行结果:
$ ./test
Title: The {\TeX}Book
Author: Knuth, D. E.
Publisher: Addison-Wesley Professional
Year: 1984

为类和对象添加方法

在面向对象程序设计方法中,类和对象皆由“属性”与“方法“构成。文档 [1-3] 所讲的均是基于 GObject 子类化的数据抽象与封装,而未有涉及到类和对象的方法。其实,这样说并不正确,因为我们已经接触到了 GObject 子类的类结构体与实例结构体的构造函数,它们分别是类的方法和对象的方法。

类的方法,形式如下:

返回值
函数名 (参数, …)
{

}

对象的方法,形式如下:

返回值
函数名 (参数, …)
{

}

#@¥%……开玩笑呢吧?这两种方法有什么区别?它们不就是普通的 C 函数么?

嗯,你以为呢?就是普通的 C 函数。

下面为 KbBibtex 类增加一个对象方法,这个函数的功能是可以在终端输出文献信息。

首先,在 kb-bibtex.h 头文件的底部增加函数声明:

1
2
/* 对象的方法 */
void kb_bibtex_printf (KbBibtex *self);

然后在 kb-bibtex.c 源文件中实现该函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
kb_bibtex_printf (KbBibtex *self)
{
gchar *title, *author, *publisher;
guint year;

g_object_get (G_OBJECT (self),
"title", &title,
"author", &author,
"publisher", &publisher,
"year", &year,
NULL);

g_printf (" Title: %s\n"
" Author: %s\n"
"Publisher: %s\n"
" Year: %d\n", title, author, publisher, year);

g_free (title);
g_free (author);
g_free (publisher);
}

这样,main.c 源文件便可以被简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "kb-bibtex.h"

int
main (void)
{
g_type_init ();

KbBibtex *entry = g_object_new (KB_TYPE_BIBTEX,
"title", "The {\\TeX}Book",
"author", "Knuth, D. E.",
"publisher", "Addison-Wesley Professional",
"year", 1984,
NULL);

kb_bibtex_printf (entry);

g_object_unref (entry);
return 0;
}

~ End ~

原文出处:http://garfileo.is-programmer.com

[转载]GObject 学习笔记:GObject 子类私有属性的外部访问

之前,写了一篇 GObject 劝学的文章 [1],还有两篇有关 GObject 子类对象数据封装的文章 [2, 3]。

虽然,创建一个 GObject 子类对象需要一些辅助函数和宏的支持,并且它们的内幕也令人费解,但是只要将足够的信任交托给 GObject 开发者,将那些辅助函数和宏当作“语法”糖一样享用,一切还是挺简单的。至于细节,还是等较为全面的掌握 GObject 库的用法之后再去挖掘!

现在,我们基本上知道了如何将数据封装并藏匿于 GObject 子类的实例结构体中。本文打算再向前走一步,关注如何实现在外部比较安全的访问(读写)这些数据。

简单的做法

像下面这样的双向链表数据结构:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
};

typedef struct _PMDList PMDList;
struct _PMDList {
PMDListNode *head;
PMDListNode *tail;
};

现在,我们希望能够安全访问 PMDList 结构题的两个成员,即链表的首结点指针 head 和尾结点指针 tail,以便进行一些操作,例如将两个双向链表 list1 和 list2 链接到一起。

所谓安全访问,意味着不要像下面这样简单粗暴:

1
2
3
/* 将 list1 与 list2 链接在一起 */
list1->tail->next = list2->head;
list2->head->prev = list1->tail;

而应当委婉一些:

1
2
3
4
5
6
7
PMDListNode *list1_tail, *list2_head;

list1_tail = pm_dlist_get (list1, TAIL);
list2_head = pm_dlist_get (list2, HEAD);

pm_dlist_set (list1, TAIL, NEXT, list2_head);
pm_dlist_set (list2, HEAD, PREV, list1_tail);

这样委婉的访问,有什么好处?答案很简单,可以将数据的变化与程序的功能隔离开,数据的变化不影响程序的功能。

试想,如果有一天,上述 PMDList 结构体的设计者使用 GObject 子类化的方法将双向链表定义为建 PMDList 类的形式,并且将链表的首结点指针 head 与尾结点都隐匿起来,那么上述的那个简单粗暴的数据访问方法便失效了。更糟糕的是,PMDList 类的设计者明知道很多人会受到这种数据变化的影响,对此也毫无办法。

如果 PMDList 结构体的设计者提供了 pm_dlist_set 与 pm_dlist_get 函数,那么即便设计者基于 GObject 子类化的方式定义了 PMDList 类,他只需要修改 pm_dlist_set 和 pm_dlist_get 函数,便可以让上述那种委婉方式访问 PMDList 结构体成员的代码不会受到任何影响。

既然 pm_dlist_set 与 pm_dlist_get 函数这样有用,我们可以像下面这样实现它们。

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
typedef enum _PM_DLIST_PROPERTY PM_DLIST_PROPERTY
enum _PM_DLIST_PROPERTY {
PM_DLIST_HEAD,
PM_DLIST_TAIL,
PM_DLIST_NODE_PREV,
PM_DLIST_NODE_NEXT
};

PMDListNode *
pm_dlist_get (PMDList *self, PM_DLIST_PROPERTY property)
{
PMDListNode *node = NULL;

switch (property) {
case PM_DLIST_HEAD:
node = self->head;
break;
case PM_DLIST_TAIL:
node = self->tail;
break;
default:
g_print ("对不起,你访问的成员不存在!\n");
}

return node;
}

void
pm_dlist_set (PMDList *self,
PM_DLIST_PROPERTY property,
PM_DLIST_PROPERTY subproperty,
PMDListNode *node)
{
switch (property) {
case PM_DLIST_HEAD:
if (subproperty == PM_DLIST_NODE_PREV)
self->head->prev = node;
else if (subproperty == PM_DLIST_NODE_NEXT)
self->head->next = node;
break;
case PM_DLIST_TAIL:
if (subproperty == PM_DLIST_NODE_PREV)
self->tail->prev = node;
else if (subproperty == PM_DLIST_NODE_NEXT)
self->tail->next = node;
break;
default:
g_print ("对不起,你访问的成员不存在!\n");
}
}

事实上,上述代码所实现的功能仅仅是实现下面这 6 种赋值运算:

1
2
3
4
5
6
7
8
9
10
PMDList *list;

list->head->prev = aaaa;
list->head->next = bbbb;

list->tail->prev = cccc;
list->tail->next = dddd;

node = list->head;
node = list->tail;

对于区区一个链表的最原始形态的属性访问模拟便已如此,那些内建支持面向对象的编程语言、动态编程语言以及函数编程语言,它们所提供的语法越高级,那么它们等价的 C 代码量便会越庞大。

如果你所解决的问题,需要很多层的数据抽象,如果使用 C 语言的话,就不得不写很多的模拟代码。倘若这些模拟代码在你全部代码所占的比重超过了你的容忍限度,可以考虑换一种更合适的编程语言。当然,你不可能是先用 C 写完代码后,再去评估那部分模拟代码所占的比重,但是这并不妨碍你凭借现有的经验去粗略估计。

我将 gtk+ 作为使用 C 语言应用的典范,gtk+ 3.0 的全部代码大约 515500 行,而 GObject 的代码大概 20000 行,其所占比重大约为 4%,这其中还不算 GTK+ 的那些基于 GObject 的底层库的代码量。我觉得 GTK+ 开发者使用 GObject 实现足够的面向对象支持,是比较划算的。

那个很二的参数

回顾一下文档 [2] 和 [3] 中出现过的 g_object_new 函数的参数:

1
PMDList *list = g_object_new (PM_TYPE_DLIST, NULL);

该函数第一个参数 PM_TYPE_DLIST 的含义在文档 [2] 中已有较为详细的解释,而第二个参数的含义一直被故意的忽略,现在才是分析它的最好时机。事实上,g_object_new 接受的是可变参数[4],第二个参数后面,还可以有第三个、第四个…理论上的无穷个。这些参数的作用可以用下面的代码来表现:

1
2
3
4
PMDList *list = g_object_new (PM_TYPE_DLIST,
"head", NULL,
"tail", NULL,
NULL);

如果采用这种方式调用 g_object_new 函数,意味着在文档 [3] 中的 dlist.c 文件中,不需要再在 PMDList 类的实例结构体初始化函数 pm_dlist_init 中对链表首结点和尾结点指针进行赋值了,即 pm_dlist_init 函数:

1
2
3
4
5
6
7
8
static void
pm_dlist_init (PMDList *self)
{
PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);

priv->head = NULL;
priv->tail = NULL;
}

可以为空:

1
2
3
4
static void
pm_dlist_init (PMDList *self)
{
}

换句话说,就是你在使用 g_object_new 函数进行对象实例化的过程中,可直接通过 g_object_new 函数的输入参数去初始化对象的属性,这是通过“属性名-属性值”参数来实现的,即 g_object_new 的第二个参数为属性名,第三个参数为属性值,它们在 g_object_new 内部会被合成为“属性名-属性值”结构;同理,第四个参数与第五个参数也可以形成“属性名-属性值”结构,依次类推,当属性名参数为 NULL 时,g_object_new 会认为“属性名:属性值”结构序列结束。上面示例中的 g_object_new 可形成 2 个“参数名:参数值”结构:

1
2
"head" : NULL
"tail" : NULL

g_object_new 函数会根据属性名匹配对象的相应属性,并将属性值赋予该属性,但是这需要 PMDList 类的设计者去实现一部分比较丑陋的代码。

将丑陋封锁在内部

要想实现上一节所讲述的让 g_object_new 函数中通过“属性名-属性值”结构为 GObject 子类对象的属性进行初始化,我们需要完成以下工作:

实现 p_t_set_property 与 p_t_get_property 函数,让它们来完成 g_object_new 函数的“属性名-属性值”结构向 GObject 子类属性的映射。
在GObject 子类的类结构体初始化函数中,让 GObject 类(基类)的两个函数指针 set_property 与 get_property 分别指向 p_t_set_property 与 p_t_get_property 函数。
在 GObject 子类的类结构体初始化函数中,为 GObject 子类安装属性。
前两个步骤,可以理解为 GObject 的两个虚函数的实现。第三个步骤,可以视为为比文档 [3] 中 GObject 子类私有属性更高级一些的模拟。

现在,开始动手吧。

首先,pm_dlist_set_property 和 pm_dlist_get_property 函数,可以像下面这样实现:

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
static void
pm_dlist_set_property (GObject *object, guint property_id,
const GValue *value, GParamSpec *pspec)
{
PMDList *self = PM_DLIST (object);
PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);

switch (property_id) {
case PROPERTY_DLIST_HEAD:
priv->head = g_value_get_pointer (value);
break;
case PROPERTY_DLIST_HEAD_PREV:
priv->head->prev = g_value_get_pointer (value);
break;
case PROPERTY_DLIST_HEAD_NEXT:
priv->head->next = g_value_get_pointer (value);
break;
case PROPERTY_DLIST_TAIL:
priv->tail = g_value_get_pointer (value);
break;
case PROPERTY_DLIST_TAIL_PREV:
priv->tail->prev = g_value_get_pointer (value);
break;
case PROPERTY_DLIST_TAIL_NEXT:
priv->tail->next = g_value_get_pointer (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}

static void
pm_dlist_get_property (GObject *object, guint property_id,
GValue *value, GParamSpec *pspec)
{
PMDList *self = PM_DLIST (object);
PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);

switch (property_id) {
case PROPERTY_DLIST_HEAD:
g_value_set_pointer (value, priv->head);
break;
case PROPERTY_DLIST_HEAD_PREV:
g_value_set_pointer (value, priv->head->prev);
break;
case PROPERTY_DLIST_HEAD_NEXT:
g_value_set_pointer (value, priv->head->next);
break;
case PROPERTY_DLIST_TAIL:
g_value_set_pointer (value, priv->tail);
break;
case PROPERTY_DLIST_TAIL_PREV:
g_value_set_pointer (value, priv->tail->prev);
break;
case PROPERTY_DLIST_TAIL_NEXT:
g_value_set_pointer (value, priv->tail->next);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}

哇,代码很多!但是请不要恐惧,因为所有 GObject 子类属性的 set 与 get 函数的实现,思路上均与上述代码相似。要理解这些代码,只需注意以下几点:

PM_DLIST (object) 宏的作用是将一个基类指针类型转换为 PMDList 类的指针类型,它需要 GObject 子类的设计者提供,我们可以将其定义为:

1
2
#define PM_DLIST(object) \
G_TYPE_CHECK_INSTANCE_CAST ((object), PM_TYPE_DLIST, PMDList))

PROPERTY_DLIST_XXXX 宏,可以采用枚举类型实现。
GValue 类型是一个变量容器,可用于存放各种变量的值,例如整型数、指针、GObject 子类等等,上述代码主要用 GValue 存放指针变量的值。
GParamSpec 类型是比 GValue 高级一点的变量容器,它不仅可以存放各种变量的值,还能为这些值命名,因此它比较适合用于表示 g_object_new 函数的“属性名-属性值”结构。不过,在上述代码中,GParamSpec 类型只是昙花一现,没关系,反正下文它还会出现。
在理解了上述代码之后,我们继续前进,迈入 PMDList 类的类结构体初始化函数,首先要覆盖 GObject 类的两个函数指针:

1
2
3
4
5
6
7
8
9
10
11
12
static void
pm_dlist_class_init (PMDListClass *klass)
{
/* 对象私有属性的安装,详见文档 [3] */
g_type_class_add_private (klass, sizeof (PMDListPrivate));


GObjectClass *base_class = G_OBJECT_CLASS (klass);
base_class->set_property = pm_dlist_set_property;
base_class->get_property = pm_dlist_get_property;

/* 未完,下文待续 */

set_property 和 get_property 是两个函数指针,它们位于 GObject 类的类结构体中。如果你看过文档 [2],也许你还记得 GObject 库中,类是由实例结构体与类结构体构成的。对象的属性,应当存储在实例结构体中,而所有对象共享的数据,应当存储于类结构体中。因此,set_property 和 get_property 是两个函数指针可以被 GObject 类及其子类的所有对象共享,并且各个对象都可以让这两个函数指针指向它所期望的函数。

类似的机制,在 C++ 中被称为“虚函数”,主要用于实现多态。不过,即便你不知道虚函数与多态是什么东西,这也无所谓,你只需要知道 PMDList 类从它的父类——GObject 类中继承了 2 个函数指针,在 PMDList 类的类结构体初始化函数中,将这 2 个函数指针指向了前文中定义的 pm_dlist_set_property 与 pm_dlist_get_property 函数,这些就足够了。

接下来,就是向 PMDList 类安装属性,紧接上面的代码:

1
2
3
4
5
6
7
8
/* 接前文尚未完成的 pm_dlist_class_init 函数 */
GParamSpec *pspec;
pspec = g_param_spec_pointer ("head",
"Head node",
"The head node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD, pspec);
/* 未完,下文待续 */

在 pm_dlist_set_property 与 pm_dlist_get_property 函数中昙花一现的 GParamSpec 类型终于又出现了。我知道,它看起来似乎很恐怖,但是它所作的事情却很简单,就是对一个键值对打包成一个数据结构,然后将之安装到相应的 GObject 子类中。

g_param_spec_pointer 函数,可以将“属性名:属性值”参数打包为 GParamSpec 类型的变量,该函数的第一个参数用于设定键名,第二个参数是键名的昵称,第三个参数是对这个键值对的详细描述,第四个参数用于表示键值的访问权限,G_PARAM_READABLE | G_PARAM_WRITABLE 是指定属性即可读又可写,G_PARAM_CONSTRUCT 是设定属性可以在对象示例化之时被设置。

g_object_class_install_property 函数用于将 GParamSpec 类型变量所包含的数据插入到 GObject 子类中,其中的细节可以忽略,只需要知道该函数的第一个参数为 GObject 子类的类结构体,第二个参数是 GParamSpec 对应的属性 ID。GObject 子类的属性 ID 在前文已经提及,它是 GObject 子类设计者定义的宏或枚举类型。第三个参数是要安装值向 GObject 子类中的 GParamSpec 类型的变量指针。

但是,一定要注意,g_object_class_install_property 函数的第二个参数值不能为 0。在使用枚举类型来定义 ID 时,为了避免 0 的使用,一个比较笨的技巧就是像下面这样设计一个枚举类型:

1
2
3
4
5
6
7
8
9
enum PropertyDList {
PROPERTY_DLIST_0,
PROPERTY_DLIST_HEAD,
PROPERTY_DLIST_HEAD_PREV,
PROPERTY_DLIST_HEAD_NEXT,
PROPERTY_DLIST_TAIL,
PROPERTY_DLIST_TAIL_PREV,
PROPERTY_DLIST_TAIL_NEXT
};

其中的 PROPERTY_DLIST_0,只是占位符,它不被使用。

按照上面的属性的安装方式,我们可以陆续写处其它属性的安装代码,即 pm_dlist_class_init 函数的剩余部分:

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
/* 接前文尚未完成的 pm_dlist_class_init 函数 */
pspec = g_param_spec_pointer ("head-prev",
"The previous node of the head node",
"The previous node of the head node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE);
g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD_PREV, pspec);
pspec = g_param_spec_pointer ("head-next",
"The next node of the head node",
"The next node of the head node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE);
g_object_class_install_property (base_class, PROPERTY_DLIST_HEAD_NEXT, pspec);
pspec = g_param_spec_pointer ("tail",
"Tail node",
"The tail node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL, pspec);
pspec = g_param_spec_pointer ("tail-prev",
"The previous node of the tail node",
"The previous node of the tail node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE);
g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL_PREV, pspec);
pspec = g_param_spec_pointer ("tail-next",
"The next node of the tail node",
"The next node of the tail node of the double list",
G_PARAM_READABLE | G_PARAM_WRITABLE);
g_object_class_install_property (base_class, PROPERTY_DLIST_TAIL_NEXT, pspec);
}

这些代码又冗余又无趣,但是并不难理解。

将简洁留给外部

对于上一节所实现的 PMDList 类,可以采用下面的代码在对象实例化时便进行属性的初始化,即将链表的首结点和尾节点指针设为 NULL:

1
2
3
4
PMDList *list = g_object_new (PM_TYPE_DLIST,
"head", NULL,
"tail", NULL,
NULL); /* 要记得键值对参数之后,要以 NULL 收尾 */

也可以调用 g_object_get_property 函数获取 PMDList 类的实例属性,例如获取链表 list 的首结点指针:

1
2
3
4
5
GValue val = { 0, };

g_value_init(val,G_TYPE_POINTER);
g_object_get_property(G_OBJECT(list),"head",val);
g_value_unset (val);

也可以调用 g_object_set_property 函数设置 PMDList 类的实例属性,例如将链表 list1 的尾结点指针所指向的结点地址赋给链表 list2 的首结点的前驱结点指针:

1
2
3
4
5
6
GValue val = {0};

g_value_init (val,G_TYPE_POINTER);
g_object_get_property (G_OBJECT(list1), "tail", val);
g_object_set_property (G_OBJECT(list2), "head-prev", val);
g_value_unset (val);

如果我们要解决本文开始时的那个 list1 与 list2 链接的问题,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GValue list1_tail = {0};
GValue list2_head = {0};

g_value_init (&list1_tail, G_TYPE_POINTER);
g_value_init (&list2_head, G_TYPE_POINTER);

g_object_get_property (G_OBJECT(list1), "tail", &list1_tail);
g_object_set_property (G_OBJECT(list2), "head-prev", &list1_tail);


g_object_get_property (G_OBJECT(list2), "head", &list2_head);
g_object_set_property (G_OBJECT(list1), "tail-next", &list2_head);

g_value_unset (&list2_head);
g_value_unset (&list1_tail);

看上去还不错。当然,前提是你需要了解一下 GValue 容器的用法,并且上述代码已经展示了它的基本用法,但是最好还是阅读文档 [5, 6]。

上述代码中使用的 g_object_set_property 与 g_object_get_property 函数,看上去很无趣,每次只能设置或获取一个属性值,并且还要借助 GValue 容器,事实上它们是为那些基于 GObject 库的语言绑定使用的。对于在 C 程序中直接使用 GObject 库的用户,可以使用 g_object_set 和 g_object_get 函数一次进行多个属性的设置与获取,它们的用法与 g_object_new 相似,可以处理以 NULL 结尾的“属性名-属性值”参数序列。

将一切放在一起

本文示例源码可从 http://garfileo.is-programmer.com/user_files/garfileo/File/test/pm-dlist.tar.gz 下载。

~End~

原文出处:http://garfileo.is-programmer.com

[转载]GObject 学习笔记:GObject 子类对象的私有属性模拟

上一篇文章“使用 GObject 库模拟类的数据封装形式”讲述了 GObject 子类化过程,本文以其为基础,进一步讲述如何对数据进行隐藏,即对面向对象程序设计中的“私有属性”概念进行模拟。

非类类型数据的隐藏

第一个问题,可以称之为非类类型数据结构的隐藏,因为 PMDListNode 是普通的 C 结构体。隐藏这个结构体成员的方法有多种。

第一种方法尤为简单,如下:

1
2
3
4
5
6
7
8
typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
/* private */
PMDListNode *prev;
PMDListNode *next;
/* public */
void *data;
};

只需要向结构体中添加一条注释,用于标明哪些成员是私有的,哪些是可以被直接访问的。

也许你会认为这是开玩笑呢吧!但,这是最符合 C 语言设计理念的做法。C 语言认为,程序员应当知道自己正在干什么,而且保证自己的所作所为是正确的。

倘若你真的这么认为这是在开玩笑,那也没什么。我们还可以使用第二种隐藏的方法,即在 pm-dlist.h 文件中保留下面代码:

1
typedef struct _PMDListNode PMDListNode;

并将以下代码:

1
2
3
4
5
struct  _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
void *data;
};

转移到 pm-dlist.c 文件中。

这下隐藏的非常彻底。当然,这并不能防止用户打开 pm-dlist.c 文件查看 PMDListNode 的定义。不过,我们是自由软件,不怕你看。

如果想半遮半掩,稍微麻烦一些。可以在 pm-dlist.h 中写入以下代码:

1
2
3
4
5
6
typedef struct _PMDListPriv PMDListPriv;
typedef struct _PMDListNode PmdListNode;
struct _PMDListNode {
PMDListPriv priv;
void *data;
};

然后,将 PMDListPriv 的定义放在 pm-dlist.c 文件中,如下:

1
2
3
4
struct _PMDListPriv {
PMDListNode *prev;
PMDListNode *next;
};

GObject 子类对象的属性隐藏

GObject 子类对象的属性即继承 GObject 类的类的实例结构体所包含的属性,这句话说起来还真拗口。

考虑一下如何隐藏 PMDList 类的实例结构体中的成员。先回忆一下这个结构体的定义:

1
2
3
4
5
6
typedef struct _PMDList PMDList;
struct _PMDList {
GObject parent_instance;
PMDListNode *head;
PMDListNode *tail;
};

我们希望 head 与 tail 指针不容他人窥视,虽然可以使用上一节的方式进行数据隐藏,但是 GObject 库为 GObject 子类提供了一种私有结构体的机制,基于它也可以实现数据隐藏,而且更像是隐藏。

首先,我们将 pm-dlist.h 中 PMDList 结构体的定义修改为:

1
2
3
4
typedef struct _PMDList PMDList;
struct _PMDList {
GObject parent_instance;
};

然后,在 pm-dlist.c 文件中,定义一个结构体:

1
2
3
4
5
typedef struct _PMDListPrivate PMDListPrivate;
struct _PMDListPrivate {
PMDListNode *head;
PMDListNode *tail;
};

再在 dm-dlist.c 中定义一个宏:

1
2
#define PM_DLIST_GET_PRIVATE(obj) (\
G_TYPE_INSTANCE_GET_PRIVATE ((obj), PM_TYPE_DLIST, PMDListPrivate))

这个宏可以帮助我们从对象中获取所隐藏的私有属性。例如,在 PMDList 类的实例结构体初始化函数中,使用 PM_DLIST_GET_PRIVATE 宏获取 PMDList 对象的 head 与 tail 指针,如下:

1
2
3
4
5
6
7
8
static void
pm_dlist_init (PMDList *self)
{
PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);

priv->head = NULL;
priv->tail = NULL;
}

但是,那个 PMDListPrivate 结构体是怎样被添加到 PMDList 对象中的呢?答案存在于 PMDList 类的类结构体初始化函数之中,如下:

1
2
3
4
5
static void
pm_dlist_class_init (PMDListClass *class)
{
g_type_class_add_private (klass, sizeof (PMDListPrivate));
}

由 于 pm_dlist_class_init 函数会先于 pm_dlist_init 函数被 g_object_new 函数调用,GObject 库的类型管理系统可以从 pm_dlist_class_init 函数中获知 PMDListPrivate 结构体所占用的存储空间,从而 g_object_new 函数在为 PMDList 对象的实例分配存储空间时,便会多分出一部分以容纳 PMDListPrivate 结构体,这样便相当于将一个 PMDListPrivate 结构体挂到 PMDList 对象之中。

GObject 库对私有属性所占用的存储空间是由限制的。一个 GObject 子类对象,它的私有属性及其父类对象的私有属性累加起来不能超过 64 KB。
将一切放到一起

将本文的示例代码综合到一起,便可以得到数据隐藏方法的全貌。

完整的 pm-dlist.h 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef PM_DLIST_H
#define PM_DLIST_H

#include <glib-object.h>

#define PM_TYPE_DLIST (pm_dlist_get_type ())

typedef struct _PMDList PMDList;
struct _PMDList {
GObject parent_instance;
};

typedef struct _PMDListClass PMDListClass;
struct _PMDListClass {
GObjectClass parent_class;
};

GType pm_dlist_get_type (void);

#endif

完整的 pm-dlist.c 文件,内容如下:

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
#include "pm-dlist.h"

G_DEFINE_TYPE (PMDList, pm_dlist, G_TYPE_OBJECT);
#define PM_DLIST_GET_PRIVATE(obj) (\
G_TYPE_INSTANCE_GET_PRIVATE ((obj), PM_TYPE_DLIST, PMDListPrivate))

typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
void *data;
};

typedef struct _PMDListPrivate PMDListPrivate;
struct _PMDListPrivate {
PMDListNode *head;
PMDListNode *tail;
};

static void
pm_dlist_class_init (PMDListClass *klass)
{
g_type_class_add_private (klass, sizeof (PMDListPrivate));
}

static void
pm_dlist_init (PMDList *self)
{
PMDListPrivate *priv = PM_DLIST_GET_PRIVATE (self);

priv->head = NULL;
priv->tail = NULL;
}

测试源代码文件 main.c 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "pm-dlist.h"

int
main (void)
{
/* GObject 库的类型管理系统的初始化 */
g_type_init ();

PMDList *list;

list = g_object_new (PM_TYPE_DLIST, NULL);
g_object_unref (list);

return 0;
}

总结

从上述各源文件来看,经过数据隐藏处理之后,pm-dlist.h 被简化,pm-dlist.c 变得更复杂。

事实上,PMDList 类实现完毕后,第三方(即类的使用者)如果要使用这个类,那么他面对的只是 pm-dlist.h 文件,因此一个简洁的 pm-dlist.h 是他最希望看到的。

pm-dlist.c 变得更加复杂,但是这并不是坏事情。因为我们已经尽力将细节隐藏在类的实现部分,而且这部分代码通常也是第三方并不关注的。

这就是数据隐藏的意义。

原文出处:http://garfileo.is-programmer.com

[转载]GObject 学习笔记:使用 GObject 库模拟类的数据封装形式

事实上,有关 GObject 库的学习与使用,GObject 库参考手册提供了一份简短且过于晦涩的指南。如果你能够理解它,那么完全可以无视这篇以及后续的几篇文章。倘若没有明白那份指南,那么建议最好能克制一下,先不要急于去做文档 [1] 中所列举那些探索,谨记 Knuth 所说的,过早优化是诸恶之源。

这篇文档主要讲述如何使用 GObject 库来模拟面向对象程序设计的最基本的要素,即基于类的数据封装,所采用的具体示例是一个双向链表的设计。

从一个双向链表的数据结构开始

对于双向链表这种数据结构,即便是 C 语言的初学者也不难创建一个名为 double-list.h 的头文件并写出以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* file name: double-list.h */

#ifndef DOUBLE_LIST_H
#define DOUBLE_LIST_H

struct double_list_node {
struct doule_list_node *prev;
struct double_list_node *next;
void *data;
};

struct double_list {
struct double_list_node *head;
struct double_list_node *tail;
};

#endif

较为熟悉 C 语言的人自然不屑于写出上面那种新手级别的代码,他们通常使用 typedef 关键字去定义数据类型,并且将 double_list 简写为 dlist,以避免重复的去写 “struct double_list_xxxxx” 这样的代码,此外还使用了 Pascal 命名惯例,即单词首字母大写[2],具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* file name: dlist.h(版本 2)*/

#ifndef DLIST_H
#define DLIST_H

typedef struct _DListNode DListNode;
struct _DListNode {
DListNode *prev;
DListNode *next;
void *data;
};

typedef struct _DList DList;
struct _DList {
DListNode *head;
DListNode *tail;
};

#endif

现在,代码看上去稍微专业了一点。

但是,由于 C 语言没有为数据类型提供自定义命名空间的功能,程序中所有的数据类型(包括函数)均处于同一个命名空间,这样数据类型便存在因为同名而撞车的可能性。为了避免这一问题,更专业一点的程序员会为数据类型名称添加一些前缀,并且通常会选择项目名称的缩写。我们可以为这种命名方式取一个名字,叫做 PT 格式,P 是项目名称缩写,T 是数据类型的名称。例如,对于一个多面体建模(Polyhedron Modeling)的项目,如果要为这个项目定义一个双向链表的数据类型,通常是先将 dlist.h 文件名修改为 pm-dlist.h,将其内容改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* file name: pm-dlist.h*/

#ifndef PM_DLIST_H
#define PM_DLIST_H

typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
void *data;
};

typedef struct _PMDList PMDList;
struct _PMDList {
PMDListNode *head;
PMDListNode *tail;
};

#endif

在以上一波三折的过程中,我们所做的工作就是仅仅是定义了两个结构体而已,一个是双向链表结点的结构体,另一个是双向链表的结构体,并且这两个结构体中分别包含了一组指针成员。这样的工作,用面向对象编程方法中的术语,叫做”数据封装“。借用《C++ Primer》中的定义,所谓数据封装,就是一种将低层次的元素组合起来,形成新的、高层次实体的技术。对于上述代码而言,指针类型的变量属于低层次的元素,而它们所构成的结构体,则是高层次的实体。在面向对象程序设计中,类是数据封装形式中的一等公民。

接下来,我们更进一步,使用 GObject 库模拟类的数据封装形式,如下:

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
/* file name: pm-dlist.h*/

#ifndef PM_DLIST_H
#define PM_DLIST_H

#include <glib-object.h>

typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
void *data;
};

typedef struct _PMDList PMDList;
struct _PMDList {
GObject parent_instance;
PMDListNode *head;
PMDListNode *tail;
};

typedef struct _PMDListClass PMDListClass;
struct _PMDListClass {
GObjectClass parent_class;
};

#endif

上述代码与 dlist.h 版本 2 中的代码相比,除去空行,多出 6 行代码,它们的作用是实现一个双向链表类。也许你会感觉这样很滑稽,特别当你特别熟悉 C++、Java、C# 之类的语言之时。

但是,上述代码的确构成了一个类。在 GObject 世界里,类是两个结构体的组合,一个是实例结构体,另一个是类结构体。例如,PMDList 是实例结构体,PMDListClass 是类结构体,它们合起来便可以称为 PMDList 类(此处的“PMDList 类”只是一个称谓,并非是指 PMDList 实例结构体。下文将要谈及的“GObject 类”的理解与此类似)。

也许你会注意到,PMDList 类的实例结构体的第一个成员是 GObject 结构体,PMDList 类的类结构体的第一个成员是 GObjectClass 结构体。其实,GObject 结构体与 GObjectClass 结构体分别是 GObject 类的实例结构体与类结构体,当它们分别作为 PMDList 类的实例结构体与类结构体的第一个成员时,这意味着 PMDList 类继承自 GObject 类。

也许你并不明白 GObject 为什么要将类拆解为实例结构体与类结构体,也不明白为什么将某个类的实例结构体与类结构体,分别置于另一个类的实例结构体与类结构体的成员之首便可实现类的继承,这些其实并不重要,至少是刚接触 GObject 时它们并不重要,应当像对待一个数学公式那样将它们记住。以后每当需要使用 C 语言来模拟类的封装形式时,只需要构建一个基类是 GObject 类的类即可。就像我们初学 C++ 那样,对于下面的代码,

1
2
3
4
5
6
7
8
9
10
11
12
class PMDListNode {
public:
PMDListNode *prev;
PMDListNode *next;
void *data;
}

class PMDList : public GObject {
public:
PMDListNode *head;
PMDListNode *tail;
};

也许除了 C++ 编译器的开发者之外,没人会对为什么使用“class”关键字便可以将一个数据结构变成类,为什么使用一个冒号便可以实现 DList 类继承自 GObject 类,为什么使用 public 便可以将 DList 类的 head 与 tail 属性对外开放之类的问题感兴趣。

继承 GObject 类的好处

为什么 DList 类一定要将 GObject 类作为父类?主要是因为 GObject 类具有以下功能:

基于引用计数的内存管理
对象的构造函数与析构函数
可设置对象属性的 set/get 函数
易于使用的信号机制

现在即使并不明白上述功能的意义也无关紧要,这给予了我们对其各个击破的机会。

虽然,我们尚且不是很清楚继承 GObject 类的好处,但是不继承 GObject 类的坏处是显而易见的。在 cloverprince 所写的 11 篇文章[3]中,从第 2 篇到第 5 篇均展示了不使用 GObject 的坏处,虽然它们对理解 GObject 类的实现原理有很大帮助,但是对于 GObject 库的使用并无太多助益。

概念游戏

我承认,我到现在也不是很明白面向对象程序设计中的”类“、”对象“和”实例“这三者的关系。不过看到还有人同我一样没有搞明白[4],心里便略微有些安慰,甚至觉得 C 语言不内建支持面向对象程序设计是一件值得庆幸的事情。暂时就按照文档[4]那样理解吧,对于上面所设计的 PMDList 类,可以用下面的代码模拟类的实例化与对象的实例化。

1
2
PMDList *list; /* 类的实例化 */
list = g_object_new (PM_TYPE_DLIST, NULL); /* 对象的实例化 */

也就是说,对于 PMDList 类,它的实例是一个对象,例如上述代码中的 list,而对象的实例化则是让这个对象成为计算机内存中的实体。

也许,对象的实例化比较令人费解,幸好,C 语言的表示会更加直观,例如:

1
2
3
4
5
6
PMDList *dlist; /* 类的实例化,产生对象 */

dlist = g_object_new (PM_TYPE_DLIST, NULL); /* 创建对象的一个实例 */
g_object_unref (dlist); /* 销毁对象的一个实例 */

dlist = g_object_new (PM_TYPE_DLIST, NULL); /* 再创建对象的一个实例 */

这里需要暂停,请回溯到上一节所说的让一个类继承 GObject 类的好处的前两条,即 GObject 类提供的“基于引用计数的内存管理”与“对象的构造函数与析构函数”,此时体现于上例中的 g_object_new 与 g_object_unref 函数。

g_object_new 用于构建对象的实例,虽然其内部实现非常繁琐和复杂,但这是 GObject 库的开发者所要考虑的事情。我们只需要知道 g_object_new 可以为对象的实例分配内存与初始化,并且将实例的引用计数设为 1。

g_object_unref 用于将对象的实例的引用计数减 1,并检测对象的实例的引用计数是否为 0,若为 0,那么便释放对象的实例的存储空间。

现在,再回到上述代码的真正要表述的概念,那就是:类的实例是对象,每个对象皆有可能存在多个实例。

下面,继续概念游戏,看下面的代码:

1
PMDList *list = g_object_new (PM_TYPE_DLIST, NULL);

这是类的实例化还是对象的实例化?自然是类的实例化的实例化,感觉有点恶心,事实上这只不过是为某个数据类型动态分配了内存空间,然后用指针指向它!我们最好是不再对“对象”和“实例”进行区分,对象是实例,实例也是对象,只需要知道它们所指代的事物在同一时刻是相同的,除非有了量子计算机。

PM_TYPE_DLIST 引发的血案

上一节所使用的 g_object_new 函数的参数是 PM_TYPE_DLIST 和 NULL。对于 NULL,虽然我们不知道它的真实意义,但是至少知道它表示一个空指针,而那个 PM_TYPE_DLIST 是什么?

PM_TYPE_DLIST 是一个宏,需要在 pm-dlist.h 头文件中进行定义,于是最新版的 pm-dlist.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
#ifndef PM_DLIST_H
#define PM_DLIST_H

#include <glib-object.h>

#define PM_TYPE_DLIST (pm_dlist_get_type ())

typedef struct _PMDListNode PMDListNode;
struct _PMDListNode {
PMDListNode *prev;
PMDListNode *next;
void *data;
};

typedef struct _PMDList PMDList;
struct _PMDList {
GObject parent_instance;
PMDListNode *head;
PMDListNode *tail;
};

typedef struct _PMDListClass PMDListClass;
struct _PMDListClass {
GObjectClass parent_class;
};

GType pm_dlist_get_type (void);

#endif

嗯,与之前的 pm-dlist.h 相比,现在又多了两行代码

1
2
#define PM_TYPE_DLIST (pm_dlist_get_type ())
GType pm_dlist_get_type (void);

显然,PM_TYPE_DLIST 这个宏是用来替代函数 pm_dlist_get_type 的,该函数的返回值是 GType 类型。

我们将 PM_TYPE_DLIST 宏作为 g_object_new 函数第一个参数,这就意味着向 g_object_new 函数传递了一个看上去像是在获取数据类型的函数。不需要猜测,也不需要去阅读 g_object_new 函数的源代码,g_object_new 之所以能够为我们进行对象的实例化,那么它必然要知道对象对应的类的数据结构,pm_dlist_get_type 函数的作用就是告诉它有关 PMDList 类的具体结构。

现在既然知道了 PM_TYPE_DLIST 及其代表的函数 pm_dlist_get_type,那么 pm_dlist_get_type 具体该如何实现?很简单,只需要再建立一个 pm-dlist.c 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "pm-dlist.h"

G_DEFINE_TYPE (PMDList, pm_dlist, G_TYPE_OBJECT);

static
void pm_dlist_init (PMDList *self)
{
g_printf ("\t实例结构体初始化!\n");

self->head = NULL;
self->tail = NULL;
}

static
void pm_dlist_class_init (PMDListClass *klass)
{
g_printf ("类结构体初始化!\n");
}

这样,在源代码文件的层次上,pm-dlist.h 文件存放这 PMDList 类的声明,pm-dlist.c 文件存放的是 PMDList 类的具体实现。

在上述的 pm-dlist.c 中,我们并没有看到 pm_dlist_get_type 函数的具体实现,这是因为 GObject 库所提供的 G_DEFINE_TYPE 宏可以为我们生成 pm_dlist_get_type 函数的实现代码。

G_DEFINE_TYPE 宏,顾名思义,它可以帮助我们最终实现类类型的定义。对于上例,

1
G_DEFINE_TYPE (PMDList, pm_dlist, G_TYPE_OBJECT);

G_DEFINE_TYPE 可以让 GObject 库的数据类型系统能够识别我们所定义的 PMDList 类类型,它接受三个参数,第一个参数是类名,即 PMDList;第二个参数则是类的成员函数(面向对象术语称之为”方法“或”行为“)名称的前缀,例如 pm_dlist_get_type 函数即为 PMDList 类的一个成员函数,”pm_dlist” 是它的前缀;第三个参数则指明 PMDList 类类型的父类型为 G_TYPE_OBJECT……嗯,这又是一个该死的宏!

也许你会注意到,PMDList 类类型的父类型 G_TYPE_OBJECT 与前面所定义的宏 PM_TYPE_DLIST 非常相像。的确是这样,G_TYPE_OBJECT 指代 g_object_get_type 函数。

为了便于描述,我们可以将 PMDList 类和 GObject 类这种形式的类类型统称为 PT 类类型,将 pm_dlist_get_type 和 g_object_get_type 这种形式的函数统称为 p_t_get_type 函数,并将 PM_TYPE_DLIST 和 G_TYPE_OBJECT 这样的宏统称为 P_TYPE_T 宏。当然,这种格式的函数名与宏名,只是一种约定。

若想让 GObject 库能够识别你所定义的数据类型,那么必须要提供一个 p_t_get_type 这样的函数。虽然你不见得非要使用 p_t_get_type 这样的函数命名形式,但是必须提供一个具备同样功能的函数。p_t_get_type 函数的作用是向 GObject 库所提供的类型管理系统提供要注册的 PT 类类型的相关信息,其中包含 PT 类类型的实例结构体初始化函数 p_t_init 与类结构体初始化函数 p_t_class_init,例如上例中的 pm_list_init 与 pm_list_class_init。

因为 p_t_get_type 函数是 g_object_new 函数的参数,当我们首次调用 g_object_new 函数进行对象实例化时,p_t_get_type 函数便会被 g_object_new 函数调用,从而引发 GObject 库的类型管理系统去接受 PT 类类型(例如 PMDList 类型)的申请并为其分配一个类型标识码作为 p_t_get_type 函数的返回值。当 g_object_new 函数从 p_t_get_type 函数那里获取 PT 类类型标识码之后,便可以进行对象实例的内存分配及属性的初始化。

将一切放在一起

这篇文章居然出乎意料的长,原本没打算涉及任何细节,结果有些忍不住。不过,倘若你顺利的阅读到这里,便已经掌握了如何使用 GObject 库在 C 语言程序中模拟面向对象程序设计中基于类的数据封装,并且我们完成了一个双向链表类 PMDList 的数据封装。

为了验证 PMDList 类是否可用,可以再建立一份 main.c 文件进行测试,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "pm-dlist.h"

int
main (void)
{
/* GObject 库的类型管理系统的初始化 */
g_type_init ();

int i;
PMDList *list;

/* 进行三次对象实例化 */
for (i = 0; i < 3; i++){
list = g_object_new (PM_TYPE_DLIST, NULL);
g_object_unref (list);
}

/* 检查实例是否为 GObject 对象 */
list = g_object_new (PM_TYPE_DLIST, NULL);
if (G_IS_OBJECT (list))
g_printf ("\t这个实例是一个 GObject 对象!\n");

return 0;
}

编译上述测试程序的命令为:
$ gcc $(pkg-config –cflags –libs gobject-2.0) pmd-dlist.c main.c -o test

测试程序的运行结果如下:

1
2
3
4
5
6
7
$ ./test  
类结构体初始化!
实例结构体初始化!
实例结构体初始化!
实例结构体初始化!
实例结构体初始化!
这个实例是一个 GObject 对象!

从输出结果可以看出,PMDList 类的类结构体初始化函数只被调用了一次,而实例结构体的初始化函数的调用次数等于对象实例化的次数。这意味着,所有实例共享的数据,可保存在类结构体中,而所有对象私有的数据,则保存在实例结构体中。

上述的 main 函数中,在使用 GObject 库的任何功能之前,必须先调用 g_type_init 函数初始化 GObject 库的类型管理系统,否则程序可能会出错。

main 函数中还使用了 G_IS_OBJECT 宏,来检测 list 对象是否为 G_TYPE_OBJECT 类型的对象:
G_IS_OBJECT (list)

因为 PMDList 类继承自 GObject 类,那么一个 PMDList 对象自然是一个 G_TYPE_OBJECT 类型的对象。

总结

也许我讲的很明白,也许我一点都没有讲明白。但是使用 GObject 库模拟基于类的数据封装,或者用专业术语来说,即 GObject 类类型的子类化,念起来比较拗口,便干脆简称 GObject 子类化,其过程只需要以下四步:

在 .h 文件中包含 glib-object.h;
在 .h 文件中构建实例结构体与类结构体,并分别将 GObject 类的实例结构体与类结构体置于成员之首;
在 .h 文件中定义 P_TYPE_T 宏,并声明 p_t_get_type 函数;
在 .c 文件中调用 G_DEFINE_TYPE 宏产生类型注册代码。

也许让我们感到晕眩的,是那些命名约定。但是,从工程的角度来说,GObject 库所使用的命名风格是合理的,值得 C 程序员借鉴。

~ End ~

原文出处:http://garfileo.is-programmer.com