Comment fonctionne le pointeur vers les pointeurs en C?

171

Comment fonctionnent les pointeurs vers les pointeurs en C? Quand les utiliseriez-vous?

se détendre
la source
43
Non, pas de devoirs .... je voulais juste savoir..coz je le vois beaucoup quand je lis le code C.
1
Un pointeur vers un pointeur n'est pas un cas particulier de quelque chose, donc je ne comprends pas ce que vous ne comprenez pas à propos de void **.
akappa
pour les tableaux 2D, le meilleur exemple est la ligne de commande args "prog arg1 arg2" est stocké char ** argv. Et si l'appelant ne veut pas allouer la mémoire (la fonction appelée allouera la mémoire)
resultsway
1
Vous avez un bel exemple d'utilisation de "pointeur vers pointeur" dans Git 2.0: voir ma réponse ci
VonC

Réponses:

359

Supposons un ordinateur 8 bits avec des adresses 8 bits (et donc seulement 256 octets de mémoire). Cela fait partie de cette mémoire (les nombres en haut sont les adresses):

  54   55   56   57   58   59   60   61   62   63   64   65   66   67   68   69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
|    | 58 |    |    | 63 |    | 55 |    |    | h  | e  | l  | l  | o  | \0 |    |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+

Ce que vous pouvez voir ici, c'est qu'à l'adresse 63, la chaîne "bonjour" commence. Donc dans ce cas, si c'est la seule occurrence de "bonjour" en mémoire alors,

const char *c = "hello";

... définit ccomme étant un pointeur vers la chaîne (en lecture seule) "bonjour", et contient donc la valeur 63. cdoit elle-même être stockée quelque part: dans l'exemple ci-dessus à l'emplacement 58. Bien sûr, nous ne pouvons pas uniquement pointer vers des caractères , mais aussi vers d'autres pointeurs. Par exemple:

const char **cp = &c;

Maintenant cp , les points à c, qui est, il contient l'adresse de c(qui est 58). On peut aller encore plus loin. Considérer:

const char ***cpp = &cp;

Stocke maintenant cppl'adresse de cp. Donc, il a la valeur 55 (basé sur l'exemple ci-dessus), et vous l'avez deviné: il est lui-même stocké à l'adresse 60.


Quant à savoir pourquoi on utilise des pointeurs vers des pointeurs:

  • Le nom d'un tableau renvoie généralement l'adresse de son premier élément. Donc, si le tableau contient des éléments de type t, une référence au tableau a un typet * . Considérons maintenant un tableau de tableaux de type t: naturellement une référence à ce tableau 2D aura type (t *)*= t **, et est donc un pointeur vers un pointeur.
  • Même si un tableau de chaînes semble unidimensionnel, il est en fait bidimensionnel, car les chaînes sont des tableaux de caractères. Par conséquent: char **.
  • Une fonction fdevra accepter un argument de typet ** si elle veut modifier une variable de type t *.
  • De nombreuses autres raisons trop nombreuses pour être énumérées ici.
Stephan202
la source
7
oui bon exemple..je comprends ce qu'ils sont..mais comment et quand les utiliser est plus important..maintenant ..
2
Stephan a fait un bon travail en reproduisant, en gros, le diagramme dans le langage de programmation C de Kernighan & Richie. Si vous programmez C, et que vous n'avez pas ce livre et que vous êtes cool avec la documentation papier, je vous suggère fortement de l'obtenir, la dépense (assez) modeste se paiera très rapidement en productivité. Il a tendance à être très clair dans ses exemples.
J. Polfer
4
char * c = "bonjour" devrait être const char * c = "bonjour". De plus, il est tout au plus trompeur de dire qu '«un tableau est stocké comme adresse du premier élément». Un tableau est stocké comme ... un tableau. Souvent, son nom renvoie un pointeur vers son premier élément, mais pas toujours. A propos des pointeurs vers des pointeurs, je dirais simplement qu'ils sont utiles lorsqu'une fonction doit modifier un pointeur passé en paramètre (alors vous passez un pointeur vers le pointeur à la place).
Bastien Léonard
4
À moins que j'interprète mal cette réponse, cela semble faux. c est stocké en 58 et pointe vers 63, cp est stocké en 55 et pointe vers 58, et cpp n'est pas représenté dans le diagramme.
Thanatos
1
Cela semble bon. Un problème mineur était tout ce qui m'empêchait de dire: Excellent message. L'explication elle-même était excellente. Passer à un vote positif. (Peut-être que stackoverflow doit revoir les pointeurs?)
Thanatos
46

Comment fonctionnent les pointeurs vers les pointeurs en C?

Tout d'abord, un pointeur est une variable, comme toute autre variable, mais qui contient l'adresse d'une variable.

Un pointeur vers un pointeur est une variable, comme toute autre variable, mais qui contient l'adresse d'une variable. Cette variable se trouve être juste un pointeur.

Quand les utiliseriez-vous?

Vous pouvez les utiliser lorsque vous devez renvoyer un pointeur vers de la mémoire sur le tas, mais sans utiliser la valeur de retour.

Exemple:

int getValueOf5(int *p)
{
  *p = 5;
  return 1;//success
}

int get1024HeapMemory(int **p)
{
  *p = malloc(1024);
  if(*p == 0)
    return -1;//error
  else 
    return 0;//success
}

Et vous l'appelez comme ceci:

int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5

int *p;    
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap

Il existe également d'autres utilisations, comme l'argument main () de chaque programme C a un pointeur vers un pointeur pour argv, où chaque élément contient un tableau de caractères qui sont les options de ligne de commande. Vous devez cependant faire attention lorsque vous utilisez des pointeurs de pointeurs pour pointer vers des tableaux à 2 dimensions, il est préférable d'utiliser un pointeur vers un tableau à 2 dimensions à la place.

Pourquoi c'est dangereux?

void test()
{
  double **a;
  int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)

  double matrix[ROWS][COLUMNS];
  int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}

Voici un exemple de pointeur vers un tableau à 2 dimensions fait correctement:

int (*myPointerTo2DimArray)[ROWS][COLUMNS]

Vous ne pouvez pas utiliser un pointeur vers un tableau à 2 dimensions si vous souhaitez prendre en charge un nombre variable d'éléments pour les ROWS et COLUMNS. Mais quand vous savez à l'avance, vous utiliserez un tableau à 2 dimensions.

Brian R. Bondy
la source
32

J'aime cet exemple de code "réel" d'utilisation du pointeur vers le pointeur, dans Git 2.0, commit 7b1004b :

Linus a dit un jour:

Je souhaite en fait que plus de gens comprennent le type de codage de bas niveau vraiment fondamental. Pas de gros trucs complexes comme la recherche de nom sans verrouillage, mais simplement une bonne utilisation des pointeurs vers des pointeurs, etc.
Par exemple, j'ai vu trop de gens qui supprimaient une entrée de liste à un seul lien en gardant une trace de l'entrée "prev" , puis pour supprimer l'entrée, en faisant quelque chose comme

if (prev)
  prev->next = entry->next;
else
  list_head = entry->next;

et chaque fois que je vois du code comme celui-là, je dis simplement "Cette personne ne comprend pas les pointeurs". Et c'est malheureusement assez courant.

Les personnes qui comprennent les pointeurs utilisent simplement un " pointeur vers le pointeur d'entrée ", et l'initialisent avec l'adresse de list_head. Et puis lorsqu'ils parcourent la liste, ils peuvent supprimer l'entrée sans utiliser de conditionnelle, en faisant simplement un

*pp =  entry->next

http://i.stack.imgur.com/bpfxT.gif

L'application de cette simplification nous permet de perdre 7 lignes de cette fonction même en ajoutant 2 lignes de commentaire.

-   struct combine_diff_path *p, *pprev, *ptmp;
+   struct combine_diff_path *p, **tail = &curr;

Chris souligne dans les commentaires de la vidéo 2016 " Le problème du double pointeur de Linus Torvalds " de Philip Buuck .


kumar souligne dans les commentaires le billet de blog " Linus on Understanding Pointers ", où Grisha Trubetskoy explique:

Imaginez que vous ayez une liste chaînée définie comme:

typedef struct list_entry {
    int val;
    struct list_entry *next;
} list_entry;

Vous devez le parcourir du début à la fin et supprimer un élément spécifique dont la valeur est égale à la valeur de to_remove.
La manière la plus évidente de procéder serait:

list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;

while (entry) { /* line 4 */
    if (entry->val == to_remove)     /* this is the one to remove ; line 5 */
        if (prev)
           prev->next = entry->next; /* remove the entry ; line 7 */
        else
            head = entry->next;      /* special case - first entry ; line 9 */

    /* move on to the next entry */
    prev = entry;
    entry = entry->next;
}

Ce que nous faisons ci-dessus est:

  • itérer sur la liste jusqu'à ce que l'entrée soit NULL , ce qui signifie que nous avons atteint la fin de la liste (ligne 4).
  • Lorsque nous rencontrons une entrée que nous voulons supprimer (ligne 5),
    • on assigne la valeur du pointeur suivant actuel au précédent,
    • éliminant ainsi l'élément courant (ligne 7).

Il y a un cas spécial ci-dessus - au début de l'itération, il n'y a pas d'entrée précédente ( prevestNULL ), et donc pour supprimer la première entrée de la liste, vous devez modifier l'en-tête lui-même (ligne 9).

Ce que Linus disait, c'est que le code ci-dessus pourrait être simplifié en faisant de l'élément précédent un pointeur vers un pointeur plutôt qu'un simple pointeur .
Le code ressemble alors à ceci:

list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;

while (entry) {
    if (entry->val == to_remove)
        *pp = entry->next;

    pp = &entry->next;
    entry = entry->next;
}

Le code ci-dessus est très similaire à la variante précédente, mais notez que nous n'avons plus besoin de surveiller le cas particulier du premier élément de la liste, car ce ppn'est pas NULLau début. Simple et intelligent.

En outre, quelqu'un dans ce fil a commenté que la raison pour laquelle c'est mieux est parce que *pp = entry->nextc'est atomique. Ce n'est certainement PAS atomique .
L'expression ci-dessus contient deux opérateurs de déréférencement ( *et ->) et une affectation, et aucune de ces trois choses n'est atomique.
C'est une idée fausse courante, mais hélas, à peu près rien en C ne doit jamais être supposé être atomique (y compris les opérateurs ++et --)!

VonC
la source
4
Cela aidera à mieux comprendre - grisha.org/blog/2013/04/02/linus-on-understanding-pointers
kumar
@kumar bonne référence. je l'ai inclus dans la réponse pour plus de visibilité.
VonC
Cette vidéo a été essentielle pour moi pour comprendre votre exemple. En particulier, je me sentais confus (et belliqueux) jusqu'à ce que je dessine un diagramme de mémoire et retrace la progression du programme. Cela dit, cela me semble encore un peu mystérieux.
Chris
@Chris Super vidéo, merci de l'avoir mentionnée! J'ai inclus votre commentaire dans la réponse pour plus de visibilité.
VonC le
14

Lorsque nous avons abordé les pointeurs d'un cours de programmation à l'université, nous avons reçu deux conseils sur la façon de commencer à en apprendre davantage sur eux. La première était de voir Pointer Fun With Binky . La seconde était de penser au passage des Haddocks 'Eyes de Lewis Carroll's Through the Looking-Glass

«Vous êtes triste», dit le chevalier d'un ton anxieux: «Laissez-moi vous chanter une chanson pour vous réconforter.

"C'est très long?" Demanda Alice, car elle avait entendu beaucoup de poésie ce jour-là.

«C'est long», a déclaré le Chevalier, «mais c'est très, très beau. Tous ceux qui m'entendent le chanter - soit ça leur fait monter les larmes aux yeux, soit… »

"Ou bien quoi?" dit Alice, car le chevalier avait fait une brusque pause.

«Ou bien ce n'est pas le cas, tu sais. Le nom de la chanson s'appelle «Haddocks» Eyes. »»

"Oh, c'est le nom de la chanson, n'est-ce pas?" Dit Alice, essayant de se sentir intéressée.

«Non, tu ne comprends pas,» dit le chevalier, l'air un peu vexé. «C'est comme ça que le nom s'appelle. Le nom est vraiment «The Aged Aged Man». »

"Alors j'aurais dû dire 'C'est comme ça que la chanson s'appelle'?" Alice se corrigea.

«Non, vous ne devriez pas: c'est tout autre chose! La chanson s'appelle «Ways And Means»: mais ce n'est que comme ça qu'elle s'appelle, vous savez! »

«Eh bien, quelle est la chanson, alors?» dit Alice, qui était à ce moment complètement perplexe.

«J'y arrivais», a déclaré le chevalier. "La chanson est vraiment 'A-sitting On A Gate': et la chanson est ma propre invention."

Edd
la source
1
J'ai dû lire ce passage plusieurs fois ... +1 pour m'avoir fait réfléchir!
Ruben Steins
C'est pourquoi Lewis Carroll n'est pas un écrivain ordinaire.
metarose
1
Alors ... ça irait comme ça? nom -> 'The Aged Aged Man' -> appelé -> 'Haddock's Eyes' -> chanson -> 'A-sitting On A Gate'
tisaconundrum
7

Lorsqu'une référence à un pointeur est requise. Par exemple, lorsque vous souhaitez modifier la valeur (adresse pointée) d'une variable de pointeur déclarée dans la portée d'une fonction appelante à l'intérieur d'une fonction appelée.

Si vous passez un seul pointeur comme argument, vous modifierez les copies locales du pointeur, et non le pointeur d'origine dans la portée appelante. Avec un pointeur sur un pointeur, vous modifiez ce dernier.

Alex Balashov
la source
Bien expliqué pour la partie `` Pourquoi ''
Rana Deep
7

Un pointeur vers un pointeur est également appelé poignée . Une utilisation pour cela est souvent lorsqu'un objet peut être déplacé en mémoire ou supprimé. On est souvent chargé de verrouiller et de déverrouiller l'utilisation de l' objet afin qu'il ne soit pas déplacé lors de son accès.

Il est souvent utilisé dans un environnement à mémoire restreinte, c'est-à-dire le Palm OS.

computer.howstuffworks.com Lien >>

www.flippinbits.com Lien >>

Epatel
la source
7

Considérez la figure et le programme ci-dessous pour mieux comprendre ce concept .

Diagramme à double pointeur

Selon la figure, ptr1 est un pointeur unique qui a l'adresse de la variable num .

ptr1 = #

De même, ptr2 est un pointeur vers un pointeur (double pointeur) qui a l'adresse du pointeur ptr1 .

ptr2 = &ptr1;

Un pointeur qui pointe vers un autre pointeur est appelé double pointeur. Dans cet exemple, ptr2 est un double pointeur.

Valeurs du diagramme ci-dessus:

Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000

Exemple:

#include <stdio.h>

int main ()
{
   int  num = 10;
   int  *ptr1;
   int  **ptr2;

   // Take the address of var 
   ptr1 = &num;

   // Take the address of ptr1 using address of operator &
   ptr2 = &ptr1;

   // Print the value
   printf("Value of num = %d\n", num );
   printf("Value available at *ptr1 = %d\n", *ptr1 );
   printf("Value available at **ptr2 = %d\n", **ptr2);
}

Production:

Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
msc
la source
5

c'est un pointeur vers la valeur d'adresse du pointeur. (c'est terrible je sais)

fondamentalement, il vous permet de passer un pointeur sur la valeur de l'adresse d'un autre pointeur, de sorte que vous pouvez modifier l'endroit où un autre pointeur pointe à partir d'une sous-fonction, comme:

void changeptr(int** pp)
{
  *pp=&someval;
}
Luke Schafer
la source
désolé, je sais que c'était assez mauvais. Essayez de lire, euh, ceci: codeproject.com/KB/cpp/PtrToPtr.aspx
Luke Schafer
5

Vous avez une variable qui contient une adresse de quelque chose. C'est un indicateur.

Ensuite, vous avez une autre variable qui contient l'adresse de la première variable. C'est un pointeur vers un pointeur.

Igor Oks
la source
3

Un pointeur vers un pointeur est, bien, un pointeur vers un pointeur.

Un exemple significatif de someType ** est un tableau bidimensionnel: vous avez un tableau, rempli de pointeurs vers d'autres tableaux, donc lorsque vous écrivez

dpointer [5] [6]

vous accédez au tableau qui contient des pointeurs vers d'autres tableaux à sa 5ème position, récupérez le pointeur (laissez fpointer son nom) puis accédez au 6ème élément du tableau référencé à ce tableau (donc, fpointer [6]).

Akappa
la source
2
les pointeurs vers des pointeurs ne doivent pas être confondus avec des tableaux de rang2, par exemple int x [10] [10] où vous écrivez x [5] [6] vous accédez à la valeur dans le tableau.
Pete Kirkham
Ceci n'est qu'un exemple où un vide ** est approprié. Un pointeur vers un pointeur n'est qu'un pointeur qui pointe vers un pointeur.
akappa
1

Comment ça marche: C'est une variable qui peut stocker un autre pointeur.

Quand les utiliseriez-vous: De nombreuses utilisations l'un d'entre eux est si votre fonction veut construire un tableau et le renvoyer à l'appelant.

//returns the array of roll nos {11, 12} through paramater
// return value is total number of  students
int fun( int **i )
{
    int *j;
    *i = (int*)malloc ( 2*sizeof(int) );
    **i = 11;  // e.g., newly allocated memory 0x2000 store 11
    j = *i;
    j++;
    *j = 12; ;  // e.g., newly allocated memory 0x2004 store 12

    return 2;
}

int main()
{
    int *i;
    int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
    for ( int j=0; j<n; j++ )
        printf( "roll no = %d \n", i[j] );

    return 0;
}
résultats
la source
0

Il y a tellement d'explications utiles, mais je n'ai pas trouvé juste une courte description, donc ..

Fondamentalement, le pointeur est l'adresse de la variable. Code de résumé court:

     int a, *p_a;//declaration of normal variable and int pointer variable
     a = 56;     //simply assign value
     p_a = &a;   //save address of "a" to pointer variable
     *p_a = 15;  //override the value of the variable

//print 0xfoo and 15 
//- first is address, 2nd is value stored at this address (that is called dereference)
     printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a); 

Vous trouverez également des informations utiles dans la rubrique Que signifie référence et déréférencement

Et je ne suis pas si sûr, quand les pointeurs peuvent être utiles, mais en commun, il est nécessaire de les utiliser lorsque vous faites une allocation de mémoire manuelle / dynamique - malloc, calloc, etc.

J'espère donc que cela aidera également à clarifier la problématique :)

xxxvodnikxxx
la source