web-dev-qa-db-fra.com

Différences lors de l'utilisation de ** en C

J'ai commencé à apprendre C récemment, et j'ai un problème pour comprendre la syntaxe du pointeur, par exemple lorsque j'écris la ligne suivante:

int ** arr = NULL;

Comment savoir si:

  • arr est un pointeur sur un pointeur d'un entier

  • arr est un pointeur vers un tableau de pointeurs sur des entiers

  • arr est un pointeur vers un tableau de pointeurs vers des tableaux d'entiers

N'est-ce pas pareil avec int **?


Si j'ai une fonction qui reçoit char ** s comme paramètre, je veux y faire référence comme pointer à un tableau de chaînes, ce qui signifie un pointeur vers un tableau de pointeurs vers un tableau de chars, mais est-ce aussi un pointeur vers un pointeur vers un char?

45
ChikChak

N'est-ce pas pareil avec int **?

Vous venez de découvrir ce qui peut être considéré comme une faille dans le système de type. Chaque option que vous avez spécifiée peut être vraie. Il est essentiellement dérivé d'une vue plate d'une mémoire de programmes, où une seule adresse peut être utilisée pour référencer diverses dispositions de mémoire logique.

La façon dont les programmeurs C ont géré cela depuis la création de C est de mettre en place une convention. Tels que la demande de paramètres de taille pour les fonctions qui acceptent de tels pointeurs et la documentation de leurs hypothèses sur la disposition de la mémoire. Ou exiger que les tableaux se terminent par une valeur spéciale, permettant ainsi aux tampons "pointus" de pointeurs de tamponner.


Je pense qu'un certain nombre de clarifications s'impose. Comme vous le verriez en consultant les autres très bonnes réponses ici, les tableaux ne sont certainement pas des pointeurs. Ils se décomposent cependant en ceux dans suffisamment de contextes pour justifier une erreur de plusieurs décennies dans leur enseignement (mais je m'égare).

Ce que j'ai écrit à l'origine fait référence au code comme suit:

void func(int **p_buff)
{
}

//...

int a = 0, *pa = &a;
func(&pa);

//...

int a[3][10];
int *a_pts[3] = { a[0], a[1], a[2] };
func(a_pts);

//...

int **a = malloc(10 * sizeof *a);
for(int i = 0; i < 10; ++i)
  a[i] = malloc(i * sizeof *a[i]);
func(a);

Supposons que func et chaque extrait de code est compilé dans une unité de traduction distincte. Chaque exemple (sauf toute faute de frappe de ma part) est valide C. Les tableaux se désintégreront en un "pointeur vers un pointeur" lorsqu'ils seront passés en arguments. Comment est la définition de func pour savoir exactement ce qui a été passé à partir du type de son seul paramètre!? La réponse est que non. Le type statique de p_buff est int**, mais il permet toujours à func d'accéder indirectement à (parties) d'objets avec des types efficaces très différents.

44
StoryTeller

La déclaration int **arr dit: "déclarer arr comme pointeur vers un pointeur vers un entier". Il (s'il est valide) pointe vers un seul pointeur qui pointe (s'il est valide) vers un seul objet entier. Comme il est possible d'utiliser l'arithmétique des pointeurs avec l'un ou l'autre niveau d'indirection (c'est-à-dire *arr est le même que arr[0] et **arr est le même que arr[0][0]), l'objet peut être utilisé pour accès l'un des 3 de votre question (c'est-à-dire, pour la seconde, accéder à un tableau de pointeurs vers des nombres entiers, et pour la troisième, accéder à un tableau de pointeurs vers premiers éléments des tableaux entiers), à condition que les pointeurs pointent vers les premiers éléments des tableaux ...


Pourtant, arr est toujours déclaré comme un pointeur vers un single pointeur vers un single objet entier. Il est également possible de déclarer un pointeur sur une tablea de défini dimensions. Ici, a est déclaré comme un pointeur vers un tableau de pointeurs à 10 éléments vers des tableaux de 10 entiers:

cdecl> declare a as pointer to array 10 of pointer to array 10 of int;
int (*(*a)[10])[10]

En pratique, les pointeurs de tableau sont les plus utilisés pour passer dans des tableaux multidimensionnels de dimensions constantes en fonctions, et pour passer dans des tableaux de longueur variable. La syntaxe pour déclarer une variable comme pointeur sur un tableau est rarement vue, car chaque fois qu’elles sont passées dans une fonction, il est un peu plus facile d’utiliser à la place des paramètres de type "tableau de taille indéfinie", donc au lieu de déclarer

void func(int (*a)[10]);

on pourrait utiliser

void func(int a[][10])

pour passer dans un tableau multidimensionnel de tableaux de 10 entiers. Alternativement, un typedef peut être utilisé pour réduire le mal de tête.

27
Antti Haapala

Comment savoir si:

  • arr est un pointeur sur un pointeur d'un entier

C'est toujours un pointeur pour pointer vers un entier.

  • arr est un pointeur vers un tableau de pointeurs sur des entiers
  • arr est un pointeur vers un tableau de pointeurs vers des tableaux d'entiers

Ça ne peut jamais être ça. Un pointeur vers un tableau de pointeurs vers des entiers serait déclaré comme ceci:

int* (*arr)[n]

On dirait que vous avez été trompé pour utiliser int** par de mauvais enseignants/livres/tutoriels. C'est presque toujours une pratique incorrecte, comme expliqué ici et ici et (avec des explications détaillées sur les pointeurs de tableau) ici .

MODIFIER

Enfin, j'ai écrit un article détaillé expliquant ce que sont les tableaux, quelles sont les tables de recherche, pourquoi ces dernières sont mauvaises et ce que vous devriez utiliser à la place: Allocation correcte des tableaux multidimensionnels .

9
Lundin

Ayant uniquement la déclaration de la variable, vous ne pouvez pas distinguer les trois cas. On peut encore discuter s'il ne faut pas utiliser quelque chose comme int *x[10] pour exprimer un tableau de 10 pointeurs en pouces ou autre chose; mais int **x peut - en raison de l'arithmétique des pointeurs, être utilisé de trois manières différentes, en supposant chacune une disposition de mémoire différente avec la (bonne) chance de faire la mauvaise hypothèse.

Prenons l'exemple suivant, où un int ** est utilisé de trois manières différentes, c'est-à-dire p2p2i_v1 comme pointeur vers un pointeur vers un (unique) entier, p2p2i_v2 comme pointeur vers un tableau de pointeurs vers int, et p2p2i_v3 comme pointeur vers un pointeur vers un tableau d'entiers. Notez que vous ne pouvez pas distinguer ces trois significations uniquement par le type, qui est int** pour les trois. Mais avec des initialisations différentes, accéder à chacun d'eux de manière incorrecte produit quelque chose d'imprévisible, sauf accéder aux tout premiers éléments:

int i1=1,i2=2,i3=3,i4=4;

int *p2i = &i1;
int **p2p2i_v1 = &p2i;  // pointer to a pointer to a single int

int *arrayOfp2i[4] = { &i1, &i2, &i3, &i4 };
int **p2p2i_v2 = arrayOfp2i; // pointer to an array of pointers to int

int arrayOfI[4] = { 5,6,7,8 };
int *p2arrayOfi = arrayOfI;
int **p2p2i_v3 = &p2arrayOfi; // pointer to a pointer to an array of ints

// assuming a pointer to a pointer to a single int:
int derefi1_v1 = *p2p2i_v1[0];  // correct; yields 1
int derefi1_v2 = *p2p2i_v2[0];  // correct; yields 1
int derefi1_v3 = *p2p2i_v3[0];  // correct; yields 5

// assuming a pointer to an array of pointers to int's
int derefi1_v1_at1 = *p2p2i_v1[1];  // incorrect, yields ? or seg fault
int derefi1_v2_at1 = *p2p2i_v2[1]; // correct; yields 2
int derefi1_v3_at1 = *p2p2i_v3[1]; // incorrect, yields ? or seg fault


// assuming a pointer to an array of pointers to an array of int's
int derefarray_at1_v1 = (*p2p2i_v1)[1]; // incorrect; yields ? or seg fault;
int derefarray_at1_v2 = (*p2p2i_v2)[1]; // incorrect; yields ? or seg fault;
int derefarray_at1_v3 = (*p2p2i_v3)[1]; // correct; yields 6;
8
Stephan Lechner

Comment savoir si:

arr est un pointeur sur un pointeur d'un entier

arr est un pointeur vers un tableau de pointeurs sur des entiers

arr est un pointeur vers un tableau de pointeurs vers des tableaux d'entiers

Vous ne pouvez pas. Cela peut être n'importe lequel d'entre eux. Ce que cela finit par être dépend de la façon dont vous l'allocez/l'utilisez.

Donc, si vous écrivez du code à l'aide de ceux-ci, documentez ce que vous en faites, passez des paramètres de taille aux fonctions qui les utilisent et assurez-vous généralement de ce que vous avez alloué avant de l'utiliser.

7
Magisch

Les pointeurs ne conservent pas les informations qu'ils pointent vers un seul objet ou un objet qui est un élément d'un tableau. De plus, pour le pointeur, les objets simples arithmétiques sont considérés comme des tableaux constitués d'un élément.

Considérez ces déclarations

int a;
int a1[1];
int a2[10];

int *p;

p = &a;
//...
p = a1;
//...
p = a2;

Dans cet exemple, le pointeur p traite des adresses. Il ne sait pas si l'adresse qu'il stocke pointe vers un seul objet comme a ou vers le premier élément du tableau a1 qui n'a qu'un seul élément ou au premier élément du tableau a2 qui comporte dix éléments.

6
Vlad from Moscow

Le type de

int ** arr;

avoir qu'une seule interprétation valable. C'est:

arr is a pointer to a pointer to an integer

Si vous n'avez pas plus d'informations que la déclaration ci-dessus, c'est tout ce que vous pouvez en savoir, c'est-à-dire si arr est probablement initialisé, il pointe vers un autre pointeur qui - s'il est probablement initialisé - pointe vers un entier.

En supposant une initialisation correcte, la seule manière valide et garantie de l'utiliser est:

**arr = 42;
int a = **arr;

Cependant, C vous permet de l'utiliser de plusieurs manières.

• arr peut être utilisé comme pointeur vers un pointeur vers un entier (c'est-à-dire le cas de base)

int a = **arr;

• arr peut être utilisé comme pointeur vers un pointeur vers un tableau d'entiers

int a = (*arr)[4];

• arr peut être utilisé comme pointeur vers un tableau de pointeurs sur des entiers

int a = *(arr[4]);

• arr peut être utilisé comme pointeur vers un tableau de pointeurs vers des tableaux d'entiers

int a = arr[4][4];

Dans les trois derniers cas, il peut sembler que vous ayez un tableau. Cependant, le type est pas un tableau. Le type est toujours juste a pointer to a pointer to an integer - le déréférencement est l'arithmétique des pointeurs. Cela ne ressemble en rien à un tableau 2D.

Pour savoir ce qui est valide pour le programme en cours, vous devez regarder le code initialisant arr.

Mise à jour

Pour la partie mise à jour de la question:

Si tu as:

void foo(char** x) { .... };

la seule chose que vous savez avec certitude est que **x donnera un caractère et *x vous donnera un pointeur char (dans les deux cas, une bonne initialisation de x est supposée).

Si vous souhaitez utiliser x d'une autre manière, par exemple x[2] pour obtenir le troisième pointeur de caractère, il faut que l'appelant ait initialisé x pour qu'il pointe vers une zone mémoire qui a au moins 3 pointeurs de caractère consécutifs. Cela peut être décrit comme un contrat pour appeler foo.

6
4386427

Il y a une astuce lorsque vous utilisez des pointeurs, lisez-la de droite à gauche:

int** arr = NULL;

Qu'obtenez-vous: arr, *, *, int, donc array est un pointeur vers un pointeur sur un entier.

Et int **arr; est le même que int** arr;.

3

La syntaxe C est logique. Comme un astérisque devant l'identifiant dans la déclaration signifie pointeur sur le type de la variable, deux astérisques signifient pointeur sur un pointeur sur le type de la variable.

Dans ce cas, arr est a pointer to a pointer to integer.

Il existe plusieurs utilisations des pointeurs doubles. Par exemple, vous pourriez représenter une matrice avec un pointeur sur un vecteur de pointeurs. Chaque pointeur de ce vecteur pointe vers la ligne de la matrice elle-même.

On peut également créer un tableau à deux dimensions en l'utilisant, comme ceci

 int **arr=(int**)malloc(row*(sizeof(int*)));  
 for(i=0;i<row;i++) {
 *(arr+i)=(int*)malloc(sizeof(int)*col); //You can use this also. Meaning of both is same. //
  arr[i]=(int*)malloc(sizeof(int)*col); }
3
minigeek
int ** arr = NULL;

C'est dire au compilateur, arr is a double pointer of an integer et la valeur NULL attribuée.

2
msc