40 votes

Tableau de pointeurs vers un tableau de taille fixe

J'ai essayé d'assigner deux tableaux de taille fixe à un tableau de pointeurs vers eux, mais le compilateur me prévient et je ne comprends pas pourquoi.

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

Ce code se compile avec l'avertissement suivant :

avertissement : initialisation à partir d'un type de pointeur incompatible [activé par défaut].

Si j'exécute le code, il soulèvera une défaut de segmentation . Cependant, si j'alloue dynamiquement A y B cela fonctionne très bien. Pourquoi ?

5 votes

Conseil : vérifiez les types de données

3 votes

"si je déclare A et B comme tableau dynamique...". C n'a pas cette fonctionnalité. Vous ne pouvez pas "déclarer A et B comme des tableaux dynamiques". Vous pouvez seulement les déclarer comme des pointeurs et ensuite manuellement allouer de la mémoire pour les faire se comporter comme des tableaux multidimensionnels dynamiques. Ces tableaux "construits manuellement" ne sont pas compatibles avec les tableaux multidimensionnels intégrés - ce sont deux choses complètement différentes.

0 votes

Ce code produit de l'UB. "L'initialisateur d'un scalaire doit être une expression unique, éventuellement entourée d'accolades." GCC et clang acceptent beaucoup de code merdique (comme une extension ?). Compiler en utilisant msvc produira une erreur (vie exemple ).

25voto

dbush Points 8590

Si vous voulez une déclaration de C qui correspond aux déclarations existantes de A y B tu dois le faire comme ça :

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

Le type de C se lit comme suit : " C est un tableau de pointeurs vers int [5][5] tableaux ". Comme vous ne pouvez pas assigner un tableau entier, vous devez assigner un pointeur sur le 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] Dans ce cas, vous devez faire ce que d'autres ont dit et allouer la mémoire de manière dynamique :

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 se lit comme suit : " C est un tableau de pointeurs vers int [5] tableaux ". Dans ce cas, chaque élément du tableau de C est de type int (*)[5] et un tableau de type int [5][5] peut se décomposer en 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 également être étendue à des dimensions supérieures :

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

5 votes

C'est vrai ! Mais si c'est possible, je n'y toucherais pas avec un bâton de dix pieds ;)

0 votes

Oui... ça me donne des frissons.

19voto

Lundin Points 21616

Malheureusement, il y a beaucoup de livres/tutoriels/professeurs merdiques qui vous apprendront de mauvaises choses.....

Oubliez les pointeurs, ils n'ont rien à voir avec les tableaux. Point barre.

En règle générale, si vous utilisez plus de deux niveaux d'indirection, cela signifie que la conception de votre programme est fondamentalement défectueuse et qu'il doit être refait à partir de zéro.


Pour faire cela correctement, vous devriez faire comme ceci :

Un pointeur vers un tableau int [5][5] s'appelle 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 il s'agirait d'un 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 semble inutilement compliqué - et il l'est. Il sera difficile d'accéder aux éléments individuels, car vous devrez taper (*arr[x])[y][z] . Signification : "dans le tableau des pointeurs de tableau, prendre le pointeur de tableau numéro x, prendre le contenu qu'il pointe - qui est un tableau 2D - puis prendre l'élément d'indice [y][z] dans ce tableau".

Inventer de telles constructions est tout simplement de la folie et rien que je puisse recommander. Je suppose que le code peut être simplifié en travaillant avec un simple pointeur de tableau :

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;

Toutefois, ce code reste quelque peu compliqué. Envisagez un design entièrement différent ! Exemples :

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

0 votes

Je vois une grande valeur dans votre règle du pouce. Le C++ ne interdire mais il vous permet de les éviter.

14voto

John Bode Points 33046

Il y a beaucoup de choses qui ne vont pas avec la ligne

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

Vous déclarez un simple pointeur C mais vous lui demandez de pointer vers plusieurs objets ; cela ne fonctionnera pas. Ce que vous devez faire, c'est déclarer C comme un tableau de pointeurs vers ces tableaux.

Les types de &A y &B son int (*)[5][5] ou "pointeur vers un tableau à 5 éléments d'un 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 à 5 éléments de int ", ou

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

qui se lit comme suit

      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 vraiment très moche. C'est encore plus moche si vous voulez accéder à un élément de l'un ou l'autre des éléments suivants A o B par le biais de 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 que nous puissions indexer le tableau vers lequel il pointe, et puisque l'opérateur d'indice [] a une priorité plus élevée que l'option unaire * nous devons regrouper *C[0] entre parenthèses.

On peut nettoyer un peu tout ça. Sauf quand c'est l'opérande de la sizeof ou unaire & (ou un littéral de chaîne de caractères utilisé pour initialiser un autre tableau dans une déclaration), un opérateur expression de type " N -tableau d'éléments de T "sera converti ("décomposition") en 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 y B avoir le type int [5][5] ou "tableau à 5 éléments de tableau à 5 éléments de int ". Par la règle ci-dessus, les deux expressions "se décomposent" en expressions de type "pointeur vers un tableau de 5 éléments de int ", ou int (*)[5] . Si nous initialisons le tableau avec A y B au lieu de &A y &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 };

D'accord, c'est encore assez choquant, mais c'est le plus propre que l'on puisse obtenir sans typographie.

Alors comment accéder aux éléments de A y B par le biais de C ?

Rappelez-vous que l'opération d'indice de tableau a[i] es défini como *(a + i) c'est-à-dire, étant donné une adresse de base a , offset i éléments ( pas d'octets ) 1 à partir 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]

En mettant tout ça 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

Nous pouvons indexer C comme si il s'agissait d'un réseau 3D de int ce 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] y &A[0][0] tous donnent le même résultat valeur (l'adresse d'un tableau et l'adresse du premier élément du tableau sont toujours les mêmes), mais la fonction types sont différentes, comme le montre le tableau ci-dessus.


  1. L'arithmétique des pointeurs prend en compte la taille du type pointé ; si p contient l'adresse d'un int l'objet, alors p+1 donne l'adresse du suivant int objet qui peut se trouver à 2 ou 4 octets.

0 votes

Très bien, en particulier votre suggestion sur la façon de déclarer C afin qu'il puisse être indexé comme s'il s'agissait d'un tableau 3D.

9voto

haccks Points 33022

Une idée fausse très répandue chez les débutants en C est qu'ils supposent que les pointeurs et les tableaux sont équivalents. C'est totalement 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, les débutants ont vérifié 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 s'affiche à propos d'une affectation de pointeur incompatible et ils pensent : "Oh mon Dieu ! Pourquoi ce tableau est-il devenu Harvey Dent ?".

Certains ont même une longueur d'avance

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

et ensuite Riddler vient à leur cauchemar de l'équivalence tableau-pointeur.

enter image description here

N'oubliez jamais 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 de type tableau ). Ce problème a été abordé il y a plusieurs années dans le C-FAQ :

Dire que les tableaux et les pointeurs sont "équivalents" ne signifie pas qu'ils sont identiques ni même interchangeables. Ce que cela signifie, c'est que l'arithmétique des tableaux et des pointeurs est définie de telle sorte qu'un pointeur peut être utilisé de manière pratique pour accéder à un tableau ou pour simuler un tableau. En d'autres termes, comme l'a dit Wayne Throop, c'est "l'arithmétique des pointeurs et l'indexation des tableaux [qui] sont équivalents en C, les pointeurs et les tableaux sont différents". )

Souvenez-vous toujours de quelques règles importantes pour éviter ce genre de confusion :

  • Les tableaux ne sont pas des pointeurs. 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 lorsqu'un opérande de l'option sizeof y & opérateur.
  • C'est le arithmétique des pointeurs y indexation des tableaux qui sont les mêmes.
  • 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 qu'en

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

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

Sur

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 un tableau) qui est incompatible avec le type int ** . Il devrait être

int (*p2)[5] = a2;

De même pour

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

éléments de a3 est de type int [5][10] et le pointeur vers son premier élément serait de type int (*)[5][10] mais p3 est de int *** donc, pour les rendre compatibles, il faut que ce soit

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

Venons-en maintenant à votre extrait

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

&A y &B sont de type int(*)[5][5] . C est de type int*** il ne s'agit pas d'un tableau. Puisque vous voulez faire C pour contenir l'adresse des deux tableaux A y B vous devez déclarer C comme un tableau de deux int(*)[5][5] éléments de type. Cela doit être fait comme

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

Cependant, si j'alloue dynamiquement A et B, cela fonctionne très bien. Comment cela se fait-il ?

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

  int*** C = &A;

En 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é d'une manière définie par l'implémentation) si une unité de traduction de prétraitement ou une unité de traduction contient une violation de toute règle ou contrainte syntaxique, même si le comportement est aussi explicitement spécifié comme non défini ou défini par l'implémentation.

Leer <a href="https://stackoverflow.com/q/28412577/2455888">ce poste </a>pour plus d'explications.

0 votes

@GauravJain ; Compilez ce code avec C99 et vous obtiendrez avertissements .

0 votes

Ok... mais comment ça a marché ? quelle coïncidence s'est produite quand je l'ai compilé avec gcc ?

0 votes

@GauravJain ; Les deux sont compilés avec GCC, la seule différence est que vous n'utilisez pas le drapeau C99 et donc votre code est compilé en mode C89 par défaut. Cette ancienne version de C peut ne pas produire d'avertissement.

8voto

Mad Physicist Points 3218

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] entonces A signifiera généralement l'adresse du premier élément, c'est-à-dire qu'il est interprété efficacement comme un int * (en fait int *[5] ), pas un int ** du tout. Le calcul de l'adresse nécessite juste deux éléments : A[x][y] = A + x + 5 * y . Il s'agit d'une commodité pour faire A[x + 5 * y] il ne favorise pas A à un tampon multidimensionnel.

Si vous voulez des pointeurs multidimensionnels en C, vous pouvez aussi le faire. La syntaxe serait très similaire, mais cela nécessite un peu plus de mise en place. Il existe deux façons courantes de procéder.

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

0 votes

Ok, je comprends maintenant. Cela explique tout :) Merci

0 votes

Je suis heureux de pouvoir vous aider. Je peux comprendre car j'ai fait la même erreur un nombre incalculable de fois avant de comprendre quel était le problème. Vous pouvez toujours upvoter/sélectionner ma réponse si vous l'aimez.

3 votes

Les tableaux ne sont pas du tout la même chose que les pointeurs, multidimensionnels ou autres. De plus, le nom d'un tableau désigne le tableau, et non un quelconque pointeur. Il est important de ne pas confondre cela avec le fait que les valeurs des tableaux décroissance aux pointeurs dans la plupart des contextes (mais pas tous).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X