72
CUST GMM 1° Année 2004/2005 Cours C++ 1 Chapitre 1 – Introduction 1- Objectifs du cours - compréhension des concepts objets - application de ces concepts avec le langage C++ - proposer une vue d'ensemble des outils et méthodes de développement objet Références bibliographiques : Bjarne Stroustrup. Le langage C++. CampusPress, 3 ème édition, 1999. Claude Delannoy. Exercices en langage C++. Eyrolles, 1997. Claude Delannoy. Programmer en langage C++. Eyrolles, 1998. Cours en ligne : http://casteyde.christian.free.fr/cpp/cours/online/book1.html http://www.prism.uvsq.fr/~hal/ 2- Vers la programmation orientée objet 2.1. Motivations pour la programmation orientée objet La qualité d'un logiciel est souvent associée à son coût, et environ 80% du coût global d'un logiciel est affecté à la maintenance. Dans les coûts de maintenance, la part la plus importante (42%) est tenue par l'adaptation du logiciel à de nouveaux besoins (extensibilité du logiciel). Les qualités requises pour les logiciels sont d'une part la validité et la robustesse, mais aussi l'extensibilité, la réutilisabilité et la compatibilité L'approche objet apporte extensibilité, réutilisabilité et compatibilité, qui impliquent validité et robustesse. La programmation orientée objet est une évolution, mais pas l'idéal absolu 2.2. Différentes approches pour la programmation a. La programmation structurée Choisissez les procédures. Utilisez les meilleurs algorithmes que vous trouverez. - L'accent est mis sur les traitements : on cherche les meilleurs algos - Approche très utilisée depuis de nombreuses années - Approche de haut en bas (top-down) : décomposition du programme en sous- programmes jusqu'à ne plus pouvoir décomposer (approche méthodique) - A beaucoup amélioré la qualité des logiciels mais données et traitements restent indépendants : pb car les données changent moins que les traitements (assurer la cohérence demande plus de maintenance) b. La programmation modulaire Choisissez vos modules. Découpez le programme de telle sorte que les données soient masquées par ces modules. - un module est un ensemble de procédures connexes avec les données qu'elles manipulent.

Chapitre 1 – Introductionfrenoux.emmanuelle.free.fr/drfrenoux/EnLigne/C++/CoursC...CUST GMM 1 Année 2004/2005 Cours C++ 5 Chapitre 2 – Fonctionnalités de base en C++ De par la

Embed Size (px)

Citation preview

CUST GMM 1° Année 2004/2005 Cours C++

1

Chapitre 1 – Introduction 1- Objectifs du cours

- compréhension des concepts objets - application de ces concepts avec le langage C++ - proposer une vue d'ensemble des outils et méthodes de développement objet

Références bibliographiques : Bjarne Stroustrup. Le langage C++. CampusPress, 3ème édition, 1999. Claude Delannoy. Exercices en langage C++. Eyrolles, 1997. Claude Delannoy. Programmer en langage C++. Eyrolles, 1998. Cours en ligne : http://casteyde.christian.free.fr/cpp/cours/online/book1.html http://www.prism.uvsq.fr/~hal/ 2- Vers la programmation orientée objet

2.1. Motivations pour la programmation orientée objet La qualité d'un logiciel est souvent associée à son coût, et environ 80% du coût global d'un logiciel est affecté à la maintenance. Dans les coûts de maintenance, la part la plus importante (42%) est tenue par l'adaptation du logiciel à de nouveaux besoins (extensibilité du logiciel). Les qualités requises pour les logiciels sont d'une part la validité et la robustesse, mais aussi l'extensibilité, la réutilisabilité et la compatibilité L'approche objet apporte extensibilité, réutilisabilité et compatibilité, qui impliquent validité et robustesse. ⇒ La programmation orientée objet est une évolution, mais pas l'idéal absolu

2.2. Différentes approches pour la programmation a. La programmation structurée

⇒ Choisissez les procédures. Utilisez les meilleurs algorithmes que vous trouverez. - L'accent est mis sur les traitements : on cherche les meilleurs algos - Approche très utilisée depuis de nombreuses années - Approche de haut en bas (top-down) : décomposition du programme en sous-

programmes jusqu'à ne plus pouvoir décomposer (approche méthodique) - A beaucoup amélioré la qualité des logiciels mais données et traitements restent

indépendants : pb car les données changent moins que les traitements (assurer la cohérence demande plus de maintenance)

b. La programmation modulaire

⇒ Choisissez vos modules. Découpez le programme de telle sorte que les données soient masquées par ces modules.

- un module est un ensemble de procédures connexes avec les données qu'elles manipulent.

CUST GMM 1° Année 2004/2005 Cours C++

2

- L'élément primordial passe de la conception des procédures à l'organisation des données.

- Principe de masquage de l'information : masquage des données, des types, des fonctions,…

- Permet la compilation séparée - Limitation : les types ainsi définis diffèrent dans leur utilisation (création,…) des types

de base.

c. Programmation par abstraction de données ⇒ Choisissez les types dont vous avez besoin. Fournissez un exemple complet d'opérations pour chaque type.

- Utilise les types abstraits de données - L'interface d'un type abstrait isole complètement l'utilisateur des détails

d'implémentation (par opposition à un type concret… type concret : l'implémentation est protégée mais visible)

- Limitation : il est nécessaire de modifier un type pour l'adapter.

d. Programmation orientée objet ⇒ Choisissez vos classes. Fournissez un exemple complet d'opérations pour chaque classe. Rendez toute similitude explicite à l'aide de l'héritage.

- définir des classes = définir des types utilisateurs - approche consistant à définir les classes puis à préciser les relations entre elles

(notamment l'héritage) Difficultés de l'approche objet :

- Moins intuitive que l'approche fonctionnelle (il n'est pas très naturel de décomposer sous forme d'objets et d'interactions entre les objets)

- Rien, dans les concepts de base de l'approche objet, ne dicte comment modéliser la structure objet d'un système de manière pertinente.

- L'application des concepts objet nécessite une grande rigueur - Programmer en C++ n'est pas "concevoir objet" : seule une analyse objet conduit à une

solution objet, le langage de programmation est un moyen d'implémentation, il ne garantit pas le respect des concepts objet.

Principaux langages objet :

- Simula, Ole-Johan Dahl et Kristen Nygaard (fin des années 1960) - SmallTalk, Alan Kay et Dan Ingalls (début des années 1970) - Eiffel, Bertrand Meyer (1985) - C++, Bjarne Stroustrup (années 1980) - Java, James Gosling (1995)

3- Le langage C++ Pour apprendre le langage C++, le point essentiel consiste à se concentrer sur les concepts et à éviter de se perdre dans des détails techniques. Il est important d’appréhender globalement la programmation et les techniques de conception. L’application irréfléchie à un langage donné de techniques efficaces dans un autre langage produit en règle générale du code maladroit, aux performances réduites et difficiles à maintenir.

CUST GMM 1° Année 2004/2005 Cours C++

3

3.1 Qu’est-ce que le C++ ? C++ est un langage de programmation polyvalent dont les caractéristiques sont les suivantes :

- C’est une version améliorée du langage C - Il supporte l’abstraction des données - Il supporte la programmation orientée objet - Il supporte la programmation générique

3.2 La conception du C++

a. Note historique

Le C++ a été inventé par Bjarne Stroustrup à partir du langage C (Kernighan, 1978), qui en forme un sous-ensemble, et du langage Simula67 (Dahl, début des années 70) auquel il emprunte le concept de classe. La fonctionnalité du C++ de surcharge des opérateurs et la liberté de placer une déclaration à chaque endroit où une instruction peut se produire s’apparentent au langage Algol68 (Woodward, 1974).

C++ par rapport aux autres langages

Depuis 1998, il existe un standard C++ international (ISO/IEC 98-14882). C++ est utilisé par des centaines de milliers de programmeurs dans tous les domaines d’application. Cette utilisation est supportée par une dizaine de mises en œuvre indépendantes, des centaines de bibliothèques, des centaines de livres, plusieurs publications techniques, de nombreuses conférences et d’innombrables consultants. C++ est beaucoup employé dans l’enseignement et la recherche car il est :

- suffisamment « propre » pour que l’enseignement des concepts de base de la programmation s’effectue dans de bonnes conditions

- suffisamment réaliste, efficace et souple pour satisfaire à l’élaboration de programmes exigeants

- suffisamment ouvert pour satisfaire les entreprises et les collaborations qui intègrent plusieurs environnements de développement et d’exécution

- suffisamment complet pour être un véhicule de l’enseignement des concepts et des techniques avancés

CUST GMM 1° Année 2004/2005 Cours C++

4

- suffisamment commercial pour que son utilisation survive largement à son enseignement dans le cadre académique

b. Objectifs et spécificités du C++

C++ a été développé à partir du langage de programmation C et, à quelques exceptions près, il retient le C en tant que sous-ensemble. Le langage de base, à savoir le sous-ensemble C du C++, a été conçu de manière à préserver une correspondance étroite entre ses types, ses opérateurs, ses instructions et les objets que les ordinateurs ont à traiter directement : nombres, caractères et adresses. L’un des objectifs à l’origine du langage C consistait à remplacer le codage de l’assemblage pour les tâches de programmation système les plus exigeantes. Lorsque le C++ a été conçu, on s’est efforcé de ne pas remettre en question les avantages acquis dans ce domaine. La différence entre C et C++ réside essentiellement dans le niveau de priorité attribué aux types et à la structure : le C est expressif et permissif, le C++ est encore plus expressif. Le C++ a été conçu pour permettre aux programmes importants (par exemple 100000 lignes de code) d’entrer dans le cadre d’une structure rationnelle visant à ce qu’une seule personne puisse raisonnablement gérer de gros volumes de code. L’objectif était également d’arriver à ce qu’une ligne de code moyenne du C++ exprime beaucoup plus que son équivalent en C ou en Pascal.

CUST GMM 1° Année 2004/2005 Cours C++

5

Chapitre 2 – Fonctionnalités de base en C++ De par la très bonne implantation du C et la compatibilité entre C et C++, le langage C++ est devenu un des langages orientés objet le plus utilisé. Il provient des laboratoires de recherche ATT Bell. Avec la "mode de l'objet", de nombreux programmeurs C sont passés à C++ en espérant pouvoir programmer OO sans bouleverser leurs habitudes. Comme nous le verrons, tout n'est pas si simple. C++ existe sur de nombreuses plate- formes (attention aux différences dues aux compilateurs existants). La structure d'un programme C++ est la suivante :

- le point d'entrée est la fonction main() - un programme exécutable est un ensemble de modules objets binaires obtenus en

compilant un texte source. Ce dernier est un ensemble de définitions de classes, fonctions et données et de directives pour le préprocesseur.

Nous pouvons noter quelques-uns des aspects lexicaux du langage : - la présentation du texte source est libre - les mots clés du langage sont réservés - le langage différentie majuscules et minuscules - les commentaires commencent par //…. et s'arrêtent en fin de ligne ou sont encadrés

par /*….*/ Exemple de programme C++ simple : // Premier programme C++ #include <iostream> int main( ) { std::cout<<"Bonjour"<<std::endl; return 0; } Le programme commence par inclure la librairie d'entrée/sortie standard <iostream>. A ce propos, on constate que par rapport au C, il n'y a plus d'extensions .h. En effet, lors de la normalisation, les fichiers standard ont "perdu" leur extension. Les fichiers de la librairie C ont été renommés en ajoutant la lettre c devant leur nom. Par exemple, le fichier <stdlib.h> est devenu <cstdlib>. On trouve ensuite le point d'entrée du programme : la fonction main. Le programme se contente d'afficher un message sur le flot de sortie standard (std::cout). La fonction main retourne la valeur 0 pour signaler au système d'exploitation que le programme s'est bien terminé.

1- Types Dans cette section, nous allons examiner les différents types disponibles en C++ (sauf la notion de classe qui sera présentée dans le chapitre suivant). Quand on regarde les types mis à notre disposition, on constate qu'il n'y a pas à proprement parler de type chaîne de caractères. En fait, cette notion est introduite dans la librairie standard sous la forme d'une classe string (voir chapitre sur la STL).

CUST GMM 1° Année 2004/2005 Cours C++

6

1.1. Types fondamentaux Types déjà présents en C

Caractères Le type char permet de stocker un caractère d'un jeu de caractères (généralement ASCII). La plupart du temps, la taille d'une variable de type char est 8 bits et peut donc contenir 256 valeurs distinctes. Par exemple : char c='a'; Chaque constante caractère a une valeur entière associée. Par exemple, la valeur de 'a' est 97. Les caractères peuvent donc être soit signés (signed char) soit non signés (unsigned char). Dans le premier cas, leurs valeurs varient entre -127 et 127, dans le deuxième entre 0 et 255. Il existe un type de caractère (wchar_t) qui est utilisé pour manipuler des jeux de caractères étendus (par exemple Unicode). Les opérations arithmétiques et logiques s'appliquent aux types caractères. Les constantes caractères sont représentées par un caractère entre guillemets simples ('a' par exemple). Elles représentent la valeur du caractère dans le jeu de caractères. Les constantes pour les caractères étendus s'écrivent L'ab'. La signification des symboles entre guillemets est dépendante de l'implémentation. Entiers Le type de base pour les entiers est int. Il peut être signé (signed int ou signed) ou non (unsigned int ou unsigned). De plus, il existe un type entier court (short int ou short) et un type entier long (long int ou long). Une constante entière peut prendre plusieurs formes :

- décimale (63), - octale représentée par un zéro suivi du nombre en base 8 (077 ó 63 en base 101), - hexadécimale représentée par un zéro suivi d'un x puis du nombre en base 16 (0x3f)2.

Il est possible d'adjoindre à une constante un suffixe précisant si cette dernière est un entier non signé (suffixe U) ou un entier long (suffixe L). Flottants Il en existe trois types :

- les simples précisions (float) - les doubles précisions (double) - les précisions étendues (long double) (introduit en C++)

Le type par défaut d'une constante flottante (e.g. 1.23) est double. Le suffixe F ou f peut être ajouté pour obtenir une constante de type float. Le type void Ce type représente l'absence d'information de type. Il ne peut pas exister d'objet de type void mais il est utilisé dans des déclarations plus complexes. Par exemple : void f() // fonction sans valeur de retour

1 077 base 8ó 7*8+7 = 63 2 0x3f base 16 ó 3*16+15 (en hexadécimal on utilise 1 à 9 puis a=10, b=11, c=12, d=13, e=14, f=15).

CUST GMM 1° Année 2004/2005 Cours C++

7

Nouveauté C++ Booléens Un booléen, bool, peut prendre deux valeurs : true ou false. Par exemple : bool b=true; Lors d'une conversion en entier, true prend la valeur 1 et false la valeur 0. Réciproquement, les types caractères, entiers et flottants peuvent être implicitement convertis en booléens. La conversion d'une valeur non nulle vaudra true alors que 0 vaudra false.

1.2. Types construits Types déjà présents en C

Pointeurs Pour un type T, une variable de type pointeur sur T (T*) contient l'adresse d'un objet de type T. On récupère la valeur pointée en déréférençant le pointeur grâce à l'opérateur *. Réciproquement, l'adresse d'une variable est donnée par l'opérateur &. int i=1; int *p=i // p contient l'adresse de i int k=*p; // k=i Il existe une valeur particulière pour les pointeurs nuls : la valeur zéro (0). Elle représente une adresse illégale car aucun objet ne doit se trouver à cette adresse. Remarque : en C, la macro NULL représente le pointeur nul. En C++, il est préférable d'utiliser la valeur zéro. Tableaux Pour un type T, on peut définir un tableau de n éléments de type T (T[n]). Les éléments sont indicés de 0 à n-1. L'accès aux éléments se fait la plupart du temps par l'opérateur []. La taille d'un tableau doit être une expression constante. Par exemple : int vecteur[10]; // tableau de 10 entiers double d[5]; // tableau de 5 flottants : d[0] à d[4]

On peut définir des tableaux à plusieurs dimensions : double matrice[10][20]; // Tableau de 10 tableaux de 20 flottants

Un tableau peut être initialisé lors de sa création : int v[]={1,4,6}; // tableau de 3 entiers : 1, 4 et 6 int w[5]={2,4,5}; // tableau de 5 entiers 2, 4, 5, 0 et 0

Attention l'affectation entre tableaux n'existe pas !!! Pointeurs et tableaux Les pointeurs et les tableaux sont très liés : le nom d'un tableau représente un pointeur constant sur son premier élément. Constantes Il est possible de définir des constantes à l'aide du mot clé const. Ce mot clé peut être ajouté à toute déclaration. Une constante peut être utilisée comme taille d'un tableau.

CUST GMM 1° Année 2004/2005 Cours C++

8

Par exemple : const int taille=10; const int s[taille]; // s[0] est constant,… const double pi=3.141592;

Constantes et pointeurs Dans le cas d'un pointeur, deux objets sont concernés : le pointeur lui-même et la variable pointée. Le mot clé const placé devant la déclaration indique que la variable pointée est constante mais pas le pointeur. Pour déclarer un pointeur constant, il faut utiliser * const. const char *p1="azerty"; //Pointeur sur caractère constant char c; char * const p2 = &c; //Pointeur constant sur caractère const char * const p3 ="azerty"; //pointeur constant sur caractère constant

→ Cette notion sera approfondie en deuxième année. struct et union Une structure est un agrégat d'éléments de types différents. Elle est l’« ancêtre » de la classe C++. Par exemple : struct adresse {

char *nom; int numero; char *rue; char cp[6]; char *ville;

}; adresse a={"Dupond", 12, "avenue des landais", "63000", "Clermont"}; Remarque : On peut noter le ; à la fin de la déclaration de la structure. C'est le seul cas (avec la déclaration de classe) où l'on doive mettre un ; après une accolade fermante. L'accès aux membres d'une structure se fait par l'opérateur . (a.numero). L'accès par l'intermédiaire d'un pointeur se fait par l'opérateur -> (p->numero). Notons que p->numero est équivalent à (*p).numero. Les union s'utilisent de la même façon que les structures, mais chacun des champs d'une union partage le même espace mémoire, celui du plus grand élément déclaré.

Nouveauté C++ Chaînes de caractères : la classe prédéfinie Une chaîne de caractères est représentée par un tableau de caractères terminé par le caractère '\0'. Une constante chaîne de caractères contient des caractères encadrés par des guillemets ("abc"). Elle est de type tableau de caractères constants. Les manipulations de chaînes de caractères se font par l'intermédiaire d'un ensemble de fonctions déclarées dans <cstring>. En C++, il existe une classe prédéfinie string pour le traitement des chaînes de caractères. Références Une référence est un "alias" pour un objet. Elles sont principalement utilisées pour le passage d'arguments par adresse des fonctions. Pour un type T, une référence sur T est notée T&. Une référence doit toujours être initialisée.

CUST GMM 1° Année 2004/2005 Cours C++

9

Par exemple : int i=1;

int &ri=i; //ri est une référence sur i int j=ri; //j=i ri=4 //i=4

1.3. Référence ou pointeur ?

Dans les deux cas, ce sont des variables (ou des constantes) dont la valeur (le contenu) est une adresse mémoire.

- Un pointeur est un concept de bas niveau permettant une manipulation précise de l'adresse (arithmétique des pointeurs, pointeur de fonction, …)

- Une référence est une abstraction de plus haut niveau qui fournit une interface plus simple mais plus limitée pour manipuler l'adresse

Fonctionnalités Référence Pointeur

Simplicité Oui Non Notation spécifique Non Oui

Arithmétique des adresses Non Oui Adresse de fonctions Non Oui

Référence Pointeur

Nom Le nom de la référence représente directement l'objet

Le nom du pointeur représente l'adresse de l'objet

Adresse L'adresse d'une référence est l'adresse de l'objet

L'adresse du pointeur est l'adresse de la variable contenant l'adresse

de l'objet

Accès Pas de notation spécifique pour accéder à l'objet

Notation spécifique pour déréférencer le pointeur

Opérations Une opération sur une référence manipule directement l'objet

Une opération sur un pointeur manipule l'adresse de l'objet

Constantes Une référence constante rend l'objet

constant

Un pointeur peut être un pointeur sur une objet constant, un pointeur

constant sur un objet ou un pointeur constant sur un objet

constant

Fonctions Pas de manipulation d'adresse de fonction par une référence

manipulation d'adresse de fonction (passage de traitement en

paramètre, tableau de pointeurs de fonction, …)

CUST GMM 1° Année 2004/2005 Cours C++

10

Exemple : #include <iostream> using namespace std; int compare_int(const void *arg1, const void *arg2 ) { return *(int *)arg1 - *(int *)arg2; } int main(int argc, _TCHAR* argv[]) { int tableau[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // Utilisation int *p = &tableau[5]; // Un pointeur // Dans le cas d'un pointeur, on différencie la valeur du pointeur

// (adresse de l'élément pointé), la valeur de l'élément pointé et l'adresse du pointeur cout << "p = " << p << ", *p = " << *p << ", &p = " << &p << endl;

// p = 0012FEC4, *p = 5, &p = 0012FEA4 ++p;

// Arithmétique des pointeurs (incrémente de sizeof(int) l'adresse contenue dans p // donc p pointe maintenant sur tableau[6])

cout << "p = " << p << ", *p = " << *p << ", &p = " << &p << endl; // p = 0012FEC8, *p = 6, &p = 0012FEA4

int &r = tableau[5]; // Une référence r += 5; // Incrémente directement la valeur (tableau[5] vaut maintenant 10) // Dans le cas d'une référence, sa valeur est la valeur de l'élément pointé

// et son adresse l'adresse de l'élément pointé cout << "r = " << r << ", &r = " << &r << endl; // r = 10, &r = 0012FEC4 // Constantes const int *q1 = new int(1); // Pointeur sur une valeur constante

// ("*q1 = 12;" interdit, "q1 = p;" OK) cout << "q1 = " << q1 << ", *q1 = " << *q1 << ", &q1 = " << &q1 << endl;

// q1 = 002F1040, *q1 = 1, &q1 = 0012FE8C int * const q2 = new int(2);

// Pointeur constant sur une valeur ("*q2 = 12;" OK, "q2 = p;" interdit) cout << "q2 = " << q2 << ", *q2 = " << *q2 << ", &q2 = " << &q2 << endl;

// q2 = 002F1070, *q2 = 2, &q2 = 0012FE80 const int * const q3 = new int(3); // Pointeur constant sur une valeur constante

// ("*q3 = 12;" interdit, "q3 = p;" interdit) cout << "q3 = " << q3 << ", *q3 = " << *q3 << ", &q3 = " << &q3 << endl;

// q3 = 002F10A0, *q3 = 3, &q3 = 0012FE74 const int &s1 = *new int(4); // Référence sur une valeur constante

// ("s1 = 12;" interdit, pas d'autres alternatives) cout << "s1 = " << s1 << ", &s1 = " << &s1 << endl; // s1 = 4, &s1 = 002F10D0 // Adresses de fonctions (passage d'un traitement en paramètre) int (*compare)(const void *, const void *) = &compare_int; qsort((void *)tableau, 10, sizeof(int), compare); for (int i = 0; i < 10; ++i) cout << tableau[i] << " "; cout << endl; // Pas d'équivalent avec les références (utiliser un objet fonction) // Simplicité et lisibilité des pointeurs... int *(*(*(*f)())[])(); // f est un pointeur de fonction sur une fonction sans paramètre

// retournant un pointeur sur un tableau de pointeurs de fonctions // retournant un pointeur sur un int

return 0; }

CUST GMM 1° Année 2004/2005 Cours C++

11

1.4. Déclarations

En C++, toute variable doit être déclarée avant d'être utilisée. Contrairement au langage C, une déclaration peut se trouver n'importe où dans le programme. Il ne faut pas confondre déclaration et définition : une déclaration précise au compilateur le type d'un identificateur alors qu'une définition représente la création d'une entité du type en question. Ces deux étapes peuvent néanmoins se dérouler simultanément. Par exemple : int i; //déclaration et définition de l'entier nommé i extern int j; //déclaration de l'entier nommé j int k=12; //déclaration, définition et initialisation de l'entier nommé k Il doit y avoir exactement une définition par identificateur dans un programme C++. Cependant, il peut exister plusieurs déclarations d'un même identificateur (si le type est identique pour chacune). Il est possible de déclarer plusieurs identificateurs dans une même déclaration. Par exemple : int i,j; //int i; int j; int *p, q; //int *p; int q; ATTENTION PAS int *q; La portée d'un identificateur est limitée au bloc (délimité par {}) dans lequel il est déclaré. Pour un identificateur global (déclaré en dehors de tout bloc), sa portée va de sa déclaration à la fin du fichier. Il est possible de déclarer un "alias" pour un type grâce au mot clé typedef. typedef unsigned int uint; //uint est un alias pour unsigned int uint i; // unsigned int i;

2- Expressions et instructions 2.1. Opérateurs

Dans le tableau 1, les opérateurs sont classés par ordre de priorité décroissant. Ceux qui se trouvent dans une même case du tableau ont même précédence. Notons également que les opérateurs d'affectation et les opérateurs unaires sont associatifs à droite. Tous les autres sont associatifs à gauche. La signification des opérateurs est donnée pour le cas des types prédéfinis.

CUST GMM 1° Année 2004/2005 Cours C++

12

TABLEAU 1

CUST GMM 1° Année 2004/2005 Cours C++

13

CUST GMM 1° Année 2004/2005 Cours C++

14

Gestion dynamique de la mémoire Quatre nouveaux opérateurs sont utilisés pour la gestion dynamique de la mémoire :

- new et delete pour les objets, - new …[] et delete[] pour les tableaux

L'opérateur new type alloue la mémoire pour un objet de type type et retourne un pointeur de type type * sur cet objet. L'opérateur new type[n] alloue la mémoire pour n objets de type type et retourne un pointeur de type type * sur le premier élément du tableau. Bien que l’utilisation de l’opérateur new semble être une opération unique, comme dans int *pi = new int(5) ; elle est en fait menée en deux étapes distinctes :

1. allocation de la mémoire demandée par l’appel de l’opérateur new approprié en transmettant à celui-ci la taille de l’objet : int *pi = _new(sizeof(int)) ;

2. initialisation de l’objet ainsi alloué : *pi=5 ; De plus, l’initialisation n’est exécutée qu’à la condition que l’allocation réussisse. L’opérateur delete est géré de la même manière : lorsque le développeur écrit : delete pi ; le langage impose que l’opérateur delete ne soit pas appliqué si pi est égal à zéro. Remarques :

- les malloc, free, calloc, … ne doivent plus être utilisés en C++ - delete [] ne doit pas être appliqué à un pointeur alloué avec new. De même que

delete ne doit pas être appliqué à un pointeur alloué avec new…[]. Conversion de type La syntaxe C pour les conversions de type était de la forme : (type) expr. La syntaxe C++ est : type(expr). Le problème est le manque de lisibilité de ces deux formes (difficile à retrouver dans un grand programme) et le fait que le compilateur n'effectue aucun contrôle. Quatre nouveaux opérateurs ont donc été construits :

- const_cast autorise la conversion si seul le qualificateur const change (est ajouté ou supprimé du type)

- static_cast autorise les conversions entre des types reliés (d'un type pointeur à un autre, d'un type énuméré en entier ou d'un flottant en entier)

- dynamic_cast fait partie du mécanisme de typage dynamique (voir dans la suite) - reinterpret_cast laisse libre de toute conversion (il fait ce que les autres ne veulent

pas faire) Remarque : l'opérateur reinterpret_cast est à utiliser avec prudence. NB : ces notions seront approfondies en deuxième année

2.2. Instructions Instructions conditionnelles Il existe deux instructions de test : if et switch if (cond) instr if (cond) instr else instr switch (cond)

CUST GMM 1° Année 2004/2005 Cours C++

15

Pour le if, la première instruction est exécutée si le résultat du test est non nul. Dans le cas contraire, c'est la seconde instruction (si elle existe). L'instruction switch prend la forme suivante : switch (cond){ case constante_1 : instr; case constante_2 : instr; default : instr; } Elle permet d'exécuter les instructions se trouvant au cas correspondant à la condition. Si aucun des cas n'est vérifié, ce sont les instructions de default qui sont exécutées. Cette partie est facultative. A la fin de l'exécution d'un cas, le programme se poursuit au cas suivant. Si ce n'est pas le comportement souhaité, il faut placer une instruction break à la fin du cas pour sortir du bloc switch. Remarque : il est possible de déclarer une variable dans une condition. Ceci permet de limiter la portée de cette variable aux instructions liées au test. if (double d=f(3)) { //d != 0 //d est accessible } //d n'est pas accessible Instructions de boucles Une boucle peut être exprimée à l'aide de l'une des instructions suivantes : while (cond) instr do instr while (expr) for (expr; cond; expr) instr L'instruction while exécute l'instruction tant que la cond ition est vraie. L'instruction for est utilisée pour les boucles dont le nombre d'itérations est connu à l'avance. La première expression représente l'initialisation. La condition représente le test d'arrêt et la dernière expression est exécutée à chaque fin de boucle. Ces trois parties sont facultatives mais les ; doivent être présents. Remarque : il est possible de déclarer une variable dans la partie initialisation de la boucle for. De même que pour le test, cela limite la portée de la variable aux instructions de la boucle. L'instruction do peut être source d'erreurs car elle est toujours exécutée au moins une fois avant que la condition ne soit évaluée. Il est donc préférable d'éviter cette instruction. Instructions break et continue Ces deux instructions permettent de sortir d'une boucle. L'instruction break provoque la sortie immédiate d'une boucle ou de l'instruction switch. L'instruction continue passe directement à l'itération suivante de la boucle. Pour un while ou un do, le test d'arrêt est immédiatement exécuté. En ce qui concerne le for, on passe à l'étape d'incrémentation.

CUST GMM 1° Année 2004/2005 Cours C++

16

Instruction goto et les étiquettes Cette instruction ne doit pas être utilisée en pratique. En effet, elle peut rendre un programme très confus. Néanmoins, une des utilisations possibles du goto est la sortie de plusieurs boucles imbriquées. Une étiquette est un identificateur suivi de ':'. L'instruction goto peut se brancher sur n'importe quelle étiquette se trouvant dans la même fonction.

2.3. Fonctions Elles sont définies (réalisées) dans un module source et peuvent être appelées dans une instruction d'une autre fonction ou d'un autre module à condition qu'elles aient été déclarées dans le module appelant. Déclaration La déclaration d'une fonction précise le nom de la fonction, son type de retour ainsi que le nombre et le type de ses arguments. Seul le type est nécessaire pour les paramètres. Le nom de l'argument est ignoré par le compilateur. Néanmoins, le nom du paramètre facilite la compréhension (documentation minimale). void f(); //fonction sans valeur de retour et sans argument char *g(const char*, int); //deux arguments char *strcpy(char *dest, const char *source); //deux arguments nommés Définition Une fonction doit être définie (une seule fois) pour pouvoir être appelée. La définition d'une fonction reprend sa déclaration mais y ajoute le corps de la fonction. Par exemple : void f(); void f() {

//Traitements } Une fonction peut-être définie comme étant inline. Cela signifie que le compilateur tentera de remplacer les appels de cette fonction par le code correspondant. Variables statiques Une variable locale est créée sur la pile (et initialisée) à chaque appel de la fonction. Pour une variable locale déclarée static, un seul objet sera alloué statiquement pour toute la durée de l'exécution du programme. Tous les appels de la fonction utiliseront donc cet unique objet. (développé en deuxième année) Arguments Les paramètres d'une fonction sont passés par valeur. Leurs modifications ne seront donc pas visibles après la sortie de la fonction. Il est toutefois possible de passer un pointeur ou une référence sur une variable. : void f1(int i); //un entier passé par valeur void f2(int *p); //un pointeur sur un entier void f3(int &r); //une référence void f4(const int &r); //une référence constante

CUST GMM 1° Année 2004/2005 Cours C++

17

L'efficacité est aussi une bonne raison pour souhaiter passer un paramètre par référence. De cette façon, on évite une copie sur la pile de l'objet passé en argument. Dans ce cas, si le paramètre n'est pas modifié, il doit être déclaré const. Valeur de retour Toute fonction qui n'est pas déclarée comme retournant void doit renvoyer une valeur. C'est l'instruction return expr qui permet cela. Dans le cas d'une fonction retournant void, le mot clé return peut aussi être utilisé (mais sans préciser de valeur) et provoque la sortie immédiate de la fonction. Les nouveautés du C++ La surcharge de fonction Contrairement au C, C++ permet de définir des fonctions homonymes i.e. de même nom, mais avec des paramètres différents (nombre ou type). Par exemple : void f(int); // effectue un traitement à partir d'un entier void f(double); // effectue un traitement à partir d'un flottant Remarque : le type de retour n'est pas pris en compte pour différencier les fonctions homonymes. Les arguments par défaut Il est possible de préciser une valeur par défaut pour les arguments d'une fonction lors de la déclaration. Seuls les paramètres terminant la déclaration peuvent posséder une valeur par défaut. C'est à dire qu'à partir du moment où un argument possède une valeur par défaut, tous ses successeurs doivent en posséder une. void f(double x, double y=0.0, double z=0.0);

2.4. Préprocesseur Les directives se reconnaissent par le symbole # Inclusion de fichier source Cette directive (#include…) permet d'importer dans tout module qui en a besoin :

- des définitions de types, de classes, de constantes, - des déclarations de fonctions

Par exemple : #include "fichier.h" //recherche dans le répertoire de travail #include <fichier2.h> //recherche dans les répertoires standard #include <fichier3> //inclusion d'un fichier d'en-tête de la lib. standard Substitution d'identificateurs C'est une notion héritée du C, peu utilisée en C++. La directive #define permet d'améliorer la lisibilité du code ou de définir des macro- instructions. Attention : une macro-instruction peut produire des effets de bord délicats à contrôler.

CUST GMM 1° Année 2004/2005 Cours C++

18

Par exemple : #define TRUE=1 #define max(a,b) ((a)>(b)?(a):(b)) int i=3; int j=max(i++,2); //j=4, I=5 !?! En C++ on remplace ces substitutions par :

- #define TRUE 1 devient const bool TRUE=1 (meilleur contrôle de type); - #define max(a,b) ((a)>(b)?(a):(b)) devient inline int max(int a, int b)

{return a>b?a:b;} (on perd la généricité, mais voir la notion de template). Remarques :

- La définition de constantes en C++ permet d’éviter l’utilisation des macros. - La déclaration de fonctions en ligne (inline) permet une plus grande rapidité

d’exécution (accès direct aux données). Compilation conditionnelle Les directives (#ifdef, #ifndef, #else, …) évitent principalement les inclusions multiples de fichiers. Elles facilitent également la mise au point (assert(cond) et macro NDEBUG) et la gestion des versions . Il est donc primordial d’utiliser ces directives. #ifndef MACLASSE.H #define MACLASSE.H //contenu de MACLASSE.H #endif //MACLASSE.H

2.5. Espaces de nommage Les espaces de nommage définissent une portée particulière pour les identificateurs qui y sont déclarés. On les introduit par le mot-clé namespace. Pour accéder à un identificateur déclaré dans un espace de nommage nom, il faut le faire précéder de nom::. De cette façon, il est par exemple possible d’accéder à deux classes de même nom se trouvant dans deux librairies distinctes. Namespace lib1 { Class Pile{/*classe Pile de lib1*/} //… } Namespace lib2 { Class Pile{/*classe Pile de lib2*/} //… } void main() { lib1::Pile p1 ; lib2::Pile p2 ; } On peut importer dans l’espace de nommage par défaut (celui que l’on utilise sans préfixe) le contenu d’un autre en utilisant l’instruction using namespace nom_de_namespace ;. Toutefois, cette fonctionnalité est à utiliser avec prudence étant donné qu’elle peut provoquer

CUST GMM 1° Année 2004/2005 Cours C++

19

des collisions entre identificateurs. La clause using permet également de n’importer qu’un identificateur particulier d’un espace de nommage (using lib1::Pile). Enfin, on peut définir un alias pour un espace de nommage de la façon suivante : namespace nom1=nom2 ; Un espace de nommage n’est ni un module ni un objet : il sert à regrouper des déclarations dans une même unité cohérente (typiquement, une classe et des fonctions non membres qui sont relatives à cette classe). De plus, les collisions de nom entre les différentes librairies peuvent être supprimées. Il est possible d’imbriquer les espaces de nommage. Leur contenu peut aussi être divisé entre plusieurs fichiers sources.

CUST GMM 1° Année 2004/2005 Cours C++

20

Chapitre 3 - Les concepts objet et leurs applications en C++ Remarque préalable : les notions d’objet, de classe et d’héritage ont déjà été vues dans le cours de Marinette Bouet. Nous ne ferons donc que des rappels concernant ces notions.

1- Introduction 1.1. Abstraction

L'abstraction est l'un des piliers de l'approche objet ; c'est un concept utilisé dans de nombreux domaines (e.g. les mathématiques) et qui consiste à ignorer les détails pour ne conserver qu'une notion générale.

- L'abstraction est un processus qui consiste à identifier les caractéristiques intéressantes d'une entité en vue d'une utilisation précise

- L'abstraction désigne aussi le résultat de ce processus, c'est-à-dire l'ensemble des caractéristiques essentielles d'une entité.

Il existe différentes sortes d'abstraction :

- l'abstraction procédurale qui consiste à considérer une opération comme étant atomique même si elle peut se décomposer en opérations de plus bas niveau (utilisée quand on écrit un sous-programme)

- l'abstraction de données qui consiste à définir un type de données par ses opérations (on accède aux éléments via les opérations)

1.2. Modèle

Un modèle est une vue subjective mais pertinente de la réalité. Il définit une frontière entre la réalité et la perspective de l'observateur. Le caractère abstrait d'un modèle doit notamment permettre de faciliter la compréhension du système étudié. Un modèle réduit (décompose) la réalité dans le but de disposer d'éléments de travail exploitables par des moyens mathématiques ou informatiques. Il réduit la complexité du système étudié, permet de le simuler, le représente et reproduit ses comportements. Un modèle est une abstraction de la réalité.

1.3. Encapsulation L'encapsulation consiste à regrouper au sein d'une même entité le code et les données et à masquer les détails d'implémentation en définissant une interface. L'interface est la vue externe d'un objet, elle définit les services accessibles aux utilisateurs de l'objet. Les données doivent être encapsulées :

- accès uniquement par les opérations - évite la duplication des contrôles à effectuer sur les données - facilite l'évolution d'une application (modification de l'implémentation des attributs

sans modifier les attributs) - permet un contrôle de l'accès aux données et donc assure une meilleure intégrité des

données L'encapsulation nécessite une réflexion de la part du concepteur afin de déterminer quelles opérations définir et ce qu'il faut rendre accessibles par ces opérations.

CUST GMM 1° Année 2004/2005 Cours C++

21

1.4. Type Selon le Petit Larousse, un type est un modèle abstrait réunissant à un haut degré les traits essentiels de tous les êtres ou de tous les objets de même nature. Un type précise les comportements communs à un ensemble d'entités

1.5. Typage Le typage est l'affectation stricte d'une classe à un objet Il existe différentes sortes de typage :

- Le typage statique • l'information de type est associée à l'identificateur • la vérification de type est effectuée à la compilation • technique la plus sûre • pb : tout ne peut pas être vérifié à la compilation

- Le typage dynamique • l'information de type est associée au contenu des variables • la vérification de type est effectuée à l'exécution • technique plus souple • pb : détection tardive des erreurs

La plupart du temps, les langages choisissent un intermédiaire : ce qui peut être vérifié à la compilation l'est, le reste est différé à l'exécution.

2- Objets et classes 2.1. Objet

Un programme orienté objet est un modèle d'une partie du monde réel : les entités du monde réel doivent être représentées dans le programme ; leur représentation est un objet (c'est donc un modèle d'une entité réelle). Un objet est formé de deux composants indissociables :

- son état, représenté par les valeurs prises par une ou plusieurs variables - son comportement, représenté par les opérations qui lui sont applicables

Un objet est une instance d'une classe (une occurrence d'un type abstrait) Exemple d'objet réel : une voiture Il faut bien différencier objets réel et logiciel : l'objet du programme étant un modèle (approximation du monde réel), son état ne comprend que ce qui est intéressant du point de vue de l'application. Etat pour la voiture : n° d'immatriculation, couleur,… Comportement pour la voiture : freiner, accélérer, changer de vitesse,… Etat

- caractéristique interne propre et cachée aux autres objets - les variables représentant un l'état d'un objet particulier sont appelées variables

d'instance ou attributs ou données membres - chaque objet possède sa propre copie des variables d'instance - Les attributs conservent leurs valeurs durant toute la durée de vie d'un objet

CUST GMM 1° Année 2004/2005 Cours C++

22

Chaque objet possède un état courant décrit par la valeur de ses attributs donc chaque objet possède une copie propre de ses attributs. Comportement

- caractéristique externe mise à disposition des autres - les opérations propres à un objet sont appelées méthodes d'instance ou fonctions

membres (fonctions associées à l'objet) - ces méthodes sont invoquées par rapport à un objet particulier

Remarque : un objet a de plus une identité (identifiant interne) qui caractérise son existence de façon indépendante de son état (égalité≠identité : deux objets peuvent être égaux, i.e. avoir même valeur, sans être identiques). Exemples d'objets

Les trois objets O1, O2 et O3 sont des instances de la classe Personne ; la valeur des attributs représente l'état de chaque objet et les méthodes ne sont pas représentées pour les objets (une méthode est invoquée par rapport à un objet mais elle est rattachée à la classe car le code est partagé par tous les objets d'une classe). O1 et O3 sont égaux, mais pas identiques. Message Un message est un moyen de communication (d'interaction) entre objets. Un paramètre représente une information complémentaire envoyée avec le message. Le message est une requête envoyée à un objet pour demander l'exécution d'une méthode ; il est constitué de trois composants :

- l'objet auquel il est envoyé - le nom de la méthode à invoquer - les paramètres

Exemple :

O1 envoie un message à O2 ; le message se traduit par l'exécution de la méthode GetAge par l'objet O2 : le résultat sera donc 30.

CUST GMM 1° Année 2004/2005 Cours C++

23

2.2. Classe Définitions Une classe est un "modèle" (un "moule") pour une catégorie d'objets structurellement identiques. Elle est à la fois un modèle et un mécanisme pour la création d'objets basés sur ce modèle. Une classe est l'ensemble de tous les objets créés selon un modèle spécifique et représente un concept du monde réel. Composition d'une classe

- la définition des attributs (leur nom et leur type) - le prototype (la signature) des méthodes (publique) ; la signature d'une fonction

englobe son nom et le type de ses paramètres. Les signatures des méthodes représentent l'interface de la classe.

- la réalisation (ou définition) des méthodes (privée) ; les définitions des méthodes représentent l'implémentation de la classe.

Remarque : l'ensemble de tous les objets d'un type est parfois appelé "sorte". Exemple : ma voiture est une instance de la classe Voiture ; l'état et le comportement des voitures est commun à toutes les voitures, mais l'état courant de chaque voiture est indépendant des autres. Remarques

- Chaque objet est une instance d'une classe (mécanisme d'instanciation) - Une classe peut contrôler l'accès à ses membres (données ou fonctions) :

• privé : accès limité à la classe • protégé : accès limité à la classe et à ses sous-classes • publique : accès non limité • paquetage (package) : accès limité au paquetage contenant la classe

Une classe possède généralement un invariant Méthodes et surcharge Il existe différents types de méthodes :

- Accesseur : permet de consulter l'état d'un objet - Mutateur : permet de modifier l' état d'un objet - Contructeur

• permet d'initialiser un objet afin de le placer dans un état cohérent • fixe l'invariant de la classe • appelé automatiquement lors de la création d'un objet

- destructeur • permet de libérer les ressources allouées par l'objet • appelé automatiquement lors de la destruction de l'objet

Attention : une méthode doit maintenir l'invariant de la classe La surcharge d'une méthode consiste à définir plusieurs méthodes de même nom mais ayant des signatures différentes (nombre et type des paramè tres).

CUST GMM 1° Année 2004/2005 Cours C++

24

Exemple de classe

Premier pavé : nom de la classe Deuxième pavé : attributs Troisième pavé : signature des méthodes + signifie publique, # signifie protégé, - signifie privée, -signifie paquetage En général, les attributs sont privés, et les méthodes sont publiques. Personne est un constructeur (surcharge), getAge un accesseur, anniversaire un mutateur (ajoute 1 an). Exemple d'invariant : l'âge est compris entre 0 et 120. Exemple de lien entre classe et objet

O1, O2 et O3 sont des instances de la classe Personne (type de lien particulier en UML) On représente rarement les classes et objets sur le même schéma (pas le même "niveau"). ATTENTION A BIEN DIFFERENCIER CLASSE ET OBJET

CUST GMM 1° Année 2004/2005 Cours C++

25

Association L’association est un outil de modélisation : nous ne le verrons pas en détail. Une association représente une connexion sémantique bidirectionnelle entre une (association réflexive) ou plusieurs classes. L’arité d’une association représente le nombre de participants à l’association (association binaire, ternaire, n-aire). Deux cas particuliers seront vus à travers des exemples : l’association binaire et l’association n-aire.

Exemple d’association binaire

- sens de lecture avec le nom de l’association - Rôle des participants - Classe d’association - Association réflexive sur une association

Exemple d’association n-aire

Agrégation L’agrégation est une association non symétrique qui exprime un couplage fort et une relation de subordinations. Elle représente une relation de type "ensemble/élément" (ou "tout/partie") entre des classes. La classe représentant l’ensemble est parfois appelée agrégat. A un même moment, une instance d’élément agrégé peut être liée à plusieurs instances d’autres classes (l’élément agrégé peut être partagé).

CUST GMM 1° Année 2004/2005 Cours C++

26

Une instance d’élément agrégé peut exister sans agrégat (et inversement) : les cycles de vie de l’agrégat et de ses éléments agrégés peuvent être indépendants.

ð si on détruit une agrégation, on ne détruit pas ses composants.

Exemple d’agrégation

Composition Une composition est une agrégation forte (agrégation par valeur) : à un même instant, une instance de composant ne peut être liée qu’à un seul agrégat et les cycles de vie des éléments (les composants) et de l’agrégat sont liés. Si l’agrégat est détruit (ou copié), ses composants le sont aussi. Remarque : le choix entre agrégation et composition est subjectif

Exemple de composition

3- Les classes en C++ Les classes permettent au programmeur de définir de nouveaux types utilisables de la même façon que les types prédéfinis. C’est un agrégat de données membres (attributs) et de fonctions membres (méthodes). Par exemple : struct Point { // Déclaration de la classe Point int x,y ; // attributs void init(int xx, int yy) ; //méthode } ; void Point::init(int xx, int yy) // Définition de la méthode { x=xx ; y=yy ; }

CUST GMM 1° Année 2004/2005 Cours C++

27

int main() {

Point p ; p.init(1,2) ; return 0 ; } Dans l’exemple précédent, on constate que la fonction init est déclarée dans la classe Point (une struct est une classe particulière). Lors de la définition de la méthode, on doit préciser à quelle classe elle appartient (Point::init). Dans une méthode, on peut accéder aux attributs de l’objet simplement en précisant leurs noms. Une méthode « sait » toujours à quel objet elle fait référence.

3.1. Accès aux membres Afin de respecter l’encapsulation des données, on utilise le mot-clé class à la place de struct. En effet, dans l’exemple précédent, l’accès aux données n’est pas limité aux méthodes. class Point {//Déclaration de la classe Point int x,y ; //attributs public : void init(int xx, int yy) ; //méthode }; On peut remarquer l’ajout du label public dans la déclaration. La partie précédant ce label (partie privée) est accessible seulement aux méthodes de la classe. Une structure (struct) est donc simplement une classe dont tous les membres sont public. Les différents niveaux de contrôle d’accès sont les suivants :

- public : permet un accès aux membres sans limitation (à réserver aux méthodes mais à éviter pour les attributs (encapsulation))

- protected : seules les méthodes des classes filles peuvent accéder aux membres (utile pour l’héritage)

- private : aucun accès possible à l’extérieur des méthodes de la classe. C’est le mode par défaut lors de la déclaration d’une classe.

3.2. Constructeurs

Un constructeur est une méthode particulière. Il porte le même nom que la classe et est invoqué lors de la création des objets de la classe. Il ne retourne pas de résultat. Il permet :

- l’initialisation de l’état de l’objet - la réservation de ressources si nécessaire (par exemple allocation de mémoire)

Il est possible de surcharger un constructeur afin de pouvoir initialiser un objet à partir de différents paramètres. Il faut néanmoins éviter la prolifération des constructeurs et fournir simplement les fonctionnalités voulues. Un constructeur sans paramètres ou dont tous les paramètres possèdent une valeur par défaut est appelé constructeur par défaut. Il est utilisé lorsqu’un objet est créé sans appel explicite à un constructeur. Dans notre exemple, un constructeur remplace avantageusement la méthode init. En effet, il joue le même rôle mais est appelé automatiquement.

CUST GMM 1° Année 2004/2005 Cours C++

28

class Point{//Déclaration de la classe Point Int x,y ; public : Point(int xx,int yy) ; //Constructeur Point(int xx) ; //Constructeur }; Remarques :

- pour des objets contenant des pointeurs, il est nécessaire de donner le code du constructeur. Le constructeur par défaut n’est pas toujours satisfaisant.

- en C, la définition de la méthode Init (cf. exemple précédent) est nécessaire, alors qu’en C++ cette dernière peut être remplacée avantageusement par le constructeur défini ci-dessus.

Initialisation des attributs Lorsque des attributs d’une classe possèdent un constructeur, il peut être nécessaire de l’appeler explicitement lors de la création d’un objet de la classe. De plus, cela est obligatoire dans le cas d’une classe sans constructeur par défaut, pour les membres constants ainsi que pour les membres références. Par exemple : class Point{//Déclaration de la classe Point Int x,y ; //Attributs public : Point(int xx,int yy) ; //Constructeur }; Point ::Point(int xx, int yy) // Définition du constructeur

:x(xx), y(yy) // initialisation de x avec xx et de y avec yy { }

3.3. Destructeurs Un destructeur est une méthode particulière appelée automatiquement lorsqu’un objet sort de sa portée, est détruit (par delete), … Il permet, par exemple, de libérer une ressource (mémoire, fichier, …) acquise généralement dans le constructeur. Il porte le même nom que sa classe préfixé par ~(tilde). class A { int *p; public : A() {p=new int ;} ~A(){delete p;} };

3.4. Méthodes constantes Les attributs d’une classe étant inaccessibles à l’extérieur de la classe, on peut fournir un ensemble de méthodes permettant de consulter les valeurs des attributs devant être exportés. Ces méthodes sont déclarées constantes afin de préciser que leurs appels ne modifient pas l’état de l’objet. L’ajout du mot-clé const après la liste des paramètres de la méthode permet cela.

CUST GMM 1° Année 2004/2005 Cours C++

29

class Point{//Déclaration de la classe Point int x,y ; //Attributs public : // …

int getx() const ; int gety() const ;

}; int Point::getx() const {

Return x; } int Point::gety() const {

Return y; }

3.5. Méthodes définies dans la déclaration de classe Une méthode peut être définie au moment de sa déclaration dans la classe. Dans ce cas, elle est considérée comme étant inline. Class Point{//Déclaration de la classe Point int x,y ; //Attributs Public : // …

int getx() const {return x;} int gety() const {return y;}

};

3.6. Auto-référence A l’intérieur d’une méthode, il est parfois nécessaire de posséder une référence à l’objet lui-même. C’est ce que permet le pointeur this qui pointe toujours sur l’objet pour lequel la méthode est appelée. Dans une méthode non constante d’une classe C, this est de type C*const. Dans une méthode constante, il est de type const C *const. Class Point{//Déclaration de la classe Point int x,y ; //Attributs Public : //…

int getx() const {return this->x;} //Equivalent à return x //…

};

CUST GMM 1° Année 2004/2005 Cours C++

30

3.7. Un exemple : la classe string de la librairie standard La librairie standard fournit un ensemble de classes pour la gestion des chaînes de caractères. Elles sont définies dans <string>. La classe basic_string Deux classes sont déclarées à partir du patron basic_string (voir plus loin les templates) :

- string qui manipule des char - wstring qui manipule des wchar_t

basic_string exporte un ensemble de types (value_type, iterator,…) et dispose d’itérateurs à accès aléatoire auxquels on peut accéder avec les méthodes classiques (begin, rbegin,…). Un certain nombre de constructeurs sont disponibles. L’accès à un élément se fait par l’opérateur [] (sans vérification d’indices) ou par la méthode at (avec vérification d’indices). L’opérateur d’affectation est disponible mais il existe aussi un ensemble de méthodes assign avec plusieurs paramètres. Différentes méthodes ou opérateurs sont fournis pour les comparaisons (compare, = =,…) Par exemple : #include <iostream> #include <string> int main( ) { std::string s1=”azerty”; // initialisation à partir d’une constante chaîne std::string s2=s1 ; if (s1 = = s2) std ::cout<<”egale\n”; // Opérateur = = s2 += ”ui” // s2 contient azertyui std::cout<<s1<<”, “<<s2<<’\n’; return 0; } La méthode data copie les caractères de la chaîne dans un tableau et retourne un pointeur sur ce tableau. Ce dernier est géré par l’objet string. La méthode c_str est similaire mais ajoute un 0 en fin de chaîne. La modification d’une chaîne peut se faire en ajoutant (méthode append) ou en insérant (méthode insert) des caractères. D’autres opérations sont supportées : la concaténation (opérateur +), la recherche (find, rfind, find_first_of, find_last_of, find_first_not_of, find_last_not_of), le remplacement (replace), l’effacement (erase), l’extraction de sous-chaînes (substr), les manipulations de tailles (size, max_size, length, empty, resize, capacity, reserve), l’échange de deux chaînes (swp) et les E/S (getline et les opérateurs << et >>).

CUST GMM 1° Année 2004/2005 Cours C++

31

3.8. Exemple de classe en C++ : une classe vecteur de réels Déclaration de la classe : fichier vecteur.h #include <iostream.h> class CVecteur // permet de déclarer des objets vecteurs de réels indexés à partir de 0 { public : // classes d’exceptions class ErreurIndice( ) ; //pour lancer des exceptions en cas d’erreur d’indice class ErreurDimension( ) ; //pour lancer des exceptions en cas d’erreur de dimension

//--------------------------------------------------------------------------------------------------------------------- // constructeurs CVecteur(void) ; // constructeur par défaut : 3 composantes initialisées à 0

CVecteur(int n) ; // n composantes initialisées à 0 CVecteur(int n, double val) ; // n composantes initialisées à val CVecteur(const CVecteur& v) ; // constructeur de copie

//--------------------------------------------------------------------------------------------------------------------- // destructeur

~CVecteur(void){delete[] vect ;}

//--------------------------------------------------------------------------------------------------------------------- // valeurs des attributs du vecteur

int ValeurDim(void) {return dim ;} // retourne la dimension du vecteur void Afficher (void) ; // affiche sur stdout les composantes du vecteur,

// en ligne //--------------------------------------------------------------------------------------------------------------------- // surcharges d’opérateurs double & operator[] (int i) throw(ErreurIndice); // retourne une reference sur la composante // d’indice i, si celle-ci existe, lance l’exception // ErreurIndice sinon int operator==(const CVecteur& v) ; // retourne 1 si le vecteur courant est égal à v, // (dimension et composantes), 0 sinon CVecteur & operator=(const CVecteur& v) ; // reconstruit le vecteur courant à partir de v et // retourne une référence sur ce vecteur CVecteur operator+(const CVecteur& v) throw(ErreurDimension) ;

// retourne un vecteur somme du vecteur // courant et de v s’ils ont même dimension, // lance l’exception ErreurDimension sinon

CVecteur operator*(double a) ; // retourne un vecteur égal au produit du // vecteur courant par a

double operator*(const CVecteur& v) throw(ErreurDimension) ; // retourne le produit scalaire du vecteur // courant par v s’ils ont même dimension, // lance l’exception ErreurDimension sinon

//--------------------------------------------------------------------------------------------------------------------- // lecture des composantes d’un vecteur (dont la dimension est connue) void Lire(void) ; // lit sur stdin les composantes du vecteur courant

//--------------------------------------------------------------------------------------------------------------------- // fonctions amies d’entrées/sorties sur flots iostream friend istream& operator >> (istream &str, CVecteur &v) ; // surcharge de >> friend ostream& operator << (ostream &str, const CVecteur &v) ; // surcharge de >> private : int dim; // nombre de composantes du vecteur double *vect ; // tableau des composantes réelles }

CUST GMM 1° Année 2004/2005 Cours C++

32

Réalisation de la classe : fichier vecteur.cpp #include <iostream.h> #include "vecteur.h" // Implémentation des méthodes et fonctions amies de la classe CVecteur //--------------------------------------------------------------------------------------------------------------------- // constructeurs CVecteur::CVecteur(void) // constructeur par défaut : 3 composantes initialisées à 0 { dim =3 ; vect=new double[3] ; for (int i=0 ;i<3 ;i++) vect[i]=0.0 ; } CVecteur(int n) // n composantes initialisées à 0 { dim =n ; vect=new double[n] ; for (int i=0 ;i<n ;i++) vect[i]=0.0 ; } CVecteur::CVecteur(int n, double val) // n composantes initialisées à val { dim =n ; vect=new double[n] ; for (int i=0 ;i<n ;i++) vect[i]=val ; } CVecteur::CVecteur(const CVecteur& v) // constructeur de copie { dim =v.dim ; vect=new double[ dim] ; for (int i=0 ;i<dim ;i++) vect[i]=v.vect[i] ; }

//--------------------------------------------------------------------------------------------------------------------- // valeurs des attributs du vecteur void CVecteur::Afficher (void) ; // affiche sur stdout les composantes du vecteur, en ligne { for (int i=0 ;i<dim ;i++) cout<<vect[i]<<’ ’; cout<<’\n’; } //--------------------------------------------------------------------------------------------------------------------- // surcharges d’opérateurs double & CVecteur::operator[] (int i) // retourne une reference sur la composante d’indice i,

// si celle-ci existe, lance l’exception ErreurIndice sinon { if (i<0||i>=dim) throw ErreurIndice( ) ; return vect[i]; } int CVecteur::operator==(const CVecteur& v) // retourne 1 si le vecteur courant est égal à v, (dimension et composantes), 0 sinon { if (dim!=v. dim) return 0;

else { int i=0 ; while (i<dim && vect[i] == v.vect[i]) i++ ; return i==dim;}

}

CUST GMM 1° Année 2004/2005 Cours C++

33

CVecteur & CVecteur::operator=(const CVecteur& v) // reconstruit le vecteur courant à partir de v et retourne une référence sur ce vecteur { if (this==&v) return *this ; // pour le cas v=v if (dim !=v.dim) { delete[] vect ; dim=v.dim ; vect=new double[dim] ; } for (inti=0 ;i<dim ;i++) vect[i]=v.vect[i] ; return *this; } CVecteur CVecteur::operator+(const CVecteur& v) // retourne un vecteur somme du vecteur courant et de v s’ils ont même dimension, // lance l’exception ErreurDimension sinon { CVecteur aux(dim) ; if (dim !=v.dim) throw(ErreurDimension) ; else { for (int i=0;i<dim;i++) aux.vect[i]= v.vect[i]+vect[i]; } return aux; } CVecteur CVecteur::operator*(double a) //retourne un vecteur égal au produit du vecteur courant par a { CVecteur aux(dim) ; for (int i=0;i<dim;i++) aux.vect[i]= a*vect[i]; return aux; } double CVecteur::operator*(const CVecteur& v) // retourne le produit scalaire du vecteur courant par v s’ils ont même dimension, // lance l’exception ErreurDimension sinon { double prod=0 ; if (dim !=v.dim) throw(ErreurDimension); else { for (int i=0;i<dim;i++) prod+=v.vect[i]*vect[i]; } return prod; } //--------------------------------------------------------------------------------------------------------------------- // lecture des composantes d’un vecteur (dont la dimension est connue) void CVecteur::Lire(void) // lit sur stdin les composantes du vecteur courant { for (int i=0;i<dim;i++) { cout<<”valeur composante d’indice”<<i<<”?”;

cin>>vect[i]; } } //--------------------------------------------------------------------------------------------------------------------- // fonctions amies d’entrées/sorties sur flots iostream friend istream& operator >> (istream &str, CVecteur &v) ; // surcharge de >> { for (int i=0;i<v.dim;i++) str>>v.vect[i]; return str; } friend ostream& operator << (ostream &str, const CVecteur &v) ; // surcharge de >> { for (int i=0;i<v.dim;i++) str<<v.vect[i]<<’ ‘; Str<<’\n’; return str; }

CUST GMM 1° Année 2004/2005 Cours C++

34

Programme d’essai (sans gestion d’exception) : fichier vecteur.h #include <iostream.h> #include "vecteur.h" void main ( )

{ CVecteur v1 ; CVecteur v2(5,1.0) ;

CVecteur v3(v2) ; v2.Afficher() ; v3.Afficher() ; v1.Afficher() ; v1=v2 ; v1.Afficher() ;

v1.Lire() ; double x ; x=v1*v2 ; cout<<"x="<<x<<’\n’ ;

v3=v2*2.5 ; v3.Afficher() ; x=v2[4] ; cout<<"x="<<x<<’\n’ ;

v2[4]=3.2 ; cout<<v2 ; cout<<v1 ; v3=v2+v1 ; v3.Afficher() ;

} Exemple avec gestion d’exception #include <iostream.h> #include "vecteur.h" void main ( ) { int i ;

CVecteur v1, v2(5,1.0), v3(v2) ; v2.Afficher() ; v3.Afficher() ; v1.Afficher() ; v1=v2 ; v1.Afficher() ;

double x ; x=v1*v2 ; cout<< »x= »<<x<<’\n’ ; v3=v2*2.5 ; v3.Afficher() ;

x=v2[4] ; cout<<"x="<<x<<’\n’ ; v2[4]=3.2 ; v3=v2+v1 ; v3.Afficher() ;

try // début du bloc « sous surveillance » { cout<<"Quelle composante du vecteur v3 voulez-vous ?" ; cin>>i ;

cout<<"v3["<<i<<"]="<<v3[i] ; // peut lancer une exception ErreurIndice } // fin du bloc sous surveillance catch(CVecteur::ErreurIndice) // gestionnaire d’exception

{ cout<<"attention, mauvais indice, recommencez\n" ; cin>>i ;

cout<<"v3["<<i<<"]="<<v3[i] ;

} }

CUST GMM 1° Année 2004/2005 Cours C++

35

Chapitre 4 – Héritage et polymorphisme 1. Héritage

1.1. Définition L’héritage exprime les similitudes entre les classes (partage des attributs, des fonctions et des contraintes dans une hiérarchie). Si Y hérite de X, cela signifie que "Y est une sorte de X" (association IS-A). On dit que Y est une classe fille (sous-classe ou classe dérivée) et que X est une classe mère (super-classe, classe de base).

Principe de substitution (Liksow, 1987) : « il doit être possible de substituer n’importe quelle instance d’une super-classe par n’importe quelle instance d’une de ses sous-classes sans que la sémantique d’un programme écrit dans les termes de la super-classe n’en soit affectée ».

1.2. Spécialisation

La spécialisation est une démarche descendante consistant à capturer les particularités d’un ensemble d’objets non discriminés par les classes déjà identifiées. Elle consiste à étendre les propriétés d’une classe, sous forme de sous-classes plus spécifiques (permet l’extension du modèle par réutilisation).

1.3. Généralisation La généralisation est une démarche ascendante consistant à capturer les particularités communes d’un ensemble d’objets issus de classes différentes. Elle consiste à factoriser les propriétés d’un ensemble de classes sous forme d’une super-classe plus abstraite.

Exemple d’héritage

1.4. Plus précisément… L’héritage est donc une technique permettant de mettre en œuvre le concept de spécialisation/généralisation. (NB : il est possible, mais pas recommandé, d’utiliser l’héritage à d’autres fins). Les classes d’une application peuvent être organisées dans une hiérarchie d’héritage qui correspond à un arbre (plutôt à une forêt) s’il n’y a pas d’héritage multiple et à un graphe sinon (la notion d’héritage multiple ne sera pas développée).

CUST GMM 1° Année 2004/2005 Cours C++

36

Terminologie concernant l’héritage : - B spécialise A - A généralise B - B hérite de A - A est hérité par B - B dérive de A - A est une classe ancêtre de B - B est un A - A est une classe de base de B - B est une classe descendante de A - A est une super-classe de B - B est une sous-classe de A Soient A et B, deux classes. ):,...,:( 11

ann

aA TaTaT = où a

iT est un type. Si B hérite de A alors :

1. ):,...,:,:,...,:( 1111b

nnba

nna

B TbTbTaTaT = où les ai sont les attributs hérités de A et les bi des attributs qui spécialisent B par rapport à A. TB est un sous-type de TA : il a des attributs supplémentaires. 2. Toutes les méthodes définies pour A sont disponibles pour B. On peut dire que tous les B sont des A (relation d’inclusion ensembliste). Remarque : les termes de sous-classe et de super-classe peuvent prêter à confusion :

- une sous-classe a une description et un comportement plus riche - une super-classe a une description et un comportement moins riche

1.5. Principe de substitution

La relation d’héritage est transitive. Si A hérite de B doit pouvoir s’employer partout où A le peut. Tous les descendants d’une classe donnée sont des sous-types. Il est possible de redéfinir des méthodes (tout ou partie) héritées dans les classes « qui héritent » (rend l’héritage sélectif).

1.6. L’héritage en C++ L’héritage en C++ s’exprime lors de la déclaration de la classe fille. class Point { Int x,y ; //Attributs public : //… } class Pixel : public Point { Couleur couleur; public :

enum Couleur {NOIR, ROUGE, BLANC} ; Pixel (int nAbs, int nOrd, Couleur C) ; //Constructeur void colorier(Couleur c=NOIR) ; //Méthode

} ; La classe héritée ne peut pas accéder aux membres privés de la classe de base, i.e. x et y sont inaccessibles. Pour palier ce problème, on peut définir ces attributs comme étant protected. Dans ce cas, toutes les sous-classes pourront accéder aux attributs protected. Néanmoins, la solution la plus propre est de n’accéder qu’aux membres public de la classe de base dans une sous-classe.

CUST GMM 1° Année 2004/2005 Cours C++

37

Constructeurs et destructeurs Les constructeurs par défaut sont invoqués implicitement

Appel des constructeurs et des destructeurs

C hérite de B qui hérite de A. Pour une instance de la classe C, on a :

- à la création, appel de A() puis de B() puis de C() - à la destruction, appel de ~C() puis de ~B() puis de ~A().

Si un constructeur a des arguments, on doit préciser dans le corps des constructeurs des classes dérivées les arguments à lui transmettre. class Point { Int x,y ; //Attributs public : Point(int xx, int xx) : x(xx), y(yy) {}

//… } class Pixel : public Point{ Couleur couleur; public :

Pixel (int xx, int yy, Couleur c) : Point(xx,yy), couleur(c) {} //Constructeur //…

} ;

2. Le polymorphisme Le polymorphisme est l’aptitude qu’ont les objets à réagir différemment à un même message. L’intérêt est de pouvoir gérer une collection d’objets de façon homogène tout en conservant le comportement propre à chaque objet. Une méthode commune à une hiérarchie de classe peut prendre plusieurs formes dans différentes classes. Une sous-classe peut redéfinir (ne pas confondre avec surcharger) une méthode de sa super-classe pour spécialiser son comportement. Le choix de la méthode à appeler est retardé jusqu’à l’exécution du programme.

CUST GMM 1° Année 2004/2005 Cours C++

38

Exemple :

2.1. Les méthodes virtuelles : le polymorphisme en C++

Les méthodes virtuelles sont une implémentation du concept de polymorphisme en C++ et permettent de définir des liaisons dynamiques (à comparer aux liaisons statiques des langages procéduraux classiques). Le choix de la méthode à exécuter n’est fa it qu’au moment de l’exécution et non pas au moment de la compilation. Il suffit de rajouter le mot clé virtual devant la déclaration de la méthode. La méthode peut alors être redéfinie dans chaque sous-classe. Pour obtenir un comportement polymorphe en C++, il faut que les méthodes soient déclarées virtual et que les manipulations se fassent par l’intermédiaire de pointeurs ou de références. class Point { Int x,y ; //Attributs public : virtual void affiche() const {std ::cout<<"x=” <<x <<”, y=” <<y;}

//… } class Pixel : public Point{ Couleur couleur; public :

void affiche() const ; //inutile de répéter virtual //…

} ; void Pixel::affiche() const { Point::affiche() ; //appel de la méthode de la classe de base std::cout << ", couleur=” <<couleur; } Remarque : les méthodes virtuelles seront développées en deuxième année

2.2. Les classes abstraites Une classe abstraite représente un concept abstrait qui ne peut être instancié (e.g. : véhicule, nourriture,…). Son comportement ne peut pas être intégralement implémenté à cause de son niveau de généralisation. Elle sera donc simplement utilisée comme une classe de base dans une hiérarchie d’héritage.

CUST GMM 1° Année 2004/2005 Cours C++

39

Exemple de classe abstraite

Hiérarchie d’héritage correspondante :

2.3. Classes abstraites en C++ Elles sont non instanciables et servent uniquement de cadre de référence (cadre contractuel pour les descendants). En C++, une classe est considérée comme abstraite si son interface contient au moins une méthode virtuelle pure. Une méthode devient virtuelle pure en ajoutant =0 à la fin de sa déclaration. On ne fournit alors pas d’implémentation pour cette méthode. Class Forme { Virtual void affiche()=0 ; //… } Le compilateur C++ interdit d’instancier la classe et vérifie que tous les descendants redéfinissent cette méthode. Les méthodes virtuelles pures peuvent être déclarées protected puisqu’elles ne sont destinées qu’à être redéfinies par les descendants. Remarques :

- une classe ne possédant que des méthodes virtuelles pures est une spécification d’une interface (noms et signatures des opérations imposés pour toutes les classes d’implémentation)

- Il ne faut pas confondre surcharger (fonctions de même nom mais avec des listes de paramètres différentes) et redéfinir (fonction identique à une fonction héritée)

- Il faut déclarer le destructeur virtual dans la classe de base si une des classes descendantes est susceptible d’être manipulée par l’intermédiaire d’un pointeur sur la classe de base. C’est typiquement le cas des classes qui contiennent des méthodes virtuelles.

CUST GMM 1° Année 2004/2005 Cours C++

40

3. L’héritage multiple Le langage C++ supporte l’héritage multiple, c’est-à-dire le fait qu’une classe hérite de plusieurs autres classes. Exemple 1 :

class B {/*…*/} ; class C {/*…*/} ; class D : public B, public C {/*…*/};

Dans ce type de cas (exemple 1), on peut avoir affaire à un problème de nommage : si B et C ont toutes deux un attribut a, la classe D aura deux attributs de même nom, provenant de deux classes différentes. Il faudra alors préfixer ces attributs par le nom de la classe dont ils sont issus afin de pouvoir les distinguer : B::a et C::a Il peut également arriver qu’une classe hérite plusieurs fois de l’un de ses ancêtres. On parle alors d’héritage à répétition. Exemple 2 :

class A {/*…*/}; class B : public A {/*…*/}; class C : public A {/*…*/}; class D : public B, public C {/*…*/};

Dans ce cas, les attributs de la classe A sont dupliqués dans la classe D. Là encore, il est possible de résoudre les ambiguïtés en préfixant le nom de l’attribut par le nom des classes. Par exemple, pour accéder à l’attribut attr déclaré dans la classe A mais correspondant à B, on écrira B::A::attr. Si l’on souhaite éviter la duplication des attributs, on déclare l’héritage virtual ; dans ce cas, une seule copie des attributs de la classe A existe.

class A {/*…*/}; class B : public virtual A {/*…*/}; class C : public virtual A {/*…*/}; class D : public B, public C {/*…*/};

Véhicule terrestre Véhicule marin

Véhicule amphibie

véhicule

B C

D

A

B C

D

CUST GMM 1° Année 2004/2005 Cours C++

41

Chapitre 5 - Les flots d’entrée/sortie en C++ Les flots permettent d’effectuer des E/S aussi bien sur des types prédéfinis que sur des types définis par l’utilisateur.

1- Sorties La sortie sur un flot se fait grâce à l’opérateur <<. Ce dernier est surchargé pour tous les types prédéfinis. La sortie de types définis par l’utilisateur se fait en surchargeant l’opérateur <<. std::cout<<"a="<<a<<’\n’ ; Un flot de sortie est une spécialisation du patron basic_ostream. Pour le type char, la classe correspondante est ostream alors que pour wchar_t, la classe est wostream. Un certain nombre de flots de sortie sont définis dans <iostream> : cout (wcout) pour la sortie standard, cerr (wcerr) pour les erreurs sans buffer et clog (wclog) pour les erreurs.

2- Entrées L’entrée à partir d’un flot se fait grâce à l’opérateur >>. Il fonctionne de la même façon que pour les sorties. Le patron pour les flots d’entrée est basic_istream et est défini dans <istream>. Il existe des méthodes pour lire des caractères dans un flot : get, gcount, getline, ignore et read.

3- Etat d’un flot Les états d’un flot (ostream ou istream) sont définis dans la classe basic_ios (<ios>). On peut tester et manipuler l’état d’un flot à l’aide des opérations suivantes :

Les drapeaux disponibles sont définis dans ios_base (super-classe de basic_ios) :

On peut indiquer que l’on souhaite que la méthode clear lance une exception dans différents états en les précisant par la méthode exceptions de basic_ios.

4- Formatage La gestion du formatage d’un flot se trouve dans les classes basic_ios et ios_base. Un ensemble de méthodes (flags(), setf(), unsetf(), copyfmt()) permet de manipuler des drapeaux gérant l’état du format. Les drapeaux suivants sont disponibles dans ios_base :

CUST GMM 1° Année 2004/2005 Cours C++

42

Pour les flottants, precision() permet de fixer le nombre de chiffres à afficher, width() permet de fixer le nombre de caractères de la sortie et fill() fixe le caractère de remplissage. Manipulateurs Un manipulateur s’insère entre des objets lus ou écrits et modifie l’état du flot. Les manipulateurs se trouvent dans différents fichiers d’en-tête (<ios>, <istream>, <ostream>, <iostream> et <iomanip>). Nous donnons ici brièvement la liste des manipulateurs (leur signification peut se déduire des drapeaux présentés précédemment : boolalpha/noboolalpha, showbase/noshowbase, showpoint/noshowpoint, showpos/noshowpos, skipws/noskipws, uppercase/nouppercase, internal, left, right, dec, oct, hex, fixed, scientific, endl (‘\n’ et flush), ends (‘\0’ et flush), flush, ws (ignore les espaces), resetiosflags/setiosflags, setbase(int), setfill(int), setprecision(int), setx(int). Par exemple : #include <iostream> #include <iomanip> int main( ) { double d=1234.5678 ; std::cout<<std::setprecision(6)<<d<<std::endl; //1234.57 std::cout<<std::setfill(‘#’) <<std::setw(12) <<d<< std::endl; //# # # # 1234.57 return 0; }

CUST GMM 1° Année 2004/2005 Cours C++

43

5- Flots et fichiers

Il est possible de définir des flux entre fichiers. Cette opération est suffisamment courante pour être supportée facilement par la bibliothèque standard. Exemple de programme permettant de copier un fichier dans un autre (les noms des fichiers sont passés sous forme d’arguments de ligne de commande) : #include <fstream> #include <cstdlib> int main(int argc, char*argv[]) { if (argc!=3) return 1 ; std::ifstream from(argv[1]) ; //ouvre le flux de fichier en entrée if (!from) return 1 ; //ouverture de fichier impossible std::ofstream to(argv[2]) ; //ouvre le flux de fichier en sortie if (!to) return 1 ; //ouverture de fichier impossible char ch ; while (from.get(ch)) to.put(ch) ; if (!from.eof() || !to) return 1; //incident en cours de copie return 0; } Un fichier est ouvert pour une entrée en créant un objet de la classe ifstream (flux de fichier d’entrée) avec le nom du fichier comme argument. De la même façon, un fichier est ouvert pour la sortie en créant un objet de la classe ofstream (flux de fichier de sortie) avec le nom du fichier comme argument. Dans les deux cas, on teste l’état de l’objet créé afin de savoir si le fichier a bien été ouvert. Le fichier d’en-tête <fstream> contient les définitions des classes ifstream (flot d’entrée à partir d’un fichier), ofstream (flot de sortie vers un fichier) et fstream (possibilité de lecture et d’écriture). Leur équivalent pour les jeux de caractères étendus existe (wifstream, wofstream et wfstream). Les constructeurs de ces classes prennent en deuxième paramètre le mode d’ouverture (défini dans la classe ios_base) :

L’ouverture du fichier se fait lors de l’appel au constructeur. La fermeture est automatique lors de la destruction de l’objet. Il est toutefois possible de fermer explicitement le fichier en appelant la commande close() sur son flot (fermeture à l’intérieur de la portée dans laquelle le flux a été déclaré).

CUST GMM 1° Année 2004/2005 Cours C++

44

6- Flots et chaines

De même que pour les fichiers, les flots peuvent être attachés à des chaînes de caractères (string). Ces flots sont déclarés dans le fichier <sstream>. Les classes sont istringstream, ostringstream et stringstream (resp. wistringstream, wostringstream et wstringstream pour les caractères étendus). Il existe également des flots pouvant être attachés à des tableaux de caractères (chaînes de caractères de style C). Ils sont déclarés dans <strstream>. Les classes sont istrstream, ostrstream et strstream.

Exemple d’utilisation de ostringstream pour formater des chaînes de messages : string compose (int n, const string &cs) { extern const char *std_message[]; ostringstream ost; ost << ”error(” << n << ”)” << std_message[n] << ”(commentaire de l’utilisateur :” << cs << ’)’ ; return ost.str() ; } Un istringstream est un flux d’entrée qui lit dans une string. Exemple de istringstream : #include <sstream> void word_per_line (const string &s) //imprime un mot par ligne { istringstream ist(s) ; string w ; while (ist >> w) cout << w << ’\n’; } int main() { word_per_line(“toto fait du vélo”); } Remarque : Il est possible de définir des flots qui lisent et écrivent directement dans des tableaux de caractères.

CUST GMM 1° Année 2004/2005 Cours C++

45

Chapitre 6 – La surcharge d’opérateurs Il peut parfois être utile d’utiliser une notation classique pour des opérations entre deux objets. Par exemple pour l’addition de deux vecteurs, v et v’, il est commode d’utiliser le raccourci v+v’. Il s’agit ici de surcharger l’opérateur + pour qu’il soit appliqué entre deux objets de type vecteur. Formellement, la surcharge de l’opérateur + effectuée ici équivaut à la définition d’une méthode : v.operator+(v’) (ce sera d’ailleurs l’appel explicite de la fonction). Attention : un abus peut rendre les programmes incompréhensibles (que donne v+v’ quand v et v’ sont des objets de type véhicule ?) Opérateurs binaires/unaires/membres/non membres Un opérateur unaire , notons- le @, peut être préfixé ou post- fixé, membre ou non membre :

- préfixé : @a est équivalent à : →membre a.operator@( ) →nonmembre operator@(a)

- postfixé : a@ est équivalent à : →membre a.operator@(int)1 →nonmembre operator@(a,int)

Un opérateur binaire , @, est forcément infixé, et peut être membre ou non membre : - a@b est équivalent à : →membre a.operator@(b)

→nonmembre operator@(a,b) Attention :

- l’arité d’un opérateur ne peut être modifiée - il existe certaines restrictions (e.g. l’opérateur = est forcément membre, …)

1- La surcharge de ==

Exemple de surcharge pour la classe complexe. class complexe { double re, im ; public : complexe(double r, double i=0) { /*…*/ } double reelle( ) const {return re ;} double imag( ) const {return im ;} } bool operator==(complexe c1, complexe c2) { return c1.real( )==c2.real( ) && c1.imag( )==c2. imag ( ); } Pour deux complexes, c1 et c2, les opérations permises par cette surcharge sont :

c1==c2 ; 1.0==c1 ; c1==1.0 Dans les deux derniers cas, le constructeur est utilisé pour convertir un double en complexe avant la comparaison. Remarque : la surcharge de l’opérateur + fonctionne de la même façon que celle de ==. 1 Exemple d’opérateur unaire préfixé ou postfixé : ++ ++i se traduit par : operator++( ) : incrémente i et la valeur de l’expression est i après incrémentation i++ se traduit par : operator++(int) : incrémente i et la valeur de l’expression est i avant incrémentation Exemple : {i=3 ; j=++i ;} => i vaut 4 et j vaut 4 {i=3 ; j=i++ ;} => i vaut 4 et j vaut 3

CUST GMM 1° Année 2004/2005 Cours C++

46

2- La copie et l’affectation Soit le code suivant :

complexe c1(1.0,2.0) ; complexe c2=c1 ; //constructeur de copie ó complexe c2(c1) ; c2=c1 ; // affectation

La ligne 2 fait appel au constructeur de copie, qui copie un objet existant, et la ligne 3 fait appel à l’opérateur d’affectation.

1.1. Le constructeur de copie Il existe un constructeur de copie par défaut, mais on peut le modifier lors de la création de la classe. Ici, pour la classe complexe, voici un exemple de constructeur de copies qui a le même comportement que le constructeur de copie par défaut : class complexe { double re, im ; public : //…

complexe(const complexe &c) : re(c.re), im(c.im) {} //…

} Remarques :

- le passage de c par référence permet d’éviter un appel infini du constructeur de copies - on accède aux composants de c (privés) car le contrôle d’accès se fait au niveau de la

classe et non au niveau des objets : c est un complexe, donc on a accès à c.re et c.im. Attention aux attributs pointeurs : le constructeur de copies par défaut copie le pointeur, mais ne fait pas d’allocation mémoire. On se retrouve avec deux pointeurs qui pointent sur la même zone mémoire.

1.2. L’affectation Par défaut, l’opérateur d’affectation réalise l’affectation membre à membre. Là encore, il est suffisant pour le type complexe. Voici un exemple de surcharge de l’opérateur d’affectation pour la classe complexe (même comportementg que l’opérateur par défaut) : class complexe { double re, im ; public : //…

Complexe & operator = (const complexe &c) { if (this != &c) {re=c.re; im=c.im;} return *this;

} //…

}

CUST GMM 1° Année 2004/2005 Cours C++

47

Remarques :

- le test if (this != &c) permet de s’assurer que l’on n’effectue pas une affectation de l’objet sur lui-même.

- L’opérateur = retourne une référence sur l’objet courant afin de permettre les affectations en cascade (c1=c2=c3 ;)

Là encore, une attention toute particulière devra être portée aux attributs pointeurs (l’affectation est réalisée, mais pas l’allocation mémoire).

3- La surcharge de << et de >> Il s’agit ici de surcharger les opérateurs d’entrées/sorties. Dans la suite, ostream désigne un flux de sortie (par exemple cout, sortie standard, généralement l’écran, est une instance de la classe ostream) et istream un flux d’entrée (par exemple, cin, entrée standard, généralement le clavier, est une instance de la classe istream). Exemple : class complexe { double re, im ; public : //…

friend ostream &operator << (ostream &os, const complexe &c); friend istream &operator >> (istream &is, const complexe &c); //…

} ostream &operator << (ostream &os, const complexe &c) {

return os << c.re <<” “ << c.im << ”\n”; } istream &operator >> (ostream &os, complexe &c) {

is >> c.re >> c.im; return is;

} Le mot-clé friend permet de définir en dehors d’une classe des fonctions amies ayant accès à tout le contenu de la classe (même aux parties privées). NB : on peut toujours obtenir le même résultat que celui donné par une fonction amie avec des méthodes membres de la classe ; on utilise les fonctions amies pour des raisons d’efficacité (rapidité). Un exercice possible : réécrire les deux opérateurs sans le mot-clé friend.

CUST GMM 1° Année 2004/2005 Cours C++

48

Chapitre 7 – Les exceptions Diverses erreurs se produisent au cours de l’exécution d’un programme. Plusieurs questions se posent alors : comment gérer l’erreur, où la gérer, le programme doit- il être stoppé ? Les exceptions sont une approche pour la gestion des erreurs.

1- Qu’est qu’une exception ? 1.1. Définition

Le terme exception est un raccourci pour événement exceptionnel. Une exception est un événement se produisant lors de l’exécution d’un programme et qui bouleverse le flot normal d’instructions. Le mécanisme des exceptions est destiné à gérer les erreurs ou les cas exceptionnels (une partie du système n’a pas pu réaliser ce qui lui était demandé). Différents types d’erreurs peuvent générer des exceptions ; chaque exception contient des informations sur l’erreur qui l’a produite. Exemples d’exceptions : erreurs matérielles (crash disk), erreurs de programmation (accès hors des limites d’un tableau), … Les exceptions peuvent être regroupées et organisées en hiérarchie.

1.2. Utilisation et gestion L’action de générer une exception s’appelle lancer (throw) ou lever (raise) une exception. Le système d’exécution doit alors trouver une portion de code sachant gérer (handle) ou capturer (catch) cette exception. Les candidats pour la gestion de l'exception sont les méthodes de la pile d'appel de la méthode ayant levée l'exception : le système d'exécution remonte la pile d'appel jusqu'à trouver un gestionnaire d'exception (exception handler) (chaque gestionnaire d'exception gère un type d'exception particulier). Le gestionnaire d'exception choisi par le système traite l'exception et si aucun gestionnaire approprié n'est trouvé, le système stoppe le programme Remarques :

- une exception peut être redéclenchée (traitement partiel => redéclenchement de l’exception par throw)

- l'ordre des gestionnaires est important (à cause de la hiérarchie des exceptions)

2- Alternatives aux exceptions Lorsqu'une erreur se produit dans une méthode :

1. Terminer le programme → très insastisfaisant dans le cadre d'une librairie (le créateur d’une librairie peut

détecter les erreurs, mais pas décider de la façon de les traiter) 2. Retourner une valeur d'erreur

→ il faut qu'il existe une valeur acceptable (elle peut être difficile à différencier des valeurs standard)

→ la valeur doit être testée à chaque appel 3. Renvoyer une valeur ``légale'' et laisser le programme dans un état ``illégal''

→ Comment détecter l' erreur ?

CUST GMM 1° Année 2004/2005 Cours C++

49

4. Appeler une fonction de gestion d'erreurs → la fonction utilisera l'un des mécanismes précédents

3- Avantages et inconvénients des exceptions

Avantages

- Séparation du code de gestion d'erreurs et du code ''normal'' (évite les ``empilements'' d'instructions conditionnelles, améliore la lisibilité du code)

- Propagation des erreurs en suivant la pile d'appels de méthodes (simplification de la propagation). Les méthodes que l'erreur ne concerne pas n'en tiennent pas compte.

- Regroupement des types d'erreurs (possibilité de gérer ensemble des exceptions de même type ; par exemple, les erreurs concernant un tableau ou un fichier)

ATTENTION : La gestion d'erreurs reste une tâche difficile ! Elle demande toujours de la réflexion, il faut donc l’intégrer tôt dans la conception des programmes. Inconvénients

- Moins structurées qu'une gestion locale (revers de la séparation code de gestion d'erreurs/code normal)

- Moins efficace (coût en termes de temps) - Peut rendre certaines situations complexes

IMPORTANT : Le mécanisme des exceptions offre une alternative aux techniques traditionnelles lorsque celles-ci se révèlent insuffisantes, peu élégantes et susceptibles d'introduire des erreurs.

4- Les exceptions en C++ Une exception est une instance d'une classe dérivée ou non de la classe exception. Une méthode peut traiter, ou spécifier une exception qui peut se produire dans la portée de cette méthode. Une méthode traite une exception si elle fournit un gestionnaire d'exception pour ce type d'exception. Elle spécifie une exception en précisant dans sa signature qu'elle peut la lancer.

4.1. Gestionnaire d’exceptions Un gestionnaire d'exception contient deux types de composants :

- un bloc try - un ou plusieurs blocs catch

Le bloc try Le bloc try englobe les instructions susceptibles de lancer une exception

try { // Instructions

} L'instruction try gouverne les instructions englobées dans le bloc. Il définit la portée des gestionnaires d'exceptions qui lui sont associés. Une instruction try doit être accompagnée d'au moins un bloc catch. Le bloc catch Les blocs catch représentent les gestionnaires d'exceptions proprement dits : un ou plusieurs blocs catch sont placés immédiatement après un bloc try :

CUST GMM 1° Année 2004/2005 Cours C++

50

try { // Instructions } catch ( /* ... */ ) { // Instructions } catch ( /* ... */ ) { // Instructions } // ...

L'instruction catch requiert un unique paramètre :

catch (<type> <variable>) { // Instructions }

<type> représente le type de l'exception (il s’agit généralement d’une référence). <variable> est le nom de la variable utilisée pour référencer l'exception dans le gestionnaire Remarques : - L'argument du catch ressemble à la déclaration d'un paramètre de méthode - Un gestionnaire peut capturer plusieurs types d'exceptions (hiérarchie d'exceptions),

donc l'ordre des gestionnaires est important. On doit systématiquement placer le gestionnaire le plus spécifique en premier.

- Il est possible de capturer toutes les exceptions avec catch(…).

Exemple try { Pile unePile = new Pile(2); unePile.empile("azerty"); unePile.empile("qsdfgh"); unePile.empile("wxcvbn"); Personne unePersonne = (Personne)unePile.depile(); } catch (PileVideException e) { // Traitement de l'exception } catch (PileException e) { // Traitement de l'exception }

Attention : l’ordre des gestionnaires est important.

4.2. Spécification d’exceptions

Une spécification d'exception précise qu'une méthode ne capture pas l'exception considérée mais peut la lancer. Pour spécifier qu'une ou plusieurs exceptions peuvent être lancées par une méthode, on utilise la clause throws dans la signature de la méthode :

typeRetour nomMethode throw (type1Exception, type2Exception,…) { //... Exemple :

public class Pile { // ... public void empile(Object unObjet) throw (PilePleineException) { // ... } public Object depile() throw (PileVideException) { // ... } // ... }

CUST GMM 1° Année 2004/2005 Cours C++

51

4.3. Lancement d’exception

L'instruction throw est utilisée pour lancer une exception. Le mot-clé throw doit être suivi d'un objet.

throw objet;

Une exception peut être relancée à partir d'un bloc catch Exemple :

void empile(Object unObjet) throw (PilePleineException) { if (sommet == contenu.length) throw PilePleineException(); contenu[sommet++] = unObjet; } Object depile() throw (PileVideException) { if (sommet == 0) throw PileVideException(); return contenu[ --sommet]; }

4.4. Les exceptions de la librairie C++

Les exceptions standard font partie d’une hiérarchie de classes dont la racine est la classe exception de la librairie standard définie dans <exception>. Cette dernière définit notamment la méthode virtuelle what qui est destinée à être redéfinie dans les classes dérivées. Cette méthode doit normalement renvoyer un message explicitant l’erreur. Exemple : try { //… } catch (exception &e){ cout << “exception de la bibliothèque standard” << e.what() << ‘\n’; } catch (…){ cout << “autre exception \n”; }

exception

logic_error runtime_error

length_error

domain_error

out_of_range

invalid_argument

range_error

overflow_error

underflow_error

bad_alloc

bad_exception

ios_base ::failure

bad_typeid

bad_cast

CUST GMM 1° Année 2004/2005 Cours C++

52

4.5. Créer des classes exceptions

Définir les classes exceptions est une étape importante de la conception d'un ensemble de classes C++. Il faut d'abord déterminer dans quelles méthodes et sous quelles conditions des exceptions seront lancées. Deux alternatives sont possibles pour choisir le type de chaque exception : utiliser une exception existante ou en créer une nouvelle. Il peut même être nécessaire de créer une hiérarchie d'exceptions. Il reste ensuite à choisir quelle sera la super-classe des exceptions définies (exception ou l'une de ses sous-classes). Exemple de hiérarchie d’exceptions en C++ class PileException : public exception { public : const char * what( ) const throw( ) {return “exception pile”;} } ; class PilePleineException : public PileException { public : const char * what( ) const throw( ) {return “exception pile pleine”;} }; class PileVideException : public PileException { public : const char * what( ) const throw( ) {return “exception pile vide”;} };

CUST GMM 1° Année 2004/2005 Cours C++

53

Chapitre 8 – La généricité Le concept de généricité est introduit en C++ par la notion de modèles ou gabarits (template). Il est possible de paramétrer des classes ou des fonctions pour les rendre indépendantes du type des éléments manipulés.

1- Classes paramétrées C’est un modèle de classe qui est paramétré par un ou plusieurs types (généricité). Ce modèle, souvent appelé patron de classe, est utilisé par le compilateur C++ pour engendrer la classe ad hoc, suivant le paramètre effectif fourni à l’instanciation. Tout se passe à la compilation : il ne reste aucune trace du patron dans le programme exécutable. Il est équivalent de définir un patron et de l’instancier ou d’écrire « à la main » des instances différentes. Néanmoins, on peut fournir une version spécifique du patron afin d’optimiser certains traitements pour des types particuliers. # include <iostream> template <class T> Point { public : Point(T abs, T ord) : x(abs), y(ord) {} void afficher() const ; protected : T x,y ; } template <class T> void Point <T>::afficher() const { std::cout<<x<<", "<<y<<std ::endl ; // << doit être surchargé pour le type T } int main() { Point<int> p1(1,2) ; Point<double> p1(1.5,2.3) ; p1.afficher() ; // 2 entiers p2.afficher() ; // 2 réels return 0 ; } Dans cet exemple, la classe Point est paramétrée par le type des coordonnées, ce qui est précisé par template <class T> . Le type T peut ensuite être utilisé comme n’importe quel autre type dans la classe. Lors de l’instanciation du patron, il suffit de préciser pour quel type le patron va être instancié (ici, int et double). Les arguments d’un patron ne sont pas forcément des types. On peut utiliser des expressions constantes, des chaînes de caractères ou des noms de fonctions. Il n’y a pas de conversion implicite pour les arguments d’un patron. On peut aussi définir des classes qui héritent de classes paramétrées, des classes paramétrées virtuelles, … On peut combiner héritage et patron de classes ou définir des patrons de classes virtuels.

CUST GMM 1° Année 2004/2005 Cours C++

54

2- Fonctions paramétrées

Les fonctions paramétrées peuvent avantageusement remplacer les macro- instructions du C (#define) dans lesquelles il n’y a pas de contrôle de type. Par exemple : # include <iostream> template <class T> T max (T a, T b) {return a<b ?b :a ;} int main() { std::cout <<max(1,3) <<std ::endl ; // instantiation de int max(int, int) std::cout <<max(‘a’,’c’) <<std ::endl ; // instantiation de char max(char, char) std::cout <<max(1,’c’) <<std ::endl ; // ERREUR : ambiguïté T=char ou int ? } De plus, pour manipuler des patrons de classe, la notion de patron de fonction est une solution naturelle. Par exemple, on peut de cette façon, définir des algorithmes manipulant des conteneurs définis comme des patrons de classe (voir partie sur la STL).

CUST GMM 1° Année 2004/2005 Cours C++

55

Chapitre 9 - La librairie standard

7- La librairie standard La librairie standard devrait être rapidement portable sur l’ensemble des plates- formes matérielles et logicielles du fait de sa standardisation, justement. Toutes les conditions sont réunies pour qu’elle influence de façon bénéfique le développement logiciel en C++. Elle est constituée des éléments suivants :

- la librairie C standard - la classe string et les flots d’entrées/sorties (support des jeux de caractères

internationaux et de la localisation des applications) - la STL (Standard Template Library) qui contient un ensemble de conteneurs (liste,

ensemble, …) et d’algorithmes - le support pour le calcul numérique (vecteurs et complexes)

Les fonctions de la librairie standard sont définies dans l’espace de nommage std et présentées comme un ensemble de fichiers d’en-tête. Les fichiers d’en-tête dont le nom commence par un c sont équivalents aux en-têtes de la librairie C standard. Chaque fichie cF qui définit les noms dans l’espace de nommage std correspond à un fichier F.h dans l’espace de nommage global. Remarque : la librairie standard ne s’appuie pas sur les concepts objets à proprement parler. En effet, les objectifs visés lors de sa conception (efficacité, souplesse,…) pouvaient difficilement s’accommoder des notions d’héritage et de polymorphisme. Elle est donc essentiellement basée sur la programmation générique. Par exemple, la plupart des algorithmes sont définis comme des fonctions et non comme des méthodes de conteneurs. Cela leur permet d’être indépendants d’un type de conteneur particulier. Voici la liste des fichiers d’en-tête avec une brève description :

CUST GMM 1° Année 2004/2005 Cours C++

56

CUST GMM 1° Année 2004/2005 Cours C++

57

8- La STL (Standard Template Library) La STL fournit aux programmeurs un ensemble de structures de données classiques (liste, pile, …) ainsi qu’un ensemble d’algorithmes opérant sur ces structures de données. Elle assure que les algorithmes fonctionnent de façon efficace et correcte. La STL est basée sur les patrons de classe. En effe t, les autres approches (pointeur void ou hiérarchie de classes avec des méthodes virtuelles) ne permettaient pas d’obtenir des performances convenables. La figure suivante permet d’illustrer l’intérêt de la STL :

L’axe des i représente les types de données, l’axe des j les structures de données et l’axe des k les algorithmes. Si l’on veut coder tous les cas possibles, il faut i*j*k versions différentes. Si on utilise la généricité, on peut supprimer l’axe des i : il ne faut plus que j*k versions. Si on parvient à faire fonctionner les algorithmes avec toutes les structures de données, il est possible de se ramener à j+k versions. C’est ce que fait la STL. Il y a six composants dans la STL. Les trois principaux sont :

- les conteneurs, qui représentent les structures de données - les algorithmes, qui effectuent les traitements - les itérateurs, qui permettent de parcourir et d’examiner les éléments d’un conteneur.

Un algorithme conçu pour fonctionner avec l’un des itérateurs pourra s’appliquer à tous les conteneurs supportant ce type d’itérateur. Les trois derniers composants de la STL sont :

- les objets fonction, qui encapsulent une fonction dans un objet (permet par exemple de passer un traitement en paramètre d’une fonction),

- les adaptateurs, qui permettent de changer l’interface d’une classe (permet par exemple d’obtenir une pile à partir d’un vecteur),

- les allocateurs, qui gèrent le modèle mémoire de la machine (permet par exemple de gérer la sauvegarde des objets).

2.1. Les conteneurs

Les conteneurs sont implémentés sous forme de patrons de classes. Il en existe deux types :

- les séquences (vector, list, deque) - les conteneurs associatifs (set, multiset, map, multimap)

Les fichiers d’en-tête permettant d’utiliser un conteneur portent le nom du conteneur (e.g. : #include <map> pour le conteneur map)

CUST GMM 1° Année 2004/2005 Cours C++

58

Tableau des conteneurs standard : vector<T> vecteur de taille variable list<T> Liste doublement chaînée deque<T> File à double entrée set<T> Ensemble (au sens mathématique du terme) multiset<T> Ensemble où une mê me valeur peut apparaître plusieurs fois map<T> Tableau associatif de type dictionnaire (couples clés/valeurs, recherche par clés) multimap<T> Tableau associatif où une même valeur de clé peut apparaître plusieurs fois

Exemple d’utilisation de la classe vector

#include <iostream> #include <vector> using namespace std ; int main ( ) { vector<int> v ; for (int i=0 ; i<100 ; i++) v.push_back(i) ; cout<<v[10]<<endl; //10 return 0; }

Exemple d’utilisation de la classe map #include <iostream> #include <string> #include <map> using namespace std ; int main( ) { map< string, string, less<string> > m ; m[“France”] = “Paris”; m[“Espagne”]=”Madrid”; cout<<m[“France”]<<endl; //Paris return 0 ; } Opérations Tableau des membres les plus communs des conteneurs standard. Voir les fichiers d’en-tête (<vector>, <list>,…) pour plus de détails. Certains conteneurs possèdent des méthodes spécifiques. Par exemple la classe list fournit les méthodes :

- splice pour déplacer efficacement des éléments dans la liste - merge pour fusionner deux listes triées - sort pour trier une liste

CUST GMM 1° Année 2004/2005 Cours C++

59

CUST GMM 1° Année 2004/2005 Cours C++

60

2.2. Les itérateurs Les structures de données et les algorithmes sont naturels pour les programmeurs. Ils sont habitués à les manipuler. La notion d’itérateur l’est moins. Néanmoins pour parvenir à appliquer un algorithme à un conteneur quelconque, il faut pouvoir le rendre indépendant de ce conteneur. C’est le rôle des itérateurs. C’est une généralisation de la notion de pointeur. C'est-à-dire qu’ils supportent l’opérateur * pour déréférencer un élément et ++ pour passer à l’élément suivant. Ils permettent d’accéder de façon uniforme aux conteneurs de la STL, aux flots d’E/S ainsi qu’aux tableaux C (voir figure). Chaque conteneur fournit une méthode qui retourne un itérateur sur le premier élément (begin( )) et une méthode qui retourne un itérateur après le dernier élément (end( )).

Remarque : on ne doit pas déréférencer un itérateur qui pointe après le dernier élément (celui renvoyé par end( )) Il existe 5 catégories d’itérateurs (voir figure) : les 3 de la figure plus entrée et sortie que nous ne détaillerons pas. Chaque catégorie représente un « contrat » à remplir par l’itérateur. Sur la figure, la relation i1 -> i2 indique que i1 supporte le comportement de i2 et est plus riche. C'est-à-dire que dans tous les cas où i2 est utilisé, on peut aussi utiliser i1. Le choix du type d’itérateur dépend de l’algorithme que l’on souhaite développer. Par exemple, un parcours

CUST GMM 1° Année 2004/2005 Cours C++

61

simple peut ne nécessiter qu’un itérateur en avant alors qu’un tri peut imposer de choisir un itérateur à accès aléatoire.

Le fichier d’en-tête est <iterator>. Le nom de l’itérateur associé à un conteneur est nom_conteneur::iterator ou nom_conteneur::const_iterator . Les itérateurs monodirectionnels (ou itérateurs en avant) Ce type d’itérateur permet un parcours unidirectionnel d’un conteneur. Il est utilisé par les algorithmes qui ont besoin de faire plusieurs passes. list<int> l; l.push_back(10); l.push_back(45); l.push_back(12); l.push_back(15); for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it) { cout << *it << endl; } Les itérateurs bidirectionnels Pour ces itérateurs, l’opérateur -- est défini et permet de revenir en arrière list<int>::const_iterator it_end = l.begin(); --it_end; for (--it; it != it_end; --it) { cout << *it << endl; } Les itérateurs à accès aléatoire Ces derniers sont plus complexes et supportent toute une gamme d’opérateurs (+,+=,…) template<class IT> void bubblesort(IT debut, IT fin) { bool stop = false; while (!stop) { stop = true; for (IT it = debut; it != fin - 1; ++it) { if (*it > *(it + 1)) { swap(*it, *(it + 1)); stop = false; } } } }

Accès aléatoire (direct)

Bidirectionnel

Monodirectionnel

CUST GMM 1° Année 2004/2005 Cours C++

62

vector<int> v; v.push_back(12); v.push_back(69); v.push_back(5); v.push_back(1); bubblesort(v.begin(), v.end()); for (vector<int>::const_iterator it = v.begin(); it != v.end(); ++it) { cout << *it << endl; } Tableau résumant les types d’itérateurs et les opérations qu’ils supportent catégories Monodirectionnel Bidirectionnel A accès aléatoire Ecriture *p= *p= *p= Itération ++ ++, -- ++, --, +, -, +=, -= Comparaison = =, != = =, != = =, !=, <, >, >=, <=

2.3. Les algorithmes Ils représentent les traitements que l’on effectue sur les conteneurs. Ils sont paramétrés par un type d’itérateur ce qui permet de les rendre indépendants des conteneurs. Ce sont donc des algorithmes génériques. Ils sont implémentés sous la forme de patrons de fonctions. On les trouve dans le fichier <algorithm>. Remarque : ils fonctionnent également avec les pointeurs C. En effet, les pointeurs ont les fonctionnalités des itérateurs à accès aléatoire. Les algorithmes se décomposent en quatre groupes :

- les traitements qui ne modifient pas la séquence des éléments (count, for_each,…) - les traitements qui modifient la séquence des éléments (copy, reverse,…) - les tris (sort, merge,…) - les traitements numériques (accumulate,…)

Remarque : les algorithmes portant le suffixe _copy créent une nouvelle séquence. Les algorithmes portant le suffixe _if s’utilisent avec un prédicat (objet fonction qui retourne un booléen) #include <iostream> #include<algorithm> #include<iterator> int main( ) { int T[0]={1,2,5,9,7,3,5,4,7,8}; std::sort(&T[0], &T[10]); //Rq : la fin est positionnée un élément après le dernier. std::copy(&T[0], &T[10], std::ostream_iterator<int>(std::cout, “,”)) // affichage : 1,2,3,… return 0; } Tableau des différents algorithmes possibles.

CUST GMM 1° Année 2004/2005 Cours C++

63

CUST GMM 1° Année 2004/2005 Cours C++

64

9- Les flots Les flots permettent d’effectuer des E/S aussi bien sur des types prédéfinis que sur des types définis par l’utilisateur.

3.1. Sorties La sortie sur un flot se fait grâce à l’opérateur <<. Ce dernier est surchargé pour tous les types prédéfinis. La sortie de types définis par l’utilisateur se fait en surchargeant l’opérateur <<. std::cout<<"a="<<a<<’\n’ ; Un flot de sortie est une spécialisation du patron basic_ostream. Pour le type char, la classe correspondante est ostream alors que pour wchar_t, la classe est wostream. Un certain nombre de flots de sortie sont définis dans <iostream> : cout (wcout) pour la sortie standard, cerr (wcerr) pour les erreurs sans buffer et clog (wclog) pour les erreurs.

CUST GMM 1° Année 2004/2005 Cours C++

65

3.2. Entrées L’entrée à partir d’un flot se fait grâce à l’opérateur >>. Il fonctionne de la même façon que pour les sorties. Le patron pour les flots d’ent rée est basic_istream et est défini dans <istream>. Il existe des méthodes pour lire des caractères dans un flot : get, gcount, getline, ignore et read.

3.3. Etat d’un flot Les états d’un flot (ostream ou istream) sont définis dans la classe basic_ios (<ios>). On peut tester et manipuler l’état d’un flot à l’aide des opérations suivantes :

Les drapeaux disponibles sont définis dans ios_base (super-classe de basic_ios) :

On peut indiquer que l’on souhaite que la méthode clear lance une exception dans différents états en les précisant par la méthode exceptions de basic_ios.

3.4. Formatage La gestion du formatage d’un flot se trouve dans les classes basic_ios et ios_base. Un ensemble de méthodes (flags(), setf(), unsetf(), copyfmt()) permet de manipuler des drapeaux gérant l’état du format. Les drapeaux suivants sont disponibles dans ios_base :

Pour les flottants, precision() permet de fixer le nombre de chiffres à afficher, width() permet de fixer le nombre de caractères de la sortie et fill() fixe le caractère de remplissage.

CUST GMM 1° Année 2004/2005 Cours C++

66

Manipulateurs Un manipulateur s’insère entre des objets lus ou écrits et modifie l’état du flot. Les manipulateurs se trouvent dans différents fichiers d’en-tête (<ios>, <istream>, <ostream>, <iostream> et <iomanip>). Nous donnons ici brièvement la liste des manipulateurs (leur signification peut se déduire des drapeaux présentés précédemment : boolalpha/noboolalpha, showbase/noshowbase, showpoint/noshowpoint, showpos/noshowpos, skipws/noskipws, uppercase/nouppercase, internal, left, right, dec, oct, hex, fixed, scientific, endl (‘\n’ et flush), ends (‘\0’ et flush), flush, ws (ignore les espaces), resetiosflags/setiosflags, setbase(int), setfill(int), setprecision(int), setx(int). Par exemple : #include <iostream> #include <iomanip> int main( ) { double d=1234.5678 ; std::cout<<std::setprecision(6)<<d<<std::endl; //1234.57 std::cout<<std::setfill(‘#’) <<std::setw(12) <<d<< std::endl; //# # # # 1234.57 return 0; }

3.5. Flots et fichiers Il est possible de définir des flux entre fichiers. Cette opération est suffisamment courante pour être supportée facilement par la bibliothèque standard. Exemple de programme permettant de copier un fichier dans un autre (les noms des fichiers sont passés sous forme d’arguments de ligne de commande) : #include <fstream> #include <cstdlib> int main(int argc, char*argv[]) { if (argc!=3) return 1 ; std::ifstream from(argv[1]) ; //ouvre le flux de fichier en entrée if (!from) return 1 ; //ouverture de fichier impossible std::ofstream to(argv[2]) ; //ouvre le flux de fichier en sortie if (!to) return 1 ; //ouverture de fichier impossible char ch ; while (from.get(ch)) to.put(ch) ; if (!from.eof() || !to) return 1; //incident en cours de copie return 0; } Un fichier est ouvert pour une entrée en créant un objet de la classe ifstream (flux de fichier d’entrée) avec le nom du fichier comme argument. De la même façon, un fichier est ouvert pour la sortie en créant un objet de la classe ofstream (flux de fichier de sortie) avec le nom du

CUST GMM 1° Année 2004/2005 Cours C++

67

fichier comme argument. Dans les deux cas, on teste l’état de l’objet créé afin de savoir si le fichier a bien été ouvert. Le fichier d’en-tête <fstream> contient les définitions des classes ifstream (flot d’entrée à partir d’un fichier), ofstream (flot de sortie vers un fichier) et fstream (possibilité de lecture et d’écriture). Leur équivalent pour les jeux de caractères étendus existe (wifstream, wofstream et wfstream). Les constructeurs de ces classes prennent en deuxième paramètre le mode d’ouverture (défini dans la classe ios_base) :

L’ouverture du fichier se fait lors de l’appel au constructeur. La fermeture est automatique lors de la destruction de l’objet. Il est toutefois possible de fermer explicitement le fichier en appelant la commande close() sur son flot (fermeture à l’intérieur de la portée dans laquelle le flux a été déclaré).

3.6. Flots et chaines De même que pour les fichiers, les flots peuvent être attachés à des chaînes de caractères (string). Ces flots sont déclarés dans le fichier <sstream>. Les classes sont istringstream, ostringstream et stringstream (resp. wistringstream, wostringstream et wstringstream pour les caractères étendus). Il existe également des flots pouvant être attachés à des tableaux de caractères (chaînes de caractères de style C). Ils sont déclarés dans <strstream>. Les classes sont istrstream, ostrstream et strstream.

Exemple d’utilisation de ostringstream pour formater des chaînes de messages : string compose (int n, const string &cs) { extern const char *std_message[]; ostringstream ost; ost << ”error(” << n << ”)” << std_message[n] << ”(commentaire de l’utilisateur :” << cs << ’)’ ; return ost.str() ; } Un istringstream est un flux d’entrée qui lit dans une string.

CUST GMM 1° Année 2004/2005 Cours C++

68

Exemple de istringstream : #include <sstream> void word_per_line (const string &s) //imprime un mot par ligne { istringstream ist(s) ; string w ; while (ist >> w) cout << w << ’\n’; } int main() { word_per_line(“toto fait du vélo”); } Remarque : Il est possible de définir des flots qui lisent et écrivent directement dans des tableaux de caractères.

10- Le calcul numérique La librairie standard propose des classes permettant de réaliser des opérations mathématiques plus évoluées que l’arithmétique de base.

4.1. Limite des types Le fichier <limits> définit le patron numeric_limits dont les spécialisations pour chaque type précisent un certain nombre d’informations sur le type en question (minimum, maximum, …). Exemple : void f(double d, int i) { if (numeric_limits<unsignedchar>::digits !=8) { //octets inhabituels (ne contiennent pas 8 bits) } if (i<numeric_limits<short>::min() || numeric_limits<short>::max()<i) { //i ne peut être stocké dans un type short sans perdre en précision }

if (0<d && d<numeric_limits<double>::epsilon()) d=0; if (numeric_limits<Quad>::is_specialized) { //informations de limites disponibles pour le type Quad } } Chaque implémentation de la bibliothèque standard fournit une spécialisation de numeric_limits pour chacun des types fondamentaux (les types caractère, entier, virgule flottante et bool). Cela ne sera pourtant pas le cas pour d’autres types tels que void, une énumération ou un type de la bibliothèque (complex<double> par exemple).

CUST GMM 1° Année 2004/2005 Cours C++

69

Un exemple d’implémentation possible pour le type float :

class numeric_limits<float> { public : static const bool is_specialized=true ; static const int radix = 2; // base de l’exposant (dans ce cas binaire) static const int digits = 24; // chiffres de la base du nombre dans la mantisse static const int digits10 = 6; // nombre en base 10 dans la mantisse static const bool is_signed = true; static const bool is_integer = false; static const bool is_exact = false; inline static float min( ) throw ( ) {return 1.17549435E-38F;} inline static float max( ) throw ( ) {return 3.40282347E+38F;} inline static float epsilon( ) throw ( ) {return 1.19209290E -07F;} inline static float round_error( ) throw ( ) {return 0.5F;} inline static float infinity( ) throw ( ) {return /*une valeur*/;} inline static float quiet_NaN( ) throw ( ) {return /*une valeur*/;} inline static float signalling_NaN( ) throw ( ) {return /*une valeur*/;} inline static float denorm_min( ) throw ( ) {return min( );} static cons int min_exponent = -125; static cons int min_exponent10 = -37; static cons int max_exponent = +128; static cons int max_exponent10 = +38; static const bool has_infinity = true; static const bool has_quiet_NaN = true; static const bool has_signaling_NaN = true; static const float_denorm_style has_denorm = denorm_absent; //enum dans <limits> static const bool has_denorm_loss = false; static const bool is_iec559 = true; //conforme à IEC-559 static const bool is_bounded = true; static const bool is_modulo = false; static const bool traps = true;

static const bool tinyness_before = true; static const float_round_style round_style = round_to_nearest; //enum dans <limits> }; min( ) est le nombre normalise positif le plus petit et epsilon est le nombre à virgule flottante positif le plus petit, de sorte que 1+epsilon-1 puisse être représenté.

CUST GMM 1° Année 2004/2005 Cours C++

70

4.2. Les fonctions mathématiques standard Les en-têtes <cmath> et <math.h> fournissent ce qui est communément nommé les fonctions mathématiques usuelles :

Fonctions mathématiques standard double abs(double) ; valeur absolue (absente en C) double fabs(double) ; valeur absolue double ceil(double d) ; plus petit entier non inférieur à d double floor(double d) ; plus grand entier non supérieur à d double sqrt(double d) ; racine carrée de d, d doit être positif

double pow(double d, double e) ; d à la puissance e erreur si d = = 0 et e<=0 ou si d<0 et e n’est pas entier

double pow(double d, int i); d à la puissance i (absent en C) double cos(double) ; cosinus double sin(double) ; sinus double tan(double) ; tangente double acos(double) ; arc cosinus double asin(double) ; arc sinus double atan(double) ; arc tangente double atan2(double x, double y) ; atan(x/y) double sinh(double) ; sinus hyperbolique double cosh(double) ; cosinus hyperbolique double tanh(double) ; tangente hyperbolique double exp(double) ; exponentiel base e double log(double d) ; logarithme naturel (base e), d doit être >0 double log10(double d) ; logarithme base 10, d doit être >0

double modf(double d, double *p) ; retourne la partie fractionnaire de d, place la partie intégrale dans *p

double frexp(double d, int *p) ; cherche x dans [.5,1) et y tel que d=x*pow(2,y) retourne x et stocke y dans *p

double fmod(double d, double m) ; reste à virgule flottante, même signe que d double ldexp(double d, int i) ; d*pow(2,i) Les en-têtes <cmath> et <math.h> fournissent ces fonctions pour les arguments float et long double. Lorsque plusieurs valeurs (par exemple pour asin( )) sont acceptables, la plus proche de 0 est retournée. Le résultat de acos( ) est positif. Remarque : certaines fonctions mathématiques se trouvent dans l’en-tête <cstdlib> et non dans <cmath>

CUST GMM 1° Année 2004/2005 Cours C++

71

4.3. Les vecteurs La classe vector de la STL n’est pas conçue pour effectuer des calculs sur ces éléments. Aussi, la classe valarray a été ajoutée. Elle supporte les opérations mathématiques les plus communes. On peut considérer seulement un sous-ensemble d’un valarray à l’aide de la classe mask_array. Un valarray peut être affecté à un autre valarray de même taille : v1=v2 copie chaque élément de v2 dans la position correspondante de v1. Si les tailles sont différentes, le résultat de l’affectation est indéfini. Il est également possible d’ajouter une valeur scalaire à un vecteur : v=7 attribue la valeur 7 à chaque élément du valarray v. Exemple : #include <iostream> #include <valarray> int main( ) { std::valarray <double> v1(2), v2(2); v1[0] = 1.2; v1[1] = 2.3; v2[0] = 5.1; v2[1] = 9.1; std::valarray<double> v = v1*3.0+v2/v1; std::cout << v[0] << ”, ” << v1[1] << std::endl; return 0; }

4.4. Les matrices La classe valarray représente un vecteur à une dimension. Néanmoins, il est possible de l’utiliser pour manipuler des matrices de dimensions quelconques en utilisant les classes slice, slice_array ou gslice.

• Un slice est une abstraction qui permet de manipuler efficacement un vecteur sous la forme d’une matrice de dimension arbitraire. Un slice représente tout n-ième élément d’une partie d’un valarray.

• Un slice_array permet de faire référence aux sous-ensembles du tableau décrit par un slice.

• Un gslice est un « slice généralisé » qui contient toutes (presque) les informations contenues dans n slice.

4.5. Les nombres complexes

La librairie définit un patron pour les complexes (complex défini dans <complex>) pouvant être instancié avec des types flottants. Les opérations unaires et binaires habituelles (+, -, *, /, = =, et !=) sont définies pour les complexes, de même que les fonctions de coordonnées, les fonctions mathématiques et les flots d’entrées/sorties (voir aide de C++ ou Stroustrup)

CUST GMM 1° Année 2004/2005 Cours C++

72

Exemple : #include <iostream> #include <complex> int main( ) { std::complex<double> dc1(1.2, 2.3), dc2(5.6, 8.4), r; r = dc1 + std::sinh(dc2); std::cout << r <<std::endl; return 0; }

4.6. Les nombres aléatoires La bibliothèque standard fournit dans les en-têtes <cstdlib> et <stdlib.h> une base simple pour la génération des nombres aléatoires. Exemple : #define RAND_MAX implementation_defined /*entier positif très grand*/ int rand( ) ; // nombre pseudo-aléatoire entre 0 et RAND_MAX int srand(int i) ; // alimente le générateur de nombres aléatoires par i (débute une nouvelle séquence de nombres aléatoires à partir de i. l’expression (double(rand( ))/RAND_MAX)*n donne souvent de bons résultats pour l’obtention d’un nombre aléatoire entre 0 et n-1. Pour avoir une valeur réellement aléatoire avec srand, choisir la valeur d’initialisation à partir de bits issus de l’horloge en temps réel, par exemple. Pour un débogage, initialiser systématiquement avec la même valeur de i permet de reproduire invariablement la même série. Remarque : voir aussi la fonction Randomize…