Cours

Transcription

Cours
Université de Nice Sophia-Antipolis
Master STIC 1ère année Informatique
Année 2005-2006
Paradigmes et Langages de Programmation
Introduction aux langages C++ et CAML
Laurence PIERRE
([email protected])
Cette partie du module Paradigmes et Langages de programmation a pour objet
d'illustrer certaines notions vues dans la partie généraliste du cours par une initiation
à deux langages aux caractéristiques bien différentes :
• C++, famille des langages à objets (4 semaines)
• CAML, famille des langages fonctionnels (2 semaines)
Université de Nice Sophia-Antipolis
Master STIC 1ère année Informatique
Année 2005-2006
Paradigmes et Langages de Programmation
Partie I. Introduction au Langage C++
Laurence PIERRE
([email protected])
Les notions de base de la programmation orientée-objet ont été présentées dans le
cours de Java de la Licence d’Informatique, elles ne seront donc que brièvement
rappelées ici. Ce cours (de 8h) se concentre sur l’apprentissage du langage C++, il
approfondit notamment certaines spécificités de ce langage, comme la surcharge
d’opérateurs, les templates, l’héritage multiple,… Au préalable, nous présentons
certaines caractéristiques non objet du langage.
P LAN DU COURS
I. Principales améliorations de C++ vis à vis de C
5
I.1 Constantes et variables
I.2 Fonctions
I.2.1 Déclaration – Surdéfinition
I.2.2 Paramètres avec valeurs par défaut
I.2.3 Passage de paramètres par référence
I.3 Gestion de la mémoire
2
I.4 Entrées/sorties
I.4.1 Opérateurs d’entrées/sorties
I.4.2 Entrées/sorties sur fichiers
II. Préambule. C++ en quelques mots
14
II.1 Classes et méthodes en C++
II.2 C++ et l'héritage
II.3 Organisation logicielle
III. Classes et encapsulation en C++
17
III.1 Classes et instances
III.1.1 Définition de classe
III.1.2 Instances
III.1.3 Discussion sur le principe d'encapsulation
III.1.4 Fonctions membres et qualificatif const
III.2 Constructeurs et destructeurs
III.2.1 Premières notions
III.2.2 Constructeurs avec paramètres
III.2.3 Constructeur de copie
III.3 Déclaration d’amitié
III.4 Agrégation d'objets
IV. Applications C++ sous Unix
32
IV.1 Namespaces
IV.2 Bibliothèques
IV.2.1 Bibliothèques statiques
IV.2.2 Bibliothèques partagées
IV.2.3 Exemple
3
V. Surcharge des opérateurs
43
V.1 Généralités
V.2 Technique de surcharge
VI. Classes génériques
48
VII. Standard Template Library
53
VII.1 Généralités sur la STL - Conteneurs
VII.2 Les chaînes de caractères
VIII. Héritage et classes dérivées
59
VIII.1 Généralités
VIII.2 Définition de classe dérivée
VIII.3 Utilisation des constructeurs de la classe parent
VIII.4 Héritage multiple
VIII.5 Classes virtuelles
IX. Polymorphisme et fonctions virtuelles
76
IX.1 Liaison statique et liaison dynamique
IX.2 Fonctions virtuelles
IX.3 Fonctions virtuelles pures
IX.4 Conversions de types explicites
IX.4.1 Opérateur de conversion classique
IX.4.2 Opérateur static_cast
IX.4.3 Opérateur dynamic_cast
IX.5 Exemple!: liste chaînée hétérogène
Complément. Exceptions en C++
92
4
I. PRINCIPALES AMELIORATIONS DE C++ VIS A VIS
DE C
Le langage C++ est une extension de C à plusieurs titres. Il propose quelques
améliorations du point de vue de la déclaration et de la définition de fonctions, des
opérateurs de gestion de la mémoire, des opérateurs d!‘entrées/sorties,... Nous allons
en faire un bref tour d’horizon dans cette première section.
Mais aussi, et surtout, C++ présente les caractéristiques d’un langage de classes et
permet donc une programmation objet. C’est ce que nous étudierons dans tout le
reste du cours.
Remarque!: l’utilisation d’un compilateur C++ pour compiler un programme C
ouvrira l’accès aux caractéristiques présentées dans cette section I. Toutefois, ne pas
faire la confusion entre le langage C et le sous-ensemble non-objet de C++!!
I.1 Constantes et variables
Pour la définition de constantes, on évitera d'avoir recours au #define, utiliser
plutôt la déclaration const.
Exemples :
const int taille=50;
const char chaine[]="bonjour";
La "variable constante" doit être initialisée lors de sa déclaration. Le compilateur
vérifiera qu'on ne tente pas de modifier la valeur de cette "variable constante".
Une telle déclaration de constante a comme portée le fichier dans lequel elle se
trouve (ce qui n'est pas le cas en C ANSI), on est donc encouragé à la placer dans
un fichier d'en-tête.
Elle peut servir à dimensionner un tableau (ce qui n'est pas le cas en C ANSI).
5
Dans un bloc, les déclarations de variables ne doivent pas nécessairement précéder
les instructions. On pourra se servir de cette caractéristique pour déclarer une
variable le plus près possible de sa première utilisation.
Exemple :
En particulier, un indice de boucle for peut être déclaré de la façon suivante (il
n'est alors connu que dans la boucle) :
for (int i=0; i<20; i++)
{... }
Notons par ailleurs que C++ propose le type bool, dont les valeurs littérales sont
false et true. Il y a compatibilité entre les booléens et les entiers. Un booléen
peut être implicitement converti en entier, false devenant 0 et true devenant 1.
Inversement, toute valeur nulle (ou pointeur nul) est interprétée en false, et toute
autre valeur est interprétée en true.
I.2 Fonctions
I.2.1 Déclaration - Surdéfinition
On rappelle qu’en (pré-standard) C, toute fonction non déclarée avant son utilisation
est supposée retourner un entier, et rien n’est supposé pour ses paramètres.
En C++, on devra toujours déclarer complètement (prototype complet) une
fonction avant de faire figurer des appels à cette fonction.
On peut définir dans le même programme plusieurs fonctions ayant le même nom
mais des signatures différentes. C’est ce que l’on qualifie de surdéfinition de
fonctions. Le compilateur, au vu (du type) des paramètres effectifs, décide de la
fonction à invoquer.
Exemples :
void affiche(int i) {
printf("entier i: %d\n",i);
}
6
void affiche(float f) {
printf("reel f: %f\n",f);
}
int main() {
affiche(12); // appel de la 1ère fonction
affiche(5.8); // appel de la 2ème fonction
}
Enfin, une particularité de C++ consiste à permettre la déclaration de fonctions
inline. A la compilation, les appels à une fonction dont la définition est précédée du
mot-clé inline provoqueront l’insertion du code de la fonction à l’emplacement
de l’appel. Le but de l’utilisation de fonctions inline est donc d’augmenter la vitesse
d’exécution du programme, mais l’inconvénient peut être l’augmentation
significative de la taille du code généré. Il ne faut donc avoir recours à cette
possibilité que pour des petites fonctions.
I.2.2 Paramètres avec valeurs par défaut
On peut prévoir des valeurs par défaut pour les paramètres. Dans le cas où ils n'ont
pas tous des valeurs par défaut, les paramètres avec valeurs par défaut doivent être
consécutifs à partir du dernier.
Les valeurs par défaut seront données à la déclaration de la fonction, et ne le
seront plus à la définition.
Exemple :
float calcule(int a, int b, float x=0.0)
{
return a * x + b;
}
Dans ce cas, c a l c u l e ( 2 , 3 , 6 . 1 ) est correct et retourne 1 5 . 2 , mais
calcule(8,4) est aussi correct et retourne 4.0, c’est à dire qu’en l’absence du
dernier paramètre, sa valeur par défaut de 0.0 a été prise.
Attention toutefois, calcule(5) n’est pas correct car le deuxième paramètre n’a
pas été muni d’une valeur par défaut.
7
I.2.3 Passage de paramètres par référence
On rappelle qu’en C, l’unique mode de passage de paramètres est le passage par
valeur. Ceci nécessite d’avoir recours à l’utilisation de pointeurs (adresses) lorsque
l’on souhaite pouvoir modifier des paramètres.
En C++, deux modes de passage de paramètres sont autorisés!: passage par valeur
et par référence.
Pour indiquer qu’un paramètre est passé par référence, on fait suivre son type par
un & dans l’entête de la fonction. A l’intérieur de la fonction, le paramètre est
manipulé de la même façon qu’un paramètre passé par valeur (pas d’utilisation de
*), de même à l’appel de la fonction (pas d’utilisation de &).
Exemple :
void echange(int &x, int &y)
{
int z = x;
x = y;
y = z;
}
...
int a=8,b=34;
echange(a,b);
On peut également faire du passage de paramètre par référence juste pour éviter la
recopie d'un paramètre volumineux. Si on ne souhaite pas modifier le paramètre, on
le précisera avec le mot-clé const, le compilateur vérifiera alors que le paramètre
n’est effectivement pas modifié.
Exemple :
void met_en_forme(grosse_structure &s)
{
...
}
void affiche(const grosse_structure &s)
{
...
met_en_forme(s);
}
8
Si la fonction met_en_forme ne précise pas const grosse_structure &s,
il n’est pas possible de garantir que affiche ne modifie pas son paramètre, on
obtiendra un message d’erreur à la compilation, par exemple
In function `void affiche(const grosse_structure&)':
could not convert `s' to `grosse_structure&'
in passing argument 1 of `void met_en_forme(grosse_structure&)'
I.3 Gestion de la mémoire
Les opérateurs new et delete permettent les allocations et désallocations mémoire
dynamiques, ils offrent une alternative aux fonctions malloc et free utilisées en
C.
L’opérateur new est suivi d'un identificateur de type (type prédéfini ou type
utilisateur), l’expression prend pour valeur l'adresse d'un bloc mémoire de taille
suffisante pour contenir une valeur du type spécifié (0 si l’allocation est impossible).
L’opérateur sizeof n’est pas utilisé.
L’utilisation d’un entier placé entre [ ] après l’identificateur de type permet
l’allocation pour un tableau.
Exemples :
int *i = new int;
*i = 15;
...
int *x = new int[100];
// similaire à malloc(100*sizeof(int))
L’opérateur delete suivi d'une variable de type pointeur, désalloue le bloc
mémoire se trouvant à cette adresse, l’expression correspondante est void.
L’utilisation de [] sera nécessaire pour la désallocation d’un tableau. Attention,
delete ne doit être utilisé qu'en conjonction avec new.
Exemples :
delete i;
delete [] x;
9
Attention, une telle désallocation globale ne peut se faire que pour un tableau. Bien
entendu, lors de l'emploi de structures de type listes chaînées, arbres, etc…,
delete appliqué à l'adresse d'un maillon ne permettra que de désallouer le bloc
mémoire correspondant à ce maillon.
Il faudra prévoir une construction itérative ou récursive pour désallouer la mémoire
correspondant à toute la structure.
Exemple : destruction d'une liste chaînée
void destruct(Liste &l)
{
Liste p=l, tmp=l;
while(tmp)
{
tmp=p->suivant;
delete p;
p=tmp;
}
l=NULL;
}
// avec typedef Item *Liste;
I.4 Entrées/sorties
I.4.1 Opérateurs d’entrées/sorties
Bien que les fonctions printf et scanf soient disponibles, on utilisera
généralement les opérateurs << e t >>. Ils sont plus souples d'emploi, et ne
nécessitent pas l'utilisation de "formats d'impression".
Le fichier iostream.h doit alors être inclus (ou iostream dans le standard).
L'opérateur << permet de faire une insertion sur un flot de sortie, en particulier sur
cout qui représente la sortie standard et cerr qui représente la sortie erreur
standard (tous deux de type ostream).
L'opérateur >> permet de faire une extraction sur un flot d'entrée, en particulier
sur cin qui représente l'entrée standard (de type istream).
Exemples :
int i;
cout << "Donner i\n";
10
cin >> i;
cout << "Et voila i : " << i << endl;
Ces deux opérateurs sont utilisables sur les types prédéfinis short, int, long, float,
double, bool, char et char *.
Des manipulateurs peuvent être utilisés par ces opérateurs!: endl (vu ci-dessus)
provoque l’écriture du caractère ‘\n’ suivie d’un flush (synchronisation) du tampon,
qui correspond au manipulateur flush. Les affichages en décimal, octal et
hexadécimal sont réalisés par dec, oct, et hex (attention, la mise en forme réalisée
par chacun d’entre eux reste valide jusqu’à l’emploi d’un autre).
La taille d’un champ et la précision pour un flottant peuvent être spécifiées grâce à
setw et setprecision. Attention, dans ce cas inclure iomanip.h.
Exemple :
int i =
float f
cout <<
cout <<
cout <<
<<
cout <<
<<
24, j = 91;
= 3.76942;
"valeur de i en hexa : " << hex << i << endl;
"et valeur de j : " << j << endl;
"Oops !... valeur de j en decimal : "
dec << j << endl;
"Enfin, valeur de f : ---" << setw(8)
setprecision(3) << f << "---\n";
On obtient à l’éxecution de ce code!:
valeur de i en hexa : 18
et valeur de j : 5b
Oops !... valeur de j en decimal : 91
Enfin, valeur de f : --3.77---
Remarquons enfin que la fonction eof() peut être utilisée pour tester la fin de
fichier. Cependant, l'opérateur d'extraction s'évalue à vrai tant qu'on n'a pas lu la
fin de fichier, on peut donc faire une boucle du genre
char c;
while (cin >> c)
cout << "on a lu : " << c << endl;
mais si l'on veut pouvoir considérer tous les caractères, y compris les séparateurs, il
faudra plutôt faire
11
while (cin.get(c))
cout << "on a lu : " << c << endl;
Pour procéder à une lecture ligne par ligne, on pourra écrire
char lecture[128];
while (!cin.eof())
{
cin.getline(lecture,128);
cout << "On a lu : " << lecture << endl;
}
I.4.2 Entrées/sorties sur fichiers
Grâce aux types et fonctions de fstream.h, on peut associer des fichiers aux
flots d'entrée et de sortie. Tout ce qui vient d’être décrit dans la section précédente
est alors utilisable sur des fichiers.
Type ifstream : entrée (ne peut être accédé qu'en lecture)
Type ofstream : sortie (ne peut être accédé qu'en écriture)
Type fstream : peut être accédé en lecture et en écriture
L'ouverture et la fermeture se font par les fonctions (membres)
open(nom_physique)
et
close()
Remarque!: en fait, open peut recevoir un deuxième paramètre qui est le mode
d’ouverture (un mode par défaut est utilisé suivant le type du fichier si rien n’est
précisé). Le mode est formé par la combinaison ("ou" bit à bit) de drapeaux, parmi
eux!: ios::in (lecture), ios::out (écriture), ios::app (concaténation), et
ios::trunc (effacement). Dans le cas de fichiers binaires, on utilisera le drapeau
ios::binary.
On peut aussi utiliser le fait que chacune de ces classes possède un constructeur qui
appelle directement open.
12
Exemple :
ofstream f1;
// déclaration
f1.open("fic.text");
// ouverture
f1 << "ceci est mon texte";
...
ifstream f2("fic2.text"); // décl. + ouverture
f2 >> x;
...
f1.close();
f2.close();
D’autres fonctions peuvent être utiles!:
- is_open() permet de tester si l’ouverture de fichier s’est bien passée
- clear() permet de remettre à zéro les drapeaux d’état, notamment celui
positionné par eof()
- tellg() et tellp() permettent de connaître la position courante des
poin-teurs de lecture (get) et d’écriture (put)
- seekg(pos_type p) et seekp(pos_type p) permettent de déplacer
ces pointeurs à la position absolue p, à partir du début du fichier
- les fonctions read(char *buffer, streamsize s) et write(char
*buffer, streamsize s) sont utilisables dans le cas de fichiers binaires.
Le premier paramètre contient les caractères lus ou à écrire, le deuxième
paramètre est un entier qui spécifie le nombre de caractères à lire/écrire.
13
II. PREAMBULE. C++ EN QUELQUES MOTS
C++ a été conçu et implémenté par Bjarne Stroustrup aux AT&T Bell Labs, la
première version commerciale date de 1985. Il a été standardisé en 1998, sous
l’appellation officielle ISO/IEC 14882-1998. Le standard couvre le langage luimême et sa bibliothèque standard (STL).
Il propose toutes les caractéristiques des langages de classes!: classes et
encapsulation, méthodes et envoi de messages, héritage (simple et multiple), et
polymorphisme.
II.1 Classes et méthodes en C++
Rappelons qu’une classe définit une famille d'objets ayant même structure et même
comportement. Elle comporte une composante statique (les données) et une
composante dynamique (les méthodes, ou fonctions membres en C++). La définition
d'une classe sert de modèle pour construire ses représentants, appelés instances.
3 Chaque objet d'une classe contient ses propres ensembles privé et public de
données représentatives de la classe, plus précisément!C++ donne la possibilité de
définir 3 sections dans une classe!:
- section privée : les données et fonctions ne sont pas accessibles de l'extérieur
(accessibles seulement par des fonctions déclarées dans la classe).
- section protégée : les données et fonctions ne sont pas accessibles de
l'extérieur, sauf des classes dérivées.
- section publique : les (données et) fonctions sont accessibles de l'extérieur.
Toutefois, la notion de déclaration d’amitié (friend) permet de donner la possibilité
à des fonctions extérieures, ou même une classe entière, d’accéder à la section
privée d’une classe.
14
3 Chaque objet d'une classe a sa propre copie des données définies par la classe. Si
on veut faire partager une ou des données par tous les objets, on les déclare comme
static (il n'y a alors qu'un exemplaire, qui est initialisé par défaut à 0). Notons
qu’un membre static peut être initialisé en dehors de sa classe (même s'il est
privé), en utilisant l’opérateur de résolution de portée (voir plus loin).
3 Les constructeurs et destructeurs sont des méthodes particulières. Les
constructeurs garantissent l'initialisation des données contenues dans un objet de la
classe, la création d'un objet active l'initialisation spécifiée. Les destructeurs
permettent une déallocation automatique de la mémoire allouée aux données
dynamiques (pointeurs).
3 Enfin, C++ autorise la surcharge d’opérateurs!: (presque) tous les opérateurs
existants peuvent prendre une nouvelle signification dans une définition de classe.
II.2 C++ et l'héritage
3 Dans une hiérarchie d’héritage, les sous-classes sont appelées classes dérivées en
C++. Elles partagent toutes les variables et méthodes de leurs superclasses.
La spécialisation peut se faire selon 2 techniques!: l'enrichissement (la sous-classe est
dotée de nouvelles variables et/ou de nouvelles méthodes représentant des
caractéristiques propres au sous-ensemble d'objets ainsi décrit) et/ou la substitution
(une nouvelle définition est donnée à une méthode héritée). Chaque sous-classe
contient les champs de données du parent, et ses propres données.
3 La représentation graphique de cette relation forme le graphe d'héritage, la
relation d’héritage est transitive. En cas d’héritage simple, la relation d'héritage est
représentée par une arborescence.
C++ autorise l’héritage multiple (une classe peut hériter directement de plusieurs
autres classes), la relation d'héritage forme donc un graphe orienté sans circuit.
15
3 Les fonctions virtuelles sont utilisées pour supporter le polymorphisme. Chaque
instance d'une sous-classe dans une hiérarchie d’héritage peut recevoir différents
messages avec le même identificateur, elle détermine dynamiquement (liaison
dynamique) l'application particulière du message qui lui est appropriée.
II.3 Organisation logicielle
L'organisation logicielle recommandée est modulaire, elle est donnée par la figure
ci-dessous.
fichier entête A
fichier entête B
fichier entête C
fichier
implémentation A
fichier
implémentation B
fichier
implémentation C
Fichier Principal
Un fichier d'entête et un fichier implémentation sont associés à chaque classe. Le
fichier d’entête contient la déclaration de la classe, voire des entêtes de fonctions
indépendantes et autres déclarations ayant un rapport avec la classe. En outre, il est
bon qu'il contienne tous les commentaires permettant d'utiliser les fonctions, dont
les définitions sont dans le fichier implémentation correspondant.
La documentation d'une application C++ est primordiale, les sources doivent être
commentés et les fichiers d'entête également.
Il est très recommandé que ces commentaires soient sous un format exploitable par
des outils de documentation comme Doc++ (http://docpp.sourceforge.net/)
ou Doxygen (http://www.doxygen.org).
16
III. CLASSES ET ENCAPSULATION EN C++
III.1 Classes et instances
III.1.1 Définition de classe
Comme nous l’avons déjà dit, une classe peut contenir jusqu’à 3 sections (privée,
protégée, et publique). La syntaxe est la suivante!:
class identificateur
{
private :
< données et méthodes >
protected :
< données et méthodes >
public :
< données et méthodes >
};
// accès privé
// accès aux sous-classes
// accès public
Exemple : Classe définissant un compteur
class counter
{
private :
unsigned int value;
// donnée membre
public :
counter()
// constructeur
{
value = 0;
}
void increment()
{
value++;
}
void decrement()
{
value--;
}
unsigned int access_value()
{
return value;
}
};
17
Attention, pour simplifier ce premier exemple, toutes les fonctions sont définies dans
le corps de la classe.
On ne peut accéder à la donnée interne value que grâce aux méthodes
increment, decrement, et access_value.
La première méthode prend le nom de la classe, c’est le constructeur : elle est
exécutée quand un objet de la classe est créé (dans l'exemple, initialisation à 0).
Comme nous l’avons déjà mentionné, il est souhaitable de séparer les déclarations
("prototypes") des méthodes de leurs définitions.
Si la définition d'une méthode est donnée hors de la classe, le nom de la classe doit
être rajouté devant le nom de la méthode, séparé par l’opérateur de résolution de
portée!:: (et ceci même si un unique fichier est utilisé).
Exemple :
// Fichier "count.h"
class counter
{
private :
unsigned int value;
public :
counter()!;
void increment()!;
void decrement()!;
unsigned int access_value()!;
};
// Fichier "count.cc"
#include "count.h"
counter::counter()
{
value = 0;
}
void counter::increment()
{
value++;
}
void counter::decrement()
{
18
value--;
}
unsigned int counter::access_value()
{
return value;
}
III.1.2 Instances
La création d’une instance statique (i.e. non dynamique) de la classe se fait comme
une déclaration de variable classique en C. Des paramètres pourront être passés au
constructeur, ce que nous verrons plus loin.
Voici un exemple d’utilisation de cette classe, c1 et c2 sont deux instances statiques
de la classe, initialisées grâce au constructeur sans paramètres (constructeur par
défaut).
On communique avec elles par envoi de message, l’opérateur . étant utilisé à cet
effet!:
main ()
{
counter c1, c2;
}
// utilisation du constructeur
// sans paramètre
for (int i=1; i<=15; i++)
{
c1.increment();
c2.increment();
cout << "c1 contient: " << c1.access_value()
<< endl;
}
for (i=1; i<=5; i++)
c2.decrement();
III.1.3 Discussion sur le principe d'encapsulation
Discutons sur le petit exemple ci-dessous la nécessité de respecter le principe
d’encapsulation.
Le cas traité montre comment encapsulation et abstraction des données sont liées;
un changement de la structure des données est invisible du monde extérieur à la
classe si les données sont correctement encapsulées.
19
Exemple :
// Version d’une classe Ensemble avec un tableau
class Ensemble
{
private :
int **tab;
int cour;
public :
...
int *get_val(int i){
return tab[i-1];
}
};
// données encapsulees, accesseur
// Version d’une classe Ensemble avec liste chainee
class Ensemble
{
private :
Noeud *debut; // les noeuds ont des champs pointeurs sur int
public :
...
int *get_val(int i){ // données encapsulees, accesseur
Noeud *p = debut;
for (int j=1; j<i; j++) p = p->suivant;
return p->valeur;
}
};
Dans les deux cas, le programme ci-dessous peut être utilisé, ce qui ne serait pas le
cas si les données n’étaient pas encapsulées et aucun accesseur fourni!!
main()
{
Ensemble e1;
...
cout << "Element " << i << " de e1 : "
<< *(e1.get_val(i)) << endl!;
}
Supposons maintenant que l’exemple devienne un peu plus élaboré et nécessite la
présence d’une méthode privée!:
class Ensemble
{
private :
int **tab;
int cour;
20
int cle; // autorisation d’acces ou pas
bool acces_autorise(int i)
{
... // utilisation de la cle
}
public :
...
int *get_val(int i)
{
if (acces_autorise(i-1))
return tab[i-1];
else
{
cerr << "acces interdit a cette donnee \n";
return NULL;
}
}
};
main()
{
Ensemble e1;
int *p;
...
p = e1.get_val(i);
if (p)
cout << "Element " << i << " de e1 : "
<< *p << endl!;
}
On voit aisément qu’encapsulation et accesseur sont indispensables. L'accesseur
get_val utilise la méthode privée acces_autorise de façon transparente pour
l'utilisateur de la classe.
III.1.4 Fonctions membres et qualificatif const
Revenons sur le passage de paramètre par référence et l’utilisation du qualificatif
const, ici dans le cas des fonctions membres.
Nous avons dit dans la section I.2.3 que l’on peut faire du passage de paramètre par
référence juste pour économiser une recopie de paramètre, mais qu’on le précisera
alors avec le mot-clé const afin que le compilateur vérifie que le paramètre n’est
effectivement pas modifié. Prenons l’exemple ci-dessous, où la fonction membre
longueur modifie la donnée membre lg de la classe!:
21
class chaine
{
private :
char lachaine[20];
int lg;
public :
chaine(char *s)
{
strcpy(lachaine,s);
}
char *getchaine()
{
return lachaine;
}
int longueur()
{
return lg=strlen(lachaine);
}
};
void affichelg(const chaine &c)
{
cout << "la longueur de la chaine est "
<< c.longueur() << endl;
}
main()
{
chaine machaine("bonjour");
affichelg(machaine);
}
La fonction affichelg reçoit en paramètre une chaine par référence, avec le
qualificatif const, ce qui signifie que la chaine ne doit pas être modifiée (ce qui
n’est clairement pas le cas ici). Le compilateur donne un message d’erreur du style
In function `void affichelg(const chaine &)':
passing `const chaine' as `this' argument of `int chaine::longueur()'
discards qualifiers
Dans un tel contexte, la fonction membre utilisée doit préciser qu’elle ne modifie
aucune donnée membre, et ceci également grâce au qualificatif const placé en fin
d’entête.
22
Bien entendu, il faut que cette assertion soit vraie. Dans l’exemple ci-dessus, si l’on
se contente d’écrire
int longueur() const
{
return lg=strlen(lachaine);
}
Alors le compilateur émettra un message tel que
In member function `int chaine::longueur() const':
assignment of data-member `chaine::lg' in read-only structure
Il faut une solution du style ci-dessous, qui est correcte!(l’affectation de lg devant
alors être faite par ailleurs !)!:
int longueur() const
{
return strlen(lachaine);
}
III.2 Constructeurs et destructeurs
III.2.1 Premières notions
Les constructeurs sont des méthodes qui portent le même nom que leur classe, et
qui fournissent une initialisation automatique des objets au moment où ils sont créés.
Les destructeurs sont des méthodes qui permettent une désallocation automatique
de la mémoire allouée aux données dynamiques d'un objet quand on sort du bloc
contenant cet objet, ou quand on provoque explicitement la "destruction" de l'objet.
Leur nom est formé par un ~ suivi du nom de la classe.
Exemple : Pile d'entiers
class stack
{
private :
int *top;
int *bottom;
public :
stack()
// constructeur
23
{
};
top = bottom = new int[100];
}
void push(int c)
{
if ((top-bottom) < 100)
*top++ = c;
}
int pop()
{
if (top > bottom)
return *--top;
}
~stack()
// destructeur
{
delete [] bottom;
}
On peut alors écrire le programme suivant, pour inverser une chaîne de caractères :
#include "stack.h"
char *reverse_name(char *name)
{
stack s;
char *reverse;
for (int i = 0; i < strlen(name); i++)
s.push(name[i]);
reverse = new char[strlen(name) +1];
for (i = 0; i < strlen(name); i++)
reverse[i] = s.pop();
reverse[strlen(name)] = '\0';
return reverse;
}
main()
{
char your_name[20];
cout << "Donnez le nom à inverser\n";
cin >> your_name;
cout << "Résultat : " << reverse_name(your_name);
}
A la sortie de la fonction reverse_name, la variable locale s est "détruite". Grâce
au destructeur de la classe stack, l'emplacement de la chaîne de caractères empilée
dans s est libéré.
24
III.2.2 Constructeurs avec paramètres
Les valeurs initiales attribuées aux données membres d'un objet peuvent varier d'une
instance à l'autre. Dans ce but, les constructeurs peuvent admettre des paramètres.
Exemple : reprenons le cas du compteur
class counter
{
private :
unsigned int value;
public :
counter(unsigned int v)
{
value = v;
}
void increment()
{
value++;
}
void decrement()
{
value--;
}
unsigned int access_value()
{
return value;
}
};
La création d’une instance doit alors prévoir de passer des valeurs comme
paramètres effectifs au constructeur. Dans le cas d’une instance statique (i.e. non
dynamique), les paramètres doivent être indiqués entre parenthèses après
l’identificateur de la variable, par exemple!:
main ()
{
counter c1(10);
for (int i=1; i<=15; i++){
c1.increment();
cout << "c1 contient: " << c1.access_value()
<< endl;
}
}
25
La création d’une instance dynamique (pointeur) se fait grâce à l’opérateur new
(comme en Java), elle doit également prévoir de passer des valeurs comme
paramètres effectifs au constructeur, ceux-ci étant placés entre parenthèses après
l’identificateur du type. Attention, l’envoi de message se fait alors par
l’intermédiaire de l’opérateur ->. Par exemple!:
main ()
{
counter *c1 = new counter(5);
for (int i=1; i<=15; i++){
c1->increment();
cout << "c1 contient: " << c1->access_value()
<< endl;
}
}
Notons qu’un constructeur, comme toute fonction en C++, peut admettre des
arguments par défaut, et il est également possible de définir plusieurs constructeurs
pour une même classe.
Ceci va être utile en particulier lorsque l'on va manipuler des tableaux d'objets. En
effet, pour déclarer un tableau d'objets d'une classe possédant un constructeur, ce
constructeur doit pouvoir être appelé sans argument.
Exemple :
class point
{
private :
int x1, x2;
public :
point(int x, int y)
{
x1 = x; x2 = y;
}
...
};
Avec une telle définition de constructeur, on peut faire par exemple
point p(3,4);
mais pas
ou
point p2;
point tabp[20];
26
Il y a alors deux solutions pour pallier à ce problème : utiliser des arguments par
défaut ou utiliser 2 constructeurs, comme ci-dessous
class point
{
...
public :
point(int x=0, int y=0)
{
x1 = x; x2 = y;
}
...
};
// valeurs par défaut
class point
{
...
public :
point(int x, int y)
// constructeur 1
{
x1 = x; x2 = y;
}
point()
// constructeur 2
{
x1 = 0; x2 = 0;
}
...
};
III.2.3 Constructeur de copie
Un constructeur de copie est un constructeur particulier, dont la signature est de la
forme :
nom-de-la-classe(const nom-de-la-classe& paramètre);
Exemple :
counter(const counter& c);
En particulier, il est automatiquement invoqué lorsqu'une instance de classe doit être
construite à partir d'une instance existante (déclaration d'une instance avec
initialisation, passage de paramètre par valeur, valeur de retour d'une fonction).
27
Exemple :
class Chaîne
{
private :
char *s;
public :
Chaîne(const Chaîne &t)
{
s = new char[strlen(t.s)+1];
strcpy(s,t.s);
}
...
};
Il est souhaitable de définir un constructeur de copie explicite dès que la classe
contient des données dynamiques (pointeurs).
III.3 Déclaration d’amitié
Les membres privés d'une classe sont inaccessibles aux fonctions membres des
autres classes et aux fonctions indépendantes. C++ donne la possibilité de faire
accéder des fonctions (ou des classes) extérieures à des données privées, ceci par
l’intermédiaire de la déclaration d’amitié.
Par exemple, si un objet a de classe A doit fréquemment envoyer un message
particulier à un objet b de classe B, il peut être bon de déclarer cette méthode de la
classe B comme un ami de la classe A. Cette possibilité doit être utilisée de façon
réfléchie et exceptionnelle.
Une déclaration d'amitié peut être placée soit dans la section privée soit dans la
section publique.
Exemple :
class A
{
...
friend int fct1(float z);
// fonction indépendante amie
friend char *B::method1();
// méthode amie (de la classe B)
28
}
friend class C;
...
// classe amie
La fonction fct1, la méthode method1 de la classe B, et toutes les méthodes de la
classe C sont autorisées à accéder aux membres privés de la classe A.
III.4 Agrégation d’objets
Il est fréquent de rencontrer dans une définition de classe des données membres qui
sont des instances d'autres classes. Il faut alors prendre en compte leur initialisation
et leur destruction.
Pour initialiser les objets internes, les indications des arguments à passer au
constructeur sont données dans l’entête du constructeur de la classe englobante,
après le caractère!:, dans n'importe quel ordre et séparées par des virgules. Les
constructeurs sont exécutés dans l'ordre correspondant aux déclarations des objets
membres.
Remarque : Si l'en-tête d'un constructeur de classe ne mentionne rien quant à un
objet membre, il faut que cet objet possède un constructeur sans argument.
En ce qui concerne le destructeur, la partie de code correspondant à la classe est
exécutée après celle correspondant aux objets internes.
Exemple :
class inner_class
{
private :
int x;
};
public :
inner_class(int z)
{
x = z;
}
void write()
{
cout << "x = " << x << endl;
}
29
class outer_class
{
private :
int y;
inner_class i1, i2;
public :
outer_class(int z, int z1, int z2);
void write()
{
cout << "y = " << y << endl;
cout << "Pour i1 : \n";
write_inner(i1);
cout << "Pour i2 : \n";
write_inner(i2);
}
void write_inner(inner_class i)
{
i.write();
}
};
outer_class::outer_class(int z, int z1, int z2) :
i1(z1), i2(z2)
{
y = z;
}
int main()
{
outer_class obj(5,10,15);
obj.write();
...
}
Remarquons que, tout comme en Java, il est possible d’imbriquer des définitions de
classes. Déclarée privée, une classe interne ne sera visible que par sa classe externe;
déclarée publique, elle pourra être instanciée à l'extérieur de sa classe (par exemple
Livre::Page ci dessous).
Exemple :
class Livre
{
class Page
{
private:
int nblignes;
30
public:
Page(int n=100)
{
nblignes = n;
}
...
};
private:
char *auteur;
Page *tab;
public:
Livre(char *a, int nbpages)
{
auteur = new char[strlen(a)+1];
strcpy(auteur, a);
tab = new Page[nbpages];
}
...
};
int main()
{
Livre l("Edgard Poe", 452);
...
}
Auto-référence dans les classes : généralement, dans la définition d'une fonction
membre, les noms des méthodes peuvent être utilisés sans référence explicite à
l’objet. S'il faut faire référence à l'objet ayant appelé la fonction membre, on utilise
le mot clé this qui représente un pointeur sur cet objet.
31
IV. APPLICATIONS C++ SOUS UNIX
Voyons quelques aspects techniques liés au développement d'applications C++ : tout
d'abord la notion de namespaces, puis la création et l'utilisation de bibliothèques
sous Unix.
Rappelons que l'architecture logicielle recommandée est la suivante :
- un fichier d'entête (contient la déclaration de la classe, voire des déclarations
ayant un rapport avec la classe) et un fichier implémentation (contient les
définitions de fonctions) sont associés à chaque classe.
- l'application utilisant les différentes classes devra inclure les fichiers d'entête, et
la compilation devra faire intervenir tous les fichiers implémentations.
IV.1 Namespaces
La notion de namespace permet de regrouper des déclarations sous un même nom
(le namespace) autorisant ainsi la présence d’identificateurs similaires dans des
namespaces différents.
Pour placer des déclarations dans un même namespace, il suffit de faire
namespace nom-de-namespace
{
< déclarations >
}
De l’extérieur du namespace, on peut accéder à ses déclarations au moyen de
l’opérateur de résolution de portée.
Exemple :
namespace moncompteur
{
class counter
{
private :
unsigned int value;
public :
counter(unsigned int v) {
value = v;
}
32
void increment();
void decrement();
unsigned int access_value();
};
}
void counter::increment()
{
value++;
}
void counter::decrement()
{
value--;
}
unsigned int counter::access_value()
{
return value;
}
main ()
{
moncompteur::counter *c1 =
new moncompteur::counter(5);
for (int i=1; i<=15; i++)
{
c1->increment();
cout << "c1 contient: " << c1->access_value()
<< endl;
}
}
On peut cependant éviter d’avoir recours à l’opérateur de résolution de portée, en
utilisant la directive using namespace. Sa portée est le bloc dans lequel elle se
trouve, ou tout le code si elle se trouve utilisée hors de tout bloc.
Exemple :
namespace moncompteur
{
class counter
{
...
}
}
main ()
{
using namespace moncompteur;
counter *c1 = new counter(5);
33
}
for (int i=1; i<=15; i++)
{
c1->increment();
cout << "c1 contient: " << c1->access_value()
<< endl;
}
Remarque 1!: on peut imbriquer des namespaces, il faudra alors utiliser l’opérateur
de résolution de portée pour "descendre" dans les sous-namespaces, par exemple
A::B::C fera référence au namespace C se trouvant dans B, se trouvant dans A.
Remarque 2!: dans le cas où la déclaration de la classe et la définition de ses
fonctions membres se trouvent dans des fichiers différents, on utilisera la déclaration
de namespace dans les deux fichiers.
Exemple :
// fichier counter.h
namespace moncompteur
{
class counter
{
private :
unsigned int value;
public :
counter(unsigned int v)
{
value = v;
}
void increment();
void decrement();
unsigned int access_value();
};
}
// fichier counter.cc
#include "counter.h"
namespace moncompteur
{
void counter::increment()
{
value++;
}
34
}
void counter::decrement()
{
value--;
}
unsigned int counter::access_value()
{
return value;
}
Remarque 3!: dans le cas de fichiers déclarant des namespaces et incluant également
d’autres fichiers contenant des namespaces, prendre garde à placer les directives
d’inclusion #include à l’extérieur des déclarations de namespaces.
IV.2 Bibliothèques
Sous Unix, la création et l'utilisation de bibliothèques, statiques ou partagées, se fait
exactement de la même façon que dans le cas d'applications en C. Revoyons
rapidement ces notions.
IV.2.1 Bibliothèques statiques
Dans le cas de l'utilisation d'une bibliothèque statique, l'éditeur de liens inclut le
code correspondant dans l'exécutable généré.
Une bibliothèque statique est un fichier archive qui contient du code exécutable et
débute par une table des symboles, i.e. une description des modules et identificateurs
permettant à l'éditeur de liens de résoudre les références correspondantes sans avoir
à parcourir toute la bibliothèque.
Les bibliothèques statiques ont un nom de la forme libnom.a. Elles peuvent contenir
un ou plusieurs fichiers objets, issus de la compilation d'une ou plusieurs classes
C++. On les crée grâce à la commande ar (option r pour l'insertion). Pour générer
l'index de l'archive et le stocker dans l'archive, on utilise ensuite la commande ranlib.
Par ailleurs, lors de la compilation du fichier utilisant la bibliothèque, il faut préciser
par l'option -l que la bibliothèque doit être prise en compte lors de l'édition de liens.
L'option -L permet d'indiquer son chemin d'accès.
35
IV.2.2 Bibliothèques partagées
Dans le cas de l'utilisation d'une bibliothèque partagée, l'éditeur de liens insère dans
le code généré un fragment de code qui est destiné à lancer l'éditeur de liens
dynamique lorsqu'on exécutera le programme. Lors de l'édition dynamique de liens
(ld.so), les bibliothèques partagées requises par l'application sont localisées et
chargées en mémoire.
Les bibliothèques partagées ont un nom de la forme libnom.so, elles peuvent
également contenir un ou plusieurs fichiers objets.
Pour créer une bibliothèque partagée, les fichiers doivent tout d'abord être compilés
de façon à générer des fichiers objets de type PIC (PIC = Position Independent
Code, c'est à dire code relogeable ou "relocatable"). L'option de compilation -fPIC
doit être utilisée.
Puis la bibliothèque partagée est produite, grâce à une compilation de l'ensemble des
fichiers objets avec l'option -shared.
Lors de la compilation du fichier utilisant la bibliothèque, les options -l et -L jouent
le même rôle que décrit ci-dessus. Attention, à l'exécution de l'application, la
variable d'environnement LD_LIBRARY_PATH devra être correctement positionnée.
IV.2.3 Exemple
Dans l'exemple qui suit, une application placée dans un fichier appli.cc utilise
deux classes Point et Counter.
//// Fichier "Point.h"
// Declaration de la classe Point
namespace LesPoints
{
class Point {
private:
int x1, x2;
public :
Point(int =0, int =0);
void affiche();
36
}
};
bool est_nul();
//// Fichier "Point.cc"
#include <iostream>
#include "Point.h"
// Methodes de la classe Point
using namespace std;
namespace LesPoints
{
Point::Point(int x, int y) {
x1 = x; x2 = y;
}
void Point::affiche() {
cout << "( " << x1 << " , " << x2 << " )" << endl;
}
}
bool Point::est_nul() {
return x1==0 && x2==0;
}
//// Fichier "Counter.h"
// Declaration de la classe Counter
namespace LesCompteurs
{
class Counter {
private :
unsigned int value;
public :
Counter(unsigned int);
void increment();
void decrement();
unsigned int access_value();
};
}
//// Fichier "Counter.cc"
#include "Counter.h"
// Methodes de la classe Counter
namespace LesCompteurs
37
{
Counter::Counter(unsigned int v) {
value = v;
}
void Counter::increment() {
value++;
}
void Counter::decrement() {
value--;
}
}
unsigned int Counter::access_value() {
return value;
}
//// Fichier "appli.cc"
#include <iostream>
#include "Counter.h"
#include "Point.h"
using namespace std;
using namespace LesPoints;
using namespace LesCompteurs;
int main()
{
Point tab[5];
Counter compt(0);
tab[0] = Point(7, 3);
tab[1] = Point(2, 0);
tab[2] = Point(4, 8);
}
cout << "Tableau de points : " << endl;
for (int i=0; i<5; i++){
tab[i].affiche();
if (!tab[i].est_nul())
compt.increment();
}
cout << "Nb de points non nuls = "
<< compt.access_value() << endl;
return 0;
Le makefile suivant peut être utilisé dans ce contexte. Afin de bien illustrer la
création d'exécutables dans les divers cas de figures, nous avons prévu 3 règles
38
différentes, qui servent à produire 3 exécutables de noms différents. Pour simplifier,
les bibliothèques sont placées dans des sous-répertoires locaux.
# compilateur C++
CC = g++
# Fichier contenant l'application
APPLI = appli.cc
# Executables generes
EXEC = $(APPLI:.cc=)
EXECA = $(APPLI:.cc=_liba)
EXECSO = $(APPLI:.cc=_libso)
# Divers fichiers sources, et objets
SRCS = Point.cc Counter.cc
OBJS = $(SRCS:.cc=.o)
# Bibliotheque statique
LIBA = libutil.a
DIRA = ./stat
# Bibliotheque partagee
LIBSO = libutil.so
DIRSO = ./shared
# Creation d'un executable a partir des .o
$(EXEC): $(APPLI) $(OBJS)
$(CC) -o $@ $(APPLI) $(OBJS)
# Creation d'un executable avec la bib. statique
$(EXECA): $(APPLI) $(DIRA)/$(LIBA)
$(CC) $(APPLI) -L$(DIRA) -lutil -o $@
# Creation d'un executable avec la bib. partagee
$(EXECSO): $(APPLI) $(DIRSO)/$(LIBSO)
$(CC) $(APPLI) -L$(DIRSO) -lutil -o $@
# Creation de la bibliotheque statique
$(DIRA)/$(LIBA): $(OBJS)
ar r $(DIRA)/$(LIBA) $(OBJS)
ranlib $(DIRA)/$(LIBA)
# Creation de la bibliotheque partagee
$(DIRSO)/$(LIBSO): $(SRCS)
$(CC) -fPIC -c $(SRCS)
$(CC) -shared -o $(DIRSO)/$(LIBSO) $(OBJS)
# Regle de suffixe, optionnelle (implicite)
#.cc.o:
#
$(CC) -c $<
39
clean:
-rm -i $(EXEC) $(EXECA) $(EXECSO)
-rm -i $(OBJS) $(DIRA)/$(LIBA) $(DIRSO)/$(LIBSO)
Et voici un exemple de session possible :
mamachine% ls -l
total 32
-rw------- 1 lpierre dept 444 sep
-rw------- 1 lpierre dept 256 sep
-rw------- 1 lpierre dept 213 sep
-rw------- 1 lpierre dept 1154 sep
-rw------- 1 lpierre dept 282 sep
-rw------- 1 lpierre dept 151 sep
drwx------ 2 lpierre dept 4096 sep
drwx------ 2 lpierre dept 4096 sep
3
3
3
3
3
3
3
3
18:56
18:55
18:55
19:04
18:54
18:54
18:58
19:02
appli.cc
Counter.cc
Counter.h
Makefile
Point.cc
Point.h
shared
stat
mamachine% make appli
g++
-c -o Point.o Point.cc
g++
-c -o Counter.o Counter.cc
g++ -o appli appli.cc Point.o Counter.o
mamachine% ./appli
Tableau de points :
( 7 , 3 )
( 2 , 0 )
( 4 , 8 )
( 0 , 0 )
( 0 , 0 )
Nb de points non nuls = 3
mamachine% make appli_liba
ar r ./stat/libutil.a Point.o Counter.o
ar: création de ./stat/libutil.a
ranlib ./stat/libutil.a
g++ appli.cc -L./stat -lutil -o appli_liba
mamachine% ls -l
total 64
-rwx------ 1 lpierre dept 9178 sep
-rw------- 1 lpierre dept 444 sep
-rwx------ 1 lpierre dept 9178 sep
-rw------- 1 lpierre dept 256 sep
-rw------- 1 lpierre dept 213 sep
-rw------- 1 lpierre dept 900 sep
-rw------- 1 lpierre dept 1154 sep
-rw------- 1 lpierre dept 282 sep
-rw------- 1 lpierre dept 151 sep
-rw------- 1 lpierre dept 2948 sep
3
3
3
3
3
3
3
3
3
3
19:05
18:56
19:05
18:55
18:55
19:05
19:04
18:54
18:54
19:05
appli
appli.cc
appli_liba
Counter.cc
Counter.h
Counter.o
Makefile
Point.cc
Point.h
Point.o
40
drwx-----drwx------
2 lpierre dept 4096 sep
2 lpierre dept 4096 sep
mamachine% ls -l stat
total 8
-rw------- 1 lpierre dept 4280 sep
3 18:58 shared
3 19:05 stat
3 19:05 libutil.a
mamachine% ./appli_liba
Tableau de points :
( 7 , 3 )
( 2 , 0 )
( 4 , 8 )
( 0 , 0 )
( 0 , 0 )
Nb de points non nuls = 3
mamachine% make clean
mamachine% make appli_libso
g++ -fPIC -c Point.cc Counter.cc
g++ -shared -o ./shared/libutil.so Point.o Counter.o
g++ appli.cc -L./shared -lutil -o appli_libso
mamachine% ls -l
total 52
-rw------- 1 lpierre dept 444 sep
-rwx------ 1 lpierre dept 8814 sep
-rw------- 1 lpierre dept 256 sep
-rw------- 1 lpierre dept 213 sep
-rw------- 1 lpierre dept 900 sep
-rw------- 1 lpierre dept 1154 sep
-rw------- 1 lpierre dept 282 sep
-rw------- 1 lpierre dept 151 sep
-rw------- 1 lpierre dept 3312 sep
drwx------ 2 lpierre dept 4096 sep
drwx------ 2 lpierre dept 4096 sep
3
3
3
3
3
3
3
3
3
3
3
mamachine% ls -l shared
total 8
-rwx------ 1 lpierre dept 7993 sep
3 19:07 libutil.so
18:56
19:07
18:55
18:55
19:07
19:04
18:54
18:54
19:07
19:07
19:06
appli.cc
appli_libso
Counter.cc
Counter.h
Counter.o
Makefile
Point.cc
Point.h
Point.o
shared
stat
mamachine% ./appli_libso
./appli_libso: symbol lookup error: ./appli_libso:
undefined symbol: _ZN5PointC1Eii
zsh: exit 127
./appli_libso
mamachine% export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:./shared
mamachine% ./appli_libso
Tableau de points :
( 7 , 3 )
41
( 2 ,
( 4 ,
( 0 ,
( 0 ,
Nb de
0 )
8 )
0 )
0 )
points non nuls = 3
42
V. SURCHARGE DES OPERATEURS
V.1 Généralités
C++ est un langage qui permet la surcharge de la plupart de ses opérateurs, c’est à
dire la surdéfinition d’opérateurs existants pour de nouveaux types (i.e. nouvelles
classes). Par exemple, l'addition de deux nombres complexes z1 et z2 sera plus
agréable sous la forme :
z1 + z2
¨ surcharge de l'opérateur +
que :
z1.add(z2)
¨ envoi du message "add" à z1 avec le paramètre z2
ou
z2.add(z1)
¨ envoi du message "add" à z2 avec le paramètre z1
Les opérateurs surchargés gardent leur priorité et leur associativité habituelles (ils
doivent conserver leur "arité"). Ils doivent toujours posséder un opérande de type
classe.
Tous les opérateurs suivants peuvent être redéfinis :
- Opérateurs binaires :
= () [] -> new delete (obligatoirement redéfinis comme des fonctions membres)
* / % + - << >> < <= > == != & ^ || && | += -= *= /= %= &= ^= |=
<<= >>= ,
- Opérateurs unaires :
+ - ++ -- ! ~ * & (cast)
Mais les opérateurs suivants ne peuvent pas être redéfinis :
:: . ?: sizeof
Attention, les surcharges des opérateurs ++ et -- correspondent a priori à la notation
"pré". Elles devront être munies d’un opérande supplémentaire, de type int, pour
être associées à la notation "post".
43
V.2 Technique de surcharge
Un opérateur binaire surchargé peut être défini :
- soit par une méthode (non statique) de la classe, ne prenant qu'un argument.
L'argument implicite "this", du type de la classe, est obligatoirement présent.
- soit par une fonction indépendante, prenant deux arguments, souvent déclarée
amie.
Similairement, un opérateur unaire surchargé peut être défini :
- soit par une méthode (non statique) de la classe, sans argument.
- soit par une fonction indépendante, prenant un argument, souvent amie.
La notation :
<operande1> op <operande2>
est équivalente à :
<operande1>.op (<operande2>)
dans le cas où op est redéfini comme une fonction membre,
et à :
op (<operande1>, <operande2>)
dans le cas où op est redéfini comme une fonction indépendante.
Exemple :
class counter
{
private :
char *identif!;
unsigned int value;
public :
counter()
{
value = 0;
}
void *set_identif(char *nom)
{
strcpy(identif,nom);
}
void *operator new(size_t);
int operator++();
// version préfixée
int operator--(int); // version postfixée
44
};
unsigned int operator()();
int operator+(int);
void *counter::operator new(size_t s)
{
counter *c = ::new counter;
// appel du new prédéfini, attention il
// y a invocation du constructeur
c->identif = new char[20];
return c;
}
int counter::operator++()
{
if (value<65535) return ++value;
}
int counter::operator--(int i)
{
if (value>0) return value--;
}
unsigned int counter::operator()()
{
return value;
}
int counter::operator+(int a)
{
return (value += a);
}
main()
{
counter my_counter;
int som;
for (int i = 0; i<12; i++)
{
++my_counter;
cout << my_counter() << endl;
}
som = my_counter + 50;
cout << "Nouvelle valeur: " << my_counter() << "\n";
my_counter--;
cout << "... maintenant: " << my_counter() << endl ;
...
}
Et voyons le même exemple avec des fonctions indépendantes amies...
45
class counter
{
friend int operator++(counter &);
friend int operator--(counter &, int);
friend int operator+(counter &, int);
private :
char *identif!;
unsigned int value;
public :
counter()
{
value = 0;
}
void *set_identif(char *nom)
{
strcpy(identif,nom);
}
void *operator new(size_t);
unsigned int operator()();
};
void *counter::operator new(size_t s)
{
counter *c = ::new counter;
c->identif = new char[20];
return c;
}
unsigned int counter::operator()()
{
return value;
}
int operator++(counter &c)
{
if (c.value<65535) return ++(c.value);
}
int operator--(counter &c, int i)
{
if (c.value>0) return (c.value)--;
}
int operator+(counter &c, int a)
{
return (c.value += a);
}
Evidemmment, le programme d'utilisation est inchangé.
•
46
Les opérateurs << et >> sont très fréquemment surchargés, uniquement comme des
fonctions indépendantes (amies).
Les signatures de ces fonctions sont :
ostream &operator<<(ostream &o, const nom_classe &);
istream &operator>>(istream &i, nom_classe &);
Exemple :
class Complexe
{
friend ostream
&operator<<(ostream &, const Complexe &);
friend istream &operator>>(istream &, Complexe &);
private!:
float reelle, imag;
public :
...
};
ostream &operator<<(ostream &o, const Complexe &c)
{
o << "partie reelle : " << c.reelle
<< " , partie imaginaire : " << c.imag << "\n";
return o;
}
istream &operator>>(istream &i, Complexe &c)
{
cout << "partie reelle ? \n";
i >> c.reelle;
cout << "partie imaginaire ? \n";
i >> c.imag;
return i;
}
Remarque!: les opérateurs surchargés définis comme fonctions membres seront
hérités dans les sous-classes, comme toutes les autres fonctions membres. Toutefois,
operator= étant par défaut défini automatiquement par le compilateur pour toute
classe, il ne sera pas hérité.
47
VI. CLASSES GENERIQUES
Le concept de template permet de réaliser des implantations de types abstraits
paramétrés (homogènes) tels que piles, listes, arbres, etc…
Syntaxiquement, une déclaration de classe générique se fait de la façon suivante :
template <class Type>
class Nom_classe
{
...
type1 methode1(…);
type2 methode2(…);
...
};
template <class Type>
type1 Nom_classe<Type>::methode1(…)
{
...
}
template <class Type>
type2 Nom_classe<Type>::methode2(…)
{
...
}
Type peut être utilisé partout dans la définition de la classe (donnée membre,
paramètres ou valeurs de retour des fonctions,…).
La définition d'une fonction membre à l'extérieur de la classe doit être précédée de
la spécification : template <class Type>, et le nom complet d'une fonction
membre est de la forme :
Nom_classe<Type>::Nom_methode
où Type est le même identificateur que dans la clause template.
48
On pourra ensuite procéder à des déclarations de variables dans lesquelles le Type
est instancié, par exemple!:
Nom_classe<int> var1;
Nom_classe<char> var2;
...
Exemple : illustrons ces concepts avec un type Pile
template <class Type>
class Pile
{
// attention aux déclarations d'amitié dans le cas
// de fonctions génériques :
friend ostream &operator<< <>(ostream &o,
const Pile<Type> &);
private :
Type *la_pile;
int sommet_pile;
const int MAX_PILE;
public :
Pile();
Pile(int);
~Pile();
void empile(Type);
void depile();
Type sommet();
int est_vide();
};
template <class Type>
Pile<Type>::Pile() : MAX_PILE(50)
{
la_pile = new Type[MAX_PILE];
sommet_pile = -1;
}
template <class Type>
Pile<Type>::Pile(int max) : MAX_PILE(max)
{
la_pile = new Type[MAX_PILE];
sommet_pile = -1;
}
template <class Type>
Pile<Type>::~Pile()
{
delete [] la_pile;
}
49
template <class Type>
void Pile<Type>::empile(Type elt)
{
sommet_pile++;
if (sommet_pile < MAX_PILE)
la_pile[sommet_pile] = elt;
else cerr << "Pile pleine ! \n";
}
template <class Type>
void Pile<Type>::depile()
{
if (sommet_pile >= 0) sommet_pile--;
}
template <class Type>
Type Pile<Type>::sommet()
{
return la_pile[sommet_pile];
}
template <class Type>
int Pile<Type>::est_vide()
{
return (sommet_pile == -1) ? 1 : 0;
}
template <class Type>
ostream &operator<<(ostream &o, const Pile<Type> &p)
{
o << "elements de la pile : " ;
for (int i=0; i<=p.sommet_pile ; i++)
o << p.la_pile[i] << " " ;
return o;
}
main()
{
Pile<int> p1, p2(5);
for (int i=0; i<5; i++)
p1.empile(i);
while (!p1.est_vide())
{
p2.empile(p1.sommet());
cout << "On empile dans p2 :"
<< p2.sommet() << "\n";
p1.depile();
}
cout << p2;
}
50
Il est à noter que des fonctions indépendantes peuvent également être génériques.
A l’appel de la fonction, si le prototype utilisé est suffisant pour déterminer le(s)
type(s) paramètre(s) mis en jeu, il ne sera pas nécessaire de le(s) préciser.
Exemple :
template <class T>
T Saisie_et_affiche()
{
T data;
cout << "Saisissez la donnee \n";
cin >> data;
cout << "Donnee saisie : " << data << endl;
return data;
}
template <class T1, class T2>
void Affiche(T1 data1, T2 data2)
{
cout << "Donnees a afficher : " << data1
<< " et " << data2 << endl;
}
main()
{
int x;
x = Saisie_et_affiche<int>();
float f = 9.34;
Affiche(x,f);
}
Remarque : les fonctions indépendantes génériques peuvent être amies de classes
non génériques.
Exemple :
class A
{
template <class T> friend void Affiche(T, A);
float x;
public:
...
};
template <class T >
void Affiche(T data1, A data2)
{
cout << "Donnees a afficher : " << data1
51
}
<< " et " << data2.x << endl;
Notons enfin qu’on peut définir des modèles de fonctions ou de classes ayant des
paramètres arithmétiques, par exemple de type entier.
Exemple : classe tableau de float indicé entre A et B
template<int A, int B>
class Tableau
{
float tab[B-A+1];
public :
float operator[](int i);
...
};
template<int A, int B>
float Tableau<A,B>::operator[](int i)
{
if (i>=A and i<=B)
return tab[i-A];
else
{
cerr << i << " hors des bornes "
<< A << " et " << B << endl;
return 0;
}
}
52
VII. STANDARD TEMPLATE LIBRARY
VII.1 Généralités sur la STL - Conteneurs
La bibliothèque standard de C++, standardisée en même temps que le langage et
souvent assimilée à la STL (Standard Template Library), contient des classes et des
fonctions pour!:
- les entrées/sorties (nous en avons dit quelques mots dans la section I.),
- les conteneurs (vecteurs, listes, deques, piles, ensembles,...) et algorithmes
associés,
- les itérateurs, pour réaliser des itérations sur les conteneurs,
- les chaînes de caractères.
Tous ces éléments se trouvent dans le namespace standard std, on les utilisera
donc avec le préfixe std:: ou bien après avoir eu recours à la directive!:
using namespace std;
La section VII.2 donnera quelques indications pour la manipulation des chaînes de
caractères. Nous allons ici illustrer quelques aspects des conteneurs et itérateurs
avec l’exemple des vecteurs. La liste complète des composants peut être trouvée à
http://www.sgi.com/tech/stl/stl_index_cat.html.
Les conteneurs sont des classes utilisables pour manipuler des collections d’objets.
Divers conteneurs sont spécifiés dans la bibliothèque standard!: vecteurs, listes,
deques, piles, ensembles,... Toutes ces classes sont définies de façon à pouvoir
contenir des objets de n’importe quel type, elles sont génériques. L’instantiation
d’un conteneur doit donc préciser le type des objets contenus.
Voyons le cas des vecteurs, représentés par la classe vector (inclure <vector>).
Les méthodes les plus couramment utilisées sont :
- size : renvoie le nombre d'éléments se trouvant dans le vecteur
- push_back : ajoute l'éléments passé en paramètre à la fin du vecteur
53
- begin et end : renvoient des itérateurs qui référencent respectivement le
premier élément et la position juste après le dernier élément. Ils peuvent être
utilisés pour parcourir le vecteur, mais un parcours classique en itérant sur
les indices (comme pour un tableau) est également possible.
On peut réaliser l'affectation d'un vecteur à un autre (opérateur =), comparer les
contenus de deux vecteurs (opérateur ==), extraire un élément d'un vecteur au
moyen de l'opérateur [].
Le contenu d'un vecteur, comme la plupart des autres conteneurs, peut être trié par
la fonction sort. Les éléments sont triés par ordre croissant, l'opérateur < doit être
défini pour le type des éléments du vecteur. Pour trier l'intégralité d'un vecteur v,
on fera sort(v.begin(), v.end()). Il existe également une version de sort
qui admet un troisième paramètre, adresse d'une fonction de comparaison
permettant d'utiliser un autre critère de tri que l'ordre croissant des éléments.
Les itérateurs (iterator) se présentent comme une généralisation des pointeurs.
Ils représentent des positions d'éléments, et existent pour chaque type de conteneur.
Un itérateur est toujours associé à un conteneur spécifique, son type dépend donc
du type du conteneur. Le type const_iterator permet de spécifier un itérateur
qui peut être utilisé pour examiner les éléments d'un conteneur sans les modifier.
Divers opérateurs peuvent être appliqués à un itérateur, parmi eux * référence
l'élément du conteneur se trouvant à la position spécifiée par l'itérateur, ++ et -permettent d'incrémenter et de décrémenter la valeur de l'itérateur.
Exemple :
main()
{
using namespace std;
const int taille = 10;
vector<float> monvecteur(taille);
cout << "Saisir les elements du vecteur : " << endl;
for (vector<float>::iterator i = monvecteur.begin();
i!=monvecteur.end(); i++)
cin >> *i;
// Tri du vecteur :
54
sort(monvecteur.begin(), monvecteur.end());
cout << "Contenu du vecteur apres tri : " << endl;
for (vector<float>::const_iterator
i = monvecteur.begin(); i!=monvecteur.end(); i++)
cout << *i << endl;
vector<float> copievect;
copievect = monvecteur;
copievect.push_back(111.222);
// Inversion du vecteur :
reverse(copievect.begin(), copievect.end());
cout << "Contenu du vecteur apres inversion : "
<< endl;
for (int i=0; i < copievect.size() ; i++)
cout << copievect[i] << endl;
}
cout << "Et contenu du vecteur d'origine : "
<< endl;
for (int i=0; i < taille; i++)
cout << monvecteur[i] << endl;
Le programme ci-dessus donnera par exemple à l’exécution!:
Saisir les elements du vecteur :
5.9
47.8
123.3
8.0
9.4
75.2
23.6
52.1
743.0
6.3
Contenu du vecteur apres tri :
5.9
6.3
8
9.4
23.6
47.8
52.1
75.2
123.3
743
Contenu du vecteur apres inversion :
111.222
743
123.3
75.2
52.1
47.8
23.6
55
9.4
8
6.3
5.9
Et contenu du vecteur d'origine :
5.9
6.3
8
9.4
23.6
47.8
52.1
75.2
123.3
743
VII.2 Les chaînes de caractères
Les chaînes de caractères peuvent être manipulées au moyen de la classe string
qui offre diverses fonctionnalités (il s’agit en fait de la réalisation de la classe
générique basic_string pour le type char). On inclura le fichier <string>.
Tout comme pour les tableaux, la position du premier caractère est numérotée 0.
Cette classe possède plusieurs constructeurs, un destructeur, et diverses méthodes
pour extraire une sous-chaîne, comparer des chaînes, concaténer des chaînes,
insérer ou remplacer des caractères dans une chaîne, trouver des caractères dans
une chaîne,... Certaines de ces fonctionnalités sont illustrées dans l’exemple cidessous.
Remarque : l’utilisation du type char * et des fonctions sur chaînes de la
bibliothèque C (strcpy, strcmp,...) est possible en C++. On préférera souvent
avoir recours à la classe string.
Exemple :
main()
{
using namespace std;
// initialisations des chaînes!:
string chaine1 = "Bonjour", chaine2("Jim");
string chaine3(4,'!');
string res1;
// concaténation et longueur!:
56
res1 = chaine1 + " " + chaine2 + " " + chaine3;
cout << "res1 = " << res1 << " et sa longueur est "
<< res1.length() << endl;
// extraction de sous-chaîne!:
string res2 = res1.substr(8,3);
cout << "sous-chaine : " << res2 << endl;
// recherche de caractères!:
int n1, n2;
n1 = res1.find("our",0);
if (n1 != -1)
cout << "On trouve 'our' a la position "
<< n1 << endl;
else cout << "chaine non trouvee... " << endl;
n2 = res1.find_first_of("baij",0);
if (n2 != -1)
cout << "On trouve un des caract. a la position "
<< n2 << endl;
else cout << "caracteres non trouves... " << endl;
// insertion!:
string res3 = res1.insert(7,1,',');
cout << "Apres insertion de la virgule : "
<< res3 << endl;
}
// comparaison!:
int comp;
comp = res1.compare(0,7,"Salut",0,5);
if (comp > 0)
cout << res1.substr(0,7)
<< " est superieure a Salut" << endl;
else cout << res1.substr(0,7)
<< " est <= a Salut" << endl;
// ou
if (res1.substr(0,7) > "Salut")
cout << res1.substr(0,7)
<< " est superieure a Salut" << endl;
else cout << res1.substr(0,7)
<< " est <= a Salut" << endl;
Ce petit programme réalise la concaténation de 3 chaînes de caractères, extrait la
sous-chaîne de longueur 3 à partir de la position 8 de res1, recherche la souschaîne "our" et l’un des caractères de "baij" dans res1, y insère 1 fois le
caractère ',' à la position 7, et compare les 7 premiers caractères de res1 avec
"Salut". L’exécution donnera!:
res1 = Bonjour Jim !!!! et sa longueur est 16
57
sous-chaine : Jim
On trouve 'our' a la position 4
On trouve un des caract. a la position 3
Apres insertion de la virgule : Bonjour, Jim !!!!
Bonjour est <= a Salut
Bonjour est <= a Salut
On remarquera la surcharge des opérateurs << et >>, de l’opérateur + pour la
concaténation, et des opérateurs de comparaison.
58
VIII. HERITAGE ET CLASSES DERIVEES
VIII.1 Généralités
L'héritage permet de réutiliser tout (ou des parties d') une classe existante, en
construisant une hiérarchie de composants logiciels réutilisables. La construction de
classe dérivée est la base de l'héritage en C++!; une classe peut avoir plusieurs
parents, c'est ce qu'on appelle l'héritage multiple.
Chaque LOO a ses propres caractéristiques quant à l'héritage. C++ permet à une
classe dérivée d'hériter et de modifier toutes ou quelques méthodes de son parent, et
d'avoir en plus ses propres méthodes.
Visibilité des données du parent par rapport à ses classes dérivées : en C++, une
classe dérivée ne peut accéder aux données privées de son parent sans une
autorisation explicite.
Il suffit alors d'utiliser le mode protégé au lieu du mode privé. Tous les membres
d'une classe déclarés comme protégés sont inaccessibles comme s'ils étaient privés,
sauf par rapport aux méthodes des classes dérivées.
VIII.2 Définition de classe dérivée
Une classe est dérivée de son parent de la façon suivante :
class identificateur!: public ident_parent
{
...
};
ou
class identificateur!: ident_parent
{
...
};
59
Dans le premier cas il s’agit d’une dérivation publique, et dans le deuxième c'est
une dérivation privée. Voyons la différence entre ces deux types de dérivation,
sachant que la dérivation publique est la plus couramment utilisée.
Dérivation publique : les membres publics (resp. protected) de la classe de base sont
membres publics (resp. protected) de la classe dérivée.
Dérivation privée : les membres publics et les membres protected de la classe
parent deviennent membres privés de la classe dérivée. Ceci signifie qu’un client de
la classe dérivée n'a plus accès aux membres publics de la classe de base, seules les
fonctions membres y ont accès.
Bien entendu, dans les 2 cas les membres privés de la classe parent ne sont pas
accessibles aux fonctions membres de la classe dérivée.
Dans le contexte classique où une classe B hérite publiquement d'une classe parent
A, les messages correspondant aux fonctions membres publiques de A peuvent être
envoyés aux objets de la classe B.
Exemple :
class A
{
...
public!:
int fct(...);
...
};
class B : public A
{
...
};
main()
{
A obj1;
B obj2;
obj1.fct(...);
obj2.fct(...);
...
}
//"fct" agit sur "obj2" comme
// s'il était de type A
60
Dans tous les cas, l'opérateur de résolution de portée :: permettra d'accéder aux
membres de la classe de base, en particulier lors de la redéfinition dans la classe
dérivée de méthodes de la classe mère, redéfinitions faisant appel aux méthodes
d’origine.
Comparons dérivations publique et privée sur un petit exemple (pas de constructeur
pour l’instant dans la classe parent, pour simplifier).
class parent_class
{
private :
int i1, i2;
public :
void assign(int p1, int p2)
{
i1 = p1;
i2 = p2;
}
int inc1()
{
return ++i1;
}
int inc2()
{
return ++i2;
}
void display()
{
cout << i1 << " , " << i2;
}
};
class derived_class1 : parent_class
{
private :
int i3;
public :
derived_class1(int p3)
{
i3 = p3;
}
void assign(int p1, int p2)
{
parent_class::assign(p1,p2);
}
// dérivation privée
61
};
int inc1()
{
return parent_class::inc1();
}
int inc3()
{
return ++i3;
}
...
Remarquons qu’il n'y a pas de redéfinition de inc2 en section publique dans la
classe derived_class1, les objets de cette classe ne pourront donc pas recevoir
ce message.
class derived_class2 : public parent_class
{
// dérivation
private :
int i4;
public :
derived_class2(int p4)
{
i4 = p4;
}
int inc1()
{
cout << "redéfinition de inc1\n";
return parent_class::inc1();
}
int inc4()
{
return ++i4;
}
void display()
{
parent_class::display();
cout << "i4 = " << i4 << endl;
}
};
main()
{
parent_class p;
p.assign(-2,-4);
//
p.display();
derived_class1 d1(-4);
d1.assign(17,28);
//
publique
affecte -2 et -4 aux données de p
affecte 17 et 28 aux données i1 et
// i2 de d1
62
}
d1.inc1();
// i1 passe de 17 à 18
...
derived_class2 d2(5);
d2.assign(-6,-8);
// affecte -6 et -8 aux i1 et i2 de d2
d2.inc1();
// message inc1 redéfini dans derived_class2
d2.inc2();
// message inc2 du parent
d2.parent_class::inc1();
// message inc1 du parent
...
VIII.3 Utilisation des constructeurs de la classe parent
Comme chaque objet d'une classe dérivée contient une copie des données du
parent, il faut un moyen d'initialiser ces données. Considérons ici l'initialisation au
moyen d'un constructeur.
Soit B une classe dérivée d'une classe A, le constructeur de B doit prévoir des
arguments à destination d'un constructeur de A (sauf si A n'a pas de constructeur
ou a un constructeur sans argument). Ces arguments (qui peuvent être des
expressions) sont précisés dans la définition du constructeur de B, au niveau de son
en-tête après un!caractère!:
Les constructeurs sont activés dans l'ordre suivant : classe parent, membres de la
classe, et finalement classe dérivée.
Exemple : reprenons l'exemple précédent avec un constructeur
class parent_class
{
private :
int i1, i2;
public :
parent_class(int p1, int p2)
{
i1 = p1;
i2 = p2;
}
int inc1()
{
return ++i1;
}
...
};
63
class derived_class1 : parent_class
{
private :
int i3;
public :
derived_class1(int p1, int p2, int p3) :
parent_class(p1,p2)
{
};
// passage de paramètres au constructeur du parent
i3 = p3;
//
initialisation de la donnée privée i3
}
int inc1()
{
return parent_class::inc1();
}
...
main()
{
parent_class p(-2,-4);
p.display();
derived_class1 d1(17,28,-4);
d1.inc1();
}
On peut, bien entendu, faire appel simultanément à l’héritage et à l’agrégation
d’objets. Le constructeur doit alors être défini en conséquence!: passage de
paramètres au constructeur de la classe parent et aux constructeurs des classes
correspondant aux objets membres.
Exemple :
class parent_class
{
private :
int i1, i2;
public :
parent_class(int p1, int p2)
{
i1 = p1; i2 = p2;
}
int inc1()
{
return ++i1;
}
...
};
64
class inner_class
{
private :
float x;
public :
inner_class(float z)
{
x = z;
}
void write()
{
cout << "x = " << x << endl;
}
};
class derived_class1 : parent_class
{
private :
int i3;
inner_class in;
public :
derived_class1(int p1, int p2, int p3, float p4) :
parent_class(p1,p2), in(p4)
{
i3 = p3;
}
int inc1()
{
return parent_class::inc1();
}
...
};
main()
{
parent_class p(-2,-4);
p.display();
derived_class1 d1(17,28,-4, 9.3);
d1.inc1();
...
}
Nous allons maintenant voir deux petits exemples de constructions de classes
dérivées.
65
• Exemple 1!: on peut dériver de la classe "counter" vue section III.1 la classe
"range_limited_counter" dans laquelle le compteur ne peut pas dépasser une
certaine valeur.
class range_limited_counter : public counter
{
private :
int max_val;
public :
range_limited_counter(int max) // rien pour counter
{
max_val = max;
}
void increment() // redéfinition de increment
{
if (access_value() < max_val)
counter::increment();
}
};
main()
{
range_limited_counter C(10);
for (int i=1; i < 10; i++)
C.increment();
cout << "Valeur : " << C.access_value() << "\n";
C.increment();
cout << "Valeur : " << C.access_value() << "\n";
// on atteint 10
C.increment();
cout << "Valeur : " << C.access_value() << "\n";
// plus d’effet
}
• Exemple 2!: cet exemple décrit la hiérarchie des étudiants et des employés dans
une université. La classe data_record rassemble ce qui est commun à toutes les
catégories de personnes.
class data_record
{
private :
string name;
string street_name;
int number;
string city;
public :
data_record(string n, string s, int num,
66
};
string c);
void print();
...
data_record::data_record(string n, string s, int num,
string c)
{
name = n;
street_name = s;
number = num;
city = c;
}
void data_record::print()
{
cout << "Name = " << name << "\n"
<< "Address = " << number << ", Rue "
<< street_name << ", " << city << "\n";
}
class student : public data_record
{
private :
int id_number;
int level;
public :
student(string n, string s, int num, string c,
int i, int l);
void print();
...
};
student::student(string n, string s, int num,
string c, int i, int l) :
data_record(n, s, num, c)
{
id_number = i;
level = l;
}
void student::print()
{
data_record::print();
cout << "id_number = " << id_number << "\n"
<< "level = " << level << "\n";
}
67
class professor : public data_record
{
private :
string dept;
float salary;
public :
professor(string n, string s, int num, string c,
string d, float sal);
void print();
...
};
professor::professor(string n, string s, int num,
string c, string d, float sal) :
data_record(n, s, num, c)
{
dept = d;
salary = sal;
}
void professor::print()
{
data_record::print();
cout << "dept = " << dept << "\n"
<< "salary = " << salary << "\n";
}
class staff : public professor
{
private :
float hourly_wage;
public :
staff(string n, string s, int num, string c,
string d, float sal, float h);
void print();
...
};
staff::staff(string n, string s, int num, string c,
string d, float sal, float h) :
professor(n, s, num, c, d, sal)
{
hourly_wage = h;
}
void staff::print()
{
professor::print();
cout << "hourly_wage = " << hourly_wage << "\n";
}
68
main()
{
student s("Dupont", "Gambetta", 3, "Nice", 123, 4);
professor p("Dubois", "Vauban", 5, "Toulon",
"Info", 2500);
staff st("Martin", "Colbert", 26, "Marseille",
"Maths", 2000, 30);
...
s.print();
// méthode "print" de la classe "student"
p.print();
// méthode "print" de la classe "professor"
st.print(); // méthode "print" de la classe "staff"
...
}
Remarque : ici, il n'est pas indispensable d'utiliser le mode "protégé" puisque les
méthodes des classes dérivées ne tentent pas d'accès direct aux données privées de
leur classe de base.
Disons quelques mots sur la compatibilité entre instances, statiques ou dynamiques,
d'une classe et de ses classes dérivées. Soit la configuration suivante!:
class A
{
...
};
class B : public A
{
...
};
main()
{
...
A obj1;
B obj2;
A *p_obj1;
B *p_obj2;
...
}
alors on peut réaliser les affectations suivantes :
p_obj1 = p_obj2;
obj1 = obj2; // conversion de obj2 dans le type A!
mais on n’écrira pas!:
69
obj2 = obj1;
enfin l’affectation suivante est légale, avec sa conversion de type explicite :
p_obj2 = (B*) p_obj1;
Notons aussi que, partout où un objet de type A * est attendu en paramètre d’une
fonction, le paramètre effectif peut être de type B * (ou ses sous-classes).
VIII.4 Héritage multiple
L’héritage multiple est réalisable en C++, de la façon suivante!:
class deriv!: public parent1, public parent2, ...
{
...
};
ou
class deriv!: parent1, parent2, ...
{
...
};
ou un mélange de dérivations publiques et privées.
L'opérateur de résolution de portée :: permet, si nécessaire, de différencier deux
membres de classes parents qui portent le même nom.
La création d'un objet de la classe dérivée entraîne l'appel des constructeurs des
classes parent dans l'ordre où ils sont donnés dans la déclaration : le constructeur de
la classe dérivée doit donc prévoir des arguments à destination de chacun des
constructeurs des classes de base (sauf pour les classes de base ayant un
constructeur sans argument ou n'ayant pas de constructeur).
Les destructeurs sont appelés dans l'ordre inverse.
70
Exemple : examinons le graphe d’héritage ci-dessous
Article
Transport
voltage
impédance
puissance
référence
désignation
prixHT
quantité
typeEmballage
dateLivraison
consommation
prixTTC
prixTransport
retirer
ajouter
AlimElectrique
prixTransport
Fragile
Périssable
température
prixTransport
prixTransport
ArticleDeLuxe
Crèmerie
prixTTC
dateLimite
Téléviseur
Œufs
typeTube
largeurEcran
télécommande
provenance
et considérons les définitions des classes Article, AlimElectrique, ArticleDeLuxe et
Téléviseur!:
class Article
{
private :
int ref;
string design;
int quant;
protected :
float prixHT;
public :
Article(int r, string d, float p, int q)
{
71
ref = r;
design = d;
prixHT = p;
quant = q;
};
}
float prixTTC()
{
return (prixHT * 1.196);
}
float prixTransport()
{
return (prixHT * 0.05);
}
void retirer(int q)
{
quant = quant - q;
}
void ajouter(int q)
{
quant = quant + q;
}
class AlimElectrique
{
private :
int voltage;
float imped;
int puissance;
public :
AlimElectrique(int v, float i, int p)
{
voltage = v;
imped = i;
puissance = p;
}
float consom(float nbh)
{
return (puissance * nbh);
}
};
class ArtDeLuxe : public Article
{
public :
ArtDeLuxe(int r, string d, float p, int q) :
Article(r,d,p,q)
{}
float prixTTC()
{
return (prixHT*1.5);
// prixHT est protected
72
}
};
class Téléviseur : public ArtDeLuxe, public AlimElectrique
{
private :
int TypeTube;
int LargEcran;
int telecom;
public :
Téléviseur(int r, float p, int q, int v, float i,
int puis, int t, int l, int tel) :
ArtDeLuxe(r,"téléviseur", p, q),
AlimElectrique(v, i, puis)
{
TypeTube = t;
LargEcran = l;
telecom = tel;
}
};
main()
{
ArtDeLuxe art1(23,"fourrure", 12000, 30);
Téléviseur tel1(56, 800, 500, 220, 2, 50, 3, 56, 1);
...
cout << "Prix TTC de art1 : " << art1.prixTTC();
cout << "Prix TTC de tel1 : " << tel1.prixTTC();
cout << "Consommation de tel1 : "
<< tel1.consom(10.3);
cout << " Prix du transport de tel1 : "
<< tel1.prixTransport() << endl;
...
}
VIII.5 Classes virtuelles
Si une classe est amenée à hériter plusieurs fois d'une même classe (par exemple les
classes B et C dérivent toutes deux d’une classe A, et D dérive de B et aussi de C),
il faut éviter que les données héritées (de A ici) ne soient dupliquées (dans D). On
pourra avoir recours à la notion de classe virtuelle.
Exemple :
class B : public A
{ ...
};
73
class C : public A
{ ...
};
class D : public B, public C
{ ...
};
Pour éviter que D possède deux copies des données membres de A, il suffit de
faire!:
class B : public virtual A
{ ...
};
class C : public virtual A
{ ...
};
class D : public B, public C
{ ...
};
Mais attention, il faut alors que le constructeur de D précise les informations à
transmettre à chaque constructeur :
D(…) : B(…), C(…), A(…)
et aussi que les classes B et C possèdent des constructeurs qui ne passent pas de
paramètres au constructeur de A.
Exemple :
class A
{
int i;
...
};
class B : public virtual A
{
float x;
public :
B(float f, int z) : A(z)
{
74
};
x = f;
}
B(float f)
{
x = f;
}
...
class C : public virtual A
{
char c;
public :
C(char ch, int z) : A(z)
{
c = ch;
}
C(char ch)
{
c = ch;
}
...
};
class D : public B, public C
{
int y;
public :
D(int i, float f, char ch, int z) : B(f), C(ch),
A(z)
{
y = i;
}
...
};
main()
{
A a1(10);
B b1(0.5, 20);
C c1('a', 17);
D d1(7, 3.8, 'w', 100);
...
}
75
IX. POLYMORPHISME ET FONCTIONS VIRTUELLES
IX.1 Liaison statique et liaison dynamique
Grâce au concept de polymorphisme, différents types d'objets peuvent accomplir
différemment le même type d'action. Une entité est polymorphique si, à l'exécution,
elle peut se référer à des instances de classes différentes.
Liaisons statique et dynamique peuvent être réalisées en C++, la liaison statique
étant réalisée par défaut. Voyons leurs caractéristiques :
- liaison statique : le système logiciel décide comment implémenter une action
(consécutive à une réception de message) au moment de la compilation, en
fonction du type déclaré de l'objet recevant le message,
- liaison dynamique : la décision est prise dynamiquement, au moment de
l'exécution, en fonction du type effectif de l'objet au moment de la réception
du message.
La première solution a comme avantage la rapidité d'exécution (le code peut être
optimisé par le compilateur), mais la deuxième offre plus de flexibilité.
Le polymorphisme peut être mis en oeuvre en C++ via le concept de fonctions
virtuelles (qui induisent une liaison dynamique) :
Les fonctions virtuelles permettent aux classes dérivées de fournir leurs propres
versions des fonctions de leurs classes de base, le choix entre ces différentes versions
se faisant en cours d'exécution en fonction du type de l'objet auquel la fonction doit
s'appliquer, pourvu qu'il s'agisse d'un objet dynamique (pointeur ou adresse).
Rappelons que dans un cas de figure tel que ci-dessous :
class A
{
...
};
class B : public A
{
76
...
};
main()
{
...
A obj1;
B obj2;
A *p_obj1;
B *p_obj2;
...
}
l'affectation suivante est réalisable :
p_obj1 = p_obj2;
mais une instruction de type p_obj1->fct(...) appelera par défaut la fonction
fct de la classe A, même après cette affectation alors que p_obj1 est alors un
pointeur sur un objet de type B.
C'est ce que le concept de fonction virtuelle va permettre d'éviter.
IX.2 Fonctions virtuelles
3 Qu'est-ce qu'une fonction virtuelle ?
Une fonction virtuelle doit être déclarée dans la classe parent, en utilisant le mot-clé
virtual devant la déclaration ou définition de fonction.
Une fonction est virtuelle si elle est déclarée virtuelle, ou s'il existe une fonction
virtuelle d'une de ses classes de base qui possède la même signature (nom, et types
de ses arguments formels). Il n'est pas nécessaire que toutes les classes dérivées aient
leur déclaration et implémentation.
Une classe qui déclare ou hérite d’une fonction virtuelle est dite classe polymorphe.
3 Quelle est la différence entre une méthode ordinaire redéfinie et une fonction
virtuelle redéfinie ?
77
Un message étant envoyé à un pointeur sur un objet, si le mot-clé virtual n'est
pas utilisé, le système détermine à la compilation la fonctionnalité associée au
message (liaison statique).
Exemple :
...
parent_class *parent;
derived_class *derived;
...
parent = derived;
parent->print();
¨
// invoque la fonction "print" de la classe parent
// (même si "print" est aussi définie dans la
// classe dérivée)
L'utilisation du mot-clé virtual devant le nom d'une méthode de la classe parent
(print ici) indique au système de ne pas invoquer la méthode suivant le type du
pointeur, mais plutôt suivant le type d'objet référencé (liaison dynamique).
3 Les fonctions virtuelles peuvent également (et surtout...) être utiles dans un cas
tel que ci-dessous :
class A
{
...
public :
virtual int fct(...);
...
};
class B : public A
{
...
public :
int fct(...);
...
};
class C : public A
{
...
public :
int fct(...);
...
};
78
void fct2(A *param)
{
...
param->fct();
...
}
main()
{
...
A *p_obj1;
B *p_obj2;
C *p_obj3;
fct2(p_obj1);
fct2(p_obj2);
fct2(p_obj3);
...
}
La méthode fct est virtuelle donc, bien que le paramètre de fct2 soit de type
pointeur sur A, la méthode à utiliser sera choisie suivant son type effectif.
Exemple 1 : considérons un exemple où la méthode d’affichage est virtuelle et
redéfinie dans toutes les sous-classes, et cette méthode est utilisée par une
fonction!print_info
class parent
{
protected :
char version;
public :
parent()
{
version = 'A';
}
virtual void print()
{
cout << "Classe mere, version = " << version
<< endl;
}
};
class derived1 : public parent
{
private :
int info;
public :
79
};
derived1(int number)
{
info = number;
version = 'B';
}
void print()
{
cout << "Classe derived1, info = " << info
<< " version = " << version << endl;
}
class derived2 : public parent
{
private :
int info;
public :
derived2(int number)
{
info = number;
}
void print()
{
cout << "Classe derived2, info = " << info
<< " version = " << version << endl;
}
};
void print_info(parent *info_holder)
{
info_holder->print();
}
Bien que i n f o _ h o l d e r soit de type pointeur sur p a r e n t, la fonction
print_info appliquera la bonne méthode puisque print est déclarée comme
virtual.
main()
{
parent b;
derived1 d1(3);
derived2 d2(15);
print_info(&b);
print_info(&d1);
print_info(&d2);
}
on le vérifie à l’exécution!:
80
avec "virtual"
Classe mere, version = A
Classe derived1, info = 3 version = B
Classe derived2, info = 15 version = A
sans "virtual"
Classe mere, version = A
Classe mere, version = B
Classe mere, version = A
Exemple 2 : cet exemple reprend les classes Article et ArtDeLuxe vues dans la
section VIII.4. Il illustre le fait que la fonction virtuelle peut être invoquée par
l’intermédiaire d’une autre méthode, non virtuelle.
class Article
{
private :
int ref;
string design;
int quant;
protected :
float prixHT;
public :
Article(int r, string d, float p, int q)
{
ref=r;
design = d;
prixHT=p;
quant=q;
}
void affiche_prixTTC()
{
cout << "On fait le calcul du prix TTC de"
<< " l'objet... il est de "
<< prixTTC() << " euros" << endl;
}
virtual float prixTTC()
{
return (prixHT * 1.196);
}
void ajouter(int q)
{
quant = quant + q;
}
...
};
class ArtDeLuxe : public Article
{
public :
ArtDeLuxe(int r, string d, float p, int q) :
Article(r,d,p,q)
{}
81
};
float prixTTC()
{
return (prixHT*1.5);
}
main()
{
Article *a1 =
new Article(124, "bijou", 350, 20);
a1->affiche_prixTTC();
ArtDeLuxe *a2 =
new ArtDeLuxe(563, "bijou luxe", 1200, 10);
a2->affiche_prixTTC();
if (typeid(*a1) == typeid(*a2)) // inclure <typeinfo>,
// typeid renvoie un objet de type type_info
}
cout << "a1 et a2 sont du meme type" << endl;
else cout << "a1 et a2 ne sont pas du meme type"
<< endl;
cout << "On realise l'affectation..." << endl;
a1 = a2;
a1->affiche_prixTTC();
if (typeid(*a1) == typeid(*a2))
cout << "a1 et a2 sont du meme type" << endl;
else cout << "a1 et a2 ne sont pas du meme type"
<< endl;
ce qui donne à l’exécution!:
On
On
a1
On
On
a1
fait le calcul du prix TTC de l'objet... il est de 422.1 euros
fait le calcul du prix TTC de l'objet... il est de 1800 euros
et a2 ne sont pas du meme type
realise l'affectation...
fait le calcul du prix TTC de l'objet... il est de 1800 euros
et a2 sont du meme type
IX.3 Fonctions virtuelles pures
Quand il n'est pas nécessaire que la classe de base possède une définition d'une
fonction virtuelle, on la déclare comme fonction virtuelle pure.
Une fonction virtuelle pure se déclare avec une initialisation à zéro :
virtual <type> <nom>(...) = 0;
82
Attention : lorsqu'une classe comporte au moins une fonction virtuelle pure, elle est
considérée comme "abstraite", et il n'est plus possible de créer des objets de son
type.
Une fonction virtuelle pure non définie dans une classe dérivée y est encore virtuelle
pure (la classe est encore abstraite).
Exemple : la classe Shape va servir à caractériser toute une famille de formes
géométriques, on pourra en dériver les classes Droite, Cercle, ... Elle contient la
fonction virtuelle pure draw (qui n'a pas de définition sensée tant qu'on n'est pas
dans une sous-classe spécialisant l'objet) :
class Shape
{
protected:
Coord xorig;
Coord yorig;
Color co;
public:
Shape(Coord x, Coord y, Color c) :
xorig(x), yorig(y), co(c)
{}
...
virtual void draw() = 0;
};
IX.4 Conversions de types explicites
IX.4.1 Opérateur de conversion classique
L’opérateur classique de conversion () est utilisable en C++ comme en C.
Exemple :
class counter
{
private!:
unsigned int value;
public :
counter(unsigned int v)
{
value = v;
}
void increment()
83
{
};
value++;
}
...
unsigned int access_value()
{
return value;
}
class range_limited_counter : public counter
{
private :
int max_val;
public :
range_limited_counter(int max=20) : counter(0)
{
max_val = max;
}
void increment()
{
if (access_value() < max_val)
counter::increment();
}
void print()
{
cout << "value = " << access_value()
<< ", max_val = " << max_val << endl;
}
};
main()
{
counter *pc1 = new counter(8);
range_limited_counter *pc2 =
new range_limited_counter(50);
for (int i=0; i<60; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
pc2->print();
pc2 = (range_limited_counter *)pc1;
pc2->print();
for (int i=0; i<50; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
}
Ce qui pourra donner à l’exécution!:
84
valeur de pc2 : 50
value = 50, max_val = 50
value = 8, max_val = 0
valeur de pc2 : 8
Pour les données de type simple, il est aussi possible d’utiliser une notation
fonctionnelle (la variable ou l’expression, et non le type, est entre parenthèses), par
exemple!:
float f;
double d;
f = 3.14;
...
d = double(f);
Dans tous les cas, les types mis en jeu peuvent être des classes, mais aucun contrôle
n’est effectué sur les conversions.
Remarque!: l’opérateur reinterpret_cast permet de convertir un pointeur en
n’importe quel autre type de pointeur, par exemple!:
class A
{
...
};
class B
{
...
};
A *pa;
...
B *pb = reinterpret_cast<B*>(pa);
Aucune verification de compatibilité n’est effectuée, et en outre certains aspects de
cet opérateur sont non portables. Son utilisation est fortement déconseillée.
IX.4.2 Opérateur static_cast
L’opérateur static_cast permet de réaliser des conversions de types de
pointeurs entre classes parents et classes dérivées. Ces conversions peuvent ne pas
être sûres, et aucun contrôle n’est effectué.
85
Exemple : reprenons l’exemple précédent des classes counter et range_
limited_counter, où la fonction main devient
main()
{
counter *pc1 = new counter(8);
range_limited_counter *pc2 =
new range_limited_counter(50);
for (int i=0; i<60; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
pc2->print();
pc2 = static_cast<range_limited_counter *>(pc1);
pc2->print();
for (int i=0; i<50; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
}
Tout comme dans l’exemple précédent, la première conversion n’est pas sûre, et
pourra donner le résultat!ci-dessous!:
valeur de pc2 : 50
value = 50, max_val = 50
value = 8, max_val = 0
valeur de pc2 : 8
Par contre, dans le main ci-dessous la conversion est valide.
main()
{
counter *pc1 = new counter(8);
range_limited_counter *pc2 =
new range_limited_counter(50);
for (int i=0; i<60; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
pc2->print();
pc1 = static_cast<counter *>(pc2);
for (int i=0; i<20; i++)
pc1->increment();
cout << "valeur de pc1 : " << pc1->access_value()
<< endl;
}
86
on aura à l’exécution!:
valeur de pc2 : 50
value = 50, max_val = 50
valeur de pc1 : 70
IX.4.3 Opérateur dynamic_cast
L’opérateur dynamic_cast s’utilise seulement sur des pointeurs (ou références).
Il réalise des vérifications à l’exécution. Si le type de conversion est une classe de
base directe ou indirecte (classe mère, grand-mère,...) de la classe type de
l’expression à convertir, la conversion est valide et l’opérateur renvoie le pointeur
converti (pas de conversion si les classes sont polymorphes).
Exemple : toujours avec les classes counter et range_limited_counter
main()
{
counter *pc1 = new counter(8);
range_limited_counter *pc2 =
new range_limited_counter(50);
for (int i=0; i<60; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
pc2->print();
pc1 = dynamic_cast<counter *>(pc2);
for (int i=0; i<20; i++)
pc1->increment();
cout << "valeur de pc1 : "
<< pc1->access_value() << endl;
}
Ici la conversion est valide et l’exécution produira!:
valeur de pc2 : 50
value = 50, max_val = 50
valeur de pc1 : 70
Dans le cas de classes polymorphes, la vérification à l’exécution permet de s’assurer
que l’expression à convertir correspond bien à un pointeur sur un objet complet du
type de conversion. Sinon, la conversion est invalide et NULL est renvoyé.
87
Exemple :
class counter
{
private!:
unsigned int value;
public :
counter(unsigned int v)
{
value = v;
}
virtual void increment()
{
value++;
}
...
};
...
main()
{
counter *pc1 = new counter(8);
range_limited_counter *pc2 =
new range_limited_counter(50);
for (int i=0; i<60; i++)
pc2->increment();
cout << "valeur de pc2 : " << pc2->access_value()
<< endl;
pc2->print();
pc2 = dynamic_cast<range_limited_counter *>(pc1);
if (pc2 != NULL)
{
...
}
else cerr << "Mauvais cast sur pc2...." << endl;
}
counter *pc3 = new range_limited_counter(100);
range_limited_counter *pc4;
pc4 = dynamic_cast<range_limited_counter *>(pc3);
if (pc4 != NULL)
{
for (int i=0; i<200; i++)
pc4->increment();
cout << "valeur de pc4 : "
<< pc4->access_value() << endl;
}
else cerr << "Mauvais cast sur pc4...." << endl;
88
Ici la première conversion est invalide car pc1 est effectivement de type counter,
ce qui n’est pas le cas pour pc3 dans le deuxième conversion, valide!:
valeur de pc2 : 50
value = 50, max_val = 50
Mauvais cast sur pc2....
valeur de pc4 : 100
IX.5 Exemple!: liste chaînée hétérogène
Voyons l’utilisation des fonctions virtuelles pour gérer une liste chaînée hétérogène,
par exemple dans le cas des membres d'une université : étudiants, professeurs,
personnel administratif, …
Nous définissons d’abord une classe parent univ_community, classe abstraite qui
contient les caractéristiques communes à tous. C’est ce maillon, abstrait mais
général, qui sera manipulé par les méthodes de la classe list.
class univ_community
{
friend class list;
protected :
string last_name, first_name;
int age;
long social_security_number;
univ_community *next;
public :
univ_community(string ln, string fn, int a,
long ss)
{
last_name = ln;
first_name = fn;
age = a;
social_security_number = ss;
next = NULL;
}
univ_community()
{
last_name = "";
first_name = "";
age = 0;
social_security_number = 0;
next = 0;
}
...
virtual void print()
{
89
cout << last_name << ", " << first_name << ", "
<< age << ", " << social_security_number
<< endl;
};
}
virtual univ_community *new_member() = 0;
Puis nous en dérivons toutes les sous-classes nécessaires (student, professor,
staff,...). Chacune d'elles contient dans sa section privée les données appropriées,
et implémente les fonctions virtuelles pures de la classe de base.
class student : public univ_community
{
private :
float grade_point_average;
int level;
public :
student(string ln, string fn, int a, long ss,
float av, int l) :
univ_community(ln, fn, a, ss)
{
grade_point_average = av;
level = l;
}
student()
{
grade_point_average = 0.0;
level = 0;
}
...
void print()
{
univ_community::print();
cout << grade_point_average << ", " << level
<< endl;
}
univ_community *new_member()
{
return new student(last_name, first_name, age,
social_security_number,
grade_point_average, level);
}
};
// Meme chose pour les autres sous-classes.
class list
{
private :
90
univ_community *root;
public :
list()
{
root = 0;
}
void insert_person(univ_community *n);
void print_list();
...
};
void list::insert_person(univ_community *n)
{
univ_community *tbi;
string key;
key = n->last_name;
univ_community *current = root;
univ_community *previous = 0;
while (current &&
current->last_name.compare(key)<0)
{
previous = current;
current = current->next;
}
tbi = n->new_member();
tbi->next = current;
if (previous == 0) root = tbi;
else previous->next = tbi;
}
void list::print_list()
{
univ_community *cur = root;
while (cur != 0)
{
cur->print();
cur = cur->next;
}
}
main()
{
list people;
student st("Dupont", "Jean", 27, 123, 10.36, 4);
student st2("Dubois", "Paul", 22, 567, 12.65, 2);
...
people.insert_person(&st);
people.insert_person(&st2);
...
people.print_list();
}
91
COMPLEMENT. EXCEPTIONS EN C++
3 Tout comme en Java, les exceptions servent à traiter les cas d'erreur, sans avoir
recours (d'une façon un peu artificielle) aux valeurs de retour des fonctions.
Le principe est le suivant : lors d'une erreur dans l'exécution d'un programme, on
"déclenche" une exception (throw). Il reste à intercepter et à traiter l'exception au
niveau voulu (try/catch).
Le "déclenchement" (la "levée") d'une exception, et donc le déroutement du
programme, se fait par
throw exception;
où l’expression exception peut être d’un type quelconque.
L'interception (la récupération) d'une exception se fait par
catch (type-exception e)
{
gestion-de-l-exception-e
}
et doit suivre un bloc réceptif
try
{
traitement-quelconque
}
où le traitement-quelconque est susceptible de lever l'exception.
Exemple :
class Date
{
private :
int jour, mois, annee;
public :
Date(int j, int m, int a)
{
if (j<=0 || j>31) throw "jour non valide";
jour = j;
92
if (m<=0 || m>12) throw "mois non valide";
mois = m;
if (a<=1900 || a>2003) throw "annee non valide";
annee = a;
};
}
void affiche_date()
{
cout << "Nous sommes le " << jour << "/"
<< mois << "/" << annee << endl;
}
...
main()
{
...
try
{
Date d(j,m,a);
d.affiche_date();
...
}
catch(const char *e)
{
cerr << "Attention, pb avec la date :" << endl;
if (!strcmp(e,"jour non valide"))
cerr << " le jour est invalide..." << endl;
else
if (!strcmp(e,"mois non valide"))
cerr << " le mois est invalide..." << endl;
else cerr << " l'annee est invalide..." << endl;
}
}
3 A priori, on utilisera autant de blocs catch que de types différents d’exceptions
susceptibles d’être levées par le bloc try.
Exemple :
using namespace std;
class Date
{
private :
int jour, mois, annee;
public :
Date(int j, int m, int a)
{
if (j<=0 || j>31) throw "jour non valide";
jour = j;
93
if (m<=0 || m>12) throw "mois non valide";
mois = m;
if (a<=1900 || a>2003) throw "annee non valide";
annee = a;
};
}
void affiche_date()
{
cout << "Nous sommes le " << jour << "/"
<< mois << "/" << annee << endl;
}
void fete_anniv(string nom, Date anniv)
{
if (anniv.jour!=jour || anniv.mois!=mois)
throw 0;
cout << "Bon anniversaire " << nom << "!"
<< endl;
}
...
main()
{
...
try
{
Date d(j,m,a);
d.affiche_date();
d.fete_anniv("Nestor", Date(1,1,1970));
...
}
catch(const char *e)
{
cerr << "Attention, pb avec la date :" << endl;
if (!strcmp(e,"jour non valide"))
cerr << " le jour est invalide..." << endl;
else
if (!strcmp(e,"mois non valide"))
cerr << " le mois est invalide..." << endl;
else cerr << " l'annee est invalide..." << endl;
}
catch(int e)
{
cerr << "Ca n'est pas l'anniversaire !" << endl;
}
}
Toutefois, il est à noter que le gestionnaire catch(...) attrape toute exception et
pourra être placé en dernière position pour remplacer plusieurs blocs catch.
94
3 Dans le corps du bloc catch on peut, après avoir traité l'exception, la relancer
par l'instruction :
throw;
et on peut même transformer l'exception en cours de route, en relançant une
exception d’un autre type!:
throw autre-exception;
Exemple :
void traitement()
{
...
try
{
Date d(j,m,a);
d.affiche_date();
d.fete_anniv("Nestor", Date(1,1,1970));
...
}
catch(const char *e)
{
cerr << "Attention, pb avec la date :" << endl;
if (!strcmp(e,"jour non valide"))
cerr << " le jour est invalide..." << endl;
else
if (!strcmp(e,"mois non valide"))
cerr << " le mois est invalide..." << endl;
else cerr << " l'annee est invalide..." << endl;
throw 0;
}
catch(...)
{
cerr << "Ca n'est pas l'anniversaire !" << endl;
}
}
main()
{
...
try
{
traitement();
}
catch(int e)
{
cerr << "Erreur de date, le pg s'arrete..."
<< endl;
95
}
exit(1);
}
...
3 Une fonction peut spécifier dans son entête les exceptions qu'elle est susceptible
de lever, soit directement (par throw) soit indirectement (par propagation). Cette
spécification doit figurer à la déclaration et à la définition.
Exemple :
void traitement() throw (char *, int)
{
...
try
{
Date d(j,m,a);
d.affiche_date();
d.fete_anniv("Nestor", Date(1,1,1970));
...
}
catch(const char *e)
{
cerr << "Attention, pb avec la date :" << endl;
if (!strcmp(e,"jour non valide"))
cerr << " le jour est invalide..." << endl;
else
if (!strcmp(e,"mois non valide"))
cerr << " le mois est invalide..." << endl;
else cerr << " l'annee est invalide..." << endl;
throw;
}
catch(int e)
{
cerr << "Ca n'est pas l'anniversaire !" << endl;
throw;
}
}
Ceci permet de préciser que l'appel à la fonction en question doit être de préférence
placé dans un bloc try avec un (des) catch approprié(s). Si la fonction est une
méthode virtuelle, les redéfinitions de cette méthode dans les classes dérivées ne
pourront pas lever d’autres types d’exceptions.
96
Lorsque rien n'est spécifié, n'importe quelle exception peut être levée. Si throw()
est spécifié alors aucune exception ne peut être levée.
3 La bibliothèque standard propose quelques classes pour les exceptions (le fichier
<exception> doit être inclus). La classe exception est au sommet de la
hiérarchie.
Parmi ses classes dérivées, on peut noter bad_alloc qui correspond à une
exception en cas d’échec d’allocation mémoire (dans <new>), bad_cast qui peut
être levée par un dynamic_cast invalide (dans <typeinfo>), et bad_typeid
qui est levée si l’opérateur typeid est appliqué à un pointeur nul (dans
<typeinfo>).
3 Les exceptions peuvent être de n’importe quel type, toutefois il est souhaitable
de définir des types appropriés.
Exemple :
class Date
{
private :
int jour, mois, annee;
public :
class Jour_non_valide {};
class Mois_non_valide {};
class Annee_non_valide {};
Date(int j, int m, int a)
{
if (j<=0 || j>31) throw Jour_non_valide();
jour = j;
if (m<=0 || m>12) throw Mois_non_valide();
mois = m;
if (a<=1900 || a>2003) throw Annee_non_valide();
annee = a;
}
...
};
main()
{
...
try
{
Date d(j,m,a);
97
d.affiche_date();
...
}
}
catch(Date::Jour_non_valide)
{
cerr << "Attention, pb avec la date
<< " le jour est invalide..."
}
catch(Date::Mois_non_valide)
{
cerr << "Attention, pb avec la date
<< " le mois est invalide..."
}
catch(Date::Annee_non_valide)
{
cerr << "Attention, pb avec la date
<< " l'annee est invalide..."
}
:"
<< endl;
:"
<< endl;
:"
<< endl;
On peut également définir les classes associées aux exceptions comme étant des
classes dérivées de la classe exception. On pourra alors surdéfinir sa méthode
(virtuelle) what qui permet d’obtenir un message explicatif sur la nature de
l’exception (attention, pour les classes exceptions prédéfinies, le message dépend de
l’implémentation).
Exemple :
using namespace std;
class Date
{
private :
int jour, mois, annee;
public :
class Jour_non_valide : public exception
{
public :
const char* what() const throw()
{
return "Attention : le jour est invalide...";
}
};
class Mois_non_valide : public exception
{
public :
const char* what() const throw()
98
{
return "Attention :
le mois est invalide...";
}
};
class Annee_non_valide : public exception
{
public :
const char* what() const throw()
{
return "Attention : l'annee est invalide...";
}
};
Date(int j, int m, int a)
{
if (j<=0 || j>31) throw Jour_non_valide();
jour = j;
if (m<=0 || m>12) throw Mois_non_valide();
mois = m;
if (a<=1900 || a>2003) throw Annee_non_valide();
annee = a;
}
...
};
main()
{
...
try
{
Date d(j,m,a);
d.affiche_date();
...
}
catch(exception &e)
{
cerr << e.what() << endl;
}
}
99
Parmi les sources de ce cours...
3 "Langage C++ - Passage du C au C++", N.Silverio, Eyrolles.
3 "Les langages à objets", G.Masini, A.Napoli, D.Colnet, D.Léonard, K.Tombre,
InterEditions, 1989.
3 http://www.cplusplus.com/doc/tutorial
3 http://www.research.att.com/~bs
3 http://www.sgi.com/tech/stl
3 http://www.msoe.edu/eecs/ce/courseinfo/stl/
100
Université de Nice Sophia-Antipolis
Master STIC 1ère année Informatique
Année 2005-2006
Paradigmes et Langages de Programmation
Partie II. Introduction au Langage CAML
Laurence PIERRE
([email protected])
La programmation fonctionnelle a été étudiée en Licence d’Informatique par
l'intermédiaire du langage Scheme, nous ne reviendrons donc que brièvement sur
les principes de base. Ce cours (de 3h) se concentre sur les spécificités du langage
CAML, comme l'inférence de types, les définitions par filtrage,…
PLAN DU COURS
I. Pour débuter...
103
I.1 Premières manipulations
I.2 Quelques mots sur les types de base
I.3 Types composites
II. Fonctions
109
II.1 Définition de fonctions
II.1.1 Fonctions à un paramètre
II.1.2 Fonctions à plusieurs paramètres
101
II.1.3 Portée des variables
II.2 Fonctions d'ordre supérieur
II.3 Fonctions polymorphes
II.4 Fonctions récursives
III. Filtrage de motif
119
III.1 Déclaration de types utilisateur
III.1.1 Enregistrements (types produit)
III.1.2 Types somme
III.2 Filtrage
III.2.1 Principes et syntaxe
III.2.2 Filtrage et fonctions récursives
Quelques compléments
130
Structures modifiables
Entrées/sorties
Chargement, compilation
Programmation modulaire, compilation séparée
102
I. POUR DEBUTER...
Le langage ML ("Meta-Language") fut développé dans les années 1970, dans
l'équipe de Robin Milner à l'Université d'Edinburgh. A l'origine, l'objectif était la
mise en oeuvre d'un système de démonstration automatique, LCF ("Logic of
Computable Functions") permettant de faire du raisonnement formel sur des
programmes. Le langage CAML ("Categorical Abstract Machine Language"),
comme Standard ML (SML), est l'un des dialectes de ML.
CAML est développé à l'INRIA (http://caml.inria.fr) depuis le milieu des
années 1980. Caml Light est une version allégée du langage CAML d'origine, et son
successeur Objective Caml, créé en 1996, propose notamment des aspects de
programmation orientée-objet. Cette brève introduction ne présentera que certains
aspects essentiels du langage (non objets), en s'appuyant sur OCaml.
ML (et donc CAML) est principalement un langage de programmation fonctionnelle, qui a subi l'influence de Lisp et s'appuie tout comme lui sur une mise en
oeuvre du lambda-calcul. On y trouve donc les caractéristiques de cette famille de
langages : fonctions de première classe (peuvent être passées en argument ou
retournées comme valeur d'une fonction, être la valeur d'une variable,...), utilisation
de la récursion, non-utilisation (autant que possible !) d'effets de bords,... C'est
également un langage fortement typé, proposant un puissant mécanisme d'inférence
de types et permettant de manipuler des fonctions polymorphes.
I.1 Premières manipulations
Avant d'apprendre à réellement programmer en Caml, et à compiler nos
programmes, nous allons commencer à découvrir la syntaxe du langage avec
quelques manipulations d'expressions sous l'interpréteur.
Une fois sous l'interpréteur OCaml, le prompt est un #, toute expression doit être
terminée par ;; (double point-virgule). La sortie de l'interpréteur se fait par #quit.
On peut faire évaluer n'importe quelle expression respectant la syntaxe. La réponse
103
de l'interpréteur donne le nom de la variable si l'expression définit une variable (on a
un - sinon), son type, et sa valeur. Si l'on procède à une définition globale de
variable (let), la variable est utilisable dans les expressions écrites ultérieurement.
Des déclarations globales simultanées peuvent être faites grâce au and, mais les
symboles ne sont connus qu'à la fin de toutes les déclarations. Un regroupement de
déclarations globales provoque une évaluation séquentielle.
Exemple :
mamachine% ocaml
Objective Caml version 3.08.3
# 3 + 8 ;;
- : int = 11
# let x = 4 * 11 ;;
val x : int = 44
# x - 23 ;;
- : int = 21
# let y = 5 and w = 8 ;;
val y : int = 5
val w : int = 8
# let a = 12 and b = a + 1 ;;
Unbound value a
# let a = 12 let b = a + 1 ;;
val a : int = 12
val b : int = 13
(* décl. simultanées *)
(* regroupement *)
Attention à faire la différence entre les définitions globales et l'utilisation de
variables locales à une expression (let..in). Attention aussi à faire la différence
entre la définition d'une variable et l'évaluation d'une expression contenant
l'opérateur de comparaison =.
Exemple :
# let x = 4 * 11 ;;
val x : int = 44
# let x = 13 in x + 5 ;;
- : int = 18
# x ;;
- : int = 44
# x = 13 ;;
- : bool = false
(* remarquer le masquage *)
104
# let x = 8 and y = 23 in y - x + 1 ;;
- : int = 16
Enfin, une déclaration locale est elle-même une expression, elle peut donc être
utilisée pour construire d'autres expressions.
Exemple :
# (let a = 3 in a * a) + 10 ;;
- : int = 19
# let a = 3 in let b = a + 78 ;;
Syntax error
# let b = (let a = 3 in a + 78) ;;
val b : int = 81
# a ;;
Unbound value a
# b ;;
- : int = 81
# let a = 3 in (let c = a + 78 in c + 1) ;;
- : int = 82
# c ;;
Unbound value c
# let a = 3 in (let c = 78 in a + c) ;;
- : int = 81
# c ;;
Unbound value c
I.2 Quelques mots sur les types de base
Nous allons ici faire un rapide tour d'horizon des types de base et de leurs
opérateurs. On pourra par exemple trouver un petit récapitulatif à ce sujet à
http://pauillac.inria.fr/ocaml/htmlman/manual003.html.
Les nombres entiers et flottants, les caractères et chaînes de caractères, et les
booléens sont prédéfinis.
On distingue les opérateurs pour nombres entiers des opérateurs pour nombres
flottants : + - * et / pour les entiers, et +. -. *. et /. pour les nombres
flottants. Les fonctions de conversion float_of_int et int_of_float
105
peuvent permettre de passer d'un type à l'autre (nécessaire notamment pour les
comparaisons !).
Exemple :
# 4 + 7 ;;
- : int = 11
# 4.2 +. 3 ;;
This expression has type int but is here used
with type float
# 4.2 +. 3. ;;
- : float = 7.2
# 13 / 3 ;;
- : int = 4
# 13. /. 3. ;;
- : float = 4.33333333333333304
# 6 = 6. ;;
This expression has type float but is here used
with type int
# float_of_int 6 = 6.0 ;;
- : bool = true
# int_of_float 7.8 ;;
- : int = 7
# floor 7.8 ;;
- : float = 7.
Les types char et string correspondent respectivement aux caractères et chaînes
de caractères.
La notation .[ ] permet l'accès aux caractères d'une chaîne. Diverses fonctions de
conversion sont disponibles : int_of_char, char_of_int, int_of_string,
string_of_int, float_of_string et string_of_float. Enfin,
l'opérateur ^ permet de concaténer des chaînes de caractères.
Exemple :
#
#
#
int_of_char 'Z' ;;
: int = 90
"Bonjour" ;;
: string = "Bonjour"
let s = "Bonjour " ^ "Jim" ;;
106
val s : string = "Bonjour Jim"
# int_of_string "2005" ;;
- : int = 2005
# float_of_string "78.3" ;;
- : float = 78.3
# s.[2] ;;
- : char = 'n'
On dispose des opérateurs de comparaison = (égal), <> (différent), et < > <= >=,
et des connecteurs logiques && (et), || (ou) et not (négation).
Exemple :
# 7.2 > 3.56 && "Bonjour" <> "bonjour" ;;
- : bool = true
# let a = 'X' ;;
val a : char = 'X'
# a = 'Y' || a >= 'W' ;;
- : bool = true
I.3 Types composites
Les types composites n-uplets et listes peuvent être construits à partir d'éléments
des types de base présentés ci-dessus. Caml donne aussi la possibilité de manipuler
des tableaux (vecteurs), dont nous ne parlerons pas ici.
Les n-uplets sont formés d'éléments, qui peuvent être de types différents, séparés
par des virgules. Les fonctions fst et snd permettent d'accéder respectivement au
1er et 2ème élement d'un couple (2-uplet).
Exemple :
# let montuple = 4 , 8.8, 'V' ;;
val montuple : int * float * char = (4, 8.8, 'V')
# let montuple2 = 4 , 8.8, (9 , 6.7), 'V' ;;
val montuple2 : int * float * (int * float) * char =
(4, 8.8, (9, 6.7), 'V')
# let couple = "elt1", 2 ;;
val couple : string * int = ("elt1", 2)
107
#
#
-
fst couple ;;
: string = "elt1"
snd couple ;;
: int = 2
Les listes sont des suites de valeurs de même type, de longueur non prédéfinie. Les
éléments sont placés entre [ ] et sont séparés par des points-virgules. L'ajout d'un
élément en tête de liste (le cons de Lisp) se fait par ::, la tête et la queue de liste
(car et cdr de Lisp) peuvent être extraites par List.hd et List.tl. La
concaténation de deux listes est réalisée par l'opérateur @.
Remarque : hd et tl sont des fonctions de la bibliothèque List. Ces fonctions,
comme toutes les fonctions de bibliothèques, peuvent être utilisées grâce à une
notation pointée comme indiqué ci-dessus, mais il est également possible d'ouvrir la
bibliothèque (open) dans l'environnement courant.
Exemple :
# let maliste = [ 48 ; 92 ; 3 ; 17 ] ;;
val maliste : int list = [48; 92; 3; 17]
# [] ;;
(* liste vide *)
- : 'a list = []
# 25 :: maliste ;;
- : int list = [25; 48; 92; 3; 17]
# List.hd maliste ;;
- : int = 48
# List.tl maliste ;;
- : int list = [92; 3; 17]
# maliste @ [ 8 ; 5 ] ;;
- : int list = [48; 92; 3; 17; 8; 5]
# open List ;;
# hd maliste ;;
- : int = 48
# tl maliste ;;
- : int list = [92; 3; 17]
108
II. FONCTIONS
Nous allons tout d'abord étudier la syntaxe des définitions de fonctions en Caml et
leur utilisation, puis nous verrons comment définir et utiliser des fonctions d'ordre
supérieur, et des fonctions récursives.
II.1 Définition de fonctions
II.1.1 Fonctions à un paramètre
La syntaxe d'une expression fonctionnelle à un paramètre est la suivante :
function paramètre -> expression
Comme précédemment, le système infère le type de cette expression, par exemple
int -> float est le type de fonction prenant un entier en paramètre et renvoyant
un réel.
L'application d'une fonction à un argument se fait en faisant suivre la fonction de
l'argument, avec ou sans parenthèses.
Exemple :
#
#
#
-
function x -> 3 * x ;;
: int -> int = <fun>
(function x -> 3 * x) 7 ;;
: int = 21
(function x -> 3 * x)(18) ;;
: int = 54
Bien entendu, manipuler ainsi une fonction anonyme n'est pas très pratique. On peut
nommer les fonctions, grâce à let, comme nous l'avons vu pour la définition de
variables dans la section I.1.
Exemple :
# let triple = function x -> 3 * x ;;
val triple : int -> int = <fun>
109
#
#
-
triple 25 ;;
: int = 75
triple(9) ;;
: int = 27
Une syntaxe alternative let nom paramètre = expression permet de
simplifier l'écriture des fonctions.
Exemple :
# let triple x = 3 * x ;;
val triple : int -> int = <fun>
# triple 5 ;;
- : int = 15
II.1.2 Fonctions à plusieurs paramètres
La première possibilité pour définir une expression fonctionnelle à plusieurs
paramètres consiste à utiliser la même syntaxe que dans le cas des expressions
fonctionnelles à un paramètre :
function p1 -> function p2 -> ... expression
Comme précédemment, l'application de la fonction se fait en faisant suivre la
fonction des arguments.
Exemple :
#
#
#
function x -> function y -> x * y ;;
: int -> int -> int = <fun>
(function x -> function y -> x * y) 7 13 ;;
: int = 91
function x -> function y -> function z ->
x * y + z ;;
- : int -> int -> int -> int = <fun>
# (function x -> function y -> function z -> x * y + z)
4 6 10 ;;
- : int = 34
Une syntaxe plus compacte fun p1 p2 ... -> expression est également
possible.
110
Exemple :
#
#
#
#
-
fun x y -> x * y ;;
: int -> int -> int = <fun>
(fun x y -> x * y) 5 9 ;;
: int = 45
fun x y z -> x * y + z ;;
: int -> int -> int -> int = <fun>
(fun x y z -> x * y + z) 2 8 1 ;;
: int = 17
Tout comme les fonctions à un paramètre, on peut nommer les fonctions à plusieurs
paramètres grâce à let. Les deux syntaxes vues ci-dessus sont encore valables.
Exemple :
# let produit = fun x y -> x * y ;;
val produit : int -> int -> int = <fun>
# produit 9 5 ;;
- : int = 45
# let poly x y z = x * y + z ;;
val poly : int -> int -> int -> int = <fun>
# poly 3 9 4 ;;
- : int = 31
Il est à noter que les fonctions ainsi définies se présentent sous forme curryfiée,
l'application peut se faire argument par argument (application partielle). Notamment,
dans l'exemple ci-dessous, produit 9 est une fonction de type int -> int, qui
est appliquée ultérieurement à 5.
Exemple :
# let mult9 = produit 9 ;;
val mult9 : int -> int = <fun>
# mult9 5 ;;
- : int = 45
# let fun1 = poly 10 4 ;;
val fun1 : int -> int = <fun>
# fun1 7 ;;
111
- : int = 47
# let fun2 = fun x y -> poly x y 1 ;;
val fun2 : int -> int -> int = <fun>
# fun2 6 3 ;;
- : int = 19
Attention, il est aussi possible de donner les paramètres entre parenthèses, séparés
par des virgules, mais le type de la fonction n'est alors plus le même. On définit ainsi
une version non curryfiée, il n'y a en fait qu'un seul paramètre, qui est un n-uplet. Il
n'est bien sûr plus possible de procéder à une application argument par argument.
Exemple :
# let poly(x, y, z) = x * y + z ;;
val poly : int * int * int -> int = <fun>
# let fun1 = poly 10 4 ;;
This function is applied to too many arguments, maybe
you forgot a `;'
# poly 3 9 4 ;;
This function is applied to too many arguments, maybe
you forgot a `;'
# poly(3, 9, 4) ;;
- : int = 31
II.1.3 Portée des variables
Les expressions fonctionnelles peuvent faire intervenir des variables locales
(construction let ..in).
Exemple :
# let polynome x =
let a = 8 and b = 3 and c = 1
in a * x * x + b * x + c ;;
val polynome : int -> int = <fun>
# polynome 2 ;;
- : int = 39
# a ;;
Unbound value a
112
Pour que l'évaluation d'une expression soit possible, il faut que toutes les variables
qui y apparaissent soient définies. Les variables apparaissant dans une expression
fonctionnelle peuvent être, hormis les paramètres et les variables locales, des
variables de l'environnement.
En ce qui concerne ces variables, Caml utilise une liaison statique : l'environnement
utilisé pour exécuter l'application d'une fonction est celui de sa déclaration, et pas
l'environnement courant au moment de l'application (i.e. pas de portée dynamique).
Exemple :
# let produit x = a * x ;;
Unbound value a
# let a = 12 ;;
val a : int = 12
# let produit x = a * x ;;
val produit : int -> int = <fun>
# produit 4 ;;
- : int = 48
# let a = 3 ;;
val a : int = 3
# a ;;
- : int = 3
# produit 5 ;;
- : int = 60
II.2 Fonctions d'ordre supérieur
Les paramètres et résultats des fonctions Caml peuvent eux-mêmes être des
fonctions. Les fonctions prenant en paramètre ou renvoyant en résultat des
fonctions sont appelées des fonctions d'ordre supérieur.
Par exemple, la fonction List.map est une fonction d'ordre supérieur très utilisée,
elle permet d'appliquer une fonction à tous les éléments d'une liste.
Exemple :
# let succ x = x + 1 ;;
val succ : int -> int = <fun>
113
# List.map succ [ 48 ; 92 ; 3 ; 17 ] ;;
- : int list = [49; 93; 4; 18]
# let applique_et_add f x = f x + x ;;
val applique_et_add : (int -> int) -> int -> int = <fun>
# applique_et_add succ 15 ;;
- : int = 31
Dans l'exemple de la fonction applique_et_add, remarquons l'inférence de type
provoquée par la présence de l'opérateur +.
II.3 Fonctions polymorphes
Jusqu'à présent, nous n'avons rencontré que des fonctions ne s'appliquant qu'à un
seul type d'objets (monomorphes). Caml donne la possibilité de définir des fonctions
polymorphes : certains des paramètres et/ou la valeur de retour sont d'un type
quelconque. Des variables de type 'a, 'b, ... sont alors utilisées, elles représentent
n'importe quel type, et seront instanciées par le type du paramètre effectif lors de
l'application de la fonction.
Exemple : reprenons l'exemple ci-dessus, sans la présence du +
# let mon_applique f x = f x ;;
val mon_applique : ('a -> 'b) -> 'a -> 'b = <fun>
# mon_applique succ 147 ;;
- : int = 148
ici la fonction succ, de type int -> int, est utilisée, 'a et 'b sont donc
instanciés par int, et le résultat est de type 'b = int.
# mon_applique succ 56.2 ;;
This expression has type float but is here used with
type int
ici l'inférence de type échoue, 'a ne peut pas simultanément être de type int et
float.
Les exemples ci-dessous illustrent également le travail réalisé par le mécanisme
d'inférence de types.
114
Exemple :
# let compose f g x = f (g x) ;;
val compose : ('a -> 'b) -> ('c -> 'a) -> 'c -> 'b
= <fun>
# compose succ List.hd [ 45; 7 ; 88 ; 13 ] ;;
- : int = 46
# List.hd ;;
- : 'a list -> 'a = <fun>
# let app2 f x y = f x y ;;
val app2 : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
# let somme l x = (List.hd l) + x ;;
val somme : int list -> int -> int = <fun>
# app2 somme [ 45; 7 ; 88 ; 13 ] 1 ;;
- : int = 46
# let couple x y = x , y ;;
val couple : 'a -> 'b -> 'a * 'b = <fun>
# couple 5.34 "flottant" ;;
- : float * string = (5.34, "flottant")
# app2 couple (List.hd [ 45; 7 ; 88 ; 13 ]) 90.4 ;;
- : int * float = (45, 90.4)
Il est toutefois possible de contraindre une expression à avoir un type donné, la
syntaxe utilisée est (expression : type). Ceci permet notamment de rendre
visible le type des paramètres d'une fonction (la contrainte de type n'est pas
indispensable, mais rend le code plus explicite), ou de limiter le contexte de
l'inférence de type.
Exemple :
# let add (x: int) (y: int) = x + y ;;
val add : int -> int -> int = <fun>
# let couple (x: float) y = x , y ;;
val couple : float -> 'a -> float * 'a = <fun>
# couple 5.34 "flottant" ;;
- : float * string = (5.34, "flottant")
# couple (List.hd [ 45; 7 ; 88 ; 13 ]) 90.4 ;;
This expression has type int but is here used with
type float
# let monhd l = List.hd l ;;
val monhd : 'a list -> 'a = <fun>
115
# monhd [ 8.3 ; 4. ; 29.1 ] ;;
- : float = 8.3
# let monhd_int (l : int list) = List.hd l ;;
val monhd_int : int list -> int = <fun>
# monhd_int [ 8.3 ; 4. ; 29.1 ] ;;
This expression has type float but is here used with
type int
# monhd_int [ 87 ; 19 ; 22 ] ;;
- : int = 87
II.4 Fonctions récursives
La construction let rec permet des définitions récursives (attention, let ne le
permet pas), la syntaxe est :
let rec nomfct p1 p2 ... = expression ;;
Une fonction récursive peut être écrite de différentes façons (du moment que son
arrêt est garanti !). La section suivante traitera du filtrage, et son utilisation dans le
contexte de définitions récursives sera étudiée. Les quelques exemples donnés cidessous ne font appel qu'à la méthode classique de définition de fonctions récursives
dans un langage fonctionnel, ils font usage de la structure de contrôle conditionnelle
if expr1 then expr2 else expr3.
La construction and permet de définir des fonctions mutuellement récursives.
La directive #trace permet de suivre les appels récursifs lors de l'application
d'une fonction récursive (annulation par #untrace).
Exemples :
# let rec factorielle x =
if x = 0 then 1 else x * factorielle(x-1) ;;
val factorielle : int -> int = <fun>
# factorielle 10 ;;
- : int = 3628800
# #trace factorielle ;;
factorielle is now traced.
# factorielle 5 ;;
factorielle <-- 5
116
factorielle <-- 4
factorielle <-- 3
factorielle <-- 2
factorielle <-- 1
factorielle <-- 0
factorielle --> 1
factorielle --> 1
factorielle --> 2
factorielle --> 6
factorielle --> 24
factorielle --> 120
- : int = 120
# #untrace factorielle ;;
factorielle is no longer traced.
# let fact n =
let rec factorielle x =
if x = 0 then 1 else x * factorielle(x-1) in
if n >= 0 then factorielle n else 0 ;;
val fact : int -> int = <fun>
# fact 8 ;;
- : int = 40320
# fact (-4) ;;
- : int = 0
# let rec longueur l =
if l = [] then 0 else 1 + longueur (List.tl l) ;;
val longueur : 'a list -> int = <fun>
# #trace longueur ;;
longueur is now traced.
# longueur [ 45; 7 ; 88 ; 13 ] ;;
longueur <-- [<poly>; <poly>; <poly>; <poly>]
longueur <-- [<poly>; <poly>; <poly>]
longueur <-- [<poly>; <poly>]
longueur <-- [<poly>]
longueur <-- []
longueur --> 0
longueur --> 1
longueur --> 2
longueur --> 3
longueur --> 4
- : int = 4
# let rec pair x =
if x = 0 then true else impair (x - 1)
and impair x =
if x = 0 then false else pair (x - 1) ;;
val pair : int -> bool = <fun>
val impair : int -> bool = <fun>
# pair 853 ;;
117
- : bool = false
# impair 51 ;;
- : bool = true
# #trace pair ;;
pair is now traced.
# #trace impair ;;
impair is now traced.
# pair 5 ;;
pair <-- 5
impair <-- 4
pair <-- 3
impair <-- 2
pair <-- 1
impair <-- 0
impair --> false
pair --> false
impair --> false
pair --> false
impair --> false
pair --> false
- : bool = false
118
III. FILTRAGE DE MOTIF
Une caractéristique du langage Caml est de permettre le filtrage de motif (aussi
appelé pattern matching). Nous allons étudier comment définir des fonctions en
utilisant le filtrage sur les paramètres, en particulier dans le cas des fonctions
récursives.
Au préalable, nous discutons rapidement la déclaration de types, notamment les
types somme que nous recontrerons fréquemment dans les définitions avec filtrage.
III.1 Déclaration de types utilisateur
La déclaration d'un type se fait de la façon suivante :
type nom = definition_type ;;
Les déclarations de type sont par défaut récursives (pas de distinction nécessaire,
comme dans le cas de let et let rec). Des types mutuellement récursifs peuvent
être déclarés par :
type nom1 = definition_type1
and nom2 = definition_type2
and ... ;;
On distingue les types produit et types somme. Les types peuvent être paramétrés
(paramètres de type 'a, 'b, ...).
III.1.1 Enregistrements (types produit)
La notion d'enregistrement est similaire à celle de "struct" en C. Un enregistrement
contient des champs, chaque champ a un nom et un type. La déclaration d'un tel
type se fait par :
type nom = { champ1 : type1; champ2 : type2 ...} ;;
Une valeur de ce type est créée en affectant une valeur à chaque champ, les
affectations étant faites dans un ordre quelconque. Classiquement, une notation
pointée permet l'accès aux champs.
119
Exemples :
# type point = { abscisse: float ; ordonnee: float } ;;
type point = { abscisse : float; ordonnee : float; }
# let point1 = { abscisse = 8.4 ; ordonnee = 23.9 } ;;
val point1 : point = {abscisse = 8.4; ordonnee = 23.9}
# let point2 = { ordonnee = 11. ; abscisse = 56.2 } ;;
val point2 : point = {abscisse = 56.2; ordonnee = 11.}
# point1 ;;
- : point = {abscisse = 8.4; ordonnee = 23.9}
# point2 ;;
- : point = {abscisse = 56.2; ordonnee = 11.}
# point1.ordonnee ;;
- : float = 23.9
# type ('a , 'b) enreg = { cle: 'a ; valeur: 'b } ;;
type ('a, 'b) enreg = { cle : 'a; valeur : 'b; }
# let x = { cle = "passwd" ; valeur = 1234 } ;;
val x : (string, int) enreg =
{cle = "passwd"; valeur = 1234}
Notons qu'il est également possible de définir des types produits cartésiens avec
constructeur (utilisation du mot-clé of). Dans ce cas, les différentes composantes ne
sont pas nommées, on peut définir des fonctions pour y accéder.
Exemple :
# type cpoint = ConstrPoint of float * float ;;
type cpoint = ConstrPoint of float * float
# let point1 = ConstrPoint(5.3, 2.0) ;;
val point1 : cpoint = ConstrPoint (5.3, 2.0)
# let abscisse (ConstrPoint(x, y)) = x ;;
val abscisse : cpoint -> float = <fun>
# let ordonnee (ConstrPoint(x, y)) = y ;;
val ordonnee : cpoint -> float = <fun>
# abscisse point1 ;;
- : float = 5.3
# ordonnee point1 ;;
- : float = 2.0
120
III.1.2 Types somme
Les types somme ("ou") caractérisent des données comprenant des alternatives. Les
différents membres de la somme, séparés par des | , sont discriminés par des
constructeurs, qui peuvent avoir des arguments (utilisation du mot-clé of suivi du
type). On prendra l'habitude de faire commencer les identificateurs de constructeurs
par une majuscule, pour des raisons de compatibilité avec Objective Caml.
La déclaration d'un tel type se fait de la façon suivante :
type nom = | Const1 (* constructeur constant *)
| Const2 of arg (* constructeur à un argument *)
| Const3 of arg1 * arg2 * ...
(* const. à plusieurs arguments *)
| ...
;;
La création d'une valeur d'un type somme se fait par application d'un des
constructeurs à une valeur du type approprié.
Exemples :
# type couleur = | Vert | Bleu | Violet ;;
type couleur = Vert | Bleu | Violet
# Bleu ;;
- : couleur = Bleu
# let c = Violet ;;
val c : couleur = Violet
# type valeur =
| Entier of int
| Reel of float
| LePoint of point
| Couple of int * int ;;
type valeur =
Entier of int
| Reel of float
| LePoint of point
| Couple of int * int
# Entier 42 ;;
- : valeur = Entier 42
# Reel 83.6 ;;
- : valeur = Reel 83.6
# LePoint {abscisse = 56.2; ordonnee = 11.0} ;;
121
- : valeur = LePoint {abscisse = 56.2; ordonnee = 11.0}
# let c = Couple (32 , 4) ;;
val c : valeur = Couple (32, 4)
Nous avons dit que les déclarations de type sont par défaut récursives. Cela permet
en particulier de définir des types somme dont les arguments de certains
constructeurs peuvent être de ce même type.
Exemple :
# type mon_type_entier =
| Zero
| Plus1 of mon_type_entier ;;
type mon_type_entier = Zero | Plus1 of mon_type_entier
# let x = Plus1 (Plus1 (Plus1 Zero)) ;;
val x : mon_type_entier = Plus1 (Plus1 (Plus1 Zero))
cette définition est tout simplement fidèle à la caractérisation des entiers naturels
dans l'arithmétique de Peano (élément de base 0 et successeur).
III.2 Filtrage
Nous avons vu dans la section II diverses variantes de définitions de fonctions, et
nous avons étudié la définition de fonctions récursives telle qu'elle est réalisée assez
classiquement dans tout langage.
Nous allons ici présenter la définition de fonction faisant usage du filtrage de motif
(qui peut s'apparenter à une définition par cas), et revenir sur la définition de
fonctions récursives dans ce contexte.
III.2.1 Principes et syntaxe
Le filtrage de motif sert à reconnaître la forme syntaxique d'une expression. Sa
syntaxe est la suivante :
match expression with
| motif1 -> expression1
| motif2 -> expression2 ...
122
Les différents motifs sont examinés séquentiellement, l'expression correspondant au
premier motif reconnu est évaluée. Les motifs doivent être du même type, et les
différentes expressions doivent également être du même type.
Le symbole _ est appelé motif universel, il filtre toutes les valeurs possibles, on peut
notamment l'utiliser comme dernière alternative.
Plusieurs motifs peuvent être combinés par un |, mais chaque motif doit alors être
une constante. Un intervalle de caractères peut être représenté par 'c1'..'c2'.
Le filtrage de motif pourra être utilisé dans les définitions de fonctions, l'expression
située entre match et with servant dans ce cas à filtrer la valeur d'un ou plusieurs
paramètres. Attention, l'ensemble des cas possibles pour les valeurs filtrées doit être
considéré, éventuellement en ayant recours au motif universel.
Exemples :
# let couleur2chaine c = match c with
| Vert -> "la couleur est Vert"
| Bleu -> "la couleur est Bleu"
| Violet -> "la couleur est Violet" ;;
val couleur2chaine : couleur -> string = <fun>
# let coul1 = Bleu ;;
val coul1 : couleur = Bleu
# couleur2chaine coul1 ;;
- : string = "la couleur est Bleu"
# let etlogique a b = match (a, b) with
| (false, false) -> false
| (false, true) -> false
| (true, false) -> false
| (true, true) -> true ;;
val etlogique : bool -> bool -> bool = <fun>
# etlogique false true ;;
- : bool = false
# let etlogiqueV2 a b = match (a, b) with
| (false, _) -> false
| (true, x) -> x ;;
val etlogiqueV2 : bool -> bool -> bool = <fun>
# etlogiqueV2 true false ;;
- : bool = false
# let etlogiqueV3 a b = match (a, b) with
123
| (true, true) -> true
|
_
-> false ;;
val etlogiqueV3 : bool -> bool -> bool = <fun>
# etlogiqueV3 false false ;;
- : bool = false
# let decode x = match x with
| '0'..'9' -> "chiffre"
| 'a' | 'A' -> "lettre a minuscule ou majuscule"
| _ -> "lettre autre que a" ;;
val decode : char -> string = <fun>
# decode '7' ;;
- : string = "chiffre"
# decode 'X' ;;
- : string = "lettre autre que a"
# decode 'A' ;;
- : string = "lettre a minuscule ou majuscule"
L'exemple de la fonction couleur2chaine ci-dessus nous montre un cas de
filtrage sur un type somme. Ce genre de filtrage est fréquent, la règle générale
suivant laquelle l'ensemble des cas possibles doit être prévu s'applique bien entendu.
Exemple :
# type valeur =
| Entier of int
| Reel of float
| LePoint of point
| Couple of int * int ;;
type valeur =
Entier of int
| Reel of float
| LePoint of point
| Couple of int * int
# let estnul v = match v with
| Entier 0 | Reel 0.
| LePoint { abscisse=0. ; ordonnee=0. }
| Couple (0,0) -> true
| _ -> false ;;
val estnul : valeur -> bool = <fun>
# estnul (Couple (0, 5)) ;;
- : bool = false
# estnul (LePoint { abscisse=0. ; ordonnee=0.1 }) ;;
- : bool = false
124
# estnul (Entier 0) ;;
- : bool = true
Lors du filtrage, le mot-clé as permet, si nécessaire, de nommer le motif ou une
partie du motif.
Notons également qu'il est possible de réaliser du filtrage avec gardes, dans lequel
une expression conditionnelle peut être évaluée juste après le filtrage d'un motif, et
conditionne l'évaluation de l'expression associée à ce motif. On utilise alors la
syntaxe :
match expression with
| motif1 when condition1 -> expression1
| motif2 when condition2 -> expression2 ...
Exemples :
# type monome = { coeff : float ; degre : int } ;;
type monome = { coeff : float; degre : int; }
# let m1 = { coeff = 3.9 ; degre = 4 } ;;
val m1 : monome = {coeff = 3.9; degre = 4}
# let m2 = { coeff = -. 17.5 ; degre = 2 } ;;
val m2 : monome = {coeff = -17.5; degre = 2}
# let m3 = { coeff = 0. ; degre = 3 } ;;
val m3 : monome = {coeff = 0.; degre = 3}
# let tostring m =
string_of_float m.coeff ^ " * x^"
^ string_of_int m.degre ;;
val tostring : monome -> string = <fun>
# let estnul m = match m with
| { coeff = 0. ; degre = d } -> "Monome nul"
| { coeff = c ; degre = d } as x ->
(tostring x) ^ " n'est pas nul" ;;
val estnul : monome -> string = <fun>
# estnul m1 ;;
- : string = "3.9 * x^4 n'est pas nul"
# estnul m3 ;;
- : string = "Monome nul"
# let valabs m = match m with
| { coeff = c ; degre = d } when c < 0.
-> { coeff = -. c ; degre = d }
| { coeff = c ; degre = d } as x -> x ;;
val valabs : monome -> monome = <fun>
125
#
#
-
valabs m1 ;;
: monome = {coeff = 3.9; degre = 4}
valabs m2 ;;
: monome = {coeff = 17.5; degre = 2}
Enfin remarquons que, pour le filtrage de paramètres, une syntaxe allégée est
souvent préférée à la syntaxe que nous avons utilisée jusqu'ici :
function
| motif1 -> expression1
| motif2 -> expression2 ...
Toutefois l'utilisation de match sera parfois plus naturelle, par exemple dans des cas
de filtrage sur le premier paramètre seulement.
Exemples :
# let couleur2chaine = function
| Vert -> "la couleur est Vert"
| Bleu -> "la couleur est Bleu"
| Violet -> "la couleur est Violet" ;;
val couleur2chaine : couleur -> string = <fun>
# let coul1 = Vert ;;
val coul1 : couleur = Vert
# couleur2chaine coul1 ;;
- : string = "la couleur est Vert"
# let etlogiqueV2 = function
| (false, _) -> false
| (true, x) -> x ;;
val etlogiqueV2 : bool * bool -> bool = <fun>
# etlogiqueV2 (false, true) ;;
- : bool = false
# let decode = function
| '0'..'9' -> "chiffre"
| 'a' | 'A' -> "lettre a minuscule ou majuscule"
| _ -> "lettre autre que a" ;;
val decode : char -> string = <fun>
# decode 'J' ;;
- : string = "lettre autre que a"
# let valabsV2 = function
(* definition alternative *)
{ coeff = c ; degre = d } as x ->
if c < 0. then { coeff = -. c ; degre = d }
else x ;;
126
val valabsV2 : monome -> monome = <fun>
# valabsV2 m2 ;;
- : monome = {coeff = 17.5; degre = 2}
# type point = ConstrPoint of float * float ;;
type point = ConstrPoint of float * float
# let abscisse = function
ConstrPoint(x, y) -> x ;;
val abscisse : point -> float = <fun>
# let ordonnee = function
ConstrPoint(x, y) -> y ;;
val ordonnee : point -> float = <fun>
# let p = ConstrPoint(5.12, 23.9) ;;
val p : point = ConstrPoint (5.12, 23.9)
# abscisse p ;;
- : float = 5.12
# let simplifmult x y = match x with
| 0 -> 0
| 1 -> y
| _ -> x * y ;;
val simplifmult : int -> int -> int = <fun>
# simplifmult 1 6 ;;
- : int = 6
# simplifmult 8 4 ;;
- : int = 32
# let simplifmult = function
| 0 -> (function y -> 0)
| 1 -> (function y -> y)
| _ as x -> (function y -> x * y) ;;
val simplifmult : int -> int -> int = <fun>
# simplifmult 0 9 ;;
- : int = 0
# simplifmult 90 2 ;;
- : int = 180
III.2.2 Filtrage et fonctions récursives
Le filtrage peut également être utilisé dans un contexte de fonctions récursives, avec
la construction let rec vue à la section II.4. Le principe est le même pour la
construction par cas, mais la forme est différente.
Par exemple, dans le cas de fonctions sur entiers naturels, on reconnaîtra souvent le
motif 0, puis les autres.
127
Exemples :
# let rec factorielle = function
| 0 -> 1
| _ as x -> x * factorielle(x-1) ;;
val factorielle : int -> int = <fun>
# factorielle 7 ;;
- : int = 5040
#
" " l
" e
" t
" " r
" e
" c
" " p
" a
" i
" r
" " =
" " f
" u
" n
" c
" t
" i
" o
" n
" "
" " " " " " |
" " 0
" " " >
" " t
" r
" u
" e
" "
" " " " " " |
" " _
" " a
" s
" " x
" " " >
" " i
" m
" p
" a
" i
" r
" (
" x
" " 1
" )
" "
" " a
" n
" d
" " i
" m
" p
" a
" i
" r
" " =
" " f
" u
" n
" c
" t
" i
" o
" n
" "
" " " " " " |
" " 0
" " " >
" " f
" a
" l
" s
" e
" "
" " " " " " |
" " x
" " " >
" " p
" a
" i
" r
" (
" x
" " 1
" )
" " ;
" ;
" "
v
" al p"ai
" r
" " :
" " i
" n
" t
" " " >
" " b
" o
" o
" l
" " =
" " <
" f
" u
" n
" >
" "
v
" al i"mp
" a
" i
" r
" " :
" " i
" n
" t
" " " >
" " b
" o
" o
" l
" " =
" " <
" f
" u
" n
" >
" "
#
" " p
" a
" i
" r
" " 5
" " ;
" ;
" "
" " :
" " b
" o
" o
" l
" " =
" " f
" a
" l
" s
" e
" "
#
" " i
" m
" p
" a
" i
" r
" " 7
" 3
" " ;
" ;
" "
" " :
" " b
" o
" o
" l
" " =
" " t
" r
" u
" e
" "
#
" " l
" e
" t
" " r
" e
" c
" " p
" a
" i
" r
" " =
" " f
" u
" n
" c
" t
" i
" o
" n
" "
(* def. alternative *)
" " " " |
" " 0
" " " >
" " t
" r
" u
" e
" " "
" " " " |
" " 1
" " " >
" " f
" a
" l
" s
" e
" "
" " " " |
" " _
" " a
" s
" " x
" " " >
" " p
" a
" i
" r
" (
" x
" " 2
" )
" " ;
" ;
" "
v
" al p"ai
" r
" " :
" " i
" n
" t
" " " >
" " b
" o
" o
" l
" " =
" " <
" f
" u
" n
" >
" "
#
" " p
" a
" i
" r
" " 1
" 2
" " ;
" ;
" "
" " :
" " b
" o
" o
" l
" " =
" " t
" r
" u
" e
" "
#
" " p
" a
" i
" r
" " 4
" 9
" " ;
" ;
" "
" " :
" " b
" o
" o
" l
" " =
" " f
" a
" l
" s
" e
" "
#
" let rec fibonacci = function
| 0 -> 1
| 1 -> 1
| n -> fibonacci(n-1) + fibonacci(n-2) ;;
val fibonacci : int -> int = <fun>
# fibonacci 10 ;;
- : int = 89
Enfin, cette forme de fonctions récursives avec filtrage est très répandue dans le cas
de fonctions sur listes. La plupart des définitions consistent à filtrer le motif
correspondant à la liste vide [], puis le motif associé à une liste de tête x et de
queue l, c'est à dire x::l.
128
Exemples :
# let rec longueur = function
| [] -> 0
| x::l -> 1 + longueur(l) ;;
val longueur : 'a list -> int = <fun>
# longueur [ 45; 7 ; 88 ; 13 ] ;;
- : int = 4
# let rec fois2 = function
| [] -> []
| tete::queue -> (2 * tete)::fois2(queue) ;;
val fois2 : int list -> int list = <fun>
# fois2 [ 45; 7 ; 88 ; 13 ] ;;
- : int list = [90; 14; 176; 26]
Remarque : la possibilité de définition de types somme permet de définir par des
constructeurs des structures récursives plus élaborées que les listes (arbres,
graphes,...). Les fonctions sur ces structures mettent en oeuvre un filtrage qui suit un
raisonnement similaire au filtrage ci-dessus.
129
QUELQUES COMPLEMENTS
Dans cette section annexe, nous présentons rapidement quelques compléments de
nature un peu plus technique : quelques mots sur les structures modifiables et effets
de bord, entrées/sorties conversationnelles, chargement de fichiers et compilation.
Structures modifiables
Parmi les structures de données que nous avons présentées, certaines sont ou
peuvent être modifiables. Disons quelques mots à ce sujet, dans le cas des chaînes de
caractères et des enregistrements.
Les chaînes de caractères (section I.2) sont des structures physiquement modifiables
(String.set ou <-), on rappelle que la notation .[ ] permet l'accès aux
caractères de la chaîne.
Exemple :
# let s = "Bonjour " ^ "Jim" ;;
val s : string = "Bonjour Jim"
# s.[0] <- 'b' ;;
- : unit = ()
# String.set s 8 'j' ;;
- : unit = ()
# s ;;
- : string = "bonjour jim"
Par défaut, les champs des enregistrements (section III.1.1) ne sont pas modifiables
("mutable"). Le mot-clé mutable permet de les rendre modifiables.
Exemple :
# type point
type point =
# let point1
val point1 :
= { abscisse: float ; ordonnee: float } ;;
{ abscisse : float; ordonnee : float; }
= { abscisse = 8.4 ; ordonnee = 23.9 } ;;
point = {abscisse = 8.4; ordonnee = 23.9}
130
# point1.abscisse <- 0. ;;
The record field label abscisse is not mutable
# type point = { mutable abscisse: float ;
ordonnee: float } ;;
type point = { mutable abscisse : float;
ordonnee : float; }
# let point2 = { abscisse = 4.62 ; ordonnee = 10.3 } ;;
val point2 : point = {abscisse = 4.62; ordonnee = 10.3}
# point2.abscisse <- 1. ;;
- : unit = ()
# point2.ordonnee <- 22.8 ;;
The record field label ordonnee is not mutable
# point2 ;;
- : point = {abscisse = 1.; ordonnee = 10.3}
Entrées/sorties
Il est possible de réaliser des affichages sur la sortie standard (std_out) au moyen
des fonctions print_string, print_int, print_float,... La fonction
print_newline affiche un retour à la ligne. La saisie d'une chaîne de caractères
sur l'entrée standard (std_in) se fait grâce à la fonction read_line.
Exemple :
# let masaisie_chaine () =
print_string "
prompt> ";
read_line();;
val masaisie_chaine : unit -> string = <fun>
# let chainelue = masaisie_chaine() ;;
prompt> salut
val chainelue : string = "salut"
# chainelue ;;
- : string = "salut"
# let masaisie_float () =
print_string "
prompt> ";
float_of_string (read_line()) ;;
val masaisie_float : unit -> float = <fun>
# let floatlu = masaisie_float() ;;
prompt> 6.31
val floatlu : float = 6.31
131
Chargement, compilation
Plutôt que de saisir vos fonctions interactivement sous l'interpréteur, votre
programme peut être placé dans un fichier, qu'il vous suffit de charger sous
l'interpréteur grâce à la directive #use.
Exemple :
mamachine% cat facto.ml
let fact n =
let rec factorielle x =
if x = 0 then 1 else x * factorielle(x-1) in
if n >= 0 then factorielle n else 0 ;;
mamchine% ocaml
Objective Caml version 3.08.3
# #use "facto.ml" ;;
val fact : int -> int = <fun>
# fact 7 ;;
- : int = 5040
#
Au lieu d'exécuter vos programmes sous l'interpréteur Caml, vous pouvez les
compiler puis les exécuter sous l'interpréteur de commandes (le shell). Une
compilation avec ocamlc produit un exécutable pour la machine virtuelle OCaml
(code portable), tandis qu'une compilation avec ocamlopt produit du code natif
optimisé.
Attention, ici vous n'êtes plus dans la boucle d'évaluation/affichage de l'interpréteur
Caml. Il faut donc prévoir les entrées/sorties conversationnelles nécessaires au bon
fonctionnement de votre programme, en particulier ne pas oublier de faire
explicitement afficher les résultats.
Exemple :
mamachine% cat facto.ml
let fact n =
let rec factorielle x =
if x = 0 then 1 else x * factorielle(x-1) in
if n >= 0 then factorielle n else 0 ;;
132
print_string "on calcule fact(9) :" ;; print_newline() ;;
print_string "resultat = " ;;
print_int (fact 9) ;; print_newline() ;;
mamchine% ocamlc -o facto facto.ml
mamchine% ocamlrun facto
on calcule fact(9) :
resultat = 362880
mamchine% ocamlopt -o factoopt facto.ml
mamchine% ./factoopt
on calcule fact(9) :
resultat = 362880
Programmation modulaire, compilation séparée
Tout comme en C par exemple, votre programme peut être modulaire, c'est à dire
que les différentes fonctions peuvent être placées dans des unités de compilation
différentes. Une unité de compilation est composée de deux fichiers, le fichier
d'implantation (.ml) et le fichier, optionnel, d'interface (.mli). La compilation d'un
fichier monfic.ml produit un module Monfic, ses fonctions peuvent être utilisées
dans un autre fichier par notation pointée ou en utilisant open.
La compilation avec l'option -c produit un fichier de code objet .cmo et un fichier
d'interface compilée .cmi. L'exécutable final sera produit par l'édition de liens des
fichiers .cmo. Cela peut vous conduire tout naturellement à élaborer des
Makefile pour vos applications Caml...
Exemple :
mamachine% ls -l
total 4224
-rw-r--r-- 1 moi staff
150 Sep 27 09:20 cnp.ml
-rw-r--r-- 1 moi staff
129 Sep 27 09:15 facto.ml
mamachine% cat facto.ml
let fact n =
let rec factorielle x =
if x = 0 then 1 else x * factorielle(x-1) in
if n >= 0 then factorielle n else 0 ;;
133
mamachine% cat cnp.ml
open Facto ;;
let comb n p =
((fact n) * (fact p)) / (fact (n - p)) ;;
print_string "C 6 2 = " ;;
print_int (comb 6 2) ;;
print_newline() ;;
mamachine% ocamlc -c facto.ml
mamachine% ocamlc -c cnp.ml
mamachine% ls -l
total 3256
-rw-r--r-- 1 moi staff
220 Sep 27 09:28
-rw-r--r-- 1 moi staff
444 Sep 27 09:28
-rw-r--r-- 1 moi staff
150 Sep 27 09:20
-rw-r--r-- 1 moi staff
173 Sep 27 09:28
-rw-r--r-- 1 moi staff
291 Sep 27 09:28
-rw-r--r-- 1 moi staff
129 Sep 27 09:15
mamachine% ocamlc -o moncnp facto.cmo cnp.cmo
mamachine% ocamlrun moncnp
C 6 2 = 60
cnp.cmi
cnp.cmo
cnp.ml
facto.cmi
facto.cmo
facto.ml
Les interfaces de modules peuvent être utilisées pour déclarer les identificateurs du
module que l'on veut rendre visibles à l'extérieur. Les identificateurs définis dans le
module mais non déclarés dans l'interface ne seront pas visibles. Ce sont des fichiers
.mli dans lesquels les déclarations d'identificateurs sont introduites par le mot-clé
val et précisent le nom et le type de l'identificateur (on pourra aussi y placer des
commentaires sur sa nature et son utilisation !...). Ces fichiers sont compilés avant les
fichiers .ml, leur compilation donne naissance aux fichiers .cmi.
Exemple :
mamachine% ls -l
total 4224
-rw-r--r-- 1 moi staff
150 Sep 27 09:20 cnp.ml
-rw-r--r-- 1 moi staff
129 Sep 27 09:15 facto.ml
-rw-r--r-- 1 moi staff
51 Sep 27 09:49 facto.mli
mamachine% cat facto.mli
(* Fonction factorielle *)
val fact: int -> int ;;
mamachine% ocamlc -c facto.mli
134
mamachine%
total 2724
-rw-r--r--rw-r--r--rw-r--r--rw-r--r-mamachine%
mamachine%
total 3224
-rw-r--r--rw-r--r--rw-r--r--rw-r--r--rw-r--r-mamachine%
mamachine%
mamachine%
C 6 2 = 60
ls -l
1 moi staff
150 Sep 27 09:20
1 moi staff
173 Sep 27 10:01
1 moi staff
129 Sep 27 09:15
1 moi staff
51 Sep 27 09:49
ocamlc -c facto.ml
ls -l
cnp.ml
facto.cmi
facto.ml
facto.mli
1 moi staff
150 Sep
1 moi staff
173 Sep
1 moi staff
295 Sep
1 moi staff
129 Sep
1 moi staff
51 Sep
ocamlc -c cnp.ml
ocamlc -o moncnp facto.cmo
ocamlrun moncnp
cnp.ml
facto.cmi
facto.cmo
facto.ml
facto.mli
27
27
27
27
27
09:20
10:01
10:02
09:15
09:49
cnp.cmo
135
Parmi les sources de ce cours...
3 "Le langage Caml", P.Weis et X.Leroy, Dunod.
3 http://caml.inria.fr
3 http://www.bath.ac.uk/~cs1cb/ML
3 http://burks.brighton.ac.uk/burks/language/ml/
3 http:/www-calfor.lip6.fr/~vmm/Enseignement/Licence_info/
Programmation/Cours/
3 http://www.pps.jussieu.fr/Livres/ora/DA-OCAML/
136