Tableau de pointeurs vers un tableau de taille fixe

J’ai essayé d’atsortingbuer deux tableaux de taille fixe à un tableau de pointeurs, mais le compilateur me met en garde et je ne comprends pas pourquoi.

int A[5][5]; int B[5][5]; int*** C = {&A, &B}; 

Ce code comstack avec l’avertissement suivant:

avertissement: initialisation du type de pointeur incompatible [activé par défaut]

Si je lance le code, cela provoquera une erreur de segmentation . Cependant, si j’alloue dynamicment A et B , cela fonctionne très bien. Pourquoi est-ce?

Si vous voulez une déclaration de C qui correspond aux déclarations existantes de A et B vous devez le faire comme ceci:

 int A[5][5]; int B[5][5]; int (*C[])[5][5] = {&A, &B}; 

Le type de C est lu comme ” C est un tableau de pointeurs vers des tableaux int [5][5] “. Comme vous ne pouvez pas affecter un tableau entier, vous devez affecter un pointeur au tableau.

Avec cette déclaration, (*C[0])[1][2] accède au même emplacement mémoire que A[1][2] .

Si vous voulez une syntaxe plus propre comme C[0][1][2] , vous devrez alors faire ce que les autres ont déclaré et allouer dynamicment la mémoire:

 int **A; int **B; // allocate memory for A and each A[i] // allocate memory for B and each B[i] int **C[] = {A, B}; 

Vous pouvez également le faire en utilisant la syntaxe suggérée par Vlad de Moscou:

 int A[5][5]; int B[5][5]; int (*C[])[5] = {A, B}; 

Cette déclaration de C est lue comme ” C est un tableau de pointeurs vers des tableaux int [5] “. Dans ce cas, chaque élément de tableau de C est de type int (*)[5] et le tableau de type int [5][5] peut se désintégrer dans ce type.

Maintenant, vous pouvez utiliser C[0][1][2] pour accéder au même emplacement mémoire que A[1][2] .

Cette logique peut être étendue à des dimensions plus élevées également:

 int A[5][5][3]; int B[5][5][3]; int (*C[])[5][3] = {A, B}; 

Malheureusement, il y a beaucoup de livres / didacticiels / enseignants sur le tas qui vont vous apprendre de mauvaises choses ….

Oubliez les pointeurs sur les pointeurs, ils n’ont rien à voir avec les tableaux. Période.

En règle générale, chaque fois que vous utilisez plus de 2 niveaux d’indirection, cela signifie probablement que la conception de votre programme est fondamentalement erronée et doit être entièrement refaite.


Pour ce faire correctement, vous devez faire comme ceci:

Un pointeur sur un tableau int [5][5] est appelé pointeur de tableau et est déclaré comme int(*)[5][5] . Exemple:

 int A[5][5]; int (*ptr)[5][5] = &A; 

Si vous voulez un tableau de pointeurs de tableau , ce serait le type int(*[])[5][5] . Exemple:

 int A[5][5]; int B[5][5]; int (*arr[2])[5][5] = {&A, &B}; 

Comme vous pouvez le constater, ce code a l’air inutilement compliqué – et c’est le cas. L’access aux éléments individuels sera pénible, car vous devrez taper (*arr[x])[y][z] . Ce qui signifie: “dans le tableau de pointeurs de tableau prendre le pointeur numéro x, prenez le contenu sur lequel il pointe – qui est un tableau 2D – puis prenez l’élément d’index [y] [z] dans ce tableau”.

Inventer de telles constructions est juste de la folie et rien que je recommanderais. Je suppose que le code peut être simplifié en travaillant avec un pointeur de tableau brut:

 int A[5][5]; int B[5][5]; int (*arr[2])[5][5] = {&A, &B}; int (*ptr)[5][5] = arr[0]; ... ptr[x][y][z] = 0; 

Cependant, ce code est encore un peu compliqué. Considérez un design entièrement différent! Exemples:

  • Créez un tableau 3D.
  • Créez une structure contenant un tableau 2D, puis créez un tableau de ces structures.

Il y a beaucoup de mal avec la ligne

 int*** C = {&A, &B}; 

Vous déclarez un seul pointeur C , mais vous lui dites de pointer vers plusieurs objects. ça ne marchera pas. Ce que vous devez faire est de déclarer C comme un tableau de pointeurs vers ces tableaux.

Les types de &A et de &B sont int (*)[5][5] , ou “pointeur vers un tableau à 5 éléments de tableau à 5 éléments de int “; ainsi, le type de C doit être “tableau de pointeur vers un tableau à 5 éléments d’un tableau de 5 éléments int “, ou

 int (*C[2])[5][5] = { &A, &B }; 

qui se lit comme

  C -- C is a C[2] -- 2-element array of *C[2] -- pointers to (*C[2])[5] -- 5-element arrays of (*C[2])[5][5] -- 5-element arrays of int (*C[2])[5][5] -- int 

Beurk. C’est plutôt moche. Il devient encore plus moche si vous voulez accéder à un élément de A ou de B à C :

 int x = (*C[0])[i][j]; // x = A[i][j] int y = (*C[1])[i][j]; // y = B[i][j] 

Nous devons déréférencer explicitement C[i] avant de pouvoir indexer dans le tableau sur lequel il pointe, et comme l’opérateur de l’indice [] a une priorité plus élevée que l’opérateur unaire * , nous devons grouper *C[0] dans parens.

Nous pouvons nettoyer cela un peu. Sauf s’il s’agit de l’opérande de sizeof ou unary & operators (ou si un littéral de chaîne est utilisé pour initialiser un autre tableau dans une déclaration), une expression de type ” N -élément tableau de T ” sera convertie (“decay”) à une expression de type “pointeur vers T “, et la valeur de l’expression sera l’adresse du premier élément du tableau.

Les expressions A et B ont le type int [5][5] ou “le tableau à 5 éléments du tableau à 5 éléments de l’ int “. Par la règle ci-dessus, les deux expressions “déclinent” en expressions de type “pointeur vers un tableau à 5 éléments de int “, ou int (*)[5] . Si nous initialisons le tableau avec A et B au lieu de &A et &B , alors nous avons besoin d’un tableau de pointeurs vers des tableaux à 5 éléments de int , ou

 int (*C[2])[5] = { A, B }; 

Okay, ça rest très beau, mais c’est aussi propre que ça va se passer sans typedefs.

Alors, comment pouvons-nous accéder aux éléments de A et B à C ?

Rappelez-vous que l’opération indice de tableau a[i] est définie comme *(a + i) ; c’est-à-dire, à partir d’une adresse de base a , décaler les éléments i ( pas les octets ) 1 de cette adresse et déréférencer le résultat. Cela signifie que

 *a == *(a + 0) == a[0] 

Ainsi,

 *C[i] == *(C[i] + 0) == C[i][0] 

Mettre tout cela ensemble:

 C[0] == A // int [5][5], decays to int (*)[5] C[1] == B // int [5][5], decays to int (*)[5] *C[0] == C[0][0] == A[0] // int [5], decays to int * *C[1] == C[1][0] == B[0] // int [5], decays to int * C[0][i] == A[i] // int [5], decays to int * C[1][i] == B[i] // int [5], decays to int * C[0][i][j] == A[i][j] // int C[1][i][j] == B[i][j] // int 

On peut indexer C comme s’il s’agissait d’un tableau 3D d’ int , qui est un peu plus propre que (*C[i)[j][k] .

Ce tableau peut également être utile:

 Expression Type "Decays" to Value ---------- ---- ----------- ----- A int [5][5] int (*)[5] Address of A[0] &A int (*)[5][5] Address of A *A int [5] int * Value of A[0] (address of A[0][0]) A[i] int [5] int * Value of A[i] (address of A[i][0]) &A[i] int (*)[5] Address of A[i] *A[i] int Value of A[i][0] A[i][j] int Value of A[i][j] 

Notez que A , &A , A[0] , &A[0] et &A[0][0] donnent tous la même valeur (l’adresse d’un tableau et l’adresse du premier élément du tableau sont toujours les mêmes) , mais les types sont différents, comme indiqué dans le tableau ci-dessus.


  1. L’arithmétique du pointeur prend en compte la taille du type pointé; Si p contient l’adresse d’un object int , alors p+1 renvoie l’adresse du prochain object int , qui peut être distante de 2 à 4 octets.

Une idée fausse commune chez les débutants C est qu’ils supposent que les pointeurs et les tableaux sont équivalents. C’est complètement faux.

La confusion vient aux débutants quand ils voient le code comme

 int a1[] = {1,2,3,4,5}; int *p1 = a1; // Beginners intuition: If 'p1' is a pointer and 'a1' can be assigned // to it then arrays are pointers and pointers are arrays. p1[1] = 0; // Oh! I was right a1[3] = 0; // Bruce Wayne is the Batman! Yeah. 

Maintenant, il est vérifié par les débutants que les tableaux sont des pointeurs et que les pointeurs sont des tableaux, ils font donc de telles expériences:

 int a2[][5] = {{0}}; int **p2 = a2; 

Et puis un avertissement apparaît à propos de l’assignation incompatible du pointeur alors ils pensent: “Oh mon Dieu! Pourquoi ce tableau est-il devenu Harvey Dent?”.

Certains vont même de l’avant

 int a3[][5][10] = {{{0}}}; int ***p3 = a3; // "?" 

et puis Riddler arrive à son cauchemar d’équivalence de pointeur.

entrer la description de l'image ici

Rappelez-vous toujours que les tableaux ne sont pas des pointeurs et vice-versa. Un tableau est un type de données et un pointeur est un autre type de données ( qui n’est pas un type de tableau ). Cela a été abordé il y a plusieurs années dans la C-FAQ :

Dire que les tableaux et les pointeurs sont “équivalents” ne signifie ni qu’ils sont identiques ni même interchangeables. Cela signifie que l’arithmétique du tableau et du pointeur est définie de telle sorte qu’un pointeur puisse être utilisé pour accéder à un tableau ou pour simuler un tableau. En d’autres termes, comme l’a dit Wayne Throop, «l’arithmétique du pointeur et l’indexation des tableaux [qui] sont équivalents en C, les pointeurs et les tableaux sont différents». )

Maintenant, souvenez-vous toujours de quelques règles importantes pour les tableaux afin d’éviter ce type de confusion:

  • Les tableaux ne sont pas des indicateurs. Les pointeurs ne sont pas des tableaux.
  • Les tableaux sont convertis en pointeur sur leur premier élément lorsqu’ils sont utilisés dans une expression, sauf en cas d’opérande de sizeof et d’opérateur.
  • C’est l’ arithmétique du pointeur et l’ indexation du tableau qui sont identiques.
  • Les pointeurs et les tableaux sont différents.
  • Ai-je dit “les pointeurs ne sont pas des tableaux et vice-versa”.

Maintenant que vous avez les règles, vous pouvez conclure que dans

 int a1[] = {1,2,3,4,5}; int *p1 = a1; 

a1 est un tableau et dans la déclaration int *p1 = a1; il converti en pointeur sur son premier élément. Ses éléments sont de type int alors que le pointeur sur son premier élément serait de type int * compatible avec p1 .

Dans

 int a2[][5] = {{0}}; int **p2 = a2; 

a2 est un tableau et dans int **p2 = a2; il se désintègre pour pointer vers son premier élément. Ses éléments sont de type int[5] (un tableau 2D est un tableau de tableaux 1D), donc un pointeur vers son premier élément serait de type int(*)[5] (pointeur vers tableau) incompatible avec le type int ** . CA devrait etre

 int (*p2)[5] = a2; 

De même pour

 int a3[][5][10] = {{{0}}}; int ***p3 = a3; 

les éléments de a3 sont de type int [5][10] et le pointeur sur son premier élément serait de type int (*)[5][10] , mais p3 est de type int *** , pour les rendre compatibles, CA devrait etre

 int (*p3)[5][10] = a3; 

Maintenant, venez à votre extrait

 int A[5][5]; int B[5][5]; int*** C = {&A, &B}; 

&A et &B sont de type int(*)[5][5] . C est de type int*** , ce n’est pas un tableau. Puisque vous voulez que C contienne l’adresse des deux tableaux A et B , vous devez déclarer C comme un tableau de deux éléments de type int(*)[5][5] . Cela devrait être fait comme

 int (*C[2])[5][5] = {&A, &B}; 

Cependant, si j’alloue dynamicment A et B, cela fonctionne très bien. Pourquoi est-ce?

Dans ce cas, vous devez avoir déclaré A et B comme int ** . Dans ce cas, les deux sont des pointeurs, pas des tableaux. C est de type int *** , donc il peut contenir une adresse de données de type int** . Notez que dans ce cas la déclaration int*** C = {&A, &B}; devrait être

  int*** C = &A; 

Dans le cas de int*** C = {&A, &B}; , le comportement du programme serait soit indéfini, soit défini par l’implémentation.

C11: 5.1.1.3 (P1):

Une implémentation conforme doit produire au moins un message de diagnostic (identifié de manière définie par l’implémentation) si une unité de traduction ou une unité de traduction de prétraitement enfreint une règle ou une contrainte de syntaxe, même si elle est explicitement définie comme non définie ou implémentée. défini

Lisez ce post pour plus d’explications.

Les tableaux ne sont pas la même chose que les pointeurs multidimensionnels en C. Le nom du tableau est interprété comme l’adresse du tampon qui le contient dans la plupart des cas, indépendamment de la façon dont vous l’indexez. Si A est déclaré comme int A[5][5] , alors A signifie généralement l’adresse du premier élément, c’est-à-dire qu’il est interprété comme un int * (en fait int *[5] ), pas comme un int ** du tout. Le calcul de l’adresse ne nécessite que deux éléments: A[x][y] = A + x + 5 * y . Ceci est pratique pour faire A[x + 5 * y] , il ne fait pas la promotion d’un tampon multidimensionnel.

Si vous voulez des pointeurs multidimensionnels en C, vous pouvez le faire aussi. La syntaxe serait très similaire, mais elle nécessite un peu plus de configuration. Il y a plusieurs façons de le faire.

Avec un seul tampon:

 int **A = malloc(5 * sizeof(int *)); A[0] = malloc(5 * 5 * sizeof(int)); int i; for(i = 1; i < 5; i++) { A[i] = A[0] + 5 * i; } 

Avec un tampon séparé pour chaque ligne:

 int **A = malloc(5 * sizeof(int *)); int i; for(i = 0; i < 5; i++) { A[i] = malloc(5 * sizeof(int)); } 

Vous êtes confus par l’équivalence des tableaux et des pointeurs.

Lorsque vous déclarez un tableau comme A[5][5] , parce que vous avez déclaré les deux dimensions, C allouera de la mémoire pour 25 objects contigus. C’est-à-dire que la mémoire sera allouée comme ceci:

 A00, A01, ... A04, A10, A11, ..., A14, A20, ..., A24, ... 

L’object résultant, A , est un pointeur sur le début de ce bloc de mémoire. Il est de type int * , pas int ** .

Si vous voulez un vecteur de pointeurs vers des tableaux, vous voulez déclarer vos variables comme:

 int *A[5], *B[5]; 

Cela vous donnerait:

 A0, A1, A2, A3, A4 

tous de type int* , que vous devez remplir en utilisant malloc() ou autre.

Alternativement, vous pouvez déclarer C comme int **C

Bien que les tableaux et les pointeurs soient étroitement associés, ils ne sont pas du tout la même chose. Les gens sont parfois perplexes à ce sujet car, dans la plupart des contextes, les valeurs de tableau se transforment en pointeurs, et parce que la notation de tableau peut être utilisée dans des prototypes de fonctions pour déclarer des parameters qui sont en fait des pointeurs. En outre, ce que beaucoup de gens considèrent comme une notation d’indexation de tableaux effectue une combinaison d’arithmétique de pointeurs et de déréférencement, de sorte qu’elle fonctionne aussi bien pour les valeurs de pointeur que pour les valeurs de tableau.

Vu la déclaration

 int A[5][5]; 

La variable A désigne un tableau de cinq tableaux de cinq int . Cela se désintègre en un pointeur de type int (*)[5] , c’est-à-dire un pointeur sur un tableau de 5 int . Par contre, un pointeur sur l’ensemble du tableau multidimensionnel a le type int (*)[5][5] (pointeur sur un tableau de 5 tableaux de 5 int ), ce qui est tout à fait différent d’ int *** (pointeur pointer sur le pointeur sur int ). Si vous voulez déclarer un pointeur sur un tableau multidimensionnel tel que celui-ci, vous pouvez le faire comme ceci:

 int A[5][5]; int B[5][5]; int (*C)[5][5] = &A; 

Si vous voulez déclarer un tableau de tels pointeurs, vous pouvez le faire:

 int (*D[2])[5][5] = { &A, &B }; 

Ajoutée:

Ces distinctions entrent en jeu de diverses manières, les plus importantes étant les contextes où les valeurs des tableaux ne se dégradent pas en pointeurs et les contextes liés à celles-ci. L’un des plus importants d’entre eux est lorsqu’une valeur est l’opérande de l’opérateur sizeof . Compte tenu des déclarations ci-dessus, toutes les expressions relationnelles suivantes sont évaluées à 1 (true):

 sizeof(A) == 5 * 5 * sizeof(int) sizeof(A[0]) == 5 * sizeof(int) sizeof(A[0][4]) == sizeof(int) sizeof(D[1]) == sizeof(C) sizeof(*C) == sizeof(A) 

En outre, il est probable, mais pas garanti, que ces expressions relationnelles soient évaluées à 1:

 sizeof(C) == sizeof(void *) sizeof(D) == 2 * sizeof(void *) 

Ceci est fondamental pour le fonctionnement de l’indexation de la baie, et essentiel pour comprendre quand vous allouez de la mémoire.

Soit vous devez déclarer le troisième tableau comme

 int A[5][5]; int B[5][5]; int ( *C[] )[N][N] = { &A, &B }; 

c’est comme un tableau de pointeurs vers des tableaux à deux dimensions.

Par exemple

 #include  #define N 5 void output( int ( *a )[N][N] ) { for ( size_t i = 0; i < N; i++ ) { for ( size_t j = 0; j < N; j++ ) printf( "%2d ", ( *a )[i][j] ); printf( "\n" ); } } int main( void ) { int A[N][N] = { { 1, 2, 3, 4, 5 }, { 6, 7, 8, 9, 10 }, { 11, 12, 13, 14, 15 }, { 16, 17, 18, 19, 20 }, { 21, 22, 23, 24, 25 } }; int B[N][N] = { { 25, 24, 23, 22, 21 }, { 20, 19, 18, 17, 16 }, { 15, 14, 13, 12, 11 }, { 10, 9, 8, 7, 6 }, { 5, 4, 3, 2, 1 } }; /* typedef int ( *T )[N][N]; TC[] = { &A, &B }; */ int ( *C[] )[N][N] = { &A, &B }; output( C[0] ); printf( "\n" ); output( C[1] ); printf( "\n" ); } 

La sortie du programme est

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 

ou comme

 int A[5][5]; int B[5][5]; int ( *C[] )[N] = { A, B }; 

c'est comme un tableau de pointeurs vers les premiers éléments des tableaux à deux dimensions.

Par exemple

 #include  #define N 5 void output( int ( *a )[N] ) { for ( size_t i = 0; i < N; i++ ) { for ( size_t j = 0; j < N; j++ ) printf( "%2d ", a[i][j] ); printf( "\n" ); } } int main( void ) { int A[N][N] = { { 1, 2, 3, 4, 5 }, { 6, 7, 8, 9, 10 }, { 11, 12, 13, 14, 15 }, { 16, 17, 18, 19, 20 }, { 21, 22, 23, 24, 25 } }; int B[N][N] = { { 25, 24, 23, 22, 21 }, { 20, 19, 18, 17, 16 }, { 15, 14, 13, 12, 11 }, { 10, 9, 8, 7, 6 }, { 5, 4, 3, 2, 1 } }; /* typedef int ( *T )[N]; TC[] = { A, B }; */ int ( *C[] )[N] = { A, B }; output( C[0] ); printf( "\n" ); output( C[1] ); printf( "\n" ); } 

La sortie du programme est la même que ci-dessus

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 

en fonction de la façon dont vous allez utiliser le troisième tableau.

Utiliser typedefs (montré dans le programme démonstratif comme commenté) simplifie les définitions des tableaux.

En ce qui concerne cette déclaration

 int*** C = {&A, &B}; 

alors, dans la partie gauche, un pointeur de type int *** est un object scalaire alors que dans la partie droite se trouve une liste d'initialiseurs de type int ( * )[N][N] .

Le compilateur émet donc un message.

Je crois beaucoup à l’utilisation de typedef :

 #define SIZE 5 typedef int OneD[SIZE]; // OneD is a one-dimensional array of ints typedef OneD TwoD[SIZE]; // TwoD is a one-dimensional array of OneD's // So it's a two-dimensional array of ints! TwoD a; TwoD b; TwoD *c[] = { &a, &b, 0 }; // c is a one-dimensional array of pointers to TwoD's // That does NOT make it a three-dimensional array! int main() { for (int i = 0; c[i] != 0; ++i) { // Test contents of c to not go to far! for (int j = 0; j < SIZE; ++j) { for (int k = 0; k < SIZE; ++k) { // c[i][j][k] = 0; // Error! This proves it's not a 3D array! (*c[i])[j][k] = 0; // You need to dereference the entry in c first } // for } // for } // for return 0; } // main()