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

Je retourne à C pour quelque chose, mais j’ai du mal à me souvenir du fonctionnement de cette gestion de la mémoire. J’aimerais avoir un pointeur sur un tableau de pointeurs sur des structures.

Dis que j’ai:

struct Test { int data; }; 

Puis le tableau:

 struct Test **array1; 

Est-ce correct? Mon problème travaille 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 faire ceci:

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

J’ai du mal à comprendre ce qui précède. Dois-je faire cela et pourquoi dois-je le faire? En particulier, que signifie allouer de la mémoire pour les pointeurs si je vais allouer de la mémoire pour chaque élément pointé par le pointeur?

Disons maintenant que j’ai un pointeur sur un tableau de pointeurs vers des structures. Je veux maintenant qu’il pointe vers le même tableau que j’ai déjà créé.

 struct Test **array2; 

Dois-je laisser de la place aux pointeurs comme ci-dessus ou puis-je simplement faire:

 array2 = array1 

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 une struct Test :

 struct Test *array[50]; 

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

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

Puis déclarez 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 ; faire référence au nième membre.

Ne vous inquiétez pas si cela vous dérange. C’est probablement l’aspect le plus difficile de C.


Tableau linéaire dynamic

Si vous souhaitez simplement allouer un bloc de structures (en réalité un tableau de structures, et non des pointeurs sur des structures) et que vous avez 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 sur ce pointeur:

 struct Test **pp = &p 

Vous n’avez plus un tableau de pointeurs à structurer, mais cela simplifie considérablement le tout.


Tableau dynamic de structures allouées dynamicment

Le plus flexible, mais pas souvent nécessaire. Cela ressemble beaucoup au premier exemple, mais nécessite une allocation supplémentaire. J’ai écrit un programme complet pour démontrer que cela devrait bien comstackr.

 #include  #include  #include  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 tout 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 dynamics de structures allouées à une seule dynamic

Ce dernier exemple est assez spécifique. Comme nous l’avons vu dans les exemples précédents, il s’agit d’un tableau dynamic de pointeurs, mais contrairement à ceux-ci, les éléments sont tous affectés dans une seule allocation. Cela a ses utilisations, notamment pour le sorting des données dans différentes configurations tout en préservant l’allocation originale.

Nous commençons par allouer un seul bloc d’éléments comme nous le faisons dans l’atsortingbution de base à un seul bloc:

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

Maintenant, nous allouons un bloc séparé de 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 de pointeur nous permet de passer d’un élément à l’autre, c’est simple:

 for (int i=0;i 

À ce stade, les deux éléments suivants font référence au même champ d'élément

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

Et après avoir examiné ce qui précède, j'espère que la raison est claire.

Lorsque nous en 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 ptrs[] pas chaque élément du ptrs[] individuellement. Ce n'est pas comment ils ont été alloués. 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 cela? Plusieurs raisons.

Premièrement, cela 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 des structures individuelles), vous n'en avez plus que deux : un pour le bloc tableau 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 (note: le fichier IO en est un autre, fyi).

Une autre raison: plusieurs représentations du même tableau de firebase database. Supposons que vous souhaitiez sortinger les données par ordre croissant ou décroissant et que les deux représentations sortingées soient disponibles en même temps . Vous pouvez dupliquer le tableau de données, mais cela nécessiterait beaucoup de copie et consumrait une quantité de mémoire importante. Au lieu de cela, allouez simplement un tableau de pointeurs supplémentaire et remplissez-le avec les adresses du tableau de base, puis sortingez ce tableau de pointeurs. Cela présente des avantages particulièrement significatifs lorsque les données sortingées sont volumineuses (peut-être en kilo-octets ou même plus par article). Les éléments d'origine restnt à leur emplacement d'origine dans le tableau de base, mais vous disposez désormais d'un mécanisme très efficace pour les sortinger. sans avoir à les déplacer réellement. Vous sortingez le tableau de pointeurs sur les éléments; les objects ne sont pas déplacés du tout.

Je me rends compte que c’est énorme, mais l’utilisation du pointeur est essentielle à la compréhension des nombreuses choses puissantes que vous pouvez faire avec le langage C, c’est pourquoi il est important de mettre les livres à jour et d’actualiser constamment votre mémoire. Ça va revenir.

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

 struct Test **array1; 

C’est un pointeur sur l’adresse d’une struct Test . (Ce n’est pas un pointeur sur la structure elle-même; c’est un pointeur sur un emplacement mémoire qui contient l’ adresse de la structure.) La déclaration alloue de la mémoire pour le pointeur, mais pas pour les éléments sur lesquels elle pointe. Puisqu’on peut accéder à un tableau via des pointeurs, vous pouvez *array1 tant que pointeur sur un tableau dont les éléments sont de type struct Test . Mais il n’existe pas encore de tableau vers lequel il faut pointer.

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

Cela alloue de la mémoire pour contenir les pointeurs MAX aux éléments de type struct Test . Là encore, il n’alloue pas de mémoire pour les structures elles-mêmes; seulement pour une liste de pointeurs. Mais maintenant, vous pouvez traiter un array comme un pointeur sur un tableau 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 

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 sur la même mémoire que celle allouée à array1 .


Parfois, vous souhaitez avoir un pointeur sur une liste de pointeurs, mais à moins que vous ne fassiez quelque chose d'extraordinaire, 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. 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? Quelques choses. Clairement, la première méthode nécessite d'allouer de la mémoire pour les pointeurs, puis d'allouer de l'espace supplémentaire pour les structures elles-mêmes. le second vous permet de sortir avec un seul appel malloc() . Qu'est-ce que le travail supplémentaire vous achète?

Comme la première méthode vous donne un tableau de pointeurs sur Test structures Test , chaque pointeur peut pointer sur n'importe quelle structure Test , n'importe où en mémoire. ils n'ont pas besoin d'être contigus. De plus, vous pouvez allouer et libérer de la mémoire pour chaque structure de 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 deuxième méthode alloue un seul bloc de mémoire contigu pour toutes les structures de Test et le partitionne en éléments MAX . Et chaque élément du tableau réside à une position fixe; le seul moyen d'échanger deux structures est de les copier.

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

Bonne chance!

Je vous suggère de construire cette 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 sur la structure.

Alors 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 sur les structures, utilisez:

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

Ensuite, si vous voulez allouer des structures dans le tableau, vous ferez 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 plutôt 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 object dans laquelle vous encapsuleriez 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’object, nommée assez à l’ 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; } 

Nous pouvons maintenant utiliser notre nouvel object comme dans le segment de code source ci-dessous. Il convient de vérifier que le pointeur renvoyé par CreateTestData () est valide, mais il s’agit simplement de montrer ce qui peut être fait.

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

Dans un environnement véritablement dynamic, vous pouvez également avoir une fonction ReallocTestData(PTestData p) qui réaffecterait un object TestData afin de modifier la taille du tableau contenu dans l'object.

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

Edit: Etendre 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 object. 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'object que nous sums en train de copier. Après avoir créé le nouvel object, nous copions les données de l’object source. La dernière étape consiste à corriger le pointeur qui, dans l'object source, pointe vers sa zone de données afin que le pointeur du nouvel object pointe maintenant vers sa zone de données plutôt que vers celle de l'ancien object.

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

Les structures ne sont pas très différentes des autres objects. 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 alloué des caractères CNT. Suivant:

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

* p est un pointeur sur un 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 quelque chose de struct, donc sizeof *p est sizeof (quelque chose de struct). Nous avons alloué quelque chose à CNT. Suivant:

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

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