web-dev-qa-db-fra.com

Façon bizarre d'allouer un tableau à deux dimensions?

Dans un projet, quelqu'un a poussé cette ligne:

double (*e)[n+1] = malloc((n+1) * sizeof(*e));

Ce qui crée soi-disant un tableau bidimensionnel de (n + 1) * (n + 1) doubles.

soi-disant, dis-je, car jusqu'à présent, personne à qui j'ai demandé ne pouvait me dire ce que cela faisait, exactement, ni d'où cela venait ni pourquoi cela devrait fonctionner (ce qui, prétendument, oui, mais je ne suis pas encore l'acheter).

Peut-être que je manque quelque chose d'évident, mais j'apprécierais que quelqu'un m'explique la ligne ci-dessus. Parce que personnellement, je me sentirais beaucoup mieux si nous utilisions quelque chose que nous comprenons réellement.

110
User1291

La variable e est un pointeur vers un tableau de n + 1 éléments de type double.

L'utilisation de l'opérateur de déréférence sur e vous donne le type de base de e qui est "tableau de n + 1 éléments de type double ".

L'appel malloc prend simplement le type de base de e (expliqué ci-dessus) et obtient sa taille, le multiplie par n + 1, et en transmettant cette taille à la fonction malloc. Allouer essentiellement un tableau de n + 1 tableaux de n + 1 éléments de double.

87

Il s'agit de la manière typique d'allouer dynamiquement des tableaux 2D.

  • e est un pointeur de tableau vers un tableau de type double [n+1].
  • sizeof(*e) donne donc le type du type pointé, qui est la taille d'un tableau double [n+1].
  • Vous allouez de la place à n+1 De tels tableaux.
  • Vous définissez le pointeur de tableau e pour pointer sur le premier tableau de ce tableau de tableaux.
  • Cela vous permet d'utiliser e comme e[i][j] Pour accéder aux éléments individuels du tableau 2D.

Personnellement, je pense que ce style est beaucoup plus facile à lire:

double (*e)[n+1] = malloc( sizeof(double[n+1][n+1]) );
56
Lundin

Cet idiome tombe naturellement hors de l'allocation de tableau 1D. Commençons par allouer un tableau 1D d'un type arbitraire T:

T *p = malloc( sizeof *p * N );

C'est simple, non? expression*p A le type T, donc sizeof *p Donne le même résultat que sizeof (T), donc nous allouons suffisamment espace pour un tableau d'éléments N- de T. Cela est vrai pour tout type T.

Maintenant, substituons T avec un type de tableau comme R [10]. Ensuite, notre allocation devient

R (*p)[10] = malloc( sizeof *p * N);

La sémantique ici est exactement la même que la méthode d'allocation 1D; tout ce qui a changé, c'est le type de p. Au lieu de T *, C'est maintenant R (*)[10]. L'expression *p A le type T qui est le type R [10], Donc sizeof *p Est équivalent à sizeof (T) qui est équivalent à sizeof (R [10]). Nous allouons donc suffisamment d'espace pour un tableau d'éléments N par 10 De R.

Nous pouvons aller encore plus loin si nous le voulons; supposons que R est lui-même un type de tableau int [5]. Remplacez cela par R et nous obtenons

int (*p)[10][5] = malloc( sizeof *p * N);

Même accord - sizeof *p Est identique à sizeof (int [10][5]), et nous finissons par allouer un morceau de mémoire contigu assez grand pour contenir un N par 10 Par 5 Tableau de int.

Voilà donc le côté allocation; qu'en est-il du côté accès?

N'oubliez pas que l'opération d'indice [] Est définie en termes d'arithmétique du pointeur: a[i] Est défini comme *(a + i)1. Ainsi, l'opérateur d'indice []implicitement déréférence un pointeur. Si p est un pointeur vers T, vous pouvez accéder à la valeur pointée en déréférençant explicitement avec l'opérateur unaire *:

T x = *p;

o en utilisant l'opérateur d'indice []:

T x = p[0]; // identical to *p

Ainsi, si p pointe vers le premier élément d'un tablea, vous pouvez accéder à n'importe quel élément de ce tableau en utilisant un indice sur le pointeur p:

T arr[N];
T *p = arr; // expression arr "decays" from type T [N] to T *
...
T x = p[i]; // access the i'th element of arr through pointer p

Maintenant, refaisons notre opération de substitution et remplaçons T par le type de tableau R [10]:

R arr[N][10];
R (*p)[10] = arr; // expression arr "decays" from type R [N][10] to R (*)[10]
...
R x = (*p)[i];

Une différence immédiatement apparente; nous déréférençons explicitement p avant d'appliquer l'opérateur d'indice. Nous ne voulons pas souscrire à p, nous voulons souscrire à ce que ppointe vers (dans ce cas, le tableaarr[0]). Puisque l'unaire * A une priorité plus faible que l'opérateur d'indice [], Nous devons utiliser des parenthèses pour grouper explicitement p avec *. Mais rappelez-vous d'en haut que *p Est identique à p[0], Nous pouvons donc le remplacer par

R x = (p[0])[i];

ou juste

R x = p[0][i];

Ainsi, si p pointe vers un tableau 2D, nous pouvons indexer dans ce tableau via p comme ceci:

R x = p[i][j]; // access the i'th element of arr through pointer p;
               // each arr[i] is a 10-element array of R

En prenant cela à la même conclusion que ci-dessus et en remplaçant R par int [5]:

int arr[N][10][5];
int (*p)[10][5]; // expression arr "decays" from type int [N][5][10] to int (*)[10][5]
...
int x = p[i][j][k];

Cela fonctionne tout de même si p pointe vers un tableau normal, ou s'il pointe vers la mémoire allouée via malloc.

Cet idiome présente les avantages suivants:

  1. C'est simple - une seule ligne de code, par opposition à la méthode d'allocation fragmentaire
    T **arr = malloc( sizeof *arr * N );
    if ( arr )
    {
      for ( size_t i = 0; i < N; i++ )
      {
        arr[i] = malloc( sizeof *arr[i] * M );
      }
    }
    
  2. Toutes les lignes du tableau alloué sont * contiguës *, ce qui n'est pas le cas avec la méthode d'allocation fragmentaire ci-dessus;
  3. La désallocation du tableau est tout aussi simple avec un seul appel à free. Encore une fois, ce n'est pas vrai avec la méthode d'allocation fragmentaire, où vous devez désallouer chaque arr[i] Avant de pouvoir désallouer arr.

Parfois, la méthode d'allocation au coup par coup est préférable, par exemple lorsque votre segment est mal fragmenté et que vous ne pouvez pas allouer votre mémoire en tant que bloc contigu, ou que vous souhaitez allouer un tableau "irrégulier" où chaque ligne peut avoir une longueur différente. Mais en général, c'est la meilleure façon de procéder.


1. N'oubliez pas que les tableaux ne le sont pas pointeurs - à la place, les tableaux expressions sont convertis en expressions de pointeur si nécessaire.
39
John Bode