-
1、 概述
1.1、Glib是什么?
gnome是基于gtk+开发的一套桌面环境,gnome和KDE作为两大最流行的桌面环境,在全世界广泛使用。只要是在Linux下工作的开发人员,对于gtk+一定不陌生。而对于glib,这个gtk+下的无名英雄,其功能强大却鲜为人知。今天,在这里简要介绍一下,如果你是开发人员,看完本文,相信你会爱上它的。
glib不是gllibc,尽管两者都是基于(L)GPL的开源软件。但这一字之差却误之千里,glibc是GNU实现的一套标准C的库函数,而glib是gtk+的一套函数库。在linux平台上,像其它任何软件一样,glib依赖于glibc。
glib不是一个学院派的东西,也不是凭空想出来的,完全是在开发gtk+的过程中,慢慢总结和完善的结果。如果你是一个工作3年以上的C语言程序员,现在让你讲讲写程序的苦恼,你可能有很多话要说,但如果你有时间研究一下glib,你会发现,很多苦恼已不再成其为苦恼,glib里很多东西正是你期望已经久的。
gobject是glib的精粹,glib是用C实现的,但在很大程序是基于面向对象思想设计的,gobject是所有类的基类。signal在其中也是一大特色,signal与操作系统中的signal并不一样,它是类似消息一样的东西,让消息在各个对象间传递,但尽量降低对象间的耦合。仔细读一下它的代码,唯一想说的话就是“绝!”。1.2、GLib 的范畴
首先研究 GLib 的范畴。
GLib 是一个提供了很多实用定义和函数的底层程序库,包括基本类型及其限定的定义、标准宏、类型转化、字节次序、内存分配、警告与断言、 消息日志、计时器、字符工具、钩子函数、词法扫描器、模块的动态加载,以及自动的字符串补齐。
GLib 还定义了很多数据结构(以及它们相关的操作),包括:
内存块(Memory chunks)
双向链表(Doubly-linked lists)
单向链表(Singly-linked lists)
散列表(Hash tables)
字符串(Strings,可以动态增长)
字符块(String chunks,成组的字符串)
数组(Arrays,当增加元素时其大小能够增长)
平衡二叉树(Balanced binary trees)
N-叉树(N-ary trees)
Quarks(字符串和唯一整型标识符的双向关联)
有关键字的数据列表(Keyed data lists,通过字符串或者整型 id 访问其数据元素的列表)
关系(Relations)和元组(tuples)(可以由任意数目的域进行索引的数据表)
缓存1.3、管理数据
每个程序都必须管理数据编写程序是为了处理数据。程序可能会从文件中读出一个名字列表、通过一个图形用户界面向用户询问某些数据,或者从一个外部的硬件设备加载数据。 但数据一旦到了程序中,就需要保持对它的追踪。用来管理数据的函数和变量称为 数据结构 或 容器。
如果使用 C 编写代码,那么您会发现它极其缺乏复杂的数据结构。当然,有很多存储数据的简单方法:
基本类型 —— int、float、char 等等。
枚举(enum),可以保存整数的一系列符号名称。
数组(array),这是 C 中最灵活的数据结构。数组可以保存基本类型的数据,或者一系列任意类型的数据,或者指向任意类型数据的指针。
但是数组也有很多局限性。它们的大小不能改变,所以,如果为一个拥有十个元素的数组分配了内存,却发现需要向其中放置十一个元素, 那么就得创建一个新数组,将旧的元素拷贝过来,然后添加并新的元素。如果要遍历数组中的每一个元素,那么,或者需要始终知道数组中有 多少个元素,或者要确保在数组的末尾处有某种“数组结束”标记,以使得您能够知道何时停止。通过使用链表和二叉树等标准容器,在 C 中保持对数据的追踪的问题已经多次得到解决。每一位刚开始学习计算机科学专业的学生都会使用一个 数据结构类;教师肯定会布置一系列编写那些容器的实现的练习。在编写这些数据结构时,学生会意识到它们是多么难以处理;粗心的学生经常会 犯诸如悬空指针(dangling pointer)和重复 释放内存(free) 的错误。
编写单元测试会有很大帮助,但是,总的来说,为每个新程序重新编写相同的数据结构是一个费力不讨好的任务。
1.4、内置的数据结构
简言之就是,在什么地方内置数据结构会有所帮助。有一些语言会内置附带这些容器。C++ 包含标准模板库(Standard Template Library,STL), 它有一个容器类工具集,比如列表、优先队列、集合(set)和映射。另外,这些容器是 类型-安全(type-safe) 的,也就是说,您只能 向创建的每一个容器对象中放入一种类型的元素。这就使得它们的使用更为安全,并避免了 C 所要求的很多冗长的强制类型转换。并且,STL 包含了 很多遍历工具和排序工具,这样就使得容器的使用更为简单。Java 编程环境也附带了一组容器类。The java.util 程序包包含 ArrayList、 HashMap、TreeSet 以及其他各种标准结构。它还包括有用于对数据进行一般排序、创建 不变集合的工具,以及其他各种便利工具。
不过,C 并没有内置容器支持;您或者只能自己编写,或者使用其他人的数据结构程序库。
幸运的是,GLib 是一个能够满足此需要的优秀的、免费的、开放源代码的程序库。它包含了大部分标准数据结构,以及在程序中有效处理数据的很多实用工具。 它诞生于 1996 年,从而经过了彻底地测试,并在发展过程中增加了很多实用功能。
1.5、100 字以内(或者更少)的算法分析
对容器的不同操作需要不同长度的时间。例如,在一个长的列表中,访问第一个元素要快于对同一列表进行排序。用来描述完成这些操作所需时间的 符号称为 O-notation。这个话题需要计算机科学专业的学生用一个学期的时间去研究,但是,简要地讲,O-notation 是对操作的最坏情况的 分析。换句话说,它对完成某个操作所需要的 最长 时间进行度量。它被证明是度量数据结构操作的有效途径,因为经常会遇到最坏情况的操作 (比如当您搜索某个列表而却没有找到所找查找的条目时)。接下来论述了一些 O-notation 示例,可以将一副扑克牌在桌子上排列一行:
O(1) —— 选择第一张牌。 O(1) 也称为“线性时间(constant time)”,因为不管在列表中有多少张牌,取得第一张牌的时间都是相同的。
O(n) —— 翻转每一张牌。O(n) 被称为“线性时间(linear time)”,因为 随着牌的数目增加,完成操作所需时间线性增加。
O(n!) —— 创建一个全部扑克牌所有可能排列的列表。每向列表添加一张新牌,排列的数目 都会阶乘式增加。您会发现,在整个教程都提及关于不同数据结构操作的 O-notation。 了解特定数据结构上特定操作的开销,能够帮助您更明智地选择容器在,最优化应用程序的性能。
1.6、编译 GLib 程序
如果在研究那些示例时编译并运行它们,那么您将通过本文学到更多。由于它们要使用 GLib,所以编译器需要知道 GLib 头文件和程序库在哪里, 以使其能够解析 GLib 定义的类型。这个简单的程序初始化一个双向链表并向其中添加一个字符串://ex-compile.c
#include <glib.h>
int main(int argc, char** argv) {
GList* list = NULL;
list = g_list_append(list, "Hello world!");
printf("The first item is '%s'\n", g_list_first(list)->data);
return 0;
}可以通过调用 GCC 来编译这个程序,如下:
$ gcc -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -lglib-2.0 -o ex-compile ex-compile.c
运行它,查看期望的输出:
$ ./ex-compile
The first item is 'Hello world!'
$不过,那是一个非常难用的 GCC 调用。更简单的方式是让 GCC 指向 GLib 程序库。
使用 pkg-config
手工指定程序库的位置容易出错而且很冗长,所以大部分现代的 Linux 发行版本都附带了 pkgconfig 工具来帮助简化此问题。 可以使用 pkgconfig 来编译上面的程序,如下:$ gcc 'pkg-config --cflags --libs glib-2.0' -o ex-compile ex-compile.c
输出与先前相同:
$ ./ex-compile
The first item is 'Hello world!'
$注意,现在不必再指定 GLib 头文件的路径;pkgconfig 的 --cflags 选项会完成此任务。 使用 --libs 选项指定的程序库也是如此。当然,没有任何神密之处;pkgconfig 只是通过一个配置 文件读取出程序库和头文件的位置。在 Fedora Core 3 系统中,pkgconfig 文件位于 /usr/lib/pkgconfig 目录下, glib-2.0.pc 文件类似如下:
$ cat /usr/lib/pkgconfig/glib-2.0.pc
prefix=/usr
exec_prefix=/usr
libdir=/usr/lib
includedir=/usr/includeglib_genmarshal=glib-genmarshal
gobject_query=gobject-query
glib_mkenums=glib-mkenumsName: GLib
Description: C Utility Library
Version: 2.4.7
Libs: -L${libdir} -lglib-2.0
Cflags: -I${includedir}/glib-2.0 -I${libdir}/glib-2.0/include这样,所有信息都由一个中间层隐藏起来。如果恰巧使用了一个不支持 pkgconfig 的 Linux 发行版本,那么随时可以重新直接为 GCC 指定头文件和程序库。
1.7、谁在用GLib
仅
仅列举出 GLib 容器并展示示例用法可能还不够,所以本教程还包含了 GLib 在一些开放源代码的应用程序中的现实应用:Gaim 是一款流行的实时消息客户机,它在 SourceForge 上的下载次数每个月都会超过二十五次。
GIMP(Graphical Image Manipulation Program)是 GLib 本身的出发点(starting point);它是一款广为应用的图像处理程序,从 1996 年开始 一直得到公众的共同开发。
Evolution 是一款极好的个人信息管理器(PIM),可以追踪电子邮件、联系人、任务和约会。
研究 GLib 在这些流行的应用程序中的应用还会让您有机会了解一些编码习惯;不仅能知道函数的名字是什么,您还能够了解通常如何使用它们。 您将会熟悉将要使用的容器,而且甚至会注意到在某些情况下有人选择了并不最适于某任务的容器。GLib 还拥有很多约定(conventions)及工具宏。在学习本教程的过程中,您将发现使用并解释了其中的很多。不必尝试一开始就将它们完全记住,而只需要 在进行过程中去学习它们,在示例中去了解它们。
2、单向链表
2.1、概念
或
许在 GLib 中最简单的容器就是单向链表;即 GSList。如其名称所示,它是一系列链接到一起的数据条目,可以从一个数据条目导航到 下一个数据条目。它之所以称为单向链表,是因为在条目之间只有一个单向的链接。所以,只能“向前”遍历列表,但不能向前移动后再向后退。进一步讲,每次向列表添加一个条目时,都会创建一个新的 GSList 结构体。这个 GSList 结构体由一个数据条目和一个指针构成。 先前列表的末尾指向这个新的节点,这表示现在这个新节点在列表的末尾。这里的术语可能令人费解,因为整个结构体称为 GSList,而每 个节点也是一个 GSList 结构体。
不过,概念上讲,一个列表只是多个列表的一个序列,其中每个列表有一个条目。这就像是在红灯前的一行汽车; 就算是只有一辆汽车在红灯前等待,它也被认为是一行汽车。
将一长串条目链接起来,随之而来有一些用法。确定列表的长度是一个 O(n) 操作;因为只有统计了每一个条目才能指出列表有多长。 向列表的起始端添加条目很快(一个 O(1) 操作),因为列表的长度不是固定的,超出限制值后不必重新构建。不过,查找某个条目 是一个 O(n) 操作,因为需要对整个列表进行一次线性搜索,直到找到所有寻找的条目。向列表的末尾添加一个条目也是一个 O(n) 操作,因为要想到达末尾,需要从头开始遍历直到列表的末尾。
GSList 可以保存两种基本类型的数据:整型或者指针。不过,这实际上意味着几乎可以向 GSList 中放入任何内容。例如,如果 需要一个“short”数据类型的 GSList,那么只需要在 GSList 中放入一个指向 short 类型数据的指针。
现在理论已经足够了;接下来真正地使用 GSList!
2.2、创建、添加和销毁
下面的代码将初始化一个 GSList,向其添加两个条目,打印出列表的长度,然后释放它://ex-gslist-1.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = NULL;
printf("The list is now %d items long\n", g_slist_length(list));
list = g_slist_append(list, "first");
list = g_slist_append(list, "second");
printf("The list is now %d items long\n", g_slist_length(list));
g_slist_free(list);
return 0;
}
***** Output *****The list is now 0 items long
The list is now 2 items long
关于以上代码的一些注解:
大部分 GLib 函数的格式都是 g_(container name)_(function name)。所以,要得到某个 GSList 的长度, 可以调用 g_slist_length。
没有创建新的 GSList 的函数;而只需要声明一个指向 GSList 结构体的指针并分配一个 NULL 值。
g_slist_append 会返回列表的新起始地址, 所以一定要保存那个返回值。
g_slist_free 不考虑列表中是否存放了条目;快速查看源代码可以发现, 如果 GSList 为 NULL,则 g_slist_free 只是立即返回。 g_slist_length 也可以作用于空的列表;在那种情况下,它只是返回 0。2.3、添加然后删除数据
可以添加数据;可能还会需要将其删除。这里是一个示例://ex-gslist-2.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = NULL;
list = g_slist_append(list, "second");
list = g_slist_prepend(list, "first");
printf("The list is now %d items long\n", g_slist_length(list));
list = g_slist_remove(list, "first");
printf("The list is now %d items long\n", g_slist_length(list));
g_slist_free(list);
return 0;
}***** Output *****
The list is now 2 items long
The list is now 1 items long
这段代码中大部分看起来都比较熟悉,不过有一些地方需要考虑;
如果调用 g_slist_remove 时传递了一个并不在列表中的条目, 那么列表将不会发生变化。
g_slist_remove 也会返回列表的新起始位置。
可以发现,“first”是调用 g_slist_prepend 添加的。这是个调用比 g_slist_append 更快;它是 O(1) 操作而不是 O(n) 操作,原因如前所述,进行附加需要遍历整个列表。所以,如果使用 g_slist_prepend 更为方便,那么就应该使用它。2.4、删除重复的条目
这里是当在一个列表中有重复的条目时会发生的问题:
//ex-gslist-3.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = NULL;
list = g_slist_append(list, "first");
list = g_slist_append(list, "second");
list = g_slist_append(list, "second");
list = g_slist_append(list, "third");
list = g_slist_append(list, "third");
printf("The list is now %d items long\n", g_slist_length(list));
list = g_slist_remove(list, "second");
list = g_slist_remove_all(list, "third");
printf("The list is now %d items long\n", g_slist_length(list));
g_slist_free(list);
return 0;
}***** Output *****
The list is now 5 items long
The list is now 2 items long所以,如果 GSList 包含了两个同样的指针,而调用了 g_slist_remove,那么只会删除第一个指针。 不过,可以使用 g_slist_remove_all 删除条目的所有指针。
2.5、最后一个、第 n 个 和第 n 个数据
在 GSList 中有了一些条目后,可以通过不同的方式提取它们。这里是一些示例,并在 printf 语句中给出了解释。
//ex-gslist-4.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = NULL;
list = g_slist_append(list, "first");
list = g_slist_append(list, "second");
list = g_slist_append(list, "third");
printf("The last item is '%s'\n", g_slist_last(list)->data);
printf("The item at index '1' is '%s'\n", g_slist_nth(list, 1)->data);
printf("Now the item at index '1' the easy way: '%s'\n", g_slist_nth_data(list, 1));
printf("And the 'next' item after first item is '%s'\n", g_slist_next(list)->data);
g_slist_free(list);
return 0;
}***** Output *****
The last item is 'third'
The item at index '1' is 'second'
Now the item at index '1' the easy way: 'second'
And the 'next' item after first item is 'second'注意,有一些可以作用于 GSList 的快捷函数;可以简单地调用 g_slist_nth_data,而不需 调用先 g_slist_nth 然后再反引用(dereference)返回的指针。
最后一个 printf 语句稍有不同。g_slist_next 不是一个函数调用,而是一个宏。 它展开为一个指向 GSList 中下一元素的指针反引用。在这种情况下,可以看到,我们传递了 GSList 中的第一个元素,于是那个 宏展并给出第二个元素。这也是一个快速的操作,因为没有函数调用的开销。
2.6、退一步:使用用户定义的类型
到现在为止我们一直在使用字符串;也就是说,我们只是将指向字符的指针放入到 GSList 中。在下面的代码示例中,将会定义一个 Person 结构体,并将这个结构体的一些实例放入到 GSList 中:
//ex-gslist-5.c
#include <glib.h>
typedef struct {
char* name;
int shoe_size;
} Person;
int main(int argc, char** argv) {
GSList* list = NULL;
Person* tom = (Person*)malloc(sizeof(Person));
tom->name = "Tom";
tom->shoe_size = 12;
list = g_slist_append(list, tom);
Person* fred = g_new(Person, 1); // allocate memory for one Person struct
fred->name = "Fred";
fred->shoe_size = 11;
list = g_slist_append(list, fred);
printf("Tom's shoe size is '%d'\n", ((Person*)list->data)->shoe_size);
printf("The last Person's name is '%s'\n", ((Person*)g_slist_last(list)->data)->name);
g_slist_free(list);
free(tom);
g_free(fred);
return 0;
}***** Output *****
Tom's shoe size is '12'
The last Person's name is 'Fred'关于使用 GLib 和用户定义类型的一些注解:
可以像使用字符串一样在 GSList 使用用户定义类型。另外要注意,当从列表中取出条目时,需要进行一些强制类型转换。
这个示例使用了另一个 GLib 宏 —— g_new 宏 —— 来创建 Fred Person 实例。这个宏只是展开并使用 malloc 为给定的类型分配适当数量 的内存,但是这比手工输入 malloc 函数调用更为简洁。
最后,如果要分配内存,那么还需要释放它。可以看到上面的代码示例如何使用 GLib 函数 g_free 来为 Fred Person 实例完成此任务(因为它是使用 g_new 分配的)。 在大部分情况下 g_free 只是会包装 free 函数, 但是 GLib 也具备内存池功能,g_free 以及其他内存管理函数可以使用它们。2.7、组合、反转,等等
GSList 附带了一些便利的工具,可以连接和反转列表。这里是它们的工作方式:
//ex-gslist-6.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list1 = NULL;
list1 = g_slist_append(list1, "first");
list1 = g_slist_append(list1, "second");
GSList* list2 = NULL;
list2 = g_slist_append(list2, "third");
list2 = g_slist_append(list2, "fourth");
GSList* both = g_slist_concat(list1, list2);
printf("The third item in the concatenated list is '%s'\n", g_slist_nth_data(both, 2));
GSList* reversed = g_slist_reverse(both);
printf("The first item in the reversed list is '%s'\n", reversed->data);
g_slist_free(reversed);
return 0;
}***** Output *****
The third item in the concatenated list is 'third'
The first item in the reversed list is 'fourth'正如所预期的,两个列表首尾相连在一起,list2 中的第一个条目成为新的列表中的第三个条目。 注意,并没有拷贝条目;它们只是被链接上,这样内存只需要释放一次。
另外,您会发现您只需要使用一个指针反引用(reversed->data)就可以打印出反向列表的第一个条目。 由于 GSList 中的每一个条目都是一个指向某个 GSList 结构体的指针,所以要获得第一个条目并不需要调用函数。2.8、遍历
简单遍历
这里是遍历 GSList 中所有内容的一个直观方法:
//ex-gslist-7.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = NULL, *iterator = NULL;
list = g_slist_append(list, "first");
list = g_slist_append(list, "second");
list = g_slist_append(list, "third");
for (iterator = list; iterator; iterator = iterator->next) {
printf("Current item is '%s'\n", iterator->data);
}
g_slist_free(list);
return 0;
}***** Output *****
Current item is 'first'
Current item is 'second'
Current item is 'third'迭代器(iterator)对象只是一个声明为指向 GSList 结构体的变量。这看似奇怪,不过却能满足要求。 由于单向列表是一系列 GSList 结构体,所以迭代器与列表的类型应该相同。
另外,注意这个代码段使用的是通常的 GLib 用法习惯;在声明 GSList 本身的时候就声明了迭代器变量。
最后,for 循环的退出表达式检查迭代器是否为 NULL。 这样是有效的,因为只有当循环传递了列表中的最后一个条目后它才会成为 NULL。使用函数进行高级遍历
遍历列表的另一种方法是使用 g_slist_foreach,并提供一个将为列表中的每一个条目调用的函数。
//ex-gslist-7.c
//ex-gslist-8.c
#include <glib.h>
void print_iterator(gpointer item, gpointer prefix) {
printf("%s %s\n", prefix, item);
}
void print_iterator_short(gpointer item) {
printf("%s\n", item);
}
int main(int argc, char** argv) {
GSList* list = g_slist_append(NULL, g_strdup("first"));
list = g_slist_append(list, g_strdup("second"));
list = g_slist_append(list, g_strdup("third"));
printf("Iterating with a function:\n");
g_slist_foreach(list, print_iterator, "-->");
printf("Iterating with a shorter function:\n");
g_slist_foreach(list, (GFunc)print_iterator_short, NULL);
printf("Now freeing each item\n");
g_slist_foreach(list, (GFunc)g_free, NULL);
g_slist_free(list);
return 0;
}***** Output *****
Iterating with a function:
--> first
--> second
--> third
Iterating with a shorter function:
first
second
third
Now freeing each item在这个示例中有很多好东西:
一条类似 GSList x = g_slist_append(NULL, [whatever]) 的语句让您能够一举声明、 初始化并将第一个条目添加到列表。
g_strdup 函数可以方便地复制字符串;如果使用了它,那么要记得释放。
g_slist_foreach 允许传递一个指针,这样就可以根据列表中的每个条目有效地为其赋与任意参数。 例如,可以传递一个累加器并收集关于列表中每个条目的信息。遍历函数的唯一受限之处在于它至少要使用一个 gpointer 作为参数;现在可以了解在只接收一个参数时 print_interator_short 如何工作。
注意,代码使用一个内置的 GLib 函数作为 g_slist_foreach 的参数来释放所有字符串。 在此示例中,所有需要做的只是将 g_free 强制类型转换为 GFunc 以使其生效。 注意,仍然可以单独使用 g_slist_free 来释放 GSList 本身。
2.9、排序
使用 GCompareFunc 排序
可以通过提供一个知道如何比较列表中条目的函数来对 GSLit 进行排序。下面的示例展示了对字符串列表进行排序的一种方法:
//ex-gslist-9.c
#include <glib.h>
gint my_comparator(gconstpointer item1, gconstpointer item2) {
return g_ascii_strcasecmp(item1, item2);
}
int main(int argc, char** argv) {
GSList* list = g_slist_append(NULL, "Chicago");
list = g_slist_append(list, "Boston");
list = g_slist_append(list, "Albany");
list = g_slist_sort(list, (GCompareFunc)my_comparator);
printf("The first item is now '%s'\n", list->data);
printf("The last item is now '%s'\n", g_slist_last(list)->data);
g_slist_free(list);
return 0;
}***** Output *****
The first item is now 'Albany'
The last item is now 'Chicago'注意,如果第一个条目小于第二个,则 GCompareFunc 返回一个负值,如果相等则返回 0,如果第二个大于第一个 则返回一个正值。只要您的比较函数符合此规范,在内部它可以做任何所需要的事情。
另外,由于各种其他 GLib 函数也遵循此模式,所以可以简单地委派给它们。实际上,在上面的示例中,可以简单地把 my_comparator 调用替换为 g_slist_sort(list, (GCompareFunc)g_ascii_strcasecmp) 等调用,会获得相同的结果。2.10查找元素
有一些方法可以在 GSList 查找元素。已经介绍了如何简单地遍历列表的全部内容,比较每个条目,直到找到了目标条目。 如果已经拥有了要寻找的数据,而只是想要获得它在列表中的位置,那么可以使用 g_slist_find。最后,可以使用 g_slist_find_custom,它允许您使用一个函数来检查 列表中的每一个条目。下面展示了 g_slist_find 和 g_slist_find_custom:
//ex-gslist-10.c
#include <glib.h>
gint my_finder(gconstpointer item) {
return g_ascii_strcasecmp(item, "second");
}
int main(int argc, char** argv) {
GSList* list = g_slist_append(NULL, "first");
list = g_slist_append(list, "second");
list = g_slist_append(list, "third");
GSList* item = g_slist_find(list, "second");
printf("This should be the 'second' item: '%s'\n", item->data);
item = g_slist_find_custom(list, NULL, (GCompareFunc)my_finder);
printf("Again, this should be the 'second' item: '%s'\n", item->data);
item = g_slist_find(list, "delta");
printf("'delta' is not in the list, so we get: '%s'\n", item ? item->data : "(null)");
g_slist_free(list);
return 0;
}***** Output *****
This should be the 'second' item: 'second'
Again, this should be the 'second' item: 'second'
'delta' is not in the list, so we get: '(null)'注意,g_slist_find_custom 也使用了一个可以指向任意内容的指针作为第二个参数,所以,如果需要,可以 传递进来一些内容以帮助查找函数。另外,GCompare 函数是最后一个参数,而不是第二个参数,因为它在 g_slist_sort 之中。最后,失败的查找会返回 NULL。
2.11、通过插入进行高级添加
既然已经接触过几次 GCompareFunc,一些更有趣的插入操作会更有意义。使用 g_slist_insert 可以将条目插入到指定的位置,使用 g_slist_insert_before 可以将条目插入到特定位置之前,使用 g_slist_insert_sorted 可以进行有序插入。这里是样例:
//ex-gslist-11.c
#include <glib.h>
int main(int argc, char** argv) {
GSList* list = g_slist_append(NULL, "Anaheim "), *iterator = NULL;
list = g_slist_append(list, "Elkton ");
printf("Before inserting 'Boston', second item is: '%s'\n", g_slist_nth(list, 1)->data);
g_slist_insert(list, "Boston ", 1);
printf("After insertion, second item is: '%s'\n", g_slist_nth(list, 1)->data);
list = g_slist_insert_before(list, g_slist_nth(list, 2), "Chicago ");
printf("After an insert_before, third item is: '%s'\n", g_slist_nth(list, 2)->data);
list = g_slist_insert_sorted(list, "Denver ", (GCompareFunc)g_ascii_strcasecmp);
printf("After inserting 'Denver', here's the final list:\n");
g_slist_foreach(list, (GFunc)printf, NULL);
g_slist_free(list);
return 0;
}***** Output *****
Before inserting 'Boston', second item is: 'Elkton '
After insertion, second item is: 'Boston '
After an insert_before, third item is: 'Chicago '
After inserting 'Denver', here's the final list:
Anaheim Boston Chicago Denver Elkton由于 g_slist_insert_sorted 使用了 GCompareFunc,所以复用内置的 GLib 函数 g_ascii_strcasecmp 也很简单。现在您应该已经能理解为什么在每个条目的末尾有额外的空间;在代码示例的最后加入了 另一个 g_slist_foreach 示例,这一次是使用 GFunc 作为参数。
2.12、实际应用
在先前提到的三个开放源代码的实际应用程序中,可以发现在很多地方使用了 GSList。大部分用法相当普通,有很多插入、附加、删除,等等。 不过有一些更有趣的东西。
Gaim 使用 GSList 来保存当前会话以及大部分插件中的各种内容:
gaim-1.2.1/src/away.c 使用有序的 GSList 来保存“离开消息”(客户机离线期间收到的消息)。 它使用了一个定制的 GCompareFunc,即 sort_awaymsg_list,用于保存那些以 发送者名称排序的消息。
gaim-1.2.1/src/protocols/gg/gg.c 使用 GSList 来保存允许帐号的一个列表;然后它使用一个定制的查找器来确认某个 帐号在列表中。定制的查找器简单地委派 g_ascii_strcasecmp,所以有可能会弃用它,并将 g_ascii_strcasecmp 直接传递给 g_slist_find_custom。
Evolution 同样使用了很多 GSList:
evolution-data-server-1.0.2/calendar/libecal/e-cal-component.c 使用 GSList 来存会议参与者。有时,它会反复调用 g_slist_prepend,并在结束时使用 g_slist_reverse 令条目按期望排序, 以此构建那个 GSList。如前所述,这样做比使用 g_slist_append 添加条目更快。
evolution-2.0.2/addressbook/gui/contact-editor/e-contact-editor.c 在一种卫述句(guard clause)中使用 g_slist_find; 它在信号处理器中使用它,以确保它在一个信号回调中接收到的 EContactEditor 在被作为参数传递到某个函数之前能够保存。
GIMP 也以一些适宜的方式使用了 GSList:
gimp-2.2.4/plug-ins/maze/algorithms.c 在迷宫生成(maze-generations)算法中使用 GSList 追踪单元(cells)。
gimp-2.2.4/app/widgets/gimpclipboard.c 使用 GSList 来保存剪贴板象素缓冲区格式(比如 PNG 和 JPEG);它向 g_slist_sort 传递一个定制的 GCompareFunc。
gimp-2.2.4/app/core/gimppreviewcache.c 使用 GSList 作为一种基于大小的队列;它在 GSList 中保存图象预览,使用 g_slist_insert_sorted 优先插入较小的图象。同一文件中的另一个函数会遍历同一个 GSList 并比较每一个条目的表面区域,找出要删除的最小那一个,以此来整理缓存。3、双向链表
3.1、概念
双
向链表与单向链表非常类似,不过它们包含有另外的指针,以支持更多导航选项;给定双向链表中的一个节点,可以向前移动,也可以向后移动。 这使得它们比单向链表更灵活,但也使用了更多内存,所以,除非确实需要这种灵活性,否则不要使用这种双向链表。
GLib 包含有一个名为 GList 的双向链表实现。GList 中的大部分操作与 GSList 中的类似。我们将查看基本用法的一些示例, 然后是 GList 所支持的附加操作。
3.2、基本操作
这
里是使用 GList 可以进行的一些常见操作://ex-glist-1.c
#include <glib.h>
int main(int argc, char** argv) {
GList* list = NULL;
list = g_list_append(list, "Austin ");
printf("The first item is '%s'\n", list->data);
list = g_list_insert(list, "Baltimore ", 1);
printf("The second item is '%s'\n", g_list_next(list)->data);
list = g_list_remove(list, "Baltimore ");
printf("After removal of 'Baltimore', the list length is %d\n", g_list_length(list));
GList* other_list = g_list_append(NULL, "Baltimore ");
list = g_list_concat(list, other_list);
printf("After concatenation: ");
g_list_foreach(list, (GFunc)printf, NULL);
list = g_list_reverse(list);
printf("\nAfter reversal: ");
g_list_foreach(list, (GFunc)printf, NULL);
g_list_free(list);
return 0;
}***** Output *****
The first item is 'Austin '
The second item is 'Baltimore '
After removal of 'Baltimore', the list length is 1
After concatenation: Austin Baltimore
After reversal: Baltimore Austin上面的代码看起来非常熟悉!上面所有操作都在 GSList 中出现过;唯一的区别是 GList 的函数名是 g_list 开头,而 不是 g_slist。当然,它们全都要使用指向 GList 结构体而不是指向 GSList 结构体。
3.3、更好的导航
已
经了解了一些基本的 GList 操作后,这里是一些可能的操作,唯一的原因就是 GList 中的每个节点都有一个指向前一个节点的链接:
//ex-glist-2.c
#include <glib.h>
int main(int argc, char** argv) {
GList* list = g_list_append(NULL, "Austin ");
list = g_list_append(list, "Bowie ");
list = g_list_append(list, "Charleston ");
printf("Here's the list: ");
g_list_foreach(list, (GFunc)printf, NULL);
GList* last = g_list_last(list);
printf("\nThe first item (using g_list_first) is '%s'\n", g_list_first(last)->data);
printf("The next-to-last item is '%s'\n", g_list_previous(last)->data);
printf("The next-to-last item is '%s'\n", g_list_nth_prev(last, 1)->data);
g_list_free(list);
return 0;
}***** Output *****
Here's the list: Austin Bowie Charleston
The first item (using g_list_first) is 'Austin '
The next-to-last item is 'Bowie '
The next-to-last item is 'Bowie '
没有太令人惊讶的事情,不过有一些注解:
g_list_nth_prev 的第二个参数是一个整数,表明需要向前导航多少个节点。如果传递的值超出了 GList 的 范围,那么要准备好处理程序的崩溃。
g_list_first 是一个 O(n) 操作。它从一个给定的节点开始向后搜索,直到找到 GList 的头。 上面的示例是一个最坏的情形,因为遍历是从列表的末尾开始的。同理,g_list_last 也是一个 O(n) 操作。
3.4、使用链接删除节点
已
经了解了在拥有指向其容纳的数据的指针的前提下如何从列表中删除一个节点;g_list_remove 可以很好地完成。 如果拥有一个指向节点本身的指针,可以通过一个快速的 O(1) 操作直接删除那个节点。//ex-glist-3.c
#include <glib.h>
int main(int argc, char** argv) {
GList* list = g_list_append(NULL, "Austin ");
list = g_list_append(list, "Bowie ");
list = g_list_append(list, "Chicago ");
printf("Here's the list: ");
g_list_foreach(list, (GFunc)printf, NULL);
GList* bowie = g_list_nth(list, 1);
list = g_list_remove_link(list, bowie);
g_list_free_1(bowie);
printf("\nHere's the list after the remove_link call: ");
g_list_foreach(list, (GFunc)printf, NULL);
list = g_list_delete_link(list, g_list_nth(list, 1));
printf("\nHere's the list after the delete_link call: ");
g_list_foreach(list, (GFunc)printf, NULL);
g_list_free(list);
return 0;
}***** Output *****
Here's the list: Austin Bowie Chicago
Here's the list after the remove_link call: Austin Chicago
Here's the list after the delete_link call: Austin
所以如果拥有一个指向节点而不是数据的指针,那么可以使用 g_list_remove_link 删除那个节点。
删除它以后,还需要使用 g_list_free_1 显式地释放它,其所做的事情正如名字所暗示的: 它释放一个节点。照例,需要保存好 g_list_remove_link 的返回值,因为那里是 列表的新起始位置。
最后,如果想做的只是删除并释放某个节点,那么可以调用 g_list_delete_link 来一步完成。
GSList 也有相同的函数;只需要将 g_list 替换为 g_slist,上面的所有信息都适用。
3.5、索引和位置
如
果只是想找出某个条目在 GList 中的位置,那么有两种选择。可以使用 g_list_index,它可以使用条目中的 数据找出它,或者可以使用 g_list_position,它使用的是指向那个节点的指针。 这个示例展示了这两种方法:
//ex-glist-4.c
#include <glib.h>
int main(int argc, char** argv) {
GList* list = g_list_append(NULL, "Austin ");
list = g_list_append(list, "Bowie ");
list = g_list_append(list, "Bowie ");
list = g_list_append(list, "Cheyenne ");
printf("Here's the list: ");
g_list_foreach(list, (GFunc)printf, NULL);
printf("\nItem 'Bowie' is located at index %d\n", g_list_index(list, "Bowie "));
printf("Item 'Dallas' is located at index %d\n", g_list_index(list, "Dallas"));
GList* last = g_list_last(list);
printf("Item 'Cheyenne' is located at index %d\n", g_list_position(list, last));
g_list_free(list);
return 0;
}***** Output *****
Here's the list: Austin Bowie Bowie Cheyenne
Item 'Bowie' is located at index 1
Item 'Dallas' is located at index -1
Item 'Cheyenne' is located at index 3
注意,如果 g_list_index 找不到那个数据,则它返回的值为 -1。 如果两个节点具有相同的数据值,那么 g_list_index 会返回最先出现的索引。如果找不到指定的节点, g_list_position 也会返回 -1。
另外,在 GSList 也有这些方法,只是名字不同。
3.6、实际应用
让
我们来研究在前面提到的开放源代码的应用程序中 GList 的应用。
Gaim 使用了很多 GList:
在关闭“add a buddy”对话框时,gaim-1.2.1/plugins/gevolution/add_buddy_dialog.c 使用 g_list_foreach 调用来释放对 每个联系人的引用。
gaim-1.2.1/src/account.c 使用一个 GList 来保存所有帐号;它使用 g_list_find 来确保某个帐号在使用 g_list_append 进行添加之前并不存在。
Evolution 使用 GList:
evolution-2.0.2/filter/filter-rule.c 使用 GList 来保存邮件过滤规则的各部分(比如要检查的主题行); filter_rule_finalise 使用 g_list_foreach 释放对那些部分的 引用。
evolution-2.0.2/calendar/gui/alarm-notify/alarm.c 使用 GList 保存警告; queue_alarm 借助一个定制的 GCompareFunc 使用 g_list_insert_sorted 将新的警告插入到适当的位置。
在 GIMP 中的应用:
gimp-2.2.4/app/file/gimprecentlist.c 使用 GList 保存最近访问过的文件; gimp_recent_list_read 从某个 XML 文件描述符中读取文件名,并在返回 GList 之前调用 g_list_reverse。
gimp-2.2.4/app/vectors/gimpbezierstroke.c 使用 GList 保存笔划连接点(stroke anchors); gimp_bezier_stroke_connect_stroke 使用 g_list_concat 来帮助将一个笔划连接到另一个笔划。
4、散列表
4.1、概念
到目前为止,本教程只介绍了有序容器,在其中插入的条目会保持特定次序不变。散列表 是另一类容器,也称为“映射”、“联合数组(associative array)” 或者“目录(dictionary)”。
正如语文辞典使用一个定义来关联一个词,散列表使用一个 键(key) 来唯一标识一个 值(value)。散列表可以根据键非常快速地执行插入、 查找和删除操作;实际上,如果使用得当,这些可以都是常数时间 —— 也就是 O(1) —— 操作。这比从一个有序列表中查找或删除条目快得多,那是 O(n) 操作。
散列表之所以能快速执行操作,是因为它们使用 散列函数 来定位键。散列函数获得一个键并为其计算一个唯一的值, 称为 散列值(hash)。例如,一个散列函数可以接受一个词并将那个词中的字母数作为散列值返回。那是个不好的散列函数,因为 “fiddle”和“faddle”将会散列为相同的值。
当散列函数为不同的键返回相同的散列值时,取决于散列表的实现会发生各种不同的事情。散列表可以使用第二个值覆盖第一个值,也可以 将值放入一个列表,或者或以简单地抛出一个错误。
注意,散列表不是必然比列表更快。如果拥有的条目较少 —— 少于一打左右 —— 那么使用有序的集合会获得更好的性能。那是因为,尽管在散列表中 存储和获取数据需要常数时间,那个常数时间值可能会很大,因为计算条目的散列值相对于反引用一两个指针会是一个较慢的过程。对于较小的值, 简单地遍历有序列表会比进行散列计算更快。
无论何时,重要的是在选择容器时要考虑自己应用程序的具体数据存储需要。如果应用程序很明显需要某种容器,那么没有理由不去使用它。
4.2、一些简单的散列表操作
这里是一些示例,可以生动地展示以上的理论:
//ex-ghashtable-1.c
#include <glib.h>
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
g_hash_table_insert(hash, "Virginia", "Richmond");
g_hash_table_insert(hash, "Texas", "Austin");
g_hash_table_insert(hash, "Ohio", "Columbus");
printf("There are %d keys in the hash\n", g_hash_table_size(hash));
printf("The capital of Texas is %s\n", g_hash_table_lookup(hash, "Texas"));
gboolean found = g_hash_table_remove(hash, "Virginia");
printf("The value 'Virginia' was %sfound and removed\n", found ? "" : "not ");
g_hash_table_destroy(hash);
return 0;
}***** Output *****
There are 3 keys in the hash
The capital of Texas is Austin
The value 'Virginia' was found and removed
有很多新东西,所以给出一些注解:
对 g_hash_table_new 的调用指定了这个散列表将使用字符串作为键。函数 g_str_hash 和 g_str_equal 是 GLib 的内置函数, 因为这很常用。其他内置 散列/等同(equality) 函数包括 g_int_hash /g_int_equal(使用整数作为键)以及 g_direct_hash/g_direct_equal(使用指针作为键)。
GLists 和 GSLists 拥有一个 g_[container]_free 函数来清除它们; 可以使用 g_hash_table_destroy 来清空 GHashTable。
当尝试使用 g_hash_table_remove 删除 键/值 对时,会获得一个 gboolean 返回值, 表明键是否找到并删除。gboolean 是 真/假 值的一个简单的跨平台 GLib 实现。
g_hash_table_size 返回散列表中键的数目。
4.3、插入和替换值
当使用 g_hash_table_insert 插入键时,GHashTable 首先检查那个键是否已经存在。如果已经存在, 那么那个值会被替换,而键不会被替换。如果希望同时替换键和值,那么需要使用 g_hash_table_replace。 它稍有不同,因此在下面同时展示了二者://ex-ghashtable-2.c
#include <glib.h>
static char* texas_1, *texas_2;
void key_destroyed(gpointer data) {
printf("Got a key destroy call for %s\n", data == texas_1 ? "texas_1" : "texas_2");
}
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new_full(g_str_hash, g_str_equal,
(GDestroyNotify)key_destroyed, NULL);
texas_1 = g_strdup("Texas");
texas_2 = g_strdup("Texas");
g_hash_table_insert(hash, texas_1, "Austin");
printf("Calling insert with the texas_2 key\n");
g_hash_table_insert(hash, texas_2, "Houston");
printf("Calling replace with the texas_2 key\n");
g_hash_table_replace(hash, texas_2, "Houston");
printf("Destroying hash, so goodbye texas_2\n");
g_hash_table_destroy(hash);
g_free(texas_1);
g_free(texas_2);
return 0;
}***** Output *****
Calling insert with the texas_2 key
Got a key destroy call for texas_2
Calling replace with the texas_2 key
Got a key destroy call for texas_1
Destroying hash, so goodbye texas_2
Got a key destroy call for texas_2从输出可以看到,当 g_hash_table_insert 尝试插入与现有键相同的字符串(Texas)时, GHashTable 只是简单的释放传递进来的键(texas_2),并令当前键(texas_1)保持不变。 但是当 g_hash_table_replace 做同样的事情时,texas_1 键被销毁, 并在使用它的地方使用 texas_2 键。更多注解:
当创建新的 GHashTable 时,可以使用 g_hash_table_full 来提供一个 GDestroyNotify 实现,在键被销毁时调用它。这让您能够为那个键进行完全的资源清除,或者(在本例中) 去查看在键变化时实际发生的事情。
在前面的 GSList 部分已经出现过 g_strdup;在这里使用它来分配字符串 Texas 的两个拷贝。可以发现,GHashTable 函数 g_str_hash 和 g_str_equal 正确地 检测到,尽管指针指向不同的内存位置,但实际上字符串是相同的。为了避免内存泄漏,在函数的末尾必须释放 texas_1 和 texas_2 当然,在本例中这并不重要,因为程序会退出, 但是无论如何能够清除是最好的。
4.4、遍历 键/值 对
有时需要遍历所有的 键/值 对。这里是如何使用 g_hash_table_foreach 来完成那项任务://ex-ghashtable-3.c
#include <glib.h>
void iterator(gpointer key, gpointer value, gpointer user_data) {
printf(user_data, *(gint*)key, value);
}
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new(g_int_hash, g_int_equal);
gint* k_one = g_new(gint, 1), *k_two = g_new(gint, 1), *k_three = g_new(gint, 1);
*k_one = 1, *k_two=2, *k_three = 3;
g_hash_table_insert(hash, k_one, "one");
g_hash_table_insert(hash, k_two, "four");
g_hash_table_insert(hash, k_three, "nine");
g_hash_table_foreach(hash, (GHFunc)iterator, "The square of %d is %s\n");
g_hash_table_destroy(hash);
return 0;
}***** Output *****
The square of 1 is one
The square of 2 is four
The square of 3 is nine在这个示例中有一些细微的不同之处:
可以发现,使用 GLib 提供的散列函数 g_int_hash 和 g_int_equal 让您能够使用指向整数的指针作为键。本示例使用的是整数的 GLib 跨平台抽象: gint。
g_hash_table_foreach 与您已经了解的 g_slist_foreach 和 g_list_foreach 函数非常类似。唯一的区别是,传递到 g_hash_table_foreach 的 GHFunc 要接受三个参数,而不是两个。 在本例中,传递进入一个用来格式化键和字符串的打印的字符串作为第三个参数。另外,尽管在本示例时键恰巧是以它们插入的顺序 打印出来,但绝对不保证那个键插入的顺序会被保留。4.5、查找条目
使用 g_hash_table_find 函数来查找某个特定的值。这个函数支持查看每一个 键/值 对,直到 定位到期望的值。这里是一个示例:
//ex-ghashtable-4.c
#include <glib.h>
void value_destroyed(gpointer data) {
printf("Got a value destroy call for %d\n", GPOINTER_TO_INT(data));
}
gboolean finder(gpointer key, gpointer value, gpointer user_data) {
return (GPOINTER_TO_INT(key) + GPOINTER_TO_INT(value)) == 42;
}
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
NULL,
(GDestroyNotify)value_destroyed);
g_hash_table_insert(hash, GINT_TO_POINTER(6), GINT_TO_POINTER(36));
g_hash_table_insert(hash, GINT_TO_POINTER(10), GINT_TO_POINTER(12));
g_hash_table_insert(hash, GINT_TO_POINTER(20), GINT_TO_POINTER(22));
gpointer item_ptr = g_hash_table_find(hash, (GHRFunc)finder, NULL);
gint item = GPOINTER_TO_INT(item_ptr);
printf("%d + %d == 42\n", item, 42-item);
g_hash_table_destroy(hash);
return 0;
}***** Output *****
36 + 6 == 42
Got a value destroy call for 36
Got a value destroy call for 22
Got a value destroy call for 12照例,本示例介绍了 g_hash_table_find 以及其他一些内容:
GHRFunc 返回 TRUE 时,g_hash_table_find 返回第一个值。 如果 GHRFunc 作用于任意条目都都不返回 TRUE(这表明没有找到合适的条目),则它返回 NULL。
本示例介绍了另一组内置的 GLib 散列函数:g_direct_hash 和 g_direct_equal。这组函数支持使用指针作为键,但却没有尝试去解释指针背后的数据。 由于要将指针放入 GHashTable,所以需要使用一些便利的 GLib 宏(GINT_TO_POINTER 和 GPOINTER_TO_INT)来在整数与指针之间进行转换。
最后,本示例创建了 GHashTable,并给予它一个 GDestroyNotify 回调函数,以使得您可以查看条目是何时被销毁的。 大部分情况下您会希望在一个与此类似的函数中释放某些内存,不过出于示例的目的,这个实现只是打印出一条消息。
4.6、难处理的情形:从表中删除
偶尔可能需要从一个 GHashTable 中删除某个条目,但却没有获得 GHashTable 所提供的任意 GDestroyNotify 函数的 回调。要完成此任务,或者可以根据具体的键使用 g_hash_table_steal,或者根据所有匹配某个条件的键使用 g_hash_table_foreach_steal。//ex-ghashtable-5.c
#include <glib.h>
gboolean wide_open(gpointer key, gpointer value, gpointer user_data) {
return TRUE;
}
void key_destroyed(gpointer data) {
printf("Got a GDestroyNotify callback\n");
}
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new_full(g_str_hash, g_str_equal,
(GDestroyNotify)key_destroyed,
(GDestroyNotify)key_destroyed);
g_hash_table_insert(hash, "Texas", "Austin");
g_hash_table_insert(hash, "Virginia", "Richmond");
g_hash_table_insert(hash, "Ohio", "Columbus");
g_hash_table_insert(hash, "Oregon", "Salem");
g_hash_table_insert(hash, "New York", "Albany");
printf("Removing New York, you should see two callbacks\n");
g_hash_table_remove(hash, "New York");
if (g_hash_table_steal(hash, "Texas")) {
printf("Texas has been stolen, %d items remaining\n", g_hash_table_size(hash));
}
printf("Stealing remaining items\n");
g_hash_table_foreach_steal(hash, (GHRFunc)wide_open, NULL);
printf("Destroying the GHashTable, but it's empty, so no callbacks\n");
g_hash_table_destroy(hash);
return 0;
}***** Output *****
Removing New York, you should see two callbacks
Got a GDestroyNotify callback
Got a GDestroyNotify callback
Texas has been stolen, 3 items remaining
Stealing remaining items
Destroying the GHashTable, but it's empty, so no callbacks
4.7、高级查找:找到键和值
针对需要从表中同时获得键和值的情况,GHashTable 提供了一个 g_hash_table_lookup_extended 函数。 它与 g_hash_table_lookup 非常类似,但要接受更多两个参数。这些都是“out”参数;也就是说, 它们是双重间接指针,当数据被定位时将指向它。这里是它的工作方式://ex-ghashtable-6.c
#include <glib.h>
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
g_hash_table_insert(hash, "Texas", "Austin");
g_hash_table_insert(hash, "Virginia", "Richmond");
g_hash_table_insert(hash, "Ohio", "Columbus");
char* state = NULL;
char* capital = NULL;
char** key_ptr = &state;
char** value_ptr = &capital;
gboolean result = g_hash_table_lookup_extended(hash, "Ohio", (gpointer*)key_ptr, (gpointer*)value_ptr);
if (result) {
printf("Found that the capital of %s is %s\n", capital, state);
}
if (!g_hash_table_lookup_extended(hash, "Vermont", (gpointer*)key_ptr, (gpointer*)value_ptr)) {
printf("Couldn't find Vermont in the hash table\n");
}
g_hash_table_destroy(hash);
return 0;
}***** Output *****
Found that the capital of Columbus is Ohio
Couldn't find Vermont in the hash table初始化能够接收 键/值 数据的变量有些复杂,但考虑到它是从函数返回多于一个值的途径,这可以理解。注意,如果您 为后两个参数之一传递了 NULL,则 g_hash_table_lookup_extended 仍会工作,只是不是填充 NULL 参数。
4.8、每个键多个值
到目前为止已经介绍了每个键只拥有一个值的散列。不过有时您需要让一个键持有多个值。当出现这种需求时,使用 GSList 作为值并及 GSList 添加新的值通常是一个好的解决方案。不过,这需要稍多一些工作,如本例中所示://ex-ghashtable-7.c
#include <glib.h>
void print(gpointer key, gpointer value, gpointer data) {
printf("Here are some cities in %s: ", key);
g_slist_foreach((GSList*)value, (GFunc)printf, NULL);
printf("\n");
}
void destroy(gpointer key, gpointer value, gpointer data) {
printf("Freeing a GSList, first item is %s\n", ((GSList*)value)->data);
g_slist_free(value);
}
int main(int argc, char** argv) {
GHashTable* hash = g_hash_table_new(g_str_hash, g_str_equal);
g_hash_table_insert(hash, "Texas",
g_slist_append(g_hash_table_lookup(hash, "Texas"), "Austin "));
g_hash_table_insert(hash, "Texas",
g_slist_append(g_hash_table_lookup(hash, "Texas"), "Houston "));
g_hash_table_insert(hash, "Virginia",
g_slist_append(g_hash_table_lookup(hash, "Virginia"), "Richmond "));
g_hash_table_insert(hash, "Virginia",
g_slist_append(g_hash_table_lookup(hash, "Virginia"), "Keysville "));
g_hash_table_foreach(hash, print, NULL);
g_hash_table_foreach(hash, destroy, NULL);
g_hash_table_destroy(hash);
return 0;
}***** Output *****
Here are some cities in Texas: Austin Houston
Here are some cities in Virginia: Richmond Keysville
Freeing a GSList, first item is Austin
Freeing a GSList, first item is Richmondg_slist_append 接受 NULL 作为 GSList 的合法参数,示例中的“insert a new city”代码利用了这一事实; 它不需要检查这是不是添加到给定州的列表的第一个城市。
当销毁 GHashTable 时,必须记住在释放散列表本身之前先释放那些 GSList。注意,如果没有在那些列表中使用静态字符串,这会更为复杂; 在那种情况下需要在释放列表本身之前先释放每个 GSList 之中的每个条目。这个示例所展示的内容之一是各种 foreach 函数多么实用 —— 它们可以节省很多输入。
4.9、现实应用
这里是如何使用 GHashTables 的样例。
在 Gaim 中:
gaim-1.2.1/src/buddyicon.c 使用 GHashTable 来保持对“好友图标(buddy icons)”的追踪。键是好友的用户名,值是指向 GaimBuddyIcon 结构体的指针。
gaim-1.2.1/src/protocols/yahoo/yahoo.c 是这三个应用程序中唯一使用 g_hash_table_steal 的地方。 它使用 g_hash_table_steal 作为构建帐号名到好友列表的映射的代码片断的组成部分。
在 Evolution 中:
evolution-2.0.2/smime/gui/certificate-manager.c 使用 GHashTable 来追踪 S/MIME 证书的根源;键是组织名,值是指向 GtkTreeIter 的指针。
evolution-data-server-1.0.2/calendar/libecal/e-cal.c 使用 GHashTable 来追踪时区;键是时区 ID 字符串,值是某个 icaltimezone 结构体的字符串描述。
在 GIMP 中:
gimp-2.2.4/libgimp/gimp.c 使用 GHashTable 追踪临时的过程。在整个代码基(codebase)中唯一使用 g_hash_table_lookup_extended 的地方,它使用 g_hash_table_lookup_extended 调用来找到某个过程,以使得在删除那个过程 之前能首先释放散列键的内存。
gimp-2.2.4/app/core/gimp.c 使用 GHashTable 来保存图像;键是图像的 ID(一个整数),值是指向 GimpImage 结构体的指针。5、数组
5.1、概念到目前为止我们已经介绍了两类有序集合:GSList 和 GList。它们非常相似,因为都依赖于指针来从一个元素链接到下一个条目,或者,在 GList 中,链接 到前一个条目。不过,有另外一类不使用链接的有序集合;它的功能与 C 数组多少有些类似。
它叫做 GArray,提供一个具备索引的单一类型的有序集合,能够为了容纳新条目而增加大小。
相对于链表,数组有什么优势?一方面,索引访问。也就是说,如果想获得数组中的第十五个元素,只需要调用一个能够在常数时间内获取它的函数; 不需要手工地遍历到那个位置,那将是一个 O(n) 操作。数组知道自己的大小,所以查询其大小是一个 O(1) 操作而不是 O(n) 操作。5.2、基本操作
这里是向数组添加和删除数据的一些主要方法://ex-garray-1.c
#include <glib.h>
int main(int argc, char** argv) {
GArray* a = g_array_new(FALSE, FALSE, sizeof(char*));
char* first = "hello", *second = "there", *third = "world";
g_array_append_val(a, first);
g_array_append_val(a, second);
g_array_append_val(a, third);
printf("There are now %d items in the array\n", a->len);
printf("The first item is '%s'\n", g_array_index(a, char*, 0));
printf("The third item is '%s'\n", g_array_index(a, char*, 2));
g_array_remove_index(a, 1);
printf("There are now %d items in the array\n", a->len);
g_array_free(a, FALSE);
return 0;
}***** Output *****
There are now 3 items in the array
The first item is 'hello'
The third item is 'world'
There are now 2 items in the array需要考虑的几点:
在创建一个 GArray 时需要考虑一些选项。在上面的示例中,g_array_new 的前两个参数表明了数组是否要以零元素 作为终止符,是否数组中的新元素自动设置为零。第三个参数告诉数组它将要保存哪种类型的数据。在这个示例中要创建一个保存 char* 类型数据的数组;在数组中放入任何其他东西都会导致段错误(segfaults)。
g_array_append_val 是一个设计不能接受字面值(literal value)的宏,所以不能调用 g_array_append_val(a, 42)。而是那个值需要放入一个变量吕,然后将那个变量传递到 g_array_append_val 中。作为对这种不方便之处的一种慰藉,g_array_append_val 的速度非常快。
GArray 是具有一个成员变量 len 的结构体,所以为了获得数组的大小,只需要直接引用那个变量;不需要函数调用。
GArray 的大小以二幂次系数增长。也就是说,如果某个 GArray 包含四个条目,而且您又增加了另一个,那么在内部它会创建另一个拥有八个元素的 GArray,将 四个现有元素拷贝到它,然后添加新的元素。这个改变大小的过程需要时间,所以,如果知道将要有大量的元素,那么创建 GArray 时 预分配期望的大小会更有效。
5.3、更多 new/free 选项
本示例中包含创建和销毁 GArray 的一些不同方法://ex-garray-2.c
#include <glib.h>
int main(int argc, char** argv) {
GArray* a = g_array_sized_new(TRUE, TRUE, sizeof(int), 16);
printf("Array preallocation is hidden, so array size == %d\n", a->len);
printf("Array was init'd to zeros, so 3rd item is = %d\n", g_array_index(a, int, 2));
g_array_free(a, FALSE);// this creates an empty array, then resizes it to 16 elements
a = g_array_new(FALSE, FALSE, sizeof(char));
g_array_set_size(a, 16);
g_array_free(a, FALSE);a = g_array_new(FALSE, FALSE, sizeof(char));
char* x = g_strdup("hello world");
g_array_append_val(a, x);
g_array_free(a, TRUE);return 0;
}***** Output *****
Array preallocation is hidden, so array size == 0
Array was init'd to zeros, so 3rd item is = 0注意,由于 GArray 以二幂次系数增长,所有,将数组的大小设置为接近二的某次幂(比如十四)的值会令效率较低。不要那样,而是 直接使用最接近的二的某次幂。
5.4、添加数据的更多方法
到目前为止您已经看到如何使用 g_array_append_val 将数据添加到数组。 不过,有其他的方式可以将数据置入数组,如下所示://ex-garray-3.c
#include <glib.h>
void prt(GArray* a) {
printf("Array holds: ");
int i;
for (i = 0; i < a->len; i++)
printf("%d ", g_array_index(a, int, i));
printf("\n");
}
int main(int argc, char** argv) {
GArray* a = g_array_new(FALSE, FALSE, sizeof(int));
printf("Array is empty, so appending some values\n");
int x[2] = {4,5};
g_array_append_vals(a, &x, 2);
prt(a);
printf("Now to prepend some values\n");
int y[2] = {2,3};
g_array_prepend_vals(a, &y, 2);
prt(a);
printf("And one more prepend\n");
int z = 1;
g_array_prepend_val(a, z);
prt(a);
g_array_free(a, FALSE);
return 0;
}***** Output *****
Array is empty, so appending some values
Array holds: 4 5
Now to prepend some values
Array holds: 2 3 4 5
And one more prepend
Array holds: 1 2 3 4 5可以将多个值附加到数组,可以在最前添加一个值,也可以在最前添加多个值。不过,在向最前添加值的时候要小心; 它是一个 O(n) 操作,因为 GArray 必须要将当前所有值向后移动,为新的数据腾出空间。在附加或者向最前添加多个值时,还 需要使用变量,不过这是相当直观的,因为可以附加或者向最前添加整个数组。
5.5、插入数据
也可以向数组中各个不同的位置插入数据;不是限于只能附加或者向最前添加条目。 这里是其工作方式://ex-garray-4.c
#include <glib.h>
void prt(GArray* a) {
printf("Array holds: ");
int i;
for (i = 0; i < a->len; i++)
printf("%d ", g_array_index(a, int, i));
printf("\n");
}
int main(int argc, char** argv) {
GArray* a = g_array_new(FALSE, FALSE, sizeof(int));
int x[2] = {1,5};
g_array_append_vals(a, &x, 2);
prt(a);
printf("Inserting a '2'\n");
int b = 2;
g_array_insert_val(a, 1, b);
prt(a);
printf("Inserting multiple values\n");
int y[2] = {3,4};
g_array_insert_vals(a, 2, y, 2);
prt(a);
g_array_free(a, FALSE);
return 0;
}***** Output *****
Array holds: 1 5
Inserting a '2'
Array holds: 1 2 5
Inserting multiple values
Array holds: 1 2 3 4 5注意,这些插入函数涉及到了向后拷贝列表中当前元素,目的是为新条目准备空间,所以使用 g_array_insert_vals 比反复使用 g_array_insert_val 更好。
5.6、删除数据
有三种方法可以从 GArray 删除数据:
g_array_remove_index 和 g_array_remove_range, 这两个函数会保持现有顺序
g_array_remove_index_fast,不保持现有顺序
这里是所有三种方法的示例://ex-garray-5.c
#include <glib.h>
void prt(GArray* a) {
int i;
printf("Array holds: ");
for (i = 0; i < a->len; i++)
printf("%d ", g_array_index(a, int, i));
printf("\n");
}
int main(int argc, char** argv) {
GArray* a = g_array_new(FALSE, FALSE, sizeof(int));
int x[6] = {1,2,3,4,5,6};
g_array_append_vals(a, &x, 6);
prt(a);
printf("Removing the first item\n");
g_array_remove_index(a, 0);
prt(a);
printf("Removing the first two items\n");
g_array_remove_range(a, 0, 2);
prt(a);
printf("Removing the first item very quickly\n");
g_array_remove_index_fast(a, 0);
prt(a);
g_array_free(a, FALSE);
return 0;
}***** Output *****
Array holds: 1 2 3 4 5 6
Removing the first item
Array holds: 2 3 4 5 6
Removing the first two items
Array holds: 4 5 6
Removing the first item very quickly
Array holds: 6 5不只是您可能会对 g_array_remove_fast 的使用情形感到奇怪; 三个开放源代码的应用程序都没有使用这个函数。
5.7、排序
对 GArray 排序很直观;它使用的是在 GList 和 GSList 部分已经出现过的 GCompareFunc://ex-garray-6.c
#include <glib.h>
void prt(GArray* a) {
int i;
printf("Array holds: ");
for (i = 0; i < a->len; i++)
printf("%d ", g_array_index(a, int, i));
printf("\n");
}
int compare_ints(gpointer a, gpointer b) {
int* x = (int*)a;
int* y = (int*)b;
return *x - *y;
}
int main(int argc, char** argv) {
GArray* a = g_array_new(FALSE, FALSE, sizeof(int));
int x[6] = {2,1,6,5,4,3};
g_array_append_vals(a, &x, 6);
prt(a);
printf("Sorting\n");
g_array_sort(a, (GCompareFunc)compare_ints);
prt(a);
g_array_free(a, FALSE);
return 0;
}***** Output *****
Array holds: 2 1 6 5 4 3
Sorting
Array holds: 1 2 3 4 5 6注意,比较函数会得到指向数据条目的一个指针,所以在这种情况下,需要将它们强制类型转换为指向正确类型的指针,然后反引用那个指针, 得到实际的数据条目。GArray 还包括另一个排序函数,即 g_array_sort_with_data,它会接受指向另外一段数据的 一个指针。
顺便提一句,这三个示例应用程序都没有使用 g_array_sort 和 g_array_sort_with_data。 不过,无论如何,知道它们是可用的,都会有所帮助。
5.8、指针数组
GLib 还提供了 GPtrArray,这是一个为保存指针专门设计的数组。使用它比使用基本的 GArray 更简单,因为在创建或者添加、索引元素时不需要指定具体类型。它与 GArray 非常类似,所以我们将只是回顾基本操作的一些示例://ex-garray-7.c
#include <glib.h>
#include <stdio.h>
int main(int argc, char** argv) {
GPtrArray* a = g_ptr_array_new();
g_ptr_array_add(a, g_strdup("hello "));
g_ptr_array_add(a, g_strdup("again "));
g_ptr_array_add(a, g_strdup("there "));
g_ptr_array_add(a, g_strdup("world "));
g_ptr_array_add(a, g_strdup("\n"));
printf(">Here are the GPtrArray contents\n");
g_ptr_array_foreach(a, (GFunc)printf, NULL);
printf(">Removing the third item\n");
g_ptr_array_remove_index(a, 2);
g_ptr_array_foreach(a, (GFunc)printf, NULL);
printf(">Removing the second and third item\n");
g_ptr_array_remove_range(a, 1, 2);
g_ptr_array_foreach(a, (GFunc)printf, NULL);
printf("The first item is '%s'\n", g_ptr_array_index(a, 0));
g_ptr_array_free(a, TRUE);
return 0;
}***** Output *****
>Here are the GPtrArray contents
hello again there world
>Removing the third item
hello again world
>Removing the second and third item
hello
The first item is 'hello '可以了解如何使用只支持指针的数组编写更为直观的 API。这可能能够解释为什么在 Evolution 中使用 g_ptr_array_new 的 次数为 178 次,而 g_array_new 只使用了 45 次。大部分情况下只支持指针的数组就足够了!
5.9、字节数组
GLib 提供了另一个特定类型的数组是 GByteArray。它与您已经了解的类型非常类似,不过有一些区别,因为 它是为存储二进制数据而设计的。它非常便于在循环中读取二进制数据,因为它隐藏了“read into a buffer-resize buffer-read some more”的周期。 这里是一些示例代码://ex-garray-8.c
#include <glib.h>
int main(int argc, char** argv) {
GByteArray* a = g_byte_array_new();
guint8 x = 0xFF;
g_byte_array_append(a, &x, sizeof(x));
printf("The first byte value (in decimal) is %d\n", a->data[0]);
x = 0x01;
g_byte_array_prepend(a, &x, sizeof(x));
printf("After prepending, the first value is %d\n", a->data[0]);
g_byte_array_remove_index(a, 0);
printf("After removal, the first value is again %d\n", a->data[0]);
g_byte_array_append(g_byte_array_append(a, &x, sizeof(x)), &x, sizeof(x));
printf("After two appends, array length is %d\n", a->len);
g_byte_array_free(a, TRUE);
return 0;
}***** Output *****
The first byte value (in decimal) is 255
After prepending, the first value is 1
After removal, the first value is again 255
After two appends, array length is 3还可以发现,在这里使用了一个新的 GLib 类型:guint8。这是跨平台的 8-位 无符号整数,有益于在本例中 准确描述字节。
另外,在这里可以了解到 g_byte_array_append 如何返回 GByteArray。 所以,这就使得可以像编写方法链(method chaining)那样将一些附加动作嵌套起来。不过,嵌套多于两个或三个 可能并不是一个好办法,除非想让代码变更得 LISP 那样。5.10、实际应用
在示例应用程序中使用了各种不同的 GLib 数组类型,虽然不如已经接触的其他容器那样广泛。
Gaim 只使用了 GPtrArrays,而且只在一两种情形下使用到了。gaim-1.2.1/src/gtkpounce.c 使用一个 GPtrArray 来保持对一些 GUI 窗口小部件的 追踪,在发生各种事件(比如好友登录进来)时它们会被触发。
Evolution 所使用的大部分是 GPtrArrays,不过也使用了很多 GArrays 和 GByteArrays:
evolution-2.0.2/widgets/misc/e-filter-bar.h 在 GPtrArrays 中保持一些搜索过滤器的类型。
evolution-2.0.2/camel/providers/imap4/camel-imap4-store.c 使用 GPtrArray 来追踪 IMAP 文件夹中的条目; 它使用 g_ptr_array_sort 以及一个委派给 strcmp 的 GCompareFunc。
GIMP 使用了相当多的 GArray,只使用了很少 GPtrArrays 和 GByteArrays:
gimp-2.2.4/app/tools/gimptransformtool.c 使用 GArray 来追踪 GimpCoord 实例列表。
gimp-2.2.4/app/base/boundary.c 中,由点(points)填充起来的 GArray 是极好的 simplify_subdivide 函数 的一部分;递归传递的指向 GArray 的双重间接指针是边界简化(boundary simplification)例程的一部分。
6、树
6.1、概念
树 是另一个实用的容器。树中有一个可以拥有子节点的根节点,每个子节点可以有更多子节点,依此类推。
树结构体的示例包括文件系统或者电子邮件客户机;它其中有包含文件夹的文件夹,文件夹中可以有更多文件夹。另外,是否还记得 散列表部分的最后出现的多值示例?(例如,以字符串作为键,GList 作为值。)由于那些 GLish 值可以容纳更多 GHashTables, 那就成为 GHashTable 中构造树结构体的示例。相对于付出努力想使用树一样使用其他容器,使用 GTree 简单得多。
GLib 包括两种树结构:GTree(平衡二叉树 实现)以及 GNode(n-叉 树实现)。
二叉树的特殊属性是,树中每个节点拥有不超过两个子节点;平衡二叉树 表示元素以特定的次序保持,以进行更快速地搜索。 保持元素的平衡意味着删除和插入可能会较慢,因为树本身可能需要进行内部重新平衡,不过寻找某个条目是 O(log n) 操作。
相反,n-叉 树 节点可以有很多子节点。本教程主要关注二叉树,不过也会有一些 n-叉 树的示例。
6.2、树的基本操作
这里是在树中可以执行的一些基本操作://ex-gtree-1.c
#include <glib.h>
int main(int argc, char** argv) {
GTree* t = g_tree_new((GCompareFunc)g_ascii_strcasecmp);
g_tree_insert(t, "c", "Chicago");
printf("The tree height is %d because there's only one node\n", g_tree_height(t));
g_tree_insert(t, "b", "Boston");
g_tree_insert(t, "d", "Detroit");
printf("Height is %d since c is root; b and d are children\n", g_tree_height(t));
printf("There are %d nodes in the tree\n", g_tree_nnodes(t));
g_tree_remove(t, "d");
printf("After remove(), there are %d nodes in the tree\n", g_tree_nnodes(t));
g_tree_destroy(t);
return 0;
}***** Output *****
The tree height is 1 because there's only one node
Height is 2 since c is root; b and d are children
There are 3 nodes in the tree
After remove(), there are 2 nodes in the tree关于代码的一些注解:
可以看到,GTree 中的每一个节点都包含一个 键-值 对。键用来确保树的平衡、节点插入到适当的位置,并确保值的指针指向应该追踪的 “payload”。
必须为 g_tree_new 提供一个 GCompareFunc,以使得 GTree 知道如何对键进行比较。这可以是一个内置函数,如上所示,或者可以自己编写。
树“高度(height)”只是从顶到底(包括顶和底)节点的数目。要执行这个函数,GTree 必须从它的根开始向下移动直到到达某个叶子节点。 g_tree_nnodes 更为复杂;它要完全遍历整棵树。
6.3、替换和提取
在前面的 GHashTable 部分已经看到了 replace 和 steal 函数名, 关于 GTree 的函数也是如此。g_tree_replace 会同时替换一个 GTree 条目的键和值,不同于 g_tree_insert,如果要插入的键是重复的,则它只是将值替换。不需要调用任何 GDestroyNotify 函数,g_tree_steal 就可以删除一个节点。 这里是一个示例://ex-gtree-2.c
#include <glib.h>
void key_d(gpointer data) {
printf("Key %s destroyed\n", data);
}
void value_d(gpointer data) {
printf("Value %s destroyed\n", data);
}
int main(int argc, char** argv) {
GTree* t = g_tree_new_full((GCompareDataFunc)g_ascii_strcasecmp,
NULL, (GDestroyNotify)key_d, (GDestroyNotify)value_d);
g_tree_insert(t, "c", "Chicago");
g_tree_insert(t, "b", "Boston");
g_tree_insert(t, "d", "Detroit");
printf(">Replacing 'b', should get destroy callbacks\n");
g_tree_replace(t, "b", "Billings");
printf(">Stealing 'b', no destroy notifications will occur\n");
g_tree_steal(t, "b");
printf(">Destroying entire tree now\n");
g_tree_destroy(t);
return 0;
}***** Output *****
>Replacing 'b', should get destroy callbacks
Value Boston destroyed
Key b destroyed
>Stealing 'b', no destroy notifications will occur
>Destroying entire tree now
Key d destroyed
Value Detroit destroyed
Key c destroyed
Value Chicago destroyed在这个示例中,使用 g_tree_new_full 创建了一个 GTree;与 GHashTable 类似,可以注册键或值的销毁 的任意组合的通知。g_tree_new_full 的第二个参数可以包含传递给 GCompareFunc 的数据,不过在此并不需要。
6.4、查找数据
GTree 具备只查找键或者同时查找键和值的方法。这与在 GHashTable 部分中接触到的非常类似;有一个 lookup 以及一个 lookup_extended。这里是一个示例://ex-gtree-3.c
#include <glib.h>
int main(int argc, char** argv) {
GTree* t = g_tree_new((GCompareFunc)g_ascii_strcasecmp);
g_tree_insert(t, "c", "Chicago");
g_tree_insert(t, "b", "Boston");
g_tree_insert(t, "d", "Detroit");
printf("The data at 'b' is %s\n", g_tree_lookup(t, "b"));
printf("%s\n", g_tree_lookup(t, "a") ? "My goodness!" : "As expected, couldn't find 'a'");gpointer* key = NULL;
gpointer* value = NULL;
g_tree_lookup_extended(t, "c", (gpointer*)&key, (gpointer*)&value);
printf("The data at '%s' is %s\n", key, value);
gboolean found = g_tree_lookup_extended(t, "a", (gpointer*)&key, (gpointer*)&value);
printf("%s\n", found ? "My goodness!" : "As expected, couldn't find 'a'");g_tree_destroy(t);
return 0;
}***** Output *****
The data at 'b' is Boston
As expected, couldn't find 'a'
The data at 'c' is Chicago
As expected, couldn't find 'a'这里再次用到了双重间接指针技术。由于 g_tree_lookup_extended 需要提供多个值, 所以它接受两个指向指针的指针(一个指向键,另一个指向值)。注意,如果 g_tree_lookup 找不到键,则它返回一个 NULL gpointer,而如果 g_tree_lookup_extended 不能找到目标,则它返回一个 FALSE gboolean 值。
6.5、使用 foreach 列出树
GTree 提供了一个 g_tree_foreach 函数,用来以有序的顺序遍历整棵对。这里是一个示例://ex-gtree-4.c
#include <glib.h>
gboolean iter_all(gpointer key, gpointer value, gpointer data) {
printf("%s, %s\n", key, value);
return FALSE;
}
gboolean iter_some(gpointer key, gpointer value, gpointer data) {
printf("%s, %s\n", key, value);
return g_ascii_strcasecmp(key, "b") == 0;
}
int main(int argc, char** argv) {
GTree* t = g_tree_new((GCompareFunc)g_ascii_strcasecmp);
g_tree_insert(t, "d", "Detroit");
g_tree_insert(t, "a", "Atlanta");
g_tree_insert(t, "c", "Chicago");
g_tree_insert(t, "b", "Boston");
printf("Iterating all nodes\n");
g_tree_foreach(t, (GTraverseFunc)iter_all, NULL);
printf("Iterating some of the nodes\n");
g_tree_foreach(t, (GTraverseFunc)iter_some, NULL);
g_tree_destroy(t);
return 0;
}***** Output *****
Iterating all nodes
a, Atlanta
b, Boston
c, Chicago
d, Detroit
Iterating some of the nodes
a, Atlanta
b, Boston注意,当 iter_some 返回 TRUE 时,遍历停止。这就使得 g_tree_foreach 可用来搜索到某个点,累加匹配某个条件前 10 个条目,或者此类事情。 当然,令 GTraverseFunc 返回 FALSE,可以一点不差地遍历整棵树。
另外,要注意在使用 g_tree_foreach 遍历树时不应该修改它。
有一个废弃的函数,即 g_tree_traverse,它本意是提供遍历树的另一种方法。例如,可以 后序(post order) 访问节点,也就是自底向上访问树。不过,这个函数从 2001 年起就废弃了,从而 GTree 文档 建议在所有使用它的地方替换为使用 g_tree_foreach 或者 n-叉 树。这里所研究的开放源代码的 应用程序都没有使用它,很不错。
6.6、搜索
可以使用 g_tree_foreach 搜索条目,如果知道键,可以使用 g_tree_lookup。 不过,要进行更复杂地搜索,可以使用 g_tree_search 函数。这里是其工作方式://ex-gtree-5.c
#include <glib.h>
gint finder(gpointer key, gpointer user_data) {
int len = strlen((char*)key);
if (len == 3) {
return 0;
}
return (len < 3) ? 1 : -1;
}
int main(int argc, char** argv) {
GTree* t = g_tree_new((GCompareFunc)g_ascii_strcasecmp);
g_tree_insert(t, "dddd", "Detroit");
g_tree_insert(t, "a", "Annandale");
g_tree_insert(t, "ccc", "Cleveland");
g_tree_insert(t, "bb", "Boston");
gpointer value = g_tree_search(t, (GCompareFunc)finder, NULL);
printf("Located value %s; its key is 3 characters long\n", value);
g_tree_destroy(t);
return 0;
}***** Output *****
Located value Cleveland; its key is 3 characters long
注意,传递到 g_tree_search 的 GCompareFunc 实际上决定了搜索如何进行, 根据搜索的方式返回 0、1 或者 -1。这个函数甚至可以在搜索进行的时候改变条件;Evolution 在使用 g_tree_search 管理其内存使用时就这样做了。
6.7、不只是二叉:n-叉 树
GLib n-叉 树实现基于 GNode 数据结构;以前所述,它允许每个父节点有多个子节点。 好像很少会用到它,不过,完整起见,这里给出一个用法示例://ex-gtree-6.c
#include <glib.h>
gboolean iter(GNode* n, gpointer data) {
printf("%s ", n->data);
return FALSE;
}
int main(int argc, char** argv) {
GNode* root = g_node_new("Atlanta");
g_node_append(root, g_node_new("Detroit"));
GNode* portland = g_node_prepend(root, g_node_new("Portland"));
printf(">Some cities to start with\n");
g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, iter, NULL);
printf("\n>Inserting Coos Bay before Portland\n");
g_node_insert_data_before(root, portland, "Coos Bay");
g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, iter, NULL);
printf("\n>Reversing the child nodes\n");
g_node_reverse_children(root);
g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, iter, NULL);
printf("\n>Root node is %s\n", g_node_get_root(portland)->data);
printf(">Portland node index is %d\n", g_node_child_index(root, "Portland"));
g_node_destroy(root);
return 0;
}***** Output *****
>Some cities to start with
Atlanta Portland Detroit
>Inserting Coos Bay before Portland
Atlanta Coos Bay Portland Detroit
>Reversing the child nodes
Atlanta Detroit Portland Coos Bay
>Root node is Atlanta
>Portland node index is 1可见,GNode 允许将节点放置几乎任何地方;可以在合适时访问它们。 这非常灵活,不过它可能会过于灵活,以至于确定其实际使用情形有些困难。实际上,在此所研究的三个开放 源代码的应用程序都没有使用它!
6.8、实际应用
GTree 是一个复杂的结构体,其应用不如我们前面研究其他容器那样广泛。Gaim 根本没有使用它。 不过,GIMP 和 Evolution 用到了一些。
GIMP:
gimp-2.2.4/app/menus/plug-in-menus.c 使用 GTree 来保持插件菜单条目。它使用 g_tree_foreach 和 一个定制的 GTraverseFunc 来遍历 GTree,向 GimpUIManager 添加插件程序。它使用标准的 C 程序库函数 strcmp 作为自己的 GCompareData 函数。
gimp-2.2.4/plug-ins/script-fu/script-fu-scripts.c 使用 GTree 来保存“script-fu”脚本。GTree 中的每一个值实际上都是由脚本构成的 GList。
Evolution 的 evolution-2.0.2/e-util/e-memory.c 使用 GTree 作为计算未使用内存块的算法的一部分。它使用了一个定制的 GCompareFunc,即 tree_compare,来对 _cleaninfo 结构体进行排序, 这些结构体指向的是空闲的内存块。
7、队列
7.1、概念
队列是另一个便利的数据结构。一个 队列 会保存一列条目,而且访问形式通常是向最后添加条目,从最前删除条目。 当需要按到达顺序进行处理时,这很有实用。标准队列的一个变种是“双端队列(double-ended queue)”,或者说是 dequeue, 它支持在队列的两端进行添加或者删除。
不过,在很多情况下最好避免使用队列。队列搜索不是特别快(是 O(n) 操作),所以,如果需要经常进行搜索,那么散列表或者树 可能更实用。这同样适用于需要访问队列中随机元素的情形;如果是那样,那么将会对队列进行很多次线性扫描。
GLib 提供了一个使用 GQueue 的 dequeue 实现;它支持标准队列操作。它的基础是双向链表(GList), 所以它也支持很多其他操作,比如在队列之中进行插入和删除。不过,如果您发现自己经常要使用这些功能,那么可能需要重新考虑容器的选择; 或许另一个容器更为合适。7.2、基本操作
这里是以“排队买票(ticket line)”为模型的一些基本的 GQueue 操作://ex-gqueue-1.c
#include <glib.h>
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
printf("Is the queue empty? %s, adding folks\n", g_queue_is_empty(q) ? "Yes" : "No");
g_queue_push_tail(q, "Alice");
g_queue_push_tail(q, "Bob");
g_queue_push_tail(q, "Fred");
printf("First in line is %s\n", g_queue_peek_head(q));
printf("Last in line is %s\n", g_queue_peek_tail(q));
printf("The queue is %d people long\n", g_queue_get_length(q));
printf("%s just bought a ticket\n", g_queue_pop_head(q));
printf("Now %s is first in line\n", g_queue_peek_head(q));
printf("Someone's cutting to the front of the line\n");
g_queue_push_head(q, "Big Jim");
printf("Now %s is first in line\n", g_queue_peek_head(q));
g_queue_free(q);
return 0;
}***** Output *****
Is the queue empty? Yes, adding folks
First in line is Alice
Last in line is Fred
The queue is 3 people long
Alice just bought a ticket
Now Bob is first in line
Someone's cutting to the front of the line
Now Big Jim is first in line大部分方法名称都是完全自我描述的,不过有一些更细致之处:
向队列压入和取出条目的各种操作不返回任何内容,所以,为了使用队列,您需要保持 g_queue_new 返回的 指针。
队列的两端都可以用于添加和删除。如果要模拟排队买票时排在后面的人离开转到另一个队列去购买,也是完全可行的。
有非破坏性的 peek 操作可以检查队列头或尾的条目。
g_queue_free 不接受帮助释放每个条目的函数,所以需要手工去完成;这与 GSList 相同。
7.3、删除和插入条目
虽然通常只通过在队列的末端 添加/删除 条目来修改它,但 GQueue 允许删除任意条目以及在任意位置插入条目。这里是其示例://ex-gqueue-2.c
#include <glib.h>
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
g_queue_push_tail(q, "Alice");
g_queue_push_tail(q, "Bob");
g_queue_push_tail(q, "Fred");
printf("Queue is Alice, Bob, and Fred; removing Bob\n");
int fred_pos = g_queue_index(q, "Fred");
g_queue_remove(q, "Bob");
printf("Fred moved from %d to %d\n", fred_pos, g_queue_index(q, "Fred"));
printf("Bill is cutting in line\n");
GList* fred_ptr = g_queue_peek_tail_link(q);
g_queue_insert_before(q, fred_ptr, "Bill");
printf("Middle person is now %s\n", g_queue_peek_nth(q, 1));
printf("%s is still at the end\n", g_queue_peek_tail(q));
g_queue_free(q);
return 0;
}***** Output *****
Queue is Alice, Bob, and Fred; removing Bob
Fred moved from 2 to 1
Bill is cutting in line
Middle person is now Bill
Fred is still at the end有很多新函数:
g_queue_index 在队列中扫描某个条目并返回其索引;如果它不能找到那个条目,则返回 -1。
为了向队列的中间插入一个新条目,需要一个指向希望插入位置的指针。如您所见,通过调用一个“peek link”函数,就可以进行此处理; 这些函数包括:g_queue_peek_tail_link、g_queue_peek_head_link 以及 g_queue_peek_nth_link,它们会返回一个 GList。然后可以将一个条目插入到 GList 之前或者之后。
g_queue_remove 允许从队列中的任何位置删除某个条目。继续使用“排队买票”模型,这表示人们可以离开队列; 他们组成队列后并不固定在其中。
7.4、查找条目
在先前的示例中已经看到,在拥有一个指向条目数据的指针或者知道其索引的条件下如何去得到它。不过,类似其他 GLib 容器, GQueue 也包括一些查找函数:g_queue_find 和 g_queue_find_custom://ex-gqueue-3.c
#include <glib.h>
gint finder(gpointer a, gpointer b) {
return strcmp(a,b);
}
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
g_queue_push_tail(q, "Alice");
g_queue_push_tail(q, "Bob");
g_queue_push_tail(q, "Fred");
g_queue_push_tail(q, "Jim");
GList* fred_link = g_queue_find(q, "Fred");
printf("The fred node indeed contains %s\n", fred_link->data);
GList* joe_link = g_queue_find(q, "Joe");
printf("Finding 'Joe' yields a %s link\n", joe_link ? "good" : "null");
GList* bob = g_queue_find_custom(q, "Bob", (GCompareFunc)finder);
printf("Custom finder found %s\n", bob->data);
bob = g_queue_find_custom(q, "Bob", (GCompareFunc)g_ascii_strcasecmp);
printf("g_ascii_strcasecmp also found %s\n", bob->data);
g_queue_free(q);
return 0;
}***** Output *****
The fred node indeed contains Fred
Finding 'Joe' yields a null link
Custom finder found Bob
g_ascii_strcasecmp also found Bob注意,如果 g_queue_find 找不到条目,则它会返回 null。并且可以在上面的示例中传递一个库函数(比如 g_ascii_strcasecmp)或者一个定制的函数(比如 finder)作为 g_queue_find_custom 的 GCompareFunc 参数。
7.5、使用队列:拷贝、反转和遍历每一个(foreach)
由于 GQueue 的基础是 GList,所以它支持一些列表处理操作。这里是如何使用 g_queue_copy、 g_queue_reverse 和 g_queue_foreach 的示例://ex-gqueue-4.c
#include <glib.h>
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
g_queue_push_tail(q, "Alice ");
g_queue_push_tail(q, "Bob ");
g_queue_push_tail(q, "Fred ");
printf("Starting out, the queue is: ");
g_queue_foreach(q, (GFunc)printf, NULL);
g_queue_reverse(q);
printf("\nAfter reversal, it's: ");
g_queue_foreach(q, (GFunc)printf, NULL);
GQueue* new_q = g_queue_copy(q);
g_queue_reverse(new_q);
printf("\nNewly copied and re-reversed queue is: ");
g_queue_foreach(new_q, (GFunc)printf, NULL);
g_queue_free(q);
g_queue_free(new_q);
return 0;
}***** Output *****
Starting out, the queue is: Alice Bob Fred
After reversal, it's: Fred Bob Alice
Newly copied and re-reversed queue is: Alice Bob Fredg_queue_reverse 和 g_queue_foreach 很直观; 您已经看到它们在各种其他有序集合中得到了应用。不过,使用 g_queue_copy 时需要稍加留心, 因为拷贝的是指针而不是数据。所以,当释放数据时,一定不要进行重复释放。
7.6、使用链接的更多乐趣
已经了解了链接的一些示例;这里是一些便利的链接删除函数。不要忘记 GQueue 中的每个条目实际上是都是一个 GList 结构体, 数据存储在“data”成员中://ex-gqueue-5.c
#include <glib.h>
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
g_queue_push_tail(q, "Alice ");
g_queue_push_tail(q, "Bob ");
g_queue_push_tail(q, "Fred ");
g_queue_push_tail(q, "Jim ");
printf("Starting out, the queue is: ");
g_queue_foreach(q, (GFunc)printf, NULL);
GList* fred_link = g_queue_peek_nth_link(q, 2);
printf("\nThe link at index 2 contains %s\n", fred_link->data);
g_queue_unlink(q, fred_link);
g_list_free(fred_link);
GList* jim_link = g_queue_peek_nth_link(q, 2);
printf("Now index 2 contains %s\n", jim_link->data);
g_queue_delete_link(q, jim_link);
printf("Now the queue is: ");
g_queue_foreach(q, (GFunc)printf, NULL);
g_queue_free(q);
return 0;
}***** Output *****
Starting out, the queue is: Alice Bob Fred Jim
The link at index 2 contains Fred
Now index 2 contains Jim
Now the queue is: Alice Bob注意,g_queue_unlink 并不释放没有被链接的 GList 结构体,所以需要自己去完成。 并且,由于它是一个 GList 结构体,所以需要使用 g_list_free 函数来释放它 —— 而不是 简单的 g_free 函数。当然,更简单的是调用 g_queue_delete_link 并让它为您释放内存。
7.7、排序
队列排序好像不太常见,不过由于各种其他链表操作都得到了支持(比如 insert 和 remove),所以此操作也得到了支持。如果恰巧您希望重新对队列进行排序,将高优先级 的条目移动到前端,那么这也会很便利。这里是一个示例://ex-gqueue-6.c
#include <glib.h>
typedef struct {
char* name;
int priority;
} Task;
Task* make_task(char* name, int priority) {
Task* t = g_new(Task, 1);
t->name = name;
t->priority = priority;
return t;
}
void prt(gpointer item) {
printf("%s ", ((Task*)item)->name);
}
gint sorter(gconstpointer a, gconstpointer b, gpointer data) {
return ((Task*)a)->priority - ((Task*)b)->priority;
}
int main(int argc, char** argv) {
GQueue* q = g_queue_new();
g_queue_push_tail(q, make_task("Reboot server", 2));
g_queue_push_tail(q, make_task("Pull cable", 2));
g_queue_push_tail(q, make_task("Nethack", 1));
g_queue_push_tail(q, make_task("New monitor", 3));
printf("Original queue: ");
g_queue_foreach(q, (GFunc)prt, NULL);
g_queue_sort(q, (GCompareDataFunc)sorter, NULL);
printf("\nSorted queue: ");
g_queue_foreach(q, (GFunc)prt, NULL);
g_queue_free(q);
return 0;
}***** Output *****
Original queue: Reboot server Pull cable Nethack New monitor
Sorted queue: Nethack Reboot server Pull cable New monitor现在您就拥有了一个模拟您的工作的 GQueue,偶尔还可以对它进行排序,可以欣喜地发现,Nethack 被提升到了其正确的位置,到了队列的 最前端!
7.8、实际应用
GQueue 没有在 Evolution 中得到应用,但是 GIMP 和 Gaim 用到了它。
GIMP:
gimp-2.2.4/app/core/gimpimage-contiguous-region.c 在一个查找相邻片段的工具函数中使用 GQueue 存储一系列坐标。 只要片段保存邻接,新的点就会被压入到队列末端,然后在下一个循环迭代中取出并被检查。
gimp-2.2.4/app/vectors/gimpvectors-import.c 使用 GQueue 作为 Scalable Vector Graphics(SVG)解析器的一部分。 它被当做栈使用,条目的压入和取出都在队列的头上进行。
Gaim:
gaim-1.2.1/src/protocols/msn/switchboard.c 使用 GQueue 来追踪发出的消息。新的消息压入到队列的尾部,当发送后从头部取出。
gaim-1.2.1/src/proxy.c 使用 GQueue 追踪 DNS 查找请求。它使用队列作为应用程序代码与 DNS 子进程之间的临时保存区域。
8、关系
8.1、概念
GRelation 类似一张简单的数据库表;它包含一系列记录,或者 元组(tuples),每一个包含某干个域。 每个元组必须拥有相同数目的域,可以为任意的域指定索引,以支持对那个域进行查找。
作为示例,可以使用一系列元组来保存名字,一个域中保存名,第二个域中保存姓。两个域都可以被索引,以使得使用名或者姓都 可以进行快速查找。
GRelation 有一个缺点,那就是每个元组最多只能包含两个域。因此,不能将它作为内存中的数据库表缓存,除非表中列非常少。 我在 gtk-app-devel-list 邮件列表中搜索关于此问题的注解,发现早在 2000 年 2 月讨论到了一个补丁, 它可以将此扩展到四个域,但好像它从来没有加入到发行版本中。
GRelation 好像是一个鲜为人知的结构体;本教程中研究的开放源代码的应用程序当前都没有使用它。在 Web 上浏览时发现了一个开放源代码的 电子邮件客户机(Sylpheed-claws),出于各种不同目的使用了它,包括追踪 IMAP 文件夹和消息线程。所有它需要的可能只是一些宣传!
8.2、基本操作
这里是一个示例,创建一个具有两个索引域的新的 GRelation,然后插入一些记录并执行一些基本的信息查询://ex-grelation-1.c
#include <glib.h>
int main(int argc, char** argv) {
GRelation* r = g_relation_new(2);
g_relation_index(r, 0, g_str_hash, g_str_equal);
g_relation_index(r, 1, g_str_hash, g_str_equal);
g_relation_insert(r, "Virginia", "Richmond");
g_relation_insert(r, "New Jersey", "Trenton");
g_relation_insert(r, "New York", "Albany");
g_relation_insert(r, "Virginia", "Farmville");
g_relation_insert(r, "Wisconsin", "Madison");
g_relation_insert(r, "Virginia", "Keysville");
gboolean found = g_relation_exists(r, "New York", "Albany");
printf("New York %s found in the relation\n", found ? "was" : "was not");
gint count = g_relation_count(r, "Virginia", 0);
printf("Virginia appears in the relation %d times\n", count);
g_relation_destroy(r);
return 0;
}***** Output *****
New York was found in the relation
Virginia appears in the relation 3 times注意,索引恰好是在调用 g_relation_new 之后而在调用 g_relation_insert 之前 添加的。这是因为 g_relation_count 等其他 GRelation 函数要依赖现有的索引,如果索引不存在,则在运行时 会出错。
上面的代码中包括一个 g_relation_exists,用来查看“New York”是否在 GRelation 中。 这个请求会精确匹配关系中的每一个域;可以在任意一个索引的域上使用 g_relation_count 进行匹配。
在前面的 GHashTable 部分已经接触过 g_str_hash 和 g_str_equal; 在这里使用它们来对 GRelation 中的索引域进行快速查找。
选择元组
数据存入 GRelation 中后,可以使用 g_relation_select 函数来取出它。 结果是一个指向 GTuples 结构体的指针,通过它进一步查询可以获得实际的数据。 这里是它的使用方法://ex-grelation-2.c
#include <glib.h>
int main(int argc, char** argv) {
GRelation* r = g_relation_new(2);
g_relation_index(r, 0, g_str_hash, g_str_equal);
g_relation_index(r, 1, g_str_hash, g_str_equal);
g_relation_insert(r, "Virginia", "Richmond");
g_relation_insert(r, "New Jersey", "Trenton");
g_relation_insert(r, "New York", "Albany");
g_relation_insert(r, "Virginia", "Farmville");
g_relation_insert(r, "Wisconsin", "Madison");
g_relation_insert(r, "Virginia", "Keysville");
GTuples* t = g_relation_select(r, "Virginia", 0);
printf("Some cities in Virginia:\n");
int i;
for (i=0; i < t->len; i++) {
printf("%d) %s\n", i, g_tuples_index(t, i, 1));
}
g_tuples_destroy(t);
t = g_relation_select(r, "Vermont", 0);
printf("Number of Vermont cities in the GRelation: %d\n", t->len);
g_tuples_destroy(t);
g_relation_destroy(r);
return 0;
}***** Output *****
Some cities in Virginia:
0) Farmville
1) Keysville
2) Richmond
Number of Vermont cities in the GRelation: 0关于选择和遍历元组的一些注解:
g_relation_select 返回的 GTuples 结构体中的记录没有特定的次序。 要找出返回了多少记录,请使用 GTuple 结构体中的 len 成员。
g_tuples_index 接受三个参数:
GTuple 结构体
正在查询的记录的索引
希望获得的域的索引
注意,需要调用 g_tuples_destroy 来正确地释放在 g_relation_select 期间所 分配的内存。就算是记录实际上并没有被 GTuples 对象引用,这也是有效的。
9、对核心应用的支持
GLib对核心应用的支持包括事件循环、内存操作、线程操作、动态链接库的操作和出错处理与日志等。
9.1、事件循环、内存操作、线程
下面代码演示了事件循环、内存操作、线程这三种功能的简单应用:#include <glib.h>
static GMutex *mutex = NULL;
static gboolean t1_end = FALSE;
static gboolean t2_end = FALSE;
typedef struct _Arg Arg;
struct _Arg
{
GMainLoop* loop;
gint max;
};
void run_1(Arg *arg)
{
int i ;
for(i=0; i<arg->max; i++)
{
if(g_mutex_trylock(mutex) == FALSE)
{
//g_print("%d : thread 2 locked the mutex \n", i);
g_print("%d :线程2锁定了互斥对象\n", i);
g_mutex_unlock(mutex);
}
else
{
g_usleep(10);
}
}
t1_end = TRUE;
}
void run_2(Arg *arg)
{
int i;
for(i=0; i<arg->max; i++)
{
if(g_mutex_trylock(mutex) == FALSE)
{
//g_print("%d : thread 1 locked mutex \n", i);
g_print("%d :线程1锁定了互斥对象\n", i);
g_mutex_unlock(mutex);
}
else
{
g_usleep(10);
}
}
t2_end = TRUE;
}
void run_3(Arg *arg)
{
for(;;)
{
if(t1_end && t2_end)
{
g_main_loop_quit(arg->loop);
break;
}
}
}
int main(int argc, char *argv[])
{
GMainLoop *mloop;
Arg *arg;
if(!g_thread_supported())
g_thread_init(NULL);
mloop = g_main_loop_new(NULL, FALSE);
arg = g_new(Arg, 1);
arg->loop = mloop;
arg->max = 11;
mutex = g_mutex_new();
g_thread_create(run_1, arg, TRUE, NULL);
g_thread_create(run_2, arg, TRUE, NULL);
g_thread_create(run_3, arg, TRUE, NULL);
g_main_loop_run(mloop);
g_print("线程3退出事件循环\n");
g_mutex_free(mutex);
g_print("释放互斥对象\n");
g_free(arg);
g_print("释放参数所用的内存\n");
}Makefile文件如下:
CC = gcc
all:
$(CC) `pkg-config --cflags --libs glib-2.0 gthread-2.0` loop.c -o loop
下面为输出结果:
0 :线程1锁定了互斥对象
1 :线程2锁定了互斥对象
2 :线程1锁定了互斥对象
3 :线程2锁定了互斥对象
4 :线程1锁定了互斥对象
5 :线程2锁定了互斥对象
6 :线程1锁定了互斥对象
7 :线程2锁定了互斥对象
8 :线程1锁定了互斥对象
9 :线程2锁定了互斥对象
10 :线程1锁定了互斥对象
线程3退出事件循环
释放互斥对象
释放参数所用的内存
以上例程创建了三个线程,其中run_1和run_2操作互斥对象,run_3检索前两个线程是否结束,如结束的话,则执行g_main_loop_quit退出事件循环。由于线程的运行是不确定的,所以不一定每次都是这一输出结果。
首 先定义一个结构类型来保存创建的事件循环的对象指针和线程运行时的最多循环次数,一般情况下,如果为此数据结构来分配内存的话,用Arg *arg = (Arg *)malloc(sizeof(Arg));,释放时用free(arg);,这种传统的做法曾经让很多C语言的初学者头痛,尤其是需要多次操作的时 候,GLib中提供了类似的函数g_malloc和g_free,最好用的方法是其将g_malloc函数封装成了宏g_new,这个宏有两个参数,第一 个是结构类型,第二个是要分配结构的数量,这段代码中只用到了一个Arg数据结构,所以是g_new(Arg, 1)。在程序结束时用g_free来释放。
在线程初始化时,首先是判断线程是否初始化的函数g_thread_supported,如果其返回FALSE则表明线程并未初始化,这时必须用g_thread_init来初始化,这是较明智的做法。
事 件循环GMainLoop在用g_main_loop_new创建之后并不马上运行,用g_main_loop_run运行后,还要用 g_main_loop_quit退出,否则循环将一直运行下去,这两个函数的参数都是GMainLoop型的指针,在主函数中并未直接运行 g_main_loop_quit,而是把它放在线程的函数中了,这一点需读者注意。9.2、随机数
GLib中包含了近二十种实用功能,从简单的字符处理到初学者很难理解的XML解析功能,这里介绍两种较简单的:随机数和计时。
下面代码演示如何产生1-100之间的随机整数和演示如何计算30000000次累加在计算时用的时间:/* until.c 用来测试实用功能 */
#include <glib.h>
int main(int argc, char *argv[])
{
GRand *rand;
GTimer *timer;
gint n;
gint i, j;
gint x = 0;
rand = g_rand_new(); //创建随机数对象
for(n=0; n<20; n++)
{ //产生随机数并显示出来
g_print("%d\t",g_rand_int_range(rand,1,100));
}
g_print("\n");
g_rand_free(rand); //释放随机数对象
//创建计时器
timer = g_timer_new();
g_timer_start(timer);//开始计时
for(i=0; i<10000; i++)
for(j=0; j<3000; j++)
x++;//累计
g_timer_stop(timer);//计时结束
//输出计时结果
g_print("%ld\tall:%.2f seconds was used!\n",x,g_timer_elapsed(timer,NULL));
}Makefile文件内容如下:
CC = gcc all: $(CC) `pkg-config --cflags --libs glib-2.0 ` until.c -o until
输出结果:
48 95 95 99 90 24 90 29 78 4 53 87 1 86 7 93 57 88 75 4
30000000 all:1.47 seconds was used!
GLIB中的每个对象几乎都有一个或多个*_new函数来创建,计时器GTimer和随机器GRand也一样,也都有相对应的函数来结束对象的使用,如GTimer的g_timer_stop和GRand的g_rand_free。
这可能是GLIB实用功能中最简单的两种了,许多朋友会一目了然。我们还应注意到GLIB的代码风格和封装技巧是具有独到之处的,这种风格和技巧足以让一些自称简洁实用的SDK汗颜,学习掌握这一风格可能会让我们受益匪浅。
数据类型
GLib中定义了十几种常用的数据结构类型和它们的相关操作函数,下面是关于字符串类型的简单示例:#include <glib.h>
int main(int argc, char *argv[])
{
GString *s;
s = g_string_new("Hello");
g_print("%s\n", s->str);
s = g_string_append(s," World!");
g_print("%s\n",s->str);
s = g_string_erase(s,0,6);
g_print("%s\n",s->str);
s = g_string_prepend(s,"Also a ");
g_print("%s\n",s->str);
s = g_string_insert(s,6," Nice");
g_print("%s\n",s->str);
}Makefile文件如下:
CC = gcc
all:
$(CC) `pkg-config --cflags --libs glib-2.0 ` string.c -o str
下面是输出结果:
Hello
Hello World!
World!
Also a World!
Also a Nice World!
字符串在编程中出现频率之高,即使是初学者也很清楚,追加、删除和插入等常用操作理解后,还可以进一步了解掌握其它更复杂的操作。
GLib提供了一种内存块(GMemChunk)数据类型,它为分配等大的内存区提供了一种非常好用的操作方式,下面程序演示了内存块数据类型的简单用法:#include <glib.h>
int main(int argc, char *argv[])
{
GMemChunk *chunk; //定义内存块
gchar *mem[10]; //定义指向原子的指针数组
gint i, j;
//创建内存块
chunk = g_mem_chunk_new("Test MemChunk", 5, 50, G_ALLOC_AND_FREE);
//名称,原子的长度, 内存块的长度,类型
for(i=0; i<10; i++)
{
//创建对象
//mem[i] = g_chunk_new(gchar, chunk);
mem[i] = (gchar*)g_mem_chunk_alloc(chunk);
for(j=0; j<5; j++)
{
mem[i][j] = 'A' + j;//为内存块中的指针赋值
}
}
g_mem_chunk_print(chunk); //显示内存块信息
for(i=0; i<10; i++)
{
g_print("%s\t",mem[i]);//显示内存块中的内容
}
for(i=0; i<10; i++)
{
g_mem_chunk_free(chunk,mem[i]); //释放所有分配的内存
}
g_mem_chunk_destroy(chunk);
}Makefile文件件如下:
CC = gcc
all:
$(CC) `pkg-config --cflags --libs glib-2.0` data1.c -o data1
以下为输出结果:
GLib-INFO: Test MemChunk: 80 bytes using 2 mem areas
ABCDE ABCDE ABCDE ABCDE ABCDE ABCDE ABCDE ABCDE ABCDE ABCDE这里说明这一数据类型的原因是通过它可以他细体会内存分配这一运行时处理环节的应用之妙。
我们在程序中分配的是50字节的空间,而实际用的是80字节,由此可以看出其在分配内存时本身用到了部分内存空间。
从 上面的示例代码中可以看出,在GLib中几乎所有的对象都是C语言的结构类型,一般命名以大写字母G开头的单词,如GList表示双向链表,所有与之相关 的操作函数都以小写的字母g加下划线加小写的单词加下划线开头,如以g_list_*开头的函数都是与这相关的操作函数,而且这些函数中的第一个参数多数 是此对象的指针。
GLIB中的数据类型在GLIB本身,尤其是GTK+中频繁用到,了解掌握这些数据类据类型的用法是非常必要的,这对进一步灵活开发GTK+程序来说是关键一环,而且是对大学中的《数据结构》一科的很好回顾。
在下一篇GOBJECT对象系统中将详细介绍GLIB中最重要的组成部分GOBJECT系统,希望这一由C语言组建的单继承的对象系统会帮助你走进GTK+的世界。
10、GObject对象系统
10.1、概念简单的说,GObject对象系统是一个建立在GLIB基础上的,用C语言完成的,具有跨平台特色的、灵活的、可扩展的、非常容易映射到其它语言的面向对象的框架。如果你是一个C语言的执着的追随者,你没有理由不研究一下它。
前言
大多数现代的计算机语言都带有自己的类型和对象系统,并附带算法结构。正象GLib提供的基本类型和算法结构(如链表、哈希表等)一样,GObject的对象系统提供了一种灵活的、可扩展的、并容易映射(到其它语言)的面向对象的C语言框架。它的实质可以概括为: 一个通用类型系统,用来注册任意的、轻便的、单根继承的、并能推导出任意深度的结构类型的界面,它照顾组合对象的定制、初始化和内存管理,类结构,保持对象的父子关系,处理这些类型的动态实现。也就是说,这些类型的实现是在运行时重置和卸载的;
一个基本类型的实现集,如整型,枚举型和结构型等;
一个基本对象体系之上的基本对象类型的实现的例子--GObject基本类型;
一个信号系统,允许用户非常灵活的自定义虚的或重载对象的方法,并且能充当非常有效力的通知机制;
一个可扩展的参数/变量体系,支持所有的能被用作处理对象属性或其它参数化类型的基本的类型。
10.2、类型(GType)与对象(GObject)
GLib中最有特色的是它的对象系统--GObject System,它是以Gtype为基础而实现的一套单根继承的C语言的面向对象的框架。
GType 是GLib 运行时类型认证和管理系统。GType API 是GObject的基础系统,所以理解GType是理解GObject的关键。Gtype提供了注册和管理所有基本数据类型、用户定义对象和界面类型的技 术实现。(注意:在运用任一GType和GObject函数之前必需运行g_type_init()函数来初始化类型系统。)
为实现类型定制和注册这一目的,所有类型必需是静态的或动态的这二者之一。静态的类型永远不能在运行时加载或卸载,而动 态的类型则可以。静态类型由g_type_register_static()创建,通过GTypeInfo结构来取得类型的特殊信息。动态类型则由 g_type_register_dynamic()创建,用GTypePlugin结构来取代GTypeInfo,并且还包括 g_type_plugin_*()系列API。这些注册函数通常只运行一次,目的是取得它们返回的专有类的类型标识。
还可以用g_type_register_fundamental来注册基础类型,它同时需要GTypeInfo和GTypeFundamentalInfo两个结构,事实上大多数情况下这是不必要的,因为系统预先定义的基础类型是优于用户自定义的。
(本文重点介绍创建和使用静态的类型。)
对象的定义
在GObject系统中,对象由三个部分组成:
1. 对象的ID标识(唯一,无符号长整型,所有此类对象共同的标识);
2. 对象的类结构(唯一,结构型,由对象的所有实例共同拥有);
3. 对象的实例(多个,结构型,对象的具体实现)。
基于GObject的对象到底是什么样的呢?下面是基于GObject的简单对象 -- Boy的定义代码:/* boy.h */
#ifndef __BOY_H__
#define __BOY_H__
#include <glib-object.h>
#define BOY_TYPE (boy_get_type())
#define BOY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),BOY_TYPE,Boy))
typedef struct _Boy Boy;
typedef struct _BoyClass BoyClass;
struct _Boy {
GObject parent;
//
gint age;
gchar *name;
void (*cry)(void);
};
struct _BoyClass {
GObjectClass parent_class;
//
void (*boy_born)(void);
};
GType boy_get_type(void);
Boy* boy_new(void);
int boy_get_age(Boy *boy);
void boy_set_age(Boy *boy, int age);
char* boy_get_name(Boy *boy);
void boy_set_name(Boy *boy, char *name);
Boy* boy_new_with_name(gchar *name);
Boy* boy_new_with_age(gint age);
Boy* boy_new_with_name_and_age(gchar *name, gint age);
void boy_info(Boy *boy);
#endif /* __BOY_H__*/
这是一段典型的C语言头文件定义,包括编译预处理,宏定义,数据结构定义和函数声明;首先要看的是两个数据结构对象Boy和BoyClass,
结构类型_Boy是Boy对象的实例,就是说我们每创建一个Boy对象,也就同时创建了一个Boy结构。Boy对象中的 parent表示此对象的父类,GObject系统中所有对象的共同的根都是GObject类,所以这是必须的;其它的成员可以是公共的,这里包括表示年 龄的age,表示名字的name和表示方法的函数指针cry,外部代码可以操作或引用它们。
结构类型_BoyClass是Boy对象的类结构,它是所有Boy对象实例所共有的。BoyClass中的 parent_class是GObjectClass,同GObject是所有对象的共有的根一样,GObejctClass是所有对象的类结构的根。在 BoyClass中我们还定义了一个函数指针boy_born,也就是说这一函数指针也是所有Boy对象实例共有的,所有的Boy实例都可以调用它;同 样,如果需要的话,你也可以在类结构中定义其它数据成员。
其余的函数定义包括三种,一种是取得Boy对象的类型ID的函数boy_get_type,这是必须有的;另一种是创建 Boy对象实例的函数boy_new和boy_new_with_*,这是非常清晰明了的创建对象的方式,当然你也可以用g_object_new函数来 创建对象;第三种是设定或取得Boy对象属性成员的值的函数boy_get_*和boy_set_*。正常情况下这三种函数都是一个对象所必需的,另外一 个函数boy_info用来显示此对象的当前状态。
宏在GObject系统中用得相当广泛,也相当重要,这里我们定义了两个非常关键的宏,BOY_TYPE宏封装了 boy_get_type函数,可以直接取得并替代Boy对象的ID标识;BOY(obj)宏是G_TYPE_CHECK_INSTANCE_CAST宏 的再一次封装,目的是将一个Gobject对象强制转换为Boy对象,这在对象的继承中十分关键,也经常用到。
对象的实现
下面的代码实现了上面的Boy对象的定义:/* boy.c */
#include "boy.h"
enum { BOY_BORN, LAST_SIGNAL };
static gint boy_signals[LAST_SIGNAL] = { 0 };
static void boy_cry (void);
static void boy_born(void);
static void boy_init(Boy *boy);
static void boy_class_init(BoyClass *boyclass);
GType boy_get_type(void)
{
static GType boy_type = 0;
if(!boy_type)
{
static const GTypeInfo boy_info = {
sizeof(BoyClass),
NULL,NULL,
(GClassInitFunc)boy_class_init,
NULL,NULL,
sizeof(Boy),
0,
(GInstanceInitFunc)boy_init
};
boy_type = g_type_register_static(G_TYPE_OBJECT,"Boy",&boy_info,0);
}
return boy_type;
}
static void boy_init(Boy *boy)
{
boy->age = 0;
boy->name = "none";
boy->cry = boy_cry;
}
static void boy_class_init(BoyClass *boyclass)
{
boyclass->boy_born = boy_born;
boy_signals[BOY_BORN] = g_signal_new("boy_born",
BOY_TYPE,
G_SIGNAL_RUN_FIRST,
G_STRUCT_OFFSET(BoyClass,boy_born),
NULL,NULL,
g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0, NULL);
}
Boy *boy_new(void)
{
Boy *boy;
boy = g_object_new(BOY_TYPE, NULL);
g_signal_emit(boy,boy_signals[BOY_BORN],0);
return boy;
}
int boy_get_age(Boy *boy)
{
return boy->age;
}
void boy_set_age(Boy *boy, int age)
{
boy->age = age;
}
char *boy_get_name(Boy *boy)
{
return boy->name;
}
void boy_set_name(Boy *boy, char *name)
{
boy->name = name;
}
Boy* boy_new_with_name(gchar *name)
{
Boy* boy;
boy = boy_new();
boy_set_name(boy, name);
return boy;
}
Boy* boy_new_with_age(gint age)
{
Boy* boy;
boy = boy_new();
boy_set_age(boy, age);
return boy;
}
Boy *boy_new_with_name_and_age(gchar *name, gint age)
{
Boy *boy;
boy = boy_new();
boy_set_name(boy,name);
boy_set_age(boy,age);
return boy;
}
static void boy_cry (void)
{
g_print("The Boy is crying ......\n");
}
static void boy_born(void)
{
g_print("Message : A boy was born .\n");
}
void boy_info(Boy *boy)
{
g_print("The Boy name is %s\n", boy->name);
g_print("The Boy age is %d\n", boy->age);
}
在这段代码中,出现了实现Boy对象的关键函数,这 是在Boy对象的定义中未出现的,也是没必要出现的。就是两个初始化函数,boy_init和boy_class_init,它们分别用来初始化实例结构 和类结构。它们并不被在代码中明显调用,关键是将其用宏转换为地址指针,然后赋值到GTypeInfo结构中,然后由GType系统自行处理,同时将它们 定义为静态的也是非常必要的。
GTypeInfo结构中定义了对象的类型信息,包括以下内容:
1. 包括类结构的长度(必需,即我们定义的BoyClass结构的长度);
2. 基础初始化函数(base initialization function,可选);
3. 基础结束化函数(base finalization function,可选);
4. (以上两个函数可以对对象使用的内存来做分配和释放操作,使用时要用GBaseInitFunc和GBaseFinalizeFunc来转换为指针,本例中均未用到,故设为NULL。)
5. 类初始化函数(即我们这里的boy_class_init函数,用GclassInit宏来转换,可选,仅用于类和实例类型);
6. 类结束函数(可选);
7. 实例初始化函数(可选,即我们这里的boy_init函数);
8. 最后一个成员是GType变量表(可选)。
定义好GTypeInfo结构后就可以用g_type_register_static函数来注册对象的类型了。
g_type_register_static函数用来注册对象的类型,它的第一个参数是表示此对象的父类的对象类型,我 们这里是G_TYPE_OBJECT,这个宏用来表示GObject的父类;第二个参数表示此对象的名称,这里为"Boy";第三个参数是此对象的 GTypeInfo结构型指针,这里赋值为&boyinfo;第四个参数是对象注册成功后返回此对象的整型ID标识。
g_object_new函数,用来创建一个基于G_OBJECT的对象,它可以有多个参数,第一个参数是上面说到的已 注册的对象标识ID;第二个参数表示后面参数的数量,如果为0,则没有第三个参数;第三个参数开始类型都是GParameter类型,它也是一个结构型, 定义为:
struct GParameter{
const gchar* name;
GValue value;
};
关于GValue,它是变量类型的统一定义,它是基础的变量容器结构,用于封装变量的值和变量的类型,可以GOBJECT文档的GVALUE部分。
信号的定义和应用
在GObject系统中,信号是一种定制对象行为的手段,同时也是一种多种用途的通知机制。初学者可能是在GTK+中首先接触到信号这一概念的,事实上在普通的字符界面编程中也可以正常应用,这可能是很多初学者未曾想到的。
一个对象可以没有信号,也可以有多个信号。当有一或多个信号时,信号的名称定义是必不可少的,此时C语言的枚举类型的功能 就凸显出来了,用LAST_SIGNAL来表示最后一个信号(不用实现的信号)是一种非常良好的编程风格。这里为Boy对象定义了一个信号 BOY_BORN,在对象创建时发出,表示Boy对象诞生。
同时还需要定义静态的整型指针数组来保存信号的标识,以便于下一步处理信号时使用。
对象的类结构是所有对象的实例所共有的,我们将信号也定义在对象的类结构中,如此信号同样也是所有对象的实例所共有的,任 意一个对象的实例都可以处理信号。因此我们有必要在在类初始化函数中创建信号(这也可能是GObject设计者的初衷)。函数g_signal_new用 来创建一个新的信号,它的详细使用方法可以在GObject的API文档中找到。信号创建成功后,返回一个信号的标识ID,如此就可以用发射信号函数 g_signal_emit向指定义对象的实例发射信号,从而执行相应的功能。
本例中每创建一个新的Boy对象,就会发射一次BOY_BORN信号,也就会执行一次我们定义的boy_born函数,也就输出一行"Message : A boy was born ."信息。
对象的属性和方法
对象实例所有的属性和方法一般都定义在对象的实例结构中,属性定义为变量或变量指针,而方法则定义为函数指针,如此,我们一定要定义函数为static类型,当为函数指针赋值时,才能有效。
对象的继承
以下为继承自Boy对象的Man对象的实现,Man对象在Boy对象的基础上又增加了一个属性job和一个方法bye。#ifndef __MAN_H__
#define __MAN_H__
#include "boy.h"
#define MAN_TYPE (man_get_type())
#define MAN(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),MAN_TYPE,Man))
typedef struct _Man Man;
typedef struct _ManClass ManClass;
struct _Man {
Boy parent;
char *job;
void (*bye)(void);
};
struct _ManClass {
BoyClass parent_class;
};
GType man_get_type(void);
Man* man_new(void);
gchar* man_get_gob(Man *man);
void man_set_job(Man *man, gchar *job);
Man* man_new_with_name_age_and_job(gchar *name, gint age, gchar *job);
void man_info(Man *man);
#endif //__MAN_H__
/* man.c */
#include "man.h"
static void man_bye(void);
static void man_init(Man *man);
static void man_class_init(Man *man);
GType man_get_type(void)
{
static GType man_type = 0;
if(!man_type)
{
static const GTypeInfo man_info = {
sizeof(ManClass),
NULL, NULL,
(GClassInitFunc)man_class_init,
NULL, NULL,
sizeof(Man),
0,
(GInstanceInitFunc)man_init
};
man_type = g_type_register_static(BOY_TYPE, "Man", &man_info, 0);
}
return man_type;
}
static void man_init(Man *man)
{
man->job = "none";
man->bye = man_bye;
}
static void man_class_init(Man *man)
{
}
Man* man_new(void){
Man *man;
man = g_object_new(MAN_TYPE, 0);
return man;
}
gchar* man_get_gob(Man *man)
{
return man->job;
}
void man_set_job(Man *man, gchar *job)
{
man->job = job;
}
Man* man_new_with_name_age_and_job(gchar *name, gint age, gchar *job)
{
Man *man;
man = man_new();
boy_set_name(BOY(man), name);
boy_set_age(BOY(man), age);
man_set_job(man, job);
return man;
}
static void man_bye(void)
{
g_print("Goodbye everyone !\n");
}
void man_info(Man *man)
{
g_print("the man name is %s\n", BOY(man)->name);
g_print("the man age is %d\n", BOY(man)->age);
g_print("the man job is %s\n", man->job);
}关键在于定义对象时将父对象实例定义为Boy,父类 设定为BoyClass,在注册此对象时将其父对象类型设为BOY_TYPE,在设定对象属性时如用到父对象的属性要强制转换下,如取得对象的name属 性,就必须用BOY(obj)->name,因为Man本身没有name属性,而其父对象Boy有,所以用BOY宏将其强制为Boy类型的对象。
测试我们定义的对象
#include <glib.h>
#include "boy.h"
#include "man.h"
int main(int argc, char *argv[])
{
Boy *tom, *peter;
Man *green, *brown;
g_type_init();//注意,初始化类型系统,必需
tom = boy_new_with_name("Tom");
tom->cry();
boy_info(tom);
peter = boy_new_with_name_and_age("Peter", 10);
peter->cry();
boy_info(peter);
green = man_new();
boy_set_name(BOY(green), "Green");
//设定Man对象的name属性用到其父对象Boy的方法
boy_set_age(BOY(green), 28);
man_set_job(green, "Doctor");
green->bye();
man_info(green);
brown = man_new_with_name_age_and_job("Brown", 30, "Teacher");
brown->bye();
man_info(brown);
}
Makefile文件如下:
CC = gcc
all:
$(CC) -c boy.c `pkg-config --cflags glib-2.0 gobject-2.0`
$(CC) -c man.c `pkg-config --cflags glib-2.0 gobject-2.0`
$(CC) -c main.c `pkg-config --cflags glib-2.0 gobject-2.0`
$(CC) -o simple boy.o man.o main.o `pkg-config --libs glib-2.0 gobject-2.0`
执行make命令编译,编译结束后,执行./simple运行此测试程序,输出结果如下:
Message : A boy was born .
The Boy is crying ......
The Boy name is Tom
The Boy age is 0
Message : A boy was born .
The Boy is crying ......
The Boy name is Peter
The Boy age is 10
Goodbye everyone !
the man name is Green
the man age is 28
the man job is Doctor
Goodbye everyone !
the man name is Brown
the man age is 30
the man job is TeacherMakefile中用到`pkg-config -cflags -libs gobject-2.0`,在GLIB中将线程(gthread),插件(gmoudle)和对象系统(gobject)这三个子系统区别对待,编译时要注意加入相应的参数。
本文只是概要的介绍了如何定义和实现GObject对象,GObject系统中还有很多相关内容,如:枚举和标识类型 (Enumeration and flags types);Gboxed,是Gtype系统中注册一种封装为不透明的C语言结构类型的机制;许多对象用到的参数对象都是C结构类型,使用者不必了解其 结构的内部定义,即不透明,GBoxed即是实现这一功能的机制;标准的参数和变量类型的定义(Standard Parameter and Value Types)等,它们都以C语言来开发,是深入了解和掌握GObject的关键。
透过以上代码实现,我们还可以看出,以GLIB为基础的GTK+/GNOME开发环境所具有的独特的编程风格和独到的开发思想。这一点在长期的编程实践中会体验得更深刻。
有了GObject系统这一基础,GTK+通过它将X窗口环境中的控件(Widget)巧妙的封装起来,这使开发LINUX平台上的GUI应用程序更方便,更快捷。
以上代码在Redhat 8.0 Linux平台,GLIB2.2.1环境下编译通过。11、日志
程序中不免会出现错误,当错误发生时,您可以使用printf()或是g_print()在控制台 (Console)显示信息给用户,如果是在窗口程序中,可能是使用消息框,您也可能想针对某个层级的信息作个别处理,例如储存在log档案之中,在GLib中,您可以使用 Message Logging 中所介绍的函数来进行日志功能。要进行日志,首先最基本的就是使用g_log()函数:
void g_log(const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *format,
...);
第一个参数是log_domain,用来区别日志信息的发出者,若有设定日志的处理函数,则log_domain亦会传递给处理函数,如果您没有指定,则预设会使用G_LOG_DOMAIN,函数库会定义G_LOG_DOMAIN,以区别于其它的函数库,例如GTK在它的Makefine中定义为"Gtk":
INCLUDES = -DG_LOG_DOMAIN=\"Gtk\"第二个参数是日志层级,可以设定为以下的值:
• G_LOG_LEVEL_ERROR(致命的,FATAL)
• G_LOG_LEVEL_CRITICAL
• G_LOG_LEVEL_WARNING
• G_LOG_LEVEL_MESSAGE
• G_LOG_LEVEL_INFO
• G_LOG_LEVEL_DEBUG另外还有两个G_LOG_FLAG_FATAL与G_LOG_FLAG_RECURSION,作为内部的标志位使用,其中与 G_LOG_FLAG_FATAL相关联的,例如G_LOG_LEVEL_ERROR,是属于严重的致命信息,当日志时以这个层级输出时,应用程序会被中 止并呼叫核心倾印(dump)。
第三个参数是要输出的信息,其它则是额外的信息。
GLib还提供了五个宏函数,方便使用日志与相对应的信息层级:#define g_message(...)
#define g_warning(...)
#define g_critical(...)
#define g_error(...)
#define g_debug(...)先前说过,G_LOG_FLAG_FATAL是内部标志位,预设是G_LOG_LEVEL_ERROR与之关联,如果您想让其它层级的信息也成为FATAL 的,则可以使用g_log_set_always_fatal()函数,例如将DEBUG与CRITICAL设定为FATAL:
g_log_set_always_fatal(G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_CRITICAL);
对于日志信息,您可以设定相对应的处理函数,这是使用g_log_set_handler()函数来达成:
guint g_log_set_handler(const gchar *log_domain,
GLogLevelFlags log_levels,
GLogFunc log_func,
gpointer user_data);
传回的整数值为Handler Id,其中GLogFunc为回调函数,它的宣告定义如下:void (*GLogFunc) (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer user_data);
设定信息处理函数之后,若想移除,则可以使用g_log_remove_handler()函数,根据Handler ID及log domain来移除:void g_log_remove_handler(const gchar *log_domain,
guint handler_id);
设置glib的log级别。
glib提供了一系列的log函数,像g_message、g_critical、g_warning、g_debug和g_error等,可以根据信息的类别调用不同的函数。
在我们的程序中,为了调试方便,很多地方调了g_debug,结果程序运行起来后,终端上的信息打印得眼花缭乱,不但影响性能,而且把真正有用的信息淹没掉了。
Glib既然提供了log级别,自然可以按log级别加以过滤。不过稍微有点麻烦,可以按下列方式实现:static void dummy_log(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message, gpointer user_data)
{
return;
}
void set_log_level(const char* progname)
{
char* basename = strrchr(progname, '/');
char* log_level_env_name = NULL;
char* log_level_evn_value = NULL;
basename = basename != NULL ? basename+1 : (char*)progname;
log_level_env_name = g_strdup_printf("%s_LOG_LEVEL", basename);
log_level_evn_value = getenv(g_strup(log_level_env_name));
if(log_level_evn_value != NULL)
{
unsigned int i = 1 << (~G_LOG_LEVEL_MASK);
unsigned int max_log_level = atoi(log_level_evn_value);
unsigned int log_level = 0;
for(; i > max_log_level; i--)
{
log_level = 1 << i;
g_log_set_handler(NULL, (GLogLevelFlags)log_level, dummy_log, NULL);
}
}
g_free(log_level_env_name);
return;
}通过设置 ”可执行文件名(大写)_LOG_LEVEL”环境变量,可以过滤不同严重程度的LOG信息,其取值为1-7,值越大,打印的信息越多。
12、正则表达式
用过perl,python,shell的人在使用c语言的字符串时都会觉得c语言字符串的处理太麻烦了。很多程序测试题都会考一些字符串匹配的题。
glib提供了一套非常好的正则表达式api,程序可以非常简单的使用c语言来做字符串的匹配。
比如一个文件 test_regex.txt11aa222bb33333cccc44444dddddddd
要匹配出所有的数字,使用了glib库的程序
[root@localhost glib_test]# ./g_regex
11
222
33333
44444#include <glib.h>
static void print_uppercase_words(const gchar* string)
{
GRegex* regex;
GMatchInfo *match_info;
GError *error = NULL;
regex = g_regex_new("[0-9]+", 0 , 0, NULL);
g_regex_match(regex, string, 0, &match_info);
while (g_match_info_matches(match_info)) {
gchar* word = g_match_info_fetch(match_info, 0);
g_print("%s\n",word);
g_free(word);
g_match_info_next(match_info, NULL);
}
g_match_info_free(match_info);
g_regex_unref(regex);
}
int main()
{
char *buf;
int length;
g_file_get_contents("test_regex.txt", &buf, &length,NULL);
print_uppercase_words(buf);
return 0;
}
程序使用起来非常简单,3步就可以搞定
1.创建一个GRegex,来定义你的正则表达式,这里定义了只匹配所有数字。
2.使用g_regex_match来匹配内容中符合正则表达式规则的所有内容。
3.因为匹配出来的是一个集合,利用g_match_info_fetch把每一项fetch出来
13、读取程序配置文件
有时在写一个程序时经常会从一个配置文件中读取一系列的参数,在度bluez代码时发现了一个非常好的方法。
这个方法基于glib-2.0,例如要从名为main.conf中读取里面的配置值。
offmode = NoScan
pagetimeout = 8192
age = 26
1 [General]
2
3 # List of plugins that should not be loaded on bluetoothd startup
4 #DisablePlugins = network,input
5
6 # Default adaper name
7 # %h - substituted for hostname
8 # %d - substituted for adapter id
9 Name = %h-%d:
10
11 # Default device class. Only the major and minor device class bits are
12 # considered
13 Class = 0x000100
14
15 # How long to stay in discoverable mode before going back to non-discoverable
16 # The value is in seconds. Default is 180, i.e. 3 minutes.
17 # 0 = disable timer, i.e. stay discoverable forever
18 DiscoverableTimeout = 0
19
20 # Use some other page timeout than the controller default one
21 # (16384 = 10 seconds)
22 PageTimeout = 8192
23
24 # Behaviour for Adapter.SetProperty("mode", "off")
25 # Possible values: "DevDown", "NoScan" (default)
26 OffMode = NoScan
27
28 # Discover scheduler interval used in Adapter.DiscoverDevices
29 # The value is in seconds. Defaults is 0 to use controller scheduler
30 DiscoverSchedulerInterval = 0
31 [test]
32 age=26
#include <glib.h>
static GKeyFile *load_config(const char *file)
{
GError *err = NULL;
GKeyFile *keyfile;
keyfile = g_key_file_new();
g_key_file_set_list_separator(keyfile, ',');
if (!g_key_file_load_from_file(keyfile, file, 0, &err)) {
error("Parsing %s failed: %s", file, err->message);
g_error_free(err);
g_key_file_free(keyfile);
return NULL;
}
return keyfile;
}int main(int argc, int **argv)
{
char* str;
int val1,val2;
GError* err = NULL;
GKeyFile* keyfile;
keyfile = load_config("main.conf");
str = g_key_file_get_string(keyfile,"General","OffMode",&err);
if (err) {
printf("%s",err->message);
g_clear_error(&err);
}
val1 = g_key_file_get_integer(keyfile,"General","PageTimeout",&err);
if (err) {
printf("%s",err->message);
g_clear_error(&err);
}
val2 = g_key_file_get_integer(keyfile,"test","age",&err);
if (err) {
printf("%s",err->message);
g_clear_error(&err);
}
printf("offmode = %s\n",str);
printf("pagetimeout = %d\n",val1);
printf("age = %d\n",val2);
return 0;
}
gcc -g `pkg-config --cflags --libs glib-2.0 gthread-2.0` glib_parser.c -o glib_parser
14、命令行解析
传统的命令行解析,如果提供的参数很多,程序很不灵活,并且很容易出问题
while(getopt())
{
swtich() {
case:
case:
case:
}
}
比如实现下面这个参数,实现起来还是非常麻烦的。[root@localhost glib_test]# ./glib_test -?
Usage:
glib_test [OPTION...]Help Options:
-?, --help Show help options
--help-all Show all help options
--help-test group display help outputApplication Options:
-n, --nodaemon Don't run as daemon in background
-d, --debug Enable debug information output
-s, --size=M Input your sizeglib提供了一套解析命令行的函数,程序实现如下,太晚了,明天再加注释。
#include <stdio.h>
#include <glib.h>
//#include <stdlib.h>
//#include <unistd.h>static gboolean option_detach = FALSE;
static gboolean option_debug = FALSE;
static gint size = 8;
static GOptionEntry options[] = {
{ "nodaemon", 'n', 0,
G_OPTION_ARG_NONE, &option_detach,
"Don't run as daemon in background" },
{ "debug", 'd', 0,
G_OPTION_ARG_NONE, &option_debug,
"Enable debug information output" },
{ "size", 's', 0,
G_OPTION_ARG_INT, &size,
"Input your size","M" },
{ NULL },
};
int main(int argc, char *argv[])
{
GOptionContext *context;
GError *err = NULL;
GOptionGroup *g_group;
context = g_option_context_new(NULL);
g_group = g_option_group_new("test group","test group display",
"display help output", NULL, NULL);
g_option_context_add_main_entries(context, options, NULL);
g_option_context_add_group(context,g_group);
if (g_option_context_parse(context, &argc, &argv, &err) == FALSE) {
if (err != NULL) {
g_printerr("%s\n", err->message);
g_error_free(err);
} else
g_printerr("An unknown error occurred\n");
exit(1);
}
g_option_context_free(context);
if (option_debug == FALSE) {
g_printf("option debug disable\n");
} else {
g_printf("option debug enable\n");
}if (option_detach == FALSE) {
g_printf("option detach disable\n");
} else {
g_printf("option detach enable\n");
}
printf("size=%d\n",size);
return 0;
}
15、解析xml文件
xml的用处越来越广泛了,解析xml得库也非常多,总的来说分为两种,一种是把xml当作一个“树”来进行解析,一种是基于事件类型的
glib就是使用事件类型解析xml。
有5种不同的事件类型
1)一个element的开始
2)一个element的结束
3)element得文本
4)一些stuff
5) 错误
There are five kinds of event which can happen:
• The start of an element
• The end of an element
• Some text (inside an element)
• Some other stuff (processing instructions, mainly, including comments and doctypes)
• An error
下面是一个简单的xmlsimple.xml:
<zoo>
<animal noise="roar">lion</animal>
<animal noise="sniffle">bunny</animal>
<animal noise="lol">cat</animal>
<keeper/>
</zoo>被解析成以下的动作
Start of “zoo”.
Start of “animal”, with a “noise” attribute of “roar”.
The text “lion”.
End of “animal”.
Start of “animal”, with a “noise” attribute of “sniffle”.
The text “bunny”.
End of “animal”.
Start of “animal”, with a “noise” attribute of “lol”.
The text “cat”.
End of “animal”.
Start of “keeper”.
End of “keeper”.
End of “zoo”.
程序运行结果
I am a lion and I go roar. Can you do it?
I am a bunny and I go sniffle. Can you do it?
I am a cat and I go lol. Can you do it?
#include <stdio.h>
gchar *current_animal_noise = NULL;
static void start(GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error)
{
const gchar **name_cursor = attribute_names;
const gchar **value_cursor = attribute_values;
while (*name_cursor) {
if (strcmp (*name_cursor, "noise") == 0)
current_animal_noise = g_strdup (*value_cursor);
name_cursor++;
value_cursor++;
}
}
static void end(GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error)
{
if (current_animal_noise)
{
g_free (current_animal_noise);
current_animal_noise = NULL;
}
}
static void text(GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error)
{
if (current_animal_noise)
printf("I am a %*s and I go %s. Can you do it?\n",
text_len, text, current_animal_noise);
printf("test text\n");
}GMarkupParser parser = {
.start_element = start,
.end_element = end,
.text = text,
.passthrough = NULL,
.error = NULL};int main()
{
char *buf;
gsize length;
GMarkupParseContext *context;
g_file_get_contents("test.xml", &buf, &length,NULL);
g_printf("%s\n",buf);
context = g_markup_parse_context_new(&parser, 0, NULL, NULL);
if (g_markup_parse_context_parse(context, buf, length, NULL) == FALSE)
{
printf("Couldn't load xml\n");
g_markup_parse_context_free(context);
return 0;
}16、使用线程池
如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源甚至可能要比花在处理实际的用户请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于 在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目 可以防止出现资源不足的情况。
一个比较简单的线程池至少应包含线程池管理器、工作线程、任务队列、任务接口等部分。glib提供了一套线程池的api,和pthread提供的那一套比起来方便了很多。
GThreadPool;
GThreadPool* g_thread_pool_new (GFunc func,
gpointer user_data,
gint max_threads,
gboolean exclusive,
GError **error);
void g_thread_pool_push (GThreadPool *pool,
gpointer data,
GError **error);
void g_thread_pool_set_max_threads (GThreadPool *pool,
gint max_threads,
GError **error);
gint g_thread_pool_get_max_threads (GThreadPool *pool);
guint g_thread_pool_get_num_threads (GThreadPool *pool);
guint g_thread_pool_unprocessed (GThreadPool *pool);
void g_thread_pool_free (GThreadPool *pool,
gboolean immediate,
gboolean wait_);
void g_thread_pool_set_max_unused_threads
(gint max_threads);
gint g_thread_pool_get_max_unused_threads
(void);
guint g_thread_pool_get_num_unused_threads
(void);
void g_thread_pool_stop_unused_threads (void);
void g_thread_pool_set_sort_function (GThreadPool *pool,
GCompareDataFunc func,
gpointer user_data);
void g_thread_pool_set_max_idle_time (guint interval);
guint g_thread_pool_get_max_idle_time (void);
下面是一个使用线程池的例子
#include<glib.h>
static void test_function(gpointer data, gpointer user_data)
{
int i;
i = GPOINTER_TO_INT(data);
g_print("test function %d\n",i);
}int main()
{
GThreadPool *pool = NULL;
GError *error = NULL;
//char data[10]="test";int i,gthreadcount;
GMutex *mutex;
if ( g_thread_supported () )
printf("support g_thread\n");
g_thread_init (NULL);
mutex = g_mutex_new();
pool = g_thread_pool_new(test_function,NULL,-1,FALSE,&error);
if(pool == NULL) {
g_print("can not create thread");
}gthreadcount = g_thread_pool_get_num_threads(pool);
g_print("gthread count is %d\n",gthreadcount);g_mutex_lock(mutex);
for(i = 0; i < 10 ; i++)
{
g_thread_pool_push(pool, (gpointer *)i , NULL);
}
g_mutex_unlock(mutex);
// g_print("gthread count is %d\n",gthreadcount);
}[root@localhost glib_test]# ./g_thread_pool
gthread count is 0
test function 1
test function 2
test function 3
test function 4
test function 5
test function 6
test function 7
test function 8
test function 9
gthread count is 1测试程序循环从池中调用,可以看到实际上只创建了一个thread.
乐那知道了这个开发包,一下子来了兴趣。
17、网络开发
GNet是基于GLib的一套网络开发包,软件包的名称是:libgnet2.0-0和libgnet2.0-dev。基于GNet开发的软件不多,不过这不能说明什么问题。
不知从哪里看到过一句话,GNet就像是Java的NIO。无论怎么样,还是先简单介绍下它吧:
GNet是一个网络库,是用C写的面向对象的(跟GTK一样),并且基于GLib。它旨在简易使用和可移植。
特性:
• TCP "client" and "server" sockets
• UDP and IP Multicast sockets
• High-level TCP connection and server objects
• Asynchronous socket IO
• HTTP client object
• Internet address abstraction
• Asynchronous DNS lookup
• IPv4 and IPv6 support
• Byte packing and unpacking
• URI parsing
• SHA and MD5 hashes
• Base64 encoding and decoding
• SOCKS support
英文不翻译了,很简单易懂。重点拿几个说说。
首先是Asynchronous socket IO(异步Socket IO),看过Java的NIO,知道它是非阻塞的。
由于对Linux Socket了解不多,就不知道怎么使用异步IO。而GNet这套网络库,把Socket以面向对象的模式包装了一遍,并有非常方便可查的API:http://www.gnetlibrary.org/docs/index.html
在GNet库下,从一个InetAddr创建一个TCP连接,使用这个API:gnet_tcp_socket_new (),写过GTK就会对这个句式非常熟悉。这样就创建了一个TCP连接的实例。
如果要创建异步的TCP连接呢?使用gnet_tcp_socket_new_async ()就可以了,其中第二个参数是一个Callback,每当有连接时,就会触发,也就是异步了。跟GTK图形编程的信号/回调是一样的道理。
另外GLib中就有GIOChannel 这个东西,在GNet中也得到了扩展。GIOChannel就是一个IO通道,一般都与异步类一起使用。使用g_io_add_watch来监视一个IO 通道,当有IO事件发生时,就可以触发相关Callback。这样就可以不用一直While(True)来收发东西了。
还有一个重要的是:Byte packing and unpacking
用于包装数据至字节流中,功能跟Python中的struct一样。利用这个,就可以非常方便地写个跟Java的ByteBuffer一样的类了。
还有URI、MD5/SHA的Hash支持,IPv6支持,都证明了这个GNet库不仅方便,而且全面和强大。
通过GNet,编写网络应用程序就方便多了。18、总结
结束语
在本教程中,研究了如何使用 GLib 程序库中的数据结构。研究了可以如何使用这些容器来有效地管理程序的数据,还研究了在 几个流行的开放源代码项目中这些容器如何得到应用。在此过程中介绍了很多 GLib 类型、宏以及字符串处理函数。
GLib 包括很多其他的优秀功能:它有一个线程-抽象(threading-abstraction)层,一个可移植-套接字(portable-sockets)层,消息日志工具, 日期和时间函数,文件工具,随机数生成,等等,还有很多。值得去研究这些模块。并且,如果有兴趣且有能力,您甚至可以改进某些文档 —— 例如, 记法扫描器的文档中包含了一个注释,内容是它需要一些示例代码,并需要进一步详述。如果您从开放源代码的代码中受益,那么 请不要忘记帮助改进它!
10-19
10-03