Introduction à la Programmation C++
Introduction à la Programmation C++
Mathématiques, Master 2
Université d’Orléans
Thomas Haberkorn
2015
ii
Table des matières
Préambule 1
1 Introduction au Langage C 3
1.1 Petit historique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Créer un Programme en Langage C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3 Types de Données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4 Les Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 Les Opérateurs en C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.6 Les Structures Conditionnelles et de Boucle . . . . . . . . . . . . . . . . . . . . . . . . 15
1.7 Types de Données Complexes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.8 Les Pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.9 Les Fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.10 Bibliothèques Standards du C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
iii
iv TABLE DES MATIÈRES
Préambule
Le langage C++ est abondamment utilisé dans le monde de l’industrie et de la recherche. C’est
un langage permettant de développer rapidement des applications complexes tout en assurant une
certaine robustesse et flexibilité.
Le C++ est avant tout une surcharge du langage C auquel on a rajouté les fonctionnalités de la
programmation orienté objet. Ce document commence donc par une introduction au C qui est un lan-
gage purement fonctionnel (par opposition aux langages objets). Puis il continue par une introduction
au C++ et à ses fonctionnalités objets.
Ce document n’est qu’une introduction et n’a donc pas la prétention de rentrer dans les subtilités
de la programmation C++. De plus, comme pour tout langage de programmation, la seule façon de
maı̂triser le C++ est de l’utiliser.
1
2 PRÉAMBULE
Chapitre 1
Introduction au Langage C
3
4 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Le Fichier Source
La recette de cuisine qu’est un programme, s’écrit tout simplement dans un (ou des) fichier(s)
texte(s) sous leur plus simple expression. Ce fichier est appelé fichier source et ne doit comporter que
des caractères ASCII, ie. pas de caractères spéciaux, pas d’italiques, pas de gras, pas de soulignés...
Il existe cependant des éditeurs de textes aidant l’écriture de programme C en mettant en page les
instructions, déclarations, fonctions.
Pour être valide, le fichier source doit posséder l’extension .c et comporter un programme main
(). Un exemple de programme contenu dans un fichier source est le suivant :
Exemple 1.2.1 (Programme Aloha Monde).
#include <stdio.h>
int main(){
printf("Aloha Monde!\n");
return 0;}
Nous reviendrons plus tard, dans des sections dédiées, à la signification des parties de ce premier
petit programme. On peut cependant préciser la structure du fichier source d’un programme de façon
générale. Un fichier source comporte :
— Déclaration des interfaces des fonctions qui seront utilisées dans le programme (ici, le # include
<stdio.h>, car la fonction printf utilisée est une fonction usuelle du C ).
— Déclaration des variables globales (il n’y en a pas ici).
— fonction main, qui renvoie obligatoirement un entier, d’où le int main() et qui peut éventuellement
avoir des arguments, cf. la section 1.9 pour le corps de la fonction. La fonction main est le point
d’entrée du programme, c’est toujours elle que le programme exécutera quand on l’appellera.
— d’autres fonctions qui la plupart du temps seront appelées dans le corps de main.
Remarque 1.2.1. En fait, en C ANSI, il n’est pas obligatoire de déclarer le type de la fonction main,
mais ce sera le cas en C++, donc ne l’oubliez pas. De plus, si vous forcez main à renvoyer autre chose
qu’un entier, vous aurez au mieux un warning lors de la compilation du fichier source, au pire une
erreur de compilation.
Attention, le C est sensible à la casse (distingue majuscules et minuscules) donc main n’est pas la
même chose que MAIN ou Main ; et c’est bien une fonction main qu’il faut absolument dans le fichier
source.
Lorsque vous écrivez un programme d’une certaine taille, il est très fortement conseillé d’y ajouter
des commentaires afin de rendre moins hermétique le code et de permettre d’éventuelles retouches/évo-
lutions une fois que vous (ou un autre) auront oublié comment est exactement fait le code. En C,
un commentaire est signalé par les balises (un peu dans l’idée du XML) /* et */. Tout ce qui se
situera entre ces 2 balises sera traité comme un commentaire, ie. sera ignoré. Attention, il est interdit
d’imbriquer des commentaires (ce qui ne sert à rien) en ouvrant deux fois par /*. De même, le
commentaire ne peut contenir la fin de balise */, sinon ce serait compris comme la fin du commentaire.
Il est de plus interdit de couper un mot par un commentaire. Un exemple de commentaires est le
suivant :
Exemple 1.2.2 (Exemples de commentaire).
x = 2; /* ceci est un commentaire */
y = 3*sin(x); /* Ceci est un autre commentaire ,
qui prend plus d’une ligne */
z = x*x + x*y; /* Ce commentaire n’est malheureusement
pas */ valide et provoquera une erreur */
var/* c’est mal */pasbonne = z*z + x*y*z;
1.2. CRÉER UN PROGRAMME EN LANGAGE C 5
Comme vous avez pu le remarquer sur les exemples précédents, les instructions (l’appel à printf ou
encore les opérations arithmétiques) sont toutes terminées par ’ ;’. Ceci est obligatoire pour signifier
au compilateur que le texte situer avant le ’ ;’ représente un tout.
La Compilation
Une fois le fichier source créé, il faut le transcrire du langage C dans un langage que comprend
l’ordinateur. Ce langage est le langage machine et la transcription se fait par l’étape de compilation.
Pour donner une idée, ce que fait le compilateur est d’analyser le fichier source, de reconnaı̂tre les
petits bouts d’instructions qui le compose, éventuellement de les réorganiser de façon plus optimale,
puis de réécrire ces petits bouts en langage machine (à noter qu’un fichier en langage machine est
illisible).
La création du programme exécutable à partir du fichier source nécessite, à proprement parler, plus
que la seule étape de compilation. En effet, avant de compiler le fichier source, on lance dessus un pré-
processeur qui lui aussi réécrit le fichier source en vrai langage C, ie. sans les quelques petits raccourcis
de notations permis au programmateur. Ces racourcis sont toutes les lignes de codes commençant par
le caractère #. On a déjà vu le # include <nom header.h> qui copie le fichier nom header.h dans le
fichier source. En général ce fichier contient des déclarations d’interfaces de fonctions (appelées plus
communément prototypes). On verra par la suite d’autres instructions qui seront préprocessées.
Remarque 1.2.2. On notera que les instructions à préprocesser ne se terminent pas par un ’ ;’, étant
donné que ce ’ ;’ est là pour le compilateur, pas le préprocesseur, qui lui mettra les ’ ;’ où il faut.
Une fois que le préprocesseur a fait son œuvre, le compilateur fait la sienne et crée un fichier qu’on
appelle objet. Ce fichier, bien qu’étant en langage compréhensible par l’ordinateur, n’est pas encore
exécutable par celui-ci. En effet, dans le fichier objet manque toutes les fonctions C prédéfinies que
le programmateur a utilisé. Ces fonctions peuvent par exemple se trouver dans des librairies déjà
compilées et notre fichier exécutable se doit de connaı̂tre toutes ces fonctions. Pour ce faire on passe
par l’étape finale d’édition des liens qui consiste tout simplement à piocher dans les primitives C ou
dans les librairies que l’utilisateur précisera, pour compléter toutes les définitions qu’il nous manque.
Par exemple, pour notre programme aloha monde, la fonction printf ne sera véritablement écrite
(recopiée en fait) en langage machine que lors de la dernière étape.
La figure suivante représente les étapes de création d’un fichier exécutable à partir d’un fichier
source.
Imaginons que notre programme aloha monde soit écrit dans le fichier aloha.c. La création du fichier
exécutable aloha (sous UNIX, ou [Link] sous Windows) se fait par les commandes suivantes :
Exemple 1.2.3 (Compilation dans (presque) sa plus simple expression).
> gcc -c aloha.c
> gcc aloha.o -o aloha
> ./aloha
Aloha Monde!
La première commande lance le préprocesseur et le compilateur sur aloha.c, ce qui crée le fichier
objet aloha.o. La seconde commande fait l’édition des liens qui ici n’a à lier aloha.o qu’avec la définition
de printf qui est une primitive de C et donc il n’est nul besoin de préciser qu’il faut chercher cette
définition dans stdio (standard input output).
6 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Il existe d’autres types, notamment les très importants pointeurs, qui seront vus en section 1.8.
Par défaut, les entiers sont signés, ie qu’ils peuvent être négatifs. Pour représenter le signe, les
ordinateurs utilisent le bit de poids fort et le complément à deux. Celà signifie que pour représenter un
entier négatif, on représente sa valeur absolue en binaire (si le bit de poids fort n’est pas à 0, c’est que
l’entier est trop grand), on complémente cette représentation binaire puis on lui ajoute 1. Un exemple,
1.3. TYPES DE DONNÉES 7
Avec ce choix de représentation, si vous additionnez 1 à l’entier (int) 32767 (le plus grand entier
représentable dans ce type), vous n’obtiendrez pas 32768 (qui n’existe pas) mais -32768, dont la
représentation en binaire ne comporte que des 0 sauf sur le bit de poids fort (10 · · · 0).
L’avantage de cette représentation est que les additions se font bits à bits. Par exemple :
binaire
137 − 136 =⇒ 0000000010001001
+ 1111111101111000
= 0000000000000001
Ensuite, suivant que le type du nombre soit float, double ou long double, on a plus ou moins de bits
utilisés pour la mantisse et l’exposant (on a toujours le bit de signe en poids fort). Le tableau suivant
résume les conventions pour les 3 types :
int main() {
/* types entiers */
printf("La valeur maximum d’un ’short’ est: %d\n",SHRT_MAX);
printf("La valeur minimum d’un ’short’ est: %d\n",SHRT_MIN);
printf("La valeur maximum d’un ’int’ est: %d\n",INT_MAX);
printf("La valeur minimum d’un ’int’ est: %d\n",INT_MIN);
printf("La valeur maximum d’un ’unsigned int’ est: %u\n",UINT_MAX);
printf("La valeur maximum d’un ’long’ est: %ld\n",LONG_MAX);
printf("La valeur minimum d’un ’long’ est: %ld\n",LONG_MIN);
/* le type ’long long’ n’existe pas forcement sur votre machine */
/*
printf("La valeur maximum d’un ’long long’ est: %d\n",LLONG_MAX);
printf("La valeur minimum d’un ’long long’ est: %d\n",LLONG_MIN);
*/
/* types flottants */
printf("La valeur maximum d’un ’float’ est: %e\n",FLT_MAX);
printf("La plus petite valeur positive d’un ’float’ est: %e\n",FLT_MIN);
printf("Le plus petit epsilon de ’float’ est: %e\n",FLT_EPSILON);
printf("La valeur maximum d’un ’double’ est: %e\n",DBL_MAX);
printf("La plus petite valeur positive d’un ’double’ est: %e\n",DBL_MIN);
printf("Le plus petit epsilon de ’double’ est: %e\n",DBL_EPSILON);
printf("La valeur maximum d’un ’long double’ est: %le\n",LDBL_MAX);
printf("La plus petite valeur positive d’un ’long double’ est: %le\n",LDBL_MIN);
printf("Le plus petit epsilon de ’long double’ est: %le\n",LDBL_EPSILON);
return 0;
}
Dans cet exemple, on a abondamment utilisé la fonction printf avec des ’%d’, ’%f’ et ainsi de suite.
On reviendra plus tard sur l’utilisation de printf mais vous aurez compris que ces symboles sont là
pour être remplacer par le second argument de la fonction, par exemple, dans le premier printf, le ’%d’
sera remplacé par la valeur de SHRT MAX. Il est important d’utiliser le bon symbole en fonction du
type de la variable à afficher, comme on le répètera lorsque l’on parlera des fonction d’entrées/sorties.
Caractère (char)
Le type char, pour character en anglais, permet de stocker la valeur ASCII (American Standard
Code for Information Interchange) d’un caractère, qui est un entier entre 0 et 255 (pour le ASCII
étendu, 127 sinon). En réalité, le type char stocke par défaut un entier signé, donc entre -128 et 127,
ce qui ne veut bien entendu pas dire qu’un caractère à un signe.
Par exemple, le caractère ’1’ a pour code ASCII 49, ’a’ correspond à 97 (puis dans l’ordre), ’A’ à
65 (puis dans l’ordre). Plus exhaustivement, la table des caractères ASCII (non étendue) est donnée
dans le tableau suivant :
En C, on peut donner une valeur à un caractère soit entre ’ ’, soit par sa représentation ASCII,
comme ici :
Exemple 1.3.3 (Affectation d’une variable char).
char c;
c = 70; /* c = ’F’ */
c = ’2’; /* le caractere ’2’, pas l’entier */
1.3. TYPES DE DONNÉES 9
En C, il n’existe pas de type pour les chaı̂nes de caractères qui seront représentées soit par un
tableau de caractères, soit à l’aide d’un pointeur. Nous verrons ces 2 notions par la suite.
Conversion de type
Bien que C soit un langage typé, il l’est très faiblement, ie qu’il est très permissif dans la mani-
pulation des types. On peut par exemple sans problème (de compilation) additionner un int avec un
double avec un char. Suivant ce à quoi le résultat d’une telle opération sera affecté, une conversion
implicite sera faite.
Nous répétons une fois de plus que le C est sensible à la casse, donc les variables toto et Toto ne
sont pas les mêmes.
Avant de pouvoir utiliser une variable, il faut la déclarer, ie lui donner un nom et un type afin
qu’un espace mémoire puisse lui être réservé. Une variable se déclare tout simplement de la façon
suivante :
Exemple 1.4.1.
type Nom_de_la_variable;
type Nom_de_la_variable1, Nom_de_la_variable2, ... ;
Par exemple :
Exemple 1.4.2.
int a;
float x, y, z;
Par exemple, pour stocker la valeur ’a’ dans une variable Caractere de type char, on écrira :
Caractere = ’a’ ;
L’affectation se fait obligatoirement après la déclaration, il est interdit d’affecter une donnée à une
variable non déclarée. Par contre, on peut réaliser une affectation en même temps que la déclaration,
en suivant la syntaxe suivante :
Exemple 1.4.3.
char Caractere = ’a’;
int Entier = 1;
int Entier1 = 2, Entier2 = -3, Entier3 = 3, ...;
int Entiern = Entier1 + Entier2;
Noter qu’il faut affecter une valeur à une variable avant de l’utiliser, sinon son contenu est aléatoire
puisqu’il s’agira simplement de la représentation dans le type de la variable du champs de bit se
trouvant à l’endroit de la mémoire réservé lors de la déclaration.
Exemple 1.5.1.
int a = 1, b = 3, c;
float x;
c = a/b; /* division entiere => c = 0 */
x = a/b; /* division entiere, le resultat est un float=> x = 0.*/
x = ((float) a)/b; /* division non entiere=> x = 1.3333... */
x = a/((float) b); /* idem */
x = (1.0*a)/b; /* idem */
Noter que l’opérateur d’affectation ’=’ renvoie une valeur correspond à la donnée affectée. On peut
donc faire des affections en cascades :
a=b=c=1 ; <=> a=(b=(c=1)) <=> c=1 ; b=c ; a=c ; /* dans cet ordre */
Noter de plus que les opérateurs ++ et - - peuvent être post- et préfixés. S’ils sont mis avant la
variable sur laquelle ils s’appliquent, la valeur renvoyée par l’opération est celle de la variable après
incrémentation/décrémentation. S’ils sont postfixés, la valeur renvoyée par l’opération est celle de la
variable avant incrémentation/décrémentation. Ainsi, le programme suivant :
14 CHAPITRE 1. INTRODUCTION AU LANGAGE C
On notera que le langage C ne possède pas à proprement parler de type booléen et donc que le
résultat d’une opération de comparaison est un entier, soit nul soit égal à 1. En réalité, la convention
pour représenter un booléen est l’entier 0 pour FAUX et n’importe quoi sauf 0 pour VRAI.
Attention, une erreur fréquente lors d’un test d’égalité est d’utiliser l’opérateur d’affectation ’=’
au lieu de l’opérateur d’égalité ’==’. Une telle étourderie ne mènera pas à une erreur de compilation
puisque l’affectation renvoie bien un résultat, par contre le résultat du test ne sera pas celui escompté.
En plus de ces opérateurs bit à bit, on a aussi la possibilité de faire des rotations de bits sur
les entiers qui correspondent en fait à des division (décalage a droite) ou multiplication (décalage à
gauche) par 2. On peut faire plus d’une rotation dans chaque direction.
On notera qu’il ne s’agit pas exactement d’une rotation puisque pour la rotation à droite, le bit de
poids faible est perdu plutôt que de devenir le bit de poids fort. On notera de plus que ces opérateurs
traitent des champs de 32 bits.
Exemple 1.6.1.
if (condition) {
liste d’instructions;
}
Si la liste d’instructions ne comporte qu’une seule instruction, les accolades ne sont pas nécessaires.
La condition est une expression booléenne et peut donc comporter des opérateurs logiques et des
opérateurs de comparaison.
La plupart du temps, on souhaite exécuter un bloc d’instructions si une condition est réalisée
mais aussi exécuter un autre bloc d’instructions dans le cas contraire. D’où l’instruction if...else,
correspondant à un si ... alors ... sinon :
Exemple 1.6.2.
if (condition) {
liste d’instructions;
}
else{
16 CHAPITRE 1. INTRODUCTION AU LANGAGE C
liste d’instructions;
}
On notera qu’il existe une structure de test beaucoup plus succinte dans le cas ou les 2 listes
d’instructions sont réduites à deux listes unitaires. La syntaxe est la suivante :
Exemple 1.6.3.
(condition) ? instruction si vrai : instruction si faux
Dans ce cas, la condition doit être entre parenthèses, l’instruction à gauche du ’ :’ est réalisée si la
condition est vraie, celle de droite est réalisée si la condition est fausse. De plus, la structure ? renvoie
la valeur résultant de l’instruction exécutée. Par exemple :
Exemple 1.6.4.
minab = ((a>b) ? b : a);
renvoie dans minab le minimum entre a et b.
On notera qu’en C, l’évaluation des expressions booléennes se fait séquentiellement. C’est à dire
que si la première partie de l’expression booléenne suffit à l’évaluer, le reste de l’expression booléenne
ne le sera pas. Ceci est particulièrement utile dans le cas où une partie de l’expression booléenne
n’est pas toujours définie (l’accès à un élément d’un tableau). Par exemple, la séquence d’instructions
suivante est bien définie (en anticipant un peu sur l’introduction des tableaux) :
Exemple 1.6.5.
int Tableau[10] = {1,2,3,4,5,6,7,8,9,10};
int indice = 20;
if ((indice<10)&&(indice>=0)&&(Tableau[indice]==5)) {
printf(’Tableau[%d] = %d\n’,indice-1,Tableau[indice]);
}
Cette suite d’instructions ne fera rien (car on n’entre pas dans la boucle). Par contre, si on avait
mis la condition Tableau[indice]==5 en premier, cela aurait provoqué une erreur d’exécution, puisque
Tableau[20] n’est pas définie.
case Valeur2:
liste d’instructions;
break;
....
default:
liste d’instructions;
break;
}
1.6. LES STRUCTURES CONDITIONNELLES ET DE BOUCLE 17
Lorsque la Variable est égale à Valeur1, on exécute la première liste d’instructions, si elle est égale
à Valeur2, on exécute la seconde liste d’instructions, et ainsi de suite. Si aucun des cas ne correspond
à Variable, on rentre dans la branche default. Dans le cas où l’instruction break ne finit pas le case, le
programme entrera dans le case suivant. Cela peut s’avérer utile si on souhaite exécuter la même liste
d’instructions pour plusieurs valeurs différentes de Variable. Par exemple :
Exemple 1.6.7.
switch (Variable){
case Valeur1:
case Valeur2:
liste d’instructions (pour Valeur1 et Valeur2);
break;
case Valeur3:
liste d’instructions;
break;
...
default:
liste d’instructions;
break;
}
Il est conseillé de toujours avoir un cas default, ne serait-ce que pour afficher un message d’erreur.
Cela permet entre autre de rendre le code plus robuste.
Exemple 1.6.8.
for (initialisation du compteur; condition pour continuer; modification du compteur) {
liste d’instructions;
}
Par exemple :
Exemple 1.6.9.
int i, x = 0;
for (i=1; i<=10; i++) {
x = x + i;
}
Exemple 1.6.10.
while (condition) {
liste d’instructions;
}
Dans ce cas, la liste d’instructions est exécutée tant que la condition est réalisée. Donc, pour pouvoir
sortir de la boucle, il faut absolument que la liste d’intructions modifie tout ou partie des composantes
de la condition afin qu’après un certain nombre d’exécutions, cette condition puisse devenir fausse.
On notera que l’instruction while est une généralisation de l’instruction for où la modification du
compteur peut être très compliquée.
L’instruction while est très bien appropriée dans le cas d’un algorithme itératif avec un critère
d’arrêt (recherche de zéro par exemple) ou encore dans le cas d’un programme contenant un menu qui
en mode texte qui réapparait tant que l’utilisateur n’a pas décidé de stopper le programme.
Comme pour l’instruction while, il est préférable de s’assurer que la condition devienne fausse au
bout d’un nombre fini d’itérations.
où type est le type des éléments du tableau (qui sont donc tous du même type). La taille qu’occupe un
tableau en mémoire est la taille qu’occupe le type des données multipliée par le nombre d’éléments.
Par exemple, un tableau de 10 double se déclare de la manière suivante :
Ce tableau occupera un espace de 10*8=80 octets en mémoire, soit 640 bits. Pour accéder à un élément
du tableau, on utilise le nom du tableau suivi de l’indice de l’élément entre crochets. Il faut toutefois
faire attention, car en C, les indices des tableaux commencent à 0. Ainsi, pour accèder au 5ème
élément de notre tableau de double, on écrira :
Tableau[4] ;
On peut manipuler un élément d’un tableau tout comme une variable classique et s’en servir dans une
expression ou lui affecter une valeur. De plus, pour initialiser un tableau, il n’est bien entendu pas
nécessaire de le faire élément par élément. On peut le faire comme suit, à la déclaration :
La liste de valeurs entres accolades ne doit pas comporter plus d’éléments que le tableau. Cependant,
elle peut en comporter moins, auquel cas les éléments non affectés du tableau prendront la valeur 0.
De plus, les éléments entre accolades doivent être des constantes. En particulier, pour initialiser un
tableau à 0, il suffit d’écrire :
On peut également utiliser une boucle for dont le compteur désignera l’indice de l’élément à affecter.
Par exemple, pour calculer les 100 premiers termes de la suite de Fibonacchi :
Exemple 1.7.1.
int Fibo[100] = {1,1};
int indice;
for (indice=2,indice<100,indice++){
Fibo[indice] = Fibo[indice-1] + Fibo[indice-2];
}
Lorsqu’on utilise un tableau, on fait souvent appel à sa taille. C’est pourquoi il est fortement
conseillé de définir la taille d’un tableau à l’aide d’une macro define. Par exemple :
Exemple 1.7.2.
#define NB_ELEMENTS_FIB 50
int Fibo[NB_ELEMENTS_FIB] = {1,1};
int indice;
for (indice=2,indice<NB_ELEMENTS_FIB,indice++){
Fibo[indice] = Fibo[indice-1] + Fibo[indice-2];
}
Ceci permet de pouvoir changer la taille du tableau en ne changeant que la valeur de la macro
NB ELEMENTS FIB.
Exemple 1.7.3.
int A[5][5];
A[0][0] = 4;
Un tableau multidimensionnel peut être initialisé de la même manière qu’un tableau unidimension-
nel, ie. avec des boucles imbriquées ou avec une affectation à la déclaration. Typiquement, l’affectation
avec des boucles imbriquées se fait de la manière suivante.
Exemple 1.7.4.
#define NB_LIGNES 10
#define NB_COLS 10
int A [NB_LIGNES][NB_COLS];
int i,j;
for (i=0,i<NB_LIGNES,i++){
for (j=0,j<NB_COLS,j++){
A[i][j] = ...;
}
}
Dans le cas où on souhaite initialiser un tableau multidimensionnel à la déclaration, on peut utiliser
une liste d’éléments entre accolades. Cependant, il faut bien faire attention à l’ordre dans lequel les
éléments d’un tableau multidimensionnel sont stockés en mémoire. Ainsi, les éléments d’un tableau
tab[n][m][p] sont stockés dans l’ordre suivant :
Donc en particulier, cela veut dire que pour une matrice (2 dimensions), ses éléments sont stockés
lignes par lignes. L’initialisation suivante :
Exemple 1.7.5.
1 2 3 4
A= 5 6 7 8
9 10 0 0
Notons qu’il est cependant préférable d’expliciter la structure ligne par ligne durant l’initialisation
en faisant des blocs avec les éléments, par exemple, pour la matrice A précédente :
Exemple 1.7.6.
int A[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10}};
S a l u t \0
Pour créer une chaı̂ne de caractères, on la déclare donc comme un tableau de caractères. Avec
une telle représentation, on doit fixer à l’avance la taille de la chaı̂ne. On peut par contre prévoir une
grande taille et ne l’utiliser que partiellement, puisque la fin de la chaı̂ne n’est pas la fin du tableau
mais la première occurence du caractère de fin de chaı̂ne ’\0’. De plus, il faut toujours penser à déclarer
un tableau du nombres de caractères de la chaı̂ne plus un, pour tenir compte du caractère de fin de
chaı̂ne. Par exemple, pour une chaı̂ne de 20 caractères, il faudra déclarer un tableau d’au minimum
21 éléments :
char Chaine Car[21] ;
Ensuite, pour manipuler une chaı̂ne de caractères, on possède tout un attirail défini dans les bi-
bliothèques standards, surtout string.h. Quelques fonctions utiles à la manipulation de chaı̂ne de
caractères :
— strcmp() : compare 2 chaı̂nes de caractères et renvoie un entier négatif, nul ou positif suivant
que la première chaı̂ne de caractère placée en argument est respectivement inférieure, égale ou
supérieure à la seconde chaı̂ne de caractères.
— strncmp() : de même que strcmp() mis à part que strncmp() accepte 3 arguments, le dernier
étant un entier n spécifiant qu’il ne faut comparer que les n premiers caractères des 2 chaı̂nes.
— strcpy() : cette fonction prend en premier argument une chaı̂ne destinataire dest et en second
une chaı̂ne source src et copie la seconde dans la première. Une version ne copiant que les n
premiers caractères de src dans dest est également disponible. La fonction renvoie la nouvelle
chaı̂ne dest.
— strcat() : cette fonction prend en premier argument une chaı̂ne destinataire dest et en second
argument une chaı̂ne source src et concatène src au bout de dest en prenant soin d’écrire sur
le caractère de fin de chaı̂ne de dest. Donc en gros il s’agit d’une fonction pour mettre bout à
bout 2 chaı̂nes de caractères. La fonction renvoie la nouvelle chaı̂ne dest.
On notera qu’un test d’égalité entre 2 chaı̂nes de caractères ne se fait pas avec l’opérateur ’==’. Il
existe de nombreuses autres fonctions dans la bibliothèque string.h, soin est laisser au lecteur de les
découvrir (le man linux/unix est pour cela très pratique). Un exemple d’utilisation des chaı̂nes de
caractères et des fonctions de string.h est le suivant :
Exemple 1.7.7.
#include <stdio.h>
#include <string.h>
int main(){
char phrase[100], mot1[20] = "Aloha", mot2[20] = "Monde";
char phrasecpy[100];
strcat(phrase,mot1);
strcat(phrase," ");
strcat(phrase,mot2);
strncpy(phrasecpy,phrase,10);
phrasecpy[10] = ’\0’;
}
22 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Dans cet exemple, on voit qu’il faut rajouter le caractère de fin de chaı̂ne après la copie tronquée
de phrase, sans quoi phrasecpy n’est pas une chaı̂ne de 10 caractères. Il est important de noter que
l’affectation d’une valeur à une chaı̂ne de caractères à l’aide de ’=’ n’est autorisé qu’au moment de la
déclaration de la chaı̂ne. En particulier :
Exemple 1.7.8.
...
char mot1[20] = "Aloha"; /* autorise */
char mot2[20];
mot2 = "bonjour"; /* interdit => erreur a la compilation*/
...
}
Exemple 1.7.9.
struct Nom_de_la_Structure {
type_champ1 Nom_Champ1;
type_champ2 Nom_Champ2;
...
};
Dans cette instruction, les noms des champs doivent tous être différents et peuvent être de n’im-
porte quel type excepté le type de la structure. Ainsi, la structure suivante est valide :
Cette structure n’est pas valide car d’une part elle possède 2 champs de même nom (Moyenne) et
d’autre part elle possède un champ ayant son propre type.
La déclaration d’une structure ne fait que définir les caractéristiques de la structure. Une fois la
structure déclarée, on peut déclarer une variable du type de la structure :
1.7. TYPES DE DONNÉES COMPLEXES 23
Exemple 1.7.12.
struct Nom_de_la_Structure Nom_Variable;
/* ou encore */
struct Nom_de_la_Structure Nom_Variable1, Nom_Variable2, ...;
Pour accèder aux champs d’une variable structurée, on fait suivre le nom de la variable par un
point ’.’ et le nom du champ auquel on veut accèder (sauf quand le champ est un pointeur, comme on
le verra dans la section dédiée à ces derniers). Par exemple, pour la structure Eleve (la valide) :
Exemple 1.7.13.
struct Eleve Benjamin;
[Link] = 16;
[Link] = 18.5;
[Link] = 1;
La structure étant un type comme un autre, on peut l’utiliser pour former un tableau :
De plus, il peut être intéressant de renommer une structure grâce au mot clé typedef qui permet
de faire de la structure un type comme les autres. Par ’type comme les autres’, il faut comprendre
un type dont les variables sont déclarées par Nom Type Variable plutôt que par struct Nom Struct
Variable. Par exemple, pour notre structure Eleve :
Exemple 1.7.14.
typedef struct Eleve {
int Age;
float Moyenne;
struct Classe Niveau;
} Eleve;
...
int main(){
Eleve Benjamin; /* Et plus struct Eleve Benjamin */
...
return 0;
}
Exemple 1.7.15.
enum Couleur {rouge, vert, bleu};
...
int main(){
enum Couleur ma_couleur;
...
}
24 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Dans notre exemple, la variable ma couleur pourra prendre les valeurs rouge, vert ou bleu (at-
tention, lors d’affectation, ce ne sont pas des chaı̂nes de caractères, donc pas de guillemets). Un type
énuméré est en fait un alias entre les valeurs possibles du type et les entiers positifs ou nuls. Dans le
cas du type Couleur, la correspondance sera : rouge = 0, vert = 1, bleu = 2.
Encore une fois, on peut utiliser un typedef lors de la définition du type énuméré, afin de pouvoir
utiliser Couleur comme un vrai type.
Le type de la variable pointée par le pointeur peut être n’importe quel type déjà défini (int, char,
float, type complexe défini avec struct, voir même int *, float*, int **...). Grâce au type du pointeur,
le compilateur saura combien de blocs mémoires sont à réserver après le bloc pointé.
L’initialisation d’un pointeur se fait grâce à l’opérateur unaire & :
Un exemple d’utilisation :
1.8. LES POINTEURS 25
Exemple 1.8.1.
int * ad;
int n;
n = 10;
ad = &n; /* ad recoit l’adresse de l’entier n */
*ad = 20; /* l’entier stocke a l’adresse ad recoit 20 */
A la fin de cet exemple, la valeur de l’entier n est 20. On verra dans la section 1.9, dédiée aux
fonctions, l’intérêt du passage par l’adresse pour changer la valeur d’une donnée.
Exemple 1.8.2.
#include <stdio.h>
#include <stdlib.h> /* pour malloc() et sizeof() */
int main(){
int * TabEntier;
char * Chaine;
int i, n;
return 0;
}
26 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Liste Chaı̂née Simple Par exemple, la structure suivante correspond à une liste chaı̂née contenant
un entier et un nombre à virgule flottante :
Exemple 1.8.3.
struct Ma_Liste {
int mon_entier;
double mon_nombre;
struct Ma_Liste * pSuivant;
};
On notera qu’on a ainsi détourné la restriction voulant qu’il est interdit d’avoir une structure ayant
un champ de son propre type. On a ainsi une structure récursive. Cependant, elle ne peut être infinie
et il nous faut donc un moyen de la stopper. Cela se fait en assignant au pointeur sur l’élément suivant
la valeur NULL, quand le maillon considéré est le dernier. On a également besoin d’un pointeur sur
le premier élément de la liste, qui lui ne fera pas parti d’un maillon. On représentera une telle liste de
la manière suivante, chaque maillon étant une variable de type Ma Liste.
Une fois la structure définie, il reste encore à la déclarer. Pour se faire, on utilise deux éléments du
type pointeur de la liste, un qui définira la tête de la liste (son commencement), un autre pointant sur
un éventuel nouveau maillon.
Exemple 1.8.4.
struct Ma_Liste *Nouveau;
struct Ma_Liste *Tete;
Tete = NULL;
Pour l’instant notre liste est vide, sa tête ne pointant vers rien. Afin de peupler notre chaı̂ne, il
faut pouvoir ajouter des maillons, mais pour cela il faudra à chaque fois réserver un espace mémoire
où stocker le nouveau maillon. Comme a priori on ne connaı̂t pas le nombre de maillons de la chaı̂ne,
il va falloir être capable de réserver de l’espace mémoire sans passer par l’étape de déclaration. Ceci
se fait par l’intermédiaire des fonctions malloc et sizeof de la bibliothèque stdlib.h. Les opérations les
plus utiles dans une liste chaı̂née, sont l’ajout d’un premier élément, l’ajout d’un élément en fin de
liste et le parcours de la liste. Pour l’ajout d’un premier élément, on procède de la façon suivante :
Exemple 1.8.5.
/* pour ne pas avoir a rappeler struct */
1.8. LES POINTEURS 27
Pour l’ajout d’un dernier élément, il faut tout d’abord parcourir la liste, puis allouer de la mémoire
pour un nouveau maillon et finalement ajouter l’élément.
Exemple 1.8.6.
Ma_Liste * Courant;
if (Tete != NULL) {
Courant = Tete;
while (Courant->pSuivant != NULL) Courant = Courant->pSuivant;
}
Nouveau = (Ma_Liste*)malloc(sizeof(struct Ma_Liste));
Courant->pSuivant = Nouveau;
Nouveau->pSuivant = NULL;
Liste Chaı̂née Double La liste chaı̂née simple ne permet de parcourir la liste que dans une seule
direction. Pour pouvoir le faire dans les deux directions, on utilise une liste chaı̂née double, dont un
exemple est le suivant :
Exemple 1.8.7.
struct Ma_Liste_Double {
int mon_entier;
double mon_nombre;
struct Ma_Liste_Double * pPrecedent;
struct Ma_Liste_Double * pSuivant;
};
On peut également utiliser cette liste chaı̂née double pour faire une liste circulaire/bouclée ou
même un arbre binaire (auquel cas les 2 pointeurs sur Ma Liste Double seraient les fils).
Exemple 1.8.8.
#include <stdio.h>
#include <stdlib.h>
int main(){
void * Tab = NULL;
int i,n;
La possibilité d’avoir un pointeur générique permet entre autre de construire des structures de
données génériques, comme par exemple une pile contenant des données spécifiées lors de l’exécution.
Ainsi, on peut définir des fonctions manipulant des pointeurs génériques, et qui accepteront donc tout
type d’argument pourvu que les instructions de la fonction aient un sens pour le type qui sera spécifié
à l’exécution.
Ceci étant une fonctionnalité relativement évoluée du C, nous n’iront pas plus avant dans sa
présentation.
Exemple 1.9.1.
type_donnee Nom_Fonction (type1 arg1, type2 arg2, ...) {
liste d’instructions;
return ...;
}
Quelques remarques :
— Le type type donnee est celui de la valeur renvoyée par la fonction.
— Tout comme pour le main du code, la fonction doit se terminer par un return arg sortie qui
sera le résultat visible de l’exécution de la fonction.
— Si la fonction ne renvoie aucune valeur, son type est void.
— Si aucun type de sortie n’est précisé, il sera par défaut considéré comme int.
1.9. LES FONCTIONS 29
— Suivant le type des arguments d’entrées, on aura un passage par valeur ou par adresse, comme
on l’expliquera un peu plus tard.
— La liste d’instructions est encadrée par des accolades.
Une fois la fonction définie, elle ne sera exécutée que si elle est appelée. Et pour l’appeler, il faut
également la déclarer, comme pour une variable, à l’aide de son prototype. Ainsi, si on a défini une
fonction polycube, évaluant un polynôme du troisième degré, on pourrait procéder de la façon suivante :
Exemple 1.9.2.
#include <stdio.h>
#include <math.h>
int main() {
double coeff[4] = {4,3,2,1};
double x;
double polycube(double a[4], double x);
return 0;
}
Dans notre exemple, on a placé les arguments par valeur, ie que lors de l’appel à la fonction, celle-ci
ne voit que le contenu des variables coeff et de x, qui est recopié lors de l’appel. Avec cette manière
de procéder, la fonction ne peut modifier ses arguments d’entrée. Pour pouvoir modifier la valeur des
arguments d’entrée, il faut faire un passage par adresse qui consiste à ne pas donner la valeur de
la variable d’entrée qu’on souhaite modifier, mais son adresse. Ainsi, une fonction qui prendrait un
nombre en paramètre d’entrée et transformerait ce nombre en son carré serait la suivante :
Exemple 1.9.3.
void carre(double * x) {
*x = (*x)*(*x);
return;
}
....
double x = 3.1;
carre(&x); // appel
printf(’’le carre de 3.1 est %f\n’’,x);
....
Exemple 1.9.4.
void carre_tab(int n, double * Tab) {
int i;
30 CHAPITRE 1. INTRODUCTION AU LANGAGE C
for(i=0;i<n;i++)
Tab[i] *= Tab[i]; /* ou encore: *(Tab+i) = *(Tab+i)* *(Tab+i); */
return;
}
....
double A[10] = 3.1;
carre_tab(10,A); // appel
....
On notera que si on passe par adresse un tableau à une fonction, il faut également passer en
paramètre la taille du tableau.
Exemple 1.9.5.
...
int factorielle(int); /* prototype */
int main(){...}
int factorielle(int n) {
if (n <= 1)
return 1;
else
return n*factorielle(n-1);
}
xn+1 = (a ∗ xn + b)mod c
avec a, b, c et x0 des constantes. Ici, on comprend bien que ce qui nous intéressera seront les valeurs
de xn pour des n croissants avec un incrément unitaire. Si on veut en faire une fonction, il faudra
donc, à chaque fois qu’on l’appel pour connaı̂tre la valeur du prochain nombre généré, se souvenir
de la valeur précédente. Pour se faire, on pourra écrire la fonction comme suit (glc pour Générateur
Linéaire Congruant) :
int glc(){
static int etat = X0;
etat = (A*etat + B)%C;
return etat;
}
Cette implémentation fera que la première fois que glc est appelée, le programme intialisera la
variable etat à X0 (0 ici), puis renverra x1 , à savoir 7. La seconde fois que glc sera appelée, la variable
etat aura conservée sa valeur 7 et ne sera plus initialisée a X0. Une suite de 10 appels à cette fonction
sortira les valeurs suivantes :
7, 11, 71, 62, 28, 23, 49, 35, 27, 8
Le premier des arguments représente le nombre d’arguments avec lequel a été appelé le programme,
augmenté de un. Le second argument, dont la dimension est argv possède comme premier élément
le nom sous lequel a été appelé le programme, ensuite, les éléments suivants correspondent aux
paramètres qui ont suivi l’appel du programme, mais sous forme de chaı̂nes de caractères. Plus
précisément :
Sur cet exemple, il faut bien faire attention au fait que durant l’exécution du programme, tous
les arguments seront considérés comme des chaı̂nes de caractères. Ainsi, si l’argument ”13” doit être
utilisé comme l’entier ”13”, il faudra penser à d’abord convertir params[2] en entier (ici avec la fonction
atoi() par exemple).
32 CHAPITRE 1. INTRODUCTION AU LANGAGE C
De plus, si une fonction doit utiliser des paramètres fournis lors de l’appel du programme, il est
toujours plus prudent de s’assurer que ces paramètres sont présents avant de vouloir les utiliser. En
pratique, cela se fait en testant le premier argument du main pour s’assurer qu’il a la bonne valeur.
Par exemple :
Il existe beaucoup d’autres fonctions dans stdio.h, que vous apprendrez à utiliser quand vous en aurez
besoin (le man est votre ami). Un exemple d’utilisation des fonctions présentées est le suivant :
int main(){
FILE * fid;
int a,b;
float x,y;
char truc[20] = "bonjour";
/* Creation fichier */
fid = fopen("[Link]","w");
fprintf(fid,"Un entier: %d\n",a);
fprintf(fid,"%f",x);
fprintf(fid,"%s",truc);
fclose(fid);
/* Lecture */
fid = fopen("[Link]","r");
fscanf(fid,"Un entier: %d",&b);
fscanf(fid,"%f",&y);
fscanf(fid,"bon%s",truc);
fclose(fid);
return 0;
}
Attention, lorsque l’on utilise les formats (%.), il faut absolument que les types soient conformes.
Par exemple, la lecture d’un %f avec affectation à un double, va donner un résultat tout à fait inattendu
(c’est un %lf qu’il faut utiliser).
Notons que pour les lectures (scanf, fscanf ), on utilise l’opérateur unaire & pour donner à la
fonction l’adresse de la variable à modifier, car si on ne lui donnait que la valeur actuelle de la
variable, aucune modification de cette dernière ne serait possible.
34 CHAPITRE 1. INTRODUCTION AU LANGAGE C
Cette bibliothèque standart regroupe plusieurs fonctions, dites utilitaires, ayant des buts différents
mais toutes très utiles. Vous l’avez déjà rencontré lors de l’introduction de l’allocation dynamique de
mémoire (avec malloc et free). Elle sert également à définir certaines fonctions de conversion de type,
de génération aléatoire de nombres, ou encore de gestion de processus. Voici un rapide tour d’horizon
de ce que l’on peut y trouver :
— Conversion de type
— atof : converti une chaı̂ne de caractère en flottant (utile pour les arguments du main par
exemple), interface : double atof (const char * string)
— atoi : converti une chaı̂ne de caractère en entier, interface : int atoi (const char * string)
— atol : converti une chaı̂ne de caractère en entier long (qui est en général un entier classique
sur la plupart des machines actuelles), interface : long atol (const char *string)
— strtod, strtol, strtoul : autres fonctions de conversions d’une chaı̂ne de caractères mais un peu
plus évoluées puisqu’elles permettent de spécifier la fin de la chaı̂ne de caractères à convertir.
Ceci permet de ne convertir qu’une partie d’une chaı̂ne de caractères. Les interfaces sont
les suivantes : double strtod(const char *nptr, char **endptr), long int strtol(const char
*nptr, char **endptr, int base), unsigned long int strtoul(const char *nptr, char **endptr,
int base). L’entier base de strtol et strtoul permet de convertir des chaı̂nes de caractères en
la considérant comme étant dans une base donnée (qui doit être entre 2 et 36 ou encore 0
pour la base par défaut, ie 10).
— Génération aléatoire La génération de nombre (pseudo)-aléatoire est gèrée par les fonctions
rand et srand.
— rand à pour interface int rand(void), ie qu’elle renvoie un entier et ne prend aucun argument.
L’entier renvoyé est compris entre 0 et la constante RAND MAX. Cette fonction ne renvoie
qu’une suite de valeur pseudo-aléatoire, ie qu’on aura une répétition de suite de valeurs
si on attend assez longtemps. De plus, si on n’initialise pas manuellement la graine du
générateur, elle est toujours égale a 0 ce qui a pour conséquence que le programme suivant
génère toujours la même suite de valeurs aléatoires d’un appel à l’autre :
Exemple 1.10.3 (Appel à rand sans initialisation de la graine).
#include <stdlib.h>
#include <stdio.h>
int main(){
int i;
printf("Suite aleatoire? : ");
for (i=1;i<10;i++){
printf(" %d,",rand());
}
printf(" %d\n",rand());
return 0;
}
Si on appelle 2 fois de suite l’exécutable provenant de ce code, on obtient 2 fois la même
suite de 10 nombres.
— srand à pour interface void srand(unsigned int seed) et permet d’initialiser la graine du
générateur pseudo-aléatoire rand avec seed. Ceci permet de palier au problème de prédictabilité
du rand. En général, on combine l’appel à cette procédure avec la fonction time qui renvoie
l’heure de la machine sur laquelle est exécuté le programme. Dans l’exemple précédent, pour
ne pas avoir 2 fois de suite la même suite de nombre, on introduira simplement un appel à
srand(time(NULL)) avant le premier appel a rand.
1.10. BIBLIOTHÈQUES STANDARDS DU C 35
La génération de nombres pseudo aléatoires est un sujet compliqué. On notera que certaines
implémentation de rand peuvent avoir le défaut que les bits de poids faible des nombres générés
ne sont pas aussi aléatoire que les bits de poids forts (en général RAND MAX est assez impor-
tant). Ceci n’est en principe plus le cas sur les implémentations récentes de rand. Pour générer
des nombres entre disons, a et b, on fait des divisions du résultat de rand par RAND MAX ou
encore des modulos.
— Allocation de mémoire Vous connaissez déjà en partie ces fonctions, il s’agit de :
— void * malloc(size t size) : réalise la réservation en mémoire de size octets contigus et renvoie
un pointeur sur le premier octet (renvoie le pointeur vide NULL en cas d’échec).
— void * calloc(size t nobj, size t size) : réalise la réservation de nobj *size octets contigus et
renvoie un pointeur sur le premier octet réservé.
— void * realloc(void *p,size t size) : réalise la réservation de size octets contigus en mémoire
et y copie les données pointées par le pointeur p. Cette fonction est surtout utile pour
augmenter la taille d’un tableau sans avoir a recopier à la main les éléments déjà existant.
Attention, ceci n’est pas très efficace comme méthode du point de vue du temps d’exécution.
— void free(void *p) : libère les octets pointées par le pointeur p (pour éviter les fuites de
mémoire).
— Contrôles de processus Dans ces fonctions, on notera :
— void abort(void) : cette procédure cause l’arrêt du programme de façon anormale, à moins
que le signal SIGABRT soit attrapé par un catch (voir la gestion d’exception dans la partie
C++). Si un programme s’arrête sur un abort, alors tous les flux ouverts sont fermés.
— void exit(int signal) : provoque l’arrêt du programme de façon normale ( ?).
— int system(const char *command) : exécute la chaı̂ne de caractère comme une commande
shell (ou ligne de commande windows). La valeur de retour est -1 si l’exécution de la
commande à levée une erreur, sinon cette valeur est celle retournée par la commande.
— char *getenv(const char *name) : retourne la variable d’environnement correspond à la
chaı̂ne de caractère name (si celle-ci existe dans l’environnement bien entendu). Ceci est,
tout comme system, une commande pour intéragir avec le système d’exploitation de la
machine sur laquelle est exécutée le programme.
— Maths, recherche et tri
— void *bsearch(const void *key, const void *base, size t nmemb, size t size, int (*compar)(const
void *, const void *)) : recherche dans un tableau de nmenb objets, le premier étant pointé
par base, le premier qui correspond à l’objet pointé par key. La taille de chacun des éléments
du tableau est de size. La fonction compar est une fonction de comparaison (le tableau pointé
par base doit être rangé dans l’ordre croissant suivant cette relation de comparaison). C’est
cette relation qui est utilisée pour dire si l’objet est présent ou non.
— void qsort(void *base, size t nmemb, size t size, int(*compar)(const void *, const void *)) :
trie un tableau de nmemb éléments dont le premier est pointé par base et dont chacun à
une taille de size. La fonction compar sert de relation de comparaison pour le tri.
— div t div(int numerator, int denominator) effectue la division entière entre numerator et
denominator, et retourne une structure de type div t contenant le quotient et le reste de la
division.
— int abs(int j) retourne la valeur absolue de l’entier j. Il existe également une fonction labs
et llabs pour les long int et long long. On notera que pour les fonctions valeurs absolues sur
des flottants, la bibliothèque à utiliser est la bibliothèque mathématiques math.h.
pulation de la mémoire.
Voici un pot-pourri de ce qu’on peut y trouver :
— NULL : la macro représentant un pointeur qui pointe sur une adresse mémoire non valide.
— size t : un type d’entier non signé qui est le type de retour de la fonction sizeof.
— char *strcpy(char *dest, const char *src) : copie la chaı̂ne de caractères pointée par src
dans la chaı̂ne de caractères pointée par dest.
— char *strncpy(char *dest, const char *src, size t n) : copie les n premiers caractères de
la chaı̂ne de caractères pointée par src vers la chaı̂ne de carcatères pointée par dest. On notera
qu’il faudra donc rajouter le caractère de fin de chaı̂ne pour rendre la chaı̂ne pointée par dest
valide après l’exécution de strncpy.
— char *strcat(char *dest, const char *src) : concatène à la suite de la chaı̂ne de caractères
pointée par dest, celle pointée par src, le caractère de fin de chaı̂ne de dest est écrasée par le
premier caractère de src.
— char *strncat(char *dest, const char *src, size t n) : comme strcat mais ne concatène
que les n premiers caractères de src à dest. La chaı̂ne résultante sera valide et se terminera bien
par le caractère de fin de chaı̂ne (qui sera rajouté).
— int strcmp(const char *s1, const char *s2) : compare les chaı̂nes de caractères pointée
par s1 et s2. Renvoie un entier négatif si s1 < s2, nul si s1 = s2, positif si s1 > s2.
— int strncmp(const char *s1, const char *s2, size t n) : idem que strcmp mais ne compare
que les n premiers caractères des 2 chaı̂nes.
— char *strchr(const char *s, int c) : cherche le caractère c (un int devient facilement un
char) dans la chaı̂ne de caractère pointée par s. Renvoie un pointeur sur le premier caractère
trouvé, et NULL si le caractère n’est pas trouvé. La fonction strrchr à la même interface mais
renvoie la dernière occurence du caractère. La fonction strchrnul effectue la même chose que
strchr à l’exception qu’en cas d’échec le pointeur retourné pointe vers le caractère de fin de
chaı̂ne de s.
— size t strlen(const char *s) : renvoie la longueur de la chaı̂ne de caractères pointée par s, le
caractère de fin de chaı̂ne ne compte pas.
— char *strpbrk(const char *s, const char *accept) : renvoie la première occurence de
n’importe lequel des caractères de la chaı̂ne pointée par accept, dans la chaı̂ne s.
— void *memcpy(void *dest, const void *src, size t n) : copie n octets à partir de la zone
mémoire pointée par src vers la zone mémoire pointée par dest. Les 2 zones mémoires ne doivent
pas avoir d’intersection commune.
— void *memmove(void *dest, const void *src, size t n) : idem que memcpy mais autorise
les zones mémoires à s’intersecter. Cependant cette fonction est moins performante que memcpy.
— int memcmp(const void *s1, const void *s2, size t n) : compare les n premiers octets
des zones mémoires pointées par s1 et s2.
Bien d’autres fonctions sont disponibles dans cette bibliothèque, mais le C++ ayant une meilleure
gestion des chaı̂nes de caractères que le C, il n’est pas nécessaire de trop approfondir.
Exemple 2.1.1.
#include <iostream> /* sans extension */
#include "point.h" /* un fichier interface avec extension, present
dans le repertoire courant */
Un code C étant compatible C++, il devrait (dépend du compilateur utilisé) encore être possible
39
40 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
d’utiliser les bibliothèques standards du C sous leur forme originelle, ie avec l’extension ’.h’ et sans le
préfixe.
Les fichiers sources, contenant l’implémentation, portent traditionnellement les extensions .cc, .cpp
ou .C. Les codes C étant compatibles, on gardera l’extension .c pour les codes écrits purement en C
(en particulier les codes uniquement fonctionnels). On notera que certains compilateurs peuvent avoir
tendance à se servir des extensions pour ’deviner’ le contenu du fichier (c’est d’ailleurs le cas des
systèmes d’exploitation). De plus, le type de retour de main doit obligatoirement être int, et
rien d’autre.
Pour compiler un code C++, il faut bien entendu un compilateur C++ et non pas un compilateur
C. Dans tout le document on choisira par défaut le compilateur C++ de GNU, appelé g++ et ayant
à peu près les mêmes options que gcc.
2.1.4 Commentaires
En plus des commentaires C encadrés par /* et */, le C++ offre la possibilité de faire des com-
mentaires n’excédant pas une ligne, et ce, en faisant précédé le commentaire par le symbole ’//’ :
Exemple 2.1.3.
i++; // commentaire court (C++)
j++; /* commentaire long qui
prend plus d’une ligne */
On notera qu’en général les compilateurs C acceptent déjà les commentaires courts.
2.1.5 Typage
Le contrôle de type en C++ est beaucoup moins permissif qu’en C. En particulier, il est obligatoire
de déclarer une fonction (avec son prototype) avant de l’utiliser. En C on pouvait éventuellement s’en
passer (mais ça restait mal !) et le compilateur assignait par défaut le type de retour int. En C++,
l’utilisation d’une fonction non déclarée provoquera une erreur de compilation. Par exemple :
Exemple 2.1.4.
int main() {
int a=3, b=1, c;
// int g(int, int); ok si on decommentait
c = g(a,b); // erreur a la compilation
...}
2.1.6 Déclarations
Dorénavant, on peut déclarer les variables du corps d’une fonction un peu partout. On n’est en
particulier plus obligé de faire toutes les déclarations au début. On doit cependant toujours déclarer
une variable avant de l’utiliser, sous peine de générer une erreur à la compilation :
Exemple 2.1.5.
#include <cstdio>
int main() {
int n;
printf("taille?: ");
scanf("%d",&n);
int tab[n];
for (int i=0;i<n;i++) {...}
return 0;}
On déconseille pourtant d’utiliser cette fonctionnalité quand on peut s’en passer, car cela est
loin d’améliorer la lisibilité du code. Cette liberté est surtout appréciable pour éviter les allocations
dynamiques, comme dans notre exemple.
2.1.7 Constantes
En C, la notion de constantes, déclarées grâce au mot-clef const n’était pas très utile. La raison
principale était que le compilateur C traite ce genre de variable comme une variable normale, dans
le sens où il alloue de la mémoire à la variable et ne connaı̂t donc pas la valeur de la variable lors
de la compilation (car l’allocation de mémoire ne se déroule qu’à l’exécution). Du coup, on ne peut
pas, en C, utiliser ce genre de variable pour définir une autre structure, comme un tableau et on en
est réduit à utiliser le #define du préprocesseur. En C++, le mot-clef const devient beaucoup plus
pratique et remplace avantageusement le #define tout en permettant de sécuriser le code. Il permet
de plus de tracer une frontière nette entre ce qui change et ce qui ne change pas (en tout cas ce qui
ne doit pas changer). Ainsi, une fois une variable déclarée comme constante, le compilateur interdira
toute opération sur cette constante qui pourrait résulter en la modification de cette dernière.
En C++, une variable globale déclarée avec la commande const a une portée limitée au fichier
source dans lequel elle est déclarée. En C, on pouvait l’exporter grâce à la commande extern, ce qui
n’est plus le cas en C++. Par contre, en C++ une telle constante peut être utilisée dans une expression
constante alors qu’en C on devait faire appel à une macro #define. Ceci permet d’utiliser une constante
déclarée par const dans la définition d’un tableau en ayant un contrôle de type (on rappelle qu’avec
le #define, il ne pouvait y avoir de contrôle de type).
Exemple 2.1.6.
const int taille = 10;
int main() {
int tableau [taille]; // bon en C++, erreur en C
...}
On notera qu’une constante doit être initialisée à la déclaration sans quoi on se retrouve avec une
erreur à la compilation.
Le compilateur s’assurant qu’une constante le reste, il devient interdit d’appeler une fonction sur
une constante à moins d’être certain que la fonction ne modifie pas son paramètre d’entrée. Ceci est
bien entendu le cas lors de passage par valeur puisque dans ce cas, il est tout à fait impossible à la
fonction de modifier son argument. Cependant, le passage par adresse étant parfois utile pour d’autres
chose que pour modifier un paramètre d’entrée d’une fonction (pour les gros tableaux par exemple),
il devient nécessaire de pouvoir signaler au compilateur, lors de la déclaration d’une fonction, que
42 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
cette dernière, bien qu’ayant la possibilité de modifier ses paramètres passés par adresse (ou référence
comme on le verra sous peu), ne le fait pas. Ceci se fait encore une fois grâce au mot-clef const mais
qu’on rajoute maintenant à l’interface de la fonction. Voici un exemple succint :
int main(){
const int T[5] = {1,2,3,4,5};
int a, b;
a = fct1(T,0); // cet appel provoque une erreur de compilation
b = fct2(T,0); // celui-ci n’en provoque pas
...
Bien entendu, si vous déclarer dans l’interface d’une fonction que vous ne modifierez pas un des
arguments, il ne faut pas le modifier, sinon le compilateur protestera.
Exemple 2.1.8.
double fct1(); // et non pas double fct1(void);
void fct2(); // et non pas fct2();
Et rappelons encore une fois que le type de retour du point d’entrée du programme (main) est
obligatoirement int.
Une caractéristique intéressante du C++ (et qui le deviendra encore plus dans sa couche objet)
est la surcharge ou surdéfinition de fonctions. En effet, il est permis de définir plusieurs fonctions
portant le même nom, à condition que leurs interfaces diffèrent. Lors de la compilation, le compilateur
choisira quelle fonction utiliser en se basant sur le contexte dans lequel la fonction est utilisée. Ainsi,
une fonction division :
Exemple 2.1.9.
int main(){
int a=3, b=2, c;
float x, y, z;
int division (int, int);
float division (float, float);
c = division(a,b); // premiere fonction appelee: donc division entiere
x = a; y = b;
z = division(x,y); // seconde fonction appelee
...}
Une telle surcharge était bien entendu déjà disponible sur des opérations telles que +, -, /, *
mais maintenant l’utilisateur peut en rajouter lui-même. Un gros avantage de la surcharge est qu’elle
permet de définir des fonctions ayant des paramètres par défaut. Imaginons qu’on décide de créer une
2.1. LE LANGAGE C++, UNE EXTENSION DU C ? 43
fonction de tri d’un tableau avec des options permettant de trier les valeurs par ordre croissant ou
décroissant et aussi d’appliquer une fonction affine aux éléments du tableau. La plupart du temps
on voudra utiliser cette fonction pour trier par ordre croissant un tableau sans appliquer de fonction
affine (enfin juste l’identité). On définira et utilisera la fonction comme dans l’exemple.
int main(){
float tableau[taille];
int sens = -1;
float c1 = 3.4, c2 = -4.3;
... initialisation du tableau ...
trier(tableau,taille); // appel avec sens=1, a=1, b=0
trier(tableau,taille,sens); // appel avec a=1, b=0
...}
/* definition de la fonction */
void trier(float *tab, int taille, int sens, float a, float b);
{... corps de la fonction ...}
Lors de l’utilisation de cette fonctionnalité, on définit en réalité plusieurs fonctions (4 dans notre
cas) ayant chacunes un nombre d’arguments différent. En particulier, on sera obligé de fournir la valeur
de ’sens’ si on veut fournir celle de ’a’ et de ’b’.
On notera que les paramètres par défaut ne sont spécifiés qu’une seule et unique fois lors de la
définition de la fonction ou de son prototype. Il est en général préférable de déclarer les paramètres
par défaut dans le prototype de la fonction car ce sera lui qui sera en général connu des instructions
qui utiliseront la fonction.
Pour utiliser la fonction template, comme elle aura déjà été déclarée dans le fichier interface (qui sera
inclus dans le fichier source), on n’a plus qu’à l’appeler et le compilateur déterminera le type effectif T
suivant le contexte. Cette première notion de généricité utilise le mot clé class qui désigne une classe,
ie un type de données comme on le verra en détail dans la troisième partie de ce chapitre.
Cette déclaration est la seule possible, en particulier, une référence ne pouvant être vide, il faut
obligatoirement l’initialiser à la déclaration. C’est dans ce type de cas que la possibilité de faire des
déclarations où bon nous semble devient intéressante. On donne ici un exemple de fonctions échangeant
2 variables entières en utilisant des pointeurs ou des références.
/* Point d’entree */
int main()
{ int m=1, n=2;
swapC(&m,&n);
swapCpp(m,n);
return 0;
}
/* Avec Pointeurs */
2.1. LE LANGAGE C++, UNE EXTENSION DU C ? 45
/* Avec References */
void swapCpp(int &i, int &j)
{ int tmp = i;
i = j;
j = tmp;}
On notera que les 2 versions de ’swap’ sont valides en C++ mais que celle utilisant les références
est plus simple à écrire et à appeler. De plus, notons qu’une référence est fixée, ie. qu’elle ne peut
référencer qu’une variable (donnée à la déclaration) et ne peut plus changer après (contrairement à
un pointeur).
/* En C++ */
int * pEntier;
float * tab;
...
pEntier = new int;
tab = new float[50];
...
delete pEntier;
delete[] tab;
...
On voit que la syntaxe de new est Pointeur = new type[taille]. On notera qu’encore une fois, c’est
dans ce genre de cas que la déclaration au petit bonheur peut être avantageuse.
46 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
Par contre, malgré sa simplicité l’allocation dynamique par new comporte un inconvénient. En
effet, elle ne renvoie rien en cas d’échec (pas de valeur NULL) mais provoque une erreur (renvoie une
exception). Alors qu’avec malloc, on pouvait tester le retour et agir en conséquence. On peut cependant
récupérer les exceptions avec try et catch, ce qui est surtout utile en cas de ”grosse” allocation de
mémoire.
On notera que le passage à la ligne peut se faire indépendamment avec le ”\n” du C ou avec endl.
On notera également l’emploi de la bibliothèque string qui permet de manipuler des chaı̂nes de
caractères plus facilement qu’avec un type char * ou char[taille].
Nous n’irons pas plus avant dans la description des fonctionnalités si ce n’est pour mentionner que
les bibliothèques ofstream, ifstream et fstream permettent respectivement d’écrire, lire et lire/écrire
dans un fichier. La raison pour laquelle nous ne détaillons pas l’emploi de ces bibliothèques est qu’il
s’agit en fait de classes dont nous ne présentons l’utilisation qu’à partir de la section 2.5.
— modularité, qui rend le code plus flexible et donc plus facilement évolutif,
— réutilisabilité, afin de ne pas repartir de zéro lors de futurs développements.
2.2.2 La Modularité
Le principe d’un code modulaire est là pour éviter de produire du code monolithique qui ren-
drait tout changement difficilement réalisable. En effet, une telle architecture donne souvent lieu à
des relations compliquées entre les composants du code et donc changer un composant implique de
changer tous les autres. La modularité tente de créer une architecture logicielle flexible en rendant les
composants de l’architecture les plus indépendants possibles pour isoler les changements éventuels à
apporter. Pour atteindre cette indépendance, on tente de limiter et cadrer les relations que les com-
posants entretiennent entre eux. De plus, dans chacun de ces composants, que l’on appellera modules,
on concentrera la connaissance des caractéristiques et des méthodes lui étant relatives.
La conception des modules peut se faire suivant 2 familles d’approches. La première est l’approche
descendante, consistant à partir du problème que l’on veut résoudre et à le découper en sous-
problèmes jusqu’à ce que ces derniers soient triviaux. La seconde approche est l’ascendante qui
consiste à partir des bouts de codes (des solutions à des problèmes existants) déjà disponibles et de
les utiliser pour construire une solution au problème courant. Bien entendu, ces deux approches sont
en général utilisées en conjonction.
Quelque soit l’approche utilisée, les modules se doivent de répondre à certains critères de qualité.
Il doivent être, entre autre :
— compréhensibles, ie. clairs, logiques et ne communiquer qu’avec peu d’autres modules,
— continus, ie. qu’une petite modification des spécifications ne doit entraı̂ner le changement que
d’un petit nombre de modules,
— protégés, ie. que l’action d’un module doit être le plus possible restreinte à lui seul. Ceci permet
d’isoler les erreurs.
Ces critères de qualité nous amènent à considérer des modules ayant des interfaces limitées et
explicites. De plus, les communications entre modules doivent également être limitées et les informa-
tions contenues dans les modules doivent être masquées. En particulier, le masquage de l’information
interdit (déconseille fortement) l’utilisation de variables globales.
2.2.3 La Réutilisabilité
Le principe de réutilisabilité est connu de longue date et est derrière l’idée de bibliothèque, ie de
ne pas repartir de zéro mais de s’appuyer sur la résolution de problèmes passés. L’idée de bibliothèque
est bonne mais en général ne donne lieu qu’à des solutions peu flexibles et difficilement adaptables.
La conception objet cherche à formaliser cette notion de réutilisabilité en définissant un bon module
comme un module réutilisable. Un tel module doit pouvoir manipuler plusieurs types différents, mais
aussi offrir des fonctions à l’utilisateur sans que celui-ci n’ait à connaı̂tre son implémentation. Les
opérations communes à un groupe de modules doivent pouvoir porter le même nom pour agir sur des
types différents. C’est l’idée du polymorphisme.
Les techniques permettant de répondre aux critères de la réutilisabilité sont la surcharge d’opérateurs
et la généricité (module paramétré par le type qu’il manipule).
les modules découlent des tâches à accomplir, ce qui les rend peu flexibles et réutilisables pour d’autres
tâches. Une architecture née d’une conception orientée traitements devient très rapidement lourde et
entortillée. De plus, les traitements sont par essence beaucoup moins stables que les données et une
évolution du programme impliquera donc une complète refonte de l’architecture. Cependant, cette
approche a tout de même l’avantage d’être intuitive, en général facilement et rapidement développable
et donc bien adaptée à des applications de taille réduite.
A l’opposé de la conception orientée vers les traitements, se trouve la conception orientée vers les
données (les objets). Elle consiste à se dire la chose suivante : ”Ne commencez pas par demander ce
que fait le système, demandez à qui il le fait” 1 . On va alors organiser le programme autour des objets
qu’il manipule, car ils sont plus stables que les traitements. Une fois que ces objets seront définis, le
concepteur n’aura plus qu’à écrire les traitements devant s’effectuer sur ces objets. Au départ, cette
approche est très contre-intuitive mais permet de satisfaire naturellement aux exigences de qualité de
conception de logiciel.
La grande question est bien entendu de comment choisir les objets. Il n’y a pas de réponses
universelles mais en général on peut tenter de se raccrocher aux objets physiques ou abstraits qui
nous entourent. Par exemple, pour simuler la logistique d’une école, on aura besoin d’objets tels que
des personnes (avec des catégories : notion d’héritage), des moyens physiques (locaux, fournitures,...)
et je ne sais quoi d’autre. Les objets vont représenter toutes les entités, des plus simples aux plus
complexes, et sont un peu comparables aux structures (struct) du langage C. Ils en sont en fait une
extension car ils ont des propriétés supplémentaires.
Les objets suivront un modèle que l’on appellera classe. Une classe sera décrite par ses attributs
(ses champs ; par exemple, pour une personne, ses attributs pourront être : sexe, âge, taille, ...) mais
aussi par les fonctions (méthodes) qu’elle fournit. Par exemple, la classe Date, déclarée comme le
montre l’exemple :
Les attributs et les méthodes d’une classe seront appelés membres de la classe. Une classe sera
un nouveau type dont les occurrences/instances seront les objets.
Voyons maintenant plus en détail cette notion de classe et la façon dont on la déclare en C++.
Cette déclaration de la classe complexe doit se trouver dans un fichier interface, par exemple
complexe.H. Comme on peut le voir, on déclare complexe comme un type comportant les attributs (les
champs) re et im. De plus, cette classe possède une méthode qui se nomme module et qui bien entendu
renverra le module du nombre complexe considéré. Dans ce fichier interface, on notera l’utilisation des
instructions du préprocesseur #ifndef, #define et #endif qui assure que la classe complexe n’a pas
déjà été déclarée (en particulier si on inclut plusieurs fois de suite complexe.H), ce qui provoquerait
une erreur à la compilation. Une fois cette classe déclarée, il faut la définir dans un fichier source, par
exemple [Link], dont le contenu sera :
On voit ici la syntaxe à utiliser pour définir une méthode de classe. Elle ressemble beaucoup à la
syntaxe pour la définition des fonctions à la différence qu’on doit rappeler le nom de la classe avant de
rappeler celui de la fonction (donc Type NomClasse::NomMethode (arguments)). Une autre différence
vient du fait que dans le corps de la méthode, on a accès aux attributs de la classe, ie qu’ici on n’a
pas a redéclarer re et im.
2.3.2 Utilisation
Une fois une classe définie, on peut l’utiliser. Pour illustrer l’utilisation de notre classe complexe,
prenons un exemple :
Ce petit programme crée un objet de la classe complexe, cette déclaration illustre le fait que
complexe est bien un type. Les 2 lignes suivantes correspondent à l’initialisation du complexe,
étape qui ne se fera en réalité jamais comme indiquée, ainsi que nous le verrons dans les paragraphes
50 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
suivants. La dernière instruction correspond à l’affichage du complexe ainsi que de son module, qui
est calculé par l’appel de la méthode correspondante. Cet appel montre bien que la méthode est
appliquée sur un objet de classe et reflète très bien l’approche orientée objet plutôt que traitement.
Dans un langage fonctionnel comme le C, les fonctions ne sont pas autant liées à un type (une classe),
et l’appel à une fonction calculant le module d’une nombre complexe prendrait ce nombre complexe
comme argument.
On notera qu’il est possible d’utiliser une méthode sur un pointeur pointant vers un objet en
utilisant la syntaxe pobjet → méthode(arguments). Cependant ce cas de figure ne se produira que
rarement d’autant plus qu’il est en général plus facile d’utiliser une référence (auquel cas on la manipule
comme l’objet lui-même). Juste au cas où, voici un exemple d’utilisation d’un pointeur sur un objet
ainsi que d’une référence :
Exemple 2.3.4 (Application de méthode sur un pointeur et une référence).
#include "complexe.H"
int main(){
complexe p;
[Link] = 1;
[Link] = 2;
complexe *pp = &p; // un pointeur qui pointe sur p
double m = pp->module();
complexe &rp = p; // une reference, attention elle doit obligatoirement
// etre initialisee a la declaration
m = [Link]();
return 0;
}
2.3.3 Masquage
Dans notre petit laı̈us sur la conception objet, nous avons souligné le fait que pour être robuste, une
classe se doit de masquer certaines informations. En règle générale, on n’autorise jamais les attributs
d’une classe à être directement consultables et modifiables, car cela poserait de gros problèmes de
sécurité (les utilisateurs des classes peuvent faire de très vilaines choses, vous n’avez pas idée !). Pour
ce faire, on dispose de trois niveaux pour la visibilité des membres d’une classe (ie. ses attributs mais
aussi ses méthodes). Un membre d’une classe peut être public, protégé ou encore privé. La visibilité
des attributs se déclare lors de la déclaration de la classe en plaçant les mots-clés public, protected ou
private suivi d’un ’ :’ juste avant la déclaration des dits membres. Ainsi, notre classe complexe avec
masquage peut se déclarer de la façon suivante :
Exemple 2.3.5 (Masquage des membres de la classe complexe (fichier complexe.H)).
class complexe{
private:
double re, im;
public:
double module();
double Re();
double Im();
double set_Re(double);
double set_Im(double);
};
La signification des 3 niveaux est la suivante :
— public (public) : membre visible par tout client de la classe (niveau par défaut),
2.3. LES CLASSES 51
— protégé (protected) et privé (private) : membre inaccessible aux clients de la classe mais pas
à la classe elle-même (ie qu’il est visible lors de la définition de la classe). La différence entre
protected et private ne sera discernable qu’au moment de l’introduction de la notion d’héritage.
Le fait de masquer les attributs, appelé encapsulation, permet de montrer à l’utilisateur ce que l’on
veut et sous la forme que l’on veut. Ainsi l’utilisateur ne connaı̂t pas le détail de la structure des
données, et celle-ci peut être modifiée/enrichie à loisir sans pour autant chambouler toute l’architec-
ture.
Dans la déclaration de notre classe complexe, on remarque 4 nouvelles méthodes. Leurs définitions
pourraient être les suivantes :
Les méthodes Re() et Im() sont appelées accesseurs et permettent au client de connaı̂tre les
parties réelle et imaginaire d’un objet de la classe complexe. En effet, les commandes [Link] et
[Link] ne sont plus réalisables par le client et provoqueraient une erreur de compilation si utilisées
dans un programme (car re et im sont privées). Les méthodes set Re(double) et set Im(double) sont
là pour permettre au client de donner une valeur aux parties réelle et imaginaire du complexe. On
verra par la suite qu’on préfèrera en général affecter une valeur aux attributs de la classe lors de la
déclaration de l’objet (ie avec une déclaration/initialisation).
Avec la continuation (...) indiquant d’autres méthodes telles que celles déjà employées précédemment.
Une définition serait alors :
...
double complexe::Re2() {return re*re;}
double complexe::module() {return sqrt(Re2()+im*im);}
// ou de maniere equivalente: return sqrt(this->Re2() + im*im);
complexe complexe::conjugue() {
im = -im;
return *this;}
double complexe::set_Re(double re) {
this->re = re;
return this->re;}
...
Exemple 2.3.9 (Classe complexe avec constructeurs : interface (complexe.H) et définition ([Link])).
// Interface (.H)
class complexe{
private:
double re, im;
public:
complexe();
complexe(const complexe &)
complexe(double,double);
};
// implementation (.cpp)
#include <iostream>
On a ici 3 constructeurs. Le premier existe toujours, même si par défaut il ne fera pas ce que l’on
souhaite. Ce constructeur par défaut est celui appelé lors de la déclaration classique d’un objet. Le
second constructeur est le constructeur de copie et est également toujours présent par défaut dans la
classe mais ne fait que copier les attributs de la classe d’un objet à l’autre. Il peut arriver qu’il faille
absolument redéfinir ce constructeur dans le cas où un des attributs de la classe est un pointeur, par
exemple un tableau dynamique (pour un statique c’est inutile), car dans ce cas le constructeur par
copie ne fera que copier les adresses au lieu de copier le tableau en réallouant de la mémoire. L’autre
constructeur permet d’initialiser un objet au moment de sa déclaration. Grâce à la surcharge de
définition, le compilateur saura quel constructeur appeler suivant le nombre et le type des arguments.
Il est important de noter qu’un constructeur n’a pas de type de retour, pas même void. De plus, comme
on peut créer plusieurs constructeurs, il est très important qu’ils aient tous un prototype différent, ie
qu’il n’y ait pas d’ambiguité possible entre eux.
Le constructeur par copie est appelé à chaque fois qu’un objet de la classe est passé par valeur
en argument d’une fonction. De plus il sert également de base à l’opération d’affection = quand il
est utilisé dans une déclaration affectation. Par contre l’opérateur d’affectation utilisé dans son cadre
classique, c’est-à-dire Objet1 = Objet2, n’est pas basé sur le constructeur par copie. Mal redéfinir le
constructeur par copie peut donc avoir de nombreux effets de bord difficilement traçable.
La redéfiniton de l’opérateur d’affectation est aussi à faire dans certains cas, voir la section sur la
surcharge d’opérateurs.
Avec notre exemple, on a 3 façons de déclarer un objet de classe complexe :
De plus, on note que notre premier constructeur est tout simplement le troisième avec comme
arguments (0,0). Ce cas de figure se reproduit assez souvent et on peut aisément regrouper ces 2
constructeurs en un seul en prenant avantage de la possibilité de définir des arguments par défauts.
Ainsi, les premier et troisième conctructeurs pourront se réécrire en un seul :
/* Dans complexe.H */
...
complexe(double=0,double=0);
/* Dans [Link] */
...
complexe::complexe(double x, double y) {re = x; im = y;}
Le pendant des constructeurs est le destructeur. Il permet de libérer la mémoire allouée à un objet
et est appelée automatiquement par le compilateur. Comme pour un constructeur, le destructeur ne
renvoie rien, même pas void. Il se nomme obligatoirement ˜NomClasse et n’admet aucun argument.
Dans notre exemple, la définition du destructeur serait vide puisqu’aucun type complexe n’est utilisé
dans la classe complexe (pas de tableaux, de chaı̂nes, de listes ..., ie. un objet nécessitant une allo-
cation explicite de mémoire). Finalement, il est conseillé de préfixer la déclaration du destructeur par
le mot-clé virtual, le pourquoi sera expliqué plus tard. Dans notre cas, on aurait :
virtual ~complexe();
...
// Definition
...
complexe::~complexe()
{ }
Ensuite, dans le cas d’un attribut statique, il ne faut pas oublier de l’initialiser dans le fichier
d’implémentation. Ceci se fait simplement par Type NomClasse::NomVarStatique = Valeur.
A noter qu’on ne peut avoir qu’une seule initialisation d’un attribut statique. De plus, une méthode
statique étant liée à la classe et pas à un de ses objets, elle n’a accès qu’aux variables statiques de
la classe, pas aux attributs de l’objet (ie pas de this dans la définition d’une méthode statique ni
d’accès aux attributs non statiques). Dans le cas d’un compteur, il faut en général l’incrémenter à
chaque création d’un objet, donc il faut s’assurer que tous les moyens de création d’objet font bien
l’incrémentation (ici il manque la redéfinition du constructeur par copie). De plus, il faudrait également
2.3. LES CLASSES 55
s’assurer qu’à chaque fois qu’un complexe est détruit, le compteur soit bien décrémenté, ce qui se
fera dans le destructeur.
Voici un exemple d’utilisation de notre classe avec compteur.
Exemple 2.3.15 (Membre statique : exemple d’utilisation).
#include <iostream>
#include "complexe.H"
int main(){
complexe p1(1,2);
complexe p2(3,4);
complexe * p3;
std::cout << "Il y a " << complexe::compteur() // renverra 2
<< " complexes en circulation\n";
p3 = new complexe(5,6);
int n = [Link](); // renverra 3
delete p3;
return 0;
}
On notera que la méthode statique compteur peut soit être appelée directement par complexe::compteur()
soit être appelée sur un objet de type complexe, ces 2 appels étant strictement équivalents puisque
la méthode statique n’est pas attachée à un complexe particulier mais à toute la classe.
Un membre statique peut être vu comme une variable globale mais privée ou protégée.
/* Definition ’[Link]’ */
...
double complexe::Re() const {return re;}
couble complexe::Im() {return im;}
/* Utilisation ’[Link]’ */
#include ...
56 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
int main(){
const complexe A(1,2);
complexe B(3,4);
double reA = [Link](); // autorisee car methode constante
double imA = [Link](); // interdit
double reB = [Link](); // autorisee
double imB = [Link](); // autorisee
...
}
Ainsi, une méthode constante peut s’appliquer sur n’importe quel objet, qu’il soit constant ou
pas. Par contre, un objet constant ne peut se voir appliquer qu’une méthode constante. Il est donc
important de déclarer les méthodes comme constantes si elles le sont, c’est notamment le
cas des accesseurs.
Après ces déclarations d’amitiés, la définition de l’opérateur ’*’ pourra contenir des accès directs
aux attributs privés x et y du point en argument. De même, la classe complexe pourra utiliser tous
les membres privés de la classe point si jamais elle a affaire à un objet de ce type (par exemple pour
construire un complexe à partir d’un point).
A Op B
Avec cette syntaxe, l’ordre des opérandes est tout aussi important que pour la surcharge externe
puisque c’est sur la première que s’applique la méthode. La surcharge interne d’opérateurs est donc
particulièrement bien adaptée aux opérateurs qui modifient l’objet sur lequel ils travaillent, comme
par exemple les opérateurs =, +=, ++, etc, mais aussi pour les opérateurs dont la première opérande
est de la classe dans laquelle on surcharge l’opérateur.
Cerains opérateurs définis en internes renverront l’objet sur lequel ils travaillent, ce qui est possible
grâce au pointeur this. Toujours pour notre classe complexe, on peut déclarer et définir les opérateurs
+ et += comme suit :
/* Implementation: [Link] */
#include <complexe.H>
...
complexe complexe::operator+(const complexe &p) const {
return complexe(re+[Link],im+[Link]);
}
complexe complexe::operator+=(const complexe &p) {
re += [Link];
im += [Link];
return *this;
}
/* Utilisation */
...
complexe A(1,2), B(4,5), C;
A += B;
C = A + B;
...
58 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
On notera qu’on retourne l’objet sur lequel on travail, qui est *this (vu que this est un pointeur
et non l’objet lui-même).
Certains opérateurs ne peuvent cependant pas être surchargés, il s’agit de ’ : :’, ’.’, ’.*’, ’ ? :’, ’sizeof’,
’typeid’, ’static cast’, ’dynamic cast’, ’const cast’ et ’reinterpret cast’.
Les opérateurs de post/pré incrémentation/décrémentation ++ et −− se surcharge d’une manière
un peu particulière. En effet, quand ils sont préfixés ils peuvent être déclaré comme interne et dans
ce cas ils ne prennent par d’arguments (autre que l’objet sur lequel ils sont appliqués). Du coup, il
faut un moyen de différencier les − − / + + postfixés des préfixés. Ce moyen est l’utilisation d’un
paramètre de type entier qui ne servira qu’à indiquer que l’opérateur −− ou ++ est postfixé plutôt
que préfixé. Cet argument entier ne servira pas dans la définition de la surcharge. Bien que pour notre
classe complexe ces opérateurs n’ont pas vraiment de raison d’être, les voici tout de même.
/* Implementation: [Link] */
#include "complexe.H"
...
complexe complexe::operator++(int dummy){ // on choisit d’incrementer im
im++;
return *this; // post incrementation, on retourne le complexe apres modification
}
complexe complexe::operator++(){ // on choisit d’incrementer re
complexe x(*this); // copie avant modification
re++;
return z; // pre incrementation, on retourne le complexe avant modification
}
/* Utilisation */
...
complexe A(1,2), B;
B = A++; // => B = complexe(1,2) et A = complexe(1,3)
B = ++A; // => B = complexe(0,3) et A = complexe(0,3)
...
Pour finir, un opérateur qu’il est quelquefois utile de surcharger (en tout cas pour les classes
numériques) est le ’<<’ qu’on a déjà pu voir dans l’utilisation du cout. Si par exemple on veut pouvoir
écrire std :: cout << complexe(1, 2) pour afficher à l’ecran 1 + (2)i, on pourra surcharger << de la
façon suivante :
Ceci représente une surcharge externe d’un flot de type ostream. La surcharge est forcément externe
car le flot sur lequel écrire le complexe sera toujours l’opérande à gauche du <<.
/* Utilisation */
#include "complexeT.H"
int main(){
complexeT <double> A(1.3,2.1); // complexe double
complexeT <int> B(1,2); // complexe entier
...
}
On notera que lors de la définition d’un objet de la classe complexeT, on spécifie le type utilisé
en l’encadrant par <...>. Les classes templates sont surtout utilisées pour des listes, des tableaux, des
piles, des files, ...
2.4 Héritage
2.4.1 Introduction
L’héritage est une technique clef de la programmation objet et sert principalement à répondre
au souci de réutilisation. Cette technique permet de copier virtuellement les caractéristiques d’une
(héritage simple) ou plusieurs (héritage multiple) classes déjà existantes dans la définition d’une nou-
velle classe.
L’héritage peut servir plusieurs intérêts dont les principaux sont :
— l’extension d’une classe,
— la spécialisation d’une classe,
— l’implantation d’une classe abstraite,
— l’adaptation d’une classe.
Ces quatre points sont très proches les uns des autres et peuvent facilement être confondus. Cependant,
quelque soit le but d’un héritage, la méthode reste la même : la dérivation.
Pour savoir si une classe hérite d’une autre, on peut utiliser un simple test sémantique. On dit
qu’une classe B hérite d’une classe A si on peut dire la chose suivante :
Un objet de la classe B est un objet de la classe A
60 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
Dans ce cas, on dit que la classe A est la classe mère ou encore la classe de base. La classe B se
nomme alors classe fille ou encore classe dérivée.
2.4.2 Dérivation
On dit qu’une classe B dérive d’une classe A si la classe B hérite de la classe A. Créer une classe
fille se fait grâce à la syntaxe suivante :
Lors d’un héritage, la classe fille possède tous les attributs et méthodes de la classe mère. L’option
<mode dérivation> permet de déterminer quelles seront les portées des membres de B, hérités de A.
Les 3 modes de dérivation sont les 3 portées définies préalablement, ie. public, protected et private.
L’effet de ces modes de dérivation est :
— public : les membres publiques et protégés de la classe mère conservent leur portée, les membres
privés deviennent inaccessibles (il faudra passer par les accesseurs).
— protected : les membres publiques et protégés de la classe mère deviennent protégés, les
membres privés inaccessibles.
— private : les membres publiques et protégés de la classe mère deviennent privés, les membres
privés deviennent inaccessibles.
Le mode de dérivation par défaut est private, mais le mode de dérivation le plus courant est public
puisqu’il donne aux membres dérivés les même statuts que ceux de la classe de base.
Cependant, quelque soit le mode de dérivation, les membres privés de la classe mère deviennent
inaccessibles dans la classe fille. Il est donc conseillé d’utiliser le statut protected plutôt que private
si l’on souhaite pouvoir manipuler directement tous les membres d’une classe fille sans avoir à passer
par les méthodes.
Voici un exemple académique de plusieurs héritages.
Dans notre exemple, la classe derive1 est fille de la classe base1 qui est alors appelée classe de
base de derive1. La classe derive2 possède 2 classes de base qui sont base1 et base2. La classe derive3
possède 3 classes de base qui sont base1, base2 et derive2. On notera que derive3 hérite en fait 2 fois
de base1 et base2 mais à des niveaux différents.
Dans la suite de cette section, on ne s’intéressera qu’au cas de l’héritage simple.
class point2d {
public:
...
double distance_origine() const;
...
protected:
double x;
double y;
};
// Utilisation
int main() {
point2d p1(2,3); // en supposant qu’on a defini le constructeur necessaire
point3d p2(1,2,3); // idem
double dp1, dp2, dp3;
Dans cet exemple on notera que la méthode de la classe de base est toujours utilisable grâce a
l’utilisation de l’opérateur de résolution de portée ’::’. De plus, la méthode redéfinie doit absolument
avoir la même interface que la méthode de la classe de base (sinon on fait de la surcharge, pas de la
redéfinition).
L’opérateur de résolution de portée ’::’ sert également à récupérer un attribut appartenant à la
classe mère, alors qu’il a été redéfini dans la classe fille. Par exemple :
Exemple 2.4.3.
// Interfaces (.H)
class mere {
protected:
int i;
...};
protected:
int i;
...};
// Implementation (.cpp)
...
void fille::une_methode() {
cout << "donnee de la mere = " << mere::i << endl;
cout << "donnee de la fille = " << i << endl;
}
class B: public A {
public:
B(int = 0, int = 0);
...
virtual ~B();
private:
int arg2;
};
B::B(int val1, int val2) : A(val1) {arg2 = val2; cout << ’’constructeur B’’;}
B::~B() {cout << ’’destructeur B’’;}
// Utilisation
...
int main(){
B objB1(1);
// Resultat: constructeur A
// constructeur B
B *pobjB;
pobjB = new B(objB1); // appel des constructeurs par recopie par defaut
// de la classe A puis de la classe B
delete pobjB;
// Resultat: destructeur B
// destructeur A
...}
On notera que grâce à l’opérateur ’ :’, on peut préciser quel constructeur de la classe de base
appeler. Si aucun n’est spécifié, ce sera le constructeur par défaut qui sera appelé (attention si le
constructeur par défaut de la classe mère n’existe pas).
2.4.5 Polymorphisme
Dans le cas d’une dérivation, certaines conversions implicites sont définies par défaut. Si B est une
classe dérivée de A, alors les conversions suivantes sont implicites :
— un objet de type B vers un objet de type A,
— un pointeur sur un objet de type B vers un pointeur sur un objet de type A,
— une référence sur un objet de type B vers une référence sur un objet de type A.
Ces trois conversions sont tout à fait naturelles puisqu’un objet de type B est également un objet
de type A. La première conversion est une conversion d’objet où seuls les attributs présents dans le
type A sont pris en compte alors que les autres (ceux qui n’appartiennent qu’au type B) sont ignorés.
Dans les deux autres cas, les objets pointés ou référencés ne sont pas modifiés, c’est juste leur type
qui change (en particulier les attributs propre au type B ne sont pas définitivement perdus). De plus,
ces conversions de type impliquent qu’un pointeur sur type A peut pointer vers un objet de type B
et qu’on peut retrouver un pointeur sur type B à condition de faire une conversion explicite. Cette
versatilité des pointeurs est appelée polymorphisme. Voici un exemple de polymorphisme utilisant les
classes point2d et point3d déclarées précédemment (on suppose que les constructeurs nécessaires ont
été définis).
int main(){
// objets
point2d p1(1,2);
point3d p2(3,4,5);
double d;
// pointeurs
point2d *pp1 = new point2d(1,2);
point3d *pp2 = new point3d(3,4,5);
Dans notre exemple, nous procédons à la conversion explicite d’un pointeur sur un objet de type
point2d vers un pointeur sur un objet de la classe fille (ie plus spécifique avec plus d’attributs). Cette
conversion n’est permise que parce que l’objet pointé est en réalité du type point3d et possède en
particulier tous les attributs nécessaires. Ce genre de conversion peut s’avérer très risquée et il peut
être utile de créer dans la classe de base un attribut permettant d’identifier la place de l’objet manipulé
dans la hiérarchie d’héritage. Un tel attribut peut être utilisé afin de vérifier que la conversion utilisée
a un sens.
Une autre manière de convertir un pointeur sur un objet d’une classe mère vers un pointeur sur
un objet d’une classe dérivée est d’utiliser de nouvelles spécifications du C++, à savoir les nouveaux
casts (un cast est une conversion explicite) :static cast < T > (expr), const cast < T > (expr),
dynamic cast < T > (expr) ou encore reinterpret cast < T > (expr). Ces casts sont basés sur
la fonctionnalité RTTI (RunTime Type Identification) et permettent des conversions de type moins
ambiguë et en toute sécurité. Nous n’entrerons cependant pas dans ces détails car ils dépasseraient la
portée d’une simple introduction au C++.
Exemple 2.4.6 (Solution statique à l’application d’une méthode à une famille d’objet).
2.4. HÉRITAGE 65
...
const int TAILLE = 100;
int main(){
point2d* tabpts[TAILLE];
point3d *aux;
// Initialisation du tableau avec des pointeurs de types point2d et point3d
Cette solution n’est pas très élégante et doit être modifiée après tout enrichissement de la hiérarchie
d’héritage, comme par exemple si on rajoutait une classe fille de point2d ou point3d. La solution
statique pose donc de vrais problèmes de maintenance.
Une solution plus élégante consiste à utiliser la liaison dynamique du C++. Pour ce faire, il suffit
de déclarer la méthode qu’on souhaite exécuter de manière dynamique à l’aide du mot-clé virtual. La
déclaration d’une méthode comme virtuelle se fera dans la classe de base et toutes les redéfinitions
de cette méthode seront automatiquement virtuelles. Une fois une méthode déclarée comme virtuelle,
c’est lors de l’exécution et non de la compilation qu’il sera décidé quelle méthode appliquer en fonction
de l’objet sur lequel elle est appelée. Notre solution dynamique sera alors :
Exemple 2.4.7 (Solution dynamique de l’application d’une méthode à une famille d’objets).
// dans l’interface de la classe point2d
class point2d{
public:
virtual void affiche() const;
...
}
int main(){
point2d* tabpts[TAILLE];
// Initialisation du tableau avec des points de types point2d et point3d
On a ainsi déclaré une classe Figure qui servira de cadre générique. Un objet d’une classe dérivée
de Figure pourra toujours être affiché.
La déclaration d’une méthode virtuelle pure doit toujours être suivi de ’= 0’, comme illustrée
dans l’exemple.
La fonction affiche n’a pas de définition dans la classe Figure. Cette définition devra être
donnée dans les classes dérivées.
Une méthode virtuelle pure étant avant tout une méthode virtuelle, elle peut être utilisée dans le
cadre de la liaison dynamique.
Attention, le fait de déclarer une méthode virtuelle pure dans une classe interdit toute instan-
ciation de cette classe. En particulier, dans notre exemple, il est impossible de créer un objet de
type Figure. On peut cependant créer un pointeur sur un objet de type Figure, mais on ne fera
jamais d’allocation mémoire correspondant au type Figure. On fera plutôt du polymorphisme en
affectant au pointeur de type Figure*, de la mémoire pour un objet d’une classe fille de Figure,
qui elle ne possèdera aucune méthode virtuelle pure et en particulier aura donné une définition à la
méthode affiche.
2.5 Entrées/Sorties
ios base et ses filles
En C++, les entrées/sorties sont gérées grâce à la classe de base ios base et à ses filles. Toute la
gestion est basée sur la notion abstraite de flux (stream) qui représente un médium (l’écran, le clavier,
un fichier ou même une chaı̂ne de caractère string) sur lequel sont effectuées les opérations. Les classes
les plus utiles pour les manipulations de flux sont :
2.5. ENTRÉES/SORTIES 67
— iostream : pour l’écriture et la lecture sur la sortie standard (écran) et à partir de l’entrée
standard (clavier).
— ifstream : pour la lecture dans un fichier (Input File Stream).
— ofstream : pour l’écriture dans un fichier (Output File Stream).
— fstream : pour la lecture/écriture dans un fichier (File Stream).
— sstream : pour la manipulation d’objet de classe string comme s’il s’agissait de flux (String
Stream).
int main(){
// Ouverture avec le constructeur
ifstream f("[Link]",ios::in);
if (f) {
//operations sur le fichier
[Link]();}
else
cerr << "Erreur d’ouverture du fichier\n";
On voit dans cet exemple qu’il faut également refermer le flux une fois qu’on n’en a plus besoin,
et ce à l’aide de la méthode close(). On notera que lors de l’ouverture, le mode d’ouverture ios::in est
facultatif car il est en fait le mode d’ouverture par défaut pour ifstream.
Dans notre exemple, on peut remarquer que si on souhaite utiliser une chaı̂ne de caractère string
comme argument pour l’ouverture (que ce soit avec le constructeur ou avec la méthode open), il faut
la convertir en un char* à l’aide de la méthode c str().
Une fois un fichier ouvert en lecture, on a principalement 3 fonctions pour effectivement lire dedans
(on en a bien plus mais celles-ci seront les plus utilisées) :
— std::ifstream flux>> variable : récupération à partir du fichier (enfin le flux associée) jusqu’à
un délimiteur (espace, nouvelle ligne).
— getline(std::ifstream flux, string s[, char c]) qui lit le contenu du fichier jusqu’à rencontrer le
caractère c et place le résultat dans la chaı̂ne de caractère s. Le dernier argument est facultatif
et vaut par défaut ’\n’, ie que par défaut on lit toute la ligne du fichier.
— [Link](char) qui lit un seul caractère et le place dans la variable. Attention, les espaces, les
sauts à la ligne ... sont considérés comme des caractères.
Voici un exemple d’utilisation de ces 3 fonctions, sur le fichier [Link] :
// code
#include <iostream>
#include <fstream>
#include <string>
int main(){
ifstream f("[Link]",ios::in);
if (f) {
string s;
getline(f,s);
cout << s << endl; //ecrit "Ceci est un fichier de test"
char c;
[Link](c); // c vaut le caractere ’a’
int n, m;
f >> n >> m >> s; // n vaut 45, m vaut 13 et s vaut "machin"
[Link]();
}
else
cerr << "Impossible d’ouvrir le fichier\n";
// Affichage de tout le fichier
[Link]("[Link]",ios::in);
if (f) {
string stmp;
while(getline(f,stmp))
2.5. ENTRÉES/SORTIES 69
Dans la dernière partie de l’exemple, on voit qu’on peut tester la réussite de la fonction getline
pour tester si on a atteint la fin du fichier ou non.
int main(){
ofstream f("[Link]",ios::out | ios::trunc);
if (f) {
string phrase = "Ma suite de fibonacci commence par";
f << phrase << endl;
70 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
On voit que l’écriture dans un fichier ne diffère pas beaucoup de l’écriture à l’écran, la seule
différence étant la destination (un ostream ou ofstream).
[Link](20,ios::cur)
positionne le curseur au 20 ème octet du fichier. Mais attention, ceci ne correspond pas forcément au
20 ème caractère, car le fichier n’est pas forcément écrit en ASCII.
Divers
Ouverture d’un fichier en lecture et écriture Il est possible d’ouvrir un fichier en lecture et
en écriture à la fois. Ceci n’est probablement utile que quand on utilise le positionnement du curseur
ou quand on veut lire tout un fichier pour ensuite écrire à sa suite (ou par dessus). L’ouverture d’un
fichier en lecture et écriture se fait l’aide d’un flux de classe fstream. La syntaxe du constructeur est
la même que pour la lecture seule ou l’écriture seule, il n’y a que le mode d’ouverture qui diffère. La
syntaxe typique est :
Dans ce cas, on ne peut pas utiliser le mode ios::app. Par contre, il faut absolument utiliser ios::trunc
ou ios::ate. De plus, le fichier qu’on ouvre doit impérativement exister.
Ensuite, la lecture et l’écriture se font comme pour les fichiers ouverts en lecture seule ou en écriture
seule.
2.6. LA BIBLIOTHÈQUE STL 71
Quelques fonctions utiles Voici quelques méthodes qui peuvent s’avérer utiles :
— [Link]() qui renvoie true si on se trouve à la fin du fichier. En réalité, elle renvoie la valeur
du bit eofbit qui est passée à true quand le curseur atteint la fin du fichier.
— [Link]() remet tous les drapeaux (comme eofbit par exemple) à leur valeur d’origine. Sur-
tout utile pour revenir au début du fichier.
— [Link]() renvoie true si le fichier n’a pas été ouvert correctement (mais un test direct sur le
flux fait la même chose).
Pour se déplacer dans un conteneur, on utilise ce qu’on appelle un itérateur (iterator ) qui est
une sorte de pointeur sur un des éléments du conteneur. Nous en verrons quelques exemples dans les
sections suivantes.
Pour utiliser ce conteneur, il faut inclure la bibliothèque <vector >. Un vecteur est un tableau
dont l’espace de stockage est géré dynamiquement et dont les éléments sont stockés dans un espace
contigu de la mémoire. La gestion de la mémoire est effectuée automatiquement et l’utilisateur peut
donc aisément raccourcir ou rallonger son tableau, sans souci de fuite de mémoire ou d’allocation de
mémoire manuelle (par new, malloc ou ...).
Voici un exemple d’utilisation de ce type de conteneurs.
Comme tous les conteneurs, un vector à une taille qui correspond aux nombres d’éléments qu’il
contient et qu’on obtient à l’aide de la méthode size. Mais un vector possède également ce qu’on appelle
une capacité et qu’on obtient grâce à la méthode capacity. Cette capacité correspond à l’espace que le
vector réserve vraiment en mémoire. Cet espace est en général supérieur à celui dont le vector a besoin
mais permet d’anticipé les éventuelles réallocation dynamique et ainsi d’être plus performant. Par
exemple, si on déclare un vector de 100 entiers, le programme pourra choisir de réserver 150 espaces
mémoire contigus de la taille d’un entier au cas ou l’utilisateur veut accroı̂tre la taille du vector.
2.6. LA BIBLIOTHÈQUE STL 73
• dequeue : Ce nom est un acronyme pour double-ended queue et est une sorte de conteneur
séquentiel. Ce qui la rapproche d’une file (queue) est que les premier et dernier éléments d’une
dequeue sont les plus rapide à être accèdés. Par contre, contrairement à une file ou pile, tous les
éléments d’une dequeue sont accessibles directement, on peut également itérer sur une dequeue
à l’aide d’une itérateur sans pour autant devoir vider la dequeue. En particulier, on peut utiliser
les méthodes d’accès [] et at, comme pour un vector.
• priority queue : Ce conteneur correspond à une file prioritaire où le premier élément est
toujours le plus grand que la file contient. La relation d’ordre permettant de définir la notion
de plus grand est soit fournie lors de la déclaration soit est l’opérateur < si celui-ci est défini
pour le type des éléments à stocker. Attention, la bibliothèque à inclure pour pouvoir utiliser
une file prioritaire est < queue >. Un exemple d’utilisation :
Exemple 2.6.5 (Exemple d’utilisation d’une priority queue).
priority_queue<int> PQ; // file prioritaire vide, d’entier
[Link](1);
[Link](2);
[Link](0);
while (![Link]()) {
cout << [Link]() << " ";
[Link]();
} // affiche: 2 1 0
Pour l’utilisation d’une file prioritaire avec une fonction de comparaison fournie par l’utilisateur,
nous renvoyons le lecteur à un manuel de référence.
2.6. LA BIBLIOTHÈQUE STL 75
Les algorithmes
L’inclusion de l’entête <algorithm> permet d’utiliser tout un éventail de fonctions pouvant agir
sur un tableau ou quelques uns des conteneurs de la STL. Il est important de noter que ces algorithmes
n’agissent que sur les éléments des conteneurs (ou du tableau) et ne peuvent en aucun cas en modifier
la structure (pas de rajout d’éléments par exemple).
Voici une sélection de ces fonctions :
• Algorithmes ne modifiant pas le conteneur :
— Function for each (InputIterator first, InputIterator last, Function f ) : Applique la fonction
f à tous les éléments de l’ensemble [f irst, last]. à une collection d’éléments.
— InputIterator find (InputIterator first, InputIterator last, const T& value) : Renvoie un
itérateur sur le premier élément de l’ensemble [f irst, last] égale à value. Si un tel élément
n’existe pas, renvoie l’itérateur sur l’elément juste après last.
76 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
Remplace toutes les valeurs comprises entre first et last qui satisfont le prédicat pred et ce
par la valeur new value.
— void generate ( ForwardIterator first, ForwardIterator last, Generator gen) : Remplace les
valeurs comprises entre first et last par celles générées par l’appel consécutifs de la fonction
gen.
— ForwardIterator remove ( ForwardIterator first, ForwardIterator last, const T& value ) :
Retire de l’ensemble pointé par first et last, toutes celles égales à value. La fonction retourne
un itérateur/pointeur sur la fin du nouvelle ensemble (qui est plus court que celui d’origine).
— ForwardIterator remove if ( ForwardIterator first, ForwardIterator last, Predicate pred) :
Même idée que le remove mais cette fois on retire un élément qui satisfait le prédicat pred.
— ForwardIterator unique ( ForwardIterator first, ForwardIterator last, BinaryPredicate pred) :
Retire les éléments qui sont égaux à leur voisin. Le teste d’egalité se fait avec la relation de
comparaison pred (par défaut cette relation est l’opérateur ==).
• Algorithmes de tri et de recherche :
— void sort ( RandomAccessIterator first, RandomAccessIterator last, Compare comp) : Trie
les éléments compris entre les pointeurs first et last suivant la relation de comparaison comp.
Par défaut cette dernière est l’opérateur <.
— ForwardIterator lower bound ( ForwardIterator first, ForwardIterator last, const T& value,
Compare comp ) : Retourne un pointeur sur le premier élément de l’ensemble trié [first,last]
qui ne soit pas inférieur à la valeur value. La comparaison se fait grâce à la fonction comp
qui par défaut est prise comme l’opérateur <.
— ForwardIterator upper bound ( ForwardIterator first, ForwardIterator last, const T& value,
Compare comp ) : Retourne un pointeur sur le premier élément de l’ensemble trié [f irst, last]
qui soit supérieur à value. La comparaison se fait grâce à la fonction comp qui part défaut
est prise comme étant l’opérateur <.
— bool binary search ( ForwardIterator first, ForwardIterator last, const T& value, Compare
comp) : Teste si la valeur value est présente dans l’ensemble [f irst, last]. Le teste d’égalité
est effectué avec la fonction de comparaison comp ou par défaut avec l’opérateur <.
• Algorithmes de fusion :
— OutputIterator merge (InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, In-
putIterator2 last2, OutputIterator result, Compare comp) : Fusionne les deux ensembles
ordonnés [f irst1, last1] et [f irst2, last2] en un nouvelle ensemble ordonné commençant en
result. La relation de comparasion utilisée est comp ou < par défaut.
— bool includes ( InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, InputItera-
tor2 last2, Compare comp) : Teste si tous les éléments de [f irst2, last2] se trouvent dans
[f irst1, last1], en utilisant la relation de comparaison comp ou < par défaut.
— OutputIterator set union (InputIterator1 first1, InputIterator1 last1, InputIterator2 first2,
InputIterator2 last2, OutputIterator result, Compare comp) : Réalise l’union entre les en-
sembles ordonnés [f irst1, last1] et [f irst2, last2] et la place en result. La relation utilisée
est comp ou par défaut <. La fonction renvoie l’itérateur/pointeur sur le dernier élément
de l’union.
— OutputIterator set intersection (InputIterator1 first1, InputIterator1 last1, InputIterator2
first2, InputIterator2 last2, OutputIterator result, Compare comp) : Réalise l’intersection
entre les ensembles ordonnés [f irst1, last1] et [f irst2, last2] et place l’intersection en result.
La comparaison est faite grâce à comp ou à < si comp n’ets pas spécifié. La fonction retourne
l’itérateur/pointeur sur le dernier élément de l’intersection.
• Algorithme d’extrémalité :
— const T& min ( const T& a, const T& b, Compare comp) : Retourne le minimum entre les
deux valeurs a et b. La relation de comparaison est comp ou < si comp n’est pas spécifié.
— const T& max ( const T& a, const T& b, Compare comp) : Idem que min mais renvoie le
78 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++
maximum.
— ForwardIterator min element (ForwardIterator first, ForwardIterator last, Compare comp
) : Renvoie un itérateur sur le plus petit élément de l’ensemble [f irst, last].
— ForwardIterator max element (ForwardIterator first, ForwardIterator last, Compare comp) :
Idem que min element mais renvoie la valeur maximum.
Tous les objets string se trouve dans l’espace de nommage standard std. Il faut donc bien penser à
utiliser le using namespace std ; avant le code ou a préfixer toutes les références à un objet ou méthode
de string par std ::.
Voici un aperçu des méthodes disponibles pour la classe string :
• Constructeur(s) : On peut construire un objet de type string de plusieurs manière différente,
en voici les plus utiles :
string s1("Aloha le monde"); // ca devrait etre clair
string s2 = "Aloha the world"; // ca aussi
string s3(s1); // constructeur par copie
string s4(s2,6); // copie des 6 premiers caracteres de s2
string s5(s2,7,9); // copie des caracteres 7 a 9 de s2
char s[10] = "bonjour";
string s6(s); // ca devrait etre limpide
• Affectation : L’opérateur d’affectation est bien entendue définie : s2 = s1 si s1 et s2 sont des
string. Mais aussi si s1 est un char* ou simplement un char.
• Itérateurs : Un string étant un conteneur, on peut le manipuler avec des itérateurs qui poin-
teront sur un des caractères de la chaı̂ne. On a pour ça les méthodes suivantes :
string s("Aloha");
string::iterator it; // un iterateur
it = [Link](); // iterateur sur le premier caractere
it = [Link](); // iterateur sur le caractere juste apres le dernier
string::reverse_iterator rit; // un iterateur inverse
rit = [Link](); // iterateur inverse sur le dernier caractere
rit = [Link](); // iterateur inverse sur l’emplacement juste avant le premier de s
for(it=[Link]();it!=[Link]();it++)
cout << *it; // pour afficher la chaine ’s’
for(rit=[Link]();rit!=[Link];rit--)
cout << *rit; // afficher la chaine ’s’ a l’envers
cout << endl << "Je viens de faire un palindrome!\n";
• Capacités : On peut aisément connaı̂tre, manipuler ou tester la capacité d’un string. Un
exemple est le suivant :
2.7. AUTRES BIBLIOTHÈQUES UTILES 79
string s("Aloha");
cout << "Taille (par size) = " << [Link]() << endl; // retourne 5
cout << "Taille (par length) = " << [Link]() << endl; // idem
cout << "Taile maximum = " << s.max_size() << endl; // taille maximum
cout << "Capacite = " << [Link]() << endl; // capacite
[Link](10); // on reserve de la memoire pour 5 caracteres supplementaires
[Link](4); // la chaine ne comporte plus que 4 caracteres, les 4 premiers
[Link](10,’a’); // on reserve de la memoire pour 6 caracteres de plus
// et ces 6 caracteres sont initialises a ’a’
[Link](); // vide la chaine de caracteres
if ([Link]()) // teste si ’s’ est vide (c’est le cas)
cout << "La chaine est vide!\n";
La taille maximum retournée par max size dépend de l’état actuel de la mémoire mais est
en général très importante (de quoi contenir un bouquin). La capacité donnée par capacity
correspond, comme pour un vector à l’espace mémoire que le constructeur à réservé pour stocké
s et qui est en général un tout petit peu plus important que le strict minimum nécessaire en
prévision des manipulations à venir.
• Accès aux éléments : Pour accéder à un élément d’une chaı̂ne de caractère, on peut (en plus
des itérateurs) utiliser [] et at. Attention, on reste dans la logique des tableaux et le premier
élément est en position 0.
string s("Aloha");
cout << s[0]; // premier element
sout << [Link](1); // second element
• Modifications : On possède plusieurs méthodes ou opérateurs pour modifier un string. Voici
un exemple d’utilisation de ces opérateurs :
string s1("Aloha"), s2("le"), s3("Monde");
string s;
s = s1 + " " + s2; // s = "Aloha le"
s += " "; // s = "Aloha le "
[Link](s3); // s = "Aloha le Monde"
s.push_back(’!’); // ajoute le caractere ’!’, donc s = "Aloha le Monde!"
[Link](s1,4); // s recoit les 4 premiers caracteres de s1, ie s = "aloh"
[Link](s1,2,3); // s recoit les 3 caracteres de s1 a partir du second,
// donc s = "loh"
[Link](6,’*’); // s recoit 5 fois le caractere ’*’, ie s = "******"
[Link](4,s1); // insert s1 a la 4eme position de s, ie s = "***Aloha***"
// fonctionne egalement avec des iterateurs
[Link](4,5); // efface les 5 caracteres en commencant a la position 4,
// donc s = "******"
// fonctionne egalement avec des iterateurs
On a également les méthodes replace, copy et swap qui peuvent être utiles et qui peuvent être
appelées de plusieurs façon différentes.
• Opérations : Voici quelques opérations utiles sur les string :
char *cs; // une chaine de caracteres C
string s("Oh la jolie phrase");
cs = new char[[Link]()+1];
strcpy(cs,s.c_str()); // transforme s en char*, vous connaissez deja strcpy
string s1("phrase");
size_t ici;
ici = [Link](s1); // cherche "phrase" dans la chaine s, renvoie la position
80 CHAPITRE 2. CONCEPTION OBJET ET LANGAGE C++