Les Pointeurs dans C++

Dans les chapitres précédents, les variables ont été expliquées sous forme d’emplacements dans la mémoire de l’ordinateur auxquels on peut accéder par leur identificateur (leur nom). De cette façon, le programme n’a pas besoin de se soucier de l’adresse physique des données en mémoire; il utilise simplement l’identifiant chaque fois qu’il doit faire référence à la variable.

Pour un programme C ++, la mémoire d’un ordinateur est semblable à une succession de cellules mémoire, chacune d’une taille en octets, chacune avec une adresse unique. Ces cellules mémoire à un octet sont ordonnées de manière à ce que les représentations de données de plus d’un octet occupent des cellules de mémoire ayant des adresses consécutives.

De cette façon, chaque cellule peut être facilement localisée dans la mémoire au moyen de son adresse unique. Par exemple, la cellule de mémoire avec l’adresse 1776 suit toujours immédiatement après la cellule avec l’adresse 1775 et précède celle avec 1777. Elle contient exactement mille cellules après 776 et exactement mille cellules avant 2776.

Lorsqu’une variable est déclarée, la mémoire nécessaire pour stocker sa valeur se voit attribuer un emplacement spécifique en mémoire (son adresse mémoire). Généralement, les programmes C ++ ne décident pas activement les adresses mémoire exactes où sont stockées ses variables. Heureusement, cette tâche est laissée à l’environnement d’exécution du programme – généralement un système d’exploitation qui détermine les emplacements de mémoire particuliers au moment de l’exécution. Cependant, il peut être utile qu’un programme puisse obtenir l’adresse d’une variable pendant l’exécution afin d’accéder aux cellules de données se trouvant à une certaine position par rapport à celle-ci.

L’opérateur adresse (&)

L’adresse d’une variable peut être obtenue en faisant précéder le nom d’une variable d’un signe esperluette (&), appelé opérateur d’adresse. Par exemple:

foo = & myvar;

Ceci assignerait l’adresse de la variable myvar à foo; en faisant précéder le nom de la variable myvar par l’adresse-de l’opérateur (&), nous n’affectons plus le contenu de la variable elle-même à foo, mais son adresse.

L’adresse réelle d’une variable en mémoire ne peut pas être connue avant l’exécution, mais supposons, afin de clarifier certains concepts, que myvar soit placé pendant l’exécution à l’adresse de mémoire 1776.

Dans ce cas, considérez le fragment de code suivant:

myvar = 25;
foo = & myvar;
bar = myvar;

Les valeurs contenues dans chaque variable après son exécution sont illustrées dans le diagramme suivant:

Pointeurs - référence

Premièrement, nous avons attribué la valeur 25 à myvar (une variable dont l’adresse en mémoire supposée est 1776).

La deuxième déclaration attribue à foo l’adresse de myvar, supposée être 1776.

Enfin, la troisième instruction attribue à bar la valeur contenue dans myvar. Il s’agit d’une opération d’affectation standard, comme cela a déjà été fait à maintes reprises dans les chapitres précédents.

La principale différence entre les deuxième et troisième déclarations est l’apparition de l’adresse de l’opérateur (&).

La variable qui stocke l’adresse d’une autre variable (comme foo dans l’exemple précédent) s’appelle en C ++ un pointeur. Les pointeurs sont une fonctionnalité très puissante du langage qui a de nombreuses utilisations dans la programmation de niveau inférieur. Un peu plus tard, nous verrons comment déclarer et utiliser des pointeurs.

Opérateur de déréférence (*)

Comme on vient de le voir, une variable qui stocke l’adresse d’une autre variable s’appelle un pointeur. On dit que les pointeurs « pointent » sur la variable dont ils stockent l’adresse.

Une propriété intéressante des pointeurs est qu’ils peuvent être utilisés pour accéder directement à la variable sur laquelle ils pointent. Pour ce faire, faites précéder le nom du pointeur avec l’opérateur de déréférence (*). L’opérateur lui-même peut être lu comme « valeur indiquée par ».

Par conséquent, en suivant les valeurs de l’exemple précédent, l’instruction suivante:

baz = * foo;

Cela pourrait être interprété comme suit: « baz est égal à la valeur indiquée par foo », et l’énoncé attribuerait en fait la valeur 25 à baz, puisque foo correspond à 1776 et que la valeur indiquée en 1776 (suivant l’exemple ci-dessus) serait 25 .

Il est important de bien distinguer que foo fait référence à la valeur 1776, tandis que * foo (suivi d’un astérisque * précédant l’identifiant) correspond à la valeur stockée à l’adresse 1776, qui est dans le cas 25. Remarquez la différence d’inclure ou non l’opérateur de déréférence (j’ai ajouté un commentaire explicatif sur la façon de lire chacune de ces deux expressions):

baz = foo; // baz égal à foo (1776)
baz = * foo; // baz égal à la valeur indiquée par foo (25)

Les opérateurs de référence et de déréférence sont donc complémentaires:

  • & est l’adresse-de l’opérateur, et peut être lue simplement comme « adresse de »
  • * est l’opérateur de déréférence, et peut être lu comme « valeur indiquée par »

Ainsi, ils ont en quelque sorte une signification opposée: une adresse obtenue avec & peut être déréférencée avec *.

Auparavant, nous avions effectué les deux opérations d’affectation suivantes:

myvar = 25;
foo = & myvar;

Juste après ces deux déclarations, toutes les expressions suivantes donneraient un résultat vrai:

myvar == 25
& myvar == 1776
toto == 1776
* foo == 25

La première expression est assez claire, étant donné que l’opération d’attribution effectuée sur myvar était myvar = 25. Le second utilise l’opérateur d’adresse (&), qui renvoie l’adresse de myvar, que nous avons supposée avoir une valeur de 1776. Le troisième est quelque peu évident, puisque la seconde expression était vraie et que l’opération d’affectation effectuée sur foo était foo = & myvar. La quatrième expression utilise l’opérateur de déréférence (*) qui peut être lu comme « valeur indiquée par », et la valeur indiquée par foo est bien 25.

Donc, après tout cela, vous pouvez aussi en déduire que tant que l’adresse indiquée par foo reste inchangée, l’expression suivante sera également vraie:

* foo == myvar

Déclarer les pointeurs

En raison de la capacité d’un pointeur à se référer directement à la valeur sur laquelle il pointe, un pointeur a des propriétés différentes lorsqu’il pointe vers un caractère que lorsqu’il pointe vers un entier ou un float. Une fois le renvoi effectué, le type doit être connu. Et pour cela, la déclaration d’un pointeur doit inclure le type de données sur lequel le pointeur va pointer.

La déclaration des pointeurs suit cette syntaxe:

type * nom;

où type est le type de données pointé par le pointeur. Ce type n’est pas le type du pointeur lui-même, mais le type des données pointées par le pointeur. Par exemple:

int * numero;
char *;
double * decimales;

Ce sont trois déclarations de pointeurs. Chacun d’entre eux est destiné à pointer vers un type de données différent, mais, en fait, tous sont des pointeurs et tous occuperont probablement le même espace en mémoire (la taille en mémoire d’un pointeur dépend de la plate-forme). où le programme fonctionne). Néanmoins, les données auxquelles ils indiquent n’occupent ni la même quantité d’espace, ni le même type: la première pointe sur un int, la seconde sur un caractère et la dernière sur un double. Par conséquent, bien que ces trois exemples de variables soient tous des pointeurs, ils ont en réalité des types différents: int *, char * et double *, respectivement, en fonction du type vers lequel elles pointent.

Notez que l’astérisque (*) utilisé lors de la déclaration d’un pointeur signifie uniquement qu’il s’agit d’un pointeur (il fait partie de son spécificateur composé de type), et ne doit pas être confondu avec l’opérateur de déréférencement vu un peu plus tôt, mais également écrit avec un astérisque (*). Ce sont simplement deux choses différentes représentées avec le même signe.

Voyons un exemple sur les pointeurs:

// mon premier pointeur
#include <iostream>
using namespace std;

int main ()
{
  int premierevaleur, deuxiemevaleur;
  int * monpointeur;

  monpointeur= & premierevaleur;
  * monpointeur= 10;
  monpointeur= & deuxiemevaleur;
  * monpointeur = 20;
  cout << "la première valeur est" << premierevaleur << '\ n';
  cout << "la seconde valeur est" << deuxiemevaleur << '\ n';
  return 0;
}

Résultat de l’exécution

la première valeur est 10
la seconde valeur est 20

Notez que même si ni la première valeur ni la deuxième valeur ne définissent directement une valeur dans le programme, les deux se voient attribuer une valeur définie indirectement via l’utilisation de monpointeur . Voici comment cela se passe:

Tout d’abord, l’adresse de première valeur est affectée à monpointeur à l’aide de l’opérateur adresse-de (&). Ensuite, la valeur désignée par monpointeur se voit attribuer la valeur 10. Étant donné que monpointeur pointe à l’emplacement de mémoire de premierevaleur, cela modifie en fait la valeur de premierevaleur.

Afin de démontrer qu’un pointeur peut pointer sur différentes variables au cours de sa durée de vie dans un programme, l’exemple répète le processus avec une seconde valeur et le même pointeur, monpointeur .

Voici un exemple un peu plus élaboré:

// plus de pointeurs
#include <iostream>
using namespace std;

int main ()
{
  int premierevaleur = 5, secondevalue = 15;
  int * p1, * p2;

  p1 = & premierevaleur ; // p1 = adresse de première valeur
  p2 = & deuxiemevaleur ; // p2 = adresse de seconde valeur
  * p1 = 10; // valeur pointée par p1 = 10
  * p2 = * p1; // valeur indiquée par p2 = valeur indiquée par p1
  p1 = p2; // p1 = p2 (la valeur du pointeur est copiée)
  * p1 = 20; // valeur pointée par p1 = 20
  
  cout << "la première valeur est" << premierevaleur << '\ n';
  cout << "la seconde valeur est" << deuxiemevaleur << '\ n';
  return 0;
}

Résultat de l’exécution

la première valeur est 10
la seconde valeur est 20

Chaque opération d’affectation inclut un commentaire sur la manière dont chaque ligne peut être lue: c’est-à-dire remplacer les esperluettes (&) par « adresse de » et les astérisques (*) par « valeur désignée par ».

À lire aussi  Projet de système de librairie en C++

Notez qu’il existe des expressions avec les pointeurs p1 et p2, avec et sans l’opérateur de déréférence (*). La signification d’une expression utilisant l’opérateur de déréférence (*) est très différente de celle qui ne le fait pas. Lorsque cet opérateur précède le nom du pointeur, l’expression fait référence à la valeur à pointer, tandis que lorsqu’un nom de pointeur apparaît sans cet opérateur, il fait référence à la valeur du pointeur lui-même (c’est-à-dire l’adresse de l’objet pointé par le pointeur).

Une autre chose qui peut attirer votre attention est la ligne:

int * p1, * p2;

Cela déclare les deux pointeurs utilisés dans l’exemple précédent. Mais notez qu’il y a un astérisque (*) pour chaque pointeur, afin que les deux aient le type int * (pointeur sur int). Cela est nécessaire en raison des règles de priorité. Notez que si, à la place, le code était:

int * p1, p2;

p1 serait en effet de type int *, mais p2 serait de type int. Les espaces ne comptent pas du tout pour cela. Quoi qu’il en soit, il suffit de ne pas oublier de mettre un astérisque par pointeur pour la plupart des utilisateurs de pointeurs intéressés par la déclaration de plusieurs pointeurs par déclaration. Ou encore mieux: utilisez une instruction différente pour chaque variable.

Pointeurs et tableaux

Le concept de tableaux est lié à celui de pointeurs. En fait, les tableaux fonctionnent très bien comme des pointeurs sur leurs premiers éléments et, en réalité, un tableau peut toujours être implicitement converti en un pointeur du type approprié. Par exemple, considérons ces deux déclarations:

int myarray [20];
int * monpointeur;

L’opération d’affectation suivante serait valide:

monpointeur = myarray;

Après cela, monpointeur et myarray seraient équivalents et auraient des propriétés très similaires. La principale différence est que monpointeur peut se voir attribuer une adresse différente, alors que myarray ne peut jamais être attribué à rien et qu’il représentera toujours le même bloc de 20 éléments de type int. Par conséquent, l’affectation suivante ne serait pas valide:

myarray = monpointeur;

Voyons un exemple qui mélange des tableaux et des pointeurs:

// plus de pointeurs
#include <iostream>
using namespace std;

int main ()
{
  nombres int [5];
  int * p;
  p = nombres; * p = 10;
  p ++; * p = 20;
  p = & nombres [2]; * p = 30;
  p = nombres + 3; * p = 40;
  p = nombres; * (p + 4) = 50;
  pour (int n = 0; n <5; n ++)
    cout << nombres [n] << ",";
  return 0;
}

Résultat de l’exécution

10, 20, 30, 40, 50,

Les pointeurs et les tableaux prennent en charge le même ensemble d’opérations, avec la même signification pour les deux. La principale différence est que les pointeurs peuvent se voir attribuer de nouvelles adresses, alors que les tableaux ne le peuvent pas.

Dans le chapitre sur les tableaux, les crochets ([]) ont été expliqués comme spécifiant l’index d’un élément du tableau. Eh bien, en réalité, ces supports constituent un opérateur de déréférencement appelé opérateur offset. Ils déréférencent la variable qu’ils suivent comme *, mais ils ajoutent également le nombre entre parenthèses à l’adresse en cours de déréférence. Par exemple:

a [5] = 0; // a [offset de 5] = 0
* (a + 5) = 0; // pointé par (a + 5) = 0

Ces deux expressions sont équivalentes et valides, non seulement si a est un pointeur, mais également si a est un tableau. Rappelez-vous que s’il s’agit d’un tableau, son nom peut être utilisé comme un pointeur sur son premier élément.

Initialisation du pointeur

Les pointeurs peuvent être initialisés pour pointer vers des emplacements spécifiques au moment même où ils sont définis:

int myvar;
int * myptr = & myvar;

L’état résultant des variables après ce code est le même que celui après:

int myvar;
int * myptr;
myptr = & myvar;

Lorsque les pointeurs sont initialisés, ce qui est initialisé est l’adresse vers laquelle ils pointent (c’est-à-dire, myptr), jamais la valeur étant pointée (c’est-à-dire, * myptr). Par conséquent, le code ci-dessus ne doit pas être confondu avec:

int myvar;
int * myptr;
* myptr = & myvar;

Ce qui de toute façon n’aurait pas beaucoup de sens (et ce n’est pas un code valide).

L’astérisque (*) dans la déclaration du pointeur (ligne 2) indique uniquement qu’il s’agit d’un pointeur, ce n’est pas l’opérateur de déréférence (comme dans la ligne 3). Les deux choses utilisent le même signe: *. Comme toujours, les espaces ne sont pas pertinents et ne changent jamais le sens d’une expression.

Les pointeurs peuvent être initialisés soit à l’adresse d’une variable (comme dans le cas ci-dessus), soit à la valeur d’un autre pointeur (ou tableau):

int myvar;
int * foo = & myvar;
int * bar = foo;

Pointeur arithmétique

Effectuer des opérations arithmétiques sur des pointeurs est un peu différent de les effectuer sur des types entiers réguliers. Pour commencer, seules les opérations d’addition et de soustraction sont autorisées. les autres n’ont aucun sens dans le monde des pointeurs. Mais l’addition et la soustraction ont un comportement légèrement différent avec les pointeurs, en fonction de la taille du type de données vers lequel ils pointent.

Lors de l’introduction des types de données fondamentaux, nous avons constaté que les types avaient des tailles différentes. Par exemple: char a toujours une taille de 1 octet, short est généralement plus grand que cela et int et long sont encore plus grands; la taille exacte de ceux-ci étant dépendante du système. Par exemple, imaginons que dans un système donné, char prend 1 octet, short prend 2 octets et long en prend 4.

Supposons maintenant que nous définissions trois pointeurs dans ce compilateur:

char * mychar;
short * myshort;
long * mylong;

et que nous savons qu’ils pointent vers les emplacements de mémoire 1000, 2000 et 3000, respectivement.

Donc, si on écrit:

++ mychar;
++ myshort;
++ mylong;

Comme on pouvait s’y attendre, mychar contiendrait la valeur 1001. Mais pas aussi évident, myshort contiendrait la valeur 2002 et mylong en contiendrait 3004, même s’ils n’ont été incrémentés qu’une seule fois. La raison en est que, lors de l’ajout d’un pointeur à un pointeur, celui-ci pointe sur l’élément suivant du même type et que, par conséquent, la taille en octets du type auquel il pointe est ajoutée au pointeur.

pointeurs arithmétique

Ceci est applicable à la fois lors de l’ajout et de la soustraction d’un nombre quelconque à un pointeur. Cela se produirait exactement de la même manière si nous écrivions:

mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;

En ce qui concerne les opérateurs increment (++) et decrement (-), ils peuvent tous deux être utilisés comme préfixe ou suffixe d’une expression, avec une légère différence de comportement: en tant que préfixe, l’incrément intervient avant l’évaluation de l’expression, et en tant que suffixe, l’incrément se produit après l’évaluation de l’expression. Cela s’applique également aux expressions qui incrémentent et décrémentent les pointeurs, qui peuvent faire partie d’expressions plus complexes qui incluent également des opérateurs de déréférencement (*). En rappelant les règles de priorité des opérateurs, nous pouvons rappeler que les opérateurs postfixés, tels que incrémenter et décrémenter, ont une priorité plus élevée que les opérateurs préfixés, tels que l’opérateur de déréférence (*). Par conséquent, l’expression suivante:

* p ++

est équivalent à * (p ++). Et ce que cela fait, c’est d’augmenter la valeur de p (pour qu’il pointe maintenant sur l’élément suivant), mais parce que ++ est utilisé comme postfixe, l’expression entière est évaluée comme la valeur indiquée à l’origine par le pointeur (l’adresse vers laquelle il pointe). avant d’être incrémenté).

Il s’agit essentiellement des quatre combinaisons possibles de l’opérateur de déréférence avec les versions à la fois de préfixe et de suffixe de l’opérateur d’incrémentation (la même chose étant applicable à l’opérateur de décrémentation):

* p ++ // identique à * (p ++): pointeur d'incrément et adresse de non référence, déréférencée
* ++ p // identique à * (++ p): pointeur d'incrémentation et adresse incrémentée de déréférence
++ * p // identique à ++ (* p): pointeur de déréférence et incrémentation de la valeur indiquée
(* p) ++ // déréférencement du pointeur, et post-incrémente la valeur vers laquelle il pointe

Une déclaration typique – mais pas si simple – impliquant ces opérateurs est la suivante:

* p ++ = * q ++;

Étant donné que ++ a une priorité supérieure à *, p et q sont tous les deux incrémentés, mais parce que les deux opérateurs d’incrémentation (++) sont utilisés comme suffixe postfixé et non comme préfixe, la valeur affectée à * p est * q avant que les deux valeurs ne soient incrémentées. . Et puis les deux sont incrémentés. Ce serait à peu près équivalent à:

* p = * q;
++ p;
++ q;

Comme toujours, les parenthèses réduisent la confusion en ajoutant de la lisibilité aux expressions.

Pointeurs et const

Les pointeurs peuvent être utilisés pour accéder à une variable en fonction de son adresse. Cet accès peut inclure la modification de la valeur pointée. Mais il est également possible de déclarer des pointeurs pouvant accéder à la valeur pointée pour la lire, mais pas pour la modifier. Pour cela, il suffit de qualifier le type pointé par le pointeur comme const. Par exemple:

int x;
int y = 10;
const int * p = & y;
x = * p; // ok: lecture de p
* p = x; // erreur: modification de p, qui est qualifié de const

Ici, p pointe sur une variable, mais pointe vers elle de manière qualifiée de const, ce qui signifie qu’elle peut lire la valeur pointée, mais qu’elle ne peut pas la modifier. Notez également que l’expression & y est de type int *, mais qu’elle est affectée à un pointeur de type const int *. Ceci est autorisé: un pointeur sur non-const peut être implicitement converti en un pointeur sur const. Mais pas l’inverse! En tant que caractéristique de sécurité, les pointeurs sur const ne sont pas convertibles implicitement en pointeurs sur non-const.

L’un des cas d’utilisation de pointeurs sur des éléments const est celui de paramètres de fonction: une fonction qui prend un pointeur sur non-const en tant que paramètre peut modifier la valeur transmise en tant qu’argument, alors qu’une fonction qui prend un pointeur sur const en tant que paramètre ne peut pas.

// pointeurs comme arguments:
#include <iostream>
using namespace std;

void increment_all (int * start, int * stop)
{
  int * current = start;
  while (current! = stop) {
    ++ (* current); // valeur d'incrémentation pointée
    ++ current; // point d'incrément
  }
}

void print_all (const int * start, const int * stop)
{
  const int * current = start;
  while (current! = stop) {
    cout << * current << '\ n';
    ++ current; // point d'incrément
  }
}

int main ()
{
  nombres int [] = {10,20,30};
  increment_all (nombres, nombres + 3);
  print_all (nombres, nombres + 3);
  return 0;
}

Notez que print_all utilise des pointeurs qui pointent vers des éléments constants. Ces pointeurs pointent vers un contenu constant qu’ils ne peuvent pas modifier, mais ils ne le sont pas eux-mêmes: c’est-à-dire que les pointeurs peuvent toujours être incrémentés ou se voir attribuer des adresses différentes, bien qu’ils ne puissent pas modifier le contenu qu’ils pointent.

À lire aussi  Structure d'un programme dans C++

Et c’est là qu’une deuxième dimension à la constance est ajoutée aux pointeurs: les pointeurs peuvent également être eux-mêmes const. Et ceci est spécifié en ajoutant const au type indiqué (après l’astérisque):

int x;
      int * p1 = & x; // pointeur non-const sur int non-const
const int * p2 = & x; // pointeur non-const sur const int
      int * const p3 = & x; // pointeur sur int non-const
const int * const p4 = & x; // pointeur sur const int

La syntaxe avec const et pointeurs est certainement délicate, et reconnaître les cas qui conviennent le mieux à chaque utilisation a tendance à nécessiter une certaine expérience. Dans tous les cas, il est important de maîtriser les pointeurs (et les références) le plus tôt possible, mais ne vous inquiétez pas trop de tout saisir si c’est la première fois que vous êtes exposé au mélange de const et de pointeurs. D’autres cas d’utilisation apparaîtront dans les chapitres à venir.

Pour ajouter un peu plus de confusion à la syntaxe de const avec des pointeurs, le qualificatif const peut précéder ou suivre le type pointé, avec exactement la même signification:

const int * p2a = & x; // pointeur non-const sur const int
int const * p2b = & x; // aussi un pointeur non-const sur const int

Comme pour les espaces entourant l’astérisque, l’ordre de const est simplement une question de style. Ce chapitre utilise un préfixe const, car pour des raisons historiques, il semble être plus étendu, mais les deux sont exactement équivalents. Les mérites de chaque style sont encore intensément débattus sur Internet.

Pointeurs et chaînes de caractères

Comme indiqué précédemment, les chaîne de caractères sont des tableaux contenant des séquences de caractères terminées par un caractère nul. Dans les sections précédentes, les littéraux de chaîne étaient utilisés pour être directement insérés dans cout, pour initialiser des chaînes et pour initialiser des tableaux de caractères.

Mais ils peuvent également être consultés directement. Les littéraux de chaîne sont des tableaux du type de tableau approprié devant contenir tous les caractères plus le caractère nul final, chacun des éléments étant du type const char (en tant que littéraux, ils ne peuvent jamais être modifiés). Par exemple:

 
const char * foo = "hello";

Cela déclare un tableau avec la représentation littérale de « hello », puis un pointeur sur son premier élément est affecté à foo. Si nous imaginons que « hello » est stocké dans les emplacements de mémoire commençant à l’adresse 1702, nous pouvons représenter la déclaration précédente de la manière suivante:

chaines de caractères

Notez que ici, foo est un pointeur et contient la valeur 1702, et non pas « h », ni « hello », bien que 1702 soit effectivement l’adresse de ces deux éléments.

Le pointeur foo pointe vers une séquence de caractères. Et comme les pointeurs et les tableaux se comportent essentiellement de la même manière dans les expressions, foo peut être utilisé pour accéder aux caractères de la même manière que les tableaux de séquences de caractères à terminaison null. Par exemple:

* (toto + 4)
foo [4]

Les deux expressions ont la valeur ‘o’ (le cinquième élément du tableau).

Pointeurs à pointeurs

C ++ permet d’utiliser des pointeurs qui pointent vers des pointeurs, qui à leur tour pointent vers des données (ou même d’autres pointeurs). La syntaxe nécessite simplement un astérisque (*) pour chaque niveau d’indirection dans la déclaration du pointeur:

char a;
char * b;
char ** c;
a = 'z';
b = & a;
c = & b;

Avec la valeur de chaque variable représentée dans sa cellule correspondante et leurs adresses respectives en mémoire représentées par la valeur située en dessous.

La nouveauté dans cet exemple est la variable c, qui est un pointeur sur un pointeur, et peut être utilisée dans trois niveaux d’indirection différents, chacun d’eux correspondant à une valeur différente:

  • c est du type char ** et a une valeur de 8092
  • * c est du type char * et a une valeur de 7230
  • ** c est de type char et a une valeur de ‘z’

Les pointeurs vides (void)

Le type de pointeur vide est un type de pointeur spécial. En C ++, void représente l’absence de type. Par conséquent, les pointeurs vides sont des pointeurs qui pointent vers une valeur qui n’a pas de type (et donc également une longueur indéterminée et des propriétés de déréférencement indéterminées).

Cela donne une grande flexibilité aux pointeurs vides, en leur permettant de pointer vers n’importe quel type de données, d’une valeur entière ou d’un nombre à virgule flottante à une chaîne de caractères. En contrepartie, ils ont une grande limitation: les données pointées par eux ne peuvent pas être directement déréférencés (ce qui est logique, car nous n’avons aucun type à déréférencer), et pour cette raison, toute adresse d’un pointeur vide doit être transformée en un autre type de pointeur qui pointe vers un type de données concret avant d’être déréférencé.

Une de ses utilisations possibles peut être de passer des paramètres génériques à une fonction. Par exemple:

// multiplicateur
#include <iostream>
using namespace std;

void augmenter (void * data, int psize)
{
  if (psize == sizeof (char))
  {char * pchar; pchar = (char *) data; ++ (* pchar); }
  sinon si (psize == sizeof (int))
  {int * pint; pinte = (int *) data; ++ (* pinte); }
}

int main ()
{
  char a = 'x';
  int b = 1602;
  augmenter (& a, sizeof (a));
  augmenter (& b, sizeof (b));
  cout << a << "," << b << '\ n';
  return 0;
}

Résultat de l’exécution :

oui, 1603

sizeof est un opérateur intégré au langage C ++ qui renvoie la taille en octets de son argument. Pour les types de données non dynamiques, cette valeur est une constante. Par conséquent, par exemple, sizeof (char) est égal à 1, car char a toujours une taille d’un octet.

Pointeurs invalides et pointeurs null

En principe, les pointeurs sont destinés à pointer sur des adresses valides, telles que l’adresse d’une variable ou l’adresse d’un élément dans un tableau. Mais les pointeurs peuvent en réalité pointer n’importe quelle adresse, y compris les adresses qui ne font référence à aucun élément valide. Des exemples typiques de ceci sont les pointeurs non initialisés et les pointeurs non existants d’un tableau:

int * p; // pointeur non initialisé (variable locale)
int myarray [10];
int * q = myarray + 20; // élément hors limites

Ni p ni q ne pointent vers des adresses connues pour contenir une valeur, mais aucune des déclarations ci-dessus ne provoque d’erreur. En C ++, les pointeurs sont autorisés à prendre n’importe quelle valeur d’adresse, qu’il y ait ou non quelque chose à cette adresse. Ce qui peut causer une erreur est de déréférencer un tel pointeur (c’est-à-dire d’accéder à la valeur qu’ils pointent). L’accès à un tel pointeur entraîne un comportement indéfini, allant d’une erreur pendant l’exécution à l’accès à une valeur aléatoire.

Mais, parfois, un pointeur doit réellement indiquer explicitement nulle part, et pas seulement une adresse invalide. Dans ce cas, il existe une valeur spéciale que tout type de pointeur peut prendre: la valeur de pointeur null. Cette valeur peut être exprimée en C ++ de deux manières: soit avec une valeur entière égale à zéro, soit avec le mot clé nullptr:

int * p = 0;
int * q = nullptr;

Ici, p et q sont tous deux des pointeurs nuls, ce qui signifie qu’ils pointent explicitement vers nulle part, et qu’ils comparent tous les deux de manière égale: tous les pointeurs nuls se comparent égaux aux autres pointeurs nuls. Il est également assez courant de voir la constante définie NULL être utilisée dans un code plus ancien pour faire référence à la valeur null du pointeur:

int * r = NULL;

NULL est défini dans plusieurs en-têtes de la bibliothèque standard et est défini comme un alias d’une valeur constante de pointeur nulle (telle que 0 ou nullptr).

Ne confondez pas les pointeurs nuls avec les pointeurs vides! Un pointeur nul est une valeur que n’importe quel pointeur peut prendre pour indiquer qu’il pointe vers « nulle part », tandis qu’un pointeur vide est un type de pointeur pouvant pointer vers quelque part sans type spécifique. L’une fait référence à la valeur stockée dans le pointeur et l’autre au type de données pointé.

Pointeurs vers fonctions

C ++ permet des opérations avec des pointeurs vers des fonctions. L’utilisation typique de cela est de transmettre une fonction en tant qu’argument à une autre fonction. Les pointeurs sur les fonctions sont déclarés avec la même syntaxe qu’une déclaration de fonction standard, à l’exception du fait que le nom de la fonction est placé entre parenthèses () et qu’un astérisque (*) est inséré avant le nom:

// pointeur sur les fonctions
#include <iostream>
using namespace std;

int addition (int a, int b)
{retour (a + b); }

soustraction int (int a, int b)
{retour (a-b); }

int operation (int x, int y, int (* functocall) (int, int))
{
int g;
g = (* functocall) (x, y);
retour (g);
}

int main ()
{
int m, n;
int (* moins) (int, int) = soustraction;

m = operation (7, 5, addition);
n = operation (20, m, moins);
cout << n;
return 0;
}

Résultat de l’exécution :

8

Dans l’exemple ci-dessus, le signe moins est un pointeur sur une fonction qui a deux paramètres de type int. Il est directement initialisé pour pointer sur la soustraction de fonction:

int (* moins) (int, int) = soustraction;

Articles similaires

Un commentaire

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Bouton retour en haut de la page