【问题标题】:C: pointer to array of pointers to structures (allocation/deallocation issues)C:指向结构指针数组的指针(分配/解除分配问题)
【发布时间】:2013-03-02 02:53:41
【问题描述】:

我一直在回到 C 语言中,但我很难记住这种内存管理的大部分工作原理。我想要一个指向结构指针数组的指针。

说我有:

struct Test {
   int data;
};

然后是数组:

struct Test **array1;

这是正确的吗?我的问题是处理这件事。所以数组中的每个指针都指向单独分配的东西。但我认为我需要先这样做:

array1 = malloc(MAX * sizeof(struct Test *));

我无法理解上述内容。我需要这样做吗,为什么需要这样做?特别是,如果我要为指针指向的每个事物分配内存,那么为指针分配内存意味着什么?

现在假设我有一个指向结构指针数组的指针。我现在希望它指向我之前创建的同一个数组。

struct Test **array2;

我是否需要像上面那样为指针分配空间,或者我可以这样做:

array2 = array1

【问题讨论】:

  • 你想要一个指向结构的实际指针数组吗?就像在声明的数组中一样,您为每个元素分配一个结构?
  • 嗯,我想要一个指向数组的指针,我可以在其中执行您所说的操作。
  • 我知道。但是你想要一个 real 数组来保存它们吗?我的意思不仅仅是指向一块内存的指针。
  • 使用适当的数组会更简单 - 如果您愿意,我可以发布一个示例。

标签: c pointers memory memory-management


【解决方案1】:

结构与其他对象没有太大区别。让我们从字符开始:

char *p;
p = malloc (CNT * sizeof *p);

*p 是一个字符,所以 sizeof *p 是 sizeof (char) == 1;我们分配了 CNT 字符。下一个:

char **pp;
pp = malloc (CNT * sizeof *pp);

*p 是一个指向字符的指针,所以sizeof *pp 是 sizeof (char*)。我们分配了 CNT 指针。下一个:

struct something *p;
p = malloc (CNT * sizeof *p);

*p 是一个结构体,所以 sizeof *p 是 sizeof (结构体)。我们为 CNT 结构分配了一些东西。下一个:

struct something **pp;
pp = malloc (CNT * sizeof *pp);

*pp 是一个指向结构的指针,所以sizeof *pp 是 sizeof (struct something*)。我们分配了 CNT 指针。

【讨论】:

  • @Yar 可能是。它也可能是 4,甚至可能是 2……这无关紧要。这也是sizeof存在的原因。
【解决方案2】:

我建议您使用 typdefs 一次构建一个层来创建类型层。这样一来,所需的不同类型就会更加清晰。

例如:

typedef struct Test {
   int data;
} TestType;

typedef  TestType * PTestType;

这将创建两种新类型,一种用于结构,另一种用于指向结构的指针。

所以接下来如果你想要一个结构数组,那么你可以使用:

TestType array[20];  // creates an array of 20 of the structs

如果你想要一个指向结构的指针数组,那么你可以使用:

PTestType array2[20];  // creates an array of 20 of pointers to the struct

然后,如果您想将结构分配到数组中,您可以执行以下操作:

PTestType  array2[20];  // creates an array of 20 of pointers to the struct
// allocate memory for the structs and put their addresses into the array of pointers.
for (int i = 0; i < 20; i++) {
    array2 [i] = malloc (sizeof(TestType));
}

C 不允许您将一个数组分配给另一个数组。您必须改为使用循环将一个数组的每个元素分配给另一个数组的元素。

编辑:另一种有趣的方法

另一种方法是更加面向对象的方法,您可以在其中封装一些东西。例如,使用相同的类型层我们创建了两种类型:

typedef struct _TestData {
    struct {
        int myData;   // one or more data elements for each element of the pBlob array
    } *pBlob;
    int nStructs;         // count of number of elements in the pBlob array
} TestData;

typedef TestData *PTestData;

接下来我们有一个帮助函数,我们用它来创建对象,命名为CreateTestData (int nArrayCount)。

PTestData  CreateTestData (int nCount)
{
    PTestData ret;

    // allocate the memory for the object. we allocate in a single piece of memory
    // the management area as well as the array itself.  We get the sizeof () the
    // struct that is referenced through the pBlob member of TestData and multiply
    // the size of the struct by the number of array elements we want to have.
    ret = malloc (sizeof(TestData) + sizeof(*(ret->pBlob)) * nCount);
    if (ret) {   // make sure the malloc () worked.
            // the actual array will begin after the end of the TestData struct
        ret->pBlob = (void *)(ret + 1);   // set the beginning of the array
        ret->nStructs = nCount;           // set the number of array elements
    }

    return ret;
}

现在我们可以在下面的源代码段中使用我们的新对象。它应该检查从 CreateTestData() 返回的指针是否有效,但这实际上只是为了说明可以做什么。

PTestData  go = CreateTestData (20);
{
    int i = 0;
    for (i = 0; i < go->nStructs; i++) {
        go->pBlob[i].myData = i;
    }
}

在真正动态的环境中,您可能还需要一个 ReallocTestData(PTestData p) 函数,该函数将重新分配 TestData 对象,以修改对象中包含的数组的大小。

使用这种方法,当您使用完特定的 TestData 对象后,您可以像在 free (go) 中一样释放该对象,并且同时释放该对象及其数组。

编辑:进一步扩展

有了这个封装的类型,我们现在可以做一些其他有趣的事情。例如,我们可以有一个复制函数PTestType CreateCopyTestData (PTestType pSrc),它会创建一个新实例,然后将参数复制到一个新对象。在下面的示例中,我们重用了函数PTestType CreateTestData (int nCount),它将使用我们正在复制的对象的大小创建我们类型的实例。在创建新对象之后,我们从源对象复制数据。最后一步是修复源对象中指向其数据区的指针,使新对象中的指针现在指向自身的数据区,而不是旧对象的数据区。

PTestType CreateCopyTestData (PTestType pSrc)
{
    PTestType pReturn = 0;

    if (pSrc) {
        pReturn = CreateTestData (pSrc->nStructs);

        if (pReturn) {
            memcpy (pReturn, pSrc, sizeof(pTestType) + pSrc->nStructs * sizeof(*(pSrc->pBlob)));
            pReturn->pBlob = (void *)(pReturn + 1);   // set the beginning of the array
        }
    }

    return pReturn;
}

【讨论】:

    【解决方案3】:

    分配数组

    使用分配的数组很容易理解。

    声明你的指针数组。该数组中的每个元素都指向一个struct Test:

    struct Test *array[50];
    

    然后根据需要分配和分配指向结构的指针。使用循环很简单:

    array[n] = malloc(sizeof(struct Test));
    

    然后声明一个指向这个数组的指针:

                                   // an explicit pointer to an array 
    struct Test *(*p)[] = &array;  // of pointers to structs
    

    这允许您使用(*p)[n]-&gt;data;引用第 n 个成员。

    如果这些内容令人困惑,请不要担心。这可能是 C 语言中最困难的方面。


    动态线性阵列

    如果你只想分配一个结构块(实际上是一个结构数组,不是指向结构的指针),并且有一个指向该块的指针,你可以更容易地做到:

    struct Test *p = malloc(100 * sizeof(struct Test));  // allocates 100 linear
                                                         // structs
    

    然后你可以指向这个指针:

    struct Test **pp = &p
    

    您不再有指向结构的指针数组,但它大大简化了整个事情。


    动态分配结构的动态数组

    最灵活,但并不经常需要。它与第一个示例非常相似,但需要额外分配。我已经编写了一个完整的程序来演示这个应该可以正常编译。

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    
    struct Test {
        int data;
    };
    
    int main(int argc, char **argv)
    {
        srand(time(NULL));
    
        // allocate 100 pointers, effectively an array
        struct Test **t_array = malloc(100 * sizeof(struct Test *));
    
        // allocate 100 structs and have the array point to them
        for (int i = 0; i < 100; i++) {
            t_array[i] = malloc(sizeof(struct Test));
        }
    
        // lets fill each Test.data with a random number!
        for (int i = 0; i < 100; i++) {
            t_array[i]->data = rand() % 100;
        }
    
        // now define a pointer to the array
        struct Test ***p = &t_array;
        printf("p points to an array of pointers.\n"
           "The third element of the array points to a structure,\n"
           "and the data member of that structure is: %d\n", (*p)[2]->data);
    
        return 0;
    }
    

    输出:

    > p points to an array of pointers.
    > The third element of the array points to a structure,
    > and the data member of that structure is: 49
    

    或整套:

    for (int i = 0; i < 100; i++) {
        if (i % 10 == 0)
            printf("\n");
        printf("%3d ", (*p)[i]->data);
    }
    
     35  66  40  24  32  27  39  64  65  26 
     32  30  72  84  85  95  14  25  11  40 
     30  16  47  21  80  57  25  34  47  19 
     56  82  38  96   6  22  76  97  87  93 
     75  19  24  47  55   9  43  69  86   6 
     61  17  23   8  38  55  65  16  90  12 
     87  46  46  25  42   4  48  70  53  35 
     64  29   6  40  76  13   1  71  82  88 
     78  44  57  53   4  47   8  70  63  98 
     34  51  44  33  28  39  37  76   9  91 
    

    单动态分配结构的动态指针数组

    最后一个例子是相当具体的。正如我们在前面的示例中看到的,它是一个动态的指针数组,但与这些不同的是,元素都在 single 分配中分配。这有其用途,最值得注意的是在不同配置中对数据进行排序,同时保持原始分配不受干扰。

    我们首先分配一个元素块,就像我们在最基本的单块分配中所做的那样:

    struct Test *arr = malloc(N*sizeof(*arr));
    

    现在我们分配一个单独的指针块:

    struct Test **ptrs = malloc(N*sizeof(*ptrs));
    

    然后,我们用原始数组之一的地址填充指针列表中的每个槽。由于指针算法允许我们从一个元素移动到另一个元素地址,这很简单:

    for (int i=0;i<N;++i)
        ptrs[i] = arr+i;
    

    此时以下两个都指的是同一个元素字段

    arr[1].data = 1;
    ptrs[1]->data = 1;
    

    在查看以上内容后,我希望清楚为什么。

    当我们处理完指针数组和原始块数组后,它们被释放为:

    free(ptrs);
    free(arr);
    

    注意:我们不会单独释放 ptrs[] 数组中的每个项目。这不是他们的分配方式。它们被分配为单个块(由arr 指向),这就是它们应该被释放的方式。

    那么为什么有人要这样做呢?几个原因。

    首先,它从根本上减少了内存分配调用的次数。而不是N+1(一个用于指针数组,N 用于单个结构)您现在只有 两个:一个用于数组块,一个用于指针数组。内存分配是程序可以请求的最昂贵的操作之一,并且在可能的情况下,最好将它们最小化(注意:文件 IO 是另一个,仅供参考)。

    另一个原因:同一基本数据数组的多种表示形式。假设您想对数据进行升序和降序排序,并同时同时使用两种排序表示。您可以复制数据数组,但这需要大量复制并占用大量内存。相反,只需分配一个额外的指针数组并用基数组中的地址填充它,然后对该指针数组进行排序。当要排序的数据很大(每个项目可能是千字节,甚至更大)时,这具有特别显着的好处。原始项目保留在基本数组中的原始位置,但是现在您有了一个非常有效的机制,您可以在其中对它们进行排序无需实际移动它们。您对指向项目的指针数组进行排序;这些项目根本不会移动。

    我意识到这是一个非常多的内容,但是指针的使用对于理解 C 语言可以做的许多强大的事情至关重要,所以请阅读书籍并不断刷新你的记忆。它会回来的。

    【讨论】:

    • 假设我有另一个结构Test2,它保存这个指向数组的指针。我将如何在堆上分配它?结构测试2 { 结构测试 *array[50]; }; struct Test2 *container = malloc(sizeof(Test2)) 够了吗?
    • @DillPixel:这是在第二个结构中声明指针数组本身。如果你只是想让一个结构指向数组,你只需要定义一个指针。 (这开始让我头疼)
    • 这里提到的每种动态分配都有术语吗?我希望能够谷歌搜索相关的东西。在此之前,我以某种方式了解“动态线性数组”和“动态分配结构的动态数组”,但不知道如何在 Google 搜索词中表达它们,而不是动态数组分配。
    • 惊人的答案。这应该是一篇文章/博客/媒体文章。
    【解决方案4】:

    正如其他人所建议的那样,声明一个实际的数组可能会更好,但您的问题似乎更多的是关于内存管理,所以我将讨论这个问题。

    struct Test **array1;
    

    这是一个指向struct Test 地址的指针。 (不是指向结构本身的指针;它是指向保存结构的地址的内存位置的指针。)声明为指针分配内存,但不为它指向的项目分配内存。由于可以通过指针访问数组,因此您可以使用*array1 作为指向其元素类型为struct Test 的数组的指针。但是还没有一个实际的数组可以指向它。

    array1 = malloc(MAX * sizeof(struct Test *));
    

    这会分配内存来保存MAX 指向struct Test 类型项目的指针。同样,它不为结构本身分配内存;仅用于指针列表。但是现在您可以将array 视为指向已分配指针数组的指针。

    为了使用array1,您需要创建实际的结构。你可以通过简单地声明每个结构来做到这一点

    struct Test testStruct0;  // Declare a struct.
    struct Test testStruct1;
    array1[0] = &testStruct0;  // Point to the struct.
    array1[1] = &testStruct1;
    

    您还可以在堆上分配结构:

    for (int i=0; i<MAX; ++i) {
      array1[i] = malloc(sizeof(struct Test));
    }
    

    分配内存后,您可以创建一个指向相同结构列表的新变量:

    struct Test **array2 = array1;
    

    您不需要分配任何额外的内存,因为array2 指向您分配给array1 的同一内存。


    有时你想要有一个指向指针列表的指针,但除非你做一些花哨的事情,否则你可以使用

    struct Test *array1 = malloc(MAX * sizeof(struct Test));  // Pointer to MAX structs
    

    这声明了指针array1,为MAX 结构分配了足够的内存,并将array1 指向该内存。现在您可以像这样访问结构:

    struct Test testStruct0 = array1[0];     // Copies the 0th struct.
    struct Test testStruct0a= *array1;       // Copies the 0th struct, as above.
    struct Test *ptrStruct0 = array1;        // Points to the 0th struct.
    
    struct Test testStruct1 = array1[1];     // Copies the 1st struct.
    struct Test testStruct1a= *(array1 + 1); // Copies the 1st struct, as above.
    struct Test *ptrStruct1 = array1 + 1;    // Points to the 1st struct.
    struct Test *ptrStruct1 = &array1[1];    // Points to the 1st struct, as above.
    

    那么有什么区别呢?一些东西。显然,第一种方法要求您为指针分配内存,然后为结构本身分配额外的空间;第二个让您可以通过一个malloc() 电话逃脱。额外的工作能给你带来什么?

    由于第一种方法为您提供了指向Test 结构的实际指针数组,因此每个指针可以指向内存中任何位置的任何Test 结构;它们不必是连续的。此外,您可以根据需要为每个实际的Test 结构分配和释放内存,并且可以重新分配指针。因此,例如,您可以通过简单地交换它们的指针来交换两个结构:

    struct Test *tmp = array1[2];  // Save the pointer to one struct.
    array1[2] = array1[5];         // Aim the pointer at a different struct.
    array1[5] = tmp;               // Aim the other pointer at the original struct.
    

    另一方面,第二种方法为所有Test 结构分配一个连续的内存块,并将其划分为MAX 项。并且数组中的每个元素都驻留在固定位置;交换两个结构的唯一方法是复制它们。

    指针是 C 语言中最有用的结构之一,但它们也可能是最难理解的结构之一。如果您打算继续使用 C,那么花一些时间使用指针、数组和调试器,直到您对它们感到满意为止,这可能是一项值得的投资。

    祝你好运!

    【讨论】:

      猜你喜欢
      • 2021-12-14
      • 1970-01-01
      • 2018-05-16
      • 2011-05-23
      • 1970-01-01
      • 1970-01-01
      • 2022-01-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多