.
Chapitre 6
Structures de données
Une structure de données est une construction qui essaie de rassembler de façon commode
et fiable des données ayant un rapport entre elles et que l'on est souvent amené à manipuler «
en bloc ». Il s'agit souvent d'une image informatique d'un objet ou d'un concept du monde réel.
Un vecteur, comme une accélération ou un champ électrique, constitue un bon exemple ; avec les
connaissances acquises dans les chapitres précédents, je peux représenter les trois composantes
d'un champ électrique comme trois nombres
Ex, Ey, Ez . Cette représentation est peu commode
du point de vue du programmeur. Une carte d'identité est un autre objet que je peux souhaiter
informatiser ; elle contient des données relative à une même personne : nom, prénom, date de
naissance, taille, couleur des yeux. . .Pour l'instant, je ne sais pas manipuler une carte d'identité
autrement qu'en lui associant une série d'identificateurs.
Les tableaux sont des « structures de données». Ils constituent en fait la catégorie la plus
importante de structure de données en programmation scientifique et technique car ils sont parfaitement
adaptés à la représentation des vecteurs et des matrices. Ces structures sont « rigides»,
c'est-à-dire qu'elles ne changent pas de taille au cours de l'exécution d'un programme ; on connaît
en informatique des structures de données (listes, arbres) dont la taille peut croître ou décroître
pendant l'exécution.
6.1 tableau : définition
Un tableau est un ensemble d'éléments qui portent tous le même nom et qui appartiennent tous
au même type de variable (
int, float, char,... ). Ces données occupent en général des cases
successives dans la mémoire, si bien que l'on a tendance à confondre élément du tableau et case
mémoire correspondante. Pour désigner un emplacement particulier, on utilise le nom collectif et le
rang ou l'indice de l'élément. La figure ci-dessous représente schématiquement un tableau de nom
c .
c[0] c[1] c[2] c[3] c[4] c[5] c[6] c[7] c[8] c[9] c[10] c[11]
6 -68 123 6874 0 -852 951 -1 528 6 1 7896
Sur la première ligne, j'ai écrit le nom en C++ de chacune des douze variables
qui sont
numérotées de 0 à 11
. La deuxième ligne montre le contenu des douze emplacements mémoire
correspondants : ces variables sont manifestement de type
int . Le nom d'un élément, le sixième par
exemple, est
c[5] . C'est une variable comme une autre, que je peux manipuler comme d'habitude ;
les expressions suivantes ont leur sens habituel :
x = c[2] + c[3]; cout << c[8] << c[11]/c[0] << endl; x = c[1]/3;
L'indice est un entier, soumis aux règles de calcul ordinaire, comme dans
a = 3; b = 4; c[a + b];
41
Intro_C
42
Il y a cependant une règle
fondamentale à respecter : l'indice qui désigne l'un des éléments d'un
tableaux
doit rester à l'intérieur de l'intervalle défini lors de la déclaration du tableau (voir plus
loin). Contrairement à ce que l'on peut voir en Pascal, il n'y a aucun mécanisme pour «surveiller» les
indices et aucun message d'erreur si un indice déborde de l'intervalle permis. La seule conséquence,
mais elle est en général spectaculaire, est que l'on lit comme valeur d'une variable une syllabe de
code ou les données d'un autre utilisateur ; de même, on peut écrire sur un fragment de programme
ou sur des données à conserver. Ainsi, le compilateur ne protestera pas si j'écris
c[12] = 22 en
croyant initialiser le dernier élément du tableau précédent.
6.2 Déclaration et initialisation de tableaux
Comme toute variable, un tableau doit être déclaré avant utilisation, mais il faut préciser sa
taille, pour que le compilateur réserve l'espace mémoire correspondant. Ainsi, pour le tableau
précédent
int c[12]; ou encore
float x[1024]; int a[10], b[10]; char texte[128];
Je peux fixer la valeur des éléments du tableau en même temps que je le déclare :
int c[8] = {1,6,-4,123,-21,0,478,360};
Si la liste entre accolades contient moins de valeurs qu'il n'y a d'éléments, les derniers éléments
sont initialisés à zéro. Le cas contraire (plus de valeurs que d'éléments) provoque une erreur de
compilation. Il est bien préférable d'écrire
int c[] = {1,6,-4,123,-21,0,478,360};
pour laisser le compilateur compter lui-même.
6.3 Exemples élémentaires
Voici un programme simpliste qui rempli un tableau avec des entiers consécutifs et en calcule
la somme.
1
// t a b l e a u 1 . cpp : r emp l i s s a g e d 'un t a b l e a u
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void ){
6
int t [ 1 0 ] , s [ 1 0 ] , i ;
7
for ( i = 0 ; i < 1 0 ; i++)
8 t [ i ] = i ;
9 s [ 0 ] = 0 ;
10
for ( i = 1 ; i < 1 0 ; i++)
11 s [ i ] = s [ i
¡ 1] + t [ i ] ;
12 cout << " element somme cumulee \n" ;
13
for ( i = 0 ; i < 1 0 ; i++)
14 cout << "\ t " << t [ i ] << "\ t \ t " << s [ i ] << "\n" ;
15 system( " pause " ) ;
16 }
Je définis deux tableaux (ligne 6) de 10 éléments chacun. J'utilise ensuite une boucle « for » pour
initialiser le tableau
t (lignes 7,
. La boucle suivante garnit le tableau s : l'élément s i est la somme
des éléments de
t , depuis 0 jusqu'à i .
Intro_C
43
Dans l'exemple suivant, j'imagine que les réponses à un questionnaire ont été codées de 1 à 10.
À partir des résultats de l'enquête, je doit déterminer la fréquence d'apparition de chaque réponse.
J'ai supposé que les données étaient assez peu nombreuses pour permettre une initialisation du
tableau dans le corps du programme.
1
// t a b l e a u 2 . cpp : t a b l e a u de f r é quenc e s ou histogramme
2
#include <ios t r eam . h>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void ){
6
int nb_reps , nb_code = 1 0 ;
7
int code , i , f r e q s [ 1 0 ] ;
8
int r eps [ 4 0 ] = { 1 , 3 , 8 , 8 , 4 , 2 , 3 , 1 , 2 , 6 ,
9 4 , 8 , 3 , 7 , 2 , 1 , 3 , 3 , 6 , 7 ,
10 5 , 8 , 2 , 3 , 1 , 9 , 7 , 8 , 6 , 1 ,
11 7 , 4 , 1 , 2 , 3 , 6 , 5 , 4 , 7 , 8 } ;
12
for ( code = 1 ; code <= nb_code ; code++)
13 f r e q s [ code ] = 0 ;
14
for ( i = 0 ; i < 4 0 ; i++)
15 ++f r e q s [ r eps [ i ] ] ;
16 cout << " code f r equenc e " << endl ;
17
for ( code = 1 ; code <= 1 0 ; code++)
18 cout << code << "\ t " << f r e q s [ code ] << endl ;
19 system( " pause " ) ;
20
return 0 ;
21 }
Une première boucle (lignes 12,13) me sert à initialiser à zéro le tableau des fréquences. Chaque
élément de ce tableau va en fait jouer le rôle d'un compteur, qui sera incrémenté de un à chaque
apparition du code correspondant. Remarquez que je numérote ces éléments/compteurs de 1 à 10,
comme les codes correspondants ; cela revient à ignorer
freqs[0] , qui peut contenir n'importe quoi.
Il serait en effet maladroit et malcommode de numéroter différemment le code et son compteur.
La deuxième boucle (lignes 14,15) parcourt les éléments du tableau des réponses ; si par exemple
reps[12]
vaut 4, l'élément freqs[4] = freqs[reps[12]] est incrémenté.
Les lignes suivantes impriment les résultats du décompte. J'insiste encore sur la nécessité de
contrôler la validité des données. Si le tableau des réponses contient la valeur 13, le programme
essaiera d'incrémenter freqs[13], qui n'existe pas, avec un résultat imprévisible ou catastrophique.
Voici ce qu'imprime le programme :
code frequence
1 6
2 5
3 7
4 4
5 2
6 4
7 5
8 6
9 1
10 0
Le programme précédent fonctionne, mais il est mal écrit ! En effet, il est difficile à modifier.
Pour changer un détail comme le nombre de réponses ou le nombre de codes, il faut parcourir tout
le programme, sans oublier aucune des apparitions de ces paramètres. C'est assez facile ici, ça le
serait moins pour un programme de 10000 lignes. En général, il faut «paramétrer» son programme,
Intro_C
44
pour permettre des modifications faciles, qui affectent une ou deux lignes ; cela est particulièrement
vrai des programmes qui utilisent des tableaux. C'est ce que j'ai déjà fait de façon très partielle,
en définissant l'entier
nb_code .
Le langage C offre la possibilité de définir et d'initialiser facilement des paramètres constants
grâce au préprocesseur. Il suffit d'introduire au début les définitions
#define NB_REPS 40
#define NB_CODE 10
Remarquez l'absence de ponctuation ! Lorsque le préprocesseur rencontre le nom
NB_CODE dans
le programme, il le remplace
mécaniquement par 10. Si l'utilisateur veut changer le nombre de
réponses, il lui suffit de le faire dans la seule ligne de définition. De même pour
NB_REPS et 40.
L'écriture en majuscules est une convention universellement respectée : elle signe une définition
destinée au préprocesseur.
Si ce mécanisme est tout à fait admis en C++, il est cependant peu utilisé, car on dispose d'un
autre procédé aussi simple et plus puissant. Il suffit de déclarer et d'initialiser (au niveau global
ou à tout autre niveau commode) des constantes entières :
const int nb_reps = 40, nb_code = 10;
Ces «variables» ne sont pas modifiables dans la suite du programme : toute tentative d'affectation
d'une nouvelle valeur à
nb_reps ou à nb_code provoquera une erreur de compilation.
Les deux méthodes précédentes offrent un autre avantage intéressant : les symboles ainsi définis
peuvent servir lors de la déclaration de la taille d'un tableau. On ne peut pas écrire
int reps[nb_reps];
........ // impossible
cin << nb_reps;
Les tableaux de « dimension variable», définie au moment de l'exécution, sont, en effet, interdits
en C comme en C++ sauf appel au mécanisme d'allocation dynamique, qui ne sera pas abordé
dans ce cours. Par contre,
const int nb_reps = 40;
int reps[nb_reps];
est licite, et permet d'adapter la taille du tableau aux circonstances, en changeant la valeur d'une
constante sur une seule ligne (ou d'un
#define ).
Voici une nouvelle version du même programme, tenant compte de ces perfectionnements. J'en
profite pour y ajouter le tracé (grossier) d'un histogramme des fréquences. Je vais imprimer, pour
chaque valeur de
code , une barre horizontale de longueur proportionnelle à freqs[code] . Auparavant,
il me faudra normaliser les valeurs des éléments de
freqs pour que les barres tiennent dans
la page.
1
// h i s t o g . cpp : c ons t r u c t i on amé l ior é e d ' histogramme
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
const int nb_reps = 40 , //nombre de r épons e s
6 nb_code = 10 ,
//nombre de codes ¡ v a l e u r s
7 l_lg = 6 0 ;
// longueur d ' une l i g n e
8
int main ( void ){
9
int code , i , j , f r e q s [ nb_code+1] , fmax ;
10
int r eps [ nb_reps ] = { 1 , 3 , 8 , 8 , 4 , 2 , 3 , 1 , 2 , 6 ,
11 4 , 8 , 3 , 7 , 2 , 1 , 3 , 3 , 6 , 7 ,
Intro_C
45
12 5 , 8 , 2 , 3 , 1 , 9 , 7 , 8 , 6 , 1 ,
13 7 , 4 , 1 , 2 , 3 , 6 , 5 , 4 , 7 , 8 } ;
14
for ( code = 1 ; code <= nb_code ; code++)
15 f r e q s [ code ] = 0 ;
16
// c onf e c t i on de l ' histogramme
17
for ( i = 0 ; i < nb_reps ; i++)
18 ++f r e q s [ r eps [ i ] ] ;
19 cout << " code f r equenc e " << endl ;
20
for ( code = 1 ; code <= nb_code ; code++)
21 cout << code << "\ t " << f r e q s [ code ] << endl ;
22 cout << endl ;
23
// r e che r che de l a f r é quenc e max
24 fmax = f r e q s [ 1 ] ;
25
for ( code = 2 ; code <= nb_code ; code++)
26
i f ( f r e q s [ code ] > fmax ) fmax = f r e q s [ code ] ;
27
// normal i s at i on
28
for ( code = 1 ; code <= nb_code ; code++)
29 f r e q s [ code ]
? = l_lg /fmax ;
30
// impr e s s ion
31
for ( code = 1 ; code <= nb_code ; code++){
32 cout << code << "\ t " << f r e q s [ code ] << "\ t " ;
33
for ( j = 1 ; j <= f r e q s [ code ] ; j++)
34 cout << '
? ' ;
35 cout << "\n" ;
36 }
37 system( " pause " ) ;
38
return 0 ;
39 }
Voici le résultat :
1 48 ************************************************
2 40 ****************************************
3 56 ********************************************************
4 32 ********************************
5 16 ****************
6 32 ********************************
7 40 ****************************************
8 48 ************************************************
9 8 ********
10 0
Vous constatez que les valeurs affichées tiennent compte de la nouvelle normalisation, mais de
façon pas tout à fait exacte. Cela est due à la ligne 29, où se produit une erreur d'arrondi. Comment
faut-il modifier le programme ?
6.4 Tableau comme argument d'une fonction
Étant donné la déclaration
int temperature[24]; , qui fait référence à un tableau de 24 entiers,
je peux appeler une fonction qui utilise les éléments de ce tableau par l'instruction
calc( temperature, 24).
Il est souvent nécessaire d'informer la fonction de la taille du tableau (24 ici) pour qu'elle traite
effectivement tous les éléments.
Attention :
à la différence des arguments « simples », un tableau argument d'une fonction est
toujours transmis par référence (on dit aussi par adresse). Cela signifie que toute modification
Intro_C
46
des éléments de
temperature dans le sous-programme calc se fera sentir dans le programme
principal ! La raison profonde est que le nom du tableau contient en fait l'adresse du premier
élément ; à l'aide de cette information, la fonction peut faire ce qu'elle veut de chaque élément
du tableau. Au contraire, un élément isolé est traité comme une variable ordinaire (passage par
valeur). On peut justifier ce comportement par le fait que copier un gros tableau pour le passer
par valeur coûterait beaucoup de temps et de place (cf le qualificatif
VAR en Pascal).
Voyons un petit exemple de ces notions.
1
/ ? tab_arg . cpp : pas sag e de t a b l e a u e t d ' élément de t a b l e a u en argument ? /
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
#define TAILLE 5
6
void modifTab ( int [ ] , int ) ;
7
void modifElem( int ) ;
8
int main ( void ){
9
int a [TAILLE] = { 0 , 1 , 2 , 3 , 4 , } , i ;
10 cout << " e l ement s du tabl eau de depar t : " << endl ;
11
for ( i = 0 ; i < TAILLE; i++)
12 cout << a [ i ] << "\ t " ;
13 cout << endl ;
14 modifTab ( a ,TAILLE) ;
15 cout << " e l ement s du tabl eau apr e s modifTab : " << endl ;
16
for ( i = 0 ; i < TAILLE; i++)
17 cout << a [ i ] << "\ t " ;
18 cout << endl ;
19 cout << " va l eur de a [ 3 ] : " << a [ 3 ] << endl ;
20 modifElem( a [ 3 ] ) ;
21 cout << " va l eur de a [ 3 ] apr e s modifElem : " << a [ 3 ] << endl ;
22 system( " pause " ) ;
23
return 0 ;
24 }
25
void modifTab ( int b [ ] , int t a i l l e ){
26
int i ;
27
for ( i = 0 ; i < t a i l l e ; i++)
28 b [ i ]
? = 2 ;
29 }
30
void modifElem( int x ){
31 x
? = 5 ;
32 cout << " va l eur l o c a l e de x modi f i e e : " << x << endl ;
33 }
dont le résultat est
elements du tableau de depart:
0 1 2 3 4
elements du tableau apres modifTab:
0 2 4 6 8
valeur de a[3]: 6
valeur locale de x modifiee: 30
valeur de a[3] apres modifElem: 6
6.4.1 Une sage précaution : la déclaration « const »
On a vu que tout tableau fourni comme argument à une fonction était modifiable par celle-ci.
Pour éviter toute modification involontaire, on peut qualifier cet argument de « constant», comme
Intro_C
47
dans l'exemple suivant.
http://tab_ct.cpp: argument tableau constant
1 #include <iostream>
2 void modif ( const int []);
3 int main(){
4 int a[] = {1,2,3,4};
5 modif(a);
6 for (int i = 0; i < 4; i++)
7 cout << a[i] << "\t" ;
8 cout << endl;
9 return 0;
10 }
11 void modif (const int b[]){
12 for (int i = 0; i < 4; i++)
13 b[i] += 5; // impossible!
14 }
Ce programme, d'apparence correcte, ne sera pas compilé : la fonction
modif tente de modifier
les éléments du tableau
a qui est déclaré comme constant, aussi bien dans l'entête (ligne 2) que
dans la déclaration de la fonction (ligne 12).
6.5 Tableaux à plusieurs dimensions
Dans le jargon de l'informatique, le « nombre de dimensions » d'un tableau est le nombre de
ses indices. Dans la pratique, on rencontre très souvent des tableaux à deux indices (lignes et colonnes),
qu'il s'agisse de représenter des dépenses par rubriques et par mois, des notes par étudiant
et par matière ou les cases d'un jeu d'échec. La figure ci-dessous montre un tableau à 3 lignes et
quatre colonnes.
col. 0 col. 1 col. 2 col. 3
ligne 0 a[0][0] a[0][1] a[0][2] a[0][3]
ligne 1 a[1][0] a[1][1] a[1][2] a[1][3]
ligne 2 a[2][0] a[2][1] a[2][2] a[2][3]
Dans cet exemple, j'ai suivi la convention du langage C++ : les indices commencent en zéro et
donc le premier élément, en haut à gauche, est
a[0][0] . J'ai aussi suivi la convention de l'algèbre
linéaire : le premier indice d'un tableau désigne la ligne, le deuxième la colonne. Ceci n'est qu'une
convention (presque universelle) mais il importe de retenir que cela ne préjuge en rien de la façon
dont les éléments sont effectivement rangés en mémoire (ils sont en fait rangés à la queue-leu-leu,
ligne par ligne ; le compilateur s'y retrouve, grâce à la déclaration du tableau, qui précise le nombre
de lignes et de colonnes).
Un élément quelconque,
a[2][3] par exemple, est une variable ordinaire, qui peut être lue,
écrite ou manipulée. La surveillance des indices s'impose ; un indice trop grand peu correspondre
à un autre élément du tableau, à une autre zone mémoire, ou à n'importe quoi.
La déclaration du tableau précédent se fait comme ceci :
int a[3][4];
Le nombre d'éléments par ligne ou colonne peut aussi être défini dans une directive du préprocesseur
ou comme une constante entière :
Intro_C
48
const int nligne = 6, ncol = 5;
.........
double T[nligne][ncol];
L'initialisation d'un tableau peut se faire au moment de la déclaration, comme dans le cas d'un
seul indice : on donne les éléments ligne par ligne, groupés au besoin à l'intérieur d'accolades. Le
compilateur initialise à zéro les éléments pour lesquels on ne fournit pas de valeur. Voici un exemple
simple de création et de manipulation de tableaux.
1
// tab2d . cpp : a f f i c h a g e de t a b l e a u x d i v e r s
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
const int NLG = 2 ;
6
const int NCL = 3 ;
7
void Af f i c h e ( char [ ] , const int [ ] [NCL] ) ;
8
int main ( ) {
9
int tab1 [NLG] [NCL] = {{ 1 , 2 , 3 } , { 7 , 8 , 9} } ,
10 tab2 [NLG] [NCL] = { 1 , 2 , 3 , 4 , 5 } ,
11 tab3 [NLG] [NCL] = {{4 ,5} ,{8}} ,
12 tab4 [NLG] [NCL] ;
13 Af f i c h e ( " tabl eau 1" , tab1 ) ;
14 Af f i c h e ( " tabl eau 2" , tab2 ) ;
15 Af f i c h e ( " tabl eau 3" , tab3 ) ;
16
for ( int l = 0 ; l < NLG; l++)
17
for ( int c = 0 ; c < NCL; c++)
18 tab4 [ l ] [ c ] = tab1 [ l ] [ c ] + tab2 [ l ] [ c ] + tab3 [ l ] [ c ] ;
19 Af f i c h e ( " tabl eau 4" , tab4 ) ;
20 system( " pause " ) ;
21
return 0 ;
22 }
23
void Af f i c h e ( char ph [ ] , const int a [ ] [NCL] ) {
24 cout << "\ t " << ph << endl ;
25 cout << "\ t " ;
26
for ( int c = 0 ; c < NCL; c++)
27 cout << c << "\ t " ;
28 cout << endl << endl ;
29
for ( int l = 0 ; l < NLG; l++){
30 cout << l << "\ t " ;
31
for ( int c = 0 ; c < NCL; c++)
32 cout << a [ l ] [ c ] <<"\ t " ;
33 cout << endl ;
34 }
35 cout << endl << endl ;
36 }
J'ai défini quatre tableaux, dont trois ont été plus ou moins complètement initialisés au même
moment. La fonction
Affiche ne fait que cela : afficher à l'écran le contenu de ces tableaux. Elle
utilise deux boucles emboîtées pour écrire les éléments, ligne par ligne. Les lignes 14,15,16 du
programme principal réalisent la somme, élément par élément, de trois tableaux, encore à l'aide
d'une double boucle. Plus surprenant peut-être, l'entête et la déclaration de la fonction
Affiche ne
contiennent pas l'indice (unique) de
ph , un tableau de caractères ni le premier indice du tableau à
deux dimensions : le compilateur peut reconstituer cette information (grâce aux variables globales
NCL, NLG
et la fonction est un peu plus générale comme cela et un peu plus compacte, un avantage
appréciable aux yeux des fanatiques.
Intro_C
49
6.6 Chaînes
6.6.1 caractères
Je rappelle que C++ définit le type caractère comme dans
char a = 'z'; qui définit une
variable
a de type char et l'initialise à la valeur de la constante 'z' . En fait, en interne, ce 'z'
est représenté (codé) par l'entier 122. Ceci fait que la distinction entre caractères et entiers sans
signe (compris entre 0 et 255) est assez ténue.
La correspondance entre caractères et codes est explicitée dans la «table ASCII». Cette table
représente le système de codage le plus fréquent, mais pas le seul (EBCDIC est utilisé par IBM).
Dans le système ASCII, les chiffres sont codés de 48 à 57, les majuscules de 65 à 90 et les minuscules
de 97 à 122. Divers signes d'imprimerie et des caractères non imprimables remplissent les autres
cases. De 128 à 255, on est dans un no man's land où toutes les conventions sont permises.
L'ordinateur peut donc classer des caractères (et aussi des chaines)par ordre alphabétique : il
lui suffit de classer par ordre de code croissant. Remarquez que A (majuscule) viendra avant a
(minuscule).
6.6.2 Définition de chaines
Une ensemble de caractères considérés comme un tout constitue une «constante chaine de
caractères». Elle s'écrit entre guillements. Les chaines de caractères sont en C/C++ des êtres
un peu hybrides, des tableaux avec des caractéristiques particulières. Les éléments d'un tableau
doivent être tous de même type, mais ce type peut être quelconque, par exemple le type
char . En
fait, une chaîne de caractères comme
bonjour ! * est considérée par C/C++ comme un tableau
de caractères TERMINÉ PAR LE CARACTÈRE «nul»,
'\0' (pas l'entier zéro mais le caractère
de code ASCII 0). La taille réelle de ce tableau est donc nombre-de-caractères + 1.
Attention :
Une chaine telle qu'elle vient d'etre définie est héritée du C. Sa construction est
complètement différente celle de la structure «string» décrite au chapitre précédent. Seules les
chaines de style C sont permises comme noms de fichiers. Rappel : On convertit une «string»
S en
une «chaine» par l'instruction
s = S.c_str()
6.6.3 déclaration et initialisation
Je peux déclarer et initialiser un tableau de caractères comme ceci :
char string[] = "bonjour !"
Ce tableau comporte
dix éléments . En effet, le compilateur ajoute automatiquement à la fin de
cette chaîne le « caractère nul ». Je peux tout aussi bien considérer la chaîne comme un tableau
de caractères individuels, que j'initialise comme un tableau, en fournissant moi-même le caractère
nul :
char string[] = {'b','o','n','j','o','u','r',' ','!','\0'}
Vous voyez que, dans ce formalisme, un caractère ne peut être confondu avec une chaîne de longueur
un (ce que l'on fait en Pascal).
6.6.4 exemple
On dispose encore d'autres méthodes pour initialiser une variable chaine. Certaines sont identiques
à ce que l'on a vu pour les tableaux standards. Voici quelques exemple de manipulation de
chaînes.
Intro_C
50
1
// chaine1 . cpp : d e f i n i t i o n s de chaine s
2
#include <ios t ream>
3
#include <cctype>
4
#include <c s t d l i b >
5
using namespace s td ;
6
int main ( void ){
7
char ch1 [ ] = " Li c enc e " , ch2 [ 4 0 ] ;
8
char ch3 [ ] = { 'm' , ' a ' , ' i ' , ' t ' , ' r ' , ' i ' , ' s ' , ' e ' , ' \0 ' } ;
9
char ch4 [ 1 0 ] ; char ? ch5 = " t ruc " ;
10 cout << " e n t r e r un mot \n" ;
11 c in >> ch2 ;
12
for ( char i = 0 ; i < 9 ; i++)
13 ch4 [ i ] = i + 4 8 ;
14 ch4 [ 9 ] = ' \0 ' ;
15 cout << ch1 << "
??? " << endl ;
16 cout << ch2 << "
??? " << endl ;
17 cout << ch3 << "
??? " << endl ;
18 cout << ch4 << "
??? " << endl ;
19 ch1 [ 1 ] = ' y ' ;
20 cout << ch1 << endl ;
21 ch1 [1]++;
22 cout << ch1 << endl ;
23
for ( int i = 0 ; i <= 8 ; i++) // imprime l e c a r a c t e r e nul
24 cout << ch3 [ i ] ;
25 cout << "
??? " << endl ;
26
for ( int i = 0 ; i < 7 ; i++)
27 ch1 [ i ] = toupper ( ch1 [ i ] ) ;
28 cout << ch1 << endl ;
29 cout << s t r l e n ( ch1 ) << ' \ t ' << s t r l e n ( ch2 )
30 << ' \ t ' << s t r l e n ( ch3 ) << ' \ t ' << s t r l e n ( ch4 ) << endl ;
31 ch1 [ 1 ] = ' i ' ;
32
for ( int i = 0 ; i < 7 ; i++)
33 ch1 [ i ] = tolowe r ( ch1 [ i ] ) ;
34 cout << ch1 << endl ;
35 cout << ch5 << endl ;
36
// ch5 [ 0 ] = 'T ' ; mo d i f i c a t i on d ' une chaine cons tant e
37
// ===> p l ant e Dev ¡ C++
38
// cout << ch5 << endl ;
39 system( " pause " ) ;
40 }
Ligne 3, j'appelle une nouvelle bibliothèque héritée du C, qui contient les fonctions
toupper()
(conversion en majuscules) et
tolower() (conversion en minuscules). La ligne 7 contient deux
définitions de chaines, comme tableaux. L'une est initialisée au moment de la déclaration (c'est
le compilateur qui compte le nombre d'éléments), l'autre est lue au clavier (la chaine se termine
au premier caractère blanc ou à la ligne). Dans ces deux cas, le compilateur introduit lui-même le
caractère final
'\0' .
Je définis ensuite (ligne
une autre chaine, élément par élément : il m'incombe de placer le
caractère nul final.
ch4[10] est un tableau de caractères de 10 éléments, numérotés de 0 à 9. Ils
sont initialisés un par un lignes 12-14. Ici, je définis des caractères par leur code ASCII : ce sont
en fait les entiers de 0 à 8. C'est toujours à moi d'insérer
'\0' .
Enfin,
ch5 est définie par l'intermédiaire d'un pointeur, ce qui sera expliqué bientôt. Attention :
beaucoup de compilateurs considèrent qu'une chaine ainsi définie est une constante inviolable et
la range dans une zone spéciale de la mémoire. Toute tentative de modification (comme je le fais
ligne 36) provoque une erreur d'exécution.
Intro_C
51
J'affiche ensuite ces chaines, suivies d'étoiles, pour que l'on voit bien où elles s'arrètent. Je
me livre après à quelques manipulations élémentaires : modification d'un caractère (ligne 19),
incrémentation d'un caractère (ligne 21) (incrémentation de son code ASCII en fait, donc passage
au caractère suivant). Ligne 22, je frole la catastrophe, en dépassant de un la longueur de la chaine.
On montre ensuite l'effet des deux fonctions
tolower et toupper , puis l'usage de strlen() , qui
renvoie la longueur de la chaine passée en argument, SANS COMPTER LE CARACTÈRE NUL.
entrer un mot
programme
Licence***
programme***
maitrise***
012345678***
Lycence
Lzcence
maitrise ***
LZCENCE
7 9 8 9
licence
truc1
C++ comprend (dans la bibliothèque
<cstring> ) un grand nombre d'autres fonctions de manipulation
de chaines, qui généralement font intervenir des pointeurs et seront abordées plus tard.
6.7 enregistrement
Un enregistrement est une structure de données qui permet de rassembler des variables de types
différents mais ayant un point commun, comme les renseignements figurant sur une carte d'identité
ou une fiche de bibliothèque. En Pascal, on parle de
RECORD , en C/C++ il s'agit de struct .
6.7.1 définition
Avant d'utiliser une de ces structutres, il faut la définir : il s'agit d'un type nouveau, sorti
de l'imagination du programmeur, mais tout aussi valable qu'un
float ou un char . Voici une
définition d'une «struct» destinée à représenter l'heure
struct HMS {
int heure;
int minute;
int seconde;
};
Un enregistrement de type
HMS contient trois «champs» (ou trois «membres») de type entier ; le
nombre et la nature des champs ne sont pas limités, sauf qu'une
struct ne peut pas contenir une
struct
de même nature. Notez le point-virgule final de la déclaration.
Ayant défini un type, je peux maintenant déclarer des variables (des objets ou des identificateurs)
de ce type, comme ceci :
HMS epoque, lediner, tabHMS[10];
qui réserve de la place en mémoire pour 12 enregistrements de type
HMS , dont 10 sont groupés dans
un tableau.
Intro_C
52
6.7.2 accès aux champs
Pour initialiser les champs des
struct que l'on vient de définir, on utilise le nom de l'enregistrement
suivi d'un point et du nom du champ ; on fait de même pour lire, écrire ou calculer avec le
contenu d'un champ. L'initialisation peut aussi se faire comme pour un tableau, avec des valeurs
entre accolades.
lediner.heure = 19; lediner.minute = 30;
epoque = {6,25,45};
epoque.heure = lediner.heure - 12;
cin >> epoque.minute;
cout << "heure du diner: " << lediner.heure <<" heure "
<< lediner.minute << endl;
6.7.3 exemple
Dans le programme qui suit, je mets en oeuvre quelques opérations d'arithmétique en nombres
complexes ; chaque nombre est représenté par une
struct , avec deux champs, une partie réelle et
une partie complexe. Chaque fonction a le type
COMPLEXE et renvoie donc DEUX valeurs vers le
programme appelant.
1
#include <ios t ream>
2
#include <c s t d l i b >
3
struct COMPLEXE{
4
double r e ;
5
double im;
6 } ;
7 COMPLEXE add (COMPLEXE a , COMPLEXE b){
8 COMPLEXE somme ;
9 somme . r e = a . r e + b . r e ;
10 somme . im = a . im + b . im;
11
return somme ;
12 }
13 COMPLEXE mult (COMPLEXE a , COMPLEXE b){
14 COMPLEXE prod ;
15 prod . r e = a . r e
? b . r e ¡ a . im ? b . im;
16 prod . im = a . r e
? b . im + a . im ? b . r e ;
17
return prod ;
18 }
19
void modif (COMPLEXE a ){
20 a . r e
? = 1 0 ; a . im ? = 1 0 ;
21 }
22
int main ( ) {
23 COMPLEXE u = {1 ,0} , v = {0 ,1} , s , p ;
24 s = add (u , v ) ;
25 cout << s . r e << ' \ t ' << s . im << endl ;
26 p = mult (u , v ) ;
27 cout << p . r e << ' \ t ' << p . im << endl ;
28 modif (u ) ;
29 cout << u . r e << ' \ t ' << u . im << endl ;
30 system( " pause " ) ;
31
return 0 ;
32 }
Ce programme affiche
1 1
Intro_C
53
0 1
1 0
Vous remarquez que la fonction
\modif n'a pas eu d'effet sur la variable u : les arguments de type
struct
, contrairement aux tableaux, sont transmis pas valeur.
Chapitre 7
Les Pointeurs
Le pointeur est un type défini en C/C++, tout comme en Pascal (et en Fortran depuis 1990).
C'est une caractéristique à la fois puissante et difficile du langage. Un pointeur permet de simuler
l'appel par référence, de créer et de manipuler des structures dynamiques et est largement utilisé
dans la programmation par objet. Ici, je ne fais qu'introduire le sujet et montrer les liens qui
existent entre pointeur, tableau et chaîne de caractères. Une variable ordinaire contient une valeur ;
un pointeur, au contraire, contient l'adresse d'une autre variable qui, elle, contient une valeur. On
dit que la variable habituelle est une référence directe à une valeur alors que le pointeur est une
référence indirecte.
7.1 Déclaration et initialisation
7.1.1 déclaration et notation
Comme toute autre variable, un pointeur doit être déclaré avant usage.
int a, *Pb, c = 3;
déclare un entier
a , un pointeur vers un entier Pb et un entier c initialisé à 3. Cette déclaration
se lit comme «a est un entier, Pb est un pointeur vers un entier, c est un entier de valeur 3» (la
parti pointeur se lit «à l'envers»). L'opérateur * n'est pas distributif, il faut l'écrire pour chaque
variable de type pointeur :
double x, *Py, *Pz;
déclare deux pointeurs (
Py, Pz ) vers des nombres en double précision. Pour limiter les risques
d'erreur, il est commode de donner aux pointeurs des noms aisément reconnaissables, comportant
par exemple les caractères
p ou ptr . On peut dire qu'il existe deux opérateurs homonymes :
l'opérateur de multiplication et l'opérateur d'indirection, représentés tous deux par une étoile.
7.1.2 Opérateur adresse et initialisation d'un pointeur
L'opérateur «adresse» & en C++, renvoie l'adresse de son unique opérande. Le fragment de
code suivant affecte à un pointeur l'adresse d'une variable :
int x = 5;
int *xP;
xP = &x;
54
Intro_C
55
J'ai déclaré et initialisé un entier
x , puis un pointeur (adresse) vers un entier xP et j'ai affecté à
xP
une valeur, l'adresse de x . On dit que xP «pointe» vers x . Si on pouvait lire le contenu de la
mémoire au moment de l'exécution de ce code, on verrait quelque chose comme
adresse contenu nom de la variable
320000 530000
xP
530000 5
x
Attention :
ces adresses, imaginaires et données à titre d'exemple, ne sont, de toute façon, pas
immuables : elles vont varier d'une machine à l'autre, d'une exécution à l'autre. Et c'est très bien
ainsi ! Vous verrez que la valeur absolue d'une adresse n'a pas d'intérêt pour le programmeur.
Attention :
déclarer un pointeur n'est en rien équivalent à réserver en mémoire de la place pour
l'objet vers lequel il pointe (pensez à un hôtelier qui vous indique un numéro de chambre dans une
aile de bâtiment qui sera construite dans deux ans).
7.1.3 l'opérateur «*»
Je répète ce que j'ai dit au paragraphe précédent, de façon plus abstraite (ou prétentieuse).
L'opérateur
* (opérateur d'indirection ou de «déréférencement») appliqué à un argument de type
pointeur, renvoie un synonyme de la variable désignée par ce pointeur.
cout << *xP << endl;
affiche la valeur de
x (si l'association entre x et xP est comme déclarée plus haut), pratiquement
comme le fait
cout << x << endl; . J'ai «déréférencé» le pointeur xP . Attention : déréférencer un
pointeur qui ne pointe sur rien provoque le plantage du programme.
On peut donc lire
*xP comme «contenu de l'adresse désignée par xP » et cette interprétation
est confirmée par l'utilisation de l'opérateur que je fais maintenant :
*xP = -100;
cout << *xP;
J'affecte à
x (la variable pointée par xP ) la valeur -100 et je l'imprime.
Quand on les applique à une variable de type pointeur, les opérateurs * et & sont inverses l'un
de l'autre et commutent entre eux, comme je le montre ci-dessous,
1
// point eur1 . cpp : p r o p r i é t é s de ? e t &
2
#include <ios t ream>
3
#include <c s t d l i b >
4
using namespace s td ;
5
int main ( void ){
6
int a , ? aP;
7 a = 7 ;
8 aP = &a ;
9 cout << " adr e s s e de a : " << &a ;
10 cout << "\ nval eur de aP: " << aP;
11 cout << "\ nval eur de a : " << a ;
12 cout << "\ nval eur de
? aP: " << ? aP;
13
? aP = ¡ 36;
14 cout << "\ nval eur de a : " << a ;
15 cout << "\ nval eur de
? aP: " << ? aP;
16 cout << "\n&
? aP: " << & ? aP;
17 cout << "\n
? &aP: " << ? &aP;
18 system( " pause " ) ;
19
return 0 ;
20 }
Intro_C
56
avec les résultats
adresse de a: 0x22ff7c
valeur de aP: 0x22ff7c
valeur de a: 7
valeur de *aP: 7
valeur de a: -36
valeur de *aP: -36
&*aP: 0x22ff7c
*&aP: 0x22ff7c
7.2 Passage d'argument par adresse
Nous avons rencontré quatre façons de faire circuler de l'information entre une fonction «appelée
» et un programme principal «appelant» : déclarer des variables communes au niveau global,
renvoyer une valeur unique par l'instruction
return , « partager » un tableau avec le programme
et enfin, mettre à disposition de la fonction des variables de
main sous forme de référence. Une
autre méthode, plus générale, de transmission par adresse (référence) fait appel à un pointeur
(méthode commune à C et C++). L'exemple suivant met en oeuvre les trois dernières méthodes
de transmission d'une valeur.
1
#include <ios t ream>
2
#include <c s t d l i b >
3
int cubeVal ( int x ){
4
return x ? x ? x ;
5 }
6
void cubeRef ( int & x ){
7 x = x
? x ? x ;
8 }
9
void cubePtr ( int ? xP){
10
? xP = ? xP ? ? xP ? ? xP;
11 }
12
using namespace s td ;
13
int main ( void ){
14
int nb = ¡ 5;
15 cout << "nb au début : " << nb << endl ;
16 nb = cubeVal (nb ) ;
17 cout << "nb apr è s cubeVal : " << nb << endl ;
18 nb =
¡ 5;
19 cubeRef (nb ) ;
20 cout << "nb apr è s cubeRef : " << nb << endl ;
21 nb =
¡ 5;
22 cubePtr (&nb ) ;
23 cout << "nb apr è s cubePtr : " << nb << endl ;
24 system( " pause " ) ;
25
return 0 ;
26 }
La seule nouveauté est le passage par un pointeur (lignes 9-10, 22). Le calcul du cube(ligne 10)
est délibérément obscur ; il fonctionne parce que l'opérateur de déréférencement (*) a une priorité
beaucoup plus élevée que l'opérateur de multiplication (*) : il est donc évalué en premier. En
pratique, on mettrait assez de parenthèses pour lever toute ambiguïté.
Intro_C
57
7.3 Pointeur constant et pointeur sur une constante
Une variable qualifiée de
const ne peut pas être modifiée par le programme : cela constitue
une bonne méthode pour sécuriser le programme. Ce qualificatif peut également s'appliquer à un
pointeur et aussi à une variable désignée par un pointeur.
On dispose en fait de quatre possibilités : pointeur vers une variable, pointeur vers une constante,
pointeur constant vers une variable, pointeur constant vers une constante. La première n'apporte
aucune restriction, les autres sont plus contraignantes. Les déclarations pourraient être les suivantes.
char * cP; //pointeur vers un caractère
const int *Pi // Pi est un pointeur sur un entier constant
float * const Px // Px est un pointeur constant
// vers un nombre fractionnaire
const double * const Ptruc // pointeur constant vers un
// réel double précision constant
Il appartient au programmeur de faire le meilleur usage de ces possibilités.
7.4 Arithmétique sur les pointeurs
On ne peut faire sur les pointeurs que quelques opérations arithmétiques (ce qui est assez évident
si l'on se souvient qu'il s'agit d'adresses) : incrémentation (
++ ), décrémentation ( -- ), addition
(
+,+= ) ou soustraction ( -, -= ) d'un entier. L'intérêt de telles opérations apparaîtra au paragraphe
suivant. Il ne s'agit pas d'une addition (soustraction) simple. En effet, supposons que
aP soit un
pointeur d'entier de valeur 40096, où 40096 désigne l'emplacement d'un OCTET en mémoire.
aP+1
désigne l'emplacement suivant pour un entier, soit 40100 parce que, en général, un entier occupe
4 octets. Ces considérations n'ont de sens que si
*aP et *(aP+1) sont tous les deux des entiers.
7.5 Pointeurs et tableaux
En C/C++, tableaux et pointeurs sont des entités très proches. En fait,
le nom d'un tableau
est un pointeur constant vers le premier élément du tableau (indice 0)
; autrement dit, le nom du
tableau est l'adresse (constante) de son premier élément.
Soient les déclarations
int b[5]; int * bP; . Le nom du tableau est l'adresse (un pointeur
vers) le premier élément. Je peut donc écrire
bP = b;
ce qui est équivalent à
bP = &b[0];
L'élément d'indice 3 peut maintenant être désigné comme
*(bP+3);
L'entier 3 est un «décalage» («offset») pour le pointeur. Les parenthèses sont rendues nécessaires
par la priorité de l'opérateur * ; l'expression
*bP + 3 ajoute 3 à la valeur désignée par bP (et donc
calcule
b[0] + 3 ).
L'adresse de l'élément d'indice 3 peut s'écrire
&b[3] ou bP + 3 . Le nom du tableau étant un
pointeur, on peut écrire, symétriquement,
*(b+3) pour désigner ce même élément.
Enfin, un pointeur peut être muni d'un indice, comme
pB[1] qui désigne le deuxième élément du
tableau, tout comme
b[1] . Vous voyez pourquoi, dans le jargon du C/C++, on parle «d'opérateur
crochet» : le brave pointeur
b , affublé de crochets, est transformé aussitôt en tableau.
Intro_C
58
Attention :
une écriture comme b += 2; est impossible, puisqu'elle tend à modifier un pointeur
constant.
En général, un programme qui manipule des tableaux est plus lisible lorsqu'il utilise des indices
plutôt que des pointeurs.
Le programme qui suit illustre les différentes méthodes d'accès aux éléments d'un tableau.
1
// tab_pt r . cpp : d i f f é r e n t e s méthodes pour accéde r
2
// aux é l ément s d 'un t a b l e a u
3
#include <ios t ream>
4
#include <c s t d l i b >
5
using namespace s td ;
6
int main ( void ){
7
int a [ ] = { ¡ 40 , ¡ 30 , ¡ 20 , ¡ 10 ,0}; //a e s t un t a b l e a u d ' e n t i e r s
8
int ? Pa = a ; //Pa e s t un p o int eur d ' ent i e r , qui d é s i gne a
9
int i , de c l g ;
10 cout << "\navec un i n d i c e " << endl ;
11
for ( i = 0 ; i < 5 ; i++)
12 cout << "a [ " << i << " ] = "<< a [ i ] << ' \ t ' ;
13 cout << "\navec un po int eur (nom du tabl eau ) e t un dé c a l a g e \n" ;
14
for ( de c l g = 0 ; de c l g < 5 ; de c l g++)
15 cout << "
? ( a+" << de c l g << " ) = "<< ? ( a+de c l g ) << ' \ t ' ;
16 cout << "\navec un po int eur e t un i n d i c e \n" ;
17
for ( i = 0 ; i < 5 ; i++)
18 cout << "Pa [ " << i << " ] = "<< Pa [ i ] << ' \ t ' ;
19 cout << "\navec un po int eur e t un dé c a l a g e \n" ;
20
for ( de c l g = 0 ; de c l g < 5 ; de c l g++)
21 cout << "
? (Pa+" << de c l g << " ) = "<< ? (Pa+de c l g ) << ' \ t ' ;
22 cout << endl ;
23 system( " pause " ) ;
24
return 0 ;
25 }
7.6 Pointeurs et chaînes
Une chaîne de caractères est un tableau : son nom est donc un pointeur constant vers l'adresse du
premier caractère. Les deux représentations sont en gros équivalentes, sauf qu'une chaîne désignée
par un pointeur est souvent considérée par le compilateur comme une constante non modifiable.
Rappel de déclarations possibles :
char vin[] = "bordeaux";
char *Pvin = "bourgogne";
char vin[] = {'c','h','i','n','o','n','\0'};
Je cite maintenant un certain nombre de fonctions de la bibliothèque
cstring> permettant la
manipulation de chaînes ; je donne d'abord le prototype, puis une description.
char * strcpy(char *s1, const char *s2)
copie la chaîne s2 dans le tableau s1 et renvoie la
valeur de
s1 .
char * strncpy(char *s1, const char *s2, int n)
copie au plus n caractères de s2 dans s1 et
renvoie
s1 .
chr * strcat(char *s1, const char *s2)
ajoute s2 à la fin de s1 ; le premier caractère de s2
écrase le 0 terminal de
s1 et renvoie la valeur de s1 .
Intro_C
59
chr * strncat(char *s1, const char *s2, int n)
ajoute au plus n caractères de s2 à la fin de
s1
; le premier caractère de s2 écrase le 0 terminal de s1 et renvoie la valeur de s1 .
int strcomp(char *s1, const char *s2)
compare s1 à s2 et renvoie une valeur nulle, négative
ou positive selon que
s1 est égale, plus petite ou plus grande que s2 .
int strncomp(char *s1, char *s2, int n)
compare au plus n caractères de s1 à s2 et renvoie
une valeur nulle, négative ou positive selon que
s1 est égale, plus petite ou plus grande que
s2
.
int strlen(const char *s)
renvoie le nombre de caractères non-nuls de l'argument.
Attention aux longueurs de chaînes lorsque vous utilisez
strcpy ; cette fonction copie le second
argument (avec son zéro) dans le premier, qui doit être assez grand pour recevoir le tout. Dans le
cas de
strncpy , il faut encore s'assurer que n est assez grand pour que le zéro terminal de s2 soit
recopié.
7.7 Et les «strings» ?
Soit la déclaration/initialisation
string ch1 = "que voici une belle petite chaine";
La norme C++ impose les propriétés de
ch1 mais ne dit rien sur la façon de les réaliser «à
l'intérieur» du compilateur. Une string n'est pas un vecteur, ne se termine pas par le caractère
nul. Son nom n'est pas un pointeur, et l'écriture
&ch1[0] n'a pas de signification. Il reste vrai que
ch1[10]
est le caractère «u».
Toutes les fonctions citées au paragraphe précédent ont leur équivalent pour les «strings».
Si je déclare
ch2 = "bonjour" , je peux comparer deux «strings» ; la condition ch1 > ch2 est
vraie (ordre lexicographique plus élevé) et
ch1 == ch2 est fausse.
cout << ch1.substr(10,3)
imprime une (la sous-chaîne qui commence au caractère de rang
10 et comporte 3 lettres).
J'emploie
ch1.find("petite") pour découvrir à quel rang commence la sous-chaine «petite».
Enfin, il est facile d'insérer des caractères dans une «string» ; l'instruction
ch1.insert(9,"voila ")
injectera «voila » après voici (après le caractère blanc de rang 9).
7.8 pointeurs et «struct»
Si j'ai défini un type d'enregistrement et si j'ai déclaré des variables de ce type, je peux déclarer
des pointeurs vers ces variables. De plus, un (ou plusieurs) des champs de l'enregistrement peut
être lui-meme un pointeur vers un objet quelconque (en particulier un objet de même type). Ces
remarques rendent possibles la contruction de structures de données extrèmement riches et variées
(listes, piles, queues, . . .) qui ne seront pas décrites ici. Je me contente de préciser quelques points
de syntaxe. Examinons les déclarations suivantes.
struct cpx {double re; double im;};
cpx u,v, *Pcpx, &Refcpx = u, Tcpx[123];
J'ai défini une
struct qui mime un nombre complexe, u et v sont deux exemplaires de cette
structure,
Pcpx est un pointeur vers un complexe, Refcpx est un pseudonyme de u (défini par
référence) et
Tcpx est un tableau de «nombres» complexes.
Pour le moment, le pointeur ne pointe sur rien. Je l'initialise et j'imprime ses deux champs
comme ceci par exemple
Pcpx = &v;
cout << (*Pcpx).re << '\t' << Pcpx->im << endl;
Intro_C
60
Les deux manières d'accéder aux champs de l'enregistrement pointé par
Pcpx sont équivalentes.
Les parenthèses de la première sont obligatoires parce que l'opérateur «point» a une priorité plus
élevée que l'opérateur de déréférencement ; la seconde est peut-être plus claire et plus concise : elle
a la faveur des spécialistes.