Langage C++/Pointeur, Tableaux et références

Début de la boite de navigation du chapitre
Pointeur, Tableaux et références
Icône de la faculté
Chapitre no 9
Leçon : Langage C++
Chap. préc. :Méthodes
Chap. suiv. :Portée du code
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « Langage C++ : Pointeur, Tableaux et références
Langage C++/Pointeur, Tableaux et références
 », n'a pu être restituée correctement ci-dessus.

Les Pointeurs

modifier

Les pointeurs servent à accéder aux données en mémoire par le biais de leur adresse. Le type pointeur est un entier non signé dont la taille dépend du type de processeur et du mode en cours. En gros, et pour simplifier, il utilise le format du "unsigned long int" actuellement en cours, où il enregistre l'adresse mémoire de la donnée à pointer.

Adresse Valeur Nom de variable
... ... ...
0x635 'P' x
... ... ...
0x7B8 0x00000635 p
... ... ...

Voici la syntaxe :


Où <Type> est le type de la donnée en mémoire, "*" signifie que l’on a affaire à un pointeur, <NomPointeur> est l'identifiant du pointeur et <Valeur> l'adresse mémoire (unsigned long int) où le pointeur doit aller chercher la donnée.


Affectation d'adresse

modifier

En C++, il est utile de récupérer l'adresse d'une variable ou d'une constante en utilisant l'opérateur "&" qui signifie "adresse de" et se place à gauche de l'identifiant.

Début de l'exemple
Fin de l'exemple


Comprendre : création d'une variable nommée "p" de type pointeur sur char "char*" prenant pour valeur "=" l'adresse de "&" la variable "x"

Mécanique d'Adressage

modifier

En C++, il peut être utile de copier l'adresse d'une variable pointée par un pointeur sur un autre pointeur. Cela se fait ainsi :

Début de l'exemple
Fin de l'exemple


On n'utilise pas l'opérateur "adresse de" dans ce cas là car "p1" renvoie l'adresse de "x"

Déréférencement

modifier

En C++, il peut être utile de récupérer la valeur d'une variable pointée par un pointeur. Cela se fait ainsi :

Début de l'exemple
Fin de l'exemple


Arithmétique des pointeurs

modifier

En C++, les pointeurs ont leur propre algorithmique. L'incrémentation et la décrémentation est particulière pour ce type. En fait, l'incrémentation (ainsi que la décrémentation) se fait en multipliant la taille du type du pointeur (obtenue avec sizeof()) par la valeur que l’on veut lui ajouter/retrancher.

L'arithmétique de pointeur est un concept particulier qu’il est possible d’éviter avec les notations liées au tableau. En particulier en C++, il est intéressant de s'interdire de recourir à l’arithmétique de pointeurs.

Début de l'exemple
Fin de l'exemple


Ce comportement permet la création et l’utilisation des tableaux. Nous verrons cela un peu plus bas.

Lorsque les pointeurs sont créés, ils prennent la valeur d'adresse qu’il y avait à l'emplacement qu’ils occupent. C'est pour cela qu’il faut impérativement les initialiser avant de s'en servir. Dans le cas contraire on peut assister, parfois, à des comportements des plus étranges.

Le pire des cas est celui du pointeur non initialisé à qui l’on affecte une valeur. Dans certains cas rares, l’application fonctionne sur l'ordinateur de développement car la donnée que le pointeur va prendre comme adresse, correspond à une adresse valide et donc le programme se déroule normalement.

Mais après installation sur le poste client, l’application plante car la donnée en mémoire n'est plus la même et ne correspond plus à une adresse valide.

 

Il est inacceptable d’utiliser la forme calculée des pointeurs pour accéder à des tableaux. Seule la forme entre crochets est valide. Le jour où un développeur instanciera un tableau dynamique sur un pointeur utilisé sous forme calculée, le programme plantera car le compilateur sera incapable de faire les déréférencements.

Préférez donc la forme entre crochets

Début de l'exemple
Fin de l'exemple

à la forme calculée

 
Début de l'exemple
Fin de l'exemple


Les Tableaux

modifier

En C++, les tableaux permettent de mémoriser des ensembles de données de même type. Il en existe 2 sortes, les tableaux statiques et les tableaux dynamiques.

La différence entre tableaux statiques et dynamiques est que les tableaux statiques sont dans tous les cas des groupes d'octets contigus monolithiques dans la pile alors que les tableaux dynamiques multidimensionnels sont, le plus souvent, des groupes d'octets contigus disséminés en plusieurs paquets dans le tas.


Tableaux "Statiques"

modifier

Le fait qu’ils soient enregistrés en pile fait que leur taille doit être connue dès la compilation et donc être constante. En effet, pour le processeur, il serait mal vu de pouvoir redimensionner le tableau après la création d'autres variables en pile. Dans le cas de l'agrandissement, on écraserait les nouvelles variables et dans le cas de la diminution, on ferait des "TROUS" dans la pile, qui alors s'effondrerait.(Rappelez vous qu’il faut considérer la pile comme une pile d'assiettes)


On peut calculer le volume que le tableau statique occupe en mémoire, en multipliant la taille du type utilisé par le nombre d'éléments.

Ainsi un tableau tridimensionnel de 4 * 3 * 2 valeurs int, fera :

4(grilles) * 3(lignes) * 2(colonnes) * 4(Octets du type int) = 24(éléments)* 4(Octets du type int) = 96 Octets contigus en pile.


Voici la syntaxe :


Où <Type> est le type des éléments du tableau, <NomTableau> est le nom de la variable tableau, [](...)[] indique que c’est un tableau à N dimensions, <ConstanteN> est le nombre d'éléments, constant, de la dimension N du tableau (facultatif si on a défini une liste de valeurs), = assigne des valeurs au tableau (facultatif si <ConstanteN> est présent), {{},(...),{}} est une liste de valeurs des dimensions (il y en a autant que <ConstanteN>, facultatif si <ConstanteN> est présent), et <VariableN1,...VariableNM> sont les différentes valeurs que le tableau doit affecter aux éléments de la ligne N (il doit y en avoir autant que <ConstanteN>, facultatif si <ConstanteN> est présent).


Concrètement, voyons ce que ça donne si on reprend notre exemple :

Début de l'exemple
Fin de l'exemple


Les tableaux statiques, sont organisés par lignes. Cela signifie que lorsque l’on incrémente l'indice du tableau, on change de colonne plus rapidement que ce que l’on change de ligne.

Pour reprendre notre exemple, cela donnerait (en supposant que l'adresse de base du tableau t1 soit 0x02C0) :

Adresse Index no Élément Grille Ligne Colonne Valeur
0x02C0 t1[0][0][0] 0 0 0 0 1
0x02C4 t1[0][0][1] 1 0 0 1 2
0x02C8 t1[0][1][0] 2 0 1 0 3
0x02CC t1[0][1][1] 3 0 1 1 4
0x02D0 t1[0][2][0] 4 0 2 0 5
0x02D4 t1[0][2][1] 5 0 2 1 6
0x02D8 t1[1][0][0] 6 1 0 0 7
... ... ... ... ... ... ...
0x03A8 t1[2][2][1] 17 2 2 1 18
0x03AC t1[3][0][0] 18 3 0 0 19
0x03B0 t1[3][0][1] 19 3 0 1 20
0x03B4 t1[3][1][0] 20 3 1 0 21
0x03B8 t1[3][1][1] 21 3 1 1 22
0x03BC t1[3][2][0] 22 3 2 0 23
0x03C0 t1[3][2][1] 23 3 2 1 24

Pointeurs et Tableaux Statiques

modifier

Nous avons pu voir tout à l’heure que l'arithmétique des pointeurs permet de travailler sur plus d'une valeur voyons ce que cela donne :

Début de l'exemple
Fin de l'exemple


Tableaux "Dynamiques"

modifier

Pour un tableau dynamique, il existe deux méthodes.

Tableaux Compacts
modifier

On peut utiliser le format des tableaux statiques multidimensionnels que l’on reproduit en tas.

Voici la syntaxe de déclaration :


Où <Type> est le type du tableau, <NomVariable> est le nom identifiant du tableau, <Variable> est une variable entière non signée définissant la première dimension du tableau, <ConstanteN> sont des constantes entières non signées définissant les autres dimensions du tableau.

Aucun tableau dynamique n'est assignable à la déclaration. On ne peut pas en C++ assigner des valeurs à un tableau dynamique lors de sa création.

Début de l'exemple
Fin de l'exemple


Cela n’est pas très judicieux pour trois raisons :

  • La première c’est que les tailles des dimensions du tableau sont fixées définitivement (à moins de recréer le tableau complet).
  • La deuxième c’est que les différents éléments ne sont pas désolidarisables du tableau.
  • La troisième pour redimensionner le tableau il faut le recopier intégralement.

Techniquement, on peut faire mieux.

La seule raison valable qui peut amener à utiliser ces types de tableaux dynamiques est leur forme compacte. Ceci dit ils ne font réellement gagner de l'espace que sur les tableaux dont on connaît à l'avance la longueur. En effet, la lourdeur du traitement pour ajouter ou soustraire des éléments est souvent rédhibitoire.

Il ne reste donc qu'une solution techniquement valable en environnement PC : L'indirection de pointeurs multiples.

Indirection de Pointeurs
modifier

Le but du jeu est de créer un tableau de pointeurs où chaque pointeur pointe vers un autre tableau de pointeurs... (ce récursivement pour chaque dimension)... où chaque pointeur pointe vers un élément.


Où <Type> est le type du tableau, <NomTableau> est le nom du tableau et <Variable> une variable entière non signée.

Voyons comment cela est représenté en mémoire :


Voici un exemple concret qui montre comment créer/détruire et manipuler un tableau utilisant l'indirection de pointeurs.

Début de l'exemple
Fin de l'exemple


Pointeurs sur Méthodes

modifier

En C++, il est possible de créer des pointeurs sur des méthodes. Cela est surtout utile pour lier les fonctions d'une bibliothèque de liens dynamiques ou, plus intéressant, pour appeler des méthodes que l’on ne connaît pas à l'avance mais qui auront un prototype que l’on a défini. Cela permet, par exemple, de créer une liste de tâches à accomplir suite à un évènement donné.


Où "<TypeRetour>" est le type de retour de la méthode à pointer, "<NomPointeur>" est le nom attribué au pointeur, "<TypeParametre>" et "<NomParametres>" sont respectivement les types et noms des paramètres de la méthode

Début de l'exemple
Fin de l'exemple

Remarquez bien les parenthèses autour de "*LaMethode". Si on ne les avait pas mis comme dans cet exemple :

Début de l'exemple
Fin de l'exemple


Le compilateur aurait compris "LaMethode" une méthode qui accepte un paramètre "Params" de type "int" et retourne un pointeur de type "bool";

Pointeurs sur Méthodes Encapsulées

modifier

Il est possible de créer des pointeurs sur des méthodes qui sont encapsulées dans des classes à deux conditions :

  • la méthode doit être publique,
  • la méthode doit être statique.
Début de l'exemple
Fin de l'exemple


Les Références

modifier

Devant la difficulté qu'avaient la plupart des programmeurs de l'époque, à gérer convenablement la mémoire avec les pointeurs, le C++ a introduit la notion de référence. Une référence est un ALIAS, un autre nom, pour une variable DÉJÀ EXISTANTE. À l'inverse du pointeur, une référence n'occupe pas de place en mémoire. Le compilateur la remplace par le nom de la variable actuellement assignée dans le code après avoir déroulé les traitements mais avant de traduire ce code en binaire.



Où "<Type>" est le type de la référence "<NomRéférence>" est l'identifiant de la référence et <NomVariable> est l'identifiant de la variable que la référence doit désigner. Contrairement aux pointeurs, la référence doit impérativement être assignée dès la déclaration et il n’est pas possible de réassigner la référence à une autre variable après initialisation.


Ou plus concrètement :

Début de l'exemple
Fin de l'exemple


Références sur Méthodes

modifier

À l'instar du pointeur, il est possible de référencer une méthode.



Où "<Type>" est le type de retour de la méthode à référencer, "<NomRéférence>" est l'identifiant de la référence, <TypeParam> et <NomParam> sont respectivement le type et le nom de l'image des paramètres de la méthode à référencer et <NomMéthode> est l'identifiant de la méthode que la référence doit désigner. Contrairement aux pointeurs, la référence doit impérativement être assignée dès la déclaration et il n’est pas possible de réassigner la référence à une autre méthode après initialisation.

Début de l'exemple
Fin de l'exemple


Le Casting :

modifier

L'une des fonctionnalités les plus appréciées dans la manipulation des variables est l'opérateur de casting, ou de refonte, que sont les parenthèses "()".

Syntaxe:
(<Type>)[<Constante>|<Variable>]

Où <Type> est le type dans lequel la valeur <Constante> ou <Variable> doit être refondue.

En fait, on peut refondre des valeurs de types différents de manière sûre si au moins l'une de ces conditions est remplie :

  • La valeur source peut être transcodée dans le type de destination.(ex. : (char)'A'; est équivalent à (char)41; "41" est un const int)
  • Le type de la valeur source peut être approximée ou être un sous ensemble du type de la variable de destination (numérique, ex. : (double)int ou (int)double).
  • Le type de la valeur source hérite du type de la variable de destination (upcasting).
  • Le type de la valeur source est parente du type de la variable de destination (downcasting).
  Attention : L'utilisation de l'opérateur de refonte est à utiliser avec précautions. Le compilateur ne vous avertira pas si un dépassement de capacité a lieu ou si vous faites des downcasting (refontes descendantes) avec des types de nature différente

Exemples :

modifier

Exemple de cast pour une valeur constante

Début de l'exemple
Fin de l'exemple


Autre exemple pour une variable :

Début de l'exemple
Fin de l'exemple