这是用户在 2024-11-7 18:36 为 https://docs.gtk.org/glib/data-structures.html#singly-linked-lists 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

 数据结构

 数据结构


GLib 包括许多基本的数据结构,例如数组、链表、哈希表、队列、树等。

 阵 列


GLib 数组 (GArray) 与标准 C 数组类似,不同之处在于它们会随着元素的添加而自动增长。


数组元素可以是任意大小(尽管一个数组的所有元素都相同大小),并且数组可以自动清除为 '0 并以零结尾。


要创建新数组,请使用 g_array_new()。


要将元素添加到最差成本为 O(n) 的数组中,请使用 g_array_append_val()、g_array_append_vals()、g_array_prepend_val()、g_array_prepend_vals()、g_array_insert_val()g_array_insert_vals()。


要访问 O(1) 中数组的元素(读取或写入),请使用 g_array_index()。


要设置数组的大小,请使用 g_array_set_size()。


要释放数组,请使用 g_array_unref()g_array_free()。


所有排序函数都在内部调用一个快速排序(或类似)函数,平均成本为 O(n log(n)),最坏情况成本为 O(n^2)。


下面是一个在 GArray 中存储整数的示例:

GArray *array;
int i;

// We create a new array to store int values.
// We don't want it zero-terminated or cleared to 0's.
array = g_array_new (FALSE, FALSE, sizeof (int));

for (i = 0; i < 10000; i++)
  {
    g_array_append_val (array, i);
  }

for (i = 0; i < 10000; i++)
  {
    if (g_array_index (array, int, i) != i)
      g_print ("ERROR: got %d instead of %d\n",
               g_array_index (array, int, i), i);
  }

g_array_free (array, TRUE);

 指针数组


指针数组 (GPtrArray) 类似于数组,但仅用于存储指针。


如果从数组中删除元素,则数组末尾的元素将移动到先前被已删除元素占用的空间中。这意味着您不应依赖特定元素的索引保持不变。在迭代数组时删除元素时也应小心。


要创建指针数组,请使用 g_ptr_array_new()。


要将元素添加到指针数组中,请使用 g_ptr_array_add()。


要从指针数组中删除元素,请使用 g_ptr_array_remove()、g_ptr_array_remove_index() g_ptr_array_remove_index_fast() .


要访问指针数组的元素,请使用 g_ptr_array_index()。


要设置指针数组的大小,请使用 g_ptr_array_set_size()。


要释放指针数组,请使用 g_ptr_array_unref()g_ptr_array_free()。


使用 GPtrArray 的示例:

GPtrArray *array;
char *string1 = "one";
char *string2 = "two";
char *string3 = "three";

array = g_ptr_array_new ();
g_ptr_array_add (array, (gpointer) string1);
g_ptr_array_add (array, (gpointer) string2);
g_ptr_array_add (array, (gpointer) string3);

if (g_ptr_array_index (array, 0) != (gpointer) string1)
  g_print ("ERROR: got %p instead of %p\n",
           g_ptr_array_index (array, 0), string1);

g_ptr_array_free (array, TRUE);

 字节数组


GByteArray 是基于 GArray 的可变字节数组,用于提供字节数组,这些字节数组会随着元素的添加而自动增长。


要创建新的 GByteArray,请使用 g_byte_array_new()。


要将元素添加到 GByteArray,请使用 g_byte_array_append()g_byte_array_prepend()。


要设置 GByteArray 的大小,请使用 g_byte_array_set_size()。


要释放 GByteArray,请使用 g_byte_array_unref()g_byte_array_free()。


使用 GByteArray 的示例:

GByteArray *array;
int i;

array = g_byte_array_new ();
for (i = 0; i < 10000; i++)
  {
    g_byte_array_append (array, (guint8*) "abcd", 4);
  }

for (i = 0; i < 10000; i++)
  {
    g_assert (array->data[4*i] == 'a');
    g_assert (array->data[4*i+1] == 'b');
    g_assert (array->data[4*i+2] == 'c');
    g_assert (array->data[4*i+3] == 'd');
  }

g_byte_array_free (array, TRUE);


如果您对表示字节序列的不可变对象感兴趣,请参阅 GBytes

 单向链表


GSList 结构及其关联函数提供标准的单链表数据结构。这种数据结构的好处是提供 O(1) 复杂性的插入/删除操作,其中访问/搜索操作为 O(n)。与 GList(双向链表)相比,GSList 的好处是它们在空间上更轻,因为它们只需要保留一个指针,但它的成本是最坏情况下访问/搜索操作的两倍。


列表中的每个元素都包含一条数据,以及一个链接到列表中下一个元素的指针。使用此指针,可以仅在一个方向上在列表中移动(与双向链表不同,双向链表允许在两个方向上移动)。


每个元素中包含的数据可以是整数值(通过使用类型转换宏之一),也可以是指向任何类型数据的指针。


请注意,大多数 GSList 函数都希望传递指向列表中第一个元素的指针。插入元素的函数返回列表的新开头,该列表可能已更改。


没有用于创建 GSList 的函数。NULL 被视为空列表,因此您只需将 GSList* 设置为 NULL。


要添加元素,请使用 g_slist_append()、g_slist_prepend()、g_slist_insert()g_slist_insert_sorted()。


要删除元素,请使用 g_slist_remove()。


要在列表中查找元素,请使用 g_slist_last()、g_slist_next()、g_slist_nth()g_slist_nth_data()、g_slist_find()g_slist_find_custom()。


要查找元素的索引,请使用 g_slist_position()g_slist_index()。


要为列表中的每个元素调用函数,请使用 g_slist_foreach()。


要释放整个列表,请使用 g_slist_free()。

 双向链表


GList 结构及其关联函数提供标准的双向链表数据结构。这种数据结构的好处是提供 O(1) 复杂性的插入/删除操作,其中访问/搜索操作在 O(n) 中。与 GSList(单链表)相比,GList 的好处是访问/搜索操作的最坏情况被除以 2,这是以空间为代价的,因为我们需要保留两个指针而不是一个指针。


列表中的每个元素都包含一条数据,以及链接到列表中的 previous 和 next 元素的指针。使用这些指针,可以在两个方向上移动列表(与单链接的 GSList 不同,它只允许向前移动列表)。


双向链表 不跟踪项目数,也不跟踪列表的开头和结尾。如果您想快速访问列表的开头和结尾,和/或列表中的项数,请改用 GQueue


每个元素中包含的数据可以是整数值(通过使用类型转换宏之一),也可以是指向任何类型数据的指针。


请注意,大多数 GList 函数都希望传递指向列表中第一个元素的指针。插入元素的函数返回列表的新开头,该列表可能已更改。


没有用于创建 GList 的函数。NULL 被视为有效的空列表,因此您只需将 GList* 设置为 NULL 即可对其进行初始化。


要添加元素,请使用 g_list_append()、g_list_prepend()、g_list_insert()g_list_insert_sorted()。


要访问列表中的所有元素,请在列表上使用循环:

GList *l;
for (l = list; l != NULL; l = l->next)
  {
    // do something with l->data
  }


要为列表中的每个元素调用函数,请使用 g_list_foreach()。


要遍历列表并对其进行修改(例如,删除某个元素),while 循环更合适,例如:

GList *l = list;
while (l != NULL)
  {
    GList *next = l->next;
    if (should_be_removed (l))
      {
        // possibly free l->data
        list = g_list_delete_link (list, l);
      }
    l = next;
  }


要删除元素,请使用 g_list_remove()。


要在列表中导航,请使用 g_list_first()、g_list_last()、g_list_next()g_list_previous()。


要在列表中查找元素,请使用 g_list_nth()、g_list_nth_data()、g_list_find()g_list_find_custom()。


要查找元素的索引,请使用 g_list_position()g_list_index()。


要释放整个列表,请使用 g_list_free()g_list_free_full()。

 哈希表


GHashTable 提供键和值之间的关联,该关联经过优化,因此在给定键的情况下,可以在摊销 O(1) 中找到、插入或删除关联的值。通过每个元素的所有操作都需要 O(n) 时间(列出所有键/值、调整表大小等)。


请注意,插入到 GHashTable 中时,不会复制键和值,因此它们必须在 GHashTable 的生存期内存在。这意味着使用静态字符串是可以的,但临时字符串(即在缓冲区中创建的字符串和 GTK 小部件返回的字符串)应在插入之前使用 g_strdup() 复制。


如果键或值是动态分配的,则必须小心确保在从 GHashTable 中删除键或值时释放它们,以及当它们被 GHashTable 中的新插入覆盖时。也不建议在 GHashTable 中混合使用静态字符串和动态分配的字符串,因为这样就很难确定是否应该释放字符串。


要创建 GHashTable,请使用 g_hash_table_new()。


要将键和值插入 GHashTable,请使用 g_hash_table_insert()。


要查找与给定键对应的值,请使用 g_hash_table_lookup() g_hash_table_lookup_extended() .


g_hash_table_lookup_extended() 也可用于简单地检查哈希表中是否存在键。


要删除键和值,请使用 g_hash_table_remove()。


要为每个键值对调用函数,请使用 g_hash_table_foreach() 或使用迭代器迭代哈希表中的键/值对,请参阅 GHashTableIter。哈希表的迭代顺序未定义,您不得依赖于按照插入键/值的相同顺序迭代键/值。


要销毁 GHashTable,请使用 g_hash_table_unref()g_hash_table_destroy()。


哈希表的一个常见用例是存储有关一组键的信息,而不将任何特定值与每个键相关联。GHashTable 优化了一种方法:如果您只存储键 == value 的键值对,则 GHashTable 不会分配内存来存储值,如果您的集合很大,这可能会节省大量空间。函数 g_hash_table_add()g_hash_table_contains() 旨在以这种方式使用 GHashTable 时使用。


GHashTable 并非设计为使用编译时已知的键和值进行静态初始化。要构建静态哈希表,请使用 gperf 等工具。

 双端队列


GQueue 结构及其关联函数提供标准队列数据结构。在内部,GQueue 使用与 GList 相同的数据结构来存储与插入/删除 (O(1)) 和访问/搜索 (O(n)) 操作具有相同复杂性的元素。


每个元素中包含的数据可以是整数值(通过使用类型转换宏之一),也可以是指向任何类型数据的指针。


与所有其他 GLib 数据结构一样,GQueue 不是线程安全的。对于线程安全队列,请使用 GAsyncQueue


要创建新的 GQueue,请使用 g_queue_new()。


要初始化静态分配的 GQueue,请使用 G_QUEUE_INITg_queue_init()。


要添加元素,请使用 g_queue_push_head()、g_queue_push_head_link()、g_queue_push_tail()g_queue_push_tail_link()。


要删除元素,请使用 g_queue_pop_head()g_queue_pop_tail()。


要释放整个队列,请使用 g_queue_free()。

 异步队列


通常需要在不同线程之间进行通信。一般来说,不通过共享内存而是通过显式消息传递来执行此操作会更安全。不过,这些消息仅对多线程应用程序异步有意义,因为同步操作也可以在同一线程中完成。


异步队列是大多数其他 GLib 数据结构的一个例外,因为它们可以从多个线程中同时使用,而无需显式锁定,并且它们带有自己的内置引用计数。这是因为异步队列的性质是它将始终由至少 2 个并发线程使用。


要使用异步队列,您首先必须使用 g_async_queue_new() 创建一个异步队列。GAsyncQueue 结构体是引用计数的,请使用 g_async_queue_ref()g_async_queue_unref() 来管理引用。


想要向该队列发送消息的线程只需调用 g_async_queue_push() 即可将消息推送到队列。


期望来自异步队列的消息的线程只需为该队列调用 g_async_queue_pop()。如果此时队列中没有可用的消息,则线程现在将进入休眠状态,直到消息到达。该消息将从队列中删除并返回。函数 g_async_queue_try_pop()g_async_queue_timeout_pop() 分别可用于仅检查消息是否存在或仅等待消息的特定时间。


几乎每个函数都存在两个变体,一个用于锁定队列,另一个不锁定队列。这样你就可以在多个队列访问指令上持有队列锁(使用 g_async_queue_lock() 获取它,并使用 g_async_queue_unlock() 释放它。这可能是确保队列完整性所必需的,但只有在真正必要时才应使用,因为如果使用不当,它会使您的生活更加困难。通常,您应该只使用锁定功能变体 (没有 _unlocked 后缀的变量)。


在许多情况下,当您需要将工作分配给一组工作线程时,使用 GThreadPool 可能比手动使用 GAsyncQueue 更方便。GThreadPool 在内部使用 GAsyncQueue

 二叉树


GTree 结构及其关联函数提供键/值对的排序集合,该集合已针对按顺序搜索和遍历进行了优化。这意味着 GTree 上的大多数操作(访问、搜索、插入、删除等)平均为 O(log(n)),最坏情况下为 O(n)。但是,请注意,保持 n 个元素的平衡排序 GTree 是在时间 O(n log(n)) 完成的。


要创建新的 GTree,请使用 g_tree_new()。


要将键/值对插入 GTree,请使用 g_tree_insert() (O(n log(n)))。


要删除键/值对,请使用 g_tree_remove() (O(n log(n)))。


要查找与给定键对应的值,请使用 g_tree_lookup()g_tree_lookup_extended()。


要找出 GTree 中的节点数,请使用 g_tree_nnodes()。要获取 GTree 的高度,请使用 g_tree_height()。


要遍历 GTree,为遍历中访问的每个节点调用函数,请使用 g_tree_foreach()。


要销毁 GTree,请使用 g_tree_destroy()。

 N 元树


GNode 结构及其关联函数提供 N 元树数据结构,其中树中的节点可以包含任意数据。


要创建新树,请使用 g_node_new()。


要将节点插入到树中,请使用 g_node_insert()、g_node_insert_before()、g_node_append()g_node_prepend()。


要创建新节点并将其插入到树中,请使用 g_node_insert_data()、g_node_insert_data_after()、g_node_insert_data_before()、g_node_append_data()g_node_prepend_data()。


要反转节点的子节点,请使用 g_node_reverse_children()。


要查找节点,请使用 g_node_get_root()、g_node_find()、g_node_find_child()、g_node_child_index()g_node_child_position()、g_node_first_child()g_node_last_child()g_node_nth_child()g_node_first_sibling()g_node_prev_sibling()、g_node_next_sibling()g_node_last_sibling()


要获取有关节点或树的信息,请使用 G_NODE_IS_LEAF()、G_NODE_IS_ROOT()、g_node_depth()g_node_n_nodes()g_node_n_children()、g_node_is_ancestor()g_node_max_height()。


要遍历树,为遍历中访问的每个节点调用函数,请使用 g_node_traverse()g_node_children_foreach()。


要从树中删除节点或子树,请使用 g_node_unlink()g_node_destroy()。

 可扩展列表


GSequence 数据结构具有列表的 API,但在内部使用平衡的二叉树实现。这意味着 GSequence 上的大多数操作(访问、搜索、插入、删除等)平均为 O(log(n)),最坏情况下为 O(n)。但是,请注意,维护 n 个元素的平衡排序列表是在时间 O(n log(n)) 中完成的。每个元素中包含的数据可以是整数值(通过使用类型转换宏),也可以是指向任何类型数据的指针。


GSequence 是通过 “iterators” 访问的,由 GSequenceIter 表示。迭代器表示序列的两个元素之间的位置。例如,“begin”迭代器表示序列的第一个元素之前的间隙,而“end”迭代器表示最后一个元素之后的间隙。在空序列中,开始迭代器和结束迭代器相同。


GSequence 上的某些方法对项目范围进行操作。例如,g_sequence_foreach_range() 将对具有给定范围的每个元素调用用户指定的函数。范围由传入的迭代器表示的间隙分隔,因此,如果传入 begin 和 end 迭代器,则所讨论的范围是整个序列。


函数 g_sequence_get() 与迭代器一起使用,以访问紧跟在迭代器表示的间隙之后的元素。迭代器被称为 “指向” 该元素。


迭代器在 GSequence 上的大多数操作中都是稳定的。例如,指向序列的某个元素的迭代器将继续指向该元素,即使在序列排序后也是如此。即使使用例如 g_sequence_move_range() 将元素移动到另一个序列也不会使指向它的迭代器无效。唯一会使迭代器失效的操作是当它指向的元素从任何序列中删除时。


要对数据进行排序,请使用 g_sequence_insert_sorted() g_sequence_insert_sorted_iter() 将数据添加到 GSequence,或者,如果要添加大量数据,则在执行未排序的插入后调用 g_sequence_sort()g_sequence_sort_iter() 会更有效。


引用计数字符串


引用计数字符串是普通的 C 字符串,它们已使用引用计数进行扩充以管理其资源。您可以分配一个新的引用计数字符串,并根据需要获取和释放引用,而不是在调用方之间复制字符串;当 String 的最后一个引用被释放时,为其分配的资源将被释放。


通常,在解析文件中的数据并将其存储到传递给各种调用者的数据结构中时,可以使用引用计数字符串:

PersonDetails *
person_details_from_data (const char *data)
{
  // Use g_autoptr() to simplify error cases
  g_autoptr(GRefString) full_name = NULL;
  g_autoptr(GRefString) address =  NULL;
  g_autoptr(GRefString) city = NULL;
  g_autoptr(GRefString) state = NULL;
  g_autoptr(GRefString) zip_code = NULL;

  // parse_person_details() is defined elsewhere; returns refcounted strings
  if (!parse_person_details (data, &full_name, &address, &city, &state, &zip_code))
    return NULL;

  if (!validate_zip_code (zip_code))
    return NULL;

  // add_address_to_cache() and add_full_name_to_cache() are defined
  // elsewhere; they add strings to various caches, using refcounted
  // strings to avoid copying data over and over again
  add_address_to_cache (address, city, state, zip_code);
  add_full_name_to_cache (full_name);

  // person_details_new() is defined elsewhere; it takes a reference
  // on each string
  PersonDetails *res = person_details_new (full_name,
                                           address,
                                           city,
                                           state,
                                           zip_code);

  return res;
}


在上面的示例中,我们有多个函数为不同的用途采用相同的字符串;对于典型的 C 字符串,每次数据的生命周期规则与从原始缓冲区解析的字符串的生命周期不同时,我们都必须复制字符串。使用引用计数字符串,每个调用方都可以对数据进行引用,并在需要拥有字符串时保留该引用。


引用计数的字符串也可以“暂存”在 GLib 拥有的全局表中;虽然实习字符串至少有一个引用,但创建具有相同内容的新实习引用计数字符串将返回对现有字符串的引用,而不是创建新的引用计数字符串实例。一旦字符串丢失其最后一个引用,它将自动从全局暂存字符串表中删除。


引用计数字符串在 2.58 中被添加到 GLib 中。