Poly CXX
Poly CXX
Poly CXX
Kévin Santugini
4
page 5 Introduction au C++ par Kévin Santugini
3 Les variables 26
3.1 Les pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.2 Les références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.3 Les structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.4 Pointeurs sur des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.5 Déclarations compliquées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.6 Les typedefs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.7 Durée de vie d’une variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.7.1 Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.7.2 Variables locales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.7.3 Variables locales statiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.7.4 Les variables dynamiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8 Conseils et conclusion 69
1 Généralités sur la programmation
Pour programmer, il est utile de connaître quelques généralités trop peu souvent expliquées car
« allant de soi ». Leur méconnaissance peut donner lieu à de désagréables surprises.
1.1.1 La compilation
La compilation est l’opération qui consiste à traduire un fichier source, i.e. un fichier contenant le
programme dans un format humainement lisible, en un exécutable qui lui, contient le programme sous
une forme directement utilisable par la machine. Cette opération est effectuée par un compilateur.
Une fois créé l’exécutable, le compilateur n’est plus nécessaire au déroulement du programme.
1.1.2 L’interprétation
Une autre possibilité d’exécuter un programme est de l’interpréter. Un interpréteur exécute le
programme au moment même où il lit le source. Contrairement à ce qui se passe pour la compilation,
l’interpréteur est nécessaire chaque fois que l’on souhaite utiliser le programme. Il est plus performant
de compiler un programme que de l’interpréter.
7
Chapitre 1. Généralités sur la programmation page 8
1.2 Le typage
1.2.1 Qu’est ce que le typage ?
Imaginons deux variables x et y flottants stockées sur 32 bits ainsi que deux variables entières
m et n elles aussi stockées sur 32 bits. Supposons que les représentations binaires de x et de n
soient identiques et que les représentations binaires de y et de m soient aussi identiques alors il n’est
pas vrai en général que les représentations binaires de x + y et de m + n soient aussi identiques.
L’addition flottante et l’addition entière sont des opérations différentes sur la machine. Cependant,
dans la plupart des langages, ces opérations sont représentées par le même opérateur +. C’est au
moment de la compilation, suivant le type de chaque variable que le compilateur décide de traduire
l’opération + par l’opération machine appropriée.
Outre cette facilité de programmation, un autre intérêt du typage est de permettre au compilateur
de repérer un certain nombre d’erreurs : un compilateur signalera des opérations entre des types
incompatibles. Par exemple, un compilateur détectera et vous avertira si par mégarde vous avez
programmer une opération n’ayant aucun sens comme la division d’un entier par une chaîne de
caractères.
Des exemples courants de types sont
cel-00725771, version 1 - 27 Aug 2012
• booléen
• nombre entier
• nombre flottant
• caractère
• chaîne de caractères
La liste est loin d’être exhaustive. Il serait d’ailleurs illusoire d’essayer d’en établir une.
séparément en un fichier objet. Une seconde opération appelée édition de liens permet alors de
regrouper tous les fichiers objets en un seul exécutable. Voir figure 1.1. L’édition de lien est une
opération beaucoup moins coûteuse que la compilation. Il est donc beaucoup plus rapide d’éditer les
liens de plusieurs fichiers objets que de compiler l’ensemble des fichiers sources d’un coup.
2. En FORTRAN, le typage est implicite par défaut, je ne connais pas d’autres exemples de langages compilés
avec un typage implicite.
Chapitre 1. Généralités sur la programmation page 10
Édition de liens
Exécutable
11
Chapitre 2. Les bases en C++ page 12
i nt main ( )
{
std : : cout << "2∗3=" << 6 << " \n" ;
return ( 0 ) ;
}
Lors de l’exécution, le programme sort :
2∗3=6
Pour des programmes plus compliqués, nous aurons besoin d’utiliser des variables.
2.2.1 Déclaration
En C++, les variables doivent être explicitement déclarées avant leur première utilisation. Voici
quelques déclarations simples de variables.
i nt n ; // n : entier signé
long m ; // m : e n t i e r long signé
short s ; // s : e n t i e r court signé
unsigned i nt u ; // u : e n t i e r non s i g n é
unsigned long ul ; // u l : e n t i e r l o n g non s i g n é
cel-00725771, version 1 - 27 Aug 2012
float a ; // a : nombre f l o t t a n t s i m p l e p r é c i s i o n
double b ; // b : nombre f l o t t a n t d o u b l e p r é c i s i o n
long double x ; // x : nombre f l o t t a n t de p r é c i s i o n
// au moins d o u b l e
bool l ; // l e s t un b o o l é e n
Il s’agit là de tous les types simples prédéfinis en C++.
On peut aussi initialiser les variables au moment de leur déclaration :
bool b1= true ;
bool b2= f a l s e ;
i nt a = 0 ;
i nt b = a ;
char c = ’ a ’ ; // ’ a ’ e s t un c a r a c t è r e ;
// "a" e s t une c h a î n e de c a r a c t è r e .
float x =1.0;
f l o a t y= 1 . 4 e −2;
Chapitre 2. Les bases en C++ page 14
true, false, 1.0, 0, ’a’ et 1.4e−2 sont ce que l’on appelle des littéraux : i.e. une suite de caractères
qui représente une valeur fixe d’un type donné. En C++98, un littéral représente forcément un type
prédéfini.
Voici un autre programme légèrement plus utile
#include <i o s t r e a m>
#include <s t r i n g >
i nt main ( )
{
std : : string mon_prenom ;
std : : cout << " Entr ez ␣ v o t r e ␣prénom\n" ;
std : : cin >> mon_prenom ;
std : : cout << " Bonjour , ␣" << mon_prenom << "\n" ;
}
Ici, nous introduisons un autre type std::string qui est le type de chaîne de caractères défini par
la bibliothèque standard. Ce n’est pas un type prédéfini du C++. Le préfixe std:: indique que le type
string à utiliser est celui défini dans la bibliothèque standard 6 .
Vous vous demandez peut-être quel est le type du littéral chaîne de caractère "Bonjour". En effet,
cel-00725771, version 1 - 27 Aug 2012
cela ne peut être std::string car ce dernier type n’est pas un type de base mais un type défini
par la bibliothèque standard. Le type de "Bonjour" est un char∗, pointeur 7 sur un caractère. Mais
peu importe car la conversion vers std::string est implicite. Et les opérations sur std::string sont
beaucoup plus faciles à utiliser que les vieilles fonctions sur les char∗ héritées du C.
2.2.2 L’assignation
Après qu’une variable a été déclarée, on peut lui assigner d’autres valeurs. Le signe pour l’assi-
gnation en C++ est le =. Supposons que les variables a et b aient préalablement été déclarées comme
entières.
a = 0 . 0 ; // a v a u t main t en an t 0 . 0
b = 1 . 0 ; // b v a u t main t en an t 1 . 0
b=a ; // b v a u t main t en an t 1 . 0
On peut assigner un entier à un flottant directement. La conversion est implicite (il ne s’agit pas
d’une assignation bit par bit) :
float x =0.0;
i nt a = 2 ;
x=a ; // x v a u t main t en an t 2 . 0
6. std est un namespace. C’est celui réservé pour la bibliothèque standard. Nous reparlerons des namespaces plus
loin.
7. Ce pointeur pointe vers une région de la mémoire qui contient le premier caractère de la chaîne, les caractères
suivants de la chaîne se trouvent derrière dans la mémoire. La fin d’une chaîne de caractères est indiqué par le caractère
ayant la valeur 0 (et non ’0’).
page 15 Introduction au C++ par Kévin Santugini
2.3 Fonctions
Dans un programme réel, on ne met pas tout son code dans la fonction main, qui nous le rappelons
constitue le point d’entrée du programme. Il est recommandé de séparer son code en fonctions.
Les novices en programmation et en C++ confondent parfois deux opérations bien distinctes : il est
primordial de bien distinguer le concept de retour d’une valeur et le concept d’affichage : le return
n’affiche rien à l’écran, et les fonctions d’affichage de la section 2.1.2 ne retournent rien à la fonction
appelante.
signifiant que le programme s’est arrêtée normalement sans erreur. Rappelons le, cette convention
ne vaut que pour la fonction main et pour aucune autre fonction.
i nt main ( )
{
i nt a =3;
valeur ( a ) ;
std : : cout << a << ’ \n ’ ;
}
cel-00725771, version 1 - 27 Aug 2012
Compilons ce programme et exécutons le. Le nombre imprimé est 3 ! La fonction valeur n’a reçu
en argument qu’une copie de a et ne change que la valeur de cette copie. Dans la fonction main, la
valeur de a ne change pas.
Il est cependant possible de passer les arguments par référence. Pour cela, il suffit de rajouter &
dans le type de l’argument
void reference ( i nt& b )
{
b =5;
}
i nt main ( )
{
i nt a =3;
reference ( a ) ;
std : : cout << a << ’ \n ’ ;
}
imprime 5 à l’écran car le passage a eu lieu par référence.
Pour des considérations d’efficacité, on peut aussi passer par référence de gros objets tout en
déclarant que leur valeur ne doit pas être modifiée. Pour cela on rajoute le mot-clef const devant le
type de l’argument. Cela permet au compilateur de mieux vérifier le code et aide au débogage.
void const_reference( const i nt& b )
{
b =5;
}
Chapitre 2. Les bases en C++ page 18
provoque une erreur et un diagnostic à la compilation. Cette forme est surtout utilisée pour passer
un gros objet telle une matrice ou un conteneur : le passage par référence coûtera un petit nombre
d’octets : 4 octets sur une machine 32 bits, 8 sur une 64 bits. En comparaison, le passage par valeur
imposerait de recopier toute la matrice ou tout le conteneur soit potentiellement plusieurs milliers
d’entiers.
déclare deux fonctions max. Le choix de la bonne fonction sera effectué par le compilateur :
max ( 3 . 0 , 5 . 0 ) ; //max ( d o u b l e , d o u b l e )
max ( 3 , 5 ) ; //max ( i n t , i n t )
max ( 3 , 5 . 0 ) ; //max ( d o u b l e , d o u b l e )
cel-00725771, version 1 - 27 Aug 2012
Le troisième choix a lieu en raison des règles de conversions implicites. Elles sont compliquées mais
donnent en général ce que l’on souhaite.
/∗ D é f i n i t i o n de l a f o n c t i o n max ∗/
i nt max ( i nt a , i nt b )
{
i f ( a<b ) {
return b ;
} else {
return a ;
}
}
page 19 Introduction au C++ par Kévin Santugini
Pour remédier à ce problème, on peut soit placer la définition de la fonction max avant celle de la
fonction main ou déclarer sans la définir la fonction max avant le main. Voici comment déclarer la
fonction max :
i nt max ( int , i nt ) ; // Noter l e p o i n t v i r g u l e
C’est ce que l’on appelle un prototype de fonction. Il déclare une fonction sans la définir. Il déclare
ici qu’il existe une fonction max qui prend deux arguments de type int et qui retourne un int. Pour
rendre bien formé le programme ci-dessus, il suffit de rajouter le prototype de la fonction max avant
le main.
2.4 Expressions
Une expression est une instruction de type
2∗a +5;
2+3∗4 −2;
a<=5;
cel-00725771, version 1 - 27 Aug 2012
a==b ; // comparaison d ’ é g a l i t é
Les expressions peuvent contenir des parenthèses et modifier l’ordre de priorité des opérations. Pour
les opérations arithmétiques, les priorités sont les priorités mathématiques usuelles : les + et le −
(négation) unitaires sont prioritaires sur les opérateurs binaires ∗ (multiplication), / (division) et %
(modulo) qui sont eux même prioritaires sur les opérateurs binaires + (addition) et − (soustraction).
L’assignation = a heureusement une priorité plus basse que celle de tous les opérateurs arithmétiques.
Les expressions arithmétiques sont très courantes dans le membre droit d’une assignation. Voici
l’exemple d’une itération de l’algorithme de Newton pour la fonction x 7→ x ∗ x − 3
x=x−(x∗x −3)/(2∗x ) ;
Remarquez qu’il n’y a pas d’opérateurs arithmétiques pour la puissance en C++. Il faut
utiliser la fonction std::pow de la bibliothèque standard 11 .
i nt a =3;
i nt b =4;
a<b ; // v r a i
a>b ; // f a u x
a<=b ; // v r a i
a>=b ; // f a u x
a <3; // f a u x
a<=3; // v r a i
a>=3; // v r a i
L’exemple suivant imprime à l’écran si le nombre a est pair.
i nt a ;
...
std : : cout << ( ( a%2)==0) << ’ \n ’ ;
La différence entre les opérateurs −−, ++ préfixes et postfixes est que pour les opérateurs préfixes,
l’incrémentation a lieu avant l’assignation alors que pour les opérateurs postfixes, elle a lieu après.
i nt i =0;
i nt b =4;
i=−−b ; // i vaut 3, b vaut 3
i=b−−; // i vaut 3, b vaut 2
i=b++; // i vaut 2, b vaut 3
i=++b ; // i vaut 4, b vaut 4
2.5.1 L’instruction if
L’instruction if est la première structure de contrôle que nous étudierons en C++. Elle nous per-
mettra de créer des programmes plus intéressants.
L’instruction if s’emploie en général avec des expressions relationnels et des expressions logiques.
La forme générale de la structure if est :
i f ( condition ) {
//Ce q u i s e p a s s e s i c o n d i t i o n e s t v r a i .
}
Voici un exemple :
13. À l’exception de l’assembleur
Chapitre 2. Les bases en C++ page 22
i f ( condition ) {
//Ce q u i s e p a s s e s i c o n d i t i o n e s t v r a i .
} else {
// Ce q u i s e p a s s e s i n o n
}
double a =3;
cel-00725771, version 1 - 27 Aug 2012
double b = 3 . 5 ;
double x=(a+b ) / 2 ;
i f ( sin ( a ) ∗ sin ( x ) <0) {
x=b ;
}
else {
x=a ;
}
x=(a+b ) / 2 ;
Naturellement, pour avoir une dichotomie complète, il est nécessaire de rajouter une boucle autour
de ce code.
Nous pouvons aussi avoir des else if
i f ( condition1 ) {
...
}
e l s e i f ( condition2 ) {
...
}
e l s e i f ( condition3 ) {
...
}
else {
...
}
page 23 Introduction au C++ par Kévin Santugini
2.5.2 Le switch
Le switch est une forme plus efficace que if else if else if else if . Voici ce que l’on pourrait
coder :
double dynamic_operation ( char c , double a , double b )
{
double x ;
i f ( c==’+ ’ ) {
x=a+b ;
}
e l s e i f ( c== ’− ’ ) {
x=a−b ;
}
e l s e i f ( c= ’ / ’ ) {
x=a/b ;
}
e l s e i f ( c= ’ ∗ ’ ) {
x=a∗b ;
cel-00725771, version 1 - 27 Aug 2012
}
else {
. . . // Erreu r
}
return x ;
}
Il est cependant plus efficace d’utiliser un switch :
double dynamic_operation ( char c , double a , double b )
{
switch ( c ) {
case ’+ ’ :
x=a+b ;
break ; // b r e a k e s t n é c e s s a i r e i c i
case ’− ’ :
x=a−b ;
break ; // b r e a k e s t n é c e s s a i r e i c i
case ’ / ’ :
x=a/b ;
break ; // b r e a k e s t n é c e s s a i r e i c i
case ’ ∗ ’ :
x=a∗b ;
break ; // b r e a k e s t n é c e s s a i r e i c i
default :
. . . // Erreu r
}
return x ;
Chapitre 2. Les bases en C++ page 24
}
S’il n’y avait pas de break, les instructions dans les case qui suivent celui atteint seraient elles aussi
exécutées ! ! Faîtes attention !
Les switch sont plus rapides que les if mais présentent un certain nombre de contraintes :
1. Seul des tests d’égalité peuvent être exécutés.
2. Seul un type prédéfini peut être testé.
3. Les valeurs de test (derrière case) doivent être constantes.
C’est une volonté d’optimisation du code qui dicte ces contraintes, cela rend le switch plus rapide
que la série de if , else if , else if , else équivalente.
do {
// I n s t r u c t i o n s
} while ( condition )
Par rapport à une boucle while, la différence est que le corps de la boucle est assuré d’être exécuté
au moins une fois.
Executionfin
}
Elle est surtout utilisée pour incrémenter des indices automatiquement
i nt i ;
for ( i =0; i<10 ; i++) {
std : : cout << i << ’ \n ’ ;
}
// La v a r i a b l e i e x i s t e t o u j o u r s
imprime les nombres de 0 à 9 à l’écran, tout comme
2.6 Conclusion
Dans ce chapitre, nous avons vu introduit les bases de syntaxe nécessaires à la programmation en
C++. Nous aborderons des sujets plus avancés dans les chapitres ultérieurs.
14. En toute rigueur, ce n’est que presque vrai. Il y a une petite nuance technique due à l’existence de l’instruction
continue que nous n’abordons pas dans ce polycopié.
3 Les variables
Dans ce chapitre, nous introduisons les types composés et définis par l’utilisateur. Ces types sont
construit à partir des types de base. Nous choisissons délibérément de ne pas introduire les tableaux
prédéfinies par le langage C++ et héritées du C : utilisez std::vector de la STL à la place, voir le
chapitre 4.
Nous nous attardons ensuite sur la notion de durée de vie d’une variable : où dans un programme
une variable est-elle accessible ?
i nt ∗ p ; // p e s t un p o i n t e u r s u r un e n t i e r .
f l o a t ∗ q ; // q e s t un p o i n t e u r s u r un
// f l o t t a n t s i m p l e p r é c i s i o n .
Un pointeur est une variable contenant l’adresse d’une autre variable. Le contenu du pointeur q est
alors accédé par ∗q et l’adresse d’une variable x est accédé par &x :
f l o a t x = 1 . 0 ; // x v a u t 1 . 0
f l o a t y = 2 . 0 ; // y v a u t 2 . 0
f l o a t ∗q ;
q=&x ; // q p o i n t e main t en an t v e r s l a v a r i a b l e x ;
∗q = 3 . 0 ; // x v a u t 3 . 0
q=&y ; // q p o i n t e main t en an t v e r s l a v a r i a b l e y ;
∗q = 4 . 0 ; // y v a u t main t en an t 4 . 0
x=∗q ; // x v a u t main t en an t 3 . 0
L’opérateur unitaire préfixe∗ est appelé opérateur de déréférencement.
Un pointeur ayant la valeur nulle ne pointe vers aucune zone mémoire et le déréférencement du
pointeur nul donne toujours lieu à un « segmentation fault ». Le pointeur nul a pour nom NULL en
C, 0 en C++98 et nullptr en C++2011.
En C et en C++, il est possible de faire de l’arithmétique sur les pointeurs. Si T est un type
quelconque alors les opérations suivante sont syntactiquement correctes.
T∗ p ;
// I n i t i a l i s a t i o n de p . . .
∗p ; // Contenu p o i n t é par p
p[0];
∗ ( p + 1 ); // Contenu à l ’ a d r e s s e mémoire p+t a i l l e m e m o i r e (T)
p [ 1 ] ; // Contenu à l ’ a d r e s s e mémoire p+t a i l l e m e m o i r e (T)
26
page 27 Introduction au C++ par Kévin Santugini
Ces opérations prennent en compte la taille mémoire de l’objet T. Attention, c’est dangereux, il faut
vraiment savoir ce que l’on fait pour utiliser l’arithmétique sur les pointeurs.
Une façon de voir les références est de les imaginer comme des pointeurs constants pointant toujours
cel-00725771, version 1 - 27 Aug 2012
vers la même variable et qui sont automatiquement déréférencés par le compilateur. Un autre façon
de voir les références est de considérer qu’une référence n’est en réalité qu’un alias pour une autre
variable. L’association d’une référence à une variable donnée ne peut avoir lieu que lors de l’ini-
tialisation de la référence. Les références sont principalement utilisées pour spécifier que le passage
d’arguments à une fonction doit être fait par référence.
Les sous-variables d’une structure sont communément appelées ses membres. Ici, la structure point
a deux membres : x et y. Cela signifie que chaque variable du type point contiendra exactement
deux double.
Pour déclarer une variable de type point, on utilise la même syntaxe que pour les types prédéfinis :
point A ; //A e s t un p o i n t
A . x =5.0; // Le membre x de A v a u t 5 . 0
A . y =3; // Le membre y de A v a u t 3 . 0
point A ;
point ∗ q ;
q=&A ;
∗q . x = 3 . 0 ; // e r r e u r : ∗ ( q . x ) e t non (∗ q ) . x
(∗ q ) . x =4.0; // OK mais l o u r d en p a r e n t h è s e s
q−>x=5; // é q u i v a u t à (∗ q ) . x=5
Il faut alors soit mettre des parenthèses soit utiliser l’opérateur de sélection −> pour les pointeurs
sur les structures.
Si on souhaite définir le type plus tard, on peut se contenter de déclarer la structure sans la définir :
struct point A ; // S t r u c t u r e A d é c l a r é e mais non d é f i n i e .
Dans une structure, on peut uniquement avoir des membres dont le type est déjà défini, y compris
d’autres structures. Par exemple, une fois la structure point définie, on peut définir la structure
triangle :
struct triangle {
point A ;
point B ;
cel-00725771, version 1 - 27 Aug 2012
point C ;
};
Par contre, une structure ne peut être récursive, on ne peut déclarer un membre dont le type n’est
pas complètement défini. Cette contrainte provient du typage statique qui impose que le compilateur
connaisse la taille en octets des types. Il est par contre possible de placer des pointeurs vers des
structures qui ne sont que déclarées. En effet, la taille d’un pointeur ou d’une référence est fixe : 4
octets sur les machines 32 bits et 8 octets sur la machines 64 bits. Par exemple,
struct recursive {
recursive next ; // I l l é g a l r e c u r s i v e n ’ e s t pas e n c o r e d é f i n i e
recursive∗ opt ; // L é g a l r e c u r s i v e e s t d é c l a r é e
recursive& up ; // L é g a l r e c u r s i v e e s t d é c l a r é e
};
double x=(a+b ) / 2 . 0 ;
/∗
p o i n t e u r s u r une f o n c t i o n p r e n a n t un e n t i e r
e t un f l o t t a n t comme argument e t r e t o u r n a n t
un p o i n t e u r s u r un e n t i e r
∗/
i nt ∗ ( ∗ fun ) ( int , f l o a t ) ;
/∗
f o n c t i o n p r e n a n t un e n t i e r e t un f l o t t a n t comme
argument e t r e t o u r n a n t un p o i n t e u r s u r un p o i n t e u r
s u r un e n t i e r
∗/
i nt ∗∗ fun ( int , f l o a t ) ;
/∗ P o i n t e u r s u r une f o n c t i o n r e t o u r n a n t un c a r a c t è r e
e t p r e n a n t en argument un e n t i e r e t un p o i n t e u r
s u r une f o n c t i o n ne p r e n a n t aucun argument e t
r e t o u r n a n t un c a r a c t è r e .
Chapitre 3. Les variables page 30
∗/
char ( ∗ fun ) ( int , char ( ∗ ) ( ) ) ;
typedef i nt Int ;
i nt a =0;
Int b =0;
/∗ a e t b on t e x a c t e m e n t l e même t y p e ∗/
a=b ; // L é g a l
b=a ; // L é g a l
1. Avoir un programme lisible est important, on code autant pour les autres programmeurs que pour le compilateur
page 31 Introduction au C++ par Kévin Santugini
à l’extérieur de toute fonction. Cette définition ne doit apparaître qu’une seule fois dans le pro-
gramme. En effet, sinon le compilateur réserverait plusieurs fois de la place mémoire pour la variable
xglobal.
Pour que la variable soit accessible depuis les fonctions définies en dehors du fichier, on doit la
déclarer :
extern i nt xglobal ;
Le mot-clef extern indique qu’il s’agit là non d’une définition mais d’une simple déclaration. De
préférence, cette déclaration aura lieu dans un header.
Nous voyons là la première nuance entre la notion de déclaration et la notion de définition. Une
déclaration indique au compilateur qu’une variable ou un objet existe déjà mais ne demande pas
au compilateur de réserver de la place mémoire pour cette variable. Elle permet au compilateur de
vérifier que les opérations faites sur la variable sont bien licites. Une définition au contraire demande
au compilateur de réserver de l’espace mémoire pour la variable. Aussi, une variable globale ne doit
être dédinie qu’une seule fois par programme. Par contre, la même variable peut être déclarée autant
de fois que l’on souhaite.
Une variable globale peut être déclarée static. Cela signifie qu’elle n’est accessible que par les
fonctions définies dans le même fichier. C’est une fonctionnalité considérée obsolète et remplacée par
cel-00725771, version 1 - 27 Aug 2012
i nt f ( i nt )
{
...
i nt x ;
{
i nt y ;
...
}
// y c e s s e d ’ e x i s t e r
// x e x i s t e t o u j o u r s .
...
}
Chapitre 3. Les variables page 32
0
1
2
3
Attention, la valeur de la variable x n’est conservée que parce qu’elle a été déclarée static.
delete p ;
∗p = 1 . 0 ; // OOPS ∗p a é t é e f f a c é
On peut aussi allouer une grande quantité de mémoire d’un coup avec les opérateurs new[] et
delete[] :
double∗ p ;
p= new double [ 1 0 0 ] ; // A l l o u e 100 d o u b l e
P [ 0 ] = 1 . 0 ; // l e premier d o u b l e a l l o u é
p [5] = 4.0;
P [ 9 9 ] = 2 . 0 ; // l e d e r n i e r d o u b l e a l l o u é
delete [ ] p ; // Rend au s y s t è m e l a r é g i o n de
// mémoire précédemment a l l o u é e
Attention, l’opérateurs new[] n’initialise pas la zone mémoire à 0.0 pour les types prédéfinis. Il n’y
a pas besoin de se rappeler combien de double ont été allouées pour faire un delete[] de toute la
région : la taille de la région est conservée en mémoire automatiquement.
Un bug classique et difficile à corriger lorsque l’on alloue de la mémoire est la “fuite de mémoire” :
on alloue de la mémoire puis on oublie de la libérer(désallouer) quand on ne s’en sert plus . Plus
le programme est destiné à tourner longtemps plus cette “fuite” s’aggrave et devient problématique.
cel-00725771, version 1 - 27 Aug 2012
Un programme qui laisse fuire de la mémoire court le risque d’en allouer plus que le système ne peut
en fournir et d’être arrêté d’office par le système (lors de l’arrêt d’un programme, la mémoire est de
toute façon retournée au système).
L’allocation dynamique est à cantonner au code de bas-niveau 2. En C++, on peut souvent éviter
d’utiliser directement l’allocation dynamique en utilisant la bibliothèque standard, en particulier
pour les chaînes de caractères et les conteneurs.
2. L’expression « bas-niveau » est à prendre au sens code proche de la machine. À opposer à l’expression « haut-
niveau » qui signifie un code proche de l’abstraction idéaliséee de l’algorithme.
4 Les conteneurs en C++ : la STL
Il est très important dans les programmes réels d’avoir des conteneurs efficaces. Un conteneur est
un type qui contient plusieurs variables d’un même type. Le conteneur de base du C++ (hérité du C)
est très primitif et nous choisissons de ne pas en parler dans ce document. En effet, la bibliothèque
standard contient la STL 1 , une bibliothèque de conteneurs très efficace.
34
page 35 Introduction au C++ par Kévin Santugini
S . push_back ( s ) ; // R ajou t e s en f i n de t a b l e a u .
// La t a i l l e du t a b l e a u augmente de $1$ .
} while ( s!=" \n" ) ;
Lorsque l’on utilise push_back()il est possible que le tableau devienne trop grand et ne tienne
plus dans la région mémoire allouée il sera déplacée automatiquement dans une région où il y a
suffisamment de place. Pour un grand tableau, cela peut-être très coûteux.
En C++, on peut simplement déclarer un tableau de T par std::vector<T>
Et nous avons respectivement un tableau d’entiers et un tableau de flottants. On peut mettre presque
n’importe quel type entre < et >. La clef d’un std::vector ne peut alors qu’être un entier.
a . size ( ) ; // r e t o u r n e l a t a i l l e de a ;
a [2]=5.0 ; // a s s i g n e 5 . 0 au t r o i s i è m e é l é m e n t de a
a [0]=5.0 ; // a s s i g n e 5 . 0 au premier é l é m e n t de a
a . at ( 2 ) = 5 . 0 ; // p a r e i l mais v é r i f i c a t i o n que a . s i z e () <=3.
a . push_back ( 3 . 0 ) ; // r a j o u t e 3 . 0 à l a f i n de a
Quand un tableau devient trop grand (après des push_back()), il faut le déplacer. Cela est fait
automatiquement mais cela prend du temps. En effet, il risque de ne pas y avoir de place dans la
mémoire après le tableau pour les nouveaux éléments. Pour éviter cela, on peut réserver de la place :
a . reserve ( 1 0 0 ) ; // r é s e r v e 100 p l a c e s
a . capacity ( ) ; // r e t o u r n e l a c a p a c i t é du t a b l e a u a
Il ne faut pas confondre la capacité et la taille d’un tableau. Les tableaux sont efficaces pour l’accès
aléatoire mais l’insertion coûte chère sauf en fin de tableau s’il y a assez de réserve.
Chapitre 4. Les conteneurs en C++ : la STL page 36
Lien∗ head
Lien∗ tail
Une liste est stockée de manière différente. Chaque élément de la liste contient en plus de sa valeur
des liens 2 vers les éléments précédents et suivants, voir figure 4.2. La liste contient juste un accès au
premier et au dernier élément. Pour déclarer une liste on utilise std::list<T> où T est un type :
Comme pour un vector, on peut mettre presque n’importe quel type entre < et >. Pour accéder
aux éléments d’une liste, on emploie un itérateur :
Les listes sont très efficaces pour l’insertion et la suppression d’éléments mais la recherche est coû-
teuse.
page 37 Introduction au C++ par Kévin Santugini
val["d"]
val["b"] val["f"]
de clef que l’on souhaite mettre dans le conteneur a un ordre, il est possible de placer les valeurs
dans l’arbre de manière ordonnée. La recherche dans un arbre ordonnée est proportionneul à la
profondeur de l’arbre qui s’il est équilibre est en O(log 2 (N ). Heureusement, il existe des algorithmes
pour maintenir un arbre ordonné équilibré lors de l’insertion de nouveaux éléments.
La complexité de toutes les opérations sur un arbre sera de O(log2 (N ). Des trois conteneurs vus
ici, c’est le plus complexe à programmer. Heureusement, nous n’avons pas à le faire car d’autres
l’ont fait pour nous. Il suffit d’utiliser les arbres fournis par la bibliothèque standard.
Pour déclarer une variable de type arbre on utilise std::list<clef,T> où clef est un type avec
un ordre et T est un type :
std : : map<std : : string , int> listedentiers ;
std : : map<std : : string , f l o a t > listedeflottants ;
listedentiers [ " i c i " ] = 3 ;
i nt a= listedentiers [ " i c i " ] ;
4.4 Conclusion
Il faut bien se rappeler que la mémoire d’un ordinateur moderne est contiguë. Cela rend les per-
formances des conteneurs différentes : aucun des conteneurs n’est optimal pour toutes les opérations.
L’insertion dans une liste est rapide mais la recherche est en temps linéaire. La recherche dans un
tableau triée est rapide mais l’insertion est coûteuse en temps linéaire. Pour un arbre, toutes les
opérations sont en temps logarithmique.
Tous les conteneurs ne sont pas abordés dans ce chapitre. Nous avons présenté les trois les plus
courant. Il y en a bien d’autres. Parmi eux, on peut citer les tableaux hashés 3 .
2. Ce sont en général des pointeurs, nous en parlerons ultérieurement
3. Introduits dans la bibliothèque standard en C++2011 sous le nom unordered_map.
5 Programmation modulaire et
organisation d’un programme
Maintenant que nous avons vu les structures de contrôle et les différents types de variables, nous
allons pouvoir créer des programmes non triviaux.
La plupart des codes créés lors de l’apprentissage d’un langage sont courts et il n’y a pas besoin de
les organiser pour qu’ils fonctionnent. Cependant, il est courant que les codes industriels atteignent
10000 lignes de codes et même 100000 lignes de code. Sans discipline et sans organisation, autant de
lignes de codes ne peuvent être maintenues. Il est donc important de prendre de bonnes habitudes
dès maintenant.
La programmation modulaire est une façon d’organiser un code. Dans la programmation modu-
laire, les fonctionnalités d’un programme sont séparées en fonctions et les fonctions apparentés sont
cel-00725771, version 1 - 27 Aug 2012
regroupées dans un même fichier, dans un même namespace, voir section 5.4 ou dans un même
module. Les différents modules sont alors aussi indépendant que possible les uns des autres. Pour
cela, on utilise soit les namespaces, soit la séparation du code source en plusieurs fichiers.
Le support pour les modules eux-mêmes ne viendra en C++ que dans un addendum technique au
C++2011. Il faudra donc attendre encore quelques années. Il est cependant possible, dès à présent,
de programmer modulairement avec de la discipline en utilisant les headers, les namespaces et la
directive #include. Pour l’instant, nous devons donc fabriquer les headers à la main. Une fois
l’addendum technique écrit et approuvé, cela sera heureusement inutile. Malheureusement, il n’est
pas encore sorti à ce jour.
38
page 39 Introduction au C++ par Kévin Santugini
unsigned i nt d ;
std : : cout << " Entr ez ␣un␣ e n t i e r ␣ : ␣ " ;
std : : cin >> d ;
Comme le programme est court, cela ne pose pas de gros problèmes. Mais une telle organisation
deviendra un cauchemar pour les codes grands de millions de lignes. C’est déjà beaucoup mieux.
sorties. Nous allons laisser la gestion des entrées sorties dans la fonction main et placer le calcul de
la factorielle dans la fonction factorielle. Voici le code complet dans un seul fichier
i nt main ( )
{
unsigned i nt d ;
std : : cout << " Entr ez ␣un␣ e n t i e r ␣ : ␣ " ;
std : : cin >> d ;
unsigned i nt res=factoriel ( d ) ;
std : : cout << res << " \n" ;
return ( 0 ) ;
}
Cependant, il est préférable de séparer les fonctions non apparentées en plusieurs fichiers.
Chapitre 5. Programmation modulaire et organisation d’un programme page 40
alors nous obtenons une erreur. En effet, le compilateur compile séparément les fichiers maths.cc
et main.cc. Quand il compile le fichier main.cc, le compilateur n’a aucune idée de ce qui se trouve
dans maths.cc.
Pour résoudre le problème, il suffit de rajouter le prototype, voir section 2.3.6, de la fonction
factorielle avant la fonction main. Voici le nouveau fichier main.cc :
/∗ Poin t d ’ e n t r é e du programme ∗/
i nt main ( )
page 41 Introduction au C++ par Kévin Santugini
{
unsigned i nt d ;
std : : cout << " Entr ez ␣un␣ e n t i e r ␣ : ␣ " ;
std : : cin >> d ;
En plus des headers standard, le programmeur peut créer ses propres headers. À la différence des
headers standard, les headers du programmeur doivent, dans les directives #include, être délimités
par des guillemets doubles " et non des signes d’inégalités < >
#include <h e a d e r s t a n d a r d>
#include " h e a d e r n o n s t a nd a r d"
Ces headers sont nécessaires. En effet, le C++ compile séparément chacun des fichiers sources.
Une fonction main définie dans un fichier main.cc peut avoir besoin d’appeler une autre fonction
print_result définie dans un autre fichier output.cc. Pour pouvoir appeler cette fonction depuis
main.cc, il faut que qu’elle ait été déclarée (prototypée) dans le fichier main.cc avant son premier
appel. Pour cela, le mieux est d’inclure le header output.h où on aura préalablement prototypé
toutes les fonctions définies dans output.cc.
Revenons maintenant à l’exemple de la section 5.1.
header nous même. Nous allons créer un header maths.h contenant les prototypes, voir section 2.3.6,
de toutes les fonctions définies dans maths.cc. Ici, nous aurons dans maths.h, le prototype de la
fonction factorielle :
Même si ce n’est pas toujours nécessaire, il est préférable d’inclure le header associé à un fichier
source dans ce fichier source : cela permet au compilateur de repérer certaines erreurs et de vous en
avertir.
Enfin, le fichier main.cc est modifié pour utiliser un header et supprimer le prototype rajouté à la
main :
/∗ Poin t d ’ e n t r é e du programme ∗/
i nt main ( )
{
unsigned i nt d ;
std : : cout << " Entr ez ␣un␣ e n t i e r ␣ : ␣ " ;
std : : cin >> d ;
Et nous avons maintenant organisé correctement le code d’un petit programme. Cela n’a pas l’air
cel-00725771, version 1 - 27 Aug 2012
très utile pour un programme aussi court mais il est préférable de prendre les bonnes habitudes dès
maintenant.
namespace mes_expressions {
struct expression {
// . . .
}
// D é f i n i t i o n s de v a r i a b l e s
// D é f i n i t i o n s de f o n c t i o n s
}
namespace les_expressions_du_voisin {
struct expression {
// . . .
}
// D é f i n i t i o n s de v a r i a b l e s
// D é f i n i t i o n s de f o n c t i o n s
}
Et on peut déclarer dans le programme des expressions de type différent avec
mes_expressions : : expr e1 ;
cel-00725771, version 1 - 27 Aug 2012
les_expressions_du_voisin e2 ;
5.5 Conclusion
Organiser son code est primordial pour des programmes longs de plusieurs millions de lignes de
code écrit en coopération par une équipe. Pour l’instant, en C++, cela signifie qu’il faut savoir créer
des headers. Espérons que cela ne soit plus le cas à l’avenir.
Il est aussi très important d’être discipliné quandon programme en équipe. En effet, les morceaux
disjoints d’un même programme écrit par des personnes différentes doivent être compatible. Pour
assurer cette compatibilité, le seul moyen est de respecter à la lettre la spécification des interfaces
données par le chef du projet : en particulier il faut respecter le nom des fonctions, ordre des argu-
ments des fonctions, nom des classes, nom des fichiers headers, nom des fichiers sources chosies par le
chef du projet. Ceci, même si vous n’aimez pas les choix du chef de projet. Sans cette discipline, les
morceaux du programme codés par des personnes différentes ne s’imbriqueraient pas correctement.
6 Les classes et la programmation orientée
objet
Par rapport au C, l’ajout le plus visible en C++ est la notion de classe. Les classes sont à la
base de la programmation orientée objet mais il ne suffit pas de créer une classe pour faire de la
programmation orientée objet.
avec les mêmes facilités que les types prédéfinies. Ces nouveaux types peuvent alors être dotés en
plus d’un certain nombre d’opération et de fonctions membres. I.E. une classe n’est pas seulement
définie par les sous-variables qui la constituent mais aussi par l’interface fournie à ses utilisateurs.
6.1.1 L’encapsulation
Dans la programmation modulaire, si l’on fait attention à diviser un programmes en plusieurs mo-
dules, toute les fonctions sont autorisées à modifier tous les membres d’un type et à accéder à toutes
les variables, y compris les variables internes d’un autre module. Par convention, les programmeurs
disciplinés s’abstiennent d’accéder directement aux données et appellent des fonctions spécialisées.
En cas de bug, le nombre de fonctions à vérifier demeure alors beaucoup plus restreint, ce qui est
primordial dans un grand programme. Cependant, le compilateur ne fait rien pour imposer cette
contrainte et un programmeur peu attentif pourrait alors modifier directement un membre interne
d’un type composé.
Ce n’est pas très grave pour un type simple comme un point de deux coordonnées :
struct point {
i nt x ;
i nt y ;
};
C’est beaucoup plus gênant pour une liste doublement chaînée.
struct lien {
i nt val ;
lien∗ next ;
lien∗ prev ;
};
46
page 47 Introduction au C++ par Kévin Santugini
struct liste {
lien ∗ tete ;
lien ∗ fin ;
}
Il est impératif pour qu’elle fonctionne que les liens next et prev soient cohérents. Il faut respecter
un invariant : si p est un lien∗ et si p−>next est non nul alors (p−>next)−>prev doit être égal
à p. C’est ce que l’on appelle un invariant.
Si toutes les fonctions peuvent y accéder et qu’un programmeur décide d’accéder directement aux
membres de lien et qu’il commet une erreur (ou que la bibliothèque de liste change son implémenta-
tion interne), on peut se retrouver avec un programme qui fera n’importe quoi et la cause de l’erreur
risque d’être difficile à identifier.
c l a s s point {
public : // Tous l e s membres d é c l a r é s a p r è s s o n t p u b l i c
double x ; // x e s t un membre p u b l i c
private : // Tous l e s membres d é c l a r é s a p r è s s o n t p r i v é s
double y ; // y e s t un membre p r i v é
};
Nous remarquons ici les mots-clefs private et public. Ces mots clefs spécifient 1 si un membre
d’une classe est un membre privé ou un membre public. Les membres publics sont directement
accessibles par toute fonction tandis que les membres privés ne le sont qu’indirectement. Par défaut,
en l’absence des mot-clefs private et public, les membres sont privés. Essayons maintenant d’accéder
aux données
point A ; //A e s t un p o i n t
A . x = 5 . 0 ; //OK, x e s t p u b l i c
A . y =3; // Erreur , y e s t p r i v é
Comment peut-on modifier le membre y ? Certaines fonctions ont le droit de modifier les membres
privés d’une classe : les fonctions membres et les fonctions amies. Nous commençons par décrire ce
qu’est une fonction membre.
c l a s s point {
public :
double x ;
double y ;
private :
double get_x ( ) { return x ; } // d é c l a r é e e t d é f i n i e .
double& get_y ( ) { return y ; } // p a r e i l e t r e t o u r n e une r é f é r e n c e .
point rotate ( f l o a t angle ) const ; // d é c l a r é e e t non d é f i n i e .
};
La classe point a deux données membres : x et y. Cette classe dispose aussi de trois fonctions
membres : get_x(), get_y() et rotate(float). Les fonctions membres d’une classe sont autorisées à
modifier les données privées des variables de cette classe. De cette manière, on limite le nombre de
fonctions autorisées à modifier la machinerie interne d’une variable d’une classe donnée.
Pour appeler une fonction membre, on utilise l’opérateur de sélection « . » :
point A ; // OK
A . x =2.5; // OK
A . y =2.2; // OK
cel-00725771, version 1 - 27 Aug 2012
c l a s s exemple {
private : // i n u t i l e p r i v a t e par d é f a u t
i nt n ;
public :
exemple ∗ adresse ( ) { return t h i s ; }
void setn_1 ( i nt m ) { n=m ; }
void setn_2 ( i nt m ) { this −>n=m ; }
void setn_2 ( i nt m ) { ( ∗ t h i s ) . n=m ; }
};
Alors, on peut l’utiliser de la manière suivante
exemple A ;
exemple ∗ p ;
p=A . adresse ( ) ; // é q u i v a l e n t à p=&A;
A . setn_1 ( 2 ) ; // A. n v a u t 2
A . setn_2 ( 3 ) ; // A. n v a u t 3
A . setn_3 ( 5 ) ; // A. n v a u t 5
Signalons enfin que le pointeur this ne peut être accédé que depuis une fonction membre.
Nous verrons des exemples d’utilisation de this moins artificiels plus loin dans ce polycopié.
La solution est d’utiliser une fonction dite amie. Les fonctions amies sont des fonctions normales
mais qui ont accès aux membres privés des classes dont elles sont amis. Nous avons besoin de la
fonction suivante.
mathvector
produit_matrice_vecteur ( const matrice& a , const mathvector& b )
{
// A l g o r i t h m e de p r o d u i t m a t r i c e v e c t e u r
}
Pour qu’elle ait accès aux membres privés des variables de type matrice et mathvector, il suffit de
la déclarer ami dans les deux classes. Rien de plus facile, il suffit de rajouter le prototype de cette
fonction précédé du mot-clef friend dans les deux classes.
c l a s s matrice {
private :
// . . . .
public :
// . . . .
cel-00725771, version 1 - 27 Aug 2012
c l a s s mathvector {
private :
// . . . .
public :
// . . . .
fri end mathvector
produit_matrice_vecteur ( const matrice& a , const mathvector& b ) ;
};
Les fonctions amies ne sont pas des fonctions membres, elles sont appelées comme des fonctions
normales :
matrice A ;
mathvector x ;
// . . .
mathvector b ;
b=produit_matrice_vecteur( A , x ) ;
Remarquez l’absence d’opérateur de sélection « . ».
Le concept de classe amie existe aussi :
class A {
// . . .
};
page 51 Introduction au C++ par Kévin Santugini
class B {
// . . .
fri end c l a s s A ; // Tou t es l e s f o n c t i o n s membres
// de A on t main t en an t a c c è s aux
// don n ées p r i v é e s de B ;
};
Les fonctions et les classes amies sont une alternative aux fonctions membres quand il s’agit d’accéder
aux données privées.
6.1.6 Constructeurs
Les constructeurs sont une notion primordiales en C++. Il est très important de comprendre ce
qu’est un constructeur. Aussi, nous laissons leur syntaxe à plus tard. Un constructeur est une fonction
membre d’une classe qui initialise la variable au moment même de la définition de cette variable.
Motivation
cel-00725771, version 1 - 27 Aug 2012
type1 var1 ;
init_type1 ( var1 ) ;
type2 var2 ;
init_type2 ( var2 , arg21 ) ;
type3 var3 ;
init_type3 ( var3 , arg31 , arg32 , arg33 , arg34 ) ;
Mais cette manière de programmer est dangereuse : il est bien trop facile d’oublier une initialisation
et pour certaines classes, particulièrement les classes utilisant l’allocation dynamique et pour les
classes contenant des pointeurs, cela peut être dramatique. On peut se retrouver avec des variables
dans un état indéfini et avoir un programme qui fait n’importe quoi. Par exemple, si un pointeur est
mal défini, on peut avoir une erreur de segmentation. Ce genre d’erreur peut être difficile à repérer
dans un long programme. La solution est d’utiliser les constructeurs, ainsi définition de la variable
et initialisation auront lieu sur la même ligne et il n’y aura aucun risque d’oubli. Il serait préférable
de pouvoir simplement écrire
type1 var1 ;
type2 var2 ( var2 , arg21 ) ;
type3 var3 ( var3 , arg31 , arg32 , arg33 , arg34 ) ;
et que les variables var1, var2 et var3 soient initialisées comme si on avait appelé les fonctions
init_. C’est exactement ce que font les constructeurs.
Chapitre 6. Les classes et la programmation orientée objet page 52
}
Puis pour utiliser ce constructeur, il suffira d’écrire
nom_classe var ( /∗ l i s t e arguments ∗/ ) ; // I n i t i a l i s a t i o n f a i t e par l e c o n s t r u c t e u r
Il peut y avoir plusieurs constructeurs pour une même classe. Deux constructeurs sont particuliè-
rement importants : le constructeur par défaut et le constructeur par copie.
Exemple
Prenons la classe matrice, cette classe a besoin d’allouer dynamiquement de la mémoire. En effet,
on ne connait pas forcément lors de la compilation la taille des variables de type matrice et donc
la quantité de mémoire dont elles auront besoin Voici comment créer des constructeurs pour cette
classe :
c l a s s matrice
{
private :
i nt m ;
i nt n ;
double ∗q ;
public :
matrice ( i nt _m , i nt _n ) {
m=_m ;
n=_n ;
q= new double [ m∗n ] ; // a l l o c a t i o n de mémoire
}
matrice ( i nt _m , i nt _n , double val ) {
m=_m ;
n=_n ;
q= new double [ m∗n ] ; // a l l o c a t i o n de mémoire
for ( i nt i =0;i<m∗n;++i )
q [ i]= val ;
}
matrice ( const matrice& b ) {
Chapitre 6. Les classes et la programmation orientée objet page 54
m=b . m ;
n=b . n ;
/∗ A t t e n t i o n , on ne p e u t a s s i g n e r b . q à q ∗/
q= new double [ m∗n ] ; // a l l o c a t i o n de mémoire
for ( i nt i =0;i<m∗n;++i ) {
q [ i]=b . q [ i ] ;
}
}
// D e s t r u c t e u r , v o i r s e c t i o n s u r l e s d e s t r u c t e u r s
// a u t r e f o n c t i o n s membres
};
6.1.7 Destructeurs
Les destructeurs sont le contraire des constructeurs. Ils servent à nettoyer une variable quand
celle-ci cesse d’exister. En effet, certaines classes aquièrent des ressources lors de leur construction.
Ces ressources peuvent être de l’espace mémoire, des accès réseaux, des ouvertures de fichiers ou
encore des locks. Le plus souvent, c’est de l’espace mémoire. Quand la variable atteint sa fin de
cel-00725771, version 1 - 27 Aug 2012
vie, elle doit libérer ces ressources. Cela est fait avec un destructeur. Les destructeurs sont appelés
automatiquement quand une variable atteint la fin de son existence. Comme les constructeurs, les
destructeurs ne renvoient aucun argument. Contrairement aux constructeurs, une classe ne peut
avoir qu’un unique destructeur et il ne prend aucun argument. Le destructeur se déclare comme un
constructeur mais précédé de ~. Voici la syntaxe générale
c l a s s nom_classe {
private :
// Données
public :
~nom_classe ( ) ;
// A u t res f o n c t i o n s membres
};
Puis pour définir un constructeur en dehors de la classe
nom_classe : : ~ nom_classe ( liste arguments)
{
// Corps de l a f o n c t i o n
}
Reprenons l’exemple de la classe matrice qui alloue de la mémoire dynamique lors de la construc-
tion. Il va falloir libérer cette mémoire dans le destructeur.
c l a s s matrice
{
private :
i nt m ;
i nt n ;
page 55 Introduction au C++ par Kévin Santugini
double ∗q ;
public :
// C o n s t r u c t e u r s , v o i r s e c t i o n p r é c é d e n t e
~matrice ( ) { delete [ ] q ; } // l i b é r a t i o n de l a mémoire
// a u t r e f o n c t i o n s membres
};
Nous rappelons que les variables allouées dynamiquement existent jusqu’à ce qu’elles aient été ex-
plicitement détruites. C’est cette allocation dynamique qui justifie l’importance des destructeurs
{
matrice m ( 4 , 4 ) ;
// . . .
même pour les variables dynamique. En effet les opérateurs delete et delete[] appelent eux aussi
automatiquement 2 les destructeurs. Ce principe de ne jamais appeler explicitement un destructeur
ne souffre qu’une unique exception que nous n’aborderons pas dans ce polycopié.
complexe a ;
a . re =2; // e r r e u r r e e s t p r i v é
double c= a . re ; // e r r e u r r e e s t p r i v é
Voici comment on souhaite pouvoir utiliser les constructeurs de la classe complexe. Rappelons
que les constructeurs servent à initialiser une variable au moment même de sa création. Par exemple,
lorsque l’on déclare :
complexe z ; // complex e : : complex e ( ) e s t a p p e l é e
complexe z2 ( 1 . 0 ) ; // complex e : : complex e ( d o u b l e ) e s t a p p e l é e
complexe z2 = 1 . 0 ) ; // complex e : : complex e ( d o u b l e ) e s t a p p e l é e
complexe z3 ( 1 . 0 , 2 . 0 ) ; // complex e : : complex e ( d o u b l e , d o u b l e ) e s t a p p e l é e
complexe z4 ( z ) ; // C o n s t r u c t e u r par c o p i e
// complex e : : complex e ( c o n s t complex e &)
/∗ A t t e n t i o n ∗/
complexe z5=z ; // C o n s t r u c t e u r par c o p i e
// complex e : : complex e ( c o n s t complex e &)
cel-00725771, version 1 - 27 Aug 2012
z2=z ; // A s s i g n a t i o n par c o p i e
// complex e : : o p e r a t o r =( c o n s t complex e &)
Ne pas confondre le constructeur par copie et l’ assignation par copie..
// . . .
complexe operator ∗ ( const complexe& b ) const ;
complexe& operator= ( const complexe& b ) ;
}
Ici, si vous suivez, vous devez vous demandez pourquoi ces opérateurs ne prennent qu’un seul ar-
gument et non deux. La réponse est qu’il s’agit ici de fonctions membres. Le premier argument est
comme pour toute fonction membre passée implicitement. Ainsi, si z1 et z2 sont des variables de
classe complexe, l’instruction z1=z2 est équivalente à z1.operator=(z2). De même, l’instruction
z1∗z2 est équivalente à z1.operator∗(z2). Ces deux formes longues sont licites mais ne sont pas uti-
lisées en pratique. Ainsi, dans les deux cas, les données membres de z1 sont accessibles directement
sans préfixe dans la définition de ces deux opérateurs membres :
complexe complexe : : operator ∗ ( const complexe& b ) const
{
return ( complexe ( re ∗b . re−im∗b . im , re ∗b . im+im∗b . re ) ;
}
{
re=b . re ;
im=b . im ;
return ( ∗ t h i s ) ;
}
Le fait que l’opérateur = retourne une référence sur un complexe et qu’il retourne return(∗this)
est nécessaire. C’est le cas pour tous les opérateurs d’assignation et seulement les opérateurs
d’assignation, i.e. pour =, +=, −=, ∗=, et /=. Nous n’expliquerons pas pourquoi dans ce polycopié.
Il est aussi possible de surdéfinir les opérateurs par des fonctions non membres
mathvector
operator ∗ ( const matrice& a , const mathvector& b )
{
// . .
}
Il suffira de déclarer cet opérateur friend dans les classes mathvector et matrice :
c l a s s matrice {
//
public :
fri end mathvector
operator ∗ ( const matrice& a , const mathvector& b ) ;
};
c l a s s mathvector {
//
public :
Chapitre 6. Les classes et la programmation orientée objet page 58
Avant d’écrire les algorithmes, on créé des classes et on définit leur comportement : on commence
d’abord par spécifier l’interface et par écrire les déclarations des fonctions membres. On implémente
la classe ensuite.
6.2.1 L’héritage
Une classe peut être dérivé d’une autre et hériter tous ses membres. Voici un exemple,
c l a s s habitation {
std : : string nom ;
i nt pieces ;
public :
habitation ( std : : string _nom , i nt _pieces ) ;
void print ( ) ;
};
public :
maison ( std : : string _nom , i nt _pieces , i nt _etages ) ;
void print ( ) ; // R e d é c l a r a t i o n de p r i n t ( )
}
/∗
I l l é g a l e car maison ne p e u t a c c é d e r
aux p r i v é s d ’ h a b i t a t i o n
∗/
std : : cout << ’ \ t ’ << pieces / etages
<< " p i e c e s ␣ par ␣ e t a g e s " << ’ \n ’ ;
}
La classe maison hérite de tous les membres de la classe habitation et est une classe hérité
de habitation. Les fonctions membres de maison ne peuvent accéder aux membres privés de
Chapitre 6. Les classes et la programmation orientée objet page 60
habitation ! !
habitation∗p=&a ; // OK
maison ∗q=&b ; // Erreu r
Mais p−>print() va appelé le print d’habitation. Et le nombre d’étages ne sera pas affiché. En
effet, le typage est statique et à la compilation, le compilateur ne peut deviner si p est non seulement
une habitation mais aussi une maison ! Pour cela nous introduisons les fonctions virtuelles.
Nous avons pour l’instant toujours déclare la classe de base public. Nous aurions aussi pu la
déclarer private :
c l a s s A { public : void print ( ) { std : : cout << "n" } } ;
c l a s s B : public A { } ;
c l a s s C : private A { } ;
A a ; a . print ( ) ; // OK
B b ; b . print ( ) ; // OK p r i n t h é r i t é de A e t p u b l i c .
C b ; c . print ( ) ; // e r r e u r l a b a s e A e s t p r i v é
Les membres publics de C peuvent accéder aux membres publics de A mais l’interface de A dans C
est maintenant privé .
c l a s s B : public A {
private :
B();
};
page 61 Introduction au C++ par Kévin Santugini
B : : B()
{
// ?? Je veu x a p p e l e r l e c o n s t r u c t e u r A ( 1 . 0 ) .
}
Nous ne pouvons pas appeler un constructeur de la classe de base depuis le corps du constructeur. En
effet, une fois dans le corps de la fonctions, les classes de bases et les membres ont déjà été initialisées
par défaut. Nous devons utiliser une autre syntaxe qui permet d’appeler les constructeurs.
B : : B() : A (1.0)
{
// A u t res i n s t r u c t i o n s
}
Cette syntaxe est non seulement utilisé pour les classe de bases mais aussi pour les membres d’une
classe. Cette syntaxe est d’ailleurs impérative pour les références qui doivent être initialisées.
c l a s s C : public A {
f l o a t& r ;
cel-00725771, version 1 - 27 Aug 2012
double& x ;
// . . . .
public :
C ( double a , f l o a t& b , double& x ) : A ( a ) , r ( a ) , x ( c ) { }
}
habitation∗ p ;
p=&a ;
Chapitre 6. Les classes et la programmation orientée objet page 62
p−>print ( ) ; //
p=&b ; // OK
p−>print ( ) ; //
Cela vient que si une classe contient une fonction virtuelle, elle est considérée classe virtuelle et un
champ est placée dans la représentation binaire de la classe par le compilateur qui précise le type
exact de l’objet. Pour une classe virtuelle, il y a typage dynamique et la bonne fonction sera choisie
lors de l’exécution. Cela ralentit l’exécution mais c’est nécessaire si on a besoin de polymorphisme
dynamique où le type n’est pas prévisible lors de la compilation.
v i r t u a l void dessiner ( ) = 0 ;
virtuel void rotation ( double)=0 ;
}
Le =0 signifie que la fonction est virtuelle pure et qu’elle devra être redéclarée dans les classes
dérivées. Une classe qui contient une fonction virtuelle pure est appelée classe abstraite et ne peut
être instanciée :
figure A ; // Erreu r . A c o n t i e n t d e s f o n c i o n s
// v i r t u e l l e s p u r e s
On peut alors dériver cette classe :
c l a s s triangle : public figure {
point A , B , C ;
public :
v i r t u a l void dessiner ( ) { /∗ code ∗/ }
virtuel void rotation ( double angle ) { /∗ code ∗/ }
}
6.3 Conclusion
Dans la pratique, la très grande majorité des classes que l’on est amenées à programmer sont des
classes concrètes autocontenues. Pour se lancer dans la programmation orientée objet au sens pure du
terme, il faut d’abord bien maîtriser la programmation de ces classes concrètes. La programmation
orientée objet trouve sa force quand on a besoin de polymophisme, i.e. de variables pouvant changer
de type, à l’exécution.
cel-00725771, version 1 - 27 Aug 2012
7 Les templates et la programmation
générique
Il est courant qu’un même algorithme soit programmé plusieurs fois pour des raisons purement
informatique. Programmer un même algorithme plusieurs fois est source d’erreur. Il serait intéres-
sant de pouvoir écrire un algorithme sous forme abstraite et laisser le compilateur l’adapter aux
différents types. C’est possible en utilisant ce que l’on appelle la programmation générique. Ce type
de programmation fait principalement appel à ce que l’on appelle les « templates ».
Beaucoup de fonctions portant le même nom implémente le même algorithme et ne diffèrent que
par le type de leur arguments. Par exemple, regardons la fonction max :
i nt max ( i nt a , i nt b )
{
i f ( a>b )
{
return a ;
}
else {
return b ;
}
}
f l o a t max ( f l o a t a , f l o a t b )
{
i f ( a>b )
{
return a ;
}
else {
return b ;
}
}
Et il faudrait écrire toutes les versions pour tous les types prédéfinis et tous ceux définis par l’utili-
sateur alors que le corps de la fonction aura toujours la même structure. Peut-on réduire le nombre
de copier-coller ? C’est possible avec les template.
64
page 65 Introduction au C++ par Kévin Santugini
template<typeneame LessThanComparable>
LessThanComparable
max ( LessThanComparable a , LessThanComparable b )
{
i f ( a>b ) {
return a ;
}
else {
return b ;
}
}
Maintenant tout appel à la fonction max avec deux arguments de même type va générer une nouvelle
fonction max à partir du template. Supposons que l’on ait un type mot pour lequel on a défini une
relation d’ordre alphabétique < alors max(mot1,mot2) retourne le mot le plus à la fin du dictionnaire.
La fonction max(mot,mot) est instanciée au premier appel de la fonction max pour le type mot.
cel-00725771, version 1 - 27 Aug 2012
On peut alors déclarer des listes de types différents sans recopier 50 fois la même définition.
ma_liste<int> liste_entiers ;
ma_liste<f l o a t > liste_flottants ;
ma_liste<char> liste_caracteres ;
Cette liste est très primitive et n’a qu’un usage pédagogique. Utilisez la liste de la STL std::list
dans un vrai programme.
Chapitre 7. Les templates et la programmation générique page 66
fun ( i nt ∗ a )
{
cel-00725771, version 1 - 27 Aug 2012
// Corps de l a f o n c t i o n
}
La deuxième fonction fun est appelée une spécialisation partielle. La troisième est une spécialisation
totale pour le type int∗. Les appels suivants sont résolus comme suit :
i nt n ;
float ∗ p ;
i nt ∗ q ;
fun ( n ) ; // U t i l i s e l e t e m p l a t e fu n (T a )
fun ( p ) ; // U t i l i s e l e t e m p l a t e fu n (T∗ a )
fun ( q ) ; // U t i l i s e l e non t e m p l a t e fu n ( i n t a )
Les règles de résolution sont extrêmement compliquées mais elles donnent en général ce que l’on
souhaite : la fonction la plus spécifique est utilisée : bon nombre d’arguments et chaque argument a
un ensemble de types acceptables inclus dans celui de toutes les autres fonctions. Par exemple si on
a défini les template
template<typename T , typename U> fun ( T a , U b )
{
// Corps de l a f o n c t i o n
}
{
// Corps de l a f o n c t i o n
}
alors l’appel à fun dans
double∗ p ;
double∗ q ;
// . . .
fun ( p , q ) ;
ne pourra être résolu : aucune des spécialisations n’est plus spécifique que l’autre pour tous les
arguments : la deuxième spécialisation l’est plus pour le premier argument et la troisième plus pour
le deuxième argument.
La STL n’est pas orientée objet : il n’y a pas de classe de base abstraite dont hérite chacun des
conteneurs. La STL est un exemple de programmation générique. Pour comprendre la STL, il faut
avant tout comprendre la notion d’itérateurs.
Voyons d’abord comment on ferait une recherche linéaire sur un tableau contigû :
unsigned i nt find ( tableau t , i nt u )
{
unsigned i nt i ;
for ( i =0; i<t . size()&& t [ i ] ! = u ; i++)
; // do n o t h i n g
return ( i ) ;
}
et comment on ferait typiquement cette même recherche sur une liste simplement chaînée
link∗ find ( liste l , i nt u )
{
for ( link ∗ p=liste−>head ; p !=0 && p−>val!=u ; p=p−>next )
; // do n o t h i n g
return p ;
}
La principale différence vient de la façon dont on itère sur une liste et de la façon dont on itère
sur un tableau. On avance contigument dans la mémoire dans un tableau et on suit des liens dans
une liste. Si on pouvait avoir une abstraction de l’itération, on pourrait utiliser les template pour
programmer en une seule fois find pour tous les conteneurs. Cette abstraction est appelée itérateur
et ce concept est la base du fonctionnement de la STL.
Un itérateur est une abstraction : tout type qui se comporte comme un itérateur est un itérateur.
On peut voir un itérateur comme un pointeur qui se comporte intelligemment. Un itérateur est un
Chapitre 7. Les templates et la programmation générique page 68
objet qui pointe vers un élément d’un conteneur. De plus, il existe un certain nombre d’opérations sur
cet itérateur. Les plus utilisées sont les opérateurs ++ et −− qui avancent et reculent respectivement
l’itérateur. Les opérateurs de comparaison == et != qui teste si deux itérateurs sont égaux. Enfin,
s’inspirant de la notation de pointeur, l’opérateur unitaire préfixe ∗ retourne le contenu de l’élément
vers lequel pointe l’itérateur. Pour obtenir un itérateur depuis un conteneur, on dispose des fonctions
membres begin() qui pointe vers le premier élément du conteneur et end() qui pointe vers le non-
élément qui se trouve derrière le dernier élément du conteneur.
Maintenant pour chaque conteneur de la STL, on définit un itérateur et en utilisant les template,
on peut définir pratiquement n’importe quel algorithme sur les conteneurs en une seule fois pour
tous les conteneurs !
template <c l a s s Iter , c l a s s ValT>
Iter find ( Iter first , Iter last , ValT>
{
Iter p ;
for ( p=first ; p!= last ; p++) {
i f ( ∗ p==ValT )
break ;
}
cel-00725771, version 1 - 27 Aug 2012
}
et maintenant on peut appeler find grâce à
std : : vector<int> a ;
std : : list<f l o a t > b ;
// Code
find ( a . begin ( ) , a . end ( ) , 0 ) ;
find ( b . begin ( ) , b . end ( ) , 0 . 0 ) ;
La plupart de ces algorithmes sont déjà dans la bibliothèque standard : utilisez-les ! Ce n’est pas
peine de les reprogrammer (sauf à titre pédagogique pour apprendre le C++).
7.5 Conclusion
La programmation générique est un outil très puissant dont nous avons à peine effleurer la surface.
Elle est cependant assez déroutante au début. Elle permet d’éviter beaucoup de copier coller sans
perte de performance à l’exécution. Un de ses désavantages est qu’elle limite l’intérêt de la compi-
lation séparée car les template, outil utilisé partout en programmation générique, doivent être placé
dans les headers. La programmation générique est donc chère en temps de compilation.
8 Conseils et conclusion
Voici quelques conseils pour la suite. Limiter le nombre de variables globales. Cela rend la pro-
grammation plus simple au début mais la maintenance deviendra un cauchemar à terme. Séparer
au maximum le code de haut-niveau et le code de bas-niveau. Créer des fonctions paramétrables et
limiter le copier-coller. Maintenir le « single point of truth » : si je veux modifier mon programme,
une petite modification devrait avoir à intervenir dans le moins de fichiers possibles et dans le
moins d’endroits différents : si vous utilisez une constante quelque part, déclarez un paramètre
const int parametre=10 et utiliser la constante parametre. Si vous modifiez ensuite cette valeur
plus tard, vous n’aurez alors qu’à la modifier en un seul endroit.
Nous n’avons fait qu’effleurer le langage C++ qui est un langage énorme. Le moindre livre d’in-
troduction au C++ fait facilement mille pages. Aussi, reste-t-il beaucoup de points qui n’ont pas pu
être abordés ici. Parmi ces points, on trouve entre autres les exceptions qui sont un moyen de gérer
cel-00725771, version 1 - 27 Aug 2012
les erreurs, l’héritage multiple, et les fonctions classes(qui peuvent remplacer les pointeurs sur les
fonctions).
69