C: pointeur vers un tableau de pointeurs vers des structures (problèmes d'allocation/deallocation)

Je suis retourné en C pour quelque chose, mais j'ai du mal à me souvenir de la façon dont cette gestion de la mémoire fonctionne. J'aimerais avoir un pointeur vers un tableau de pointeurs vers des structures.

Dites que j'ai:

struct Test {
   int data;
};

, Puis le tableau:

struct Test **array1;

Est-ce correct? Mon problème est de travailler avec cette chose. Ainsi, chaque pointeur dans le tableau pointe vers quelque chose qui est alloué séparément. Mais je pense que je dois le faire en premier:

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

J'ai du mal à comprendre le surtout. Ai-je besoin de le faire, et pourquoi dois-je faire? En particulier, qu'est-ce que cela signifie d'allouer de la mémoire pour les pointeurs si je vais allouer de la mémoire pour chaque chose que le pointeur pointe?

Dites Maintenant que j'ai un pointeur vers un tableau de pointeurs vers des structures. Je veux maintenant qu'il pointe vers le même tableau que j'ai créé plus tôt.

struct Test **array2;

Ai-je besoin d'allouer de la place pour les pointeurs comme je l'ai fait ci-dessus, ou Puis-je simplement faire:

array2 = array1
21
demandé sur Michael Anderson 2013-03-14 02:27:57

4 réponses

Tableau Alloué

Avec un tableau alloué, il est assez simple à suivre.

Déclarez votre tableau de pointeurs. Chaque élément de ce tableau pointe vers un struct Test:

struct Test *array[50];

Puis allouez et affectez les pointeurs aux structures comme vous le souhaitez. Utiliser une boucle serait simple:

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

Puis déclarer un pointeur sur ce tableau:

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

Cela vous permet d'utiliser (*p)[n]->data; pour référencer le nième membre.

Ne vous inquiétez pas si cela les choses sont déroutantes. C'est probablement l'aspect le plus difficile de C.


Tableau Linéaire Dynamique

Si vous voulez juste pour allouer un bloc de structures (un tableau de struct pas pointeurs vers des structures), et un pointeur vers le bloc, vous pouvez le faire plus facilement:

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

Vous pouvez alors pointer vers ce pointeur:

struct Test **pp = &p

Vous n'avez plus de tableau de pointeurs vers les structures, mais cela simplifie le tout considérablement.


Tableau dynamique de structures allouées dynamiquement

Le plus flexible, mais pas souvent nécessaire. C'est très similaire au premier exemple, mais nécessite une allocation supplémentaire. J'ai écrit un programme complet pour démontrer cela qui devrait bien se compiler.

#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;
}

Sortie:

> 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

, Ou l'ensemble:

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 

Tableau de pointeurs dynamiques de structures allouées à une seule dynamique

Ce dernier exemple est plutôt spécifiques. C'est un tableau dynamique de pointeurs comme nous l'avons vu dans les exemples précédents, mais contrairement à ceux-ci, les éléments sont tous alloués dans une allocation unique. Cela a ses utilisations, le plus notable pour trier les données dans différentes configurations tout en laissant l'allocation d'origine intacte.

Nous commençons par allouer un seul bloc d'éléments comme nous le faisons dans l'allocation de bloc unique la plus basique:

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

Maintenant, nous allouons un bloc séparé de les pointeurs:

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

Nous remplissons ensuite chaque emplacement de notre liste de pointeurs avec l'adresse de l'un de nos tableaux d'origine. Puisque l'arithmétique du pointeur nous permet de passer d'un élément à l'adresse de l'élément, c'est simple:

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

À ce stade, les éléments suivants se réfèrent tous deux au même champ d'élément

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

Et après examen de ce qui précède, j'espère qu'il est clair pourquoi .

Lorsque nous avons terminé avec le tableau de pointeurs et le tableau de blocs d'origine, ils sont libérés comme:

free(ptrs);
free(arr);

Remarque: Nous ne libérons pas chaque élément du tableau ptrs[] individuellement. Ce n'est pas ainsi qu'ils ont été attribués. Ils ont été alloués comme un seul bloc (pointé par arr), et c'est ainsi qu'ils devraient être libérés.

Alors pourquoi quelqu'un voudrait faire ça? Plusieurs raisons.

Tout d'abord, il réduit radicalement le nombre d'appels d'allocation de mémoire. Plutôt que N+1 (un pour le tableau de pointeurs, N pour les structures individuelles), vous n'avez maintenant que deux : un pour le tableau bloc, et un pour le tableau de pointeurs. Les allocations de mémoire sont l'une des opérations les plus coûteuses qu'un programme peut demander, et si possible, il est souhaitable de les minimiser (note: file IO en est une autre, fyi).

Une autre raison: plusieurs représentations du même tableau de données de base. Supposons que vous vouliez trier les données à la fois ascendantes et descendantes, et que les deux représentations triées soient disponibles en même temps. Vous pourriez dupliquer le tableau de données, mais ce serait nécessite beaucoup de copie et consomme une utilisation importante de la mémoire. Au lieu de cela, il suffit d'allouer un tableau de pointeurs supplémentaire et de le remplir avec des adresses du tableau de base, puis de trier ce tableau de pointeurs. Cela présente des avantages particulièrement importants lorsque les données triées sont volumineuses (peut-être des kilo-octets, voire plus, par élément) les éléments d'origine restent à leur emplacement d'origine dans le tableau de base, mais vous disposez maintenant d'un mécanisme très efficace dans lequel vous pouvez les trier sans avoir à déplacer ils. Vous triez le tableau de pointeurs vers des éléments; les éléments ne sont pas déplacés du tout.

Je me rends compte que c'est beaucoup à prendre, mais l'utilisation du pointeur est essentielle pour comprendre les nombreuses choses puissantes que vous pouvez faire avec le langage C, alors frappez les livres et continuez à rafraîchir votre mémoire. Il va revenir.

63
répondu teppic 2013-03-14 02:21:27

Il peut être préférable de déclarer un tableau réel, comme d'autres l'ont suggéré, mais votre question semble être plus sur la gestion de la mémoire, donc je vais en discuter.

struct Test **array1;

Ceci est un pointeur vers l'adresse d'un struct Test. (Pas un pointeur vers la structure elle-même; c'est un pointeur sur un emplacement mémoire qui contient la adresse de la structure.) La déclaration alloue de la mémoire pour le pointeur, mais pas pour les éléments vers lesquels il pointe. Comme un tableau est accessible via des pointeurs, vous pouvez travailler avec *array1 comme un pointeur vers un tableau dont les éléments sont de type struct Test. Mais il n'y a pas encore de tableau réel vers lequel pointer.

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

Cela alloue de la mémoire pour contenir des pointeurs MAX vers des éléments de type struct Test. Encore une fois, il n'alloue pas de mémoire pour les structures elles-mêmes; seulement pour une liste de pointeurs. Mais maintenant, vous pouvez traiter array comme un pointeur vers un alloués tableau de pointeurs.

Pour utiliser array1, Vous devez créer les structures réelles. Vous pouvez le faire par il suffit de déclarer chaque structure avec

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

Vous pouvez également allouer les structures sur le tas:

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

Une fois que vous avez alloué de la mémoire, vous pouvez créer une nouvelle variable qui pointe vers la même Liste de structures:

struct Test **array2 = array1;

Vous n'avez pas besoin d'allouer de mémoire supplémentaire, car array2 pointe vers la même mémoire que vous avez allouée à array1.


Parfois, vous voulez avoir un pointeur vers une liste de pointeurs, mais à moins que vous ne fassiez quelque chose de fantaisie, vous peut être capable d'utiliser

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

Cela déclare le pointeur array1, alloué suffisamment de mémoire pour les structures MAX, et pointe array1 vers cette mémoire. Maintenant, vous pouvez accéder aux structures comme ceci:

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.

Alors, quelle est la différence? Un certain nombre de choses. De toute évidence, la première méthode vous oblige à allouer de la mémoire pour les pointeurs, puis à allouer de l'espace supplémentaire pour les structures elles-mêmes; la seconde vous permet de vous en sortir avec un appel malloc(). Qu'est-ce que le travail supplémentaire achète vous?

Puisque la première méthode vous donne un tableau réel de pointeurs vers Test structs, chaque pointeur peut pointer vers n'importe quelle Test struct, n'importe où dans la mémoire; ils n'ont pas besoin d'être contigus. De plus, vous pouvez allouer et libérer la mémoire pour chaque structure Test réelle si nécessaire, et vous pouvez réaffecter les pointeurs. Ainsi, par exemple, vous pouvez échanger deux structures en échangeant simplement leurs pointeurs:

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.

D'autre part, la seconde méthode alloue un seul bloc contigu de mémoire pour toutes les structures Test et les partitions en éléments MAX. Et chaque élément du tableau réside à une position fixe; la seule façon d'échanger deux structures est de les copier.

Les pointeurs sont l'une des constructions les plus utiles en C, mais ils peuvent aussi être parmi les plus difficiles à comprendre. Si vous prévoyez de continuer à utiliser C, ce sera probablement un investissement intéressant de passer du temps à jouer avec des pointeurs, des tableaux et un débogueur jusqu'à ce que vous soyez à l'aise avec ils.

Bonne chance!

3
répondu Adam Liss 2013-03-13 23:18:20

Je suggère que vous construisiez ceci une couche à la fois en utilisant typdefs pour créer des couches de types. Ce faisant, les différents types nécessaires seront beaucoup plus claires.

Par exemple:

typedef struct Test {
   int data;
} TestType;

typedef  TestType * PTestType;

Cela créera deux nouveaux types, un pour la structure et un pour un pointeur vers la structure.

Donc, si vous voulez un tableau des structures, vous utiliserez:

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

Si vous voulez un tableau de pointeurs vers les structures, vous utiliserez:

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

Alors si vous souhaitez allouer des structures dans le tableau que vous feriez quelque chose comme:

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 ne vous permet pas d'attribuer un tableau à un autre. Vous devez plutôt utiliser une boucle à attribuer à chaque élément d'un tableau à un élément de l'autre.

EDIT: une Autre Approche Intéressante

Une autre approche serait une approche plus orientée objet dans laquelle vous encapsulez quelques choses. Par exemple, en utilisant les mêmes couches de types, nous créons deux types:

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;

Suivant nous avoir une fonction d'aide que nous utilisons pour créer l'objet, nommée de manière appropriée 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;
}

Maintenant, nous pouvons utiliser notre nouvel objet comme dans le segment de code source ci-dessous. Il devrait vérifier que le pointeur retourné par CreateTestData () est valide mais c'est vraiment juste pour montrer ce qui pourrait être fait.

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

Dans un environnement vraiment dynamique, vous pouvez également avoir une fonction ReallocTestData(PTestData p) qui réallouerait un objet TestData afin de modifier la taille du tableau contenu dans le objet.

Avec cette approche, lorsque vous avez terminé avec un objet Testdata particulier, vous pouvez simplement libérer l'objet comme dans free (go) et l'objet et son tableau sont tous deux libérés en même temps.

Modifier: Étendre Plus Loin

Avec ce type encapsulé, nous pouvons maintenant faire quelques autres choses intéressantes. Par exemple, nous pouvons avoir une fonction de copie, PTestType CreateCopyTestData (PTestType pSrc) qui créerait une nouvelle instance, puis copierait l'argument dans un nouvel objet. Dans l'exemple suivant, nous réutilisons la fonction {[13] } qui va créer une instance de notre type, en utilisant la taille de l'objet que nous copions. Après avoir fait la création du nouvel objet, nous faisons une copie des données de l'objet source. La dernière étape consiste à fixer le pointeur qui dans l'objet source pointe vers sa zone de données de sorte que le pointeur dans le nouvel objet pointe maintenant vers la zone de données de lui-même plutôt que la zone de données de l'ancien objet.

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
répondu Richard Chambers 2013-03-31 02:36:25

Les structures ne sont pas très différentes des autres objets. Commençons par les caractères:

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

* p est un caractère, donc sizeof *p est sizeof (char) = = 1; nous avons alloué des caractères CNT. Suivant:

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

*p est un pointeur de caractère, donc sizeof *pp est sizeof (char*). Nous avons alloué des pointeurs CNT. Suivant:

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

*p est un struct quelque chose, donc sizeof *p est sizeof (struct quelque chose). Nous avons alloué quelque chose de CNT struct. Suivant:

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

* pp est un pointeur sur struct, donc sizeof *pp est sizeof (struct quelque*). Nous avons alloué des pointeurs CNT.

2
répondu wildplasser 2016-10-23 10:55:23