灵活的数组成员和指针成员:优点和缺点?

时间:2019-01-13 00:58:20

标签: c

使用灵活数组成员(FAM)或指针成员有什么区别?在这两种情况下,必须完成一个malloc和一个元素的影响。但是,对于FAM,将为整个结构进行内存分配,而对于ptr成员,将仅对ptr成员进行内存分配(请参见代码)。这两种方法的利弊是什么?

#include <stdio.h>
#include <stdlib.h>

typedef struct farr_mb {
    int lg;
    int arr[];
} Farr_mb;

typedef struct ptr_mb {
    int lg;
    int * ptr;
} Ptr_mb;

 int main() {

    int lg=5;

    Farr_mb *a=malloc(sizeof(Farr_mb)+lg*sizeof(int));
    Ptr_mb b; b.ptr=malloc(lg*sizeof(int));

    for (int i=0;i<lg;i++) (a->arr)[i]=i;
    for (int i=0;i<lg;i++) (b.ptr)[i]=i;

    for (int i=0;i<lg;i++) printf("%d \t",(a->arr)[i]=i);
    printf("\n");
    for (int i=0;i<lg;i++) printf("%d \t",(b.ptr)[i]=i);

    return 0;
}

1 个答案:

答案 0 :(得分:5)

在探讨利弊之前,让我们先看一些真实的例子。

假设我们希望实现一个哈希表,其中每个条目都是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 }

这实际上使用了。哈希表本身是具有两个成员的结构。 size成员指示哈希表的大小,而entry成员是指向哈希表条目指针数组的指针。这样,每个未使用的条目只是一个NULL指针。将元素添加到哈希表条目时,整个struct entry可以重新分配(对于sizeof (struct entry) + allocates * sizeof (element)或释放,只要entrystruct hash_table成员中的相应指针会相应地更新。

如果我们改用element *array,则需要在struct hash_entry *entry:中使用struct hash_table;或将struct hash_entry与数组分开分配;或在单个块中同时分配struct hash_entry和数组,而array指针指向同一struct hash_entry之后。

这样做的代价是为每个未使用的哈希表槽使用两个额外的size_t s内存,以及访问element时一个额外的指针取消引用。 (或者,要获取数组的地址,请使用两个连续的指针取消引用,而不是一个指针取消引用加偏移量。)如果这是在实现中大量使用的键结构,则此开销可以在性能分析中看到,并对缓存性能产生负面影响。对于随机访问,元素array越大,则存在 less 的差异。当array较小且与allocatedused成员位于同一缓存行(或几条缓存行)内时,成本最大。

我们通常不希望使entry中的struct hash_table成员成为灵活的数组成员,因为这意味着您不再可以使用struct hash_table my_table = HASH_TABLE_INITIALIZER;静态声明哈希表;您将需要使用指向表的指针和初始化函数:struct hash_table *my_table; my_table = hash_table_init();或类似的函数。

我确实使用指针成员和灵活数组成员都拥有another example的相关数据结构。它允许一个人使用matrix类型的变量来表示具有double个条目的任何2D矩阵,即使矩阵是另一个视图(例如,转置,块,行或列向量,甚至对角线向量);这些视图都是相等的(例如在GNU科学库中,矩阵视图由单独的数据类型表示)。这种矩阵表示方法使编写鲁棒的数字线性代数代码变得容易,并且与使用GSL或BLAS + LAPACK时相比,其后的代码更具可读性。我认为是。


因此,让我们从如何选择使用哪种方法的角度来看一下优缺点。 (因此,由于确定情况取决于上下文和每个特定用例,因此我不会将任何功能指定为“ pro”或“ con”。)

  • 具有灵活数组成员的结构无法静态初始化。您只能通过指针引用它们。

    您可以使用指针成员声明和初始化结构。如上面的示例所示,使用预处理程序初始化程序宏可能意味着您不需要初始化程序功能。例如,即使struct hash_table *table为NULL,接受realloc(table->entry, newsize * sizeof table->entry[0])参数的函数也始终可以使用table->entry调整指针数组的大小。这样可以减少所需功能的数量,并简化其实现。

  • 通过指针成员访问数组可能需要额外的指针取消引用。

    如果我们将静态初始化结构中对数组的访问与指向该数组的指针进行比较,将其与具有通过静态指针引用的灵活数组成员的结构进行比较,则会进行相同数量的取消引用。

    如果我们有一个将结构地址作为参数的函数,则通过指针成员访问数组元素需要两个指针解除引用,而访问一个灵活的数组元素仅需要一个指针解除引用和一个偏移量。如果数组元素足够小并且数组索引足够小,以使得所访问的数组元素位于同一高速缓存行中,则灵活数组成员的访问通常会快得多。对于较大的阵列,性能上的差异往往很小。但是,这在硬件体系结构之间确实有所不同。

  • 通过指针成员重新分配数组对于使用该结构作为不透明变量的用户而言,隐藏了复杂性。

    这意味着如果我们有一个函数可以接收指向结构的指针作为参数,并且该结构具有指向动态分配的数组的指针,则该函数可以重新分配该数组,而调用方不会看到结构地址的任何变化本身(仅更改 contents 结构)。

    但是,如果我们有一个函数来接收指向具有灵活数组成员的结构的指针,则重新分配数组意味着重新分配整个结构。这可能会修改结构的地址。由于指针是按值传递的,因此修改对调用者不可见。因此,可以调整灵活数组成员大小的函数必须接收一个指向带有灵活数组成员的结构的指针。

    如果函数仅使用灵活的数组成员检查结构的内容,例如对满足某些条件的元素的数量进行计数,则指向该结构的指针就足够了;指针和指向的数据都可以标记为const。这可能有助于编译器生成更好的代码。此外,所有访问的数据在内存中都是线性的,这有助于更复杂的处理器更有效地管理缓存。 (要对具有指针成员的数组执行相同的操作,则需要至少将指针传递给该数组以及size字段作为计数函数的参数,而不是传递给包含这些值的结构的指针。)

  • 具有灵活数组成员的未使用/空结构可以由NULL指针(指向此类结构)表示。当您有一个数组数组时,这可能很重要。

    对于具有灵活数组成员的结构,外部数组只是一个指针数组。对于具有指针成员的结构,外部数组可以是结构的数组,也可以是指向结构的指针的数组。

    如果结构作为第一个成员具有公共的类型标记,并且您使用这些结构的并集,则两者都可以支持不同类型的子数组。 (在这种情况下,“使用”的含义是令人争议的。有些人声称您需要通过联合访问数组,我声称这种联合的可见性就足够了,因为其他任何事情都会破坏大量现有的POSIX C代码;基本上所有使用套接字的服务器端C代码。)

这些是我现在可以想到的主要内容。两种形式在我自己的代码中都无处不在,而且我都没有遇到任何问题。 (特别是,我更喜欢使用无结构的辅助函数,该函数使结构中毒以帮助在早期测试中检测释放后使用的错误;而且我的程序通常不存在任何与内存相关的问题。)

如果我错过了重要的方面,我将编辑上面的列表。因此,如果您有建议或认为我忽略了上述内容,请在评论中告知我,以便我进行适当的验证和编辑。