在讨论优缺点之前,让我们先看看一些现实世界的例子。
假设我们希望实现一个哈希表,其中每个条目是elements 的动态管理数组:
struct hash_entry {
size_t allocated;
size_t used;
element array[];
};
struct hash_table {
size_t size;
struct hash_entry **entry;
};
#define HASH_TABLE_INITIALIZER { 0, NULL }
这实际上使用了both。哈希表本身是一个有两个成员的结构。 size 成员表示哈希表的大小,entry 成员是指向哈希表条目指针数组的指针。这样,每个未使用的条目只是一个NULL 指针。向哈希表条目添加元素时,可以重新分配整个struct entry(对于sizeof (struct entry) + allocates * sizeof (element) 或释放,只要相应更新struct hash_table 中的entry 成员中的相应指针即可。
如果我们使用element *array 代替,我们需要在struct hash_table 中使用struct hash_entry *entry:;或从数组中单独分配struct hash_entry;或者在单个块中分配struct hash_entry 和数组,array 指针指向相同的struct hash_entry 之后。
这样做的代价是为每个未使用的哈希表槽使用两个额外的 size_ts 内存,以及访问 elements 时额外的指针取消引用。 (或者,要获得数组的地址,需要两个连续的指针解引用,而不是一个指针解引用加上偏移量。)如果这是在实现中大量使用的关键结构,则该成本可以在分析中看到,并对缓存性能产生负面影响.但是,对于随机访问,元素array 越大,less 差异也存在;当arrays 较小并且与allocated 和used 成员位于同一高速缓存行(或几个高速缓存行)内时,成本最高。
我们通常不想让struct hash_table 中的entry 成员成为灵活的数组成员,因为这意味着您不再可以使用struct hash_table my_table = HASH_TABLE_INITIALIZER; 静态声明哈希表;您需要使用指向表的指针和初始化函数:struct hash_table *my_table; my_table = hash_table_init(); 或类似的。
我确实有another example 使用指针成员和灵活数组成员的相关数据结构。它允许使用matrix 类型的变量来表示具有double 条目的任何二维矩阵,即使矩阵是另一个矩阵的视图(例如,转置、块、行或列向量,甚至是对角线)向量);这些视图都是相等的(与 GNU 科学库中的矩阵视图不同,其中矩阵视图由单独的数据类型表示)。这种矩阵表示方法使编写稳健的数值线性代数代码变得容易,并且随后的代码比使用 GSL 或 BLAS+LAPACK 时更具可读性。在我看来,是的。
所以,让我们从如何选择使用哪种方法的角度来看看利弊。 (出于这个原因,我不会将任何功能指定为“赞成”或“反对”,因为确定取决于上下文,取决于每个特定的用例。)
-
具有灵活数组成员的结构不能静态初始化。您只能通过指针引用它们。
您可以使用指针成员声明和初始化结构。如上例所示,使用预处理器初始化器宏可能意味着您不需要初始化器函数。例如,接受struct hash_table *table 参数的函数始终可以使用realloc(table->entry, newsize * sizeof table->entry[0]) 调整指针数组的大小,即使table->entry 为NULL。这减少了所需函数的数量,并简化了它们的实现。
-
通过指针成员访问数组可能需要额外的指针解引用。
如果我们将静态初始化结构中的数组访问与指向数组的指针进行比较,与具有通过静态指针引用的灵活数组成员的结构进行比较,则会进行相同数量的取消引用。
如果我们有一个获取结构地址作为参数的函数,那么通过指针成员访问数组元素需要两次指针解引用,而访问灵活数组元素只需要一次指针解引用和一次偏移。如果数组元素足够小,数组索引也足够小,以至于被访问的数组元素在同一个缓存行中,那么灵活数组成员的访问速度通常会明显更快。对于较大的阵列,性能差异往往是微不足道的。但是,这在硬件架构之间确实有所不同。
-
通过指针成员重新分配数组隐藏了使用结构作为不透明变量的复杂性。
这意味着如果我们有一个函数接收指向结构的指针作为参数,并且该结构有一个指向动态分配数组的指针,则该函数可以重新分配该数组,而调用者不会看到结构地址的任何变化本身(只有结构内容改变)。
但是,如果我们有一个函数接收指向具有灵活数组成员的结构的指针,那么重新分配数组意味着重新分配整个结构。这可能会修改结构的地址。因为指针是按值传递的,所以调用者看不到修改。因此,可以调整灵活数组成员大小的函数必须接收指向具有灵活数组成员的结构的指针。
如果函数只检查具有灵活数组成员的结构的内容,例如计算满足某些条件的元素的数量,那么指向该结构的指针就足够了;并且指针和指向的数据都可以标记为const。这可能有助于编译器生成更好的代码。此外,所有访问的数据在内存中都是线性的,这有助于更复杂的处理器更有效地管理缓存。 (要对具有指针成员的数组执行相同操作,需要将指向数组的指针以及至少大小字段作为参数传递给计数函数,而不是指向包含这些值的结构的指针.)
-
具有灵活数组成员的未使用/空结构可以由 NULL 指针(指向此类结构)表示。当你有一个数组时,这可能很重要。
对于具有灵活数组成员的结构,外部数组只是一个指针数组。对于具有指针成员的结构,外部数组可以是结构数组,也可以是指向结构的指针数组。
如果结构有一个共同的类型标记作为第一个成员,并且您使用这些结构的联合,则两者都可以支持不同类型的子数组。 (不幸的是,在这种情况下,“使用”的含义是有争议的。有人声称您需要通过联合访问数组,我声称这样的联合的可见性就足够了,因为其他任何事情都会破坏大量现有的 POSIX C 代码;基本上所有使用套接字的服务器端 C 代码。)
这些是我现在能想到的主要内容。这两种形式在我自己的代码中都很普遍,而且我都没有遇到任何问题。 (特别是,我更喜欢在早期测试中使用使结构中毒的结构释放辅助函数,以帮助检测释放后使用的错误;而且我的程序通常不会出现任何与内存相关的问题。)
如果我发现我遗漏了重要方面,我将编辑上面的列表。因此,如果您有任何建议或认为我忽略了上面的某些内容,请在评论中告诉我,以便我进行适当的验证和编辑。