Préface : Explication des types complexes

Pour comprendre les pointeurs, vous rencontrerez inévitablement des types relativement complexes. Laissez-moi donc d'abord vous présenter comment bien comprendre un type complexe. En fait, comprendre les types complexes est très simple : un type contient de nombreux opérateurs qui, tout comme les expressions ordinaires, ont une priorité. Leur priorité est la même que celle des opérateurs. J'ai donc résumé le principe : en partant du nom de la variable, combiner selon la priorité des opérateurs et analyser étape par étape.

Ci-dessous, commençons par les types simples et analysons-les progressivement.

  • int p;-- Il s'agit d'une variable entière ordinaire.
  • int *p;-- D'abord, en partant de p, on combine d'abord avec `*`.*Cela indique que p est un pointeur ; ensuite, on combine avec int, ce qui indique que le type du contenu pointé par le pointeur est int. Donc p est un pointeur qui renvoie des données entières.
  • int p[3]-- D'abord, en partant de p, on combine d'abord avec `[]`.[]Cela indique que p est un tableau ; ensuite, on combine avec int, ce qui indique que les éléments du tableau sont des entiers. Donc p est un tableau composé de données entières.
  • int *p[3];-- D'abord, en partant de p, on combine d'abord avec [] car sa priorité est plus élevée que*`*`, donc p est un tableau ; ensuite, on combine avec *, ce qui indique que les éléments du tableau sont des pointeurs ; puis on combine avec int, ce qui indique que le type du contenu pointé par les pointeurs est entier. Donc p est un tableau composé de pointeurs qui renvoient des données entières.
  • int (*p)[3];-- D'abord, en partant de p, on combine d'abord avec *, ce qui indique que p est un pointeur ; ensuite, on combine avec [] (l'étape "()" peut être ignorée ; elle sert seulement à changer la priorité), ce qui indique que le contenu pointé par le pointeur est un tableau ; puis on combine avec int, ce qui indique que les éléments du tableau sont des entiers. Donc p est un pointeur vers un tableau composé de données entières.
  • int **p;-- D'abord, en partant de p, on combine d'abord avec *, ce qui indique que p est un pointeur ; puis on combine de nouveau avec *, ce qui indique que l'élément pointé par le pointeur est un pointeur ; ensuite on combine avec int, ce qui indique que l'élément pointé par ce pointeur est une donnée entière. Comme les pointeurs de deuxième niveau et de niveau supérieur sont rarement utilisés dans les types complexes, nous ne considérerons pas les pointeurs multi-niveaux dans les types plus complexes par la suite ; au plus, nous ne considérerons que les pointeurs de premier niveau.
  • int p(int);-- En partant de p, on combine d'abord avec (), ce qui indique que p est une fonction ; puis on analyse l'intérieur de (), ce qui indique que la fonction a un paramètre de type entier ; puis on combine avec le int extérieur, ce qui indique que la valeur de retour de la fonction est une donnée de type entier.
  • int (*p)(int);-- En partant de p, on combine d'abord avec l'opérateur pointeur `*`, ce qui indique que p est un pointeur ; puis on combine avec (), ce qui indique que le pointeur pointe vers une fonction ; puis on combine avec le int à l'intérieur de (), ce qui indique que la fonction a un paramètre int ; puis on combine avec le int le plus externe, ce qui indique que le type de retour de la fonction est entier. Donc p est un pointeur vers une fonction qui a un paramètre entier et dont le type de retour est entier.
  • int *(*p(int))[3];-- Vous pouvez ignorer ce type pour l'instant ; il est trop complexe. En partant de p, on combine d'abord avec (), ce qui indique que p est une fonction ; puis on va à l'intérieur de (), on combine avec int, ce qui indique que la fonction a un paramètre de type entier ; puis on combine avec le * extérieur, ce qui indique que la fonction renvoie un pointeur ; puis on va à la couche la plus externe, on combine d'abord avec [], ce qui indique que le pointeur renvoyé pointe vers un tableau ; puis on combine avec *, ce qui indique que les éléments du tableau sont des pointeurs ; puis on combine avec int, ce qui indique que le contenu pointé par les pointeurs est une donnée de type entier. Donc p est une fonction dont le paramètre est un entier et qui renvoie une variable pointeur pointant vers un tableau composé de variables pointeurs d'entiers.

Voilà à peu près tout ce qu'il y a à dire ; notre tâche ne va pas plus loin. Une fois que vous comprenez ces types, les autres types sont un jeu d'enfant pour nous. Cependant, nous n'utilisons généralement pas de types trop complexes, car cela réduirait grandement la lisibilité du programme. Veuillez les utiliser avec prudence. Les quelques types ci-dessus nous suffisent déjà.


1. Explication détaillée des pointeurs

Un pointeur est une variable particulière dont la valeur stockée est interprétée comme une adresse en mémoire. Pour comprendre un pointeur, vous devez comprendre quatre aspects : le type du pointeur, le type pointé par le pointeur, la valeur du pointeur (ou la zone mémoire pointée par le pointeur), et la zone mémoire occupée par le pointeur lui-même. Expliquons chacun séparément.

Tout d'abord, déclarons quelques pointeurs à titre d'exemples :

Exemple :

int *ptr; char *ptr; int **ptr; int (*ptr)[3]; int *(*ptr)[4];

1. Le type d'un pointeur

D'un point de vue syntaxique, il vous suffit de retirer le nom du pointeur de l'instruction de déclaration du pointeur ; la partie restante est le type du pointeur. C'est le type que le pointeur lui-même possède. Examinons les types des différents pointeurs dans l'exemple 1 :

  • 1, int *ptr;: Le type du pointeur estint*
  • 2, char *ptr;: Le type du pointeur estchar*
  • 3, int **ptr;: Le type du pointeur estint**
  • 4, int (*ptr)[3];: Le type du pointeur estint(*)[3]
  • 5, int *(*ptr)[4]; : Le type du pointeur estint*(*)[4]

Alors, qu'en dites-vous ? La méthode pour trouver le type d'un pointeur est très simple, n'est-ce pas ?

2. Le type pointé par le pointeur

Lorsque vous utilisez un pointeur pour accéder à la zone mémoire pointée par le pointeur, le type pointé par le pointeur détermine ce que le compilateur considérera comme le contenu de cette zone mémoire.

Syntaxiquement, il suffit de retirer le nom du pointeur et l'opérateur de déclaration de pointeur `*` situé à sa gauche de l'instruction de déclaration du pointeur ; ce qui reste est le type pointé par le pointeur. Par exemple :

  • 1, int*ptr;: Le type pointé par le pointeur estint
  • 2, char*ptr;: Le type pointé par le pointeur estchar
  • 3, int**ptr;: Le type pointé par le pointeur estint*
  • 4, int(*ptr)[3];: Le type pointé par le pointeur estint()[3]
  • 5, int*(*ptr)[4]; : Le type pointé par le pointeur estint*()[4]

Dans l'arithmétique des pointeurs, le type pointé par le pointeur joue un rôle important.

Le type du pointeur (c'est-à-dire le type du pointeur lui-même) et le type pointé par le pointeur sont deux concepts distincts. À mesure que vous vous familiariserez avec le C, vous découvrirez que diviser le concept de « type » associé aux pointeurs en ces deux concepts — « type du pointeur » et « type pointé par le pointeur » — est l'un des points clés pour maîtriser les pointeurs. J'ai lu de nombreux livres et j'ai constaté que, dans certains livres mal écrits, ces deux concepts sont mélangés, si bien que ces livres semblent contradictoires et que plus vous les lisez, plus vous êtes perdu.

3. La valeur d'un pointeur — ou la zone mémoire ou l'adresse pointée par le pointeur

La valeur d'un pointeur est la valeur numérique stockée dans le pointeur lui-même. Cette valeur sera traitée par le compilateur comme une adresse, et non comme une valeur numérique ordinaire. Dans les programmes 32 bits, la valeur d'un pointeur de n'importe quel type est un entier de 32 bits, car dans les programmes 32 bits, les adresses mémoire font toutes 32 bits de long. La zone mémoire pointée par un pointeur commence à l'adresse mémoire représentée par la valeur du pointeur et a une longueur de sizeof(type pointé par le pointeur). Désormais, lorsque nous disons que la valeur d'un pointeur est XX, cela équivaut à dire que le pointeur pointe vers une zone mémoire ayant XX comme adresse de début ; lorsque nous disons qu'un pointeur pointe vers une certaine zone mémoire, cela équivaut à dire que la valeur du pointeur est l'adresse de début de cette zone mémoire. La zone mémoire pointée par un pointeur et le type pointé par le pointeur sont deux concepts entièrement différents. Dans l'exemple 1, le type pointé par le pointeur existe déjà, mais comme le pointeur n'a pas été initialisé, la zone mémoire vers laquelle il pointe n'existe pas, ou n'a pas de sens.

Désormais, chaque fois que vous rencontrez un pointeur, vous devez vous demander : Quel est le type de ce pointeur ? Quel est le type vers lequel il pointe ? Où pointe le pointeur ? (Faites particulièrement attention)

4. La zone mémoire occupée par le pointeur lui-même

Combien de mémoire le pointeur lui-même occupe-t-il ? Il vous suffit d'utiliser la fonction sizeof(type de pointeur) pour le mesurer. Sur une plateforme 32 bits, un pointeur lui-même occupe 4 octets. Le concept de mémoire occupée par le pointeur lui-même est utile pour déterminer si une expression de pointeur (expliquée plus tard) est une lvalue.


2. Opérations arithmétiques sur les pointeurs

On peut ajouter ou soustraire un entier à un pointeur. La signification de cette opération sur les pointeurs diffère de l'addition et de la soustraction numériques ordinaires ; elle se fait en unités d'éléments. Par exemple :

char a[20]; int *ptr=(int *)a; // Le transtypage ne change pas le type d'un ptr++;

Dans l'exemple ci-dessus, le type du pointeur ptr est int*, et le type vers lequel il pointe est int. Il est initialisé pour pointer vers la variable entière a. Dans l'instruction suivante, le pointeur ptr est incrémenté de 1. Le compilateur le gère comme suit : il ajoute sizeof(int) à la valeur du pointeur ptr. Dans un programme 32-bit, cela ajoute 4 car int occupe 4 bytes dans un programme 32-bit. Comme les adresses sont mesurées en bytes, l'adresse pointée par ptr a augmenté de 4 bytes par rapport à l'adresse de la variable d'origine a vers des adresses plus élevées. Comme la longueur d'un type char est d'un byte, à l'origine ptr pointait vers les quatre bytes commençant à l'élément 0 du tableau a ; à ce stade, il pointe vers les quatre bytes commençant à l'élément 4 du tableau a. Nous pouvons utiliser un pointeur et une boucle pour parcourir un tableau. Regardez l'exemple :

int array[20]={0}; int *ptr=array; for(i=0;i<20;i++) { (*ptr)++; ptr++; }

Cet exemple incrémente de 1 la valeur de chaque élément du tableau d'entiers. Comme chaque boucle incrémente le pointeur ptr d'un élément, chaque boucle peut accéder à l'élément suivant du tableau.

Regardez un autre exemple :

char a[20]="You_are_a_girl"; int *ptr=(int *)a; ptr+=5;

Dans cet exemple, ptr est incrémenté de 5. Le compilateur le gère comme ceci : ajouter 5 fois sizeof(int) à la valeur du pointeur ptr ; dans un programme 32-bit, cela revient à ajouter 5 fois 4 = 20. Comme l'unité d'une adresse est un byte, l'adresse maintenant pointée par ptr s'est déplacée de 20 bytes dans la direction des adresses plus élevées par rapport à l'adresse pointée avant l'ajout de 5.

Dans cet exemple, avant d'ajouter 5, ptr pointe vers les quatre octets à partir de l'élément 0 du tableau a. Après avoir ajouté 5, ptr pointe au-delà de la plage valide du tableau a. Bien que cela puisse poser problème en pratique, c'est syntaxiquement autorisé. Cela reflète aussi la flexibilité des pointeurs. Si, dans l'exemple ci-dessus, ptr était diminué de 5, le processus serait à peu près le même, sauf que la valeur de ptr serait diminuée de 5 fois sizeof(int), et l'adresse pointée par le nouveau ptr se déplacerait de 20 octets dans la direction des adresses inférieures par rapport à l'adresse initialement pointée par ptr.

Permettez-moi maintenant de donner un autre exemple : (une idée fausse)

#include<stdio.h> int main() { char a[20]=" You_are_a_girl"; char *p=a; char **ptr=&p; //printf("p=%d\n",p); //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); ptr++; //printf("ptr=%d\n",ptr); //printf("*ptr=%d\n",*ptr); printf("**ptr=%c\n",**ptr); }

Idée fausse 1. La sortie est Y et o.

Malentendu : ptr est un pointeur secondaire vers char. Lorsque ptr++ est exécuté, le pointeur est incrémenté de sizeof(char), donc la sortie est comme ci-dessus. Cela peut être le résultat pour seulement quelques personnes.

Idée fausse 2.La sortie est Y et a. Malentendu : ptr pointe vers un type char *. Lorsque ptr++ est exécuté, le pointeur est incrémenté de sizeof(char *) (certains peuvent penser que cette valeur est 1, ce qui conduirait à la réponse de l'idée fausse 1 ; cette valeur devrait être 4, reportez-vous au contenu précédent), c'est-à-dire &p+4. Alors, une opération de déréférencement ne pointerait-elle pas vers le cinquième élément du tableau ? La sortie ne serait-elle pas le cinquième élément du tableau ? La réponse est non.

Solution correcte :Le type de ptr est char **, et le type vers lequel il pointe est char *. L'adresse vers laquelle il pointe est l'adresse de p (&p). Lorsque ptr++ est exécuté, le pointeur est incrémenté de sizeof(char*), c'est-à-dire &p+4. Alors, vers où pointe *(&p+4) ? Vous pouvez demander à Dieu ; peut-être vous dira-t-il où. Donc la sortie finale sera une valeur aléatoire, ou peut-être une opération illégale.

. Mais si array est considéré comme un pointeur, il pointe vers le 0e élément du tableau. Son type est int*, et le type vers lequel il pointe est le type de l'élément du tableau, à savoir int. Par conséquent, il n'est pas du tout surprenant que *array soit égal à 0. De même, array+3 est un pointeur vers le 3e élément du tableau, donc

Après avoir ajouté (ou soustrait) un entier n à un pointeur ptrold, le résultat est un nouveau pointeur ptrnew. Le type de ptrnew est le même que le type de ptrold, et le type pointé par ptrnew est également le même que le type pointé par ptrold. La valeur de ptrnew sera augmentée (ou diminuée) de n fois sizeof(type pointé par ptrold) octets par rapport à la valeur de ptrold. C'est-à-dire que la zone mémoire pointée par ptrnew se sera déplacée de n fois sizeof(type pointé par ptrold) octets dans la direction d'adresse supérieure (ou inférieure) par rapport à la zone mémoire pointée par ptrold. Addition et soustraction pointeur-pointeur : deux pointeurs ne peuvent pas être additionnés ; c'est une opération illégale car après addition, le résultat pointe vers un endroit de direction inconnue et n'a pas de sens. Deux pointeurs peuvent être soustraits, mais ils doivent être du même type. Ceci est généralement utilisé avec des tableaux ; je n'entrerai pas dans plus de détails.


3. Les opérateurs & et *

Ici&est l'opérateur d'adresse,*est l'opérateur d'indirection.

&aLe résultat de l'opération est un pointeur. Le type du pointeur est le type de a plus un *, le type pointé par le pointeur est le type de a, et l'adresse pointée par le pointeur est, eh bien, l'adresse de a.

*pLe résultat de l'opération est toutes sortes de choses. En bref, le résultat de *p est la chose pointée par p. Cette chose a ces caractéristiques : son type est le type pointé par p, et l'adresse qu'elle occupe est l'adresse pointée par p.

Exemple :

int a=12; int b; int *p; int **ptr; p=&a; // Le résultat de &a est un pointeur, son type est int*, et le type vers lequel il pointe est // int. L'adresse vers laquelle il pointe est l'adresse de a. *p=24; // Le résultat de *p, ici son type est int, et l'adresse qu'il occupe est // l'adresse pointée par p. Évidemment, *p est la variable a. ptr=&p; // Le résultat de &p est un pointeur. Le type de ce pointeur est le type de p plus un *, // ici, il s'agit de int **. Le type pointé par ce pointeur est le type de p, et ce // ici, il s'agit de int*. L'adresse pointée par ce pointeur est l'adresse du pointeur p lui-même. *ptr=&b; // *ptr est un pointeur, et le résultat de &b est aussi un pointeur. De plus, ces deux pointeurs // ont le même type et le même type pointé, donc utiliser &b pour affecter à *ptr // est tout à fait correct. **ptr=34; // Le résultat de *ptr est ce qui est pointé par ptr ; ici, c'est un pointeur, // appliquer une autre opération * à ce pointeur donne une variable int.

4. Expressions de pointeurs

Si le résultat d'une expression est un pointeur, alors cette expression est appelée une expression de pointeur.

Voici quelques exemples d'expressions de pointeurs :

Exemple :

int a,b; int array[10]; int *pa; pa=&a; // &a est une expression de pointeur. Int **ptr=&pa; // &pa est aussi une expression de pointeur. *ptr=&b; // *ptr et &b sont toutes deux des expressions de pointeur. pa=array; pa++; // C'est aussi une expression de pointeur.
char *arr[20]; char **parr=arr; // Si arr est considéré comme un pointeur, arr est aussi une expression de pointeur. char *str; str=*parr; // *parr est une expression de pointeur. str=*(parr+1); // *(parr+1) est une expression de pointeur. str=*(parr+2); // *(parr+2) est une expression de pointeur.

Comme le résultat d'une expression de pointeur est un pointeur, une expression de pointeur possède aussi les quatre éléments d'un pointeur : le type du pointeur, le type pointé par le pointeur, la zone mémoire pointée par le pointeur et la mémoire occupée par le pointeur lui-même.

Bon, lorsque le pointeur résultant d'une expression de pointeur possède déjà explicitement la mémoire occupée par le pointeur lui-même, alors cette expression de pointeur est une lvalue ; sinon, ce n'est pas une lvalue. Dans l'exemple 7, &a n'est pas une lvalue parce qu'il n'a pas encore occupé de mémoire explicite. *ptr est une lvalue, parce que le pointeur *ptr a déjà occupé de la mémoire. En fait, *ptr est le pointeur pa. Comme pa a déjà sa propre position en mémoire, bien sûr *ptr a aussi sa propre position.


5. La relation entre les tableaux et les pointeurs

Le nom de tableau d'un tableau peut en fait être considéré comme un pointeur. Regardez l'exemple suivant :

int array[10]={0,1,2,3,4,5,6,7,8,9},value; value=array[0]; // Cela peut aussi s'écrire : value=*array; value=array[3]; // Cela peut aussi s'écrire : value=*(array+3); value=array[4]; // Cela peut aussi s'écrire : value=*(array+4);

Dans l'exemple ci-dessus, en général, le nom de tableau array représente le tableau lui-même, de type int*(array+3)vaut 3. Le reste peut être déduit par analogie.

Exemple :

char *str[3]={ "Hello,thisisasample!", "Hi,goodmorning.", "Helloworld" }; char s[80]; strcpy(s,str[0]); // Cela peut aussi s'écrire strcpy(s,*str); strcpy(s,str[1]); // Cela peut aussi s'écrire strcpy(s,*(str+1)); strcpy(s,str[2]); // Cela peut aussi s'écrire strcpy(s,*(str+2));

Dans l'exemple ci-dessus, str est un tableau de trois éléments, et chaque élément du tableau est un pointeur, chaque pointeur pointant vers une chaîne. Si le nom du tableau de pointeurs str est traité comme un pointeur, il pointe vers l'élément d'indice 0 du tableau. Son type est char **, et le type vers lequel il pointe est char *.

*str est aussi un pointeur. Son type est char *, et le type vers lequel il pointe est char. L'adresse vers laquelle il pointe est l'adresse du premier caractère de la chaîne "Hello,thisisasample!", c'est-à-dire l'adresse de 'H'. Remarque : une chaîne est équivalente à un tableau ; elle est stockée en mémoire sous la forme d'un tableau. Cependant, une chaîne est un tableau constant, son contenu ne peut pas être modifié, et elle ne peut être qu'une rvalue. Si on la considère comme un pointeur, elle est à la fois un pointeur vers une constante et un pointeur constant.

str+1 est aussi un pointeur. Il pointe vers le 1er élément du tableau. Son type est char**, et le type vers lequel il pointe est char*.

*(str+1) est aussi un pointeur. Son type est char*, et le type vers lequel il pointe est char. Il pointe vers le premier caractère 'H' de "Hi,goodmorning.".

Maintenant, résumons la question du nom de tableau d’un tableau (où ce qui est stocké dans le tableau est aussi un tableau) :

Lorsqu’un tableau est déclaréTYPE array[n], le nom de tableau array a alors deux significations :

  • Premièrement, il représente le tableau entier ; son type estTYPE[n];

  • Deuxièmement, il est un pointeur constant ; le type de ce pointeur estTYPE*, et le type pointé par ce pointeur estTYPE, c’est-à-dire le type de l’élément du tableau. La zone mémoire pointée par ce pointeur est le 0e élément du tableau. Le pointeur lui-même occupe une zone mémoire distincte ; notez qu’elle est différente de la zone mémoire occupée par le 0e élément du tableau. La valeur de ce pointeur ne peut pas être modifiée, c’est-à-dire que des expressions comme array++ sont incorrectes. Dans différentes expressions, le nom de tableau array peut jouer différents rôles. Dans l’expression sizeof(array), le nom de tableau array représente le tableau lui-même, donc à ce moment la fonction sizeof mesure la taille du tableau entier.

Dans l’expression *array, array joue le rôle d’un pointeur, donc le résultat de cette expression est la valeur du 0e élément du tableau.sizeof(*array)Ce qui est mesuré est la taille d’un élément de tableau.

Dans l’expression array+n (où n=0, 1, 2, ...), array agit comme un pointeur, donc le résultat de array+n est un pointeur, dont le type estTYPE *, le type qu’il pointe est TYPE, et il pointe vers le n-ième élément du tableau. Par conséquent, sizeof(array+n) mesure la taille du type pointeur. Dans un programme 32 bits, le résultat est 4.

Exemple :

int array[10]; int (*ptr)[10]; ptr=&array;:

Dans l’exemple précédent, ptr est un pointeur, dont le type estint(*)[10], le type qu’il pointe est int

Cette section a mentionné la fonction sizeof(). Maintenant, posons la question : est-ce que sizeof(nom du pointeur) mesure la taille du type propre du pointeur ou la taille du type qu’il pointe ?

La réponse est la première. Par exemple :

int(*ptr)[10];

Alors, dans un programme 32 bits, nous avons :

sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

En fait, sizeof(object) mesure toujours la taille du type de l'objet lui-même, et non la taille d'un autre type.


6. La relation entre les pointeurs et les types de structure

Vous pouvez déclarer un pointeur vers un objet d'un type de structure. Exemple :

struct MyStruct { int a; int b; int c; }; struct MyStruct ss={20,30,40}; // Déclare l'objet de structure ss et initialise les membres de ss à 20, 30 et 40. struct MyStruct *ptr=&ss; // Déclare un pointeur vers l'objet de structure ss. Son type est // MyStruct *, et le type vers lequel il pointe est MyStruct. int *pstr=(int*)&ss; // Déclare un pointeur vers l'objet de structure ss. Mais pstr et // le type pointé par ptr sont différents.

Comment accéder aux trois variables membres de ss via le pointeur ptr ?

Réponse :

ptr->a; //指向运算符,或者可以这们(*ptr).a,建议使用前者
ptr->b;
ptr->c;

Et comment accéder aux trois variables membres de ss via le pointeur pstr ?

Réponse :

*pstr; //访问了ss 的成员a.
*(pstr+1); //访问了ss 的成员b.
*(pstr+2) //访问了ss 的成员c.

Bien que j'aie débogué le code ci-dessus sur mon MSVC++6.0, vous devez savoir que l'utilisation de pstr de cette manière pour accéder aux membres d'une structure n'est pas standard. Pour expliquer pourquoi ce n'est pas standard, voyons comment accéder à chaque élément d'un tableau via un pointeur : (remplacez la structure par un tableau)

int array[3]={35,56,37}; int *pa=array; // La façon d'accéder aux trois éléments du tableau array via le pointeur pa est : *pa; // Accès à l'élément 0 *(pa+1); // Accès à l'élément 1 *(pa+2); // Accès à l'élément 2

Sur la forme, cela ressemble exactement à la méthode non standard d'accès aux membres d'une structure via un pointeur.

Tous les compilateurs C/C++, lorsqu'ils disposent les éléments d'un tableau, stockent toujours les différents éléments du tableau dans un stockage contigu, sans espace vide entre les éléments. Mais lors du stockage des membres d'un objet structure, dans certains environnements de compilation, un alignement sur mot, un alignement sur double mot ou un autre alignement peut être nécessaire, et plusieurs « octets de remplissage » peuvent devoir être insérés entre des membres adjacents. Cela peut entraîner des écarts de plusieurs octets entre les membres.

Par conséquent, dans l'exemple 12, même si *pstr accède à la première variable membre a de l'objet structure ss, rien ne garantit que *(pstr+1) accédera nécessairement au membre de structure b. Car il peut y avoir plusieurs octets de remplissage entre le membre a et le membre b, et peut-être que *(pstr+1) accède justement à ces octets de remplissage. Cela prouve aussi la flexibilité des pointeurs. Si votre but est de voir s'il y a des octets de remplissage entre les membres d'une structure, hé, c'est en fait une bonne méthode.

Cependant, la méthode correcte pour qu'un pointeur accède aux membres d'une structure devrait être la méthode utilisant le pointeur ptr comme dans l'exemple 12.


7. La relation entre les pointeurs et les fonctions

Vous pouvez déclarer un pointeur comme un pointeur vers une fonction.

int fun1(char *,int);
int (*pfun1)(char *,int);
pfun1=fun1;
int a=(*pfun1)("abcdefg",7); //通过函数指针调用函数。

Vous pouvez utiliser un pointeur comme paramètre formel d'une fonction. Dans une instruction d'appel de fonction, une expression de pointeur peut être utilisée comme argument effectif.

int fun(char *); inta; char str[]="abcdefghijklmn"; a=fun(str); int fun(char *s) { int num=0; for(int i=0;;) { num+=*s;s++; } return num; }

La fonction fun dans cet exemple calcule la somme des valeurs de code ASCII de chaque caractère d'une chaîne. Comme mentionné précédemment, un nom de tableau est aussi un pointeur. Dans l'appel de fonction, lorsque str est passé comme argument réel au paramètre formel s, la valeur de str est effectivement passée à s. L'adresse pointée par s devient la même que l'adresse pointée par str, mais str et s occupent chacun leur propre espace de stockage. Effectuer un auto-incrément de 1 sur s à l'intérieur du corps de la fonction ne signifie pas que str est aussi auto-incrémenté de 1 en même temps.


8. Conversion de type de pointeur

Lorsque nous initialisons un pointeur ou affectons une valeur à un pointeur, le côté gauche de l'opérateur d'affectation est un pointeur, et le côté droit de l'opérateur d'affectation est une expression de pointeur. Dans la plupart des exemples donnés précédemment, le type du pointeur est le même que le type de l'expression de pointeur, et le type pointé par le pointeur est le même que le type pointé par l'expression de pointeur.

float f=12.3; float *fptr=&f; int *p;

Dans l'exemple ci-dessus, si nous voulons que le pointeur p pointe vers le nombre réel f, que devons-nous faire ?

Est-ce l'instruction suivante ?

p=&f;

Non. Parce que le type du pointeur p est int *, et le type vers lequel il pointe est int. Le résultat de l'expression &f est un pointeur ; le type du pointeur est float *, et le type vers lequel il pointe est float.

Les deux sont incompatibles, donc une affectation directe ne fonctionnera pas. Au moins sur mon MSVC++6.0, une instruction d'affectation de pointeur exige que les types des deux côtés de l'opérateur d'affectation soient identiques, et que les types pointés le soient aussi. Je ne l'ai pas essayé sur d'autres compilateurs ; vous pouvez essayer. Pour atteindre notre objectif, nous devons effectuer une « conversion de type forcée » :

p=(int*)&f;

S'il existe un pointeur p, et que nous devons changer son type et le type vers lequel il pointe enTYEP *TYPE, alors la syntaxe est :(TYPE *)p

Le résultat d'une telle conversion de type forcée est un nouveau pointeur. Le type du nouveau pointeur estTYPE *, et le type vers lequel il pointe estTYPE, et l'adresse vers laquelle il pointe est l'adresse pointée par le pointeur d'origine. Aucune des propriétés du pointeur d'origine p n'est modifiée. (Retenez ceci)

Si une fonction utilise des pointeurs comme paramètres formels, alors lors de la liaison des arguments réels et des paramètres formels dans une instruction d'appel de fonction, les types doivent être cohérents ; sinon, une conversion forcée est nécessaire :

void fun(char*); int a=125,b; fun((char*)&a); void fun(char*s) { char c; c=*(s+3);*(s+3)=*(s+0);*(s+0)=c; c=*(s+2);*(s+2)=*(s+1);*(s+1)=c; }

Notez qu'il s'agit d'un programme 32 bits, donc le type int occupe quatre octets, et le type char occupe un octet. La fonction fun inverse l'ordre des quatre octets d'un entier. L'avez-vous remarqué ? Dans l'instruction d'appel de fonction, le résultat de l'argument réel &a est un pointeur ; son type est int *, et le type vers lequel il pointe est int. Le type du pointeur de paramètre formel est char *, et le type vers lequel il pointe est char. Ainsi, lors de la liaison de l'argument réel et du paramètre formel, nous devons effectuer une conversion de int * vers char *.

Dans le cadre de cet exemple, nous pouvons faire ceci :

Imaginez le processus de conversion du compilateur : le compilateur construit d'abord un pointeur temporaire char *temp, puis exécute temp=(char *)&a, et enfin passe la valeur de temp à s. Donc le résultat final est : le type de s est char *, le type vers lequel il pointe est char, et l'adresse vers laquelle il pointe est l'adresse de début de a.

Nous savons déjà que la valeur d'un pointeur est l'adresse vers laquelle il pointe. Dans un programme 32 bits, la valeur d'un pointeur est en fait un entier 32 bits.

Pouvons-nous directement affecter un entier comme valeur à un pointeur vers un pointeur ? Comme dans les instructions suivantes :

unsigned int a; TYPE *ptr; // TYPE est int, char, ou un type de structure, etc. a=20345686; ptr=20345686; // Notre objectif est de faire pointer le pointeur ptr vers l'adresse 20345686 ptr=a; // Notre objectif est de faire pointer le pointeur ptr vers l'adresse 20345686 // Essayez de le compiler. Vous constaterez que les deux dernières instructions sont fausses. Ne pouvons-nous donc pas atteindre notre objectif ? Non, il existe encore un moyen : unsigned int a; TYPE *ptr; // TYPE est int, char, ou un type de structure, etc. a=N // N doit représenter une adresse valide ; ptr=(TYPE*)a; // Haha, ça fera l'affaire.

À proprement parler, le (TYPE *) ici n'est pas le même que le (TYPE *) dans la conversion de type de pointeur. La signification de (TYPE*) ici est de traiter la valeur de l'entier non signé a comme une adresse. Il a été souligné ci-dessus que la valeur de a doit représenter une adresse valide ; sinon, lorsque vous utilisez ptr, une erreur d'opération illégale se produira. Réfléchissez à si l'inverse peut être fait : extraire l'adresse pointée par un pointeur, c'est-à-dire la valeur du pointeur, sous forme d'entier. Absolument. L'exemple suivant montre comment extraire la valeur d'un pointeur sous forme d'entier, puis affecter cet entier comme adresse à un pointeur :

int a=123,b; int *ptr=&a; char *str; b=(int)ptr; // Extraire la valeur du pointeur ptr sous forme d'entier. str=(char*)b; // Affecter la valeur de cet entier comme adresse au pointeur str.

Maintenant, nous savons déjà que nous pouvons extraire la valeur d'un pointeur sous forme d'entier, et que nous pouvons également affecter une valeur entière comme adresse à un pointeur.


9. Problèmes de sécurité des pointeurs

Regardez l'exemple suivant :

char s='a'; int *ptr; ptr=(int *)&s; *ptr=1298;

Le pointeur ptr est un pointeur de type int *, et le type vers lequel il pointe est int. L'adresse vers laquelle il pointe est l'adresse de début de s. Dans un programme 32 bits, s occupe un octet, et le type int occupe quatre octets. La dernière instruction ne modifie pas seulement l'octet occupé par s, mais modifie aussi les trois octets adjacents à s dans la direction des adresses supérieures. À quoi servent ces trois octets ? Seul le compilateur le sait, et il est peu probable que le programmeur le sache. Peut-être ces trois octets stockent-ils des données très importantes, ou peut-être ces trois octets sont-ils justement un morceau de code du programme. En raison de votre utilisation imprudente du pointeur, les valeurs de ces trois octets ont été modifiées ! Cela provoquera une erreur fatale.

Regardons un autre exemple :

char a; int *ptr=&a; ptr++; *ptr=115;

Cet exemple peut certainement être compilé et exécuté. Mais voyez-vous ? Après que l'instruction 3 a effectué une auto-incrémentation de 1 sur le pointeur ptr, ptr pointe vers une zone de stockage dans la direction des adresses supérieures adjacente à la variable entière a. Que contient cette zone de stockage ? Nous ne le savons pas. Il peut s'agir de données très importantes, voire d'un morceau de code.

Pourtant, l'instruction 4 écrit réellement des données dans cette zone mémoire ! C'est une erreur grave. Par conséquent, lorsqu'il utilise des pointeurs, le programmeur doit avoir très clairement à l'esprit : où mon pointeur pointe-t-il exactement ? Lorsqu'on utilise des pointeurs pour accéder à des tableaux, il faut aussi veiller à ne pas dépasser la borne inférieure ou supérieure du tableau, car cela peut également provoquer des erreurs similaires.

Dans le transtypage forcé de pointeur :ptr1=(TYPE *)ptr2Si sizeof(type of ptr2) est supérieur à sizeof(type of ptr1), alors il est sûr d'utiliser le pointeur ptr1 pour accéder à la zone mémoire pointée par ptr2. Si sizeof(type of ptr2) est inférieur à sizeof(type of ptr1), alors il n'est pas sûr d'utiliser le pointeur ptr1 pour accéder à la zone mémoire pointée par ptr2. Quant à la raison, le lecteur devrait pouvoir la comprendre en y réfléchissant conjointement avec l'exemple 18.