web-dev-qa-db-fra.com

Pointeurs vers pointeurs vs pointeurs normaux

Le but d'un pointeur est de sauvegarder l'adresse d'une variable spécifique. La structure de la mémoire du code suivant devrait alors ressembler à:

int a = 5;
int *b = &a;

...... adresse mémoire ...... valeur
a ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002

D'accord, très bien. Supposez alors que maintenant je veux enregistrer l'adresse du pointeur * b. Ensuite, nous définissons généralement un double pointeur, ** c, comme

int a = 5;
int *b = &a;
int **c = &b;

La structure de la mémoire ressemble alors à:

...... adresse mémoire ...... valeur
a ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002
c ... 0x000020 ................... 0x000010

Donc ** c fait référence à l'adresse de * b.

Maintenant, ma question est, pourquoi ce type de code,

int a = 5;
int *b = &a;
int *c = &b;

générer un avertissement?

Si le but du pointeur est simplement de sauvegarder l'adresse mémoire, je pense qu'il ne devrait pas y avoir de hiérarchie si l'adresse que nous allons enregistrer fait référence à une variable, un pointeur, un double pointeur, etc., donc le type de code ci-dessous devrait être valide.

int a = 5;
int *b = &a;
int *c = &b;
int *d = &c;
int *e = &d;
int *f = &e;
74
user42298

Dans

int a = 5;
int *b = &a;   
int *c = &b;

Vous recevez un avertissement car &b est de type int **, et vous essayez d'initialiser une variable de type int *. Il n'y a aucune conversion implicite entre ces deux types, conduisant à l'avertissement.

Pour prendre l'exemple plus long que vous voulez travailler, si nous essayons de déréférencer f le compilateur nous donnera un int, pas un pointeur que nous pouvons approfondir.

Notez également que sur de nombreux systèmes int et int* ne sont pas de la même taille (par exemple, un pointeur peut avoir une longueur de 64 bits et un int de 32 bits). Si vous déréférencez f et obtenez un int, vous perdez la moitié de la valeur, puis vous ne pouvez même pas la convertir en un pointeur valide.

91

Si le but du pointeur est juste de sauvegarder l'adresse mémoire, je pense qu'il ne devrait pas y avoir de hiérarchie si l'adresse que nous allons enregistrer fait référence à une variable, un pointeur, un double pointeur, ... etc

Au moment de l'exécution, oui, un pointeur ne contient qu'une adresse. Mais au moment de la compilation, il existe également un type associé à chaque variable. Comme les autres l'ont dit, int* et int** sont deux types différents et incompatibles.

Il existe un type, void*, qui fait ce que vous voulez: il ne stocke qu'une adresse, vous pouvez lui attribuer n'importe quelle adresse:

int a = 5;
int *b = &a;
void *c = &b;

Mais quand vous voulez déréférencer un void*, vous devez fournir vous-même les informations de type "manquantes":

int a2 = **((int**)c);
54
alain

Maintenant, ma question est, pourquoi ce type de code,

int a = 5; 
int *b = &a; 
int *c = &b; 

générer un avertissement?

Vous devez revenir aux fondamentaux.

  • les variables ont des types
  • les variables contiennent des valeurs
  • un pointeur est une valeur
  • un pointeur fait référence à une variable
  • si p est une valeur de pointeur alors *p est une variable
  • si v est une variable alors &v est un pointeur

Et maintenant, nous pouvons trouver toutes les erreurs dans votre publication.

Supposons maintenant que je souhaite enregistrer l'adresse du pointeur *b

Non. *b est une variable de type int. Ce n'est pas un pointeur. b est une variable dont valeur est un pointeur. *b est un variable dont la valeur est un entier.

**c fait référence à l'adresse de *b.

NON NON NON. Absolument pas. Vous avez pour comprendre cela correctement si vous voulez comprendre les pointeurs.

*b est une variable; c'est un alias pour la variable a. L'adresse de la variable a est la valeur de la variable b. **c ne fait pas référence à l'adresse de a. C'est plutôt un variable qui est un alias pour la variable a. (Et il en est de même *b.)

L'instruction correcte est: la valeur de la variable c est la adresse de b. Ou, de manière équivalente: la valeur de c est un pointeur qui fait référence à b.

Comment le savons nous? Revenons aux fondamentaux. Vous avez dit que c = &b. Alors, quelle est la valeur de c? Un pointeur. À quoi? b.

Assurez-vous que vous entièrement comprenez les règles fondamentales.

Maintenant que vous comprenez, espérons-le, la relation correcte entre les variables et les pointeurs, vous devriez pouvoir répondre à votre question sur la raison pour laquelle votre code génère une erreur.

23
Eric Lippert

Le système de type de C l'exige, si vous voulez obtenir un avertissement correct et si vous voulez que le code compile du tout. Avec un seul niveau de profondeur de pointeurs, vous ne savez pas si le pointeur pointe vers un pointeur ou vers un entier réel.

Si vous déréférencez un type int** vous savez que le type que vous obtenez est int* et de même si vous déréférencez int* le type est int. Avec votre proposition, le type serait ambigu.

D'après votre exemple, il est impossible de savoir si c pointe vers un int ou int*:

c = Rand() % 2 == 0 ? &a : &b;

Quel type pointe c? Le compilateur ne le sait pas, donc cette ligne suivante est impossible à effectuer:

*c;

En C, toutes les informations de type sont perdues après la compilation, car chaque type est vérifié au moment de la compilation et n'est plus nécessaire. Votre proposition gaspillerait en fait de la mémoire et du temps car chaque pointeur devrait avoir des informations d'exécution supplémentaires sur les types contenus dans les pointeurs.

20
2501

Les pointeurs sont abstractions des adresses mémoire avec une sémantique de type supplémentaire, et dans un langage comme le type C est important.

Tout d'abord, rien ne garantit que int * et int ** ont la même taille ou la même représentation (sur les architectures de bureau modernes, ils le font, mais vous ne pouvez pas vous fier à ce que cela soit universellement vrai).

Deuxièmement, le type est important pour l'arithmétique des pointeurs. Étant donné un pointeur p de type T *, l'expression p + 1 donne l'adresse du prochain objet de type T. Supposons donc les déclarations suivantes:

char  *cp     = 0x1000;
short *sp     = 0x1000;  // assume 16-bit short
int   *ip     = 0x1000;  // assume 32-bit int
long  *lp     = 0x1000;  // assume 64-bit long

L'expression cp + 1 nous donne l'adresse de l'objet char suivant, qui serait 0x1001. L'expression sp + 1 nous donne l'adresse de l'objet short suivant, qui serait 0x1002. ip + 1 nous donne 0x1004, et lp + 1 nous donne 0x1008.

Donc, étant donné

int a = 5;
int *b = &a;
int **c = &b;

b + 1 nous donne l'adresse du prochain int et c + 1 nous donne l'adresse du prochain pointeur à int.

Des pointeurs vers des pointeurs sont requis si vous souhaitez qu'une fonction écrive dans un paramètre de type pointeur. Prenez le code suivant:

void foo( T *p )    
{
  *p = new_value(); // write new value to whatever p points to
}

void bar( void )
{
  T val;
  foo( &val );     // update contents of val
}

Cela est vrai pour tout type T. Si nous remplaçons T par un type de pointeur P *, le code devient

void foo( P **p )    
{
  *p = new_value(); // write new value to whatever p points to
}

void bar( void )
{
  P *val;
  foo( &val );     // update contents of val
}

La sémantique est exactement la même, ce sont juste les types qui sont différents; le paramètre formel p est toujours un niveau d'indirection de plus que la variable val.

17
John Bode

Je pense qu'il ne devrait pas y avoir de hiérarchie si l'adresse que nous allons enregistrer fait référence à une variable, un pointeur, un double pointeur

Sans la "hiérarchie", il serait très facile de générer de l'UB partout sans aucun avertissement - ce serait horrible.

Considère ceci:

char c = 'a';
char* pc = &c;
char** ppc = &pc;
printf("%c\n", **ppc);   // compiles ok and is valid
printf("%c\n", **pc);    // error: invalid type argument of unary ‘*’

Le compilateur me donne une erreur et ainsi il m'aide à savoir que j'ai fait quelque chose de mal et je peux corriger le bogue.

Mais sans "hiérarchie", comme:

char c = 'a';
char* pc = &c;
char* ppc = &pc;
printf("%c\n", **ppc);   // compiles ok and is valid
printf("%c\n", **pc);    // compiles ok but is invalid

Le compilateur ne peut donner aucune erreur car il n'y a pas de "hiérarchie".

Mais quand la ligne:

printf("%c\n", **pc);

s'exécute, c'est UB (comportement indéfini).

Première *pc lit le char comme s'il s'agissait d'un pointeur, c'est-à-dire qu'il lit probablement 4 ou 8 octets même si nous n'avons réservé qu'un octet. C'est UB.

Si le programme ne tombait pas en panne en raison de l'UB ci-dessus, mais renvoyait juste une valeur de poubelle, la deuxième étape serait de déréférencer la valeur de poubelle. Encore une fois UB.

Conclusion

Le système de type nous aide à détecter les bogues en voyant int *, int **, int ***, etc. comme différents types.

11
4386427

Si le but du pointeur est simplement de sauvegarder l'adresse mémoire, je pense qu'il ne devrait pas y avoir de hiérarchie si l'adresse que nous allons enregistrer fait référence à une variable, un pointeur, un double pointeur, etc., donc le type de code ci-dessous doit être valide.

Je pense que voici votre malentendu: le but du pointeur lui-même est de stocker l'adresse mémoire, mais un pointeur a généralement un type afin que nous sachions à quoi nous attendre à l'endroit où il pointe.

Surtout, contrairement à vous, d'autres personnes veulent vraiment avoir ce genre de hiérarchie pour savoir quoi faire avec le contenu de la mémoire pointé par le pointeur.

C'est le but même du système de pointeurs de C d'avoir des informations de type attachées.

Si tu fais

int a = 5;

&a Implique que ce que vous obtenez est un int * De sorte que si vous déréférencez c'est à nouveau un int.

Amener cela aux niveaux suivants,

int *b = &a;
int **c = &b;

&b Est également un pointeur. Mais sans savoir ce qui se cache derrière, resp. ce qu'il indique, il est inutile. Il est important de savoir que le déréférencement d'un pointeur révèle le type du type d'origine, de sorte que *(&b) est un int *, Et **(&b) est l'original int valeur avec laquelle nous travaillons.

Si vous pensez que dans vos circonstances, il ne devrait pas y avoir de hiérarchie de types, vous pouvez toujours travailler avec void *, Bien que l'utilisabilité directe soit assez limitée.

10
glglgl

Si le but du pointeur est simplement de sauvegarder l'adresse mémoire, je pense qu'il ne devrait pas y avoir de hiérarchie si l'adresse que nous allons enregistrer fait référence à une variable, un pointeur, un double pointeur, etc., donc le type de code ci-dessous doit être valide.

Eh bien, c'est vrai pour la machine (après tout, à peu près tout est un nombre). Mais dans de nombreuses langues, les variables sont typées, ce qui signifie que le compilateur peut alors s'assurer que vous les utilisez correctement (les types imposent un contexte correct aux variables)

Il est vrai qu'un pointeur vers pointeur et un pointeur (probablement) utilisent la même quantité de mémoire pour stocker leur valeur (attention ce n'est pas vrai pour int et pointeur vers int, la taille d'une adresse n'est pas liée à la taille d'un maison).

Donc, si vous avez une adresse d'une adresse que vous devez utiliser telle quelle et non pas comme une simple adresse, car si vous accédez au pointeur vers le pointeur comme un simple pointeur, vous pourrez alors manipuler une adresse d'int comme s'il s'agissait d'un int , ce qui n'est pas le cas (remplacez int sans autre chose et vous devriez voir le danger). Vous pouvez être confus parce que tout cela n'est que des chiffres, mais dans la vie de tous les jours, vous ne le faites pas: personnellement, je fais une grande différence pour 1 $ et 1 chien. chien et $ sont des types, vous savez ce que vous pouvez en faire.

Vous pouvez programmer dans Assembly et faire ce que vous voulez, mais vous observerez à quel point c'est dangereux, car vous pouvez faire presque ce que vous voulez, en particulier des choses étranges. Oui, la modification d'une valeur d'adresse est dangereuse, supposons que vous ayez une voiture autonome qui devrait livrer quelque chose à une adresse exprimée en distance: 1200 rue mémoire (adresse) et supposez que les maisons de rue sont séparées de 100 pieds (1221 est une adresse non valide), si vous êtes capable de manipuler des adresses comme vous le souhaitez en tant qu'entiers, vous pourrez essayer de livrer à 1223 et laisser le paquet au milieu du trottoir.

Un autre exemple pourrait être, maison, adresse de la maison, numéro d'entrée dans un carnet d'adresses de cette adresse. Tous ces trois sont des concepts différents, des types différents ...

9

Le langage C est fortement typé. Cela signifie que, pour chaque adresse, il existe un type, qui indique au compilateur comment interpréter la valeur à cette adresse.

Dans votre exemple:

int a = 5;
int *b = &a;

Le type de a est int et le type de b est int * (lu comme "pointeur vers int"). En utilisant votre exemple, la mémoire contiendrait:

..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*

Le type n'est pas réellement stocké en mémoire, c'est juste que le compilateur sait que, lorsque vous lisez a, vous trouverez un int, et lorsque vous lisez b vous trouverez l'adresse d'un endroit où vous pouvez trouver un int.

Dans votre deuxième exemple:

int a = 5;
int *b = &a;
int **c = &b;

Le type de c est int **, lu comme "pointeur vers pointeur vers int". Cela signifie que, pour le compilateur:

  • c est un pointeur;
  • lorsque vous lisez c, vous obtenez l'adresse d'un autre pointeur;
  • lorsque vous lisez cet autre pointeur, vous obtenez l'adresse d'un int.

C'est,

  • c est un pointeur (int **);
  • *c est également un pointeur (int *);
  • **c est un int.

Et la mémoire contiendrait:

..... memory address ...... value ........ type
a ... 0x00000002 .......... 5 ............ int
b ... 0x00000010 .......... 0x00000002 ... int*
c ... 0x00000020 .......... 0x00000010 ... int**

Étant donné que le "type" n'est pas stocké avec la valeur et qu'un pointeur peut pointer vers n'importe quelle adresse mémoire, la façon dont le compilateur connaît le type de la valeur à une adresse consiste essentiellement à prendre le type du pointeur et à supprimer le point le plus à droite *.


Soit dit en passant, c'est pour une architecture 32 bits commune. Pour la plupart des architectures 64 bits, vous aurez:

..... memory address .............. value ................ type
a ... 0x0000000000000002 .......... 5 .................... int
b ... 0x0000000000000010 .......... 0x0000000000000002 ... int*
c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**

Les adresses sont désormais de 8 octets chacune, tandis qu'un int n'est toujours que de 4 octets. Comme le compilateur connaît le type de chaque variable, il peut facilement gérer cette différence et lire 8 octets pour un pointeur et 4 octets pour le int.

9
CesarB

Il en existe différents types. Et il y a une bonne raison à cela:

Ayant …

int a = 5;
int *b = &a;
int **c = &b;

… l'expression …

*b * 5

… Est valable, tandis que l'expression…

*c * 5

ça n'a aucun sens.

Le gros problème n'est pas, comment les pointeurs ou pointeurs vers pointeurs sont stockés, mais à quoi ils se réfèrent.

9
Amin Negm-Awad

Pourquoi ce type de code génère-t-il un avertissement?

int a = 5;
int *b = &a;   
int *c = &b;

L'opérateur & Renvoie un pointeur sur l'objet, c'est-à-dire que &a Est de type int * Donc l'affecter (via l'initialisation) à b qui est aussi de le type int * est valide. &b Renvoie un pointeur vers l'objet b, c'est-à-dire que &b Est de type pointeur vers int *, C'est-à-dire., int **.

C dit dans les contraintes de l'opérateur d'affectation (qui valent pour l'initialisation) que (C11, 6.5.16.1p1): "les deux opérandes sont des pointeurs vers des versions qualifiées ou non qualifiées de types compatibles". Mais dans la définition C de ce qui est un type compatibleint ** Et int * Ne sont pas des types compatible.

Il y a donc une violation de contrainte dans l'initialisation int *c = &b; Ce qui signifie qu'un diagnostic est requis par le compilateur.

L'une des raisons de la règle ici est que la norme ne garantit pas que les deux types de pointeurs différents sont de la même taille (sauf pour void * Et les types de pointeurs de caractères), c'est-à-dire sizeof (int *) et sizeof (int **) peuvent être des valeurs différentes.

6
ouah

Ce serait parce que n'importe quel pointeur T* Est en fait de type pointer to a T (Ou address of a T), Où T est le type pointé. Dans ce cas, * Peut être lu comme pointer to a(n) et T est le type pointé.

int     x; // Holds an integer.
           // Is type "int".
           // Not a pointer; T is nonexistent.
int   *px; // Holds the address of an integer.
           // Is type "pointer to an int".
           // T is: int
int **pxx; // Holds the address of a pointer to an integer.
           // Is type "pointer to a pointer to an int".
           // T is: int*

Ceci est utilisé à des fins de déréférencement, où l'opérateur de déréférencement prendra un T* Et renverra une valeur dont le type est T. Le type de retour peut être vu comme tronquant le "pointeur vers a (n)" le plus à gauche, et étant tout ce qui reste.

  *x; // Invalid: x isn't a pointer.
      // Even if a compiler allows it, this is a bad idea.
 *px; // Valid: px is "pointer to int".
      // Return type is: int
      // Truncates leftmost "pointer to" part, and returns an "int".
*pxx; // Valid: pxx is "pointer to pointer to int".
      // Return type is: int*
      // Truncates leftmost "pointer to" part, and returns a "pointer to int".

Notez comment pour chacune des opérations ci-dessus, le type de retour de l'opérateur de déréférencement correspond au type T de la déclaration T* D'origine.

Cela aide grandement les compilateurs et les programmeurs primitifs à analyser le type d'un pointeur: pour un compilateur, l'opérateur address-of ajoute un * Au type, l'opérateur de déréférence supprime un * Du type, et tout décalage est une erreur. Pour un programmeur, le nombre de * Est une indication directe du nombre de niveaux d'indirection auxquels vous avez affaire (int* Pointe toujours vers int, float** pointe toujours vers float* qui à son tour pointe toujours vers float, etc.).


Maintenant, en tenant compte de cela, il y a deux problèmes majeurs avec l'utilisation d'un seul * Quel que soit le nombre de niveaux d'indirection:

  1. Le pointeur est beaucoup plus difficile à déréférencer pour le compilateur, car il doit se référer à l'affectation la plus récente pour déterminer le niveau d'indirection et déterminer le type de retour de manière appropriée.
  2. Le pointeur est plus difficile à comprendre pour le programmeur, car il est facile de perdre la trace du nombre de couches d'indirection.

Dans les deux cas, la seule façon de déterminer le type réel de la valeur serait de la revenir en arrière, vous forçant à chercher ailleurs pour la trouver.

void f(int* pi);

int main() {
    int x;
    int *px = &x;
    int *ppx = &px;
    int *pppx = &ppx;

    f(pppx);
}

// Ten million lines later...

void f(int* pi) {
    int i = *pi; // Well, we're boned.
    // To see what's wrong, see main().
}

C'est ... un problème très dangereux, et qui est facilement résolu en ayant le nombre de * S directement représenter le niveau d'indirection.

4
Justin Time