C语言:指向结构体指针数组的指针(分配/释放问题)

40

我最近在学习C语言时遇到了一些问题,主要是记不清楚内存管理的工作原理。我希望有一个指向结构体指针数组的指针。

假设我有以下代码:

struct Test {
   int data;
};

然后是数组:

struct Test **array1;

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

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

我对上面的内容有些困惑。我需要这样做吗?为什么需要这样做?特别是,如果我将为指针指向的每个东西分配内存,那么分配指针的内存是什么意思?

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

struct Test **array2;

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

array2 = array1

你需要一个结构体指针的实际数组吗?就是一个声明了的数组,你可以为每个元素分配一个结构体? - teppic
我想要一个指向数组的指针,以便我可以执行你所说的操作。 - DillPixel
我认为它们对于我的目的来说会表现得相同,所以不,我不认为我需要一个“真正”的数组。 - DillPixel
1
如果使用适当的数组,就会变得更加简单 - 如果您想要,我可以提供一个示例。 - teppic
@WhozCraig:我已经把两种情况都写下来了(希望是正确的,我的头现在很疼)。 - teppic
显示剩余5条评论
4个回答

110

已分配数组

对于已分配的数组,遵循以下步骤即可:

声明指针数组。该数组中的每个元素都指向一个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]->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 

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

这个例子比较特殊。它是一个指针动态数组,就像我们之前看到的例子那样,但不同的是,所有元素都在单个分配中分配。这有其用途,最显著的是可以对数据进行不同配置的排序,同时保留原始分配。

我们首先像在最基本的单块分配中一样分配一组元素:

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指向),因此应该如何释放。

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

首先,它极大地减少了内存分配调用的数量。现在只有两个:一个是数组块,另一个是指针数组。内存分配是程序可以请求的最昂贵的操作之一,尽可能地减少它们是可取的(请注意:文件IO是另一个需要注意的地方)。

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

我知道这很难理解,但指针用法是理解C语言许多强大功能的关键,因此请好好学习并不断刷新您的记忆。它会回来的。


假设我有另一个结构体Test2,它包含了一个指向数组的指针。那么我该如何在堆上分配内存?struct Test2 { struct Test *array[50]; };struct Test2 *container = malloc(sizeof(Test2))这样就足够了吗? - DillPixel
@DillPixel:这是在第二个结构体中声明指针数组本身。如果你只想让一个结构体指向该数组,你只需要定义一个指针即可。(这开始让我头疼了) - teppic
2
这里提到的每种动态分配类型都有术语吗?我想要能够通过谷歌搜索相关内容。在此之前,我已经了解了“动态线性数组”和“动态分配结构体的动态数组”,但不知道如何用谷歌搜索术语来表达它们,除了动态数组分配。 - raymai97
4
惊人的回答。这应该成为一篇文章/博客/媒体发布。 - Kingkong Jnr

5

可能更好的做法是声明一个实际的数组,正如其他人所建议的那样,但是你的问题似乎更多地涉及内存管理,因此我将讨论这个问题。

struct Test **array1;

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

这段文本涉及IT技术,意思是在分配内存以容纳指向类型为"struct Test"的项目的MAX个指针。需要注意的是,这并没有为结构体本身分配内存,只为指针列表分配空间。现在可以将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语言,花点时间玩指针、数组和调试器可能会是一个值得的投资,直到您对它们感到舒适为止。
祝你好运!

3
我建议您逐层构建,使用typedef创建类型层。这样做,所需的不同类型将更加清晰。
例如:
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;
}

2

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

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

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

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

*p是指向字符的指针,因此sizeof *ppsizeof(char*)。我们分配了CNT个指针。接下来:

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

*p是一个结构体,因此sizeof *pstruct something的大小。我们分配了CNT个struct something。接下来:

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

*pp是一个指向结构体的指针,因此sizeof *pp等于sizeof(struct something*)。我们分配了CNT个指针。


@Yar 可能是这样。也可能是4,甚至2...这并不重要。这也是为什么 sizeof 存在的原因。 - wildplasser

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接