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