La différence entre les pointeurs et les notations de tableau en C/C++

Ilja Kostenko 12 octobre 2023
  1. Tableau C++
  2. Pointeur C++
La différence entre les pointeurs et les notations de tableau en C/C++

Les pointeurs et les tableaux sont sans aucun doute l’un des aspects les plus importants et les plus complexes de C++. Ils prennent en charge les listes chaînées et l’allocation de mémoire dynamique, et ils permettent aux fonctions de modifier le contenu de leurs arguments.

Tableau C++

Un tableau est un ensemble d’éléments du même type accédés par l’index - le nombre ordinal de l’élément dans le tableau. Par example:

int ival;

Il définit ival comme une variable de type int et l’instruction.

int ia[10];

Il définit un tableau de dix objets int. Chacun de ces objets, ou éléments de tableau, est accessible à l’aide de l’opération consistant à prendre un index.

ival = ia[2];

Il affecte à la variable ival la valeur d’un élément du tableau ia d’indice 2. De même

ia[7] = ival;

Il attribue la valeur ival à l’élément d’indice 7.

Une définition de tableau se compose d’un spécificateur de type, d’un nom de tableau et d’une taille. La taille spécifie le nombre d’éléments du tableau (au moins 1) et est placée entre crochets. La taille du tableau doit être connue dès la phase de compilation, et par conséquent, il doit s’agir d’une expression constante, bien qu’elle ne soit pas nécessairement définie par un littéral.

La numérotation des éléments commence à partir de 0, donc pour un tableau de 10 éléments, la plage d’index correcte n’est pas de 1 à 10, mais de 0 à 9. Voici un exemple de tri de tous les éléments du tableau.

int main() {
  const int array_size = 10;
  int ia[array_size];
  for (int ix = 0; ix < array_size; ++ix) ia[ix] = ix;
}

Lors de la définition d’un tableau, vous pouvez l’initialiser explicitement en listant les valeurs de ses éléments entre accolades, séparées par des virgules.

const int array_size = 3;
int ia[array_size] = {0, 1, 2};

Si nous spécifions explicitement une liste de valeurs, nous ne pouvons pas spécifier la taille du tableau : le compilateur lui-même comptera le nombre d’éléments.

Pointeur C++

Un pointeur est un objet contenant l’adresse d’un autre objet et permettant la manipulation indirecte de cet objet. Les pointeurs sont généralement utilisés pour travailler avec des objets créés dynamiquement, créer des structures de données associées, telles que des listes chaînées et des arbres hiérarchiques, et transmettre de grands objets (tableaux et objets de classe) à des fonctions en tant que paramètres.

Chaque pointeur est associé à un certain type de données. Leur représentation interne ne dépend pas du type interne : la taille de la mémoire occupée par un objet de type pointeur et la plage de valeurs sont les mêmes. La différence est la façon dont le compilateur perçoit l’objet adressable. Les pointeurs vers différents types peuvent avoir la même valeur, mais la zone mémoire des types correspondants peut être différente.

Voici quelques exemples:

int *ip1, *ip2;
complex<double> *cp;
string *pstring;
vector<int> *pvec;
double *dp;

Le pointeur est indiqué par un astérisque devant le nom. En définissant des variables par une liste, un astérisque doit être placé devant chaque pointeur (voir ci-dessus : ip1 et ip2). Dans l’exemple ci-dessous, lp est un pointeur vers un objet de type long et lp2 est un objet de type long.

long *lp, lp2;

Dans le cas suivant, fp est interprété comme un objet flottant, et fp2 est un pointeur vers celui-ci :

float fp, *fp2;

Soit une variable de type int soit donnée :

int ival = 1024;

Voici des exemples de définition et d’utilisation de pointeurs vers int pi et pi2.

[//] pi is initialized with the null address
int *pi = 0;
[//] pi2 is initialized with the address ival
int *pi2 = &ival;
[//] correct: pi and pi2 contain the ival address
pi = pi2;
[//] pi2 contains the null address
pi2 = 0;

Un pointeur ne peut pas être affecté d’une valeur qui n’est pas une adresse.

[//] error: pi cannot take the value int
pi = ival

De même, vous ne pouvez pas affecter une valeur à un pointeur d’un type qui est l’adresse d’un objet d’un autre type si les variables suivantes sont définies.

double dval;
double *ps = &dval;

Ensuite, les deux expressions d’affectation données ci-dessous provoqueront une erreur de compilation.

[//] compilation errors
[//] invalid assignment of data types: int* <== double*
pi = pd
pi = &dval;

Ce n’est pas que la variable pi ne peut pas contenir les adresses de l’objet dval - les adresses d’objets de types différents ont la même longueur. De telles opérations de mélange d’adresses sont délibérément interdites car l’interprétation des objets par le compilateur dépend du type de pointeur.

Bien sûr, il y a des cas où l’on s’intéresse à la valeur de l’adresse elle-même, pas à l’objet vers lequel elle pointe (disons que l’on veut comparer cette adresse avec une autre). Pour résoudre de telles situations, nous pouvons introduire un pointeur invalide spécial, qui peut pointer vers n’importe quel type de données, et les expressions suivantes seront correctes :

[//] correct: void* can contain
[//] addresses of any type
void *pv = pi;
pv = pd;

Le type de l’objet pointé par le void* est inconnu, et nous ne pouvons pas manipuler cet objet. Tout ce que nous pouvons faire avec un tel pointeur est d’attribuer sa valeur à un autre pointeur ou de le comparer avec une valeur d’adresse.

Pour accéder à un objet avec son adresse, il faut appliquer une opération de déréférencement, ou adressage indirect, signalé par un astérisque (*). Par example,

int ival = 1024;
, ival2 = 2048;
int *pi = &ival;

Nous pouvons lire et stocker la valeur de ival en appliquant l’opération de déréférencement au pointeur pi.

[//] indirect assignment of the ival variable to the ival2 value
*pi = ival2;
[//] value indirect use of variable value and pH value value
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;

Quand on applique l’opération de prendre une adresse (&) à un objet de type int, on obtient un résultat de type int*

int *pi = &ival;

Si la même opération est appliquée à un objet de type int* (pointeur vers le type int C) et que l’on obtient un pointeur vers un pointeur vers le type int et, c’est-à-dire le type int*. int** est l’adresse d’un objet qui contient l’adresse d’un objet de type int.

En déréférencant ppi, on obtient un objet int* contenant l’adresse ival. Pour obtenir l’objet ival lui-même, l’opération de déréférencement doit être appliquée deux fois au PPI.

int **ppi = &pi;
int *pi2 = *ppi;
cout << "ival value\n"
     << "explicit value: " << ival << "\n"
     << "indirect addressing: " << *pi << "\n"
     << "double indirect addressing: " << **ppi << "\n"
     << end;

Les pointeurs peuvent être utilisés dans les expressions arithmétiques. Faites attention à l’exemple suivant, où deux expressions effectuent des actions totalement différentes.

int i, j, k;
int *pi = &i;
[//] i = i + 2
*pi = *pi + 2;
[//] increasing the address contained in pi by 2
pi = pi + 2;

Vous pouvez ajouter une valeur entière au pointeur et également en soustraire. L’ajout de 1 au pointeur augmente sa valeur de la taille de la zone mémoire allouée à l’objet du type correspondant. Si le type char occupe 1 octet, int – 4 et double - 8, alors ajouter 2 aux pointeurs vers le caractère, entier et double augmentera leur valeur de 2, 8 et 16, respectivement.

Comment cela peut-il être interprété ? Si des objets du même type sont localisés en mémoire l’un après l’autre, augmenter le pointeur de 1 le fera pointer vers l’objet suivant. Par conséquent, les opérations arithmétiques avec des pointeurs sont le plus souvent utilisées lors du traitement de > tableaux ; dans tous les autres cas, elles ne sont guère justifiées.

Voici un exemple typique d’utilisation de l’arithmétique d’adresse lors de l’itération d’éléments de tableau à l’aide d’un itérateur :

int ia[10];
int *iter = &ia[0];
int *iter_end = &ia[10];
while (iter != iter_end) {
  do_the event_ with_(*iter);

Article connexe - C++ Pointer

Article connexe - C++ Array