Comment les pointeurs vers les pointeurs fonctionnent-ils en C? Quand les utiliseriez-vous?
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 "hello" commence. Donc, dans ce cas, s'il s'agit de la seule occurrence de "hello" en mémoire,
const char *c = "hello";
... définit c
comme étant un pointeur sur la chaîne "hello" (en lecture seule) et contient donc la valeur 63. c
doit lui-même être stocké quelque part: dans l'exemple ci-dessus à l'emplacement 58. Bien sûr, nous ne pouvons pas uniquement pointer aux personnages, mais aussi à d'autres pointeurs. Par exemple.:
const char **cp = &c;
Maintenant, cp
pointe sur c
, c'est-à-dire qu'il contient l'adresse de c
(qui est 58). Nous pouvons aller encore plus loin. Considérer:
const char ***cpp = &cp;
Maintenant, cpp
stocke l'adresse de cp
. Il a donc la valeur 55 (d'après l'exemple ci-dessus) et vous l'avez deviné: il est lui-même stocké à l'adresse 60.
Quant à pourquoi on utilise des pointeurs à pointeurs:
t
, une référence au tableau est de type t *
. Considérons maintenant un tableau de tableaux de type t
: naturellement, une référence à ce tableau 2D aura le type (t *)*
= t **
et constitue donc un pointeur sur un pointeur.char **
.f
devra accepter un argument de type t **
si elle doit modifier une variable de type t *
.Comment les pointeurs vers les pointeurs fonctionnent-ils en C?
Tout d'abord, un pointeur est une variable, comme toute autre variable, mais qui contient l'adresse d'une variable.
Un pointeur sur un pointeur est une variable, comme toute autre variable, mais qui contient l'adresse d'une variable. Il se trouve que cette variable est simplement un pointeur.
Quand les utiliseriez-vous?
Vous pouvez les utiliser lorsque vous devez renvoyer un pointeur sur une mémoire du tas, 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 ça:
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 y a aussi d'autres utilisations, comme l'argument main () de chaque programme C a un pointeur sur 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 sur des tableaux à 2 dimensions, il est préférable d'utiliser un pointeur sur un tableau à 2 dimensions.
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 sur un tableau à 2 dimensions correctement:
int (*myPointerTo2DimArray)[ROWS][COLUMNS]
Toutefois, vous ne pouvez pas utiliser de pointeur sur un tableau à 2 dimensions si vous souhaitez prendre en charge un nombre variable d'éléments pour ROWS et COLUMNS. Mais quand vous savez à l'avance, vous utiliseriez un tableau à 2 dimensions.
J'aime cet exemple de code "real world" de pointeur sur usage de pointeur, dans Git 2.0, commit 7b1004b :
Linus a dit une fois:
En fait, je souhaite 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 verrou, mais simplement une bonne utilisation des pointeurs sur pointeurs, etc.
Par exemple, j'ai vu trop de personnes supprimer une entrée de liste à lien unique en gardant une trace de l'entrée "prev", puis supprimer l'entrée, en effectuant une opération comme
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
et chaque fois que je vois un code comme celui-ci, je dis simplement "Cette personne ne comprend pas les indicateurs" Et c'est malheureusement assez commun.
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. Ensuite, lorsqu'ils parcourent la liste, ils peuvent supprimer l'entrée sans utiliser de condition, simplement en effectuant une opération.
*pp = entry->next
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 à la vidéo de 2016 " Problème du double pointeur de Linus Torvalds " de Philip Buuck.
kumar souligne dans les commentaires l'article de blog " Linus sur la compréhension des pointeurs ", 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 itérer dessus 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 le faire 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 au précédent,
- éliminant ainsi l'élément actuel (ligne 7).
Il existe un cas particulier ci-dessus - au début de l'itération, il n'y a pas d'entrée précédente (
prev
estNULL
), et donc pour supprimer la première entrée de la liste, vous devez modifier la tête elle-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 sur un pointeur plutôt que simplement un 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, puisque
pp
n'est pasNULL
au début. Simple et intelligent.En outre, quelqu'un dans ce fil a fait remarquer que c'est mieux parce que
*pp = entry->next
est atomique. Ce n'est certainement pas atomique.
L'expression ci-dessus contient deux opérateurs de déréférence (*
et->
) et une affectation. Aucune de ces trois choses n'est atomique.
_ {C'est une idée fausse courante, mais hélas, pratiquement rien dans C ne devrait être supposé être atomique} (y compris les opérateurs++
et--
)!
Lorsque nous couvrons des indicateurs d’un cours de programmation à l’université, nous avons reçu deux indications sur la manière de commencer à les apprendre. La première était de voir Pointer Fun With Binky . La seconde était de penser aux Les yeux de Haddocks passage de Lewis Carroll's À travers le miroir
"Tu es triste", dit le Chevalier d'un ton inquiet: "Laisse-moi te chanter une chanson pour te réconforter."
"Est-ce que c'est très long?" Demanda Alice, car elle avait entendu beaucoup de poésie ce jour-là.
«C'est long, dit le chevalier, mais c'est vraiment très beau. Tous ceux qui m'entendent le chanter - soit ça leur met les larmes aux yeux, soit sinon… »
"Ou bien quoi?" Dit Alice, car le chevalier avait fait une pause soudaine.
«Sinon, tu sais pas. 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, vous ne comprenez pas," dit le chevalier, l'air un peu vexé. «C'est comme ça que le nom s'appelle. Le nom est vraiment "Le vieil homme âgé". "
"Alors j’aurais dû dire" c’est comme ça que la chanson s’appelle "?" Se corrigea Alice.
«Non, tu ne devrais pas: c'est tout autre chose! La chanson s’appelle ‘Ways And Means’: mais c’est comme ça qu’elle s’appelle, vous savez! ”
"Alors, quelle est la chanson, alors?" Dit Alice, qui était à ce moment-là complètement désemparée.
«J'y venais», dit le chevalier. "La chanson est vraiment" A-sitting On A Gate ": et la mélodie est ma propre invention."
Vous voudrez peut-être lire ceci: Pointeurs en pointeurs
J'espère que cela aide à clarifier certains doutes fondamentaux.
Lorsqu'une référence à un pointeur est requise. Par exemple, lorsque vous souhaitez modifier la valeur (adresse pointée vers) d'une variable de pointeur déclarée dans l'étendue d'une fonction appelante à l'intérieur d'une fonction appelée.
Si vous transmettez un seul pointeur en tant qu'argument, vous modifierez les copies locales du pointeur, et non le pointeur d'origine dans l'étendue de l'appel. Avec un pointeur sur un pointeur, vous modifiez ce dernier.
Un pointeur sur un pointeur est également appelé un handle. Une utilisation est souvent lorsqu'un objet peut être déplacé en mémoire ou supprimé. On est souvent responsable de verrouiller et déverrouiller l'utilisation de objet afin qu'il ne soit pas déplacé lors de son accès.
Il est souvent utilisé dans des environnements à mémoire restreinte, tels que Palm OS.
Vous avez une variable qui contient l'adresse de quelque chose. C'est un pointeur.
Ensuite, vous avez une autre variable qui contient l'adresse de la première variable. C'est un pointeur à pointeur.
Considérez la figure et le programme ci-dessous pour mieux comprendre ce concept.
Selon la figure, ptr1 est un pointeur unique ayant l'adresse de la variable num.
ptr1 = #
De même, ptr2 est un pointeur sur 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 = #
// 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);
}
Sortie:
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
c'est un pointeur sur la valeur d'adresse du pointeur. (c'est terrible je sais)
en gros, il vous permet de passer un pointeur sur la valeur de l'adresse d'un autre pointeur, afin que vous puissiez modifier l'emplacement d'un autre pointeur à partir d'une sous-fonction, comme:
void changeptr(int** pp)
{
*pp=&someval;
}
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 sur d'autres tableaux, donc lorsque vous écrivez
dpointer [5] [6]
vous accédez au tableau qui contient des pointeurs vers d'autres tableaux en 5ème position, obtenez le pointeur (laissez fpointer son nom) puis accédez au 6ème élément du tableau référencé à ce tableau (donc, fpointer [6]).
J'ai créé une vidéo de 5 minutes qui explique le fonctionnement des pointeurs:
Comment ça marche: C'est une variable qui peut stocker un autre pointeur.
Quand les utiliseriez-vous: Plusieurs en utilisent un si votre fonction veut construire un tableau et le retourner à 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;
}
Il y a tellement d'explications utiles, mais je n'ai pas trouvé une description courte, 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);
Des informations utiles peuvent également être trouvées dans le sujet Que signifie référence et déréférence
Et je ne suis pas sûr de savoir quand les pointeurs peuvent être utiles, mais en commun, il est nécessaire de les utiliser lorsque vous effectuez des allocation de mémoire manuelle/dynamique - malloc, calloc, etc.
J'espère donc que cela aidera également à clarifier la problématique :)