
Le langage C++ permet de définir et manipuler des champs dont la taille est inférieure à un octet et qui se mesure en nombre de bits. Un champ de bits aura un type de donnée entier (également caractère ou même booléen), signé ou non signé. L'identificateur de champ de bits est suivi d'un deux points : ' , puis d'une expression constante indiquant le nombre de bits.
Les champs de bits définis consécutivement dans le corps de la structure sont regroupés dans des bits adjacents du même entier, permettant ainsi de compresser la mémoire.
Un champ de bit est accédé de la même manière que les autres données membres d'une structure.

Les champs de bits peuvent procurer des facilités dans certains cas ; ils sont surtout utiles dans des applications très techniques faisant intervenir le matériel ou les périphériques.
Une union est une sorte de structure spéciale. Les données membres dans une union sont stockées en mémoire de façon à ce qu'elles se recouvrent. Chaque membre commence à la même adresse mémoire. La quantité mémoire allouée à une union est celle nécessaire pour contenir la plus grande de ses données membres. Seul un membre à la fois peut être effecté d'une valeur.

Lorsque nous combinons les structures de bits avec des unions, nous obtenons une grande richesse d'expression. Il sera alors possible de manipuler des données dotées d'une infrastructure très compliquée, avec au contraire, une utilisation d'une simplicité décourcertante. Par exemple, il est possible de représenter un nombre réel avec sa représentation à virgule flottante, et en même temps dans sa représentation en notation scientifique avec la séparation de la mantisse, de l'exposant et de la valeur du signe.

La plupart du temps, une union propose une alternative de représentation.
Nous avons dit qu'une donnée est une case de la mémoire qui possède un certain type. Pour pouvoir parler de cette case, on peut soit utiliser son adresse mémoire (qui est la méthode des pointeurs), soit faire référence directement à la donnée, ce qui est le cas quand on utilise un nom de variable. Ainsi, après la déclaration suivante :
int i = 18 ;
Le compilateur a réservé une case mémoire de 4 octets pour la variable i, et chaque fois que dans un programme on parle de i, le compilateur sait qu'il doit utiliser cette case là. Nous dirons que i est une référence pour la donnée correspondante de valeur 18.
En général un seul nom suffit pour une case mémoire. Mais dans certain cas, on a besoin d'un autre nom, d'une seconde référence. Pour la créer et l'initialiser, on utilise l'opérateur de référence & '.
int &j = i ;
Ceci signifie : j est une référence sur un entier qui est équivalent à i (on dit souvent que j est un alias de i). Dès lors, tous les usages de j dans la suite du programme seront équivalent à ceux de i. Par exemple, si l'on écrit :

A la fin de ces instructions, i et j seront tous deux égaux à 20. Plus exactement, l'unique case mémoire dénommée à la fois i et j contiendra la valeur 20.
.

Dans les programmes courants, les références sont plutôt utilisées comme paramètres formels d'une fonction, généralement pour passer des objets de classe volumineux. Toutefois, il peut être intéressant de les utiliser pour des cas bien particuliers.

Lorsqu'un programme entre en exécution, des cases de cette mémoire sont réservées pour chaque variables, le nombre de cases étant fonction du type de la variable. Chaque variable se trouve quelque part en mémoire. On dira plus précisément qu'elle se trouve à telle adresse. Il est possible d'accéder à une variable :
Dans nos précédents programmes, l'accès à une variable (plus précisément à son contenu) se faisait par l'intermédiaire de son nom. Mais au lieu d'accéder par ce nom directement à la variable concernée, on peut aussi choisir un chemin d'accès indirect par le biais de l'adresse de la variable. Pour cela, on utilise ce que l'on appelle un pointeur.
Un pointeur désigne une variable qui contient l'adresse d'une autre variable. On dit aussi que le pointeur renvoie ou pointe' (d'où le nom) vers la variable concernée, cela via son contenu consistant en une adresse de variable. Les pointeurs sont parfois appelés indirections '.
Comme toute variable, un pointeur doit être déclaré préalablement à son utilisation. Un pointeur est défini en préfixant l'identificateur avec l'opérateur de déréférencement * '. Le programmeur doit également indiquer le type de la variable pointée. Il est possible, mais non obligatoire (contrairement aux références), d'initialiser les pointeurs. Il suffit alors d'indiquer l'adresse de la variable à pointer en utilisant l'opérateur de référence & ' (& indique l'adresse de).

Il existe deux façons d'utiliser un pointeur.


Chaque pointeur possède un type associé. La différence entre des pointeurs de type de données différents ne se trouve ni dans la représentation du pointeur, ni dans les valeurs (adresses) que le pointeur peut contenir en effet toutes les adresses ont la même capacité mémoire. La différence est plutôt dans le type de la variable adressée. Le type du pointeur instruit le compilateur sur la façon dont il doit interpréter la mémoire trouvée à une adresse particulière ainsi que sur la quantité de mémoire que doit couvrir cette interprétation :
Comme nous l'avons expérimenté dans le scénario précédent, il est tout à fait possible de modifier un pointeur, c'est-à-dire de changer l'adresse à laquelle il réfère. Nous avons d'ailleurs déjà utiliser l'opérateur d'affectation pour réaliser cela. On dispose de plusieurs autres opérateurs qui permettent de changer la valeur de l'adresse relativement à la précédente valeur.
L'opérateur d'incrémentation ++ ' par exemple agit de la même façon que sur des entiers. Cependant, le pointeur est augmenté de une position, non de un octet. En effet, le pointeur pointe sur un type T qui a une certaine taille en octets t (t = 4 dans le cas d'un int) ; lorsqu'on écrit p++, dans ce cas, l'adresse est augmentée de t octets, afin de pointer sur l'élément de type T supposé suivre dans la mémoire.

On peut se demander légitimement si la technique de l'arithmétique des pointeurs est judicieuse puisqu'elle comporte de nombreux risques d'atteindre un emplacement mémoire non prévu au départ. Il existe deux types de risque :
En fait, l'arithmétique des pointeurs a été mis en place pour permettre de se déplacer librement au sein d'un tableau ou au travers d'une chaîne de caractères (qui est également un tableau). La particularité d'un tableau, c'est justement de disposer de cases contigües d'un même type d'éléments. Par ailleurs, n'oubliez pas que le nom du tableau correspond à un pointeur constant placé sur sa première case.
| Lignes de code successives | Explications |
int a[ ] = {11, 22, 33, 44} ; |
Déclaration d'un tableau de quatre entiers initialisé respectivement par les valeurs a[0]=11, a[1]=22, a[2]=33, a[3]=44. |
int *pa, x; |
Déclaration d'un pointeur sur un entier ainsi que d'un entier appelé x. |
pa = &a[0];
ou
pa = a; |
Copie dans pa l'adresse de la première case du tableau. Comme le nom du tableau correspond justement à un pointeur constant sur la première case du tableau, la deuxième écriture est préférable puisque plus concise.
Le nom d'un tableau, sans référence à un indice, peut être utilisé dans un programme, à condition de le considérer comme une constante. |
x = *pa; |
Copie le contenu de l'entier pointé par pa (c'est-à-dire a[0] ) dans x. x <== 11 |
pa++; |
pa est incrémenté d'une unité et contient maintenant l'adresse de la deuxième case du tableau, c'est-à-dire l'adresse de a[1].
pa = &a[1] |
x = *pa; |
pa pointe sur a[1], *pa est le contenu de a[1] . x <== 22 |
x = *pa + 1; |
Copie dans x la valeur a[1]+1. x <== 23 |
x = *(pa+1); |
Comme les parenthèses sont prioritaires par rapport à l'opérateur d'indirection, le calcul intermédiaire pa+1 est effectué en premier, ce qui donne comme résultat l'adresse de a[2] puisque l'unité d'incrémentation est la taille de la variable pointée.
L'entier contenu à cette adresse (c'est-à-dire a[2] ) est copié dans x. x <== 33 Notez que si pa intervient dans le calcul, il n'est pas modifié, pa pointe toujours sur a[1]. |
x = *++pa; |
En vertu des règles de priorité des opérateurs ( * et ++ sont de niveau 2), l'opération la plus à droite est d'abord effectuée, c'est-à-dire ++pa .
pa , qui est modifié par cette opération, contient maintenant l'adresse de a[2] . pa = &a[2]
L'opération d'indirection * est ensuite effectuée. x contient dès lors a[2]. x <== 33 |
x = ++*pa; |
*pa est d'abord évalué.
pa pointant sur a[2], *pa est a[2]. La cellule a[2] est soumise à incrémentation, toujours de 1 car a[2] n'est pas un pointeur mais bien un entier.
x <== a[2] <== 34
Bien que l'on ait à faire à une préincrémentation, * est prioritaire par rapport à ++ car ces deux opérateurs se situent au niveau 2 de priorité. Or, à ce niveau, l'opérateur le plus à droite dans l'expression est prioritaire. |
x = *pa++; |
La postincrémentation, même si elle paraît prioritaire ici, n'est effectuée qu'en dernier lieu, par définition de la postincrémentation. Les règles de prorité indiquent cependant que l'opération ++ porte sur pa et non sur *pa.
Le contenu de *pa (c'est-à-dire 34) est copié dans x. x <== 34 pa est ensuite incrémenté et contient maintenant l'adresse de a[3]. pa = &a[3] |
Nous venons de voir que l'accès à un élément quelconque de tableau peut se faire non seulement par le nom du tableau accompagné d'un indice, mais aussi par un pointeur manipulé par des opérations spécifiques d'arithmétique de pointeurs. En voici un autre exemple :
| En utilisant les indices | Avec l'arithmétique des pointeurs |
int x[10] ; |
int x[10], *px = x ; |
En fait px et x, l'un comme l'autre sont des pointeurs. Il n'existe qu'une seule différence. Un tableau est un pointeur constant qui pointe toujours sur la première case du tableau. Le fait qu'il y ait similitude entre les tableaux et les pointeurs est très important. Cela veut dire que lorsque nous aurons affaire à un pointeur, il sera possible d'avoir une écriture spécifiquement prévu pour les tableaux en utilisant notamment les indices. De la même manière, lorsque nous aurons affaire à un tableau, il sera également possible d'avoir une écriture spécifiquement prévu pour les pointeurs en utilisant l'arithmétique des pointeurs en faisant attention toutefois à ce que l'adresse du tableau ne change pas puisqu'elle doit être constante. Utilisons ce principe pour x et px :
Arithmétique des pointeurs pour le tableau |
Indice de tableau sur un pointeur |
int x[10] ; |
int x[10], *px = x ; |
L'exemple ci-dessus mérite quelques explications. Il est impératif de bien comprendre ce qui se passe au niveau du compilateur lorsqu'il analyse l'écriture d'un tableau. Revenons justement sur la variable x qui représente un tableau d'entier de 10 cases :
Donc lorsqu'on écrit &x[n], cela veut bien dire : adresse de la case du tableau x indicé par n qui se traduit plus simplement par x+n, qui dans ce cas, correspond plus à un décalage par rapport à l'adresse d'origine (première case du tableau). De toute façon, quel que soit l'écriture, nous sommes là en présence d'une adresse.
Lorsque nous voulons maintenant atteindre la donnée relative à cette adresse. Pour la syntaxe liée aux indices, il suffit d'enlever le symbole & ' (qui indique bien une adresse). Pour l'arithmétique des pointeurs, il est nécessaire de déréférencer en utilisant le symbole * '.
Donnée relative à : ![]()
De toute façon, en interne, lorsque le compilateur rencontre une écriture utilisant la notation en indice, il sait que c'est un pointeur, et il transforme donc cette écriture pour donner l'équivalent avec l'arithmétique des pointeurs. Lorsque nous avons :
Incidemment :
*(t+4) <==> *(4+t) puisque l'addition est commutative donc ==> t[4] <==> 4[t]
.
| Tableau | Pointeur |
Pointeur constant, mais avec une réservation d'une zone mémoire correspondant à la capacité du tableau pour stocker les données |
Pointeur variable. On peut donc modifier son contenu. Par contre, il n'y a pas de réservation mémoire de la variable pointée. Cette réservation doit être réalisée au préalable. |
Tout ce que nous avons pu dire ou faire sur les tableaux s'applique également pour les chaînes de caractères. D'ailleurs, on utilise très souvent un pointeur de caractère pour référencer une chaîne. Cela dépend du cas de figure. Lorsque nous décidons de prendre un tableau, cela veut dire que notre variable chaîne de caractères aura plusieurs valeurs possibles au cours du programme, et nous sommes donc obligé de faire une réservation mémoire suffisante pour pouvoir stocker la plus grande des chaînes admissible (le tableau est donc impératif).
Lorsque nous avons besoin que d'une seule valeur de chaîne qui servira généralement de message, à ce moment là, il est préférable de prendre un pointeur qui sera initialiser par la constante littérale.
char texte[7] = « Salut » ; // tableau de caractères
char *message = « Bonjour » ; // pointeur vers un caractère

La variable message est un pointeur de caractère, il est donc tout à fait possible de changer sa valeur au cours du temps. Attention toutefois, cela reste un pointeur, ce n'est pas une chaîne de caractères en tant que tel. Si vous proposer une nouvelle affectation, vous ne copiez pas la nouvelle chaîne, vous pointez vers cette nouvelle chaîne. Par ailleurs, si vous faites cette affectation, l'adresse de la chaîne précédente est perdue, ce qui fait que l'ancienne chaîne n'est plus du tout accessible.

A titre d'exercices, à partir d'une variable chaîne de caractères initialisée, déterminez la longueur de cette chaîne en proposant deux solutions. D'une part en utilisant la syntaxe des tableaux, et d'autre part, en utilisant l'arithmétique des pointeurs. Comme deuxième exercice, prévoyez un programme qui permet de copier réellement une chaîne de caractère vers une autre, là aussi, en utilisant les deux types de syntaxe.
Le symbole &' de calcul d'adresse peut porter sur un pointeur : il s'agit de l'adresse d'implantation en mémoire du pointeur.
int x=5, *p = &x ;
déterminez :
p = ? // désigne le contenu du pointeur
&p = ? // désigne l'adresse du pointeur
*p = ? // désigne le contenu de la variable pointée.


Si nous désirons qu'un pointeur ne pointe sur rien momentanément, il est préférable de l'initialiser à 0.
.
Les pointeurs peuvent intégrer des adresses de variable de n'importe quel type, et bien évidemment, c'est aussi vrai pour les variables de type structure ou union. Les exemples que nous prendrons, seront uniquement proposés pour des structures, mais la syntaxe demeure identique pour les unions.

Dans l'exemple ci-dessus, nous désirions travailler avec la totalité de la structure pointée. Nous remarquons que la syntaxe est identique au pointeur de type entier ou caractère. Il suffit d'utiliser l'opérateur de déréférencement. Par contre, nous avons souvent besoin de d'accéder à un seul des champs de la structure. Il faut que le système procède en deux étapes. En premier, il faut déréférencer la structure (grâce à l'opérateur de déréférencement * ' ) et ensuite accéder au champ désiré (grâce à l'opérateur de champ .' ). Exemple :
(*pdate).jour = 12 ; // change le jour de noel
Attention, les parenthèses sont nécessaires à cause de la priorité des opérateurs. .' est prioritaire sur *'. Sans les parenthèses, le compilateur aurait interprété :
*(pdate.jour) = 12 ; // par cette écriture, c'est le champ de la structure qui est déréférencé ==> Erreur compilation
Cette syntaxe est un peu lourde. Les concepteurs du langage ont donc prévus un opérateur spécialisé sur l'accès à un champ d'une structure (ou d'une union) pointée. Il s'agit de l'opérateur ->'. Du coup, l'exemple précédent devient :
pdate ->jour = 12 ; // jour est un champ d'une structure pointée par pdate.
| Déclaration | Question | Réponse |
struct { |
Afficher nom de p1 |
cout << p1.nom ; |