web-dev-qa-db-fra.com

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

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. Je voudrais avoir un pointeur sur un tableau de pointeurs sur des structures.

Dis que j'ai:

struct Test {
   int data;
};

Ensuite, le tableau:

struct Test **array1;

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

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

J'ai du mal à comprendre ce qui précède. Dois-je le faire et pourquoi dois-je le faire? En particulier, qu'est-ce que cela signifie d'allouer de la mémoire aux pointeurs si je vais allouer de la mémoire pour chaque chose vers laquelle pointe le pointeur?

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

struct Test **array2;

Dois-je allouer de la place aux pointeurs comme je l'ai fait ci-dessus, ou puis-je simplement faire:

array2 = array1
24
DillPixel

tableau alloué

Avec un tableau alloué, c'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];

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

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

Déclarez ensuite 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 ce truc est déroutant. C'est probablement l'aspect le plus difficile de C.


Réseau linéaire dynamique

Si vous voulez simplement allouer un bloc de structures (en fait un tableau de structures, pas pointeurs vers des structures), et avoir un pointeur sur le bloc, vous pouvez le faire plus facilement:

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

Vous pouvez ensuite pointer vers ce pointeur:

struct Test **pp = &p

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


Tableau dynamique de structures allouées dynamiquement

Le plus flexible, mais pas souvent nécessaire. Il 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 à dynamique unique

Ce dernier exemple est assez spécifique. Il s'agit d'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 single. Cela a ses utilisations, notamment 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 de pointeurs séparé:

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. Étant donné que l'arithmétique des pointeurs nous permet de passer d'un élément à une autre, c'est simple:

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

À ce stade, les deux suivants font référence 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 que c'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 suit:

free(ptrs);
free(arr);

Remarque: nous ne libérons PAS chaque élément dans le ptrs[] tableau individuellement. Ce n'est pas ainsi qu'ils ont été répartis. Ils ont été alloués en un seul bloc (indiqué par arr), et c'est ainsi qu'ils devraient être libérés.

Alors pourquoi quelqu'un voudrait-il 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 bloc de tableaux 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 puisse demander, et dans la mesure du possible, il est souhaitable de les minimiser (remarque: fichier IO en est une autre, fyi).

Autre raison: plusieurs représentations du même tableau de base de données. Supposons que vous vouliez trier les données en ordre croissant et décroissant et que les deux représentations triées soient disponibles en même temps. Vous pouvez dupliquer le tableau de données, mais cela nécessiterait beaucoup de copie et consommerait beaucoup de mémoire. Au lieu de cela, allouez simplement un tableau de pointeurs supplémentaire et remplissez-le avec les adresses du tableau de base, puis triez ce tableau de pointeurs. Cela présente des avantages particulièrement importants lorsque les données en cours de tri 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 à les déplacer. Vous triez le tableau de pointeurs sur les éléments; les objets ne bougent pas du tout.

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

69
teppic

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

struct Test **array1;

Il s'agit d'un pointeur vers l'adresse d'un struct Test. (Pas un pointeur vers la structure elle-même; c'est un pointeur vers un emplacement de mémoire qui contient 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. Puisqu'un tableau est accessible via des pointeurs, vous pouvez travailler avec *array1 Comme pointeur vers un tableau dont les éléments sont de type struct Test. Mais il n'y a pas encore de tableau réel à indiquer.

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

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

Pour utiliser array1, Vous devez créer les structures réelles. Vous pouvez le faire en déclarant simplement 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 pour avoir un pointeur vers une liste de pointeurs, mais à moins que vous ne fassiez quelque chose de fantaisiste, vous pourrez peut-être utiliser

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

Cela déclare le pointeur array1, Alloue suffisamment de mémoire pour les structures MAX et pointe array1 Vers cette mémoire. Vous pouvez maintenant 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? Quelques 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; le second vous permet de vous en sortir avec un seul appel à malloc(). Que vous apporte le travail supplémentaire?

Puisque la première méthode vous donne un tableau réel de pointeurs vers les structures Test, chaque pointeur peut pointer vers n'importe quelle structure Test, 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'un autre côté, la deuxième méthode alloue un seul bloc de mémoire contigu pour toutes les structures Test et la partitionne 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 également ê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 eux.

Bonne chance!

4
Adam Liss

Je suggère que vous construisiez cela sur 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 clairs.

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 alors:

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

Ensuite, si vous souhaitez allouer des structures dans le tableau, 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'affecter un tableau à un autre. Vous devez à la place utiliser une boucle pour affecter 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 éléments. 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;

Ensuite, nous avons 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 renvoyé par CreateTestData () est valide, mais c'est 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 voudrez peut-être également avoir une fonction ReallocTestData(PTestData p) qui réallouerait un objet TestData afin de modifier la taille du tableau contenu dans l'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 davantage

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 PTestType CreateTestData (int nCount) qui créera une instance de notre type, en utilisant la taille de l'objet que nous copions. Après avoir créé le 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 du nouvel objet pointe désormais vers la zone de données de lui-même plutôt que vers 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
Richard Chambers

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

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

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

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

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

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

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

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

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

2
wildplasser