J'ai cherché et cherché une réponse à cette question, mais je ne trouve rien que je "reçoive".
Je suis très nouveau dans le domaine du c ++ et je n'arrive pas à me faire une idée de l'utilisation de pointeurs doubles, triples, etc. À quoi servent-ils?
Quelqu'un peut-il m'éclairer
Honnêtement, en C++ bien écrit, vous devriez rarement voir un T**
en dehors du code de la bibliothèque. En fait, plus vous avez d'étoiles, plus vous êtes près de gagner un prix d'une certaine nature .
Cela ne veut pas dire qu'un pointeur à pointeur n'est jamais appelé; vous devrez peut-être construire un pointeur sur un pointeur pour la même raison que vous avez besoin de construire un pointeur sur un autre type d'objet.
En particulier, je pourrais m'attendre à voir une telle chose dans une implémentation d'algorithme ou de structure de données, lorsque vous mélangez des nœuds alloués dynamiquement, peut-être?
En général, cependant, en dehors de ce contexte, si vous devez faire passer une référence à un pointeur, vous feriez exactement cela (c.-à-d. T*&
) plutôt que de doubler les pointeurs, et même cela devrait être assez rare. .
Sur Stack Overflow, vous verrez des gens faire des choses horribles avec des pointeurs vers des tableaux de pointeurs alloués dynamiquement aux données, en essayant de mettre en œuvre le "vecteur 2D" le moins efficace auquel ils puissent penser. S'il vous plaît ne soyez pas inspiré par eux.
En résumé, votre intuition n’est pas sans mérite.
Une raison importante pour laquelle vous devez/devez savoir sur le lien point à point -... est que vous devez parfois vous connecter à d'autres langages (comme le C par exemple) via une API (par exemple, l'API Windows).
Ces API ont souvent des fonctions qui ont un paramètre de sortie qui retourne un pointeur. Cependant, ces autres langages n'ont souvent ni références ni références compatibles (avec C++). C'est une situation où un pointeur à l'autre est nécessaire.
Je suis très nouveau dans le domaine du c ++ et je n'arrive pas à me faire une idée de l'utilisation de pointeurs doubles, triples, etc. À quoi servent-ils?
Le truc pour comprendre les pointeurs en C est simplement de revenir aux bases, qui n’ont probablement jamais été enseignées. Elles sont:
x
est une variable de type T
, alors &x
est une valeur de type T*
.x
donne une valeur de type T*
, alors *x
est une variable de type T
. Plus précisement...x
donne une valeur de type T*
égale à &a
pour une variable a
de type T
, alors *x
est un alias pour a
.Maintenant tout suit:
int x = 123;
x
est une variable de type int
. Sa valeur est 123
.
int* y = &x;
y
est une variable de type int*
. x
est une variable de type int
. Donc, &x
est une valeur de type int*
. Par conséquent, nous pouvons stocker &x
dans y
.
*y = 456;
y
évalue le contenu de la variable y
. C'est une valeur de type int*
. L'application de *
à une valeur de type int*
donne une variable de type int
. Nous pouvons donc lui affecter 456. Qu'est-ce que *y
? C'est un alias pour x
. Par conséquent, nous venons d’attribuer 456 à x
.
int** z = &y;
Qu'est-ce que z
? C'est une variable de type int**
. Qu'est-ce que &y
? Puisque y
est une variable de type int*
, &y
doit être une valeur de type int**
. Par conséquent, nous pouvons l'assigner à z
.
**z = 789;
Qu'est-ce que **z
? Travailler de l'intérieur. z
est évalué à int**
. Par conséquent, *z
est une variable de type int*
. C'est un alias pour y
. C'est donc pareil que *y
, et on sait déjà ce que c'est; c'est un alias pour x
.
Non vraiment, à quoi ça sert?
Ici, j'ai un morceau de papier. Il dit 1600 Pennsylvania Avenue Washington DC
. Est-ce une maison? Non, c'est un morceau de papier avec l'adresse d'une maison écrite dessus. Mais nous pouvons utiliser ce morceau de papier pour trouver la maison.
Ici, j'ai dix millions de feuilles de papier, toutes numérotées. Le numéro de papier 123456 indique 1600 Pennsylvania Avenue
. 123456 est une maison? Est-ce un morceau de papier? Non, mais c’est toujours assez d’informations pour trouver la maison}.
C’est le point: pour des raisons pratiques, nous devons souvent faire référence à des entités par le biais de plusieurs niveaux d’indirection.
Cela dit, les doubles pointeurs sont source de confusion et indiquent que votre algorithme est insuffisamment abstrait. Essayez de les éviter en utilisant de bonnes techniques de conception.
C'est moins utilisé en c ++. Cependant, en C, cela peut être très utile. Dites que vous avez une fonction qui va malloc une certaine quantité de mémoire aléatoire et la remplir de mémoire. Il serait difficile d’appeler une fonction pour obtenir la taille que vous devez allouer, puis d’appeler une autre fonction qui remplira la mémoire. Au lieu de cela, vous pouvez utiliser un double pointeur. Le double pointeur permet à la fonction de définir le pointeur sur l'emplacement de la mémoire. Il y a d'autres choses pour lesquelles il peut être utilisé, mais c'est la meilleure chose à laquelle je puisse penser.
int func(char** mem){
*mem = malloc(50);
return 50;
}
int main(){
char* mem = NULL;
int size = func(&mem);
free(mem);
}
Un double pointeur est simplement un pointeur sur un pointeur. Un usage courant est pour les tableaux de chaînes de caractères. Imaginez la première fonction de presque tous les programmes C/C++:
int main(int argc, char *argv[])
{
...
}
Qui peut aussi être écrit
int main(int argc, char **argv)
{
...
}
La variable argv est un pointeur sur un tableau de pointeurs sur char. C'est un moyen standard de faire circuler des tableaux de "chaînes" en C. Pourquoi faire ça? Je l'ai vu utilisé pour la prise en charge multilingue, des blocs de chaînes d'erreur, etc.
N'oubliez pas qu'un pointeur est juste un nombre - l'index de la "fente" de mémoire à l'intérieur d'un ordinateur. C'est ça, rien de plus. Ainsi, un double pointeur est l'index d'un morceau de mémoire qui détient un autre index ailleurs. Une jointure mathématique si vous aimez.
Voici comment j'ai expliqué les indications à mes enfants:
Imaginez que la mémoire de l'ordinateur soit une série de boîtes. Chaque case comporte un numéro, commençant à zéro, augmentant de 1, quel que soit le nombre d'octets de mémoire disponibles. Supposons que vous ayez un pointeur sur un endroit de la mémoire. Ce pointeur n'est que le numéro de la boîte. Mon pointeur est, disons 4. Je regarde dans la case n ° 4. À l’intérieur, il y a un autre numéro, cette fois-ci, il s’agit du 6. Nous allons maintenant examiner la case 6 et obtenir la dernière chose que nous voulions. Mon pointeur d'origine (qui disait "4") était un pointeur double, car le contenu de sa boîte était l'index d'une autre boîte plutôt qu'un résultat final.
Il semble que ces derniers temps, les pointeurs eux-mêmes sont devenus un paria de la programmation. Dans un passé pas si lointain, il était tout à fait normal de faire passer des pointeurs entre pointeurs. Mais avec la prolifération de Java et l'utilisation croissante de la méthode de référence par passe en C++, la compréhension fondamentale des pointeurs s'est affaiblie, en particulier lorsque Java est devenu le langage d'initiation à l'informatique de première année, contrairement à Pascal et C.
Je pense que le plus souvent, c'est que les gens ne les comprennent pas correctement. Les choses que les gens ne comprennent pas se moquent. Alors ils sont devenus "trop durs" et "trop dangereux". J'imagine que même avec des personnes supposément instruites qui préconisent des pointeurs intelligents , etc., il faut s'attendre à ces idées. Mais en réalité, il existe un outil de programmation très puissant. Honnêtement, les pointeurs sont la magie de la programmation et, après tout, ce ne sont que des chiffres.
Dans de nombreuses situations, un Foo*&
remplace un Foo**
. Dans les deux cas, vous avez un pointeur dont l'adresse peut être modifiée.
Supposons que vous ayez un type abstrait non valeur et que vous deviez le renvoyer, mais que la valeur de retour est reprise par le code d'erreur:
error_code get_foo( Foo** ppfoo )
ou
error_code get_foo( Foo*& pfoo_out )
Maintenant, un argument de fonction étant mutable est rarement utile, ainsi la possibilité de changer où le le plus à l'extérieur pointeur ppFoo
est rarement utile. Cependant, un pointeur est nullable - donc si l'argument de get_foo
est optional , un pointeur se comporte comme une référence facultative.
Dans ce cas, la valeur de retour est un pointeur brut. S'il renvoie une ressource possédée, il devrait généralement s'agir plutôt de std::unique_ptr<Foo>*
- un pointeur intelligent à ce niveau d'indirection.
Si, au lieu de cela, il retourne un pointeur sur quelque chose dont il ne partage pas la propriété, un pointeur brut a alors plus de sens.
Foo**
a d'autres utilisations que ces "paramètres bruts". Si vous avez un type polymorphe sans valeur, les handles non propriétaires sont Foo*
, et pour la même raison que vous voudriez avoir un int*
, vous voudriez avoir un Foo**
.
Ce qui vous amène alors à vous demander "pourquoi voulez-vous un int*
?" En C++ moderne, int*
est une référence mutable nullable non propriétaire de int
. Il se comporte mieux lorsqu'il est stocké dans une struct
qu'une référence (les références dans les structures génèrent une sémantique déroutante autour de l'affectation et de la copie, surtout si elles sont mélangées à des non-références).
Vous pouvez parfois remplacer int*
par std::reference_wrapper<int>
, et bien std::optional<std::reference_wrapper<int>>
, mais notez que sa taille sera 2x plus grande qu'un simple int*
.
Il y a donc des raisons légitimes d'utiliser int*
. Une fois que vous avez cela, vous pouvez légitimement utiliser Foo**
lorsque vous souhaitez un pointeur sur un type sans valeur. Vous pouvez même accéder à int**
en ayant un tableau contigu de int*
s sur lequel vous souhaitez opérer.
Obtenir légitimement un programmateur trois étoiles devient plus difficile. Maintenant, vous avez besoin d’une raison légitime pour (par exemple) vouloir passer un Foo**
par indirection. Bien avant que vous n'atteigniez ce point, vous devriez avoir envisagé de résumer et/ou de simplifier la structure de votre code.
Tout cela ignore la raison la plus courante; interagir avec les API C. C n'a pas unique_ptr
, il n'a pas span
. Il a tendance à utiliser des types primitifs plutôt que des structures, car celles-ci nécessitent un accès basé sur une fonction peu pratique (pas de surcharge d'opérateur).
Ainsi, lorsque C++ interagit avec C, vous obtenez parfois 0 à 3 *
s de plus que le code C++ équivalent.
En C++, si vous souhaitez passer un pointeur en tant que paramètre out ou in/out, vous le transmettez par référence:
int x;
void f(int *&p) { p = &x; }
Cependant, une référence ne peut pas ("légalement") être nullptr
; ainsi, si le pointeur est facultatif, vous avez besoin d'un pointeur vers un pointeur:
void g(int **p) { if (p) *p = &x; }
Bien sûr, depuis C++ 17, vous avez std::optional
, mais le "double pointeur" est un code C/C++ idiomatique depuis de nombreuses décennies et devrait donc être OK. En outre, l'utilisation n'est pas très agréable, vous non plus:
void h(std::optional<int*> &p) { if (p) *p = &x) }
ce qui est un peu moche sur le site d’appel, à moins que vous n’ayez déjà un std::optional
, ou:
void u(std::optional<std::reference_wrapper<int*>> p) { if (p) p->get() = &x; }
ce qui n'est pas si gentil en soi.
En outre, certains pourraient dire que g
est plus agréable à lire sur le site d’appel:
f(p);
g(&p); // `&` indicates that `p` might change, to some folks
L’utilisation est d’avoir un pointeur sur un pointeur, par exemple si vous voulez passer un pointeur sur une méthode par référence.
Un cas où je l'ai utilisé est une fonction manipulant une liste chaînée, en C.
Il y a
struct node { struct node *next; ... };
pour les noeuds de liste, et
struct node *first;
pour pointer sur le premier élément. Toutes les fonctions de manipulation prennent un struct node **
, car je peux garantir que ce pointeur n'est pas -NULL
même si la liste est vide, et je n'ai pas besoin de cas particulier pour l'insertion et la suppression:
void link(struct node *new_node, struct node **list)
{
new_node->next = *list;
*list = new_node;
}
void unlink(struct node **prev_ptr)
{
*prev_ptr = (*prev_ptr)->next;
}
Pour insérer au début de la liste, il suffit de passer un pointeur sur le pointeur first
et il agira correctement, même si la valeur de first
est NULL
.
struct node *new_node = (struct node *)malloc(sizeof *new_node);
link(new_node, &first);
Quelle est l'utilisation réelle d'un double pointeur?
Voici un exemple pratique. Supposons que vous ayez une fonction et que vous souhaitiez lui envoyer un tableau de paramètres de chaîne (vous avez peut-être une DLL à laquelle vous souhaitez transmettre des paramètres). Cela peut ressembler à ceci:
#include <iostream>
void printParams(const char **params, int size)
{
for (int i = 0; i < size; ++i)
{
std::cout << params[i] << std::endl;
}
}
int main()
{
const char *params[] = { "param1", "param2", "param3" };
printParams(params, 3);
return 0;
}
Vous allez envoyer un tableau de pointeurs const char
, chaque pointeur pointant au début d'une chaîne C terminée par un caractère null. Le compilateur décompose votre tableau en pointeur sur l'argument de la fonction. Vous obtenez donc const char **
un pointeur sur le premier pointeur du tableau de pointeurs const char
. Comme la taille du tableau est perdue à ce stade, vous voudrez le passer comme deuxième argument.