Upload
trinhdan
View
227
Download
5
Embed Size (px)
Citation preview
DU PROCEDURAL A L'OBJET : LES LANGAGES C ET C++
Méthodologie, grammaire, sémantique, syntaxe
Jacques PHILIPP
9 janvier 2009
AVANT PROPOS
L'informatique à considérablement évolué depuis son origine dans son utilisation. Bien évidemment, les méthodes de programmation ont du être adaptées pour intégrer les nouveaux usages : internet, téléphonie mobile, chat, etc., basés tous essentiellement sur la technologie objet, apparue à la fin des années 1970…
Or, cette dernière est essentiellement basée sur une évolution progressive et naturelle de la programmation procédurale structurée. Ainsi, le langage C traditionnel de Kernighan et Ritchie (1972), considéré alors comme un langage « d'assemblage de haut niveau » est normalisé (1986), ce qui a apporté à ce langage à la fois :
• plus de rigueur tout en préservant ses acquits à savoir la syntaxe d'un langage de haut niveau (Pascal, Fortran, PL/1) avec la puissance d'un langage de bas niveau (assembleur),
• une évolution sémantique importante avec l'intégration des concepts de type et de prototype d'objet structurée doté de ses traitements. Ainsi, le système d'exploitation Unix, écrit en C considère un fichier comme un objet sur lequel des traitements peuvent être réalisés (lecture, écriture) au travers d'interfaces adéquates selon son type (fichier usuel, clavier, écran, réseau, etc.).
L'utilisation du langage C comme langage à objet est donc possible, même si sa syntaxe rend ce type de programmes très complexe, d'où les méthodes de programmation actuelles (langages de navigation et de description d'objets tels HTML et XML), gestion des systèmes client-serveur (Javascript, CLI,…) à la fois orientée objet (Java, C++, etc.) mais également utilisant des langages procéduraux (C, PHP, etc.).
Des rappels des notions fondamentales d'algorithmique (complexité, preuves de programmes, méthodes récursive et itératives) sont présentés dans la première partie de l'ouvrage.
La sémantique des concepts de programmation procédurale et structurée (programme, instruction, structures de contrôle, fonctions, procédures, structuration des données…), développés à partir des années 70, est présentée dans la deuxième partie de l'ouvrage, complétée par la présentation normalisée de la grammaire du langage C. La structuration des données est devenue une des bases de la technologie objet car une donnée est devenue un objet typé. Cette évolution sémantique permet de décrire le langage C dans l'esprit de la programmation objet. Sont également mis en évidence la puissance de ce langage et ses limites.
Le langage C++, conçu comme un sur ensemble du C par intégration de compléments fonctionnels (références) et objets (classe, prototype d'objet, surdéfinition, modèle d'objet, gestion des exceptions, héritage, objets virtuels, espace de nommage, bibliothèques de classes), est présenté dans la troisième partie de l'ouvrage.
Public : étudiants des 1er et 2nd cycles universitaires, élèves ingénieurs, ingénieurs informaticiens, BTS, IUT…
TABLE DES MATIERES
CHAPITRE I : LE GENIE LOGICIEL 11
1. De l'art de la programmation au génie logiciel 11
2. Développement d'un logiciel 15
CHAPITRE II : ALGORITHMIQUE ELEMENTAIRE 19
1. Définitions 19
2. Mise au point et preuve d'un algorithme 19
3. Complexité, problèmes NP-complets 20
4. Comportement asymptotique 20
5. Enoncés de problèmes 21
6. Méthodes de conception d'algorithmes 22
7. Structures de contrôle et méthodes itératives 26
8. Récursivité 32
CHAPITRE III : BASES DE PROGRAMMATION EN C ET C++ 39
1. Quelques règles d'or en programmation 39
2. Grammaire, syntaxe, sémantique 40
3. Mots clés 41
4. Instruction, action 41
5. Structures de contrôle : boucles, tests, branchements 45
6. Fonctions, procédures, bibliothèques 51
7. Représentation interne des objets de base des langages C et C++ 53
8. Accès à un objet 60
9. Structures de données abstraites 63
10. Exercices 68
CHAPITRE IV : LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS 69
1. Popularité du langage C et des langages dérivés 69
2. Esprit du langage C 70
3. Principes généraux du langage 71
4. Variables 73
5. Types 75
6. Variables qualifiées constantes 82
7. Opérateurs 84
8. Fonctions et procédures 91
9. Pointeurs 96
10. Arguments de fonctions et pointeurs 101
6 DU PROCEDURAL A L'OBJET : LES LANGAGES C ET C++ ───────────────────────────────────────────────────
11. Tableaux et pointeurs 101
12. Chaînes de caractères et pointeurs 106
13. Applications des pointeurs 108
14. Fonction à nombre variable d'arguments 110
15. Structures de données abstaites et objets structurés 113
16. Unions 123
17. Type énuméré 125
18. L'instruction typedef 126
19. Exercices 127
CHAPITRE V : BASES DE LA PROGRAMMATION ORIENTEE OBJET 139
1. Classes 139
2. Approche objet, donnée abstraite, encapsulation 142
3. Initialisation des objets 143
4. Polymorphisme, surdéfinition et généricité 144
5. Collections d'objets. 145
6. Principes généraux de protection des données 145
7. Abstraction et encapsulation en C et C++ 146
CHAPITRE VI : LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL 151
1. Introduction 151
2. Objets de base 152
3. Entrées/sorties élémentaires en C++ 153
4. Instruction et expression 156
5. Fonctions et procédures 159
6. Référence 163
7. Exercices 166
CHAPITRE VII : CLASSES EN LANGAGE C++ 169
1. Rappels 169
2. Définitions 169
3. Qualification d'accès aux membres d'une classe 172
4. Méthode 174
5. Le pointeur this 175
6. Méthode spécifiée constante 175
7. Pointeur sur les membres d'une classe 177
8. Exercice 177
TABLE DES MATIERES ───────────────────────────────────────────────────
7
CHAPITRE VIII : INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQUES 179
1. L'initialisation des variables en langage C 179
2. L'initialisation des instances en langage C++ 184
3. Constructeur 185
4. Destructeur 190
5. Gestion dynamique de la mémoire en langage C 191
6. Gestion dynamique de la mémoire en C++ 193
7. Objets statiques en langage C++ 197
8. Exercices 201
CHAPITRE IX : SURDEFINITION DES OPERATEURS 211
1. Généralités et syntaxe 211
2. Surdéfiniton d’un opérateur non membre d'une classe 212
3. Surdéfinition d’un opérateur membre d'une classe 213
4. Amitie et levée partielle de l'encapsulation 214
5. Opérateurs relationnels 215
6. Opérateur de transtypage 216
7. Opérateur [] 219
8. Constructeur copie 221
9. Affectation 223
10. Incrémentation et décrémentation 225
11. Gestion dynamique de la mémoire 226
12. Classe encapsulee ou imbriquée 227
13. Déréférenciation, référence, sélection de membre 229
14. Opérateur fonctionnel 230
15. Exercices 231
CHAPITRE X : L'HERITAGE EN LANGAGE C++ 239
1. Définitions 239
2. Qualifications d'accès aux objets d'une classe 241
3. Qualification d'héritage 241
4. Constructeur dans les classes dérivées 248
5. Héritages multiples 249
CHAPITRE XI : METHODES VIRTUELLES, LIAISON DYNAMIQUE, POLYMORPHISME 253
1. Méthodes virtuelles et liaison dynamique 253
2. Méthode virtuelle pure - classe abstraite 255
3. Récapitulation des règles de dérivation 259
8 DU PROCEDURAL A L'OBJET : LES LANGAGES C ET C++ ───────────────────────────────────────────────────
CHAPITRE XII : LES MODELES GENERATEURS D'OBJETS (TEMPLATE) 261
1. Le préprocesseur du langage C 261
2. Les modèles générateurs d'objet 264
3. Modèle d'argument 265
4. Modèle de fonction, de classe, de méthode 266
5. Instanciation d'un modèle 270
6. Modèle spécialisé 273
7. Métaclasse modèle (template template) 277
8. Modèles de constantes 278
9. Généricité et méthode virtuelle 279
10. Mot clé typename 281
11. La bibliothèque template 281
12. Fonctions exportées 284
13. Exercices 285
CHAPITRE XIII : ESPACES DE NOMMAGE 291
1. Espace nommé et anonyme 291
2. Déclaration using 294
3. Directive using 296
CHAPITRE XIV : REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++ 299
1. Principes sémantiques 299
2. Génération et traitement d'une exception 300
3. Gestionnaire d'exceptions élémentaires 303
4. Exception typée 303
5. Exception et constructeur 305
6. Exception et allocation mémoire 307
7. Hiérarchie des exceptions 307
CHAPITRE XV : TYPES DYNAMIQUES 309
1. Identification dynamique d'un type 309
2. Transtypages en langage C++ 311
CHAPITRE XVI : ANNEXES 315
1. Programmation objet et appel système 315
2. La gestion objet des entrées/sorties 321
3. Quelques appels système 330
4. Génération d'applications et commande make 331
5. Règles de priorité des opérateurs 337
TABLE DES MATIERES ───────────────────────────────────────────────────
9
BIBLIOGRAPHIE 339
INDEX 340
LE GENIE LOGICIEL
1. DE L'ART DE LA PROGRAMMATION AU GENIE LOGICIEL
1.1 De la programmation empirique à Internet Les méthodes de développement ont évolués avec les générations d'ordinateurs.
Génération : 1ère 2ème 3ème 4ème 5ème langage machine Assembleur langage évolué SGBD Internet
■ Programmation, rendement, coûts du développement
En 1972, le rendement moyen d'un programmeur professionnel est de 8 lignes par heure (source Pentagone : The high cost of software). L'écriture de "bons" programmes est très longue et difficile. Les mêmes statistiques chiffrent l'écriture d'une instruction à 75 $, celui de sa mise au point à 4000 $.
■ Evolution historique du profil du programmeur
1950 Il est un initié qui utilise exclusivement le langage machine. 1968 Il est considéré comme un artiste (Knuth). 1975 Il respecte une discipline de programmation non empirique (Dijkstra). 1981 La programmation est devenue une science (D.Gries). 1990 L'ingénierie du logiciel est devenue la règle.
■ Les causes
Les tests et branchements conditionnel conduisant à la rupture du déroulement séquentiel du programme (Von Neumann) ont permis l'écriture de la de la première génération des programmes procéduraux.
La complexification des applications conduisit :
• à supprimer les ruptures de séquence devenues l'ennemie du programmeur car source d'erreurs dues aux branchements erronés,
• à organiser les programmes par une programmation structurée, décrite par cette citation de Boileau "Ce qui se conçoit bien s'énonce clairement et les mots pour le dire arrivent aisément", base des langages structurés (Pascal, C) ou à objet (Smalltalk, Java, C++).
■ Erreurs et preuves de programmes
Tout programmeur doit tenir compte de ce constat d'E. Dijkstra : "Essayer un programme peut seulement indiquer des erreurs mais ne prouve jamais qu'il est juste".
CHAPITRE I
12 CHAPITRE I ───────────────────────────────────────────────────
1.2 Le génie logiciel Le développement du logiciel est une discipline d'ingénierie solidement établie dans ses méthodes et principes.
■ Difficultés de conception des logiciels
Plusieurs facteurs rendent difficile la conception et le développement des logiciels :
• Ils peuvent être d'une très grande complexité, à la limite de l'ingéniosité humaine.
• Aucune loi mathématique ou physique ne délimite l'univers des solutions possibles.
• Les causes de certaines erreurs d'exécution peuvent être aléatoires.
• Une erreur d'exécution d'un programme peut provoquer l'interruption du service fournit par ce dernier.
■ Chaque détail compte
Des erreurs d'apparence anodine peuvent se glisser dans des programmes ainsi que l'illustre le programme en langage C suivant :
if (i = 0) printf("la valeur de i est zéro\n"); else printf('la valeur de i n'est pas zéro\n");
Contrairement à l'intention du programmeur, ce programme imprime "la valeur de i est zéro" quel que soit i car dans la syntaxe du langage C, le symbole = représente l'affectation, pas la comparaison logique (qui s'écrit == ). L'expression i = 0 étant toujours nulle, le test d'arrêt n'est jamais satisfait ce qui provoque un cycle perpétuel indétectable.
■ Une simple question d'argent ?
Le logiciel embarqué dans la navette spatiale américaine, développé par la NASA et dont le prix de revient est estimé à 3300 € par ligne de code, contient encore des erreurs dont la suivante : lors de l'utilisation simultanée de deux claviers, tout caractère saisi est modifié par un "ou logique". La solution retenue a été de n'utiliser qu'un clavier à la fois…
■ Le développeur de logiciels
On pense souvent que le développement de logiciel est une tâche simple, à confier à des ingénieurs débutants. Pourtant, la conception d'ouvrages d'art importants est toujours confiée à des ingénieurs confirmés. Et on comprend mal pourquoi il faudrait opérer différemment pour la conception des logiciels car c'est une tâche qui requiert une grande expertise pour être menée à bien.
■ Valeur marchande d'un logiciel
La valeur marchande d'un logiciel tient autant à son introduction rapide sur le marché qu'à ses qualités propres, le client étant rarement prêt à attendre patiemment qu'un programmeur exceptionnellement doué ait trouvé une solution élégante et gratuite à son problème. Il a tort à long terme, mais il achète le produit du moment.
LE GENIE LOGICIEL ───────────────────────────────────────────────────
13
1.3 Productivité du développement logiciel Les aspects économiques du développement du logiciel sont encore mal maîtrisés.
Le coût du logiciel de fabrication de l'Airbus A320 fut estimé, à partir de celui de l'A310, connu, et de son cahier des charges spécifiques. Les différentes évaluations variaient entre 3 et 12.
Une des difficultés de ce type d'exercice prévisionnel est le manque de précision de la mesure de productivité du logiciel que plusieurs techniques permettent, imparfaitement, de mesurer.
■ Le nombre de lignes de code par programmeur et par jour
La mesure la plus couramment utilisée de la productivité d'une équipe de développement est le nombre de lignes de programme produites par jour. Elle est mauvaise pour au moins deux raisons :
• Un programmeur compétent consacre un temps parfaitement productif à réduire la taille de ses programmes ce qui en diminue la complexité et en augmente la valeur.
• Cette mesure ne fait pas la différence entre la productivité d'une équipe qui réalise un logiciel de 10 000 lignes de code en un an, et celle qui réalise en deux ans un logiciel similaire de 20 000 lignes de code.
Une mesure empirique de la productivité quotidienne d'un programmeur en fonction de la complexité du logiciel est donnée par la formule :
productivité = constante * (taille du programme)1.5
■ Le coût du développement
Une meilleure mesure de la productivité d'une équipe est basée sur le coût du développement d'un logiciel.
Ce dernier prend en compte simultanément :
• sa valeur marchande,
• son évolution probable,
• la complexité intrinsèque du problème résolu,
• le surcoût du au retard éventuel de sa commercialisation, celui de son support et de sa maintenance pendant sa durée de vie,
• la possibilité de le réutiliser.
Le coût est aussi une indication de la difficulté qu'aura la concurrence à reproduire un effort de développement similaire.
14 CHAPITRE I ───────────────────────────────────────────────────
1.4 Qualité du logiciel
■ Critères de qualité
Un logiciel doit être doté des qualités suivantes :
• Validité : aptitude du logiciel à réaliser exactement les tâches de sa spécification.
• Robustesse : aptitude du logiciel à fonctionner dans des conditions anormales.
• Extensibilité : facilité de modification des spécifications du logiciel.
• Ré-utilisabilité : aptitude du logiciel à être réutilisé partiellement ou en totalité.
• Compatibilité : aptitude du logiciel à pouvoir être combiné avec d'autres.
• Efficacité : bonne utilisation des ressources matérielles et logicielles
• Portabilité : facilité d'adaptation du logiciel à différents environnements.
• Vérifiabilité : présence de procédures de test, de déboguage, de recette et de certification.
• Intégrité : aptitude du logiciel à protéger ses différentes composantes (programmes, données) contre des accès ou des modifications non autorisées.
• Ergonomie du logiciel (fonctionnement, utilisation, interprétation des résultats, fiabilité).
■ L'ergonomie
Des défauts peuvent s'avérer fatals comme l'illustre la catastrophe de l'Airbus A320 du Mont Sainte Odile. Cet avion est doté d'un moniteur sur lequel s'affichait alors, selon un mode choisi par le pilote, la vitesse verticale de l'avion en pieds par seconde ou l'inclinaison de sa trajectoire en degrés. Il semble que le pilote ait mal interprété le contenu de l'écran le copilote ayant modifié le mode d'affichage, ou l'inverse. L'avion a ainsi, à leur insu, perdu rapidement de l'altitude ce qui a conduit au crash. Ainsi, un défaut de conception dans l'ergonomie de l'interface homme machine s'est révélé être aussi dangereux qu'un bogue.
■ Le long terme
Le manque de vision à long terme des développements de logiciels est aussi la source de nombreux déboires. Le problème le plus fréquent est causé par l'introduction de limites arbitraires d'adressage ou de précision dont voici deux illustrations.
• Dans les années cinquante, pour économiser l'espace, seuls, les deux derniers chiffres de l'année calendaire étaient stockées. De nombreux programmes développés à cette époque étant toujours utilisés en l'an 2000, il a fallu dépenser des milliards d'euros pour corriger ce fameux "bogue de l'an 2000".
• Le système de défense anti-aérienne Patriot n'a pas intercepté un missile SCUD qui a fait 28 victimes au nord de l'Arabie Saoudite durant la première guerre du Golfe à cause d'un décalage de son horloge, le système ayant été initialement conçu pour intercepter des avions, et non des missiles balistiques plus rapides.
LE GENIE LOGICIEL ───────────────────────────────────────────────────
15
2. DEVELOPPEMENT D'UN LOGICIEL
Les étapes de développement d'un logiciel sont présentées en séquence.
■ Analyse
L'analyse consiste à déterminer les contraintes d'utilisation et d'exploitation du logiciel et les besoins de l'utilisateur. Voici quelques questions typiquement abordées lors de cette phase.
• Type de programme (transactionnel, temps réel, bureautique, etc.).
• Entrées et sorties.
• Matériel et système d'exploitation utilisé.
• Temps de développement et personnel disponible pour développer l'application.
• Extensions futures les plus probables.
• Destinataires (grand public ou professionnels).
■ Spécification
La spécification, réponse formelle aux besoins identifiés à l'analyse, comprend des données chiffrées résultant des spécifications (temps de développement, configurations matérielles et logicielles requises, nombre d'utilisateurs, temps de réponse, entrées et sorties, version préliminaire de la documentation, etc.).
■ Documentation
La documentation fait partie intégrante du développement du logiciel sous la forme d'un document technique normatif de référence formalisant sa conception :
• Architecture générale.
• Découpage en modules avec leurs fonctions respectives, leur interface d'accès et les protocoles de communication utilisés.
• Description des composants logiciels utilisés (bibliothèques, générateurs d'interface ou de requêtes, composants réutilisables, etc.).
2.1 Méthodologie de programmation
■ La méthode des approximations successives
La programmation met en évidence des erreurs de conception nécessitant une modification des modules donc des documents rédigés à l'étape précédente.
■ Programmation et chirurgie
Programmer est long et n'est pas une tâche d'exécutant. Ainsi, les éditeurs les plus performants utilisent le modèle de l'équipe chirurgicale, le chirurgien responsable des parties les plus délicates étant supporté par l'équipe chargée des parties plus simples et de la documentation.
16 CHAPITRE I ───────────────────────────────────────────────────
■ Preuves de programmes
La garantie qu'un programme correspond à ses spécifications et qu'il ne comporte pas d'erreurs en est la preuve mathématique par analyse formelle, réalisée uniquement à partir de spécifications non ambiguës et parfaites. Or, la complexité d'une telle preuve peut être très grande et les logiciels d'analyse formelle limités. Ainsi, il n'existe aucun algorithme pouvant identifier une boucle infinie.
L'analyse formelle n'apporte donc que des aides partielles à la preuve, d'application limitée car imposant une méthodologie de développement très rigide.
■ Jeux d'essais
On se rabat sur des simulations ou tests de validation d'un logiciel dont il faut vérifier l'intégralité des modules. Deux approches sont utilisées :
• des tests ciblés sur les conditions limites comme le fonctionnement d'une procédure de tri sur un tableau de taille nulle ou contenant des entrées identiques.
• Des tests en utilisation réelle, généralement faits par un client enthousiaste ou impatient, réalisés sur des versions préliminaires du logiciel, appelées alpha ou bêta releases. C'est ce qu'a fait à 500 000 exemplaires Microsoft avant la sortie de Windows 95 ce qui n'a pas suffit à éliminer ses bogues.
■ Maintenance et support
La maintenance d'un logiciel consiste à en corriger les bogues, à en étendre les fonctionnalités, et à en assurer le portage sur différentes plateformes matérielles et systèmes d'exploitation. Elle est coûteuse (70% du coût total de développement) et son importance souvent sous-estimée. Ainsi, le logiciel Word, développé par une équipe de quatre personnes, est maintenu par une équipe bien plus nombreuse.
Le support d'un logiciel consiste à assurer un service de formation et d'assistance auprès de la clientèle. Son coût n'est pas non plus négligeable.
2.2 La chaîne de développement
■ Cycle du développement
La conception d'une application doit respecter le cycle suivant :
• Ecriture du programme dans un langage évolué : c'est le code source.
• Transformation du code source en langage machine : c'est la compilation.
• Recherche par l'éditeur de liens dans les bibliothèques des références non satisfaites à la compilation pour la génération du code exécutable.
Divers outils permettent de créer des applications : éditeur de texte, générateur de programmes, analyseur syntaxique et sémantique, outils de génie logiciel et utilitaires.
■ Editeur de textes
Un éditeur de texte permet de modifier des mots d'une ligne ou d'un programme, d'insérer, de modifier, de déplacer, de dupliquer des lignes dans un texte, etc.
LE GENIE LOGICIEL ───────────────────────────────────────────────────
17
■ Mise en forme et analyse syntaxique
Un enjoliveur de programmes met en forme un fichier source.
La vérification de la grammaire d'un programme (source) est effectuée par le compilateur ou préalablement à la compilation par un analyseur syntaxique qui retourne les messages d'erreurs et/ou d'avertissement lorsqu'il détecte des incohérences de syntaxe ou des ambiguïtés. L'utilisation d'un tel outil est recommandée pour éliminer tous les messages d'avertissement (warning) ou d'erreur.
■ Compilation et édition de lien
• L'adressage symbolique associe une adresse en mémoire ou une valeur à un symbole alphanumérique, référencé dans une instruction. Cette référence est interne si le symbole est défini dans le programme, externe sinon.
• Un symbole peut être référencé avant sa définition par une référence en avant.
• Une référence est satisfaite par la définition dans la table des symboles d'une correspondance entre un symbole et son adresse.
• La plupart des compilateurs procèdent en deux phases :
◊ phase 1 : construction de la table des symboles rencontré dans le programme,
◊ phase 2 : achèvement de la table des références en avant.
• L'éditeur de lien cherche dans les bibliothèques les références non satisfaites à ce stade pour construire le fichier exécutable.
■ Langage machine
Le langage machine est spécifique au calculateur. Toutes les instructions en langage machine sont représentées par une suite de 0 et 1 sous la forme :
CO CA AD code condition zone opération d'adressage adresse
� Exemple
Soit l'instruction (codée ici en hexadécimal et en binaire) sur 32 bits
Hexadécimal 10 25 0208 Binaire 0001 0000 0010 1001 0000 0010 0000 1000
■ Outils de génie logiciel
Un débogueur symbolique (debugger) contrôle l'exécution d'un programme et permet l'examen de ses données pour identifier l'origine d'erreurs d'exécution.
La génération de fichiers exécutables peut être réalisée à partir :
• de fichiers source, éventuellement de différents langages,
• de fichiers binaires compilés,
• de fichiers édités de telle sorte que l'on puisse modifier un fichier (source, bibliothèque,...) sans recompiler tout l'ensemble, surtout s'il est important.
18 CHAPITRE I ───────────────────────────────────────────────────
■ Profileur
Un profileur génère des statistiques d'exécution pour identifier d'éventuelles séquences inutilisées (code mort).
■ Outils de test
Les tests sont essentiels au développement et la maintenance d'un logiciel.
• Un test de régression permet de vérifier qu'une modification n'a pas introduit d'erreur donc n'a pas fait régresser le logiciel.
• Un test de couverture contrôle l'intégralité d'un logiciel.
■ Automatiseur et gestionnaire de versions
Deux outils complètent la chaîne : le générateur de fichier exécutable à partir des ses dépendances (fichiers sources, objets, bibliothèques, etc.) présenté en annexe et les gestionnaire des différentes versions d'un logiciel comme SCCS sous Unix/Linux.
Analyse
Editeur de texte
Fichier source
Enjoliveur Vérificateur
Compilateur
Fichier (source) à inclure
Fichier objet Bibliothèques système Bibliothèques utilisateur
Fichier exécutable Débogueur Profileur
Exploitation
Editeur de liens
Automatisation
Archiveur
ALGORITHMIQUE ELEMENTAIRE
1. DEFINITIONS
Le mot algorithme dérive du nom du mathématicien Mohammed Al-Khoarizmi, auteur du traité d'arithmétique : "Règles de restauration et de réduction".
Un problème calculatoire est une relation binaire entre un ensemble d'entrées (données) et un ensemble de sorties (résultats) qu'il faut identifier.
Un algorithme est une méthode mathématique de résolution d'un problème calculatoire donné.
Un calcul effectif est une suite d'opérations élémentaires effectuées à la main.
2. MISE AU POINT ET PREUVE D'UN ALGORITHME
La mise au point d'un algorithme nécessite d'analyser le problème pour en déduire un énoncé mathématique permettant d'obtenir les résultats recherchés, préalablement à sa programmation
L'algorithme retenu doit avoir les qualités suivantes :
■ Robustesse
Il doit être robuste pour fournir un résultat exact quelles que soient les données.
Tous les cas doivent avoir été prévus de telle sorte qu'il existe toujours un traitement correspondant à la situation du problème (toujours particulier) à résoudre.
■ Preuve de l'algorithme
Il faut au moins vérifier, à défaut de pouvoir le prouver théoriquement, que l'algorithme retenu fournit la solution théorique et définir, préalablement au passage sur ordinateur, un jeu complet d'essais devant représenter l'univers des cas possibles.
■ Programmation d'un algorithme
L'algorithme retenu doit être transcrit dans un langage de programmation, choisi en principe en fonction du problème à résoudre. Cette dernière étape est à priori la plus rapide l'écriture du programme devant être ramenée à une simple traduction de l'algorithme dans le langage de programmation retenu.
Le compilateur ou l'interprète de commandes permet de traduire le problème en langage machine, selon le type du langage choisi, interprété ou compilé.
CHAPITRE II
20 CHAPITRE II ───────────────────────────────────────────────────
3. COMPLEXITE, PROBLEMES NP-COMPLETS
■ Complexité
La complexité d'un problème est le nombre d'opérations nécessaires à sa résolution. Elle doit être évaluée pour déterminer à priori le temps de calcul.
La solution théorique de certains problèmes est inexploitable, comme par exemple celle qui implémente simplement les règles du jeu d'échecs : un ordinateur peut parfaitement battre le champion du monde dans la mesure où il peut faire, de façon itérative, l'inventaire de toutes les solutions, en ...1 siècle de calcul pour les ordinateurs les plus performants actuellement.
L'algorithme retenu doit donc, sans être aberrant, réaliser un compromis entre sa complexité et sa difficulté de mise en œuvre. Souvent, les algorithmes les plus simples sont les plus efficaces.
� Exemple
La résolution d'un système d'équations linéaires par la méthode de Cramer a un temps de calcul très important le calcul d'un déterminant d'une matrice carrée d'ordre n par cette méthode nécessitant n! opérations.
L'inversion d'un système linéaire par cette méthode nécessite donc O((n+1)!) opérations ce qui devient rédhibitoire dès que n est "suffisamment" grand. En comparaison, la méthode de Gauss de résolution de systèmes linéaires par
triangulation ne nécessite que O(n2) opérations.
■ Problèmes NP-complets
Les problèmes dont la complexité n'est pas polynomiale et dont il est impossible de calculer le nombre d'opérations nécessaires à leur résolution sont dits NP-complets.
4. COMPORTEMENT ASYMPTOTIQUE
L'analyse de la performance d'un algorithme permet de déterminer les ressources matérielles nécessaires à son exécution : temps de calcul, mémoire, nombre d'accès disque pour un algorithme de gestion de bases de données opérant sur une grande quantité d'informations, etc.
■ Comportement asymptotique
Le comportement asymptotique d'un algorithme est une borne supérieure caractéristique de ses entrées en O(f(n)), où f est un polynôme en n ou une fonction logarithmique en log(n).
� Exemple
Le nombre d'éléments à trier soumis à un algorithme de tri, les nombres de sommets et d'arêtes pour le parcourt d'un graphe.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
21
L'analyse du comportement asymptotique d'un algorithme distingue trois situations : l'analyse du cas pire, l'analyse en moyenne, l'analyse amortie.
• L'analyse du cas pire détermine une borne supérieure du temps d'exécution.
• L'analyse en moyenne détermine une borne supérieure du temps d'exécution moyen en définissant un espace de probabilités sur l'ensemble des entrées selon leur taille.
• L'analyse amortie fournit des résultats en moyenne sans hypothèse probabiliste. Elle détermine une borne supérieure du temps d'exécution d'une séquence d'opérations algorithmiques dont le temps d'exécution moyen est obtenu par division du temps total par le nombre d'opérations.
■ Algorithmes presque toujours corrects
Un algorithme probabiliste tire des nombres au hasard pour prendre des décisions et fournit un résultat correct avec une probabilité arbitrairement proche mais non égale à 1. Ce concept a permit de déterminer de très grands nombres premiers.
5. ENONCES DE PROBLEMES
■ Recherche d'une valeur dans un tableau
Entrées A, un tableau d'entiers, contenant n éléments, notés (A[0],...,A[n-1]); X, un entier;
Sorties Recherche, s'il existe, du plus petit entier positif i tel que A[i] = X; sinon n.
Algorithme Tous les éléments du tableau sont examinés jusqu'à ce qu'à la première valeur de i telle que A[i] = X ou n si aucun élément de A n'est égal à X.
Cet algorithme est optimal si aucune information sur le contenu de A n'est connue. Si les éléments de A sont triés par ordre de croissant, la recherche est beaucoup plus efficace, avec un temps proportionnel à log(n). En voici une réalisation en langage C :
int recherche_element_tableau(const int a[], int n, int x) { int i; // Tableau a[], dimension n, élément recherché x
for (i = 0; i < n; i++) if (x = = a[i]) return i; return n; }
■ Problème du tri
Entrées Un tableau d'entiers A, contenant n éléments, notés (A[0],...,A[n-1])
Sorties Une permutation σ de {0,...,n-l} telle que la suite (A[σ
0],..., A[σ
n-1]) soit ordonnée par
ordre croissant.
22 CHAPITRE II ───────────────────────────────────────────────────
■ Problème du représentant de commerce
Entrées Un ensemble fini de villes {V
1,...,V
n},
dij la distance entre Vi et Vj pour tout couple (Vi,Vj), i≠j
B >0 la distance effectivement parcourue par le représentant de commerce.
Sorties On cherche une permutation σ de {1,..., n} telle que la visite des villes dans l'ordre indiqué par σ conduit à une distance totale minimale :
d dn i n i iσ σ σ σ1 1 1 1+ ∑≤ ≤ − + ≤ B
Ce problème NP-difficile n'a pas aujourd'hui de solution polynomiale connue.
■ Problème de l'arrêt
Entrées Deux chaînes de caractères, FN et DATA, FN étant l'appellation d'une fonction.
Sorties La réponse à la question: l'exécution de la fonction FN sur les données DATA est-elle terminée ?
Il n'existe aucun algorithme de résolution de ce problème indécidable.
6. METHODES DE CONCEPTION D'ALGORITHMES
6.1 Relativité du choix d'un algorithme • Les conditions et limites d'utilisation d'un algorithme sont relatives à certaines
situations qui doivent être clairement identifiées avant son utilisation.
• Des algorithme différents sont souvent possibles.
• Un algorithme efficace sur un problème dont la complexité (spatiale ou temporelle) est faible peut s'avérer inefficace quand cette dernière croît.
• Les gains apportés par l'emploi d'un algorithme adapté sont beaucoup plus importants que ceux apportés par l'usage d'un ordinateur plus puissant.
• L'augmentation de la puissance des processeurs permet d'obtenir des résultats plus rapidement. Elle ne garantit aucunement qu'ils sont meilleurs.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
23
6.2 L'abstraction procédurale, ancêtre de l'objet L'abstraction procédurale a pour objectif le développement d'applications complexes en minimisant les dépendances entre les modules et la communication entre les équipes de développement.
■ Mécanismes d'abstraction
Les mécanismes d'abstraction simplifient la conception et la réalisation de logiciels en distinguant l'information nécessaire à son utilisation de celle nécessaire à sa réalisation.
L'abstraction représente le fait de considérer à part un élément (qualité ou relation) d'une représentation ou d'une notion en portant spécialement l'attention sur lui et en négligeant les autres.
Une bonne abstraction dissimule une action, quelquefois complexe, derrière une interface simple à utiliser. La difficulté de la conception réside dans son choix.
On distingue l'abstraction procédurale de l'abstraction des données.
■ L'abstraction procédurale
L'abstraction procédurale implémente une action (calcul, action de contrôle, manipulation de données, etc.) dans un programme utilisable sans nécessité de connaître les détails de sa réalisation. Ainsi, l'utilisation d'une procédure de tri n'impose pas la connaissance de l'algorithme de tri utilisé par celle-ci.
■ L'abstraction des données et les structures de données abstraites
Une structure de données abstraite est une organisation de l'information qui en rend l'accès efficace pour certains traitements tel l'index d'un annuaire téléphonique pour une recherche. Ainsi, les applications sont organisées autour de la manipulation de certains types de données : fichiers, processus, fenêtres, transactions, relations, etc.
L'abstraction des données masque leur représentation interne et n'autorise leur manipulation qu'au travers de procédures de traitement spécifiques appelées méthodes, dérivant de leur définition mathématique.
Un type abstrait est constitué d'un ensemble de valeurs et procédures nécessaires à la manipulation des données abstraites (accès, modification).
� Exemple
Dans le système d'exploitation UNIX, un fichier est la représentation abstraite d'un objet sur lequel est effectuée une opération d'entrée/sortie à partir d'une sémantique d'utilisation uniforme (ouverture, lecture, écriture, fermeture), quelle que soit sa nature, données abstraites ou périphériques (Cf. annexe).
■ La Programmation Orientée Objet
L'abstraction des données est une idée très forte issue du génie logiciel de ces vingt dernières années dont l'usage simplifie considérablement l'architecture des applications. Possible et complexe avec un langage procédural comme le C développé historiquement pour implémenter le système Unix, elle a été introduite dans la grammaire des langages orientés objet tels C++ et Java.
24 CHAPITRE II ───────────────────────────────────────────────────
6.3 Partages et invariants L'abstraction procédurale comme l'abstraction des données permettent de respecter un des principes les plus importants du développement logiciel : le principe du partage.
• Aucune partie (programme ou donnée) d'un logiciel ne doit être dupliquée.
• Toute violation de ce principe est une source d'erreurs donc de surcoûts pour sa maintenance car il est fréquent d'oublier de modifier une des multiples occurences d'une donnée ou d'une fonction, créant ainsi des inconsistances.
• Le principe du partage est un excellent guide pour déterminer les bonnes abstractions bases de l'architecture d'un logiciel.
Un invariant est une propriété d'un programme vérifiée à un point donné de son exécution. Ce concept essentiel est à la base des méthodes itératives ou récursives.
6.4 Méthodes par décomposition Dans ce qui suit, le mot problème désigne son sens abstrait de problème calculatoire ou son sens concret de problème calculatoire avec un jeu d'entrées donné.
Un algorithme fait référence au sens abstrait.
La taille d'un problème fait référence au sens concret.
Un sous problème P' d'un problème P est un problème concret correspondant au même problème abstrait que P dont le jeu d'entrées forme une sous partie du jeu d'entrées de P.
On suppose ici que la taille d'un problème peut être caractérisée par un entier n, par exemple le nombre d'élément à trier induit la taille du tableau utilisé par la procédure.
■ Diviser pour régner
La résolution d'un problème complexe se ramène, par décompositions successives, à une suite de sous problèmes dont la résolution est immédiate. Ainsi, un problème P de taille n peut souvent être décomposé en sous problèmes (P
1,...,P
k) de taille plus petite,
de telle sorte que les solutions des sous problèmes (P1,...,P
k) puissent être combinées
efficacement pour fournir une solution du problème P.
Selon ce principe, les sous problèmes (P1,...,P
k) peuvent être à leur tour décomposés,
en sous problèmes suffisamment simples pouvant être résolus directement.
Un algorithme conçu selon ce principe résout un problème par application successive de la même méthode de calcul sur des sous problèmes de taille successivement plus petite. La programmation de tels algorithmes est effectuée généralement par récursion, c'est-à-dire en utilisant des procédures qui s'appellent elles-mêmes. La correction et l'analyse de leur performance s'établissent généralement par induction. La méthode "diviser pour régner" (du latin divide et impera) est donc l'équivalent algorithmique de l'induction mathématique.
■ Programmation dynamique
La programmation dynamique est une méthode informatique de recherche de la solution optimale d'un problème à partir de solutions optimales de sous problèmes.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
25
6.5 Programmation modulaire La décomposition d'un problème en problèmes d'une complexité moindre conduit à la programmation modulaire.
Les critères de modularité suivants ont été définis :
■ Décomposabilité modulaire
Méthode de conception permettant de décomposer un problème en plusieurs sous problèmes dont la solution peut être recherchée séparément.
■ Composabilité modulaire
Méthode favorisant la production d'éléments de logiciels pouvant être combinés librement les uns avec les autres pour produire de nouveaux systèmes, comme par exemple les bibliothèques de programmes.
■ Compréhension modulaire
Méthode de production d'éléments dont chacun peut être compris isolément.
■ Continuité modulaire
Méthode de conception telle qu'une petite modification de la spécification du problème n'amène à modifier qu'un seul (ou peu de) module(s) de l'application.
■ Protection modulaire
Méthode de conception telle que l'effet d'une condition anormale se produisant lors de l'exécution d'un module y reste localisée ou tout au moins ne se propage qu'à quelques modules voisins.
■ Unités modulaires linguistiques
Chaque module abstrait doit correspondre à une unité syntaxique du langage.
■ Peu d'interfaces
Tout module doit communiquer avec un minimum de modules (appel, partage de données) et la quantité d'informations échangées doit être minimale.
■ Interfaces explicites
Les informations échangées entre deux modules doivent être connues et accessibles par chacun.
26 CHAPITRE II ───────────────────────────────────────────────────
7. STRUCTURES DE CONTROLE ET METHODES ITERATIVES
7.1 Test et branchement sur condition
■ Test et sémantique
Un test sélectionne une partie du programme selon un critère logique. En programmation structurée, un test a la forme générale est la suivante :
si <condition logique> alors action1 sinon action2 is
■ Branchement conditionnel
L'instruction correspondant à l'action à exécuter peut être étiquetée.
Forme générale aller_à si < condition logique > alors aller_à instruction_étiquetée
� Exemple 1
lire x si x > 0 alors aller_à impression sinon x = -x is impression : imprimer x
� Exemple 2
Résolution d'une équation du second degré à coefficients réels : soient a,b,c réels. On
cherche x solution de l'équation ax2 + bx + c = 0. Soit delta = b
2 - 4ac.
si delta ≥ 0 alors x=a2
deltab ±− is sinon pas de solution réelle is
Algorithme Lire a,b,c calculer delta = b2 - 4ac si delta < 0 aller_à Delta négatif
x1=a2
deltab +− x2=
a2
deltab −−
aller à Fin du calcul Delta négatif Imprimer "pas de solution". Fin du calcul
Cet algorithme, écrit dans un langage voisin de BASIC, ne tient pas compte des cas particuliers (a = 0, b ≠ 0, c quelconque), (a = b = 0, c ≠ 0 ou c = 0).
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
27
10 lire a,b,c 20 si a = 0 aller_à 200 30 ' cas a 0 40 delta = b*b - 4*a*c 50 si delta > 0 aller_à 100 60 si delta négatif aller_à 400 70 ' delta = 0' 80 x = -b/(2*a) 90 aller_à 500 100 ' delta = positif 110 x1 = (-b + racine(delta))/(2*a) 120 x2 = (-b - racine(delta))/(2*a) 130 aller_à 500 200 ' a = 0' 210 si b = 0 aller_à 300 220 x = -c/b 230 aller_à 500 300 si c = 0 aller_à 330 310 imprimer 'impossible' 320 aller_à 500 330 imprimer 'impossible' 340 aller_à 500 400 imprimer "pas de racine réelle" 500 FIN
Ce programme peu lisible ne fonctionne pas car tous les cas ne sont pas traités. L'algorithme suivant, sans branchement est lisible, clair et efficace.
Début Lire a,b,c. si a = 0 alors
si b≠ 0 alors xc
b= −
sinon si c = 0 alors indétermination sinon impossibilité is is sinon
delta = b ac2 4−
si delta = 0 alors x = − b
a2; imprimer "racine double "
sinon si delta < 0 alors imprimer "pas de racine réelle" sinon
a2
deltabx,
a2
deltabx 21
−−=+−=
imprimer x1, x2 is is is
Fin du calcul
Les différentes parties du programme, effectuant chacune une action différente, apparaissent nettement. La structure de bloc des langages de programmation structurés permet de les implémenter.
28 CHAPITRE II ───────────────────────────────────────────────────
7.2 Généralités sur les méthodes itératives On souhaite éditer le bulletin de salaire des employés d'une entreprise.
■ Méthode 1
Calcul du salaire du 1er employé; édition ... Calcul du salaire du dernier employé; édition. C'est long surtout s'il y a 10.000 employés.
■ Méthode 2
Du 1er jusqu'au dernier employé de la firme Faire Calcul du salaire; édition Recommencer
Ce qui est équivalent à la séquence :
Nombre total d'employés : nombre Pour n = 1 jusqu'à nombre faire // Marque de début de boucle Calcul du salaire de l'employé n; édition // Corps de la boucle FinFaire // Marque de fin de boucle
■ Exécution répétitive d'une action
Une séquence d'instructions représente une action, susceptible d'être répétée pendant l'exécution du programme. C'est la structure de bloc des langages Pascal, C, Java, etc.
Une itération exécute une action un certain nombre de fois, fixé ou non.
Toutes les actions définies entre les marques de début et de fin de boucle constituent le corps de la boucle dont il est nécessaire de prévoir l'arrêt. C'est le test de fin de boucle dont l'importance de la position dans la boucle apparaît ci-dessous.
� Exemple 1
n = 1 calcul : calculer le salaire de l'employé n Edition n = n+1; si n < nombre alors aller en calcul sinon fin is
Le test de fin de boucle étant effectué à la fin de la boucle, celle-ci est exécutée au moins une fois, même s'il n'est pas vérifié.
� Exemple 2
n = 1 test : si n > nombre alors Fin du travail
sinon calcul du salaire de l'employé n et édition; n = n+1; aller à test
is Le test est effectué en début de boucle qui ne s'exécute pas si le test n'est pas vérifié.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
29
7.3 Boucles
■ Indice de boucle
Initialisé préalablement à l'exécution de la boucle, l'indice de boucle ne doit pas être modifié pendant une itération. Incrémenté en fin d'itération d'une valeur appelée pas de boucle, il est comparé à une variable définissant la condition d'arrêt de la boucle. Cette action préalable à toute itération peut éviter une action inutile et parfois néfaste.
■ Boucle à nombre d'itérations fixé et borné
Le nombre d'itérations connu avant l'exécution de la boucle est contrôlé avec l'indice de boucle.
Sémantique Pour <indice variant de valeur initiale à valeur finale > Faire instruction(s) ou action FinFaire
� Application au cas précédent
Pour n = 1 jusqu'à nombre faire Calculer la paie de l'employé n et édition FinFaire
■ Boucles à condition d'arrêt
Le nombre d'itérations étant inconnu avant l'exécution de la boucle, son arrêt est contrôlé par une condition logique d'arrêt (sinon, la boucle devient "perpétuelle").
• Effectué avant l'itération, cette structure de contrôle est la clause Tant que.
• Effectué en fin d'itération, la boucle s'exécute au moins une fois (clause Répéter).
Sémantique des deux boucles Tant que < condition d'arrêt non vérifiée > Faire Répéter Action Action FinFaire Jusqu'à < condition d'arrêt vérifiée >
� Exemple
n = 1 Répéter imprimer n, n*n; n = n+1; Jusqu'à n > 100
■ Equivalence des clauses Tant que et Répéter
Tant que < condition > faire si condition alors Répéter Instruction(s) Instruction(s) FinFaire jusqu'à is
30 CHAPITRE II ───────────────────────────────────────────────────
7.4 Méthodes itératives et approximations successiv es Un programme itératif est basé sur le raisonnement par récurrence :
• On vérifie que l'algorithme est correct pour n petit.
• On suppose qu'il fonctionne à l'ordre n-1 (hypothèse de récurrence) pour démontrer qu'il fonctionne à l'ordre n.
Le résultat recherché est alors obtenu par approximations successives, chaque instruction exécutée s'en rapprochant.
Une méthode générale de conception d'un algorithme itératif est la donc suivante :
• Recherche d'un invariant de boucle (hypothèse de récurrence).
• Si c'est fini, alors sortir de la boucle.
• Se rapprocher de la solution et rétablir l'hypothèse de récurrence si nécessaire.
• Des valeurs satisfaisant l'hypothèse de récurrence doivent initialiser le processus.
� Exemple 1
n! = fac(n)
Hypothèse de récurrence
A l'étape i, fac(i) est supposé connu.
Boucle si i = n alors fini is
// A l'étape i+1 fac(i+1) = (i+1)*fac(i) i = i+1 // Rétablissement de l'hypothèse de récurrence aller_à Boucle
Initialisation i = 1 ; fac(i) = 1
� Exemple 2
xn = p(x,n)
Hypothèse de récurrence
On construit par récurrence la suite (x1, ..., x
i) = (p(x,1) ..., p(x,i)) ∀ i = 1,2,…n
Boucle si i > n alors fini
sinon p(x,i+1) = x*p(x,i) ; i = i+1 aller_à Boucle
is
Initialisation p(x,1) = x
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
31
7.5 Le problème du drapeau Hollandais Soit (a
1,...,a
n) une suite non ordonnée d'entiers à ordonner de la façon suivante :
les entiers de la liste congrus à 0 modulo 3 sont classés à gauche, les entiers de la liste congrus à 2 modulo 3 sont classés à droite, les entiers de la liste congrus à 1 modulo 3 sont classés "au milieu".
■ Méthode de calcul : hypothèse de récurrence et notations
Soient : i l'indice du 1er élément congru à 1 modulo 3, j l'indice de l'élément courant, que l'on va trier, k l'indice du 1er élément congru à 2 modulo 3.
Avant le tri, à l'indice j, on est dans la situation suivante : i-1 nombres congrus à 0 modulo 3 sont triés, en place, j-i nombres congrus à 1 modulo 3 sont triés, en place, n-k+1 nombres congrus à 2 modulo 3 sont triés, en place.
tableau (≡ 0, ≡ 1, x, ,≡ 2 ...) indice 1 i j k …. n états triés triés non triés triés
Soit r = mod(aj, 3) le reste de la division de a
j modulo(3). Alors
si r = 1 alors aj est à sa place is
si r = 2 alors permuter (aj, a
k-1) is
si r = 0 alors permuter (ai, a
j) is
� Recherche d'une hypothèse de récurrence et d'un invariant de boucle.
Test d'arrêt si j = k alors FINI is
Se rapprocher de la solution en préservant l'hypothèse de récurrence Soit a
j, nouvel élément de la liste à trier. Il y a 3 situations à évaluer :
r = 1 L'élément étant placé, l'hypothèse de récurrence reste vérifiée pour le suivant.
si r = 1 alors j = j+1; SUIVANT is
r = 0 On permute (a
i, a
j) ce qui classe aj sans perturbation du tri antérieur.
(≡ 0 ≡ 1 1 ≡ 2) 1 i j k n
L'hypothèse de récurrence est ensuite rétablie par incrémentation de i et j. si r = 0 alors permuter(ai , aj); // Modifie l'hypothèse de récurrence
i = i+1; j = j+1; SUIVANT // Rétablit l'hypothèse de récurrence is
r = 2 On procède également par échange de a
k-1 inconnu avec a
j connu.
permuter(ai, a
k-1); k = k-1 ; SUIVANT
32 CHAPITRE II ───────────────────────────────────────────────────
Initialisation du programme Les valeurs initiales doivent respecter l'hypothèse de récurrence et les conditions initiales. Or, le nombre d'éléments en place au départ est tel que :
i-1 = 0 ⇒ i = 1 ; j-i = 0 ⇒ j = i = 1; n-k+1 = 0 ⇒ k = n+1
� Remarques
• A chaque pas de boucle, d(j,k) diminue de 1.
• Le nombre d'échanges est inférieur à n par cette méthode.
• Il existe une autre hypothèse de récurrence, pas forcément meilleure (ici, boucle Tant Que).
Le programme final obtenu est le suivant :
Initialisation k = n+1; i = 1; j = 1
Tri Tant que j ≠ k Faire r = mod(aj,3) si r = 1 alors j = j+1; is // Aj est à sa place donc au suivant si r = 2 alors permuter (aj, ak-1); k = k-1; is // Permuter ak-1, aj si r = 0 alors permuter (a
j, a
i); i = i+1; j = j+1 is
FinFaire
8. RECURSIVITE
8.1 Fonction récursive Une fonction récursive s'appelle elle même.
� Exemple
La fonction n! (factorielle n) notée fac(n) est définie par :
fac(n)=
>=
0n si 1)-fac(n*n
0 n si 1
Ainsi :
fac(4) = 4*fac(3) // fac(3) inconnu, à calculer = 4*3*fac(2) // fac(2) inconnu, à calculer = 4*3*2*fac(1) // fac(1) fin de la phase descendante = 4*3*2*1*fac(0) // jusqu'à l'obtention du résultat
Il faut donc empiler la suite des valeurs inconnues (ici 4!, puis 3!, puis 2!) jusqu'à une valeur connue (1!) ce qui permet alors de calculer les valeurs inconnues, empilées.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
33
■ Remarques
• Un algorithme récursif peut souvent être formulé d'une façon itérative. La définition récursive est donc un axiome mathématique, pas toujours une stratégie de calcul.
• Il faut impérativement définir une condition d'arrêt pour éviter une descente infinie.
■ Théorème
Un appel récursif provoque la création d'une pile et l'exécution de deux boucles : une boucle descendante jusqu'à l'arrêt, une boucle ascendante pour la remontée.
La complexité d'un algorithme récursif peut donc être très importante.
Démonstration Une définition générale d'une fonction récursive est la suivante :
f(x)=
(1)sinon d(x) o f(b(x))
est vraie c(x)arrêt d'condition la si a(x) ,
L'opérateur o désigne une loi de composition interne quelconque.
Soit (uk) le terme général de la suite générée par les appels récursifs successifs et soit i la valeur de k tel que la condition d'arrêt c(ui) soit vérifiée.
On pose :
uo = x u1 = b(x) = b(uo) … ui = b(ui-1)
L'application de la formule (1) jusqu'à la vérification de la condition d'arrêt conduit à la relation :
f(x) = f(uo) = f(b(uo)) o d(uo) = f(u1) o d(uo) = f(b(u1)) o d(u1) o d(uo) =...= = f(ui) o d(ui-1) o d(ui-2)o...o d(u1) o d(uo) (2)
De plus, le terme de la suite f(uk) peut être calculé avec la formule (1) d'où la relation :
f(uk) = f(b(uk)) o d(uk) = f(uk+1) o d(uk) ∀k = i-1,...,0 (3)
Le calcule de f(x) nécessite d'empiler préalablement la suite (uk)k=1,i, ce qui permet de calculer proche en proche la suite (f(uk))k=i,0 (phase de descente) puis la suite (uk) (phase de remontée).
34 CHAPITRE II ───────────────────────────────────────────────────
Algorithme de calcul // Génération et empilement de la suite (uk) k : = 0 ; u : = x ; empiler(u); Tant que non c(u) faire k : = k+1 ; u : = b(u) ; empiler(u) FinFaire
// Génération et dépilement de la suite f(uk) y : = a(u) ; i = k ; Tant que i est non nul faire dépiler ; y : = y o d(u) ; i : = i-1 ; FinFaire
� Exemple
Calcul récursif de xn
On pose, pour x donné :
xn =
==
sinon1)-(np*x(n)p
1 n si x
xx
On a ici, avec les notations de la formule (1) :
a(y) = d(y) = x ; bx(n) = xn-1
et la loi de composition o est la multiplication usuelle.
Alors :
uo = xn inconnu, empilé u1 = bx(n) = xn-1 inconnu, empilé u2 = bx(n-1) = xn-2 inconnu, empilé ... un-1 = bx(2) = x connu, défini par la condition d'arrêt.
On peut maintenant calculer la suite (yk), ∀k = n-1,n-2,---,0 :
yn-1 = a(un-1) = x yn-2 = yn-1 o d(un-2) = x * x = x2 ... yo = y1 o d(uo) = xn-1 * x = xn
■ Remarques
L'algorithme se simplifie dans les cas suivants : l'applications b est inversible ou d est constante et il n'est pas nécessaire d'empiler la suite (uk), l'une des applications a ou c est constante, la loi de composition * est associative, commutative...
Malheureusement, le compilateur ne le détecte pas toujours.
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
35
8.2 Récursivité et complexité
■ Première formulation récursive de la fonction puissance
De la relation :
xn = x * xn-1
On obtient :
==
sinon1)-np(x,*x1 n si x
)n,x(p
Soit g(x,n) le nombre d'appels récursifs et de multiplications de l'algorithme. On a :
+==
sinon11)-ng(x,1 n si 0
)n,x(g
⇒ g(x,n) = g(x,n-1) + 1 ∀(x,n)
Or g(x,1) = 0 ⇒ g(x,n) = g(n) = n-1 ∀ (x, n)
L'algorithme est de complexité linéaire.
■ Deuxième formulation récursive de la fonction puissance
Cette méthode est la procédure récursive logarithmique. On a :
n)x(x 2
n2n ∀= d'où :
=
=
n de entière partie la ent(n) avec impair,est n si))2
n ent(x,*p(x
pairest nsi)2
n x,*p(x
1 n si x
)n,x(p
Soit g(n) le nombre d'appels récursifs et d'opérations de l'algorithme. On a :
+
+
=
=
impairest n si))2
ng(ent(2
pairest nsi)2
n g(1
1 n si 0
)n(g
Lorsque n est pair, xn est calculé à partir de 2n
x diminuant de moitié la complexité du problème à chaque pas du calcul et le nombre d'appels récursifs à la fonction puissance tend vers log2 (n).
Le cas n impair étant similaire, l'algorithme est de complexité logarithmique.
� Exemple
2 2 2 2 4 2 4 2 16 2 16 2 256 2 256 25625 2 12 12 2 6 6 2 3 3 2= × = × = × = × = × = × = × ×( ) ( ) ( )
36 CHAPITRE II ───────────────────────────────────────────────────
■ Troisième formulation récursive de la fonction puissance
On a la relation triviale :
qpn x*xx = avec p+q = n
Si n est pair, on a :
2
n
2
nn x*xx = ∀n
Si n est impair, alors
2
1n
2
1nn x*x*xx
−−
= ∀n
D'où la définition de la fonction puissance :
=
=
impairest n si ) 2
1-n(x, p * )
2
1-n(x, p*x
pairest nsi) 2
n(x, p * )
2
n(x, p
1 n si x
)n,x(p
Une double évaluation à chaque pas du calcul est évitée en posant :
z = )) 2
nent((x, p avec ent(n) la partie entière de n.
On obtient alors :
==
impairest n si z*z *xpairest nsiz * z
1 n si x )n,x(p
Comme dans le cas précédent, l'algorithme est de complexité logarithmique.
8.3 Suite de Fibonacci
■ Définition
u1 = u2 = 1 un = un-1 + un-2 ∀n >2
■ Méthode récursive
On applique simplement la relation :
u1 = u2 = 1 un = f(n) = f(n-1) + f(n-2) ∀n >2
Le calcul de f(n-1) nécessite de calculer f(n-2) et f(n-3), celui de f(n-2) nécessite de calculer f(n-3) et f(n-4), etc. Le temps de calcul croît donc exponentiellement avec l'exposant puisque le nombre d'appels récursifs g (n) vaut :
g(n) = 2n-1
+ 2n-2
= 3*2n-2
∀n
ALGORITHMIQUE ELEMENTAIRE ───────────────────────────────────────────────────
37
■ Méthode itérative classique
i = 1; p = u1 = p = 1; q = u2 = 1; Faire si i = n + 1 alors Fini is i = i+1; r = p p = p+q ; // Calcul de un q = r FinFaire
Le temps de calcul croît linéairement avec l'exposant.
■ Méthode itérative de Gries
Cet algorithme, d'une complexité en O(log(n)), s'écrit sous la forme matricielle suivante :
==
−−
=
− 1
10111
...)2n(f)1n(f
0111
)1n(f)n(f
n
8.4 Tours de Hanoi Ce problème amusant illustre à la fois l'élégance d'une formulation récursive tout en mettant en évidence :
• sa complexité exponentielle.
• la formulation itérative induite, moins intuitive, finalement beaucoup plus simple et de complexité linéaire.
■ Position du problème
Soient trois tours numérotées T1, T2, T3. On suppose que sur une des tours (T1 par exemple) se trouvent n disques de diamètre respectif Di tels que
Di > Dj, ∀i , j pour 1 ≤ i < j ≤ n .
On a donc la condition :
Dn < Dn-1 < Dn-2 < ... < D1 ∀n
Le problème est de transférer n disques de T1 à T3 avec T2 comme tour intermédiaire, l'ordre initial des disques devant être conservé pendant le transfert et à l'arrivée.
■ Notations
T1 = TO (tour origine) T2 = TI (tour intermédiaire) T3 = TD (tour de destination)
38 CHAPITRE II ───────────────────────────────────────────────────
■ Hypothèse de récurrence sur le nombre de disques transférés.
On suppose que n-1 disques peuvent être transférés de TO à TD en passant par TI ce qui est trivialement vérifié pour n = 2 ou 3.
Il faut démontrer que n disques peuvent être transférés de TO à TD en passant par TI.
� Démonstration
Le transfert de n disques se décompose toujours en deux phases : le transfert des n-1 disques supérieurs puis celui du disque restant.
• L'hypothèse de récurrence justifie le transfert de n-1 disques. Deux cas se présentent : les disques peuvent être transférés sur TD par TI ou l'inverse. Or ce dernier cas est impossible car il imposerait de mettre le disque Dn sur le dessus de
la pile. Les disques sont donc transférés sur TI.
• Le disque restant Dn est transféré sur TD puis les n-1 disques de TI sont à leur tour
transférés sur TD en utilisant TO (justifié par l'hypothèse de récurrence).
Chaque pas de calcul est donc composé des trois étapes :
• Transfert des n-1 disques ordonnés de TO à TI en passant par TD.
• Transfert du disque Dn de TO à TD.
• Transfert des n-1 disques ordonnés de TI à TD en passant par TO.
■ Nombre d'appels récursifs
Cet algorithme effectue deux appels de procédures récursives à chaque pas de calcul. Soit g(n) le nombre d'appels récursifs pour transférer n disques. Alors
g(1) = 1
g(n) = g(n-1) + g(1) + g(n-1) = 2g(n-1) + 1
⇒ g(n) + 1 = 2(g(n-1)+1) = ... = 2n g(1) ⇒ g(n) = 2n - 1
Le nombre d'appels récursif croît exponentiellement avec le nombre de disques.
■ Conclusion sur les traitements récursifs
La formulation récursive n'apporte pas la solution à tous les problèmes mais est néanmoins très utile.
Dans le présent exemple, la méthode récursive de résolution du problème apparaît naturellement dans la démonstration, est élégante et … se révèle être d'une complexité exponentielle. Elle permet surtout de justifier théoriquement la méthode itérative, de complexité linéaire, dont la formulation (simple) et la démonstration (difficile) sont laissées en exercice au lecteur.
BASES DE PROGRAMMATION EN C ET C++
1. QUELQUES REGLES D'OR EN PROGRAMMATION
■ Choix du langage
Le langage n'est que le support de l'algorithme programmé qui en est indépendant. Son choix peut être imposé (disponibilité, stratégie d'entreprise, connaissance préalable du programmeur, etc.) ou être fait selon la nature du problème (calcul scientifique, gestion, application système, programmation Web, etc.). Ainsi, la plupart des logiciels sont développés dans des langages d'usage général tels le C ou le C++. Le langage Fortran est surtout utilisé en calcul scientifique, et Cobol en informatique de gestion, mais les langages C, C++ et Java peuvent aussi être utilisés dans ce type d'applications. Certains développements imposent le langage, par exemple XML pour la programmation de site Web.
■ Maximes
E. Dijkstra a énoncé la maxime suivante : "Essayer un programme peut seulement montrer qu'il contient des erreurs mais ne prouve jamais qu'il est juste". D'où la maxime "Tout programme doit être totalement testé et validé, à chaque modification. Cette action est nécessaire mais non suffisante pour garantir son exactitude".
■ Commentaires et lisibilité
Un programme est destiné à être lu ultérieurement, en général par d'autres. Sa lisibilité est donc essentielle pour le maintenir et le développer.
Il est aussi indispensable de le commenter.
� Exemple
/* Un commentaire en C et C++ */ // Un commentaire en C++
■ Identification des objets
Les noms des objets (variables, fonctions, procédures) doivent être appropriés.
� Exemple
float r; // Que représente le nombre réel r ? float rayon; // C'est mieux
CHAPITRE III
40 CHAPITRE III ───────────────────────────────────────────────────
Chaque traitement effectué par une procédure ou fonction doit être identifié par une entête qui spécifie son action, ses arguments et leur domaine de validité, ses effets de bord éventuels et ses limitations et qui doit respecter un format permettant l'extraction d'un manuel de référence.
Les variables doivent être regroupées par type, thèmes, etc.
Des commentaires doivent indiquer la méthode de résolution choisie, la description des conventions utilisées pour nommer les variables, les fonctions et les procédures utilisées, les bibliothèques appelées, les éventuelles astuces (à éviter par principe).
� Exemple
// Résolution d'une équation du 2° degré // Coefficients de l'équation a, b ,c à valeurs réelles, solution réelles et complexes // Programmeur : tartempion le 10 Janvier 2018 // Variables utilisées : float a, b ,c // etc.
2. GRAMMAIRE, SYNTAXE, SEMANTIQUE
■ Langage et grammaire
Tout langage de programmation est défini par l'utilisation de règles très précises (grammaire) que le programmeur se doit de respecter de façon impérative. Certains langages sont très rigoureux de telle sorte que le programmeur n'ait aucun choix possible à part le bon (langage Pascal). D'autres sont plus permissifs (le C).
Chaque langage a ses propres règles de syntaxe donc de grammaire.
■ Méta langage
Un métalangage est un langage syntaxique de description des différentes opérations (affectation, boucle, etc.) d'un algorithme.
■ Syntaxe
La syntaxe d'une instruction est son mode d'emploi. Elle est vérifiée par le compilateur ou l'interprète de commandes par une analyse syntaxique préalable.
■ Sémantique
La sémantique d'une instruction correspond à la description de l'action qu'elle exécute.
Une instruction doit être correcte à la fois sur les plans syntaxique et sémantique. Ces deux conditions sont nécessaires et non suffisantes pour qu'elle fournisse un résultat exact, une instruction pouvant être syntaxiquement correcte et sémantiquement fausse comme l'instruction en langage C ci-dessous :
if (i = 1)
dont le résultat est l'affectation de la valeur 1 à i et non sa comparaison à la valeur 1.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
41
3. MOTS CLES
Un langage est constitué par un ensemble de mots clés qui représentent les instructions autorisées. Ils ont une syntaxe d'utilisation définie par sa grammaire.
� Exemple
La liste ci-dessous récapitule pratiquement la totalité des mots clés du langage C.
■ Déclarations de type
char double enum float int long short signed sizeof struct typedef union unsigned void
■ Déclarations de la classe de mémorisation
auto extern register static const volatile
■ Structures de contrôle
break case continue default do else for goto if return switch while
4. INSTRUCTION, ACTION
4.1 Objets de base des langages C et C++
■ Objets de base du langage
Tout langage de programmation permet d'accéder à des objets dits "de base". Ainsi, le langage C++ permet, entre autres, à partir des objets scalaires prédéfinis de base (entiers, réels, caractères) de construire des tableaux typés de vecteurs. A chacun de ces objets de base est associé un ou plusieurs types :
Objet déclaration de type Entier naturel int, short, unsigned Entier relatif int, short, signed Rationnel float, double Réel float, double Caractère char Vecteur tableau
■ Opérations sur les objets de base
Les opérateurs du langage permettent de réaliser des opérations sur les objets de base. On distingue :
• les opérateurs arithmétiques +, -, *, /, % (opérateur modulo)
• les opérateurs logiques et, ou , non,
• les opérateurs relationnels, permettant de faire des comparaisons entres objets (relation d'ordre).
42 CHAPITRE III ───────────────────────────────────────────────────
■ Construction d'objet
Un objet complexe peut être construit à partir d'objets de base. Ainsi, en C, un tableau est un ensemble d'objets de même type stockés consécutivement en mémoire dont chaque élément est accessible par un indice relatif au premier élément.
� Exemple
int a[100]; // Un tableau de 100 entiers
Des objets de type composite (tableau, variables structurées, classe, etc.) peuvent être construits.
� Exemples
L'objet structuré individu est décrit par son nom, prenom, adresse.
struct individu {char nom[20] ; char prenom[20] ; char adresse[50] ;}; struct complexe {float x ; float y}; // Une structure complexe
■ Surdéfinition (surcharge)
Certains langages objet permettent de redéfinir le comportement des opérateurs. Ainsi, il est possible en C++ de (re)définir l'opérateur + pour qu'il permette d'additionner des matrices. Ce principe est appelé la surdéfinition des opérateurs.
4.2 Instruction simple Un programme est une suite finie d'instructions (élémentaires ou complexes).
■ Instruction simple
Une instruction opère sur un ou plusieurs opérandes, des variables ou des expressions en utilisant des opérateurs.
Dans les langages C, C++, Java, toute instruction doit se finir par le terminateur ;
On distingue les instructions exécutables et non exécutables.
� Exemple
int a, b; // Non exécutable en C , exécutable en C++ float c = 45.67; a = 12; // Exécutable b = a+c; if ((a > 1) && (b < 5)) ...; else ...;
Une instruction peut appeler des procédures ou des fonctions, prédéfinies ou non.
� Exemple
x = f(a); // Appel de la fonction f g(y); // Appel de la procédure g
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
43
■ Alphabet
Les caractères autorisés dans un langage sont définis par un alphabet. En C, les lettres minuscules, majuscules, les caractères alphanumériques, et certains caractères spéciaux utilisés pour constituer les variables symboliques sont autorisées. L'utilisation des mots clés pour définir les variables symbolique est interdite (en PL/1, c'est autorisé). Enfin, les lettres minuscules et majuscules sont considérées comme différentes.
� Exemple
int a = 1; float A = 6.78; char chaine[128] = "bonjour"; char ceci_est_une_longue_chaine[128] = "anticonstitutionnellement";
■ Définition
La définition d'une variable lui donne naissance.
Selon la grammaire utilisée, il est obligatoire (Pascal ou C) ou facultatif (Fortran) de définir préalablement les variables utilisées.
A chaque objet est associée une variable symbolique du programme, accessible par un identificateur qui doit suivre les règles de grammaire du langage. C'est en général un ensemble de caractères alphanumériques.
� Exemple
// Définition de variables int i; // L'entier i float a,b; // Deux réels a et b char chaine[10] = "bonjour"; // Une chaîne de caractères
■ Déclaration
Une déclaration est indicative. Elle peut être obligatoire (C++). Ainsi, la déclaration :
float somme(int, int);
indique que somme est une fonction à valeur réelle avec deux variables entières. Une déclaration est aussi appelée prototype, maquette, signature.
■ Affectation
Une instruction d'affectation comporte deux membres : celui de gauche et celui de droite, séparé par l'opérateur d'affectation souvent représenté par le signe = (C, C++, Java, Fortran). Le membre de droite appelé Rvalue précise la valeur à affecter à la variable de gauche appelé Lvalue. L'opération d'affectation n'est pas commutative.
� Exemple
b = 3; pi = 3.14; c = b + pi;
44 CHAPITRE III ───────────────────────────────────────────────────
■ Clarté
L'affectation provoque une modification de la situation initiale des variables et de l'état du programme en en réduisant la clarté. Considérons les instructions suivantes
// Situation initiale x = b; // Situation 1 : porte sur x x = x+1; // Situation 2 : porte sur x-1 … x = a; y = b; x = x-y; y = y-x; x = y-x; y = x-y; Que valent (x,y) après exécution de ces instructions ?
■ Entrées/sorties élémentaires en C++
Les opérations d'entrée/sortie permettent de gérer les échanges d'informations entre la mémoire centrale et les périphériques. Ainsi, il existe des instructions permettant de faire la saisie, l'affichage, l'écriture ou la lecture de fichiers.
� Exemple
int main(void) { int a ; // Définition de a cin >> a; // Saisie de l'entier a cout << "a =" << a << endl; // Impression de l'entier a }
4.3 Action simple et structure de bloc Une instruction peut être simple ou complexe, selon les variables et les opérateurs manipulés. Certaines instructions deviennent illisibles vu leur complexité dont on distingue plusieurs niveaux.
La formule simple Soit h le salaire brut horaire, t le nombre d'heures de travail. Alors
b = h × t;
Le résultat intermédiaire On souhaite ici calculer le salaire net b. Soit p le pourcentage de retenue.
b = h × t - h × t × p;
On évite de calculer deux fois l'expression h× t en procédant avec deux instructions :
aux = h× t; b = aux× (1-p);
La structure de bloc Une action est exécutée par un ensemble d'instructions appelé bloc, représenté par une accolade ouvrante suivie d'un traitement (une suite d'instructions) terminée par une accolade fermante {...} dans les langages C, C++, Java.
� Exemple
if (a > 0) {traitement} else {autre traitement}
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
45
5. STRUCTURES DE CONTROLE : BOUCLES, TESTS, BRANCHEMENTS
Les tests, boucles et rupture de séquence contrôlent l'exécution du programme.
5.1 Boucle à nombre borné d'itérations L'exécution de cette boucle est conditionnée par l'évaluation d'expressions entières.
Synopsis for (expression1; expression2; expression3) action expression1 // Initialisation de la boucle expression2 // Test de fin de boucle expression3 // Expression de fin de boucle
■ Sémantique
Cette syntaxe très puissante se traduit sous la forme suivante :
expression1 // Initialisation de la boucle tant que (expression2) action // Corps de la boucle expression3; // Action exécutée à la fin de boucle
� Exemples
for (i=0; i<n; i++) printf("%d",i); for (i=0; i<n; i+=2) printf("%d",i);
Interprétation i=0 i = 0 Tant que i<n faire tant que i < n faire imprimer i imprimer i i=i+1 i = i+2 FinFaire FinFaire
■ Remarques
• Le test de fin de boucle est effectué en début de boucle.
• Chacune des expressions peut être simple, complexe, vide. Dans ce dernier cas, expression
2 est toujours positive et la boucle for(;;) perpétuelle.
� Exemple
for(i=0,j=1; i+j<10; i++,j+=2) printf(" i = %d j = %d \n",i,j); printf(" Boucle terminée \n"); printf(" i = %d j = %d \n",i,j);
// Résultat i = 0 j = 1 // Initialisation de la boucle i = 1 j = 3 // Incrémentation de i et j en fin de boucle i = 2 j = 5 // Incrémentation de i et j en fin de boucle Boucle terminée i = 3 j = 7
46 CHAPITRE III ───────────────────────────────────────────────────
5.2 La clause if… else
Synopsis
if (expression) actionv else action
f
Description La clause if...else évalue numériquement expression qui peut prendre la valeur numérique 0 (faux) ou 1 (vrai). L'action
v est exécutée si expression est non nulle,
l'actionf sinon. La séquence else est optionnelle.
5.3 La clause else if
Synopsis if (expression
1) action
1 else if (expression
2) action
2 else action
3
Description La clause else...if évalue les expressions en séquence. Si expression
1 est vraie,
l'action1 est exécutée. Le dernier else s'applique si aucune condition n'est satisfaite.
■ Principe de localisation
Si il y a moins de else que de if, le dernier else est associé au dernier if sans else qui le précède.
� Exemple
Les séquences suivantes ne sont pas équivalentes :
if ( n > 0 ) if (a > b) z = a; else z = b; if (n>0) { if (a>b) z = a; } else z = b;
Description si n>0 alors si n>0 alors si a>b alors z = a is
si a>b alors z = a sinon z = b is sinon z = b is is
5.4 Branchement multiple : la clause switch
■ Sémantique des tests multiples
La clause switch généralise les tests multiples sous la forme :
switch(valeur_de_a) { cas 1 : action
1
cas 2 : action2
... cas par défaut : action par défaut }
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
47
La valeur de l'expression (entière ou caractère) détermine l'action à exécuter décrite suite à l'une des clauses case ou default.
Synopsis switch (expression) { case constante
1 : instruction(s)
1
... case constante
n : instruction(s)
n
default : instruction(s) par défaut // Optionnel fortement conseillé }
■ Règles de syntaxe
• La clause switch évalue l'expression entière et compare sa valeur à celle qui suit chacune des clauses case, libellée par une constante de type entier ou caractère.
• La clause optionnelle default ne s'exécute que si aucune clause case n'est satisfaite.
• Les clauses case et default peuvent être utilisées dans n'importe quel ordre.
• Tous les cas doivent être différents.
■ Rupture de séquence
• L'instruction break permet la sortie immédiate d'un bloc interne de la clause switch. Si elle n'est pas utilisée, l'exécution se poursuit même après l'exécution d'un cas favorable. Ainsi, chaque instruction suivant un case doit se finir par l'instruction break si on veut éviter l'exécution (inutile et quelquefois malencontreuse) en séquence des autres cas.
• Il peut y avoir des cas multiples pour une action unique.
� Exemple
switch(c) { case 'a': printf(" a"); break; // Affichage de a
case 'b': printf(" b"); break; // Affichage de b default : printf(" r"); break; // Affichage de r
}
5.5 Boucle sur condition d'arrêt : la clause while
Synopsis while(expression ≠ 0) instruction ou bloc
� Exemple
// Comptage des espaces (' '), tabulation (\t), passage à la ligne (\n) int c, nb = 0 , ntab = 0, nl = 0; while (( c = getchar()) != EOF ) { if (c == ' ' ) ++ nb; else if (c == '\t') ++ntab; else if (c == '\n') ++nl; }
48 CHAPITRE III ───────────────────────────────────────────────────
5.6 Clause do while
Synopsis do instruction(s) while (expression)
� Exemple
Calcul de e = ∑∞
=0n !n
1
■ Condition d'arrêt du calcul
Soit ∑=
=n
0in !i
1S
Le calcul s'arrête dès que D = ε≤+
−+ 1)!(n
1 =
nS
1nS
avec ε (le seuil de précision) fixé par l'utilisateur.
■ Algorithme
Initialisation
D = 1 n = 0 somme= 0
Tant que D > ε Faire somme=somme+D n = n+1
D = n
D
FinFaire
■ Calcul de D et arrêt du calcul
Deux modes de calcul possibles pour D à chaque pas, l'un à partir de la valeur précédemment calculée de D (le plus économique et le plus simple, indiqué ci-dessus), l'autre à partir de la fonction factorielle (le plus naturel mais le plus "cher", surtout si le calcul est récursif, programmé ci-après).
Si l'algorithme diverge, le calcul ne peut s'arrêter. Il faut donc toujours prévoir cette éventualité et ajouter une condition d'arrêt pour éviter une boucle perpétuelle (ici un compteur du nombre maximum d'itérations autorisé).
■ Convergence
La convergence de cet algorithme est très rapide (7 itérations pour une seuil de précision de 10-6) ce qui est normal l'écart de deux termes consécutifs étant inversement proportionnel à n!.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
49
� Programme
#include <iostream.h> #include <stdio.h> int main() // Calcul itératif de la valeur de e = 2.71828... { double epselon ; cout << "indiquez la valeur de epselon : "; cin >> epselon; // Précision du calcul int factorielle(int); // Prototype (maquette), retour entier, 1 argument entier double somme=0, valeur;
// Initialisation de la boucle int n=0; valeur=1./factorielle(n); // Appel while ((valeur > epselon) && (n <20)) {somme+=valeur; n=n+1; valeur=1./factorielle(n); }
cout << "nombre d'itérations : " << n << " valeur de e approchée : "<< somme << endl ; printf("e = %16.13g\n",somme); }
int factorielle(int p) // Fonction factorielle définie récursivement {if (p==0) return 1; else return factorielle(p-1)*p; }
5.7 Branchements inconditionnels - ruptures de séqu ence Les instructions break et continue permettent de programmer des ruptures de séquence sans faire référence à une instruction étiquetée.
■ L'instruction break
Description Cette instruction force la sortie d'une boucle.
Son effet étant limité à un unique niveau d'imbrication, elle permet de réaliser la sortie du bloc englobant.
■ Diagramme
while (...) ou for(...){.... if(...) break; ...}
� Exemple 1
for (i=0; i<10; i++) if (i== 3) break;
Description Sortie de la boucle quand i = 3
50 CHAPITRE III ───────────────────────────────────────────────────
� Exemple 2
for(i = 0; i < 10; i++) for (j=0; j<10; j++) if (j== 3) break;
Description Nous avons deux boucles imbriquées. Or, la clause
for (j=1; j<10; j++) if (j== 3) break;
étant l'affectation invariante j=3, elles se réduisent aux affectations i=10 et j=3.
■ L'instruction continue
Description Arrêt de l'exécution de la boucle à l'endroit spécifié en omettant le reste du bloc jusqu'au début de l'itération suivante.
Diagramme
while(...) do{.... {... .... ... continue; continue; .... ....} } while(...);
� Exemple
for (i = 0; i < n; i++) { if ( i== 3 ) continue; printf("%d",i); }
Description Impression sauf si i = 3
� Contre exemple
Voici un exemple vicieux de ce qu'il faut éviter d'écrire :
for(;;) { if(cas_particulier()) break; else continue;
traitement_général(); /* Ne sera jamais exécuté */ }
■ L'instruction goto
L'instruction goto, dont il faut rappeler qu'elle n'est pas dans l'esprit de la programmation structurée, est utilisée pour sortir de boucles imbriquées.
Synopsis goto étiquette;
...
étiquette :
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
51
6. FONCTIONS, PROCEDURES, BIBLIOTHEQUES
6.1 Fonction
■ Définitions
Une fonction opère sur des arguments scalaires. C'est une application
(E1 X E2 X ... X En) → D
où (Ei)i=1,n sont les domaines de définition des arguments de la fonction et D est le
domaine de valeur du résultat scalaire.
Les arguments sont d'un type prédéfini (entier, réel, caractère, pointeur, etc.) ou non.
■ Transmission du résultat
Le résultat d'exécution une fonction est transmis à l'expression appelante par l'instruction return .
� Exemple
int main(void) { int x = 2, y = 3; printf("somme = %d\n", somme(x,y)); // Appel de la fonction somme }
// Corps de la fonction somme int somme (int a; int b) { return (a+b);}
6.2 Procédure et action
■ Définition
Une procédure représente une action, par exemple inverser un système linéaire.
■ Syntaxe en langages C et C++
Une procédure est une fonction qui ne retourne rien (mot clé void).
� Exemple
Le programme ci-dessous est constitué d'un programme principal qui appelle les procédures nécessaires à la solution du problème.
// Début du programme float a[100][100], b[100], x[100]; // Appels des procédures d'initialisation initialiser_tableau(b); initialiser_tableau(x); initialiser_matrice(a); inverser(a,x,b); affichage(x); // Inversion du système puis affichage // Fin du programme principal // Définition des procédures void initialiser_tableau(float x[]) {/* Code de la procédure */ } void affichage(float x[]) {/* Code de la procédure d'affichage */ } void inverser(float a[][], float x[], float b[]){/* Code de l'inversion */}
52 CHAPITRE III ───────────────────────────────────────────────────
6.3 Bibliothèques de programmes Il existe des bibliothèques (fonctions, procédures, classes, boites à outils, etc.), appelables depuis la plupart des langages de programmation (C, C++, Java, Fortran, etc.).
� Exemple
La fonction de la bibliothèque standard C qsort effectue le tri d'un tableau selon l'algorithme de Quick sort. Son prototype est le suivant :
#include < stdlib.h > void qsort(void *tableau, size_t n, size_t t,int(*comp)(const void *arg1, const void *arg2));
■ Description
La variable tableau représente l'adresse de la première composante d'un tableau de n éléments chacun de t octets, à trier par ordre croissant.
La variable comp est l'adresse d'une fonction de comparaison de deux composantes du tableau écrite par le programmeur, qui retourne 0 si les éléments comparés sont identiques, un nombre positif si l'élément pointé par arg1 est supérieur à arg2, un nombre négatif sinon.
Le type void * indique que le type effectif du tableau et des arguments de la fonction de comparaison est déterminé dynamiquement à l'exécution.
6.4 Structure d'un programme C/C++
■ Programme principal
Tout programme est constitué d'un programme principal qui en est le point d'entrée et contenant les définitions, déclarations, blocs, et éventuellement des appels de fonctions et des procédures. En C ou C++, c'est la fonction main.
� Exemple
int main(void) { cout << "coucou!! c'est moi" << endl;} // Instruction d'impression
■ Structure élémentaire d'un programme en langage C ou C++
Tout programme en langage C/C+ doit respecter les règles suivantes :
int main(void) /* Point d'entrée de la fonction main */ {// Début de la fonction main()
définition(s) et déclaration(s) obligatoires de toutes les variables utilisées initialisation (recommandée) des variables instructions du programme fin de la fonction main()
}
Définition des éventuelles fonctions et/ou procédures appelées.
L'ordre de la définition des différentes fonctions est arbitraire.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
53
7. REPRESENTATION INTERNE DES OBJETS DE BASE DES LANGAGES C ET C++
Le système binaire est un système de représentation naturel de l'information par un ordinateur l'information digitale représentant l'état d'un signal électrique.
Un ensemble de n bits permettant la représentation de 2n états, le codage consiste à établir une loi de correspondance (appelée code) entre les informations à représenter et les configurations binaires possibles de telle sorte qu'à chaque opération corresponde une et une seule configuration binaire.
La conversion d'un système de codage en un autre est appelée transcodage.
■ Interprétation selon le type de l'objet
Les différents types de codage utilisés dans un ordinateur sont tous basés sur le système binaire et dépendent de l'entité à représenter. Une même suite de bits a de multiples interprétations ce qui explique que l'opération de lecture du contenu de la mémoire d'un ordinateur ne puisse être faite sans la connaissance de l'adresse et de la nature de l'information à laquelle on désire accéder.
7.1 Notation binaire et hexadécimale En système binaire, les informations sont formées de suites "assez longues" de 0 et de 1. Si l'utilisateur dialogue sous cette forme avec l'ordinateur, la longueur de la chaîne de bits du codage freine considérablement la compréhension du dialogue. Les risques d'erreurs sont nombreux. Le système de base 16 (hexadécimal), permet une représentation condensée et claire de 4 bits. Comme il est nécessaire de disposer de 16 symboles, on utilise les chiffres 0 à 9 et les lettres A, B, C, D, E F. La correspondance s'effectue suivant le tableau suivant :
Hexadécimal binaire hexadécimal binaire 0 0000 8 1000 1 0001 9 1001 2 0010 A 1010 3 0011 B 1011 4 0100 C 1100 5 0101 D 1101 6 0110 E 1110 7 0111 F 1111 Un octet est alors représenté par deux caractères hexadécimaux. Ainsi l'octet
1011 0010 s'écrit B2.
Il existe aussi des représentations en système octal. Dans ce cas, seuls les chiffres de 0 à 7 sont utilisés et 3 bits suffisent à leur représentation. Les deux premières colonnes du tableau au précédent fournissent la correspondance décimal/octal.
Nous présentons ci-après divers codages en distinguant données et instructions.
54 CHAPITRE III ───────────────────────────────────────────────────
7.2 Données alphanumériques On appelle donnée alphanumérique, (par opposition à donnée numérique) toute donnée qui ne peut donner lieu à un calcul arithmétique. Dans la pratique une chaîne de caractères est constituée d'une suite de caractères.
• lettres majuscules et minuscules,
• chiffres 0, 1,... 9,
• symboles ! ? ; : , . < > = etc.,
• caractères spéciaux : saut de ligne, saut de page, etc.
Tout caractère est codé sur un octet à partir d'un système de codage.
On en distingue deux : les codes EBCDIC et ASCII. Le code EBCDIC (Extended Binary Coded Decimal Interchange Code), d'origine IBM, utilisant un octet par caractère permet d'en coder 256.
Le code ASCII (American Standard Code for Information Interchange), utilisé sur les micro et mini-ordinateurs, utilise à l'origine 7 bits, le 8ème bit étant utilisé comme bit de contrôle de la validité des transferts de données. Le code ASCII étendu, sur 8 bits, étend le jeu de caractères de base.
■ Remarque
Comme il existe de nombreux alphabets (russe, grec, etc.), il n'y a pas unicité de la représentation. C'est pourquoi il existe un code ascii anglais, allemand, français, etc. ce qui pose de nombreux problèmes pour l'édition. Une norme "universelle " est à l'étude.
Le code ASCII international s'interprète de la façon suivante : les caractères de contrôle ont un code compris entre 0 et 32, les caractères usuels ont un code compris entre 33 et 127, les autres caractères ont un code compris entre 128 et 256.
Le lecteur pourra vérifier qu'avec ce système de codage, la chaîne JEAN est codée en hexadécimal 4A45414E et que la chaîne 12 est codée 3132. Un nombre peut donc être codé sous la forme d'une chaîne de caractères. Il est alors impossible d'effectuer sur celui-ci des opérations arithmétiques sans l'avoir converti en donnée numérique.
■ Police de caractères
Une police de caractères est une représentation particulière des caractères d'un système de codage. Ainsi, les traitements de textes usuels utilisent une grande variété de polices tels le Times, Arial, etc. Des polices particulières permettent de représenter divers symboles, par exemple mathématiques.
7.3 Nombres entiers Les nombres relatifs sont représentés à partir de deux systèmes de codage : les nombres entiers non signés et signés.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
55
■ Nombres entiers non signés
On appelle nombre entier non signé tout entier naturel codé sans signe. Pour un codage sur n bits numérotés de droite à gauche et de 0 à n-1, le nombre entier non signé p s'écrit, selon l'algorithme d'Euclide, sous la forme binaire :
p = ∑−
=
δ1n
0ii
i2
où δi représente sur le bit i le reste des divisions successives par 2.
� Exemple
13 s'écrit sur un octet 0000 1101.
Les bits de droite sont appelés bits de poids faible et ceux de gauche bits de poids fort.
■ Nombres entiers signés
On appelle nombre entier signé tout entier relatif. Le bit le plus à gauche, dit bit le plus significatif (most significant bit), code le signe. Quand ce bit est nul, le nombre est positif; quand il est égal à 1, le nombre est négatif. Sa valeur absolue utilise les n-1 bits restants (entier non signé). Ainsi sur un octet
+ 13 s'écrit 00001101 0 s'écrit 00000000 + 127 s'écrit 01111111
Quand le nombre est négatif, on utilise les n-1 bits restants non pour écrire sa valeur absolue mais son complément dont il existe deux définitions.
Le complément à 1 d'un nombre A codé sur n bits, noté C1(A) vérifie :
C1(A) + A = 2n - 1
Le complément à 2 d'un nombre A codé sur n bits, noté C2(A) vérifie :
C2(A) + A = 2n
On démontre que le complément à 1 d'un nombre est sa négation bit à bit et que
C2(A) = C1(A) + 1 = C1(A-1)
Ainsi, pour écrire -3 sur un octet :
Codage de 2 sur 7 bits 0000010 Complément à 1 1111101 Ajout du bit de signe 11111101
De la même façon :
- 1 s'écrit 11111111 - 128 s'écrit 10000000 - 11 s'écrit 11110101
56 CHAPITRE III ───────────────────────────────────────────────────
� Exemple
Représentation sur 32 bits du nombre + 1973
En numération :
binaire 1973 = 0…0 0111 1011 0101 hexadécimale 1973 = 0…0 7 B 5
■ Remarque
Le lecteur vérifiera que n bits permettent de représenter des nombres entiers signés m tels que :
− ≤ ≤ −− −2 2 11 1n nm
7.4 Nombres réels Les nombres réels sont composés des nombres rationnels et des nombres irrationnels.
• Seuls sont représentables exactement les nombres rationnels.
• Les nombres irrationnels sont approximés et l'erreur de représentation est appelée erreur de troncature.
On distingue trois représentations des nombres réels : les réels DCB, les réels avec une virgule flottante ou réels flottants, les réels avec une virgule fixe. Ces représentations peuvent être liées à une machine ou un logiciel ce qui peut provoquer des problèmes de portabilité.
■ Nombres réels flottants
Dans cette représentation, le nombre est décomposé en deux parties : l'exposant e qui est un entier signé, la mantisse M.
La valeur d'un nombre x ainsi représenté est par construction
x = S M bc
avec S le signe du nombre, M la mantisse, b la base de numération (en général 2 ou 16), c la caractéristique, sur p bits, liée à l'exposant e par la relation
c = e + 2p-1
Il faut représenter le signe, la mantisse, l'exposant.
signe exposant mantisse
Chaque constructeur pouvant utiliser des longueurs de mot, de caractéristique, ou d'exposant différentes, il n'y a pas unicité de la représentation ce qui peut conduire à des résultats différents pour un calcul donné. Nous choisissons ici des mots de 32 bits, une caractéristique sur 7 bits et une mantisse sur 24 bits. Nous présentons en outre la norme IEEE 754 des nombres flottants qui utilise une caractéristique sur 8 bits et une mantisse sur 23 bits pour les nombres en simple précision, une caractéristique sur 11 bits et une mantisse sur 53 bits pour les nombres en double précision.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
57
■ Signe
Le bit de gauche est le bit de signe. Par convention, il vaut 0 si le nombre est positif 1 si le nombre est négatif
■ Caractéristique
Elle est représentée sur les p bits suivants. Puisque
0 ≤ c ≤ 2p
on a :
-2p-1 ≤ e ≤2p-1 - 1
Quand p = 7 ou 8 (norme IEEE 754), on peut représenter des exposants positifs et négatifs d'une valeur absolue "raisonnable".
■ Mantisse
La mantisse indique le nombre de chiffres de la représentation. Avec des mots de 32 bits, elle est représentée sur (32 - p-1) bits soit 24 ou 23 bits (norme IEEE 754). Le premier problème est le choix de la représentation de la mantisse. En effet, on peut écrire le nombre suivant de différentes manières :
1973 = 197,3 * 10 = 19,73 * 102 = 0,1973 *104
Dans la dernière égalité, la mantisse vaut 0,1973 et l'exposant 4.
La forme normalisée de la mantisse est définie par la relation :
1/b ≤ M < 1 (1)
Elle permet d'obtenir le maximum de chiffres significatifs.
� Exemple
En base 10, le nombre 1,973 a pour mantisse normalisée 0,1973 et pour caractéristique 65 (1+64).
Dans la pratique, seule la partie fractionnaire de la mantisse soit ici 0,1973 est représentée.
Calcul de la mantisse Chacun des bits de la mantisse s'il vaut 1 représente 2-i, i étant sa position. D'où :
M ii
i
n
= −
=∑δ 2
1
où δi est la valeur 0 ou 1 de son ième bit.
Si tous les δi valent 1, alors
M nn= + + + = − −1
2
1
4
1
21 2...
58 CHAPITRE III ───────────────────────────────────────────────────
� Exemple
Le nombre réel +1973 s'écrit :
1973 = 7B5 = 0,7B5 * 163
donc :
S = 0 ; M = 7B5 c = 2p-1 + 3 = 26 + 3 = 67 soit 43 en base 16.
En hexadécimal : 1973 = 4 3 7 B 5 0...0 Codé en binaire : 1973 = 0100 0011 0111 1011 0101 0...0
Avec la norme IEEE 754, on obtient :
S=0; M = 7B5
c = 2p-1 + 3 = 27 + 3 = 131 soit 83 en base 16.
En hexadécimal : 1973 = 4 1 B D A 8...0...0
Le nombre réel -1973 s'écrit à partir du complément à 2 du nombre positif soit BC84B000. Avec la norme IEEE 754, on obtient BD425800
■ Choix de la base de représentation
Le choix est effectué selon la précision de la représentation obtenue.
� Remarque 1
Pour additionner deux nombres réels flottants, il faut les réduire au même exposant.
Si b vaut 2, la valeur du nombre n'est pas modifiée en décalant la mantisse d'une position vers la droite et en augmentant l'exposant de 1.
Si b vaut 16, il faut décaler la mantisse de quatre positions et augmenter l'exposant de 1 pour ne pas changer la valeur du nombre. Mais lors du décalage d'une position vers la droite, le bit n de la mantisse est perdu ce qui provoque une perte de précision.
� Remarque 2
Si b vaut 2, l'inéquation (1) devient :
1/2 ≤ M < 1
Le premier bit de la mantisse M est toujours égal à 1 pour tout x non nul et cette représentation assure le nombre maximum de bits significatifs.
Si b vaut 16, l'inéquation (1) devient :
1/16 ≤ M < 1
4 bits sont nécessaires pour représenter les différentes valeurs possibles 1/16, 2/16,...15/16 du premier chiffre de la mantisse. Le lecteur vérifiera que tout nombre compris entre 1/16 et 1/8 a une mantisse dont les 3 premiers bits sont nuls et que dans ce cas, trois bits significatifs sont perdus.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
59
� Remarque 3
Pour un mot en base b, avec une mantisse sur n bits et une caractéristique sur p bits, le plus grand nombre positif représentable mb est :
mb
SMb bp p
= ≤− −1
2
1
2
Ce nombre indépendant du nombre de bits de la mantisse dépend de la base choisie.
En base 16 et 2 et avec p = 7, on obtient :
m16 = 1663 ≈ 1075 m2 = 263 ≈ 1019
En base 16 et 2 et avec p = 8, (norme IEEE 754), on obtient :
m16 = 16127 ≈ 10151 m2 = 2127 ≈ 1038
Le choix de la base 16 est donc un compromis entre la grandeur de la valeur absolue et la précision de la représentation.
■ Recherche du nombre de bits de la mantisse
Soient :
x = sbn M et y = sbn M'
alors, y devient négligeable devant x dès que l'équation :
x + y = x
est vérifiée. Or, la mantisse M représente la suite des puissances négatives de 2 ce qui permet d'en calculer le nombre n de bits par l'algorithme suivant :
Début n = 0; y = 2-n ; x = 1; Faire si x+y = x alors imprimer n-1; exit ; sinon n = n+1; y = 2-n ; is FinFaire Fin
■ Calcul du nombre de chiffres significatifs
Soit y = 2-n le plus petit nombre représentable sur la machine considérée. On cherche p tel que :
2-n = 10-p
d'où on tire la relation :
p = n log2 ≈ 0.3 n
Une mantisse de n bits représente p chiffres significatifs d'où une mantisse sur 23 bits (norme IEEE 754 en simple précision) donne 6 chiffres significatifs et une mantisse sur 53 bits (norme IEEE 754 en double précision) donne 15 chiffres significatifs.
60 CHAPITRE III ───────────────────────────────────────────────────
7.5 Images Une image est un ensemble de lignes constituées par des point élémentaires appelés pixels (picture element) caractérisé par leur teinte appelée niveau de gris pour les images monochromes et couleur pour les images en couleur. Chaque pixel d'un moniteur graphique courant est souvent représenté sur un octet et peut avoir 256 niveaux de gris. Dans le cas d'images en couleur, chaque pixel est coloré à partir des couleurs de base rouge, vert, bleu, chacune pouvant être représentée sur un octet. Dans ce cas, on a une panoplie d'environ seize millions de couleurs. Les définitions les plus courantes varient entre 340*200 et 1600*1200 pixels.
7.6 Instructions Le codage d'une instruction machine est lié à l'architecture interne de la machine et varie selon la longueur des mots et le nombre d'adresses. Le code opération dépend du nombre d'instructions possibles.
A chacun des éléments d'information composant l'instruction est associée une zone de plusieurs bits pour coder les différents états possibles de cette information. Par exemple, 6 bits pour le code instruction permettent de coder 26 instructions, 4 bits pour l'adresse du premier opérande permettent un choix de 16 registres, et 16 bits d'adresse mémoire peuvent adresser 216 mots.
8. ACCES A UN OBJET
■ Définitions
L'adressage est une transformation entre l'adresse de l'objet dans le programme et son adresse effective dans la mémoire.
L'adresse absolue est définie par rapport à l'adresse 0 de la mémoire.
L'adresse effective est calculée selon un mode d'adressage, technique utilisée pour accéder aux différents objets stockés en mémoire.
Les différentes modes d'adressage sont les suivants :
■ Adressage direct
Appelé également adressage normal, absolu ou réel, l'adresse effective de l'objet est contenue dans l'instruction. Le temps de recherche est d'un cycle mémoire.
■ Adressage immédiat
La partie adresse contient la valeur de l'objet et ne nécessite aucune recherche en mémoire.
� Exemple
i=6;
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
61
■ Adressage indirect
La partie adresse est un pointeur sur l'adresse effective de l'objet dont le chargement nécessite de deux cycles mémoire : un pour l'adresse de l'objet, l'autre pour l'objet.
Le nombre total de cycles est le nombre d'indirections de l'instruction dont le maximum est le niveau d'indirection du calculateur.
� Exemple
Dans les compilateurs Fortran, cette technique est utilisée pour gérer les adresses de retour des sous-programmes. Pour ce faire, l'adresse de retour est stockée, lors de l'appel, dans la première instruction du sous-programme appelé. Au moment du retour dans le programme appelant, le compteur ordinal est chargé avec la première adresse contenue dans le sous programme. Ainsi, le programme Fortran :
CALL SP1 20 instruction SUBROUTINE SP1 RETURN END
se déroulera comme suit : // Appel de SP1 et stockage de l'adresse de retour (ici 20) Ranger l'adresse de retour à l'adresse SP1 aller_à SP1 exécuter SP1 aller_à *SP1 // Notation usuelle pour l'adressage indirect
■ Adressage relatif
L'adresse effective est obtenue par addition ou soustraction d'un déplacement à l'adresse de l'instruction en cours, l'instruction relative, ce qui permet de d'accéder simplement à n'importe instruction du programme.
■ Adressage par base et déplacement
La transformation des adresses symboliques d'un processus en adresses réelles utilise une adresse relative (déplacement) à son adresse initiale d'implantation, choisie par le système d'exploitation, appelée l'adresse de base.
Soient Aa l'adresse absolue, Ab l'adresse de base, D le déplacement.
L'adresse effective est la somme du contenu du registre de base et du déplacement.
Aa = Ab+D
Pour déplacer un processus en mémoire, il suffit de modifier l'adresse de base (adresse de référence), contenue dans le registre de base. On peut ainsi translater des processus dans la mémoire par simple modification du contenu du registre de base, sans avoir à réaffecter toutes les adresses réelles des instructions. Les compilateurs génèrent des codes objets avec des adresses relatives à une adresse initiale nulle. Le déplacement D représente les adresses des instructions, repérées à partir de l'adresse initiale. De tels programmes sont dits translatables et ce mode d'adressage s'appelle aussi la translation d'adresse.
62 CHAPITRE III ───────────────────────────────────────────────────
� Exemple
Considérons le processus P, implémenté à l'instant T à l'adresse Aa comme suit :
Instant Aa Aa+Dmax Ab D Dmax T 3200 3599 3200 0 399 T+δt 1100 1499 1100 0 399
A l'instant T+δt, le processus P est déplacé ce qui libère l'espace mémoire comprit entre les adresses 3200 et 3599. Seule l'adresse de base Ab a été modifiée.
Un autre intérêt de ce procédé est la protection mémoire : il garantit que le processus ne puisse lire et écrire dans la région mémoire située avant l'adresse de base. De plus, il suffit de stocker dans un registre la valeur maximale autorisée pour les déplacements pour garantir que le processus ne puisse lire ou écrire dans la région mémoire située après l'adresse de base augmentée du déplacement maximum.
■ Adressage par rapport à l'adresse courante
Le contenu du compteur ordinal sert d'adresse de référence.
� Exemple
Déplacement par rapport à la ligne courante de n lignes dans un éditeur de texte.
■ Adressage indexé
L'indexation permet d'atteindre chaque composante d'un tableau d'objets dont l'adresse initiale est connue.
L'adresse effective est obtenue par ajout à l'adresse de base de l'objet (celle de la première composante du tableau) d'un index préalablement initialisé puis incrémenté d'un pas à l'exécution de l'instruction de fin de boucle.
On appelle pré-indexation l'adressage par addition du déplacement de base, et post indexation l'adressage par addition de l'index.
� Exemple : soit le programme C :
int main(void) { float a[100]; int i ; for (i=0;i < 100; i++) a[i] = 0;} }
Le déroulement de ce programme est le suivant :
• réservation d'une zone de 100 mots en mémoire commençant à l'adresse du symbole A.
• boucle de remplissage de la zone à partir de son adresse initiale indexée par I (registre d'index).
On obtient alors le programme assembleur suivant (les notations sont décrites dans les commentaires) :
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
63
DEBUT A RES 100 // Réservation du tableau A CHA RB 0 // Chargement du registre d'index (valeur 0) COMPTEUR EQU 100 BOUCLE CP RB COMPTEUR // Comparaison de RB avec COMPTEUR BG FIN // Branchement si (COMPTEUR)>(RB) à FIN CHA RB // Chargement avec le contenu de RB RNG A,RB // Rangement du contenu de l'accumulateur à // L'adresse A indexée par le contenu de RB INC RB // Incrémenter de 1 le contenu de RB B BOUCLE FIN STOP // Fin du programme
■ Application : calcul de la longueur maximale d'un tableau
La longueur maximale d'un tableau est calculée à partir du nombre de bits du registre d'index puisque l'accès à une de ses composantes nécessite d'y accéder.
■ Récapitulation des modes d'adressage
On note (A) le contenu de l'adresse A.
Accès immédiat à l'objet opérande=(AD) Adressage normal (direct, absolu) adresse effective=(AD) Adressage indirect adresse effective=((AD)) Adressage relatif base et déplacement (pré-indexation) adresse effective=(B)+(AD) relatif à l'adresse courante adresse effective=(P)±(AD) dans la page courante adresse effective= adresse de page+(AD) Adressage indexé (post indexation) adresse effective=(AD)+(X)
9. STRUCTURES DE DONNEES ABSTRAITES
9.1 Tableaux, pointeurs, tables
■ Construction d'objets
La programmation procédurale définit les objets de base suivants : variable, tableau, pointeur, table, liste, file, construits à partir des objets de base. Des objets de type composite (tableau, enregistrement, variable structurée, classe, etc.) peuvent être construits.
■ Tableau, pointeur
L'organisation d'un tableau en mémoire est séquentielle. Cette structure d'objet, simple est bien adaptée au calcul scientifique. L'accès en séquence à l'information est réalisé à partir d'un pointeur (index) qui représente son adresse (typée).
info i-1 info i info i+1
64 CHAPITRE III ───────────────────────────────────────────────────
■ Table
Une structure de données abstraite très utilisée en informatique de gestion est la table : l'accès aux informations d'un fichier est réalisé par l'intermédiaire d'une clé d'accès contenue dans un autre fichier appelé table. Cette clé est un pointeur contenant l'adresse de l'information dans le fichier.
clé_info i info j
clé_info j info i
clé_info k info k
9.2 Piles Une pile est une structure de données permettant la gestion de l'historique d'une séquence d'événements. Deux cas se présentent :
• la gestion des interruptions ou des branchements durant lesquels il faut sauvegarder le numéro d'une interruption masquée ou l'adresse de l'instruction suivante pour reprendre ultérieurement l'exécution du programme à cette adresse,
• la transmission des arguments à l'appel d'une fonction ou d'une procédure.
Les objets d'une pile donnée sont de même nature même si les types de ces derniers peut être de nature diverse (variable, pointeur, tableau, instances de classes, etc.).
L'objet le plus récent est situé au sommet de la pile, le plus ancien à la base, la position du sommet étant stockée dans le registre pointeur de pile (stack pointer).
sommet info i info i-1 info i-2
base info 1
Une pile matérielle est un circuit qui gère automatiquement le pointeur de pile. L'avantage est une grande rapidité dans l'utilisation, l'inconvénient sa taille limitée.
Une pile logicielle est une zone réservée de la mémoire vive dont l'adresse en mémoire est gérée par le programme.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
65
■ Opérations sur les piles
Les opérations de manipulation d'une pile sont l'empilement dont l'instruction est PUSH (pousse ou dépose), le dépilement supérieur dont l'instruction est POP (prends ou retire), le dépilement inférieur dont l'instruction est POPD pour déposer ou retirer un des éléments sur le sommet ou sur la base de la pile permettant l'accès à la dernière ou la première information de la pile.
sommet........base
PUSH POP
PUSH
sommet........base
POP
� Exemple : empilement des adresses de retour
Voici un algorithme de gestion des adresses de retour lors de l'appel de sous-programmes en Fortran où un sous-programme est écrit sans faire aucune hypothèse, ni sur le programme appelant, ni sur son implantation en mémoire. Lors de son appel, il est nécessaire de stocker l'adresse de l'instruction du programme qui suit l'instruction d'appel, naturellement nommée adresse de retour. Voici une méthode, basée sur l'utilisation des piles. Les commentaires sont après les délimiteurs //.
Etiquettes Instructions 1 // Début du programme principal 10 call SP1 11 Instruction END (PP) // Fin du programme principal 20 SUBROUTINE SP1 call SP2 25 Instruction END (SP1) 50 SUBROUTINE SP2 60 call SP3 61 Instruction END (SP2) 100 SUBROUTINE SP3 110 call SP4 111 call SP5 112 Instruction END (SP3) 130 SUBROUTINE SP4 140 END (SP4) 150 SUBROUTINE SP5 160 END (SP5)
La pile des adresses de retour sera gérée selon l'algorithme suivant :
66 CHAPITRE III ───────────────────────────────────────────────────
// Début programme principal tableau pile(n); pile = vide // Appel de SP1 PUSH (11) // 11 : 1-ère adresse de retour; aller_à SP1; // Appel de SP2 PUSH (25) // 25 : 2-ème adresse de retour; aller_à SP2; // Appel de SP3 PUSH (61) // 61 : 3-ème adresse de retour; aller_à SP3; // Appel de SP4 PUSH (111) // 111 : 4-ème adresse de retour; aller_à SP4; // Fin de SP4, retour dans SP3 POP ; aller_à adresse_retour (111); // Appel de SP5 PUSH (112) // 112 : 5-ème adresse de retour; aller_à SP5; // Fin de SP5, retour dans SP3 POP ; aller_à adresse_retour (112) ; // Fin de SP3, retour dans SP2 POP ; aller_à adresse_retour (61) ; // Fin de SP2, retour dans SP1 POP ; aller_à adresse_retour(25) ; // Fin de SP1, retour au programme principal POP ; aller_à adresse_retour(11) ; // Déroulement du programme principal fin;
9.3 File et liste
■ File
Une file contient une liste de travaux en attente que le système gère soit :
• dans l'ordre d'arrivée par une pile FIFO (First In, First Out) ou premier entré, premier sorti,
• dans l'ordre inverse d'arrivée par une pile LIFO (Last In, First Out) ou dernier entré, premier sorti.
■ Liste chaînée
Une liste chaînée est un ensemble d'informations composées de deux parties: l'information proprement dite, complétée par un pointeur contenant l'adresse de l'information suivante de la liste.
Les opérations sur une liste chaînée sont l'initialisation d'un élément de la liste, l'insertion ou la suppression d'un des éléments de la liste, le parcourt de la liste.
BASES DE PROGRAMMATION EN C ET C++ ───────────────────────────────────────────────────
67
� Exemple
Les blocs constituant un fichier ne sont pas toujours consécutifs sur le disque; chacun contient, en dernière information, l'adresse sur le disque du bloc qui lui est (logiquement) consécutif constituant ainsi une liste chaînée par des pointeurs.
■ Liste chaînée parcourue selon un algorithme FIFO
Une liste est chaînée avec un algorithme FIFO quand toute information de la liste adresse l'information suivante. Son parcourt est séquentiel à partir de son premier élément.
info (i-1) pointeur(i) info(i) pointeur(i+1) info(i+1) pointeur(i+2)
■ Liste chaînée parcourue selon un algorithme LIFO
Quand les pointeurs adressent l'information précédente, ils constituent une liste chaînée dont les éléments sont accessibles par un algorithme LIFO dont le balayage est séquentiel à partir de son dernier élément.
info (i-1) pointeur(i-2) info(i) pointeur(i-1) info(i+1) pointeur(i)
■ Liste doublement chaînée
Une liste doublement chaînée est simultanément FIFO et LIFO et nécessite deux pointeurs de chaînage avant et arrière (exercice laissé au lecteur).
info(i) pt(i-1) pt(i+1) info(i+1) pt(i) pt(i+2) info(i+2) pt(i+1) pt(i+3)
68 CHAPITRE III ───────────────────────────────────────────────────
10. EXERCICES
� Exercice 1
Ecrire un programme qui affiche horizontalement un histogramme des fréquences des caractères d'un fichier texte. On pourra distinguer les lettres majuscules des lettres minuscules, les chiffres et les autres caractères. Le principe est le suivant :
• constitution d'un tableau entier des effectifs, à partir du caractère saisi,
• calcul des fréquences (attention au type),
• affichage (attention au type).
� Exercice 2
Histogramme horizontal des fréquences des longueurs des mots d'au plus 25 caractères d'un texte. Les pourcentages correspondants sont ensuite calculés.
� Exercice 3
Calcul de π par la formule 6n
1 2
12
π=∑∞
// Complexité importante pour un résultat minable : 46000 itérations pour 5 décimales #include <iostream.h> #include <math.h> int main() { int carre(int); // La fonction carre (retourne un int, 1 argument int double epselon ; cout << "indiquez la valeur de epselon : "; cin >> epselon; // Précision du calcul
double somme=0, valeur; // Boucle de calcul int n=1; valeur=1./carre(n);
cout << "nombre maximum d'itérations : "; int iter; cin >> iter ; while ((valeur > epselon) && (n < iter )) // Boucle de calcul // Nombre d'itérations inconnu à priori donc borné par un maximum autorisé
{somme+=valeur; n=n+1; valeur=1./carre(n); }
double pi = sqrt(6*somme); cout << " nombre d'itérations : " << n << " pi = " << pi << endl; }
int carre(int p) // Fonction de calcul du carré d'un nombre fourni en argument (ici p) { return (p*p);}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS
1. POPULARITE DU LANGAGE C ET DES LANGAGES DERIVES
Le langage C, très populaire, n'est toutefois pas sans défaut.
• Sa grammaire, très permissive, peut conduire à de graves erreurs de sémantique.
• Il n'est pas fortement typé, contrairement aux langages à objet.
• Certains mécanismes d'abstraction (objets) et de contrôle (exceptions) font défaut.
• Il oblige le programmeur à gérer lui-même son espace mémoire dynamiquement.
Ces défauts sont corrigés avec les langages C++ ou Java, dont une des raisons du succès est leur compatibilité ascendante avec le langage C.
La popularité du langage C a plusieurs causes :
■ UNIX et C
Le langage C a été défini dans les laboratoires Bell d'AT&T pour implémenter les premières versions d'UNIX. Il a ensuite bénéficié de la diffusion de ce système.
■ Langage de bas niveau
Le langage C offre des services système de bas niveau (de type langage machine) avec une syntaxe évoluée aujourd'hui normalisée.
■ Universalité, normalisation, portabilité
Le langage C a une vocation universelle car utilisé dans des applications diverses (système, client/serveur, graphique, etc.).
Le langage C étant normalisé depuis 1988, une application développée avec sur une machine peut être compilée et exécutée sans modification sur la plupart des autres systèmes informatiques ce qui la rend portable, avantage économique considérable pour le développement de logiciels.
■ Interface homme machine
La plupart des systèmes offrent une interface avec le langage C : système d'exploitation, système de gestion de bases de données, système de fenêtrage, tableurs, pour ne citer que les plus importants. En outre, de nombreux outils de développement existent.
CHAPITRE IV
70 CHAPITRE IV ───────────────────────────────────────────────────
2. ESPRIT DU LANGAGE C
Le terme "Esprit du langage C" fait référence aux principes de programmation sous-jacents de ce dernier. Ils ne sont pas formellement définis mais la norme permet d'en dégager les principaux :
• Faire confiance au programmeur en lui permettant de faire ce qu'il estime nécessaire (langage permissif).
• Conserver un langage concis, puissant, et simple.
• N'autoriser qu'une unique façon de réaliser une opération.
• La réaliser rapidement, même si la garantie de portabilité n'est pas assurée. Ce dernier principe signifie que le standard n'empêche pas le programmeur d'écrire un programme adapté à une architecture de machine particulière.
Traditionnellement, le langage C est considéré comme un assembleur de haut niveau. La norme tente de conserver au langage une flexibilité suffisante pour donner aux utilisateurs une chance très sérieuse d'écrire des codes portables.
Le langage C ayant été conçu pour écrire un système d'exploitation, sa philosophie générale est la suivante :
• Le code réalisé doit être le plus compact possible.
• Il faut réaliser des fonctions complexes de façon simple.
• Les codes doivent être modulaires le langage C étant structuré.
• La grammaire de base du langage est simple : nombre de mots clés réduit (32), opérateurs classiques, fonctions traditionnelles (mathématiques, traitement de chaînes de caractères, appels systèmes) compilées et éditées dans des bibliothèques standards, fichiers de description des prototypes et des variables qualifiées constantes prédéfinies suffixés par.h à inclure dans le programme pour les utiliser.
2.1 Règles générales de programmation et de portabi lité Un programme portable s'exécute de façon identique sur des systèmes différents. Quoique la portabilité absolue soit difficile à réaliser, voici quelques règles de base qui permettent d'assurer qu'un programme sera à peu près portable :
■ Attention aux règles de grammaire
• En C, certaines déclarations sont facultatives, comme celle du type de retour d'une fonction, par défaut entier, ce qui ne correspond pas toujours à la réalité. Ce problème disparaît en C++ car tout objet utilisé doit être déclaré.
• Il ne faut pas écrire d'instruction dont le résultat de l'exécution dépend d'un ordre d'évaluation des arguments différent de celui du standard.
■ Utilisation du préprocesseur
• Toujours utiliser les variables qualifiées constantes prédéfinies (EOF).
• Utiliser systématiquement des fichiers en-tête pour spécifier les déclarations et les définitions dépendantes du site d'exécution local.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
71
■ Prototypage
Déclarer explicitement tous les objets utilisés, même quand la grammaire ne l'impose pas. En particulier, utiliser le type void pour les fonctions qui ne retournent aucune valeur.
■ Bonne utilisation des pointeurs
• Ne pas oublier que les pointeurs sont typés et qu'ils ne peuvent pointer n'importe où et sur n'importe quel objet.
• Ne pas supposer qu'un entier et un pointeur ont la même taille.
• Ne pas utiliser de fonctionnalités dépendantes du compilateur C (ordre d'évaluation des arguments d'une fonction, ordre d'évaluation de certains opérateurs), dépendantes de la machine, ou du système local.
• Toujours utiliser les bibliothèques standards.
• Dans la mesure du possible, rendre l'application indépendante de la machine, par exemple en écrivant des programmes indépendants de la taille du mot machine. Ainsi, il faut éviter d'utiliser le type int.
• Utiliser systématiquement l'opérateur sizeof pour déterminer la taille des objets utilisés et l'opérateur de transtypage pour les conversions de type.
2.2 Esprit, règles de bonne programmation et maxime s • Le dicton "Small is beautiful" rappelle la nécessité de développer des programmes
modulaires dont chaque module ne réalise qu'une seule action garantie correcte.
• Le résultat d'exécution d'une application (sortie standard) doit pouvoir être utilisé comme donnée d'une autre donc redirigée sur l'entrée standard de cette dernière (principe du tube (pipe)).
• Utiliser les outils disponibles pour ne pas réinventer la roue.
3. PRINCIPES GENERAUX DU LANGAGE
Pour des raisons historiques, il existe deux variantes du langage C : le C traditionnel de Kernighan et Ritchie, obsolescent, et le C normalisé, beaucoup plus rigoureux.
La grammaire de ce langage est très puissante. Les expressions du langage sont constituées à partir d'instructions simples ou composées (structure de bloc), de délimiteurs, d'identificateurs.
3.1 Expressions et instructions
■ Expressions
Une expression est une variable, une "variable qualifiée constante", une expression arithmétique, logique, relationnelle, ou une fonction.
72 CHAPITRE IV ───────────────────────────────────────────────────
■ Instructions
Une instruction est une expression suivie d'un ;
expression;
� Exemple : l'instruction vide
; /* Instruction vide : ne fait rien ! */
■ Opérateur d'affectation
variable = valeur
L'affectation n'est pas une instruction mais est une opération non commutative qui retourne la valeur affectée ce qui permet les affectations multiples.
� Exemple
i=j=k=m=0; /* Annule les variables i, j, k et m. */
■ Délimiteurs
Un délimiteur est un caractère spécial permettant au compilateur de discriminer les unités syntaxiques (token) du langage. Les principaux délimiteurs sont les suivants :
; Terminateur de toute instruction et déclaration , Séparateur de deux éléments d'une liste () Délimiteurs des arguments ou des paramètres formels d'une fonction [] Délimiteur de la dimension ou d'indices dans les tableaux {} Délimiteur de début et de fin de bloc /* Délimiteur de début de commentaire */ Délimiteur de fin de commentaire
■ Structure de bloc
La structure de bloc implémente le concept d'instruction généralisée. Délimité par les caractères { et }, il a la structure suivante :
{déclaration des variables locales au bloc (optionnelles) instruction(s)
}
La portée des variables est limitée au bloc dans lequel elles sont définies.
On peut créer des blocs imbriqués, avec leurs variables locales, qui masquent les variables du bloc englobant du même nom.
� Exemple
int main(void) { int a = 1, b= 2; float x = 10.45;
printf(" a = %d b = %d x = %6.2f\n", a,b,x); { float a = 38.45, x = -32.46; /* Redéfinition des variables dans le deuxième bloc */
printf(" a = %6.2f b = %d x = %6.2f\n", a,b,x); } printf(" a = %d b = %d x = %6.2f\n", a,b,x); /* Retour aux variables initiales */
}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
73
// Résultat a = 1 b = 2 x = 10.45 a = 38.45 b = 2 x = -32.46 a = 1 b = 2 x = 10.45 Dans le cas de blocs imbriqués, toutes les variables du bloc englobant sont des variables globales du bloc englobé, sauf celles qui y sont redéfinies.
■ Objets du langage
On distingue six types d'objets en C : les mots clés, les identificateurs, les variables qualifiées constantes, les chaînes de caractères, les opérateurs, les délimiteurs.
3.2 Mots clés du langage C La liste ci-dessous récapitule pratiquement la totalité des mots clés du langage.
■ Déclarations de type
char double enum float int long short signed sizeof struct typedef union unsigned void
■ Déclarations de la classe de mémorisation
auto extern register static const volatile
■ Structures de contrôle
break case continue default do else for goto if return switch while
4. VARIABLES
4.1 Identificateur, type et classe de mémorisation En C, une variable est caractérisée par trois attributs : son identificateur, son type, sa classe de mémorisation.
■ Identificateur
L'identificateur est un symbole permettant de référencer les différents objets utilisés (variables, fonctions, procédure, etc.).
■ Alphabet
Tout symbole interne est formé d'une suite quelconque de caractères alphanumériques dont 31 au plus sont significatifs. Le caractère "_" est autorisé. Les mots clés du langage sont réservés et inutilisables comme nom de variable.
Tout identificateur de variable doit commencer par une lettre autorisée.
Le compilateur différencie les lettres minuscules et majuscules. Usuellement, les lettres minuscules sont utilisées pour les noms de variables et les lettres majuscules pour les constantes symboliques accessibles par le préprocesseur.
74 CHAPITRE IV ───────────────────────────────────────────────────
■ Type
C'est le type prédéfini (entier, réel, ...) ou non de la variable utilisée.
� Exemple
int toto; char caractere;
■ Classe de mémorisation
La classe de mémorisation des variables ou des fonctions détermine leur mode d'allocation mémoire qui peut être statique (static), dynamique (auto, type par défaut), ou dans les registres (register).
4.2 Définition et déclaration On distingue en C les notions de définition et de déclaration des identificateurs.
■ Définition
La définition d'un objet (variable, fonction, procédure) en spécifie les attributs et provoque l'allocation de l'espace mémoire nécessaire.
■ Déclaration
Une déclaration
• peut avoir différentes significations selon le contexte,
• ne provoque pas d'allocation d'espace mémoire,
• déclare les objets définis dans d'autres fichiers (déclaration extern).
• peut être située :
◊ à l'extérieur des fonctions (déclaration de variable globale),
◊ au début d'un bloc (déclaration de variable locale).
◊ entre la définition d'une fonction et son corps (C Kernighan & Ritchie).
■ Remarque
Définition et déclaration ont le même aspect et une syntaxe voisine mais représentent deux notions différentes : une déclaration permet au compilateur de se référer à une variable et de la décrire alors que la définition lui donne naissance.
4.3 Règles d'utilisation des variables Toute variable doit être définie de façon unique dans un bloc et être déclarée avant son utilisation. On peut définir une nouvelle variable avec le même nom dans un bloc englobé, qui masque la variable correspondante du bloc englobant comme ci-dessous.
int main(void) { int a = 2, b = 3; { float a = 0.; } }
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
75
4.4 Accès aux variables En langage C, les divers modes d'accès aux variables sont les suivants : accès direct d'un objet statique en mémoire, accès indirect à l'aide de pointeurs, accès indexé pour les tableaux, accès aux registres de la machine, accès du sommet de la pile de travail.
On peut ainsi réaliser toutes les opérations d'adressage des langages d'assemblage.
4.5 Lvaleur, Rvaleur et affectation Les expressions à gauche et à droite du symbole d'affectation n'obéissent pas aux mêmes règles syntaxiques. C'est pourquoi le standard définit respectivement les termes Lvaleur et Rvaleur pour désigner une expression située à gauche et à droite de l'opérateur d'affectation.
■ Lvaleur
• Une Lvaleur (Lvalue) désigne un objet en mémoire de n'importe quel type à l'exception du type void.
• Une Lvaleur modifiable ne peut être d'un des types suivants : tableau, type incomplet, type qualifié const, variable structurée ou union dont un des champs est qualifié par const.
• Seule, une Lvaleur modifiable est autorisée à la gauche de l'opérateur d'affectation.
• Une Lvaleur est convertie à la valeur définie à la droite du symbole d'affectation conformément aux règles de conversion de la grammaire du langage.
• L'origine du terme est l'abréviation américaine de l'expression "Left value".
■ Rvaleur
• Une Rvaleur est la valeur d'une expression. Le terme Rvaleur vient de l'instruction d'assignation E1=E2; dans laquelle le membre de droite est une Rvaleur.
• Contrairement à une Lvaleur, une Rvaleur peut être une variable ou une constante.
• L'origine du terme est l'abréviation américaine de l'expression "Right value", quelquefois remplacé par "la valeur d'une expression".
5. TYPES
5.1 Types de base Le type d'un objet en décrit la nature (nombre, image, caractère, traitement, etc.) permettant sa représentation interne. Ainsi, les nombres entiers sont représentés sur 16 ou 32 bits, les caractères sur un octet, etc.
Il existe un grand nombre de types prédéfinis présentés ci-dessous, à partir desquels il est possible de construire des objets structurées (variables structurées, union) ou composites.
76 CHAPITRE IV ───────────────────────────────────────────────────
La norme du langage C définit exhaustivement les types suivants.
■ Type arithmétique
L'ensemble des types entiers et flottants.
■ Type de base
Les types (signé ou non signé) caractère et entier. Le type énuméré n'y figure pas.
■ Type scalaire
L'ensemble des types arithmétiques et pointeurs
■ Type entier signé
Tous les types signed char, int, long int, short int.
■ Type entier non signé
Tous les types unsigned char, unsigned int, unsigned long int, unsigned short int.
■ Type qualifié
Type précisé par un qualificatif.
■ Type flottant
Les types float, double ou long double.
■ Type fonction
Une fonction, ses arguments typés et leur nombre, le type de l'argument retourné.
■ Type pointeur
Descripteur de l'objet pointé.
■ Type énuméré
Une énumération est constituée d'une liste finie et ordonnée de variables qualifiées constantes entières (re)nommées.
■ Type tableau
Ensemble d'objets de même type dans une zone contiguë en mémoire.
■ Type structuré
Type décrivant un groupe d'objets nommés contigus, chacun d'un type spécifique.
■ Type union
Type décrivant des objets nommés de type spécifique se recouvrant en mémoire.
■ Type agrégat
Les types tableaux et structurés.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
77
■ Type incomplet
Type ne fournissant pas au compilateur l'ensemble des informations nécessaires à la définition d'un objet. Par exemple un tableau dont la taille est inconnue, une variable structurée de contenu indéterminé. Un type incomplet est complété à l'exécution.
� Exemple
int tableau[];
■ Type intégral
Les types caractères, entier signé et non signé, et les types énumérés.
■ Type composite
Type dérivé des types de base tableaux, fonctions, pointeurs, variables structures, unions.
■ Type objet
L'ensemble des types décrivant des objets plutôt que des fonctions.
■ Type majeur
Le type majeur d'un type de base est le type lui-même.
Le type majeur d'un type dérivé est le premier type (avec les priorités) utilisé pour décrire l'objet. Ainsi, le type int * est le type pointeur sur un entier dont le type majeur est le type pointeur.
■ Type non qualifié
Tout type non qualifié par les qualificatifs const, noalias, et volatile.
5.2 Types scalaires
■ char
Caractère codé sur huit bits (ASCII étendu ou EBCDIC). Dans le cas particulier de l'ASCII, le bit de poids fort est habituellement forcé à zéro.
La taille des types n'est spécifiée dans aucune norme, sauf pour le type char (un octet). En revanche, les inégalités suivantes sont toujours vérifiées :
char ≤ short int ≤ int ≤ long int float ≤ double ≤ long double
L'opérateur ≤ signifie "a une plage de valeur plus petite ou égale que".
■ int
Entier signé dont la représentation dépend de la machine (16 ou 32 bits).
■ float
Réel flottant sur 32 bits représenté selon la norme IEEE 744.
78 CHAPITRE IV ───────────────────────────────────────────────────
5.3 Qualificatif Un qualificatif est optionnel. Il précise les déclarations de type précédentes.
■ short
Nombre entier signé court sur 16 bits dont le bit de signe.
■ long
Nombre entier représenté avec 32 bits dont le bit de signe.
■ long double
Nombre réel flottant, représenté au moins en double précision, quelquefois plus, dont la structure est définie dans le fichier en-tête limits.h.
■ unsigned
Utilisable avec les caractères, les nombre entiers court, les nombres entiers standards, les nombre entiers longs sans signe.
■ signed
Utilisable avec les caractères, les nombres entiers courts, les nombres entiers standards, les nombres entiers longs avec signe.
■ double
Nombre réel flottant en double précision, représenté au moins en simple précision selon l'implémentation, dont la structure est définie dans le fichier en-tête limits.h.
■ const
Qualificatif constant : l'objet est dans une zone de mémoire en lecture seule.
■ noalias
Identificateur d'un objet sans alias qu'il n'est possible de modifier qu'avec des pointeurs sur ce dernier.
■ volatile
Ce type interdit certaines optimisations. Ainsi, une variable invariante dans une boucle est déclarée volatile pour empêcher le compilateur de la sortir de la boucle.
� Exemple
extern int heure; /* La variable heure indique l'heure courante */ int main(void) { int i; for(i=0;i<10000;i++) printf("%d:%d\n",i,heure);}
La variable heure étant un invariant de boucle, le compilateur optimise la boucle :
tmp =heure; for(i=0;i<10000;i++) printf("%d:%d\n",i,tmp); ce qui est évité par :
extern volatile int heure; ...
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
79
■ Récapitulation des déclarations de type
char double, double float enum float int, signed, signed int long double long , long int, signed long, signed long int short, short int, signed short int, signed short struct typedef union unsigned char unsigned int, unsigned unsigned long int, unsigned long unsigned short int, unsigned short void
Sur une même ligne figurent les déclarations de types synonymes.
5.4 Tableau Un tableau est constitué d'objets d'un même type stockés consécutivement en mémoire, accessibles à partir de l'adresse du premier. Nous verrons que son nom est l 'adresse symbolique de son premier élément en mémoire.
Tout identificateur suivi de [...] représente un tableau dont les éléments sont des variables, éventuellement structurées, de n'importe quel type précédemment défini.
■ Règles d'utilisation des tableaux
Le premier indice est toujours initialisé à la valeur 0. Ainsi, la déclaration :
int tableau[10] ;
crée un tableau de 10 entiers indicés de 0 à 9.
La définition d'un tableau est récursive permettant ainsi un nombre théoriquement illimité d'indices. Les règles de priorité des opérateurs (évaluation de la gauche vers la droite des opérateurs []) indiquent que la déclaration a[m][n] crée un tableau de m tableaux de n éléments : c'est la définition de m vecteurs de n composantes.
Une chaîne de caractères est un tableau de type char.
� Exemple
int tableau[10]; /* Un tableau de 10 entiers*/ float d[10][20]; /* 10 vecteurs de 20 composantes réelles*/ char chaine[30]; /* Une chaîne de 30 caractères */ float e[10,20] /* Interdit */
80 CHAPITRE IV ───────────────────────────────────────────────────
5.5 Conversions de type implicites Toute opération impose la conversion des opérandes d'une expression arithmétique dans un type commun à partir des règles implicites suivantes :
• Une variable char ou short est convertie en objet de type int. Les types char et int sont compatibles un caractère étant remplacé par sa valeur ASCII.
• Un opérande double provoque la conversion de l'autre en long double avec un résultat long double.
• Un opérande double provoque la conversion de l'autre en double avec un résultat double.
• Un opérande float provoque la conversion de l'autre en float avec un résultat float.
• Un opérande long provoque la conversion de l'autre en long avec un résultat long.
• Un opérande unsigned provoque la conversion de l'autre en unsigned avec un résultat unsigned.
• Un opérande unsigned long int provoque la conversion de l'autre en unsigned long int avec un résultat unsigned long int.
• Un opérande long int provoque la conversion de l'autre en unsigned int si l'opérande de type long int peut représenter toutes les valeurs de l'autre; sinon les deux sont convertis en unsigned long int. Dans les autres cas, un opérande unsigned int provoque la conversion de l'autre en unsigned int avec un résultat unsigned int.
• Un opérande long int provoque la conversion de l'autre en long int avec un résultat long int.
• Les nombres entiers sont convertis en réels.
• Les calculs sur des variables réelles sont effectués en simple ou double précision.
• La plupart des conversions sont résumées dans le tableau suivant :
opérande2 char int short unsigned long float
opérande1 char int short int unsigned long float unsigned unsigned unsigned long float long long long long float float float float float float
Les instructions d'assignation suivent les règles suivantes :
• Le membre de droite est converti, après calcul, dans le type du membre de gauche, qui est donc le type par défaut. Ainsi, un caractère est converti en entier, avec ou sans extension de signe. L'opération inverse est bien sûr possible. La conversion float en int est faite par troncature. Le type double devient le type float, arrondi.
• Le mélange de tous les types entiers (short, long, unsigned, char, int) dans les expressions arithmétiques est autorisé. Ainsi, l'expression 'c'+3 a pour résultat la valeur caractère 'f' (code ASCII du caractère 'c' auquel est ajouté 3).
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
81
5.6 Conversion de type explicite On peut spécifier les conversions explicites d'opérandes en utilisant l'opérateur de transtypage ou encore opérateur de transtypage noté () encore appelé cast. Le transtypage ou la coercition est la conversion temporaire du type d'une expression.
Synopsis (type_désiré) expression_dont_on_force_le_type
ou type est un type de base prédéfini (char, int, short, long, float, double, char *, etc.) ou non. Tout se passe, au moment d'un transtypage, comme si une variable du type spécifiée par (type), était utilisée avec la valeur initiale de expression, convertie dans le type spécifié. Le membre de droite n'est alors en aucun cas modifié.
� Exemple
#include <math.h> /* Contient les prototypes des fonctions mathématiques */ int main(void) { int i , j = 10;
double x = 3.1235678, y = 3.987; double sqrt(double); /* Prototype de sqrt, calcul de la racine carrée d'un nombre */ i = x; printf(" conversion implicite : i = %d \n x = %13.8f \n",i,x); i = y; printf(" conversion implicite : i = %d \n y = %13.8f \n",i,y); i = (int) y; printf(" conversion explicite : i = %d \n y = %13.8f \n",i,y); y = i / j; printf(" conversion implicite : y = i/j = %13.8f \n ",y); y = (float) i / j; printf(" conversion explicite : y = (float)i/j=%13.8f \n ",y); printf(" conversion implicite : sqrt(i) = % 13.8f \n ",sqrt(i)); printf(" conversion explicite : sqrt(i) = %13.8f\n ", sqrt((double) i));
}
// Résultats conversion implicite : i = 3 x = 3.12356780 conversion implicite : i = 3 y = 3.98700000 conversion explicite : i = 3 y = 3.98700000 conversion implicite : y = i/j = 0.00000000 conversion explicite : y = (float) i/j = 0.30000000 conversion implicite : sqrt(i) = 1.73205081 conversion explicite : sqrt(i) = 1.73205081
5.7 Diagramme de définition des variables Les variables et leurs attributs sont définies selon le diagramme suivant :
Classe Qualificatif Type Identificateur Initialisation ;
,
82 CHAPITRE IV ───────────────────────────────────────────────────
La classe de mémorisation est de type auto par défaut et le qualificatif optionnel. Les listes de variables sont autorisées.
La déclaration du type de la variable est obligatoire comme son identificateur.
Les règles d'initialisation sont par défaut fantaisistes.
� Exemple
int main(void) { int k, w; /* Deux entiers */
short int a,b,l = 0; /* Entiers courts*/ long int longueur, i = 780987; /* Entiers longs*/ unsigned i1 = 10; /* Un entier non signé */ unsigned int i2 = i1; /* Un entier non signé*/ char c; /* Un caractère */ float rayon; /* Un flottant simple précision */ double ray; /* Un flottant double précision */ long double ld = 18.456732; /* Un flottant avec la précision maximale */ const double e = 2.71828182845905; /* Variable qualifiée constante double */ printf(" i = %ld \n e = %16.14f \n ",i,e); /* Attention au format d'impression pour les entiers longs */ printf(" i1 = %d \n ",i1); printf(" i2 = %d \n ",i2); printf(" ld = %13.6f \n ",ld);
}
// Résultat i = 780987 e = 2.71828182845905 i1 = 10 i2 = 10 ld = 18.456732
6. VARIABLES QUALIFIEES CONSTANTES
On peut qualifier, avec le qualificatif const, une variable ou un tableau pour lui imposer de rester constant après son initialisation. Les quatre types d'objets caractère, entier, flottant, énumérées sont autorisés.
Il est également possible de définir à la compilation des constantes symboliques d'un type entier, flottant, ou caractère avec la directive #define du préprocesseur.
■ Variables qualifiées constantes entières
Les variables qualifiées constantes entières sont d'un des types entiers prédéfinis. Toute variable qualifiée constante dont la valeur excède les capacités par défaut de la machine devient automatiquement de type long.
■ Variables qualifiées constantes réelles
Les variables qualifiées constantes en virgule flottante sont de type float ou double.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
83
� Exemples
const int i = 1; const long int longueur = 56789543; const float x = 3.55; const double y = 6.789876543432;
■ Variables qualifiées constantes de type caractère
Une variable qualifiée constante de type caractère est un caractère unique, entouré par des ' comme 'a' (définition littérale). Deux types de définitions :
const char car = 'c'; // Définition directe const char car = '\014'; // Code ASCII 12 format octal sous la forme'\0dd' const char car = '\0x14'; // Code ASCII 20 format hexadécimal sous la forme'\0xdd'
Des caractères spéciaux peuvent être utilisés par des instructions d'entrées/sorties. Le caractère d'échappement (escape sequence), permettant d'"échapper" à la signification primitive d'un caractère pour lui donner une signification différente, est nécessaire. Ce sont les caractères suivants :
Caractère Code ASCII Abréviation Description \\ 92 Escape Caractère d'échappement, \a 07 BELL Alerte (bell) \b 08 BS Espace en arrière (Backspace) \f 12 FF Alimentation du papier (Form Feed) \n 10 NL /LF Fin de ligne (New Line/Line Feed) \r 13 CR Retour chariot (Carriage Return) \t 09 HT Tabulation Horizontale \v 11 VT Tabulation Verticale \0 00 NULL Délimiteur de fin de chaîne.
La constante symbolique EOF est définie dans le fichier standard stdio.h. Selon les machines, elle est définie par la valeur 0 ou la valeur -1, valeur qui n'est interprétée comme marque de fin de fichier que si elle suit un caractère de fin de ligne (\n).
■ Variables qualifiées constantes de type chaîne de caractères
Une chaîne de caractères est une suite de caractères délimitée par des guillemets, représentée sous la forme d'un tableau de caractères dont le dernier caractère est obligatoirement le délimiteur de fin de chaîne '\0' (NULL), utilisé par les fonctions d'entrée/sortie pour y accéder caractère par caractère, jusqu'à sa rencontre.
� Exemple 1
const char chaine[4] = "toto";
La constante symbolique NULL, définie dans le fichier stdio.h, est également utilisée pour initialiser des pointeurs ou des objets d'un type quelconque.
84 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple 2
#include <stdio.h> int main(void) // Déclarations diverses { int i; // Variables qualifiées constantes entières
const short court = 4356; // Variable qualifiée constante entière format court const unsigned int entier = 8; // Variable qualifiée constante entière non signée const int maxline = 10; // Variable qualifiée constante entière const long l1 = 4567797L; // Variable qualifiée constante entière de type long const long l2 = 5656897l; // Variable qualifiée constante entière de type long const int tableau[5] = {0,1,2,3,4}; // Tableau de variables qualifiées constantes const int *pt = &i; // Pointeur constant sur l'adresse de i
// Variables qualifiées constantes virgule flottante et divers formats exponentiels const float pi = 3.1415, p1 = 1E-6; const float p2 = 1e-6, p3 = 0.31416e+2, const float alpha = 31.45E-3; const double beta = 2.67543288990087;
// Variables qualifiées constantes de type caractère const char car1 = 'c', car2 = '\104', chaine[5]="toto";
// Impression des résultats printf(" court = %d entier = %d\n",court, entier); printf(" maxline = %d l1 = %ld l2 = %ld\n",maxline,l1,l2); printf(" pi = % f p1 = %e p2 = %g p3 =%g\n",pi, p1, p2, p3); printf(" car1 = %c car2 = %c\n",car1,car2); printf(" alpha = %e béta = %16.15f\n", alpha,beta); printf(" i= %d &i = %x\n",i,&i); printf(" pt = %x\n",pt); printf(" car1 = %c car2 = %c chaine= %s\n",car1, car2, chaine); for(i=0;i<5;i++) printf(" tab[%1d] = %1d ",i, tableau[i]); return(1);}
// Résultat court = 4356 entier = 8 maxline = 10 l1 = 4567797 l2 = 5656897 pi = 3.141500 p1 = 1.000000e-06 p2 = 1e-06 p3 =31.416 car1 = c car2 = D alpha = 3.145000e-02 béta = 2.675432889900870 i= 5716 &i = fff4 pt = fff4 car1 = c car2 = D chaine= toto tab[0] = 0 tab[1] = 1 tab[2] = 2 tab[3] = 3 tab[4] = 4
7. OPERATEURS
Trois classes d'opérateurs sont définis en langage C : les opérateurs unaires, qui précèdent (ou suivent) un identificateur, une expression, ou une variable qualifiée, les opérateurs binaires (resp. ternaires) mettant en relation deux (resp. trois) expressions, identificateurs ou constantes.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
85
7.1 Opérateurs binaires
■ Affectation
Lvaleur = Rvaleur;
■ Opérateurs arithmétiques sur les objets de type int, float, double, long
+ - * / % opérateur modulo défini par la relation : x % y est le reste de la division entière de l'entier x par l'entier y
■ Opérateurs relationnels
> >= < <=
■ Opérateurs sur les champs de bits
& ET, | OU, ^ OU exclusif, << décalage à gauche, >> décalage à droite, ~ complément à un.
■ Opérateurs logiques
Ces opérateurs sont utilisés pour effectuer les tests booléens.
&& et logique, || ou logique, ! non logique, == identité logique, != inégalité logique.
Les opérateurs = (affectation) et == (test d'identité logique) ne sont pas équivalents.
� Exemple
for( i = 0; i < 5; i++) if ( i == 1 ) printf("%d",i);
Description Impression de i si i = 1
Soit maintenant la séquence :
for( i = 0; i < 5; i++) if ( i = 1 ) printf("%d",i);
La boucle est infinie puisque chaque test affecte la valeur 1 à la variable i.
86 CHAPITRE IV ───────────────────────────────────────────────────
7.2 L'opérateur ternaire La clause if (condition) expression
v ; else expression
f ;
s'écrit avec l'opérateur ternaire () ? : (condition) ? expressionv : expression
f
� Exemple
#include <stdio.h> int main(void) // impression des composantes d'un tableau d'entier par ligne de 10 { const n = 25; int i, a[25];
for ( i = 0; i < n; i++ ) a[i] = i; for ( i = 0; i < n; i++ ) printf( "%3d %c" , a[i], ( i % 10 == 9 || i== n-1) ? '\n' : ' ');
}
// Résultat 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Description Dix nombres sont imprimés par lignes.
La condition d'impression porte sur le caractère d'impression qui suit a[i].
Si le reste de la division de i modulo 10 est égal à 9 ou N-1, le caractère spécial \n (passage à la ligne) est imprimé. Sinon, c'est le caractère d'espacement. On obtient ainsi l'impression des coefficients du tableau a par groupe de dix en laissant un espace après chaque colonne.
� Remarque
Le test est possible dans une expression à l'intérieur des arguments de la fonction printf.
7.3 Opérateurs d'incrémentation et décrémentation Les opérateurs unaires ++ et -- sont utilisables avant ou après l'opérande :
++n incrémentation avant l'affectation ou pré-incrémentation, n++ incrémentation après l'affectation ou post-incrémentation, --n décrémentation avant l'affectation ou pré-décrémentation, n-- décrémentation après l'affectation ou post-décrémentation.
� Exemple 1 séquence équivalente effet n = 5; x = n++; x = n; n = n+1; x = 5 n = 6 n = 5; x = ++n; n = n+1; x = n; x = 6 n = 6 s[j++] = s[i]; s[j] = s[i]; j = j+1; s[j++] = s[i++]; s[j] = s[i]; j = j+1; i = i+1;
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
87
� Exemple 2
#include <stdio.h> int main(void) {int n,x,i,j, s[5]; n = 5; x=n++; /* Post incrémentation */ printf(" x = %d n = %d\n",x,n); n=5; x=++n; /* Pré incrémentation */ printf(" x = %d n = %d\n",x,n); j = 0; for(i=0;i<5;i++) s[i]=i; /* Initialisation du tableau s */ printf("incrémentation de j après affectation \n"); for(i=0;i<3;i++) {s[j++]=s[i]; printf("i = %d j = %d s[i] = %d s[j] = %d\n",i, j, s[i], s[j]); } printf("*************************\n"); for(i=0;i<3;i++) printf(" s[%d]=%d",i,s[i]); printf("\n"); j = 0; printf("incrémentation de j avant affectation \n"); for(i=0;i<3;i++) {s[++j] = s[i]; printf("i = %d j = %d s[i] = %d s[j] = %d\n",i,j,s[i],s[j]); } printf("*************************\n"); for(i=0;i<3;i++) printf(" s[%d]=%d",i,s[i]); printf("\n"); }
// Résultats x = 5 n = 6 x = 6 n = 6 incrémentation de j après affectation i = 0 j = 1 s[i] = 0 s[j] = 1 i = 1 j = 2 s[i] = 1 s[j] = 2 i = 2 j = 3 s[i] = 2 s[j] = 3 ************************* s[0]=0 s[1]=1 s[2]=2 incrémentation de j avant affectation i = 0 j = 1 s[i] = 0 s[j] = 0 i = 1 j = 2 s[i] = 0 s[j] = 0 i = 2 j = 3 s[i] = 0 s[j] = 0 ************************* s[0]=0 s[1]=0 s[2]=0
7.4 Opérateurs composés L'opérateur d'affectation peut être composé avec les opérateurs binaires + - * / % << >> &.
Soient e1 et e2 deux expressions, l'instruction :
e1 opérateur = e2;
est équivalente à l'instruction :
e1 = (e1) opérateur (e2);
� Exemples
a += i; s'écrit également a = a+i; a *= i; s'écrit également a = a*i; a *= y-2; s'écrit également a = a*(y-2);
88 CHAPITRE IV ───────────────────────────────────────────────────
7.5 Opérateurs sur les champs de bits Les opérations sur les bits peuvent être effectuées sur la représentation interne des opérandes de type entier signé ou non. Leur principe est d'opérer simultanément sur l'ensemble des bits de la représentation du nombre. Plus précisément, soient a,b et c trois nombres entiers non signés et soit n le nombre total de bits de leur représentation. On a alors :
a = ana
n-1...a
1
b = bnb
n-1...b
1
c = cnc
n-1...c
1
Soit T un opérateur sur les bits de a,b,c. Le résultat de l'opération :
c = a T b
est défini bit à bit par les opérations :
ci = a
i T B
i , ∀ i = 1,n
Les opérateurs sur les champs de bits sont :
■ L'opérateur &
Forme : opérande1 & opérande2
Description : résultat de type entier de l'opération et logique sur chacun des bits des deux opérandes.
■ L'opérateur |
Forme : opérande1 | opérande2
Description : résultat de type entier de l'opération ou logique sur chacun des bits des deux opérandes.
■ L'opérateur ^
Forme : opérande1 ^ opérande2
Description : résultat de type entier de l'opération ou exclusif sur chacun des bits des deux opérandes.
& ET | OU ^ OU exclusif << décalage à gauche >> décalage à droite ~ complément à un
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
89
■ L'opérateur <<
Forme : opérande1 << opérande2
Description : décalage logique vers la gauche de opérande2 bits de opérande1. Les
bits de droite de la représentation binaire sont remplacés par des zéros.
■ L'opérateur >>
Forme : opérande1 >> opérande2
Description : décalage logique vers la droite de opérande2 bits de opérande1 avec
extension du signe. Les bits de gauche sont remplacés par des zéros () quand le bit de signe est nul, par des uns quand il vaut 1.
■ L'opérateur ~
Forme : ~ opérande
Description : résultat de type entier de l'opération complément à un sur chacun des bits de opérande.
� Exemple
L'algorithme de la multiplication égyptienne permet d'effectuer la multiplication de deux nombres entiers positifs en utilisant les opérateurs logiques. Soit z le résultat cherché du produit x*y.
Début lire x,y z = 0 Tant que y est non nul faire si y pair alors y = y/2 x = 2*x sinon y = y-1 z = z+x is Finfaire
Fin
Cet algorithme est programmé uniquement avec des instructions logiques et des instructions de décalage :
• pour réaliser le test de parité, on définit un "masque" avec la valeur 1. On effectue l'opération logique et sur la variable y et sur le masque. La variable y est impaire si le résultat est 1, paire sinon.
• pour multiplier un nombre par deux, il suffit de décaler d'un bit vers la gauche sa représentation binaire; de même, il suffit de décaler d'un bit vers la droite sa représentation binaire pour le diviser par deux.
90 CHAPITRE IV ───────────────────────────────────────────────────
7.6 Associativité des opérateurs binaires Les opérateurs binaires sont les suivants :
Affectation =
Opérateurs arithmétiques + -* / %
Opérateurs relationnels > >= < <=
Opérateurs logiques && et logique || ou logique ! non logique == identité logique != inégalité logique
Opérateurs sur les champs de bits & ET | OU ^ OU exclusif << décalage à gauche >> décalage à droite ~ complément à un
Opérateurs unaires ++ pré ou post incrémentation -- pré ou post décrémentation
■ Priorité et règles d'associativité des opérateurs binaires
Le tableau ci-dessous présente les règles d'associativité ou ordre d'évaluation des opérateurs (y compris ceux qui sont présentés plus loin). Les lignes sont présentées par ordre de priorité décroissante et les opérateurs de même priorité sont sur la même ligne.
Opérateurs Ordre d'évaluation () [] ->. gauche à droite ! ~ ++ -- + - * & (type) sizeof droite à gauche * / % gauche à droite + - gauche à droite << >> gauche à droite << = >= > < gauche à droite == != gauche à droite & gauche à droite ^ gauche à droite | gauche à droite && gauche à droite || gauche à droite ? : gauche à droite = += -= etc. droite à gauche , gauche à droite
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
91
8. FONCTIONS ET PROCEDURES
8.1 Définitions et règles d'utilisation
■ Définition normalisée
En C, une fonction est une application
(E1 X E2 X... X En) → ℜ
où (Ei)i=1,n sont les domaines de définition des arguments de la fonction et ℜ est le domaine de valeur du résultat.
• Des arguments de type scalaire prédéfini (entier, réel, caractère, pointeur, void) ou non (objet structuré,etc.) sont autorisés.
• Le type par défaut du résultat d'une fonction est int.
• La déclaration et la définition des fonctions permettent le contrôle du nombre et du type des arguments à la compilation par la génération de messages d'erreur ou d'avertissement (warning).
• La prudence impose au programmeur d'éliminer de son programme tous les message d'avertissement, sauf peut-être ceux dont il connaît précisément la raison.
La définition d'un objet, unique dans un programme, provoque l'allocation de la mémoire nécessaire à son utilisation.
La syntaxe est la suivante :
type_de_ℜ identificateur_de_la_fonction( type du paramètre_formel identificateur_paramètre_formel, ... type du paramètre_formel identificateur_paramètre_formel) {liste de variables locales corps de la fonction }
Type Identificateur ( Type Argument formel ) Type Argument_formel ,
Diagramme de la déclaration des arguments formels
� Exemple 1
// Définition de la fonction addition et déclaration de type des arguments int addition(int a, int b) // Corps de la fonction { int aux; // Variables locales à la fonction aux = a+b; return(aux); // Transmission du résultat à la fonction appelante }
92 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple 2
int addition(int a, int b) // Définition de la fonction addition { return(a+b); } // Transmission du résultat à la fonction appelante
■ Identificateur
Un identificateur non préalablement déclaré utilisé dans une expression et suivi d'une parenthèse gauche sera déclaré, par contexte, comme celui d'une fonction.
■ Fonctions imbriquées
La norme interdit de définir une fonction dans une autre. Toutes les fonctions sont externes.
■ Fonction d'appel
Une fonction est d'accès public sauf si elle est qualifiée static.
■ Ordre des définitions
L'ordre des définitions des différentes fonctions dans un fichier est sans importance.
8.2 Déclaration et prototypes La syntaxe du C étant très permissive sur ce plan, un prototype est une déclaration facultative dans la fonction appelante, qui peut éventuellement décrire le nombre et le type des arguments et du résultat de la fonction appelée, permettant ainsi à l'analyseur syntaxique de contrôler les appels de fonction à la compilation et de détecter divers types d'erreurs :
• implémentation d'une fonction non conforme à la norme,
• erreur dans le nombre ou le type des arguments d'appel,
• erreur dans le type du résultat.
Les noms d'arguments utilisés dans le prototype ne sont pas visibles à l'extérieur et n'affectent en aucune manière les autres variables de la fonction. Il est même possible de les omettre.
Un prototype peut apparaître plusieurs fois dans le corps d'un programme.
Il faut les regrouper avec les autres déclarations et définitions (fichiers en tête...).
� Exemple
// Prototypes normalisés int addition (int, int); // Addition à valeur entière avec deux arguments de type entier double simpson(double); // Simpson à valeur double avec un argument de type double float *f(int, double); // f retourne un pointeur sur un flottant, un argument entier, l'autre de type double.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
93
8.3 Le mot clé void On distingue plusieurs utilisations du mot clé void :
• Définition et prototype de fonctions sans argument,
• Type conventionnel d'une procédure (fonction ne retournant aucun résultat),
• Pointeurs ou expressions.
Nous étudions ici les deux premiers cas.
■ Fonction sans argument
Le type void utilisé dans la définition et le prototype de la fonction indique que la liste des paramètres formels est vide. Son omission signifie que rien n'est précisé sur les arguments donc que tout est possible et autorisé.
Synopsis : identificateur_de_fonction(void ) { ...}
� Exemple
int f(void ); // Prototype : aucun argument autorisé int g(); // Prototype imprécis : tout est possible f(); // Seul appel autorisé g(a,b); // Appel autorisé
■ Procédure
Rien n'imposant d'utiliser le résultat d'exécution d'une fonction, une instruction se limitant à son appel est valide. Sur le plan syntaxique, une procédure est une fonction ne retournant aucun résultat ce qui s'indique par la déclaration void.
Synopsis : void identificateur_de_procédure(...);
� Exemple
int main(void ) { void f(void ); // Prototype de la procédure f f(); // Appel de f return(1); }
void f(void ) // Définition de la procédure f { printf ("procédure f\n");}
8.4 Résultat : transmission et type
■ Transmission du résultat
Le résultat d'exécution d'une fonction est transmis à l'instruction appelante par l'instruction return.
Synopsis return(expression) ou return expression
Dans le cas où expression est vide, il n'y a pas de résultat retourné par la fonction. Si le type retourné est non entier (type par défaut), il est nécessaire de le spécifier dans la définition de la fonction pour éviter une erreur d'exécution
94 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple
int main(void ) { int i = 1, j = 2; printf(" addition (i,j) = %d\n",addition (i,j)); return(1); }
addition (int a,int b) // Définition de la fonction addition { int c; // Variable locale c = a + b; return(c); // Transmission du résultat au programme appelant } // Résultat addition (i,j) = 3
■ Type du résultat
En langage C, la déclaration du type du résultat d'une fonction est facultative le type par défaut étant int. Il est donc fortement recommandé de toujours le spécifier par un prototype pour faciliter la détection d'éventuelles erreurs de compilation de fonctions définies dans différents fichiers, avec des risques d'incompatibilité (nombre ou type de ses arguments).
En langage C++, le maquettage des fonctions et méthodes est impératif.
� Exemple
int main(void ) { double sphere(double); // Prototype double f = 1.; printf( " volume de la sphère = % e ", sphère(f)); return(1); }
double sphere(double rayon) // Définition du type du résultat de la fonction et du type de l'argument { double pi = 3.1416, v; v = 4./3*pi* rayon* rayon* rayon; return(v); }
// Résultat volume de la sphère = 4.188800e+000 Supposons le cas suivant :
int main(void ) { float f (...);… // Prototype de f
f(...);… // Appel de f }
Dans une compilation simultanée de la fonction main et de la fonction f (fonctions main et f dans le même fichier), le type du résultat est vérifié. Dans une compilation séparée, la fonction f est par défaut à valeur entière dans la fonction main. En l'absence de prototypes, cette erreur est indétectable.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
95
8.5 Appel Une fonction est toujours appelée depuis une autre car toutes sont définies dans des blocs externes.
Un appel de fonction invoque une fonction à un point particulier du programme. Sa forme syntaxique est l'identificateur de la fonction suivi d'une paire de parenthèses contenant la liste des arguments effectifs qui peuvent être des constantes, des variables, des expressions, ou des appels de fonction.
� Exemple
int main(void ) { int a = 1 ,b = 2, y; int add(int, int); /* Prototype de add */ y = add(a,b); /* Appel de add */ return(1); } int add (int a, int b) /* Définition de la fonction add et de ses arguments formels */ { /* Corps de la fonction add */ }
8.6 Récursivité Une fonction récursive s'appelle elle même et ne nécessite aucune autre déclaration.
� Exemples
La suite de Fibonacci est définie par :
>∀+===
1 n u u u
1 u u
2-n1-nn
10
Cette suite est définie récursivement (arborescence binaire) et on démontre aisément que la profondeur de la récursivité est en O(2n).
#include <stdio.h> int main(void ) /* Suite de Fibonacci */ { long int n = 10, i;
long fib(long ); /* Prototype */ printf("\n nombre n : "); for(i = 0;i<n;i++) printf("\n i = %3d fib(i) = %ld",i,fib(i)); return(1);
}
long fib(long n) { if (n < 2) return(1);
else return(fib(n-1) + fib(n-2)); }
96 CHAPITRE IV ───────────────────────────────────────────────────
9. POINTEURS
Un pointeur opère sur une variable dont on veut accéder au contenu par utilisation d'une référence à son emplacement en mémoire.
Ils permettent en particulier d'accéder aux composantes d'un tableau. C'est une fonctionnalité essentielle du langage C qui en fait toute la puissance.
Des pointeurs sur n'importe quel objet peuvent être définis (variables, tableaux, fonctions, etc.).
La définition mathématique de ces notions est explicitée par de nombreux exemples.
9.1 Définitions Un pointeur est une variable contenant l'adresse d'une autre variable permettant son accès indirect.
Soient x une variable typée et px une variable pointeur sur x. On définit deux
opérateurs mathématiques A et C de la façon suivante :
A : x → A(x) = px
C : px → C(px) = x
L'opérateur A fournit l'adresse de x et l'opérateur C le contenu de px. On démontre
aisément que les fonctions A et C sont des fonctions réciproques. D'où les identités :
C(px) = C(A(x)) = x A(x) = A(C(px)) = px
■ Opérateurs de référence et de déréférenciation
En langage C, les notations suivantes sont utilisées :
• l'opérateur A, dit de référence ou d'adresse et noté & , fournit l'adresse de l'objet concerné. Il n'est employé qu'avec des variables ou des tableaux.
• l'opérateur C, dit de déréférenciation ou d'indirection et noté *, accède au contenu d'un pointeur.
La syntaxe du langage omet l'écriture des parenthèses ce qui conduit aux définitions usuelles des opérateurs & et * suivantes :
&x = adresse de la variable x *px = contenu de l'adresse contenue dans px
� Exemple
int i = 7; int *pi,** ppi; /* pi est un pointeur sur un entier */ /* ppi est un pointeur sur un pointeur sur un entier */ pi = & i; /* Initialisation de pi */ ppi = π /* Initialisation de ppi */
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
97
ppi pi i
&pi &i 7
et on a
*ppi = pi *pi = i =7 et donc ** ppi = * (*ppi) = * (*& pi) = *pi = i = 7
On a par définition l'équivalence fondamentale :
px = &x et y = *px ⇔ y = x
D'où on déduit :
*px = x (1) ⇔ px = &x (2)
■ Corollaire 1
Au sens mathématique, les opérateurs * et & sont des fonctions réciproques. On a donc l'identité :
*& x = x
Démonstration : par application des formules (1) et (2)
*px = x = *& x
■ Corollaire 2
Tout pointeur est typé car *px est une variable dont le type est celui de l'objet pointé
ce qui justifie les déclarations de la forme :
int *pa; double *py;
■ Corollaire 3
Dans les instructions d'affectation, les variables peuvent être remplacées par des pointeurs. On suppose que *px = x
Compte tenu de la hiérarchie des opérateurs, on a :
x = 0; ⇔ *px = 0; y = x+1; ⇔ y = *px+1; x = x+1; ⇔ *px += 1; x++ ; ⇔ (*px)++ ;
■ Corollaire 4
Soient deux pointeurs px et py sur des objets de même type. Alors l'instruction
px = py;
est licite et recopie le contenu du pointeur py dans le pointeur px.
98 CHAPITRE IV ───────────────────────────────────────────────────
9.2 Transmission par adresse et par valeur Deux modes de transmission des arguments sont implémentés en C :
■ Mode de transmission par adresse
L'argument d'appel est directement transmis par son adresse ce qui permet à la fonction appelée d'utiliser la variable originelle. La grammaire du langage C utilise explicitement les pointeurs pour effectuer ce mode de transmission dont un des inconvénients est le suivant : la pérennité des variables transmises n'est pas garantie puisque elles sont modifiables par la fonction appelée. Or, une erreur d'indice dans un tableau transmis risque de provoquer l'écrasement des variables stockées avant ou après l'espace alloué au tableau.
Un tableau est transmis par son adresse son nom étant un pointeur constant déréférencé.
� Exemple
La fonction strcat de concaténation des chaînes de caractères chaine1 et chaine2.
#include <stdio.h> #define LON 500 int main(void ) { char chaine1[LON], chaine2[LON];
int strcat(char [] , char []); printf("\t programme de concaténation de chaîne\n\t saisir dans l'ordre chaine1, chaine2\n"); scanf("\n %s",chaine1); scanf("\n %s",chaine2); strcat(chaine1,chaine2); return(1);
}
strcat(char s[],char t[]) { int i,j;
i = j = 0; while (s[i] != '\0') i++ ; // Lecture de la première chaîne while((s[i++ ]= t[j++ ]) != '\0'); // Remplissage de la première chaîne avec la deuxième printf("\n chaîne totale %s ",s);
}
■ Mode de transmission par valeur
L'argument d'appel est sauvegardé dans une pile d'exécution et une variable auxiliaire, initialisée avec cette valeur de l'argument d'appel, est utilisée lors de l'exécution de la fonction ce qui rend la variable initiale accessible par la fonction appelée. Ce mode, indispensable pour la gestion interne de la récursivité, garantit en outre l'intégrité dans la fonction appelante des variables transmises par valeur.
L'exemple suivant met en évidence la nécessité de pouvoir utiliser ces deux modes.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
99
� Exemple
Procédure de permutation de deux variables avec une transmission par valeur.
int main(void ) { int a = 1 ,b = 2; void swap (int, int); // Prototype de swap printf( " a= %d b= %d\n", a,b); swap(a,b); // Appel de la procédure swap printf( " a= %d b= %d\n", a,b); return(1); }
void swap (int x, int y) // Définition de la procédure swap { int aux; // Corps de la procédure swap aux = x; x = y; y = aux; }
// Résultats a= 1 b= 2 a= 1 b= 2
■ L'art du compromis
La transmission par adresse est délicate à programmer les objets étant des variables dans la fonction appelante et des pointeurs déréférencés dans la fonction appelée. La transmission par référence, similaire, en simplifie l'écriture.
Toutes variable transmise par valeur est sauvegardée dans la pile d'exécution. La fonction appelée est exécutée avec une copie ce qui peut être pénalisant dans le cas ou l'espace mémoire occupé par l'argument effectif est important.
L'utilisation du mot clé const permet de faire une transmission par adresse ou par référence en garantissant que la variable transmise n'est pas modifiée ce qui permet un gain d'espace mémoire en assurant la pérennité des arguments transmis.
9.3 Objets composites
■ Pointeur sur un objet
Classe Type * Identificateur ;
,
DIAGRAMME DE DEFINITION DES POINTEURS
identificateur est une variable scalaire (int, float, etc.), structurée, un tableau.
■ Pointeur sur une fonction
Type_résultat ( * identificateur_pointeur) ( type_argument,...)
DIAGRAMME DE DEFINITION DES POINTEURS SUR DES FONCTIONS
100 CHAPITRE IV ───────────────────────────────────────────────────
9.4 Construction d'objets composites Les règles de priorités et d'associativité des opérateurs permettent les définitions d'objets suivantes :
Pointeur sur une variable scalaire int *p;
Tableau de pointeurs int *tab[3]; // tab est un tableau de trois pointeurs sur des entiers.
Pointeur sur une variable flottante float *pt; // pt est un pointeur sur un flottant.
Pointeur sur un tableau int (* pt)[3]; // pt est un pointeur sur un tableau de 3 entiers.
Fonction retournant un pointeur int *phi();
phi, fonction avec des arguments indéterminés retournant un pointeur sur un entier.
Pointeur sur une fonction float(*phi)();
phi, pointeur sur une fonction avec des arguments indéterminés, à valeur flottante (évaluation de gauche à droite).
Pointeur sur un tableau float (*phi)[];
phi est un pointeur sur un tableau de flottants (associativité des opérateurs () et []).
Fonction et variables structurés struct *(*f( float, char *))[];
f est une fonction avec un argument de type float et un argument de type char *, retournant un pointeur sur un tableau de pointeurs sur des structures.
N'importe quoi float (*(*(*f())[])())[];
f, fonction avec des arguments indéterminés retournant un pointeur sur un tableau de pointeurs sur une fonction retournant un pointeur sur un tableau de flottants.
Fonctions et pointeurs int *comp(void * , int *);
comp est une fonction retournant un pointeur sur un entier avec deux arguments de type respectif void* et int*.
int (*comp)(float, void *);
comp est un pointeur sur une fonction retournant un entier avec deux arguments de type respectif float et void *.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
101
10. ARGUMENTS DE FONCTIONS ET POINTEURS
La transmission des arguments par adresse est réalisée de la façon suivante : le programme appelant transmet les adresses des variables concernées; les déclarations des variables formelles sont modifiées en conséquence.
� Exemple
#include <stdio.h> // Permutation de deux entiers : transmission par adresse int main(void) { int a = 2 ,b = 3; void swap(int *, int *); // Prototype swap (&a, &b); // Appel printf("\n a= %d b= %d",a,b); return(1); }
void swap (int *px, int *py) // Définition { int temp = * px; *px = * py; *py = temp; }
// Résultat a = 2 b = 3 a = 3 b = 2
■ Application aux fonctions de la bibliothèque C scanf et printf
Une saisie modifiant une variable, scanf transmet ses arguments par adresse.
Toute variable à imprimer est transmise par valeur à la fonction printf.
11. TABLEAUX ET POINTEURS
Nous présentons les relations fondamentales entres les pointeurs et les tableaux qui justifient la gestion des suites d'objets d'un même type en mémoire.
11.1 Calcul d'adresse Soient un tableau a[N] d'objets d'un type quelconque et pa0 un pointeur du même type.
Si pa0 = &a
0 alors :
pa0++ pointe en type sur l'objet qui suit x
0 et
pa0 += i pointe sur le i-ème objet suivant de même type.
a0 a
1 ... a
i ⇑ ⇑ ⇑ ⇑ pa0
pa0+1 ... pa0+i
Vu les règles de priorité et d'associativité des opérateurs, l'expression *p++ est équivalente à *(p++), l'incrémentation du pointeur étant exécutée avant l'indirection.
102 CHAPITRE IV ───────────────────────────────────────────────────
11.2 Correspondance tableau pointeur
■ Théorème 1
Soient a[N] un tableau de variables d'un type donné, pa un pointeur sur une variable du même type et x une variable du même type définis par les instructions :
type a[N], *pa, x; pa = &a[0]; (1)
L'instruction :
x = * pa; (2)
est équivalente à l'instruction
x = a[0];
Démonstration La déclaration
type a[N];
définit un tableau de N objets consécutifs référencés par a[0],..., a[N-1]. Par application de l'opérateur * à la relation (1), on obtient :
*pa = *& a[0] = a[0] = x (relation 2)
■ Théorème 2
En C, l'identificateur d'un tableau est un pointeur constant déréférencé contenant l'adresse de son premier élément. De plus, quelque soit le type des variables du tableau, on a les identités mathématiques :
pa ≡ a *(pa+ i) ≡ a[i] ∀ i ∈ Z
Démonstration L'identificateur du tableau est un synonyme de l'adresse du premier élément. D'où
pa = &a[0] = a
Un identificateur de tableau étant un pointeur constant déréférencé et un pointeur une variable, il est interdit d'écrire l'instruction :
a = pa // Reviendrait à affecter pa à la constante a.
La deuxième relation découle directement de la définition du paragraphe précédent.
■ Théorème 3
On a, pour tout tableau d'objets d'un type donné les identités fondamentales :
a[i] = *(a+i) et *(pa+i) = pa[i]
Démonstration Compte tenu du théorème 2, on a :
*(a+ i) = * (pa+ i) = a[i] ce qui démontre la première relation.
La deuxième relation en découle directement les objets a et pa étant identiques.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
103
■ Conclusions fondamentales
• En C, toute opération sur les tableaux peut être réalisée par des pointeurs.
• Un tableau n'est pas un pointeur. Seul, son identificateur en est un.
• La définition d'un tableau provoque la réservation de l'espace mémoire nécessaire à toutes ses composantes.
• La définition d'un pointeur ne réserve que l'espace nécessaire à son stockage.
• Un programme utilisant des pointeurs est plus rapide.
■ Corollaire
Utilisées comme paramètres formels d'une fonction, les expressions
type a[] et type *a
sont équivalentes, quelque soit le type de base du tableau a.
Ainsi, les paramètres formels suivants sont équivalents.
char a[] et char *a int b[] et int * b
� Exemple
#define TAILLE 2 int main(void ) // Les tableaux ci-dessous sont imprimés en utilisant des pointeurs. {long tab1[TAILLE] , *pt1 = tab1; short i, tab2[TAILLE] , *pt2 = tab2; float reel[TAILLE] , *preel = reel; void * v1, *v2;
for ( i = 0; i < TAILLE; i++) {tab1[i]= (long) (2*i); tab2[i] = 2*i+ 10; reel[i] = (float) (2*i + 20); }
for (i = 0 ; i < TAILLE; i++) { printf(" tab1[%d] = %d\n", i , tab1[i]); printf(" tab2[%d] = %d\n", i , tab2[i]); printf(" reel[%d] = %6.3f\n", i , reel[i]); }
printf( " impression de *(tab1+i), *(tab2+i), *(reel+i)\n");
for (i = 0 ; i < TAILLE; i++) { printf(" tab1[%d] = %d\n", i , *(tab1+i)); printf(" tab2[%d] = %d\n", i , *(tab2+i)); printf(" reel[%d] = %6.3f\n", i , *(reel+i)); }
printf(" impression de *tab1+i, *tab2+i, *reel+i\n"); for (i = 0 ; i < TAILLE; i++) { printf(" tab1[%d] = %d\n", i , *tab1+i); printf(" tab2[%d] = %d\n ", i , *tab2+i); printf(" reel[%d] = %6.3f\n", i , *reel+i); } printf( " impression de *pt1+i, *pt2+i, *preel+i\n"); for (i = 0 ; i < TAILLE; i++) { printf(" tab1[%d] = %d\n", i , *pt1+i); printf(" tab2À%d] = %d\n", i , *pt2+i); printf(" reel[%d] = %6.3f\n", i , *preel+i); }
104 CHAPITRE IV ───────────────────────────────────────────────────
v1 = pt1; v2 = (void *) pt1 ; /* Manipulation sur les pointeurs génériques */ printf(" &tab1 = %x &tab2 = %x\n", tab1 , tab2 ) ; printf(" v1 = %x v2 = %x pt1 = %x\n", v1 , v2, pt1); printf(" pt2 = %x void * pt2 = %x\n", pt2, (void *) pt2); printf(" impression de pt1[i], pt2[i] , preel[i] \n"); for (i = 0 ; i < TAILLE; i++) { printf(" tab1[%d] = %d\n", i , pt1[i]); printf(" tab2[%d] = %d\n", i , pt2[i]); printf(" reel[%d] = %6.3f\n", i , preel[i]); } return(1); }
// Résultat tab1[0] = 0 tab2[0] = 10 reel[0] = 20.000 tab1[1] = 2 tab2[1] = 12 reel[1] = 22.000 impression de *(tab1+i), *(tab2+i), *(reel+i) tab1[0] = 0 tab2[0] = 10 reel[0] = 20.000 tab1[1] = 2 tab2[1] = 12 reel[1] = 22.000 impression de *tab1+i, *tab2+i, *reel+i tab1[0] = 0 tab2[0] = 10 reel[0] = 20.000 tab1[1] = 1 tab2[1] = 11 reel[1] = 21.000 impression de *pt1+i, *pt2+i, *preel+i tab1[0] = 0 tab2[0] = 10 reel[0] = 20.000 tab1[1] = 1 tab2[1] = 11 reel[1] = 21.000 &tab1 = ffec &tab2 = ffe8 v1 = ffec v2 = ffec pt1 = ffec pt2 = ffe8 void * pt2 = ffe8 impression de pt1[i], pt2[i] , preel[i] tab1[0] = 0 tab2[0] = 10 reel[0] = 20.000 tab1[1] = 2 tab2[1] = 12 reel[1] = 22.000
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
105
11.3 Opérations algébriques sur les pointeurs D'un point de vue logique, il est seulement possible de comparer des pointeurs pointant sur des objets d'un même tableau. Seules sont autorisées les opérations suivantes : addition ou soustraction de constante entière à un pointeur, soustraction de pointeurs de même type, comparaison de pointeurs.
■ Addition et soustraction
L'addition (resp. la soustraction) d'une constante à un pointeur signifie que l'on pointe sur n objets après (resp. avant) le pointeur. Les opérateurs correspondants sont respectivement notés + et -.
■ Comparaison
Les comparaisons sont à effectuer au sens d'une relation d'ordre (avant ou après). Les opérateurs relationnels sont < <= > >=.
■ Initialisation
Pour initialiser un pointeur, il faut procéder comme suit :
int i= 5; // Définition et initialisation de i int *pi = & i; // Définition et initialisation du pointeur pi
On peut aussi initialiser un pointeur à la valeur NULL en écrivant :
int *pi; pi = (int *) NULL;
Si on souhaite que pi pointe sur le caractère i, il faut utiliser l'opérateur de transtypage (cast) pour définir le type de l'objet pointé de la façon suivante :
int i = 5; char *pi = (char*) &i;
L'initialisation d'un pointeur sur une fonction s'écrit :
int (*pfonc)(void) = fonc;
La variable pfonc est un pointeur sur l'adresse de la fonction entière sans argument fonc.
■ Conversion du type de l'objet pointé
Un pointeur sur un objet d'un type donné peut être converti en un pointeur sur un objet d'un type différent. Le pointeur résultant peut être incohérent si le pointeur d'origine ne fait pas référence à un objet correctement aligné en mémoire.
La norme ANSI garantit qu'un pointeur sur un objet d'un type donné peut être converti en un pointeur sur un objet d'un type différent si ce dernier a un alignement strictement identique ou inférieur.
106 CHAPITRE IV ───────────────────────────────────────────────────
12. CHAINES DE CARACTERES ET POINTEURS
Une chaîne de caractères est un tableau de caractères, accessible par un pointeur sur son premier élément et toutes les fonctions de manipulation de chaînes de caractères peuvent être développées avec des pointeurs dont voici une application étonnante.
■ Initialisation
Considérons le programme suivant :
int main(void ) { char *chaine = "bonjour", *pchaine = chaine;
// Création d'une chaîne de caractères accessible par le pointeur pchaine printf( " chaîne initiale : %s\n",chaine); // Impression caractère par caractère while (*chaine!= '\0') { printf("%c",*chaine);chaine++ ;} // Le pointeur a été modifié d'où aucune impression printf ("\n aucune impression %s\n",chaine); // Réinitialisation du pointeur chaine= pchaine; printf(" chaîne totale %s \n",chaine); return(1);
}
// Résultat chaîne initiale : bonjour bonjour aucune impression chaîne totale bonjour
■ Interprétation
• La variable chaine est de type pointeur. Sa définition provoque la définition d'une chaîne de huit caractères que l'on peut imprimer (premier ordre d'impression).
• L'impression caractère par caractère prouve que la variable chaine est bien de type pointeur (deuxième impression).
• Le pointeur chaine ayant été incrémenté jusqu'à la fin de la chaîne, il n'y a plus rien à imprimer (troisième impression).
• Une réinitialisation du pointeur donne la dernière impression.
� Remarque 1
La chaîne de caractères crée est sans nom explicite car l'instruction
char *chaine = "bonjour";
définit un pointeur sur une chaîne de caractères et l'initialisation provoque l'allocation de mémoire nécessaire à la gestion de la chaîne de caractères comme le confirme le programme suivant.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
107
#include <stdio.h> int main(void ) { char *chaine ="bonjour";
printf("chaine = %s sizeof(chaine) = % d\n", chaine, sizeof(chaine)); printf("&chaine = %x *chaine = %c \n",&chaine, *chaine); printf("sizeof(*chaine) = % d\n", sizeof(*chaine)); return(1);
}
// Résultats chaine = bonjour sizeof(chaine) = 2 &chaine = ffde *chaine = b sizeof(*chaine) = 1
� Remarque 2
Soient les deux instructions :
char message[] = "Bonjour Monsieur "; char *pmessage = "Bonjour Monsieur ";
message est un tableau dont la taille est fixée à la définition, pmessage est un pointeur initialisé sur le premier caractère d'une chaîne non nommée.
� Exemples
char string[30] = "bonjour"; char chaine[30],*pchaine = "bonjour"; while(*pchaine != '\0') chaine[i++ ]=* pchaine++ ; chaine[i++ ]= '\0'; for (i=0;*pchaine != '\0';chaine[i++ ] = * pchaine++ ); chaine[i] = ' \0';
■ Taille
Le calcul de la taille d'une chaîne de caractère illustre la puissance et la concision de l'utilisation des pointeurs dont voici des versions de plus en plus compactes.
int str(char *s) { int n;
for (n = 0; *s != '\0'; s++ ) n++ ; return(n);
}
char *str(char *s) { char *n = s; // Initialisation de n, pas de *n
while (*n != '\0') n++ ; // clause identique à while (*n) n++ ; return(n-s); // Soustraction de pointeurs
}
Les deux dernières instructions peuvent aussi s'écrire :
while (*n++ ) ; return(n-1-s);
108 CHAPITRE IV ───────────────────────────────────────────────────
■ Comparaison
La fonction strcmp compare, caractère par caractère et dans l'ordre lexicographique, le nombre et la valeur des occurrences de deux chaînes de caractères. Suivant la valeur de l'argument de retour, la longueur de s est inférieure à celle de t s'il est négatif, égale à celle de t s'il est nul, supérieure à celle de t s'il est positif. Voici deux versions : une avec tableau, l'autre avec pointeur.
int strcmp(char s[], char t[]) // Comparaison de chaînes de caractères { int i = 0; while (s[i] == t[i]) if (s[i++ ] == ' \0') return(0); return(s[i] - t[i]); }
char *strcmp (char *s, char *t) { for (; *s = * t; *s++, * t++ ) if (*s == ' \0') return (0); return(*s - *t); }
13. APPLICATIONS DES POINTEURS
■ Transmission d'arguments à la fonction main
Une application fondamentale de la notion de tableaux de dimension incomplète est d'écrire des fonctions dont le nombre d'arguments d'appel peut être variable. Cette propriété permet de transmettre un nombre quelconque d'arguments à une commande sous forme de chaînes de caractères à son appel.
Le principe de base est le suivant : deux arguments sont transmis lors de l'appel de la fonction main. Ce sont :
int argc; char *argv[]; // Nombre et liste des arguments d'appel.
� Exemple
int main(int argc, char *argv []) { int i;
for (i = 0; i < argc; i++ ) printf("%s%c",argv[i], (i<argc-1) ? ' ' : '\n'); return(1);
}
La boucle peut avoir la forme
while(--argc >=0) printf("%s%c",*argv++, (argc >0)?' ' : '\n');
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
109
■ Pointeur sur des tableaux et tableaux de pointeurs
Des pointeurs sur des tableaux permettent, pour des tableaux à plusieurs indices, d'accéder directement à une ligne du tableau, sans en modifier les composantes.
� Exemple
// Permutation des composantes d'un tableau de chaînes de caractères char * tab[3] = {"Bonjour","Demain","Aujourd'hui"} ; char * aux; aux = tab[0]; tab[0] = tab[2]; tab[2] = aux ;
■ Transtypage et pointeurs
Il est dangereux d'utiliser sans précaution l'opérateur de transtypage sur des pointeurs. Supposons par exemple le programme suivant :
char *c; int *a; c = (char *) a;
Le compilateur choisit un des octets pointés par a et retourne son adresse. Le choix est toujours le même sur une machine donnée (l'octet de poids faible ou de poids fort) mais il varie d'une implémentation à une autre.
■ Pointeurs sur des fonctions
L'identificateur d'une fonction est l'adresse de sa première instruction. C'est donc un pointeur constant déréférencé, comme celui d'un tableau.
� Exemple 1
int plus(int x, int y) // Mini calculatrice (addition et soustraction) { return(x+y);}
int moins(int x, int y) { return (x-y);}
int main(void ) {int (*f[])(int, int) = {plus, moins};
/* f est un tableau de pointeurs initialisé à l'adresse des deux fonctions précédentes */ int m = 4, n = 5 ,i; for(i=0; i<2 ;i++) printf("m = %d n = %d res %d\n",m,n,(*f[i])(m,n)); return(1);
}
// Résultat m = 4 n = 5 res = 9 m = 4 n = 5 res = -1
110 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple 2
Voici un programme (certes un peu compliqué), de calcul de fonction de multiplication et de fonction puissance.
int plus (int x, int y) { return(x+y);}
int prod (int x, int y) { return(f(x,y,plus,0));}
int puis (int x, int y) { return(f(x,y,prod,1));}
int f(int x, int y, int (*g)(), int a) { return( (y==0) ?a : (*g)(f(x,y-1,g,a),x));}
int main(void ) { int i,j; scanf("%d %d",&i,&j); printf("%d * %d = %d\n %d ** %d = %d\n", i,j,prod(i,j), i,j , puis(i,j)); return(1); }
// Résultat 2 * 3 = 6 2 ** 3 = 8
14. FONCTION A NOMBRE VARIABLE D'ARGUMENTS
Le programmeur peut définir des fonctions avec un nombre variable d'arguments, chacun d'un type aléatoire. C'est par exemple le cas de la fonction printf.
14.1 Principe général Le principe est le suivant :
• la fonction est définie en indiquant l'argument d'appel à partir duquel le nombre des arguments suivants varie,
• le nombre d'arguments ainsi que leur type respectif est indiqué à chaque appel,
• chaque type d'argument est décrit préalablement par un entier, utilisé comme indicateur de son type.
� Exemple
void f_var_arg(int nb_arg,...)
14.2 Gestion de la pile des arguments La pile d'exécution contient les arguments d'appel. Elle est gérée automatiquement, pour des fonctions à nombre d'arguments fixe et dont le type est déterminé à la définition de la fonction. Quand le nombre d'arguments est variable et leur type aléatoire, le programmeur va devoir décrire cette gestion. Il dispose pour cela d'un pointeur et de primitives permettant de réaliser des opérations sur ce dernier.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
111
■ Pointeur de pile
Dans le corps de la fonction, une variable pointeur de pile, de type prédéfini va_list, est utilisée pour gérer la pile des différents arguments.
■ Opérations sur le pointeur de pile
Les opérations définies sur le pointeur de pile sont l'initialisation, la mise à jour selon le type de l'argument, la fermeture.
Les primitives associées sont respectivement va_start, va_arg, va_end.
■ Le type va_list
Le type prédéfini va_list est défini par une instruction typedef dans le fichier en-tête stdarg.h.
Ce type représente, selon les implémentations un pointeur ou un tableau de pointeurs sur des objets de type très divers. Il est utilisé pour les implémentations des fonctions printfou scanf.
■ La fonction va_start
Synopsis #include <stdarg.h> void va_start(va_list pointeur, paramètre_le_plus_à_droite);
Description La macro va_start pointe sur la base de la liste des arguments. Son utilisation est nécessaire pour initialiser le pointeur, de type va_list.
Le paramètre_le_plus_droite est le dernier paramètre avant l'unité syntaxique "..." . C'est en général un entier définissant le nombre de couples d'arguments variables de la fonction.
� Exemple
va_list p_liste; int nb_arg; va_start(p_liste, nb_arg);
■ La fonction va_arg
Synopsis #include <stdarg.h> nom_de_type * va_arg(va_list pointeur, nom_de_type);
Description La fonction va_arg met à jour le pointeur de la pile en fonction du type de l'argument courant. Elle retourne un pointeur sur l'argument suivant de la liste. Elle est toujours utilisé en conjonction avec les fonctions va_start et va_end.
va_arg(p_liste, int); ... va_arg(p_liste, char *);
112 CHAPITRE IV ───────────────────────────────────────────────────
■ La fonction va_end
Synopsis #include <stdarg.h> void va_end(va_list pointeur);
Description La fonction va_end, dont l'usage est seulement recommandé, met à jour le pointeur de pile en fin d'utilisation.
� Exemple
#include <stdarg.h> #define NMAX 10 // Nombre maximum d'arguments variables accepté void f_var_arg(int nb_arg,...) // Forme générale : f_var_arg(int nb_arg,int n1,arg1, int n2, arg2, int n3, arg3,...) // Le premier arguments d'appel indique le nombre d'arguments d'une liste // Constituée de couples d'arguments (indicateur_du_type, argument) // Le premier paramètre du couple est toujours de type int, descripteur du type de l'argument // suivant et ainsi de suite. Par exemple 0 : char *, 1 : int , 2 : double { va_list p_liste; // Initialisation du pointeur p_liste
int i; if(nb_arg>NMAX) nb_arg=NMAX; // Le nombre d'arguments est borné par NMAX va_start(p_liste, nb_arg); // Initialisation obligatoire du pointeur de pile p_liste
// Boucle sur le nombre d'arguments for(i=0; i < nb_arg; i++) { printf("paramètre %d --> ",i);
switch(va_arg(p_liste,int)) // Test sur le type de l'argument suivant et mise à jour de la pile { case 0 : // Type char *
// Mise à jour du pointeur p_liste printf("type : char * valeur : %s\n", va_arg(p_liste, char *)); break; case 1 : printf("type : int valeur : %d\n",va_arg(p_liste, int)); break; case 2 : printf("type : double valeur : %lf\n",va_arg(p_liste, double)); break; case 3 : printf("type : float valeur : %lf\n",va_arg(p_liste, double)); break; // L'instruction : printf("type : float valeur : %lf\n",va_arg(p_liste, float)); // ne fonctionne pas
} }
va_end(p_liste); // Recommandé pour une ultime mise à jour du pointeur de pile }
int main(void ) { printf("\t\nPremier appel\n"); // 3 arguments : char *, int, double
f_var_arg(3,0,"abcdefg",1,255,2,3.5657); printf("\n\tDeuxième appel\n"); // 4 arguments : entier, char *, int, double f_var_arg(4,1,-2345,0,"DFGHbhj",1,456,2,-67.67); printf("\n\tTroisième appel\n"); // 5 arguments : double, char *, int, double, char * f_var_arg(5,2,-23.45,0,"Bonjour",1,-56,3,-7.67,0,"Au revoir"); return(1);
}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
113
// Résultat Premier appel paramètre 0 --> type : char * valeur : abcdefg paramètre 1 --> type : int valeur : 255 paramètre 2 --> type : double valeur : 3.565700 Deuxième appel paramètre 0 --> type : int valeur : -2345 paramètre 1 --> type : char * valeur : DFGHbhj paramètre 2 --> type : int valeur : 456 paramètre 3 --> type : double valeur : -67.670000 Troisième appel paramètre 0 --> type : double valeur : -23.450000 paramètre 1 --> type : char * valeur : Bonjour paramètre 2 --> type : int valeur : -56 paramètre 3 --> type : float valeur : -7.670000 paramètre 4 --> type : char * valeur : Au revoir
15. STRUCTURES DE DONNEES ABSTAITES ET OBJETS STRUCTURES
Le langage C permet de définir des objets structurées conformément à une structure de donnée abstraite (variable structurée et union). Ainsi, un agenda est constitué du nom, du prénom, de l'adresse, du numéro de téléphone de différentes personnes.
Un objet abstrait est appelé enregistrement logique lors de requêtes de lecture ou d'écriture
En programmation objet, l'objet abstrait devient une classe dotée de données structurées (ses attributs) et des traitements associés appelés méthodes.
■ Définitions
La déclaration struct décrit une modèle de données abstraites constituée d'un agrégat d'objets typés, et définit des variables structurées (appelées structures) conformes aux attributs (champs, zones) de ce modèle, stockés séquentiellement.
La taille nécessaire au stockage d'une variable structurée est le total des tailles des différents attributs qui la constituent, complété si nécessaire de bits d'alignement.
En programmation objet, les attributs sont appelés données membres.
114 CHAPITRE IV ───────────────────────────────────────────────────
15.1 Déclaration et définition La déclaration struct décrit les attributs d'une variable structurée sans allocation mémoire. Elle devient une définition quand une variable structurée lui est associée.
struct { type déclarateur ; } ;
nom_structure identificateur
Diagramme d'utilisation du mot clé struct
■ Interprétation
struct mot clé nom_structure définition (optionnelle) du nom du type de la variable
structuré (tag) ce qui permet de créer un nouveau type de variable
{ ...} marque de début/fin de la description de la structure abstraite type type prédéfini ou non de l'attribut déclarateur identificateur de l'attribut identificateur définition optionnelle des identificateurs des variables
structurées.
� Exemples
struct date {int jour; int mois; int année;}; // Déclaration de la structure abstraite date
// Deux définitions similaires des variables structurées de type date aujourd_hui, demain struct date aujourd_hui,demain; struct date {int jour; int mois; int année;} aujourd_hui,demain;
// Définitions des variables structurées aujourd_hui, demain struct { int jour; int mois; int année;} aujourd_hui, demain;
Dans ce cas, le type de structure abstrait date n'a été déclaré.
15.2 Règles d'utilisations Une variable structurée peut être définie à partir d'une autre variable structurée. Ainsi, la déclaration
#define L 50 struct personne {char nom[L]; char prenom[L]; struct date date_mois;} individu;
est licite. C'est la définition d'une variable structurée personne et la déclaration d'un identificateur individu de type personne. Le symbole date_mois est de type prédéfini date.
■ Initialisation d'une variable structurée
Une variable structurée peut être initialisée de la façon suivante :
struct personne individu = {"Dupont", "Albert", 20, 3, 53};
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
115
■ Récursivité
Une définition récursive est autorisée.
� Exemple
Soit la variable structurée noeud :
struct noeud {int code; struct noeud *gauche; struct noeud *droite;};
Elle a la représentation suivante en mémoire :
entier pointeur_sur_une_structure_noeud pointeur_sur_une_structure_noeud
� Exemples
struct noeud arbre[10 ]; // Déclaration d'un tableau de 10 noeuds struct noeud *p; // p pointeur sur un noeud struct noeud * f (void); // f fonction sans argument retournant un pointeur vers un noeud
15.3 L'opérateur de sélection de membre L'opérateur de sélection de membre accède aux champs d'une variable structurée par une construction (arborescente) de la forme :
nom_variable.nom_du_champ[.nom_du_champ]
� Exemple
demain.jour = 20; individu.date_mois.jour = 20;
15.4 Pointeurs sur des variables structurées : l'op érateur -> La définition :
struct date *pt;
définit la variable pt de type pointeur sur la variable structurée date.
Pour accéder au champ mois, on écrit :
(*pt).mois = 11 ; (1)
Les parenthèses sont nécessaires compte tenu de la hiérarchie des opérateurs * et ().
L'opérateur binaire -> permet l'accès à un attribut de la variable structurée. Il est défini de la façon suivante :
L'instruction
p -> x;
est équivalente à l'instruction :
(*p).x;
Ainsi, l'instruction (1) s'écrit également :
pt ->mois = 11;
116 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple
#include <stdio.h> #define TAILLE 30 int main(void) {// Déclaration des variables structurées date et personne
struct date {int jour; int mois; int annee;}; struct personne {char nom[TAILLE]; char prenom[TAILLE]; struct date date_mois;}; struct date demain; // Définition des variables structurées demain et individu
// Initialisation de la variable individu de type personne struct personne individu = {"Dupond","Jean - Albert",20,3,1953 }; printf("Caractéristiques initiales de l'individu :\n"); printf("Nom : %s\t Prénom : %s \n",individu.nom, individu.prenom); printf("Date de naissance : %d - %d - %d \n", individu.date_mois.jour, individu.date_mois.mois, individu.date_mois.annee); printf("fin du premier acte\n ");
// Autres formes d'initialisation demain.jour = 20; printf("La date de demain est : %d\n ",demain.jour); individu.nom = "Dupont"; individu.prenom = "Albert "; individu.date_mois.jour = 20; individu.date_mois.mois = 03; individu.date_mois.annee = 1920;
// Impression des résultats printf("Caractéristique de individu :\n"); printf("Nom : %s\t Prénom : %s \n",individu.nom, individu.prenom); printf("Date de naissance : %d - %d - %d \n", individu.date_mois.jour, individu.date_mois.mois, individu.date_mois.annee);
}
// Résultat Caractéristiques initiales de l'individu : Nom : Dupond Prénom : Jean - Albert Date de naissance : 20 - 3 - 1953 fin du premier acte La date de demain est : 20 Caractéristique de individu : Nom : Dupont Prénom : Albert Date de naissance : 20 - 3 - 1920
15.5 Associativité des opérateurs * ->++ -- () [ ] Nous avons vu précédemment que la hiérarchie des opérateurs de manipulation des pointeurs est la suivante :
Opérateurs Ordre d'évaluation () [ ] ->. gauche à droite ! ~ ++ -- - * & sizeof droite à gauche
La bonne utilisation de ces règles nécessite certaines précautions.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
117
� Exemple
#include <stdio.h> int main(void) { int i = 16, j = 32;
int x = 0; int *pt =&i; printf(" valeur d'initialisation\n"); printf(" i = %d j = %d",i,j); printf(" &i = %x &j = %x\n", &i,&j); printf(" \n\névaluation de pt\n"); printf(" i = %d *pt = %d pt = %x\n", i, *pt, pt); *pt++; // Cette expression équivalente à *(pt++) doit être une Lvalue. printf("\n\n évaluation de *pt++\n"); printf(" *pt = %d pt = %x\n", *pt, pt); (*pt)++; // pt est inchangé car cette expression n'a de sens que pour une Lvalue printf("\n\n évaluation de (*pt)++\n"); printf(" *pt = %d pt = %x\n", *pt, pt); x=*(pt++); /* x = *pt; pt++ ; */ printf("\n\n évaluation de x = *(pt++)\n"); printf(" x =%d *pt = %d pt = %x\n", x, *pt, pt); x=*pt++; /* x = *pt; pt++ */ printf("\n\n évaluation de x = *pt++\n"); printf(" x =%d *pt = %d pt = %x\n", x, *pt, pt); return(1);
}
■ Applications
1°) Vu les règles d'associativité, l'instruction :
++p -> x est équivalente à l'instruction ++ (*p).x
D'où incrémentation du champ x, pas du pointeur p.
Démonstration : ++p ->x = ++(p->x) = ++((*p).x)) = ++(*p).x
La première égalité résulte de la hiérarchie des opérateurs -> et ++.
La deuxième est une conséquence de la définition de l'opérateur ->.
La troisième résulte de la hiérarchie des opérateurs (),.,++.
2°) L'instruction
++p -> x;
n'est pas équivalente à l'instruction (L-valeur ou R-valeur)
(++p ) -> x;
En effet, la deuxième instruction s'écrit :
(++p ) -> x = (* (++p )).x
118 CHAPITRE IV ───────────────────────────────────────────────────
L'instruction exécute la post-incrémentation du pointeur après un accès au champ x, l'expression entre parenthèses étant évaluée en premier.
3°) Soit l'instruction :
(p++ ) ->x =...;
Compte tenu des règles de priorité, on a :
(p++ ) ->x =...⇔ (* (p++ )).x =... ;
L'opérateur de post-incrémentation ++ étant utilisé, l'instruction précédente s'écrit :
((*p)).x =...; p++ ;
4°) L'instruction (*p)-> y accède à l'objet pointé par y puisque :
(*p)-> y = (* (*p)).y = (** p).y
5°) L'instruction (*p)->y++ est une Rvaleur ou une Lvaleur. Son effet est de post-incrémenter le champ y, puisque :
(*p)-> y++ = ((**p).y)++
6°) Compte tenu des hiérarchies respectives des opérateurs ++ et ->, la post-incrémentation du pointeur pt s'écrit pt++->y .
7°) La post-incrémentation du pointeur *p s'écrit (*p)++ -> y . Il suffit de poser q = *p dans l'égalité précédente.
8°) L'instruction (*p++ )->y post-incrémente le pointeur p puisque :
(*p++ )->y = (* (p++ ))->y
15.6 Pointeurs, variables structurées et fonctions Les opérations sur les variables structurées sont les suivantes :
• accès à son adresse avec l'opérateur & ,
• accès, puis modification d'un champ avec l'opérateur ->,
• transmission d'une structure comme argument d'une fonction (valeur ou adresse),
• retour d'une variable structurée par une fonction.
� Exemple
• Ce programme définit les types structurés date et personne.
• La fonction lit effectue la saisie des champs d'un individu de type personne.
• Les arguments sont transmis par adresse.
• Les résultats sont écrits de deux façons différentes (fonctions ecrit et ecrit2).
La première transmet la structure par valeur, ce qui est naturel puisque aucune modification ne doit être faite à l'écriture, la seconde par adresse.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
119
#include <stdio.h> #define TAILLE 30 struct date {int jour; int mois; int annee;}; // Déclaration des variables globales struct personne { char nom[TAILLE]; char prenom[TAILLE]; struct date date_mois;}; void main(void) { struct date demain; // Définitions
struct personne individu; // Prototypes void ecrit(struct personne); // Transmission par valeur (la meilleure) void ecrit2(struct personne *); // Transmission par adresse void lit (struct personne *); // Transmission par adresse // Initialisation demain.jour = 20; printf(" La date de demain est : %d\n ", demain.jour); // Lecture d'une structure lit(&individu); // Transmission par adresse // écriture d'une structure ecrit(individu); // Transmission par valeur ecrit2(&individu); // Transmission par adresse
}
void lit (struct personne * pindividu) {(*pindividu).nom = "Dupont"; // Remplissage du champ nom
pindividu->prenom = "Albert "; // Remplissage du champ prénom // Remplissage du champ date pindividu->date_mois.jour = 20; (*pindividu).date_mois.mois = 03; pindividu->date_mois.annee = 1920;
}
void ecrit( struct personne individu) // Transmission par valeur { printf(" Caractéristique de individu :\n");
printf(" Nom : %s \t Prénom : %s \n", individu.nom, individu.prenom); printf("Date de naissance : %d - %d - %d \n", individu.date_mois.jour, individu.date_mois.mois, individu.date_mois.annee);
}
void ecrit2( struct personne *individu) // Transmission par adresse { printf(" Caractéristique de individu :\n"); printf(" Nom : %s \t Prénom : %s \n", individu->nom, individu->prenom); printf(" Date de naissance : %d - %d - %d \n", individu->date_mois.jour, individu->date_mois.mois, individu->date_mois.annee); }
// Résultat La date de demain est : 20 Caractéristique de individu : Nom : Dupont Prénom : Albert Date de naissance : 20 - 3 - 1920 Caractéristique de individu : Nom : Dupont Prénom : Albert Date de naissance : 20 - 3 - 1920
120 CHAPITRE IV ───────────────────────────────────────────────────
15.7 Variables structurées et tableau La définition de tableaux de variables structurées ou de variables structurées contenant un tableau est autorisée. Ainsi, l'instruction
struct t tabs [8 ] [3 ];
définit huit tableaux de trois variables structurées de type t.
� Exemple
// Structure constituée d'un tableau #include <stdio.h> #define NLIGNE 2 #define NCOL 3 #define NTAB 4 struct Super_tableau {int tab[NLIGNE][NCOL][NTAB]; }; int main (void) { struct Super_tableau super_tableau, *pt = &super_tableau, super_tableau2;
void remplissage(struct Super_tableau *); void affichage(struct Super_tableau); remplissage(pt); printf("\nAffichage de super_tableau\n"); affichage(super_tableau); super_tableau2 = super_tableau; printf("\nAffichage de super_tableau2\n"); affichage(super_tableau2); return(0);
}
void remplissage(struct Super_tableau * tableau) { int i , j, k ;
for (i = 0 ; i < NLIGNE ; i++) for (j = 0 ; j < NCOL; j++) for (k =0 ; k < NTAB; k++) tableau->tab[i][j][k] = i+j+k; }
void affichage (struct Super_tableau tableau) { int i , j, k , compteur = 0;
for (i = 0 ; i < NLIGNE ; i++) for (j = 0 ; j < NCOL; j++) for (k =0 ; k < NTAB; k++) { if(compteur %4 !=0)
printf(" tab[%d][%d][%d] = %d ",i,j,k,tableau.tab[i][j][k]); else printf("\n tab[%d][%d][%d] = %d ", i, j, k, tableau.tab[i][j][k]); compteur++;
} printf("\n");
}
// Résultat Affichage de super_tableau tab[0][0][0] = 0 tab[0][0][1] = 1 tab[0][0][2] = 2 tab[0][0][3] = 3 tab[0][1][0] = 1 tab[0][1][1] = 2 tab[0][1][2] = 3 tab[0][1][3] = 4 tab[0][2][0] = 2 tab[0][2][1] = 3 tab[0][2][2] = 4 tab[0][2][3] = 5 tab[1][0][0] = 1 tab[1][0][1] = 2 tab[1][0][2] = 3 tab[1][0][3] = 4 tab[1][1][0] = 2 tab[1][1][1] = 3 tab[1][1][2] = 4 tab[1][1][3] = 5 tab[1][2][0] = 3 tab[1][2][1] = 4 tab[1][2][2] = 5 tab[1][2][3] = 6 Affichage de super_tableau2 tab[0][0][0] = 0 tab[0][0][1] = 1 tab[0][0][2] = 2 tab[0][0][3] = 3 …
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
121
15.8 Affectation de variable structurée Une variable structurée peut être affectée à une autre variable structurée à condition d'être constituée des mêmes champs. C'est une première forme de surdéfinition de l'opérateur d'affectation, généralisée en C++.
� Exemple
#include <stdio.h> void main(void) { typedef struct{ int jour; int mois; int annee;} date;
date jour1 = {20,12,1988}, jour2 = jour1; printf("jour = %d mois = %d annee = %d\n",jour1.jour, jour1.mois, jour1.annee); printf("jour = %d mois = %d annee = %d\n", jour2.jour, jour2.mois, jour2.annee);
}
// Résultat jour = 20 mois = 12 annee = 1988 jour = 20 mois = 12 annee = 1988
■ Application : assignation globale d'un tableau
Un tableau d'une variable structurée peut être assigné globalement. C'est d'ailleurs le seul moyen en C.
int main(void) { struct GrossTab {int tableau[5];} tab1, tab2 = {1, 2, 3, 4, 5}; int i; printf("sizeof (tab1) = %d",sizeof(tab1)); for (i=0; i <5; i++) tab1.tableau[i]=i; tab2= tab1; return 1; }
15.9 Champs de bits Des variables structurées dont les champs sont de type int, ou unsigned int constituent des champs de bits.
Synopsis struct { unsigned int champ_optionnel: nombre_de_bits_du_champ; ...}
Il peut être nécessaire d'intercaler des bits de remplissage.
� Exemple 1
struct { unsigned int sexe : 1 ; // 2 états unsigned int mois : 4 ; // 12 états utiles sur 16 possibles unsigned int : 3 // Bits de remplissage (padding)
}
122 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple 2
Programme de création, de saisie, d'écriture d'un numéro de sécurité sociale.
#include <stdio.h> struct SecSoc { unsigned int sexe : 1 ; // 2 états
unsigned int annee : 7 ; // 100 états unsigned int :4; // 4 bits de remplissage unsigned int mois : 4 ; // 12 états unsigned int :1; // 1 bit de remplissage unsigned int departement : 7; // 98 états unsigned int :8; // 8 bit de remplissage unsigned int :6; // 6 bit de remplissage unsigned int canton : 10 ; // 1000 états unsigned int :6; // 6 bit de remplissage unsigned int quantieme : 10 ; // 1000 états
};
int saisie(struct SecSoc *secu) { unsigned int sexe=0, annee =0, mois=0, departement=0, canton=0, quantieme =0;
printf("\n sexe : 1 (garçon) ou 2 (fille) "); scanf("%u", &sexe); switch(sexe)
{ case 1 : secu->sexe = 0; break; case 2 : secu->sexe = 1; break; default : printf("erreur sexe\n") ; return(0);
} printf("année de naissance (2 chiffres) "); scanf("%u",&annee); if(annee > 99) { printf("erreur année\n") ; return(1);} secu->annee=annee;
printf("\nmois (1-12) "); scanf("%u", &mois); if(mois > 12) { printf("erreur mois \n"); return(2);} secu->mois=mois;
printf(" \nnuméro de département (1-98) "); scanf("%u",&departement); if(departement > 98) {printf("erreur departement\n"); return(3);} secu->departement=departement;
printf(" \nnuméro de canton (0-999) "); scanf("%u",&canton); if(canton > 999) {printf("erreur canton\n"); return(4);} secu->canton=canton;
printf(" \nnuméro de quantième (0-999) "); scanf("%u",&quantieme); if(quantieme > 999) {printf("erreur quantième\n"); return(5);} secu->quantieme=quantieme;
}
void ecriture(struct SecSoc secu) { char * Mois[12] = {"Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet",
"Août" ,"Septembre", "Octobre", "Novembre" , "Décembre" } ; printf("\nsexe (0-masculin, 1-féminin) : %u\n",secu.sexe); printf("année de naissance 19%u\n", secu.annee); printf("mois de naissance : %s \n",Mois[secu.mois-1] ); printf("département de naissance : %u \n canton : %u \n ",secu.departement, secu.canton); printf("quantième : %u \n",secu.quantieme);
}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
123
int main(void) // Gestion des champs de bits { struct SecSoc num_sec, *pt = & num_sec;
unsigned int *pt2 = (unsigned int *) pt ; int i ; // Prototypes int saisie(struct SecSoc *); void ecriture (struct SecSoc);
// Appels saisie(pt); ecriture(num_sec); // Impression en hexadécimal for (i = 0; i <4; i++) {printf("%x ", *pt2); pt2++;} printf("\n%x\n",*pt); printf("sizeof(unsigned int) = %d\n",sizeof(unsigned int)); printf("sizeof(struct (SecSoc)) = %d\n",sizeof(struct SecSoc)); return(1);
}
// Résultat sexe : 1 (garçon) ou 2 (fille) 1 année de naissance (2 chiffres) 53 mois (1-12) 03 numéro de département (1-98) 75 numéro de canton (0-999) 012 numéro de quantième (0-999) 130 sexe (0-masculin, 1-féminin) : 0 année de naissance 1953 mois de naissance : Mars département de naissance : 75 canton : 12 quantième : 130 306a e97 316 2090 306a sizeof(unsigned int) = 2 sizeof(struct (SecSoc)) = 8
16. UNIONS
Les variables structurées et les unions ont une syntaxe d'utilisation très voisine. Par contre, la sémantique est différente : les variables structurées sont constitués de champs consécutifs, les unions sont constitués de champs qui se recouvrent.
■ Déclaration et définition
La déclaration union permet de définir des structures de données permettant de manipuler alternativement des objets de différents types localisés à une même adresse dans la mémoire. Une union est une variable structurée dont tous les attributs sont superposés sur la même adresse ce qui permet de l'interpréter de plusieurs façons différentes.
La syntaxe du mot clé union est similaire à celui du mot clé struct.
124 CHAPITRE IV ───────────────────────────────────────────────────
� Exemple 1
union typunion {char car;int entier;long longueur;double x;}var;
Le type courant de var est défini par la variable typunion. Ainsi peut-on écrire :
int i; double y; typeunion var1, var2; var1.entier = i; var2.x = y;
� Exemple 2
int main(void) { union bizarre{ char car; int entier; long longueur; double x; } var1, var2;
int i = 5; double y = 38.6785; var1.entier = i; var2.x = y; var1.car = 'c'; printf(" var1.car = %c var1.entier = %d\n var1.longueur = %d var1.x = %e\n",
var1.car, var1.entier, var1.longueur,var1.x); printf(" var2.car = %c var2.entier = %d\n var2.longueur = %d var2.x = %e\n",
var2.car, var2.entier,var2.longueur, var2.x); }
// Résultats var1.car = c var1.entier = 99 var1.longueur = 99 var1.x = 2.199951e-034 var2.car = _ var2.entier = 11011 var2.longueur = 11011 var2.x = 2.356809e+110
Le seul résultat exact est var1.car les variables imprimées n'ayant pas été initialisées.
� Exemple 3
void main(void) { union bizarre { char car; int entier; long longueur; double x; } var1, var2;
int i = 5; double y = 38.6785; printf("y = %e\n",y); var1.entier = i; printf("var1.entier = %d\n",var1.entier); var2.x = y; printf("var2.x = %e\n",var2.x); var1.car = 'c'; /* Modifie l'interprétation de var1.entier */ printf(" var1.car = %c var1.entier = %d var1.longueur = %d var1.x = %e \n",var1.car,
var1.entier,var1.longueur,var1.x); printf(" var2.car = %c var2.entier = %dvar2.longueur = %d var2.x = %e\n",
var2.car, var2.entier,var2.longueur,var2.x); }
// Résultats y = 3.867850e+001 var1.entier = 5 var2.x = 3.867850e+001 var1.car = c var1.entier = 99 var1.longueur = 99 var1.x = 1.390369e+093 var2.car = _ var2.entier = 11011 var2.longueur = 11011 var2.x = 2.356809e+110
■ Variable structurée et union
Une union peut être imbriquée dans une variable structurée.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
125
17. TYPE ENUMERE
Le mot clé enum définit un ensemble ordonné fini de constantes symboliques de type entier.
Forme enum nom_de_type {liste de constantes symboliques} [liste_de_variables] ;
� Exemple
enum jours { lundi, mardi, mercredi, jeudi, vendredi, samedi, dimanche}; jours jour; // La variable jour est de type jours
Les instances de la liste d'énumération sont considérées comme des constantes entières dont les valeurs sont numérotées par défaut à partir de zéro, par pas de 1. Ainsi, lundi = 0; mardi = 1;...
L'instruction
jour = mardi;
est équivalente à l'instruction
jour = 1;
L'initialisation explicite du premier élément de la liste avec une valeur entière non nulle est autorisée.
enum mois { Janvier = 1, Février, Mars, ..., Décembre };
� Exemple
int main(void) { enum jours {lundi, mardi, mercredi, jeudi,vendredi, samedi, dimanche};
enum mois {Janvier = 1, Février, Mars, Avril, Mai, Juin, Juillet, Août, Septembre, Octobre, Novembre, Décembre} month; enum jours jour; jour = lundi; printf ("jour = %d\n",jour); jour = dimanche; printf ("jour = %d\n",jour); month = Décembre; printf("month = %d\n",month);
}
// Résultats jour = 0 jour = 6 month = 12
126 CHAPITRE IV ───────────────────────────────────────────────────
18. L'INSTRUCTION TYPEDEF
■ Type synonyme et interface objet
L'instruction typedef permet de définir un nouveau nom de type ou type synonyme à partir d'un type existant.
Ce nouvel identificateur de type, défini sans création d'un type nouveau, est utilisé pour accéder simplement à tous types d'objets tels les tableaux, les variables structurées, les unions et les énumérations ce qui améliore la lisibilité des interfaces d'accès. C'est un concept de programmation orienté objet.
Syntaxe : typedef <type(s)_existant(s)> type_synonyme;
� Exemple
#include <stdio.h> int main(void) { typedef int entier; // entier synonyme de int
typedef char octet; // octet synonyme de char typedef char *texte []; typedef struct { float reel; float imaginaire;} complexe; entier i = 1; texte chaine = "bonjour"; octet byte = '\063'; complexe z; ...
}
L'instruction typedef permet de s’affranchir des problèmes de portabilité la taille des objets de base (int, short, long...) pouvant différer d'un système à un autre.
Il est aussi possible de définir un synonyme de différents types.
� Exemple
typedef int short char long geant;
Le type geant est synonyme de tous les types précédents.
■ Définition d'un nom de type
La grammaire du langage C impose, à l'utilisation d'un type structuré, la répétition du mot clé struct contrairement aux types prédéfinis. L'instruction typedef permet de l'éviter de la façon suivante :
typedef struct [nom_ structure] {type1 champ1;..., typep champ; } nom_structure;
� Exemple
#define TAILLE 5 int main(void) { typedef struct { char nom[TAILLE]; char prenom[TAILLE];} chaine;
chaine personne; …
}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
127
19. EXERCICES
� Exercice 1
Programmer la multiplication égyptienne en utilisant exclusivement des opérateurs binaires.
� Exercice 2
Calculer le nombre de bits à 1 de la représentation binaire d'un nombre entier non signé. Pour tester la valeur du dernier bit du nombre n, il suffit de tester le résultat de l'opération logique n et 1 puis de faire un décalage à droite de 1 bit et d'itérer tant que n est différent de 0.
� Exercice 3
Ecrire un programme d'extraction à droite de n bits à partir du bit p du nombre entier x, le bit d'indice 0 est celui qui est plus à droite.
� Exercice 4
L'objectif est de calculer le volume d'une sphère par utilisation d'appels successifs de fonctions. Ecrire une fonction vol(r) qui appelle la fonction surf(r) qui appelle la fonction circonf(r). La fonction vol est appelée par la fonction main où le rayon est défini.
� Exercice 5
La fonction 91 est définie de la façon suivante :
f(n) = n - 10 si n > 100 f(n) = f(f(n+11)), 0 < n ≤100
Quand n est supérieur à 100, il n'y a pas de problème pour calculer f.
Dans le cas où n est inférieur ou égal à 100, l'appel de fonction est doublement récursif.
1°) Programmer cette fonction. Conclusion ?
2°) Démontrer mathématiquement le résultat obtenu.
� Exercice 6
Ecrire un programme de suppression d'une occurrence c dans une chaîne de caractère s. La fonction de suppression est la fonction squezze.
� Exercice 7
Ecrire un programme d'écriture des caractères d'une chaîne dans l'ordre inverse.
� Exercice 8
Ecrire une fonction atoi qui assure la conversion d'une chaîne numérique en un nombre entier. Cette fonction de la bibliothèque C retourne zéro si la chaîne est non numérique.
128 CHAPITRE IV ───────────────────────────────────────────────────
� Exercice 9
Rechercher la ligne la plus longue d'un fichier à partir des fonctions getline (saisie d'une ligne de texte), copy (recopie une chaîne de caractères dans une autre).
� Exercice 10
Interpréter le programme suivant :
int main(void) { void saisie(float *);
float r; saisie(&r);
}
void saisie(float * rayon) { printf("indiquer le rayon : "); scanf("%f",rayon); printf(" r = %f \n",*rayon); printf("indiquer le rayon : "); scanf("%f",&(*rayon)); printf(" r = %f \n",*&(*rayon)); printf("indiquer le rayon : "); scanf("%f",&*rayon); printf(" r = %f \n", *&*rayon); }
#include <stdio.h> int main(void) { void saisie(float *); /* Prototypes */
void saisie2(float *); void saisie3(float *); void saisie4(float *); void affiche (char *, float); float r; /* Appel de saisie */
saisie(&r); affiche("main",r); saisie2(&r); affiche("main",r); saisie3(&r); affiche("main",r); saisie4(&r); affiche("main",r); }
void affiche(char *chaine, float valeur) { printf("Depuis la fonction %s, la variable saisie est : %f\n\n",chaine, valeur);}
void saisie(float * rayon) { printf("saisie\nindiquer le rayon : ");
scanf("%f",rayon); printf("rayon = %f\n",rayon);
}
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
129
void saisie2(float * rayon) { printf("saisie2\nindiquer le rayon : ");
scanf("%f",&(*rayon)); printf("&(*rayon) = %f\n",rayon);
}
void saisie3(float * rayon) { printf("saisie3\nindiquer le rayon : ");
scanf("%f",&*rayon); printf("&*rayon = %f\n",rayon);
}
void saisie4(float * rayon) { printf("saisie4\nindiquer le rayon : ");
scanf("%f",&rayon); printf("rayon = %f\n",rayon);
}
// Résultats saisie indiquer le rayon : 4.568 rayon = 0.000000 Depuis la fonction main, la variable saisie est : 4.568000 saisie2 indiquer le rayon : 89.56 &(*rayon) = 0.000000 Depuis la fonction main, la variable saisie est : 89.559998 saisie3 indiquer le rayon : -86.93 &*rayon = 0.000000 Depuis la fonction main, la variable saisie est : -86.930000 saisie4 indiquer le rayon : 965.325 rayon = 3.172861892505597550000000000000000000000e+100 Depuis la fonction main, la variable saisie est : -86.633675
� Exercice 11
Transmettre par valeur un tableau de variables entières à une fonction et modifier ce tableau dans le corps de la fonction.
� Exercice 12
Soit un pointeur p sur un objet d'un type donné. Evaluer, quand c'est possible, les expressions *p++-- , * (p++ )--, (* )(p++ )--, (*p++ )—
� Exercice 13
Ecrire une fonction qui permette d'additionner p composantes d'un tableau de type entier ou réel, à partir d'un indice quelconque. On utilisera successivement trois méthodes :
• la fonction somme transmet directement le nom du tableau,
• la fonction sommeb transmet un pointeur sur une variable du type du tableau.
• la fonction sommec transmet un pointeur sur un tableau.
130 CHAPITRE IV ───────────────────────────────────────────────────
� Exercice 14
Ecrire trois versions différentes d'un programme de recopie d'une chaîne de caractères. Le premier programme utilise des tableaux. Les deux autres utilisent des pointeurs.
� Exercice 15
Ecrire deux programmes de concaténation de chaînes de caractères : une version tableau et une version pointeur.
� Exercice 16
Ecrire un programme de permutation des composantes d'un tableau d'entier à deux indices en utilisant un tableau de pointeurs.
� Exercice 17
Créer un tableau à trois indices et imprimer, à l'aide de pointeurs ou de tableaux de pointeurs toutes ses composantes.
� Exercice 18
Rechercher une chaîne de caractères donnée dans un fichier et imprimer de la ligne correspondante.
� Exercice 19
Ce programme est une calculatrice. L'utilisateur saisit dans l'ordre un nombre, un des opérateurs + ,-,*,/,%, un nombre. Le résultat de l'opération est affiché. L'algorithme est le suivant :
• saisie des données,
• comparaison de l'opérateur saisi avec la liste des opérateurs mathématiques autorisés stockés dans un chaîne de caractères (fonction ind),
• branchement vers la fonction correspondante de calcul,
• calcul du résultat.
� Exercice 20
Ecrire un programme qui teste la hiérarchie et l'associativité des opérateurs (),*,->,. utilisés avec l'opérateur d'affectation, sur des expressions situées à gauche de cet opérateur (L-valeur) ou à droite (R-valeur).
� Exercice 21
Ecrire un programme de :
• saisie d'un nombre complexe,
• calcul de la somme et du produit de nombres complexes,
• affichage des résultats.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
131
� Exercice 22
Ecrire un module de gestion d'une liste chaînée de tableaux de flottants.
#define SIZE 10 #define N 5 #include <stdio.h>
int main(void) { int i, nombre;
typedef struct chaine {float tab[SIZE]; struct chaine *suivant;} chaine; chaine * debut, *next, *pred; /* Initialisation du premier élément de la liste chainée */ debut = (chaine * ) calloc(1,sizeof(chaine)); printf("adresse initiale de la chaîne: %x\n",debut); printf("valeur initiale de la chaîne:\n"); for(i=0;i<SIZE;i++ ) printf("%6.2f ",debut->tab[i]); printf("\n"); printf("debut->suivant = %x\n",debut->suivant);
/* Remplissage du premier élément de la liste chaînée */ for(i=0;i<SIZE;i++ ) debut-> tab[i]= (float)i;
/* Allocation et initialisation de la mémoire pour la nouvelle structure */ next = (chaine * ) calloc(1, sizeof(chaine)); /* Initialisation de la structure */ for(i=0; i< SIZE; i++ ) next->tab[i] = (float) 2* i; next->suivant=(chaine *) NULL; debut->suivant = (chaine * ) next; /* Chaînage des deux structures */ /* Création et remplissage de plusieurs listes chaînées */ for(nombre=3;nombre<N+3;nombre++ ) {pred =next;
/* Création du nouvel élément de la liste */ next = (chaine * ) calloc(1,sizeof(chaine));
/* Remplissage de la nouvelle structure crée */ for(i=0; i< SIZE; i++ ) next->tab[i] = (float) nombre * i; next->suivant=(chaine *) NULL; pred->suivant=next; /* Chaînage entre le prédécesseur et le successeur */
}
/* Affichage des résultats */ printf("affichage des résultats\n"); next=debut; while (next->suivant !=(chaine *) NULL) { for(i=0; i< SIZE; i++ ) printf("%6.2f ",next->tab[i]);
printf("\n next->suivant = %x\n", next->suivant); next = (chaine * ) next->suivant;
}
for(i=0; i< SIZE; i++ ) printf("%6.2f ",next->tab[i]); printf("\n next->suivant = %x", next->suivant); exit(0); }
132 CHAPITRE IV ───────────────────────────────────────────────────
// Résultat adresse initiale de la chaîne: a7c valeur initiale de la chaîne: 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 debut->suivant = 0 affichage des résultats 0.00 1.00 2.00 3.00 4.00 5.00 6.00 7.00 8.00 9.00 next->suivant = aaa 0.00 2.00 4.00 6.00 8.00 10.00 12.00 14.00 16.00 18.00 next->suivant = ad8 0.00 3.00 6.00 9.00 12.00 15.00 18.00 21.00 24.00 27.00 next->suivant = b06 0.00 4.00 8.00 12.00 16.00 20.00 24.00 28.00 32.00 36.00 next->suivant = b34 0.00 5.00 10.00 15.00 20.00 25.00 30.00 35.00 40.00 45.00 next->suivant = b62 0.00 6.00 12.00 18.00 24.00 30.00 36.00 42.00 48.00 54.00 next->suivant = b90 0.00 7.00 14.00 21.00 28.00 35.00 42.00 49.00 56.00 63.00 next->suivant = 0
� Exercice 23 : les listes
Un tri par chaînage constitue, à partir d'une liste de noms saisis dans un ordre quelconque, une liste chaînée d'indices permettant son parcourt par ordre alphabétique.
Rappelons qu'une liste chaînée a la structure suivante :
info (i-1) pointeur(i) info(i) pointeur(i+1) info(i+1) pointeur(i+2)
Soit le tableau liste, de N structures, chacune composée de deux champs :
• la chaîne de caractères nom contiendra le nom saisi,
• l'entier tsuc, contiendra l'indice, dans le tableau liste, du successeur (dans l'ordre alphabétique) du nom précédemment saisi, avec les conventions suivantes :
liste [0 ].tsuc pointe sur le premier nom, dans l'ordre alphabétique, de la liste, liste [i ].tsuc=k, indice du successeur de liste [i].nom, dans l'ordre alphabétique, si
k≠0, liste [i ].tsuc=0, si le i-ème nom saisi n'a pas de successeur dans l'ordre
alphabétique.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
133
Toute nouvelle saisie va modifier deux enregistrements du tableau liste. Soit les tableaux :
i-ème nom saisi liste [i ].nom liste [i].tsuc
3
2
1
0
MARTIN
ALBERT
DUPOND
4
successeur de pas an' 0,
Albertd' successeur 1,
DUPOND de successeur 3,
liste la de nompremier
L'introduction du nom ALAIN va modifier le tableau liste[].tsuc selon le chaînage suivant :
i-ème nom saisi liste [i ].nom liste [i ].tsuc
4
3
2
1
0
ALAIN
MARTIN
ALBERT
DUPOND
4
2
0
1
3
L'algorithme de création de la liste chaînée, pour déterminer la valeur du prédécesseur et du successeur du nom saisi, est le suivant.
■ Notations
Soient j et pred les indices respectifs du candidat successeur et du candidat prédécesseur du ième nom saisi.
Au départ :
pred = 0; j= liste [0 ].tsuc
Le premier candidat prédécesseur est initialisé à 0. Il est comparé au prédécesseur du premier nom classé dans l'ordre alphabétique. Il y a deux possibilités :
liste [i ].nom s'intercale dans la liste
Dans ce cas :
∃ j0 tel que liste [pred ].nom < liste [i ].nom ≤ liste [j0 ].nom
et
liste [pred ].tsuc = i; liste [i ].tsuc = j0;
liste [i ].nom ne s'intercale pas dans la liste.
C'est donc le (nouveau) dernier élément de la liste, dans l'ordre alphabétique. Il n'aura pas de successeur. Soit j0 l'indice du dernier nom par ordre alphabétique de la liste avant l'insertion du nouveau nom dans la liste.
134 CHAPITRE IV ───────────────────────────────────────────────────
■ Représentation graphique
Soit Li la liste de noms à l'étape i, alors Li = Li-1 ∪ liste [i ].nom
L i-1 liste [i ].tsuc Li
1-i
1
n
...
...
...
n
∪
1-i
1
p
...
...
...
p
=
i
1-i
1
n
n
...
...
...
n
■ Evolution du tableau liste.tsuc
Voyons l'évolution du tableau liste [].tsuc dans les deux cas.
Premier cas : le nom s'intercale dans la liste. En utilisant les relations ci-dessus, on obtient :
L i-1 liste [i ].tsuc Li liste[].tsuc
1-i
k
1
n
...
n
...
n
∪
1-i
0
1
p
...
j
...
p
0
=
i
1-i
k
1
n
n
...
n
...
n
0
1-i
1
j
p
...
j
...
p
0
Avant Après
Deuxième cas : le nom ne s'intercale pas dans la liste : On a la représentation suivante :
L i-1 liste [i ].tsuc Li liste[].tsuc
1-i
j0
1
n
...
n
...
n
∪
1-i
1
p
...
0
...
p
0
=
i
1-i
j0
1
n
n
...
n
...
n
0
p
...
i
...
p
0
1-i
1
Avant Après
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
135
■ Analyse
Avant l'insertion, on a les relations :
liste [j0 ].tsuc = 0
liste [j ].nom < liste [j0 ].nom pour tout j = 1,... , i-1
Après l'insertion, on a les relations :
liste [i ].tsuc = 0 = liste [j0 ].tsuc liste [j0 ].tsuc = i
■ Arrêt du calcul
Deux possibilités :
∃ j0 tel que liste [i ].nom > liste [j ].nom
j0= 0
■ Algorithme
L'algorithme se décompose en deux phases :
Phase 1 Recherche de la position
• Initialisation de la recherche : pred = 0; j = liste[0].tsuc
• Recherche effective
• Arrêt de la recherche
◊ dès qu'est déterminé j0 tel que liste [j0 ].nom ≥ liste [j ].nom > liste [pred ].nom
◊ ou dès que j = 0 ce qui implique que ni n'a pas de successeur.
La négation de ces deux conditions s'écrit :
j ≠ 0 ou liste[j].nom < liste[i].nom
Phase 2 Modification des deux champs à modifier de liste[].tsuc par les formules : liste[i].tsuc = j liste[pred].tsuc = i
Ces formules valables dans les deux cas simplifient l'écriture de l'algorithme.
136 CHAPITRE IV ───────────────────────────────────────────────────
■ Programme
Initialisation lire N, dimension du tableau liste liste[i].tsuc = 0, pour tout i= 0,N liste[i].NOM = "" pour tout i = 1,N i = 1 Tant que(i < N et liste [i].nom ≠"") faire saisir liste [i ].nom; pred = 0; j = liste [0 ].tsuc; // Initialisation Recherche Tant que (j ≠ 0 et (liste [j ].nom < liste [i ].nom)) faire pred = j; // j : nouveau candidat prédécesseur j = liste [j ].tsuc; // Nouveau candidat successeur fin_faire liste [i ].tsuc = j; liste [pred ].tsuc = i; & modification de liste [ ].tsuc fin_faire
Le tri alphabétique est trivial.
j = liste [0 ].tsuc; Tant que (j ≠ 0) faire imprimer liste [j ].nom; j = liste [j ].tsuc; fin_faire
■ Programmation
La saisie au clavier de la liste des individus et son stockage dans un fichier est réalisée à partir d la fonction saisie.
La fonction lire permet, à partir du fichier des individus, de créer le tableau des successeurs. La comparaison de deux chaînes de caractères est réalisée par la fonction st qui retourne, à partir de la fonction de la bibliothèque C strcmp (prototype dans string.h), un nombre entier, utilisable dans le test de fin de recherche.
La fonction tri réalise le tri alphabétique.
LANGAGE C : TYPES, OPERATEURS, FONCTIONS, POINTEURS ───────────────────────────────────────────────────
137
#include <stdio.h> #include <string.h> #define MAX 20 #define ever (;;) typedef struct { char nom [20 ]; int tsuc;} pers;
void saisie(void) /* Saisie des données */ { FILE *fp;
pers accueil={"" ,0} , vide= {"" ,0}; fp= fopen("personnel","a"); for ever { printf("Donnez le nom : ");
gets (accueil.nom); if (!strlen(accueil.nom)) break; /* Fin de saisie */ fwrite( (char * ) & accueil, sizeof(accueil),1,fp); fflush(fp); /* Vidage du tampon après chaque saisie */ accueil=vide; /* Réinitialisation entre deux saisies*/
} fclose(fp); /* Fermeture du fichier */
}
int lire(pers liste [ ]) /* Remplissage de la liste et constitution du tableau tsuc */ { FILE * fp;
pers accueil; /* Initialisation */ int i , pred , j; int st(char *, char *); /* Fonction de comparaison de deux chaînes*/ liste [0 ].tsuc = 0; i = 1; fp=fopen("personnel","r"); while (fread ((char*) &accueil, sizeof(accueil),1,fp)) /* Chargement d'un enregistrement de longueur sizeof(accueil) du fichier */ /* Dont l'étiquette logique est fp dans le champ mémoire accueil*/ {liste [i ] = accueil; /* Affectation de variable structurée */
pred = 0; j = liste [0 ].tsuc; /* Initialisation de la recherche */ while (j!=0 && st(liste [j ].nom, liste [i ].nom)) { pred = j; j = liste [j ].tsuc; } liste [i ].tsuc = j; /* Modification du champ tsuc */ liste [pred ].tsuc = i; i++ ; /* Enregistrement suivant */ }
fclose(fp); return(i); }
int st(char *s, char *t) /* Comparaison de deux chaîne de caractères */ { int ret;
ret = strcmp(s,t); if (ret == 0) return(-1); if (ret<0) return(1); else return(0);
}
void tri(pers liste [ ]) { int j = liste [0 ].tsuc;
printf("\n\ntri par ordre alphabétique\n"); while(j) { printf(" %s\n",liste [j ].nom); j = liste [j ].tsuc;}
}
138 CHAPITRE IV ───────────────────────────────────────────────────
int main(void) { int i,k; pers liste [MAX ];
/* Prototypes */ void saisie(void), tri(pers [ ]); int lire(pers [ ]); saisie(); /* Saisie de la liste dans un fichier */ k = lire(liste); /* Constitution du tableau des successeurs*/ printf ("liste non triée et indice du successeur\n"); for(i=0;i<k;i++ ) printf("%d%-30.30s%d\n",i,liste [i ].nom, liste [i ].tsuc); tri(liste);
}
// Résultat Donnez le nom : liste non triée et indice du successeur 0 12 1 PHILIPPE 4 2 LUIS 1 3 ALBERTY 10 4 PHILIPPE 11 5 ALBERT 8 6 JULES 2 7 JEAN-LOUIS 6 8 ALBERTA 3 9 HUBERT 7 10 ALBERTINE 9 11 ADELE 5
tri par ordre alphabétique ADELE ALBERT ALBERTA ALBERTY ALBERTINE HUBERT JEAN-LOUIS JULES LUIS PHILIPPE PHILIPPE
On pourra réécrire ce programme avec le champ tsuc de type pointeur sur une variable structurée pers.
BASES DE LA PROGRAMMATION ORIENTEE OBJET
■ Philosophie objet
L'interface d'accès à un objet est implémentée conformément à un modèle abstrait et constitue la couche objet.
Dans la formulation abstraite des problèmes, les données, représentées par des variables structurées, et les propriétés des objets, représentées par les opérations qu'on peut leur appliquer, sont pris en compte simultanément.
L'encapsulation des données et traitements leurs garantit une meilleure protection donc une plus grande fiabilité des programmes.
Avec ce point de vue, les données et le code sont logiquement inséparables, même s'ils sont gérés dans différentes régions de la mémoire.
Ces considérations conduisent à la définition d'un objet : ensemble de données sur lesquelles des procédures, appelées méthodes, peuvent opérer. La programmation d'un objet est réalisée à partir de ses données (séparées) et méthodes ces dernières pouvant être partagées.
1. CLASSES
1.1 Définitions
■ Objet (Object)
Un objet est implémenté sous la forme d'un "paquet" (ou paquetage) logiciel constitué d'un ensemble de procédures appelées méthodes et des données associées.
C'est une abstraction informatique d'une entité du monde réel caractérisée par une identité, un état, un comportement.
Les objets peuvent être définis et gérés indépendamment les uns des autres.
■ Identificateur d'objet (Object Identifier)
Un identificateur d'objet est une référence unique et invariante attribuée à un objet lors de sa création permettant de l'identifier et de le référencer pendant son existence.
■ Attribut (Attribute)
Un attribut représente une donnée caractéristique d'un objet désignée par un identificateur.
CHAPITRE V
140 CHAPITRE V ───────────────────────────────────────────────────
■ Opération (Operation)
Une opération est la représentation d'une action applicable sur un objet, caractérisée par un en-tête appelé signature définissant son nom, ses arguments d'appel (identificateur et type) et le type de l'objet retourné.
■ Méthode - fonction membre
Autres appellations des opérations sur les objets, utilisées dans les langages de programmation à objet tels les langages C++, Java, ou SmallTalk.
■ Messages
Les objets peuvent échanger des informations sous forme de message que le langage Simula définit comme le résultat de l'application d'une méthode opérant sur un objet. L'objet à l'origine du message en est l'émetteur et l'objet destinataire le récepteur.
Un message est donc un ensemble composé d'un identificateur d'objet récepteur, d'un identificateur d'une méthode et de ses arguments permettant, suite à son émission, l'invocation externe de la méthode (publique) de l'objet récepteur.
Dans les environnements objets, ces derniers communiquent donc par des messages comportant le nom d'une méthode et ses arguments.
Un objet peut réceptionner un message et réagir à ce dernier.
L'émission d'un message est une implémentation flexible et contrôlée du traditionnel appel de procédure par valeur des langages de programmation.
■ Classe
Une classe est l'implémentation d'une interface d'utilisation d'un objet décrit selon un modèle abstrait permettant de spécifier ses propriétés (attributs et opérations) et de créer (instancier) des instances de cet objet dotées de ces propriétés communes.
Interface d'accès aux objets publics.
Données Traitements
Attributs Méthodes
Accès public (Utilisateur)
Accès privé (Développeur)
BASES DE LA PROGRAMMATION ORIENTEE OBJET ───────────────────────────────────────────────────
141
Une classe spécifie donc la structure abstraite et le comportement commun des objets qu'elle permet de créer (instanciation). L'instanciation est ainsi une relation d'appartenance d'un objet à une classe donnée. Chaque instanciation provoque une allocation de mémoire dont l'initialisation est réalisée par une méthode appelée constructeur. Lorsqu'elle est détruite, une méthode conjuguée est exécutée : le destructeur. Le programmeur de la classe peut les redéfinir.
La classe définit la structure des données appelée champ, variable d'instance, ou donnée membres de la classe (aspect statique) dont seront constitués ses objets.
Les opérations sur les instances sont réalisées par les méthodes ou fonctions membres de la classe (aspect dynamique).
Une méthode multi-classes est définie dans une classe et peut s'appliquer à des instances de classes différentes, ces dernières étant référencées comme des paramètres de la classe de définition de la méthode.
1.2 Relations d'héritage
■ Héritage (Inheritance)
L'héritage permet de transmettre les propriétés d'une classe de niveau supérieur (classe mère, classe de base, super classe) à une classe de niveau inférieur (classe fille, classe dérivée) ce qui permet la réutilisation par la classe fille de tous les attributs et traitements accessibles de la classe mère.
■ Généralisation (Generalization)
L'héritage représente un lien hiérarchique entre deux classes spécifiant que les objets de la classe de niveau supérieur sont plus généraux que ceux de la classe de niveau inférieur.
■ Hiérarchie de classes
Les classes peuvent être organisées selon un modèle arborescent pour intégrer des cas de plus en plus particuliers au modèle de base.
■ Classes virtuelles (ou abstraites)
Une classe virtuelle (abstraite) est une classe sans possibilité d'instanciation d'objet, créée pour définir les données membres et les méthodes associées qui s'appliqueront à ses classes de plus bas niveau dans la hiérarchie de classes.
■ Héritage multiple (Multiple Inheritance)
Une classe dérivée peut hériter de plusieurs classes de base.
142 CHAPITRE V ───────────────────────────────────────────────────
2. APPROCHE OBJET, DONNEE ABSTRAITE, ENCAPSULATION
2.1 Types abstraits
■ Approche orientée objet
Les logiciels orientés objet modélisent un système qui est une représentation du monde réel.
■ Types abstraits
• La plupart des langages de programmation permettent de définir des types de données abstraites, en complément de ses types prédéfinis.
• Les langages orientés objets permettent en outre d'utiliser les opérateurs traditionnels sur ces données abstraites par surdéfinition. De nouveaux types abstraits y sont définis par la création de nouvelles classes.
■ Extension de la notion de type du langage C en langage C++
La couche objet constitue l'apport essentiel du langage C++ au langage C dont le typage traditionnel a été transformé en classe.
� Exemple
Les types prédéfinis char, int, double, etc. représentent l'ensemble des propriétés des variables de ce type et en constituent la classe avec les opérateurs arithmétiques comme méthodes tel l'addition.
■ Utilisateur et programmeur de la classe
Les règles d'accès aux objets sont différentes pour l'utilisateur et le programmeur pour distinguer leur utilisation de leur implémentation.
• L'utilisateur d'un objet n'a pas à connaître sa représentation interne; pour lui, seul compte son comportement (principe de la boîte noire).
• Le programmeur ne peut ignorer la façon dont une instance d'un type donné est représentée en binaire car la plage des valeurs possibles est essentielle. Ainsi, un bug d'exécution résultant d'un choix erroné de la représentation d'un nombre a provoqué une modification incontrôlable de la trajectoire du lanceur Ariane 5 conduisant au crash de son premier vol de qualification, d'un coût estimé à plusieurs milliards d'euros.
2.2 Agrégat et encapsulation
■ Agrégation (Aggregation)
Une agrégation est une association entre deux classes exprimant que les objets de l'une sont des composants de l'autre. L'agrégation traduit la relation "fait partie de" (is a part off).
Les objets contenus dans un autre constituent un agrégat.
BASES DE LA PROGRAMMATION ORIENTEE OBJET ───────────────────────────────────────────────────
143
■ Encapsulation, objet encapsulé, interface d'objet
• Un objet d'une classe, accessible par une interface d'objet y est encapsulé.
• Les logiciels orientés objet sont conçus par assemblage de modules dans lesquels des données et des comportements peuvent être encapsulés.
• Le fait de rendre un objet privé est l'encapsulation. Cette dernière permet la dissimulation d'informations, l'accès à une instance n'étant possible qu'au travers des méthodes autorisées ce qui la protège et améliore la fiabilité. L'encapsulation permet donc de contrôler les modifications de l'objet concerné.
• Les types prédéfinis garantissent l'abstraction et l'encapsulation des données prédéfinies (comme le type float en C). Par contre, la complexité de la grammaire de ce langage ne permet pas ou n'incite pas le programmeur à définir des types utilisateur garantissant ces mêmes propriétés.
• Les méthodes autorisées constituent l'interface d'accès de l'utilisateur des instances en masquant leur implémentation.
Les avantages sont immédiats :
• l'utilisateur ne peut provoquer d'erreurs d'exécution par modification intempestives de certaines données.
• D'interface d'accès standard, l'objet est utilisable par d'autres applications.
• Le programmeur de la classe peut modifier l'implémentation interne de l'objet sans réécrire tout le programme, les méthodes utilisées devant conserver les mêmes identificateurs et arguments.
• L'interface d'objet (Object Interface) est décrite par l'ensemble des signatures des méthodes et des attributs publics d'un objet.
3. INITIALISATION DES OBJETS
• En langage C, les règles d'initialisation des variables sont complexes voire confuses. Ainsi, les variables de classe de mémorisation statique sont par défaut toujours initialisées, les variables automatiques non.
• Ces règles sont appliquées sur des tableaux de façon fantaisistes par les éditeurs de logiciel ce qui nuit à la portabilité et à la robustesse des programmes.
■ Constructeur et destructeur
• Une méthode d'initialisation appelée constructeur est définie par défaut ou peut être redéfinie par le programmeur. Elle est toujours appelée implicitement ou explicitement lors de l'instanciation d'une variable de la classe.
• La mémoire allouée par un constructeur est toujours récupérable par une méthode appelée destructeur ou déconstructeur (principe du ramasseur d'ordures (garbage collector)).
144 CHAPITRE V ───────────────────────────────────────────────────
4. POLYMORPHISME, SURDEFINITION ET GENERICITE
■ Polymorphisme (Polymorphism)
On appelle polymorphisme la faculté de dissimulation des détails de l'implémentation à travers une interface d'utilisation ce qui simplifie la communication entre objets. Ainsi, en langage C++, une méthode peut avoir différentes signatures.
■ Surdéfinition (Overloading)
• La surdéfinition (surcharge) d'une fonction (resp. procédure ou méthode) consiste à donner un même identificateur à plusieurs fonctions (resp. procédure ou méthode), la fonction (resp. procédure ou méthode) appropriée étant déterminée par le nombre et le type de ses arguments d'appel.
• La surdéfinition (surcharge) d'un opérateur permet d'étendre ses caractéristiques d'origine à des opérandes d'un type différent ce qui est une forme de polymorphisme.
• La surdéfinition peut s'exécuter en mode :
◊ statique (early binding) : l'objet surdéfini est déterminé à la compilation.
◊ dynamique (dynamic binding ou late binding) : l'objet surdéfini est déterminé à l'exécution (méthodes virtuelles du langage C++).
� Exemple de fonction surdéfinie
Soit la classe des figures géométriques (cercles, carrés, etc.). La méthode surdéfinie dessiner opère sur les instances de la classe… et un cercle n'est pas un carré.
� Exemple d'opérateur surdéfini
Les opérateurs arithmétiques traditionnels du langage C sont surdéfinis. Ainsi, l'opérateur arithmétique + est utilisé sur des objets de type int, float, double, etc..
En C++, cet opérateur surdéfini peut opérer sur des tableaux, des instances de classe, etc.
■ Généricité
Un traitement générique (unique) opère sur des objets de type différent contrairement à une fonction surdéfinie qui a une implémentation spécifique pour chaque type d'objet.
Le langage C permet de définir des variables et des fonctions génériques sous une forme symbolique à partir du préprocesseur.
Le langage C++ implémente les objets génériques sous forme d'objets modèles paramétrés (fonctions, classes, méthodes et constantes) appelés template.
■ Redéfinition (Overriding)
La redéfinition d'une méthode est la spécification dans une classe dérivée d'une méthode définie dans sa classe de base.
BASES DE LA PROGRAMMATION ORIENTEE OBJET ───────────────────────────────────────────────────
145
5. COLLECTIONS D'OBJETS.
■ Conteneur (Container)
Un conteneur d'objets (Container) typés est désigné par un identificateur.
Il contient une collection d'instances organisées conformément une structure particulière et accessibles par des opérations spécifiques au conteneur.
� Exemples
• L'ensemble (Set) définit une collection non ordonnée sans doublon.
• Le sac (Bag) définit une collection non ordonnée avec doublon.
• La liste (List) définit une collection ordonnée avec doublon.
• Le tableau (Array) définit une collection ordonnée, indexée.
■ Objet polymorphique
Un conteneur d'objets polymorphiques contient des objets dont le type peut être paramétré.
■ Classe modèle (classe Template)
Modèle d'objet paramétré.
6. PRINCIPES GENERAUX DE PROTECTION DES DONNEES
Le langage C++ n'offre pas de moyens de contrôle permettant de réaliser n'importe quelle protection. Les principes de base sont les suivants :
• La protection des données dépend de l'application.
• La protection contre les accidents de programmation est assurée par le compilateur sans garantie contre une violation explicite des règles.
• L'unité de protection et d'accès est la classe.
• Le contrôle d'accès est réalisé à partir de l'identificateur de l'objet et non à partir de son type.
• La visibilité n'est pas contrôlée.
146 CHAPITRE V ───────────────────────────────────────────────────
7. ABSTRACTION ET ENCAPSULATION EN C ET C++
Dans le présent paragraphe, les concepts de programmation objet sont utilisés en C puis en C++ pour définir une structure abstraite de pile et les méthodes associées.
■ Représentation interne et méthodes
• L'objet de la classe est une pile représentée par un tableau.
• La base et le sommet de la pile sont représentés par les pointeurs top et pile.
• Les traitements sont les suivants : la fonction pile_vide initialise la pile, la fonction push empile un objet, la fonction pop dépile un objet.
• L'ensemble est décrit dans le fichier pile.c pour être intégré dans une bibliothèque.
■ Interface utilisateur
L'interface utilisateur est décrite dans le fichier pile.h.
7.1 Première implémentation d'un modèle abstrait en C
■ Interface utilisateur
/* fichier pile.h : opérations autorisée */ int pile vide(void), push(int), pop(void); void reinit_pile(void);
■ Fichier pile.c
#include "pile.h" #define TAILLE 1024 static int pile[TAILLE]; static int* top=pile;
int pile_vide (void){ return top==pile;}
int push(int e) { if (top-pile == TAILLE) return 0; *top++=e; return 1; } int pop(void) {if (top!=pile) return *--top;}
void reinit_pile(void) {top=pile;}
■ Avantages
• Abstraction et encapsulation des données : l'implémentation peut être modifiée (structure de données, corps des méthodes) sans modifier l'interface.
• Simplicité de l'écriture.
• Efficacité à l'exécution.
■ Inconvénients
• Mémoire gérée statiquement.
• Absence de définition de l'objet abstrait pile ce qui ne permet pas d'instancier d'autres objets du même type. D'où l'implémentation suivante.
BASES DE LA PROGRAMMATION ORIENTEE OBJET ───────────────────────────────────────────────────
147
7.2 Deuxième implémentation d'un modèle abstrait en C
■ Interface utilisateur (deuxième version)
/* fichier pile.h */ typedef struct { int taille; int* base; int* top;} PILE; void init_pile(PILE *, int); /* Opérations autorisées */ int pile_vide(PILE *) , push(PILE*, int) , pop(PILE*); void effacer_pile(PILE*);
■ Fichier pile.c (deuxième version)
#include <malloc.h> #include "pile.h"
void init_pile(PILE * pile, int taille) {pile->top = pile->base = (int* ) malloc((pile->taille=taille) *sizeof(int));}
int pile_vide(PILE *pile) {return (pile->top == pile->base);} int push(PILE* pile, int entier) { if ((pile->top - pile->base) == pile->taille) return 0;
*pile->top++ = entier; return 1; }
int pop(PILE* pile) {if (pile->top != pile->base) return *--pile->top;} void effacer_pile(PILE* pile) { free(pile->base); pile->top=pile->base=0; pile->taille = 0; }
� Utilisation
#include "pile.c" #include <stdio.h>
int main(void) { int i=0; PILE pile; init_pile(&pile, 10); /* Initialisation non automatique */ while (push(&pile, i++)) /* Utilisation de la pile */ while (!pile_vide(&pile)) printf("%d\n", pop(&pile)); effacer_pile(&pile); /* Destruction de la pile après utilisation */ }
■ Avantages
• Efficacité à l'exécution.
• Gestion dynamique de la mémoire.
• Le type abstrait pile permet à l'utilisateur d'instancier des piles.
■ Inconvénients
• Rien n'empêche de modifier la pile sans utiliser l'interface.
• Complexité du programme due à l'utilisation des pointeurs.
• Initialisation et vidage de la pile non sécurisés car à la charge de l'utilisateur.
148 CHAPITRE V ───────────────────────────────────────────────────
7.3 Troisième implémentation d'un modèle abstrait e n C Ces remarques conduisent à l'implémentation suivante :
■ Interface utilisateur (troisième version)
/* fichier pile.h */ typedef void* PILE ; void init_pile(PILE, int), effacer_pile(PILE*); int pile_vide(PILE), push(PILE, int), pop(PILE);
■ Fichier pile.c (troisième version)
#include <malloc.h> #include "pile.h" typedef struct{ int taille; int *base; int *top;}pile;
void init_pile(void*a, int t) {a = malloc(sizeof(pile)); // Gestion de l'allocation dynamique
((pile*)a)->top = ((pile*)a)->base = (int* )malloc((((pile*)a)->taille=t)*sizeof(int)); }
int pile_vide(void* a) { if (((pile*)a)->top == ((pile*)a)->base) return 1; else return 0;}
int push(void* a, int e) { if ((((pile*)a)->top - ((pile*)a)->base) == ((pile*)a)->taille) return 0;
*((pile*)a)->top++=e; return 1; }
int pop(void* a) { if (((pile*)a)->top !=((pile*)a)->base) return *--((pile*)a)->top;}
void effacer_pile(void** a) {free(((pile*)*a)->base); free(*a); *a=0; }
■ Avantages
• Abstraction et encapsulation des données.
• Gestion dynamique de la mémoire.
• Le type abstrait pile permet à l'utilisateur d'instancier des piles.
■ Inconvénients
• Complexité de la syntaxe.
• Inefficacité à l'exécution due aux trop nombreuses indirections.
• Initialisation et vidage de la pile à la charge de l'utilisateur donc non sécurisés.
■ Conclusions sur les types abstraits en C
• Le langage C permet d'encapsuler des données et de définir des types abstraits.
• Il ne facilite pas la tâche du programmeur d'applications l'encapsulation nécessitant d'utiliser l'indirection d'où une grande complexité d'écriture.
• Tout effort d'abstraction et d'encapsulation en C se traduit également par une écriture plus complexe et un ralentissement possible à l'exécution.
BASES DE LA PROGRAMMATION ORIENTEE OBJET ───────────────────────────────────────────────────
149
7.4 Implémentation d'un modèle abstrait en C++ En C++, l'implémentation du type abstrait PILE est simplifiée par l'utilisation de constructeurs, destructeurs et de l'opérateur d'allocation dynamique de mémoire new.
■ Interface utilisateur (quatrième version)
class PILE {/* fichier pile.h */ private : int taille; int* base; int* top;
public: void init(int), effacer(void); int push(int) , pop(void), vide(void); };
■ Fichier pile.C
#include <iostream.h> #include "pile.h"
void PILE::init(int t){top=base=new int[taille=t]; }
int PILE::vide(void){ return top==base;}
int PILE::push(int e) // Référence implicite à l'objet { if ((top-base)==taille) return 0; *top++ =e; return 1; }
int PILE::pop(void){if (top!=base) return *--top;}
void PILE::effacer(void) { delete base; this->top = this->base = this->taille = 0;}
� Utilisation
int main(void) { int i=0; PILE pile;
pile.init(10); /* Initialisation */
while (pile.push(i++)); while (!pile.vide()) cout << pile.pop() << endl ; pile.effacer(); /* Destruction */ return 1;
}
■ Avantages
• Encapsulation des données : l'utilisateur ne peut accéder à une pile qu'en utilisant l'interface, la partie privée étant réservée au développeur.
• Abstraction : l'utilisateur accède à la représentation des données sans pouvoir la modifier.
• Simplicité de l'écriture : pas de pointeurs explicites.
• Exécution optimisée par le langage.
• Gestion dynamique de l'allocation mémoire.
• L'objet abstrait pile est défini pour instanciations ultérieures.
150 CHAPITRE V ───────────────────────────────────────────────────
7.5 Deuxième implémentation d'un modèle abstrait en C++ La classe pile est encapsulée dans la classe PILE.
■ Interface utilisateur (cinquième version)
class PILE {/* fichier pile.h */ void* adr; public: PILE(int); ~PILE(void); int vide(void), push(int), int pop(void); void effacer(void); };
■ Fichier pile.C (deuxième version)
class pile {int taille; int* base; int * top;
public: // Constructeur par défaut d'une pile de taille 1024 pile(int t=1024) {top=base=new int[taille=t]; } int vide(void){ return top==base; } int push(int e) {if((top-base)==taille) return 0; *top++= e; return 1; } int pop(void) { if(top!=base) return *--top;} ~pile(void) {delete base;}
};
#include "pile.h" typedef pile* adr_pile; PILE::PILE(int t) {adr=new pile(t);} int PILE::vide(void) { return adr_pile(adr)->vide(); } int PILE::push(int e) {return adr_pile(adr)->push(e);} int PILE::pop(void) { return adr_pile(adr)->pop(); } PILE::~PILE(void) {delete adr_pile(adr); adr=0;}
� Utilisation
#include <iostream.h> #include "pile.h" int main() { int i=0; PILE pile(10);
while (pile.push(i++)); while (!pile.vide()) cout << pile.pop() <<endl ; return 1;
}
■ Avantages
• Encapsulation des données abstraites: les données sont protégées.
• Simplicité de l'écriture : pas de pointeurs explicites.
• Gestion dynamique de l'allocation mémoire.
• L'objet pile est défini pour instanciation.
• Implémentation des données abstraites modifiable sans que l'interface ne le soit.
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL
1. INTRODUCTION
■ Le C++, langage de programmation orienté objet
Le langage C++, a été développé par Bjarne Stroustrup pour intégrer les concepts de la programmation orientée objet au langage C à savoir :
• définition d'objet typé basé sur les concepts de classes, d'instances et de méthodes,
• contrôle de la syntaxe basé sur un typage fort,
• initialisation et suppression d'objet à partir de constructeur et destructeur,
• prise en compte de l'héritage (simple ou multiple),
• définition d'objets modèles (objets paramétrés),
• contrôle à priori par le programmeur des erreurs d'exécution à partir de la gestion des exceptions.
Le langage C++ est efficace, performant, et complexe donc quelquefois difficile à interpréter. Avec le C, il est idéal pour réaliser des programmes dont le code source est portable.
■ Le C++, langage procédural et fonctionnel
Les caractéristiques du C++, langage procédural et fonctionnel sont les suivantes :
• syntaxe compatible avec celle du langage C en évitant d'utiliser certaines de ses fonctionnalités grammaticalement douteuses ce qui permet de considérer le langage C++ comme une extension du langage C, certains programmes C devant être adaptés avec des modifications minimes.
• performances similaires à celles du langage C et extension de ses fonctionnalités :
◊ transmission par référence,
◊ généralisation du prototypage,
◊ définition autorisée de valeurs par défaut des arguments d'appel des fonctions,
◊ surdéfinition des fonctions et des opérateurs.
CHAPITRE VI
152 CHAPITRE VI ───────────────────────────────────────────────────
2. OBJETS DE BASE
■ Commentaire
Le délimiteur // permet d'insérer des commentaires, en particulier à la droite d'une instruction.
� Exemple
int valeur; // Ceci est un entier
■ Types de base
On retrouve les types de base traditionnels du langage C, avec quelques évolutions :
void; void *; unsigned char , signed char ; short int, int; long int (unsigned ), unsigned short int, unsigned int; unsigned long int, float, double, long double (non implémenté); enum;
■ Nommage des agrégats
Le nommage des agrégats évite de réutiliser les mots clés struct, class, union à l'instanciation.
� Exemple
struct complexe {float reel; float imaginaire; }; complexe z; // Et non struct complexe z;
■ Type booléen et énumération
Type booléen prédéfini. Définition de constante symbolique par énumération avec contrôle de valeur.
� Exemple
enum Booleen {FAUX, VRAI }; // Différent de {VRAI, FAUX}; enum COULEUR {VERT=5, ROUGE, JAUNE=9 }; // ROUGE équivaut à 6 par défaut. COULEUR couleur = ROUGE; // couleur initialisé à 6. couleur = -7; // Incorrect
■ Type intégral
char , wchar_t; // caractère, caractère long short, int, long, enum;
■ Type flottant
float, double ;
■ Types arithmétiques
Les deux types précédents.
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
153
■ Types dérivés
& opérateur de référence, * opérateur de déréférenciation, [] tableau, () fonction, const qualification d'un type, d'une variable ou du comportement
d'une méthode, class, struct, union types agrégat, .* sélecteur sur objet membre (donnée ou méthode), -> sélecteur sur pointeur.
3. ENTREES/SORTIES ELEMENTAIRES EN C++
3.1 Opérateurs associés Le langage C++ utilise les flux (streams) de préférence aux fonctions de la bibliothèque C scanf, printf, fscanf, fprintf, sscanf, sprints, gets, puts, fgets, fputs, getchar , putchar , etc.
Quatre opérateurs gestionnaires des flux sont définis :
cin et le délimiteur associé >> cout et le délimiteur associé << cerr et le délimiteur associé << clog et le délimiteur associé << L'utilisation de ces opérateurs nécessite l'utilisation du fichier en tête iostream.h.
■ Saisie
• L'utilisation de l'opérateur cin permet d'éviter les erreurs de saisie dues à une mauvaise utilisation de la fonction scanf comme l'illustre l'exemple ci-dessous :
int i ; scanf("%d", i); // Sous Windows98, le plantage total du PC est possible. scanf("%d",&i); // Correct mais les pointeurs arrivent.
• L'opérateur cin saisit en format libre un flux de données d'un quelconque type prédéfini sans le spécifier, retourne la valeur saisie si cette dernière s'est déroulée correctement, 0 sinon.
• Le symbole >> délimite les objets à saisir.
• Le séparateur des données saisies est représenté par un ou (ou non exclusif) plusieurs caractères d'espacement, de tabulation ou les deux. Le caractère CR (retour chariot) est également autorisé.
154 CHAPITRE VI ───────────────────────────────────────────────────
■ Affichage
• L'opérateur cout affiche des données d'un type prédéfini en format libre.
• L'éventuel texte d'accompagnement est délimité par deux doubles quotes ".
• Le délimiteur associé << précède les objets à imprimer.
• L'impression s'effectue de la gauche vers la droite.
• La fonction endl permet de gérer le passage à la ligne.
� Exemple 1
#include <iostream.h> int main() { int a,b ; cout << "Saisir a et b : "; // Affichage du message de saisie (format libre) cin >> a >> b; // Saisie des variables a et b cout << "a= " << a << "\tb= " << b << endl ; // Affichage et passage à la ligne }
// Résultat Saisir a et b : 2 3 a= 2 b= 3
� Exemple 2
// Fonction fact #include <iostream.h> unsigned long fact(unsigned n) { return ((n>1) ? n*fact(n-1) : 1); }
int main() { int i =1 ;
while (i) { cout << "Entrez un nombre : ";
if (! (cin >> i) ) break; cout << "fact(" << n << ") = << fact(n);
} }
■ Affichage de données en format libre
char c='a'; int i=17; float f=4.2; char *s="coucou"; char *v=s; cout << c << ' ' << i << ' '<< f << '\t' << v << ': ' << *v << endl;
// Résultat a 17 4.2 0x4ab2: coucou
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
155
3.2 Interprétation objet des entrées/sorties
■ Objet et traitement associé
• En programmation orientée objet, les objets contrôlent les traitements par refus ou acceptation d'un message. Dans cette dernière situation, l'objet destinataire déclenche le traitement correspondant au message accepté.
• En C++, un message résulte de l'usage d'un opérateur ou d'un appel de fonction.
• Les gestionnaires des flux cin et cout peuvent être considérées comme des objets opérant respectivement sur les flux de données (messages) stdin et stdout.
� Exemple
#include <iostream.h> // Interface d'accès à la gestion des flux standards int main() { float PrixLu; cout << "Prix hors taxes : "; // L'opérateur cout opère sur le flux standard stdout cin >> PrixLu; // L'objet (l'opérateur) cin traite un message cout << "Prix TTC : " << PrixLu*1.196 << endl ; // L'opérateur cout traite trois messages consécutifs cout.width(16); // Un message est un appel de méthode width() cout << PrixLu*1.196 << endl ; cout.width(16); // Cadrage sur 16 caractères d'espacement cout.fill('*'); // Remplissage (limite du facteur de cadrage) cout << PrixLu*1.196 << endl ; return (1); }
// Résultat Prix hors taxes : 100 Prix TTC : 119.60 119.60 ***********119.60
3.3 Fichiers Les classes istream et ostream et les flux cin, cout et cerr gèrent les données des entrées/sorties standard. La gestion des flux sur des fichiers nécessite d'inclure le fichier <fstream.h> qui permet l'accès aux classes ifstream et ofstream.
� Exemple 1
#include <iostream.h> // Ecriture dans un fichier (classe ofstream) #include <fstream.h> int main(void) { ofstream dest("essai.txt");
dest << "Ceci est un test d'écriture" << 3 << endl; dest << "fin de fichier" << endl; dest.close(); return 0;
}
156 CHAPITRE VI ───────────────────────────────────────────────────
� Exemple 2
#include <iostream.h> // Lecture d'un fichier (classe ifstream) #include <fstream.h> int main(void) { ifstream source("essai.txt");
char *ch = new char[50]; source >> ch; cout << ch; return 0;
} Il existe d'autres méthodes dans ces classes : open, seek, tell, flush, eof, etc.
4. INSTRUCTION ET EXPRESSION
■ Instruction exécutable
En C++, les définitions des objets sont des instructions exécutables ce qui permet de définir une variable dans le corps du programme contrairement au C.
� Exemple
#include <iostream.h> int main() { int t[] = {7, 4, 2, 9, 3, 6, 1, 4}; // Tableau d'entiers de dimension incomplète
for (int i=0; i<8; i++) cout<<t[i]; // Définition de la variable entière i cout <<endl ;
}
// Résultats 7 4 2 9 3 6 1 4
■ Déclaration
En langage C++, l'ambiguïté entre définition et déclaration est interdite toute déclaration de variable globale devant comporter obligatoirement le mot-clef extern, contrairement au langage C.
■ Définition
Toute variable globale doit être définie une et une seule fois dans le programme et déclarée explicitement au moins une fois dans chaque fichier où elle est utilisée.
■ Opérateurs spécifiques au langage C++
• L'opérateur d'allocation dynamique d'instances new.
• L'opérateur conjugué de libération de mémoire delete.
• La conversion de type par appel de fonction ou transtypage fonctionnel.
• L'opérateur de résolution de visibilité :: démasque un nom global masqué.
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
157
� Exemple
#include <iostream.h> int x =1; // X variable globale int main() { void f(); // Prototype
cout << " x global " << x << endl ; f(); cout << " x global depuis la fonction main : " << x << endl ;
}
void f() { int x = 3; // Masque le x global
cout << " x local avant affectation x global : " << x << endl ; ::x= 2; // Affectation du x global cout << " x local après affectation du x global : " << x << endl ; cout << " x global depuis f : " << ::x << endl ;
}
// Résultats x global 1 x local avant affectation x global : 3 x local après affectation x global : 3 x global depuis f : 2 x global depuis la fonction main : 2
■ Fichier en tête
En langage C++, un fichier en tête (header) peut contenir :
• des commentaires,
• des déclarations de type, de constantes, de méthodes en ligne (inline), de données et de fonctions externes, de types énumérés,
• des directives d'inclusion et de substitution du préprocesseur C.
Il ne doit jamais contenir de définition de donnée, de fonction, d'agrégat de constantes.
■ Le spécificateur const et les variables
Le spécificateur const peut qualifier tout variable typée pour indiquer qu'elle reste constante après son initialisation.
� Exemple
const char new_line = endl ; const char * voyelles = "aeiouy"; const char * format1 = "%4d\t%d << endl" ; const float pi = 3.14159265;
L'utilisation du mot clé const est une bonne alternative à celle de la directive #define pour définir des constantes symboliques, la vérification de type étant alors effectuée dès la compilation.
En langage C++, la portée du qualificatif const est limitée au fichier courant.
158 CHAPITRE VI ───────────────────────────────────────────────────
■ Nommage des agrégats
• En langage C++, une instance d'une classe est définie à partir de l'identificateur de cette dernière, en omettant le mot clé struct, union ou class sans qu'il ne soit nécessaire d'utiliser le mot clé typedef (langage C).
• Des variables structurées construites selon des modèles abstraits similaires sont différentes.
� Exemple
struct type1 {int a; int b;}; struct type2 {int a; int b;}; type1 s10, s11 type2 s20; s11 = s10; // OK s11 = s20; // Erreur de type
■ Le qualificatif mutable
Cette classe de mémorisation spécifique au langage C++ est utilisée pour redéfinir la qualification d'accès d'attributs en passant outre leur éventuel caractère constant.
■ Types énumérés
Le contrôle de type est plus rigoureux en langage C++. Ainsi, les types énumérés ne sont plus un sous ensemble des types entiers. La conversion implicite d'un élément d'une énumération en un type entier est autorisée. La réciproque impose d'utiliser une conversion de type explicite.
� Exemple
enum Feu {vert, orange, rouge}; int main(void) { Feu croisement = vert;
Feu intersection = rouge; int Valeur = intersection; cout << Valeur << endl; croisement = (Feu) 2; cout << croisement << endl;
}
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
159
5. FONCTIONS ET PROCEDURES
5.1 Prototypage
■ Obligation
Le prototype est obligatoire si la fonction n'est pas définie préalablement à la fonction appelante dans le fichier courant contrairement au langage C il est facultatif.
Synopsis définition : = en_tête corps_de_la_fonction déclaration : = extern en_tête; en_tête: = classe_memorisation type_résultat identificateur_fonction (déclar_params) corps : = instruction_composée
� Exemple
// Définition int f(int a, float b, int c) // En-tête {/* Corps de la fonction */ }
// Déclarations (prototypes, signature) extern int f(int, float, int); extern int printf(const char *, ...); // Nombre et type d'arguments variables.
5.2 Procédure Une procédure est une fonction qui ne retourne rien ce qui se traduit sur le plan syntaxique par le retour d'un objet de type void. L'utilisation du mot clé return est donc interdite dans cette situation.
Une fonction ou procédure avec une liste vide est déclarée sans argument, contrairement au langage C où cette déclaration en indique un nombre indéterminé.
� Exemple
void f() {...} // Procédure sans argument
5.3 Surdéfinition d'une fonction
• Dans les langages procéduraux, plusieurs fonctions effectuant la même action, avec la même sémantique, opérant sur des objets de types différents doivent être implémentées avec des identificateurs et des types d'arguments différents.
• En langage C, le préprocesseur (directive #define) permet d'implémenter des fonctions génériques dont l'inconvénient est l'absence de vérification syntaxique des instructions symboliques et des types symboliques.
• Le langage C++ autorise une définition multiple d'une fonction dont le nombre et les types d'arguments peuvent différer (surdéfinition).
• Ce concept est généralisé à la plupart des (fonctions) opérateurs prédéfinies.
160 CHAPITRE VI ───────────────────────────────────────────────────
■ Signature et surdéfinition d'une fonction
• La signature d'une fonction est définie par sa portée, le nombre et le type de ses arguments ainsi que par le type de l'objet retourné. Elle est représentée par le prototype de la fonction.
• Une fonction surdéfinie a plusieurs signatures.
■ Choix de la fonction à l'exécution
Le choix est effectué à l'exécution selon la signature de la fonction en trois étapes :
• recherche d'une correspondance exacte entre les paramètres formels et les arguments d'appel,
• recherche d'une correspondance en utilisant les conversions de type prédéfinis,
• recherche d'une correspondance en utilisant les conversions définies par l'utilisateur et s'il n'en n'existe qu'une l'appliquer, sinon arrêt du programme du à un problème d'ambiguïté.
� Exemple
#include <iostream.h> int main() { // Prototypes divers de la fonction test surdéfinie
float test(float, float), test(float, int); double test( double, double );
float x, x2; double y, z; int n; cout << "Test (float, int) " << endl ; cout <<"saisir x et n : " ; cin >> x >> n; cout << "test("<< x << "," << n << ")=" << test(x,n) << endl ;
cout << "Test (float, float) " << endl ; cout <<"saisir x et x2 : " ; cin >> x >> x2; cout << "test("<< x << "," << x2 << ")=" << test(x,x2) << endl ;
cout << "Test ( double, double ) " << endl ; cout <<"saisir y et z : " ; cin >> y >> z; cout << "test("<< y << "," << z << ")=" << test(y, z) << endl ; return 1;
}
float test(float x, int n) { if (n <0) {cout << "erreur exposant négatif" << endl ; return -1;}
switch(n) { case 0 : return 1;
case 1 : return x; default : return x*test(x,n-1);
} }
float test (float x, float n) { cout << "float test (float x, float n)" << endl ;
cout << "x= " << x << "\tn = " << n << endl ; return (x*n);
}
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
161
double test (double x, double n) { cout << "double test (double x, double n)" << endl ; cout << "x= " << x << "\tn = " << n << endl ; return (x*n); }
// Résultat Test (float, int) saisir x et n : 3 2 // Flottant puis réel test(3,2)=9
Test (float, float) saisir x et x2 : 2 3 // Deux flottants float test (float x, float n) x= 2 n = 3 test(2,3)=6
Test ( double, double ) saisir y et z : 2 3 // Deux doubles double test (double x, double n) x= 2 n = 3 test(2,3)=6
5.4 Valeur par défaut des arguments d'appel
• En langage C++, les prototypes permettent de définir des valeurs par défaut des arguments depuis la droite vers la gauche de la liste.
• Une nouvelle surdéfinition ne peut pas modifier la valeur par défaut d'un argument mais peut en augmenter le nombre.
• Les appels ambigus sont interdits.
Soit le prototype :
int calcul( int, int =5, int =0);
Tout appel de la forme calcul(x,y,z), calcul(x,y), calcul(x) est licite et les appels calcul(x,y) et calcul(x) se traduisent respectivement par calcul(x,y,0) et calcul(x, 5, 0).
� Exemple 1
#include <iosteam.h> // Fonctions avec 3 valeurs par défaut int main() { int somme(int =0, int=2, int =-3);
int a,b,c; cout << "saisir a, b ,c" << endl; cin >> a >> b >> c; cout <<" a = "<< a << " b = " << b << " c = " << c << endl; cout<<"somme(a,b,c) ="<< somme(a,b,c) << endl << "somme(a,b) ="<<somme(a,b)<<endl; cout << "somme(a) =" << somme(a) << endl << "somme() =" << somme()<< endl ;
}
int somme(int x, int y ,int z) {return x+y+z;}
162 CHAPITRE VI ───────────────────────────────────────────────────
// Résultat saisir a, b ,c 5 -5 -10 a = 5 b = -5 c = -10 somme(a,b,c) = -10 // Somme des 3 valeurs saisies somme(a,b) = -3 // 5 + (-5) + -3 somme(a)=4 // 5 + 2 + -3 somme() = -1 // 0 + 2 + -3
� Exemple 2
#include <iostream.h> int main() { int somme(int =0, int=2, int =-3);
float somme(int, int, int, float); // Surcharge int a,b,c; float t; cout << "saisir a, b ,c" << endl; cin >> a >> b >> c; cout <<" a = "<< a << " b = " << b << " c = " << c << endl; cout << "somme(a,b,c) =" << somme(a,b,c) << endl ; cout << "somme(a,b) =" << somme(a,b) << endl; cout << "somme(a) =" << somme(a) << endl; cout << "somme() =" << somme()<< endl ; cout << "saisir a, b ,c, t" << endl; cin >> a >> b >> c >> t ; cout <<" a = "<< a << " b = " << b << " c = " << c << "t = " << t << endl; cout << "somme(a,b,c,t) =" << somme(a,b,c,t) << endl ; cout << "somme(a,b) =" << somme(a,b) << endl; cout << "somme(a,t) =" << somme(a,t) << endl; cout << "somme() =" << somme()<< endl ;
}
int somme(int x, int y ,int z) { return x+y+z; }
float somme (int x, int y, int z, float t) { return x + y + z +t; }
// Résultat saisir a, b ,c 5 -5 -10 a = 5 b = -5 c = -10 somme(a,b,c) = -10 // Somme des 3 valeurs saisies somme(a,b) = -3 // 5 + (-5) + -3 somme(a)=4 // 5 + 2 + -3 somme() = -1 // 0 + 2 + -3 saisir a, b ,c, t 5 -5 -10 3.14 a = 5 b = -5 c = -10 t = 3.14 somme(a,b,c, t) = -6.86 // Somme des 4 valeurs saisies somme(a,b) = -3 // 5 + (-5) + -3 somme(a,t)=5 // Erreur sur certains compilateurs somme() = -1 // 0 + 2 + -3
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
163
6. REFERENCE
6.1 Définitions • Une référence est un identificateur synonyme d'une variable existante dont
l'utilisation simplifie l'écriture de la transmission des arguments entre les fonctions appelantes et appelées en évitant l'utilisation des pointeurs.
• L'opérateur & est l'opérateur de référence ou d'adresse.
• L'opérateur * est l'opérateur de déréférenciation ou d'indirection.
• Une référence est initialisée en ce sens qu'il doit toujours exister un objet de référence sur lequel les opérateurs opèrent par l'intermédiaire de ladite référence, implémentée sous la forme d'un pointeur constant déréférencé.
� Exemple
#include <iostream.h> int main() { int i = 1;
int & r1 = i, & r2 = i; // r1, r2 sont des références à i int x = r1; // x = i r1 += 2; // i=3 cout << " i = " << i << " r1 = " << r1 << " r2 =" << r2 << " x = " << x << endl;
}
// Résultat i = 3 r1 = 3 r2 = 3 x = 1
6.2 Transmission d'argument par référence • En langage C, les deux modes de transmission des arguments par valeur et par
adresse sont implémentés. Ce dernier mode est délicat à programmer les objets concernés étant représentés sous la forme de variables référencées dans la fonction appelante et de pointeurs déréférencés dans la fonction appelée.
• Le langage C++ définit la transmission des arguments par référence qui évite l'utilisation explicite de pointeur déréférencé dans la fonction appelée tout en permettant l'accès et la modification de l'objet depuis la fonction appelante.
Syntaxe
• Dans la fonction appelante :
◊ l'appel est identique à celui de la transmission par valeur le rendant alors impossible,
◊ seul, le prototype indique le mode de transmission des variables par référence.
• Dans la fonction appelée :
◊ le corps de la fonction est identique à celui de la transmission par valeur,
◊ seul, l'opérateur de référence définit le type des paramètres formels transmis.
164 CHAPITRE VI ───────────────────────────────────────────────────
■ Surdéfinition et ambiguïté
La fonction swap peut être surdéfinie pour une transmission par référence et par adresse car la syntaxe d'appel n'est pas ambiguë, contrairement à la transmission par référence et par valeur.
� Exemple
#include <iostream.h> int main() { void swap(int &, int &); // Prototype de l'appel par référence void swap(int *, int*); // Prototype de l'appel par adresse
int a =2, b=3; swap(a,b); cout << "a= " << a << "\tb = " << b << endl ; swap(&a,&b); cout << "a= " << a << "\tb = " << b << endl ; }
void swap(int &x, int &y) // Transmission par référence { int aux; aux =x; x = y; y = aux; }
void swap (int *a, int *b) // Transmission par adresse { int aux; aux=*a; *a=*b; *b=aux;}
// Résultat a= 3 b = 2 a= 2 b = 3
6.3 Le spécificateur const et la transmission d'argument par référence
■ Inconvénient de la transmission par valeur
Toutes variable transmise par valeur est sauvegardée dans la pile d'exécution. La fonction appelée s'exécute avec une copie des arguments effectifs transmis ce qui est pénalisant quand la mémoire nécessaire pour l'argument effectif est importante.
■ Inconvénient de la transmission par référence ou par adresse
La transmission par référence permet à la fonction où la procédure appelée de modifier les arguments transmis, même quand le programmeur ne le souhaite pas.
■ L'art du compromis
L'utilisation du spécificateur const permet une transmission par adresse ou référence en garantissant l'intégrité de la variable transmise par la fonction ou la procédure appelée avec un gain de mémoire.
extern char * strcpy(const char *, char *);
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
165
6.4 Fonction retournant une référence Une fonction peut retourner une référence (pointeur déréférencé) donc une Lvaleur. D'où la conséquence : un appel de fonction peut être une Lvaleur quand la fonction retourne une référence et figurer à gauche de l'opérateur d'affectation.
Syntaxe type_résultat & identificateur_de_fonction(...){corps_de_la_fonction}
� Exemple
Nous anticipons ici sur l'utilisation des notions de classe et de constructeur.
#include <iostream.h> #include <stdlib.h> #define TAILLE 80 class Ligne // Une instance de cette classe est une ligne de TAILLE caractères { private : char t[TAILLE + 1]; public: // Appel explicite du constructeur Ligne(char C = ' '); // Construction d'une ligne vide par défaut // Fonction (membre inline) retournant le caractère de la position d'un caractère donné char & Pos(int Position) { if (Position < 1 || Position > TAILLE){ cerr << "Position inacceptable" << endl ; exit(1); } return t[Position-1]; } };
Ligne::Ligne(char C) // Constructeur d'une ligne de C caractères identiques {for (int k=0; k<TAILLE; k++) t[k]=C; t[TAILLE]='\0'; }
int main() { Ligne L, La('A');
cout << "La.Pos(3) = " << La.Pos(3) << endl ; // La fonction peut modifier une position si le résultat est transmis par référence La.Pos(3) = 'Z'; cout << "La.Pos(3) = " << La.Pos(3) << endl ; La.Pos(2) = La.Pos(3); // Erreur de compilation en C: Lvalue required cout << "La.Pos(2) = " << La.Pos(2) << endl ; return 1;
}
// Résultat La.Pos(3) = A La.Pos(3) = Z La.Pos(2) = Z
166 CHAPITRE VI ───────────────────────────────────────────────────
7. EXERCICES
� Exercice 1
Analyser le programme suivant #include <iostream.h> int saisir(char * texte) { cout << texte ;
int a; cin >> a; return a;
}
int additionner(int a, int b) { return(a+b);}
void afficher(char *texte , int a) { cout << texte << a << endl;}
int main() { int a, b;
int saisir (char *); void afficher(char * , int) , afficher(char *, float), afficher(char *, double ) ; int additionner(int, int) ; float additionner(float, float) ; double additionner( double, double ) ; a = saisir("a = "); b = saisir("b = "); afficher("a = ",a); afficher("b = ", b); afficher("c =", additionner(a,b));
}
� Exercice 2
Ecrire une fonction surdéfinie de multiplication de 3 nombres entiers et/ou flottants.
� Exercice 3
Analyser le programme suivant.
#include <iostream.h> int main(void) { int i =1;
int &r =i; // R est une référence à i r++; cout << "\nr = " << r << "\ti = " << i; i++; cout << "\nr = " << r << "\ti = " << i; // I est r cout << "\nLe résultat qui suit est étrange. Pourquoi ? "; cout << "\nr = " << r << "\ti = " << i++; cout << "\nr = " << r << "\ti = " << i; cout << "\nLe résultat qui suit est attendu. Pourquoi ? "; cout << "\nr = " << r++ << "\ti = " << i; cout << "\nr = " << r << "\ti = " << i;
}
LE C++, LANGAGE PROCEDURAL ET FONCTIONNEL ───────────────────────────────────────────────────
167
// Résultat r = 2 i = 2 r = 3 i = 3 Le résultat qui suit est étrange. Pourquoi ? r = 4 i = 3 r = 4 i = 4 Le résultat qui suit est attendu. Pourquoi ? r = 4 i = 4 r = 5 i = 5
� Exercice 4
Comment une variable, transmise par référence et qualifiée constante est-elle (re)transmise à une fonction appelée encapsulée ?
#include <iostream.h> void f1(const int &a) { void f2( int &); cout << "a =" << a << endl; int b =a ; f2(b); }
void f2( int & a) {a++; cout << "f2 : a =" << a << endl; }
int main() { void f1(const int &); int a =1; f1(a); a++; f1(a); }
� Exercice 5
On souhaite définir des opérations sur des nombres telles la saisie et l'affichage d'un nombre, la somme et le produit, la permutation de deux nombres, entiers ou flottants.
1°) Ecrire les fonctions ou procédures correspondantes (surdéfinies) en utilisant quand c'est nécessaire la transmission par valeur ou par référence.
2°) Ecrire une fonction main() qui se décompose uniquement en appel des fonctions précédentes pour effectuer la saisie, l'affichage de nombres que l'on additionnera, multipliera ou permutera selon les besoins.
2°) Compléter ce programme de telle sorte qu'il effectue les mêmes opérations surdéfinies avec des nombres complexes.
CLASSES EN LANGAGE C++
1. RAPPELS
La classe permet d'implémenter un type d'objet abstrait dont la définition comprend :
• une partie privée de représentation des données, réservée au développeur,
• une partie publique constituant l'interface d'appel pour les utilisateurs des objets de la classe et contenant la description de l'ensemble des opérations (méthodes) autorisées sur ces derniers.
Le développeur se réserve l'usage exclusif d'un objet (attribut, méthode) en le déclarant privé ou permet à l'utilisateur d'y accéder en le déclarant public.
Le contrôle des accès aux objets est garanti par le compilateur.
2. DEFINITIONS
En langage C++, la classe est une généralisation aux langages orientés objets des variables structurées du langage C complétée à la fois :
• par la définition de fonctions internes (méthodes) opérants sur les objets de la classe,
• par des mécanismes de protection des données.
CHAPITRE VII
Interface d'accès aux objets publics.
Données Traitements
Attributs Méthodes
Accès public (Utilisateur)
Accès privé (Développeur)
170 CHAPITRE VII ───────────────────────────────────────────────────
2.1 Classes et instances • Une classe est la représentation abstraite d'un ensemble d'objets dotés de propriétés
identiques.
• Chaque objet créé de la classe est appelé instance.
• L'opération de création est appelée instanciation. Elle nécessite bien évidemment de gérer la mémoire nécessaire à l'objet créé.
■ Propriétés
Les propriétés des instances d'une classe sont caractérisées par :
• la définition des informations caractérisant l'objet, appelées données membres, attributs de classe, ou champ,
• la définition des traitements autorisés sur ces objets appelés fonctions membres ou méthodes qui peuvent être définies dans la classe (méthodes en ligne) ou à l'extérieur.
• Dans ce dernier cas, le spécificateur inline peut précéder la définition, externe à la classe, de la méthode.
■ Mots clés
Les spécificateurs struct et class permettent de définir des classes d'objets.
■ Opérateur de résolution de visibilité
L'opérateur de résolution de visibilité (opérateur scope) :: permet de définir ou d'accéder à des méthodes d'une classe à l'extérieur de celle-ci selon la syntaxe :
Type_résultat identificateur_de_la_classe::identificateur_méthode(liste_des_paramètres_formels_typés) {corps_de_la_méthode}
■ Accès à une donnée membre
L'accès à une donnée membre est réalisé à partir de l'opérateur de sélection de membre ., comme en langage C pour une variable structurée.
■ Opération d'une méthode sur une instance
Le traitement d'une instance sur laquelle opère une méthode s'écrit, d'une façon analogue :
instance.methode(argument(s));
CLASSES EN LANGAGE C++ ───────────────────────────────────────────────────
171
� Exemple
#include <string.h> #include <iostream.h> const int NbMaxCarac = 25; struct Produit { // Définition de la classe Produit // Définition des données-membres pour chaque instance char Nom[NbMaxCarac+1]; // Nom du produit float PrixHT; // Prix HT float TauxTVA; // Taux de TVA
// Définition et prototypes des méthodes float PrixTTC() {return PrixHT * (1+TauxTVA);} // Définition d'une méthode inline int FixeNom (const char *); // prototype de la méthode FixeNom }; // Fin de la définition de la classe Produit
int Produit::FixeNom (const char * Texte) // La fonction membre FixeNom {// Recopie au plus les NbMaxCarac premiers caractères de l'argument Texte
// Dans la donnée membre Nom. strncpy (Nom, Texte, NbMaxCarac); Nom[NbMaxCarac] = '\0'; // Chaîne à copier trop longue return strlen(Nom);
}
int main() {Produit P1; // Une instance de la classe Produit
Produit TabProduits[50]; // Un tableau de 50 instances P1.FixeNom("Chocolat Meunier 500g"); // Appel de la méthode FixeNom TabProduits[3] = P1; // Affectation (surdéfinie) entre instances TabProduits[5].FixeNom("Baril 5kg Lessive économique"); TabProduits[5].PrixHT = 45; TabProduits[5].TauxTVA = 0.196; cout << TabProduits[5].Nom <<": " << TabProduits[5].PrixTTC(); // Problème de débordement de chaîne de caractères avec // Strcpy (TabProduits[2].Nom,"Baril 5kg Lessive économique"); return (1);
}
// Résultat Baril 5kg Lessive économi: 53.82
■ Remarque
Le résultat semble satisfaisant sauf pour les débordements de chaînes de caractères (à priori limité par NbMaxCarac).
172 CHAPITRE VII ───────────────────────────────────────────────────
2.2 Propriétés des méthodes en ligne • Une méthode définie dans le corps de la classe est dite inline.
• La sémantique d'utilisation d'une méthode inline est similaire à celle de la classe de mémorisation register pour une variable car c'est une directive de compilation qui spécifie le remplacement de la rupture de séquence de l'appel traditionnel par l'intégration immédiate du corps de la fonction dans le programme dont le code utilise davantage de mémoire mais est d'exécution plus rapide.
• Son utilisation est une bonne alternative à l'emploi de la directive #define :
◊ elle en présente les mêmes avantages liés à la substitution du corps de la fonction à l'appel.
◊ L'utilisation correcte des parenthèses est garantie par le compilateur ce qui limite les effets de bord possibles en langage C et offre une meilleure clarté.
• Une méthode inline n'a pas d'adresse, ne peut être ni récursive ni exportée.
• Une méthode inline étant insérée à son appel doit être définie préalablement à ce dernier (référence en avant).
• Sur le plan syntaxique, il est interdit de déclarer une méthode inline dans les fonctions appelantes ou de les définir dans un fichier séparé, contrairement aux fonctions traditionnelles.
• Une méthode, définie à l'extérieur de la classe peut être spécifiée inline.
� Exemple
inline void bonjour() { cout << "bonjour" << endl;}
3. QUALIFICATION D'ACCES AUX MEMBRES D'UNE CLASSE
L'accès à un membre d'une classe est qualifié privé, protégé (classe dérivée), ou public.
■ Accès privé
L'accès à une donnée membre privée n'est autorisé que pour les fonctions membres et amies de la classe où elle est déclarée. Il doit être réalisé au travers d'un assesseur (permettant d'en connaître la valeur) et d'un modificateur (permettant de la changer). Ces deux méthodes sont usuellement appelées setAttr, getAttr, où Attr représente l'attribut.
Un objet d'accès privé contenu dans un autre y est encapsulé. C'est la définition sémantique de l'encapsulation.
■ Accès public
L'accès à une donnée membre publique est autorisé pour n'importe quelle méthode, fonction, ou opérateur.
CLASSES EN LANGAGE C++ ───────────────────────────────────────────────────
173
■ Qualification d'accès par défaut
Par défaut, les données et fonctions membres des variables structurées (struct) sont d'accès public. Celles des classes (class) sont d'accès privés.
Les mode d'accès par défaut des données et fonctions membres peuvent être requalifiés à partir des spécificateurs d'accès public, private, protected.
■ Accès protégé pour les classes dérivées
L'accès à un attribut protégé n'est autorisé que pour les fonctions membres et amies de la classe où il est déclaré ainsi que celles des classes dérivées par héritage.
■ Portée d'un spécificateur
Un spécificateur d'accès définit les règles d'accès pour les membres de la classe qui le suivent jusqu'à la fin de la classe ou jusqu'à un autre spécificateur d'accès.
Synopsis spécificateur_d'accès : liste_des_membres
■ Retour sur les variables structurées (struct et union)
En langage C++, les variables structurées et les unions sont des classes où tous les accès aux objets membres sont par défaut publics et où il est possible de définir une partie privée. Ainsi, la séquence :
struct pile {int taille, int *base, int *top, int pile(int); }
équivaut à
class pile {public : int taille, int *base, int *top, int pile(int); };
Cette définition est compatible avec le langage C normalisé.
� Exemple
// Deuxième version de la classe Produit #include <iostream.h> #include <string.h> const int NbMaxCarac = 25; // Par défaut, les accès des données membres d'une classe sont privées class Produit { char nom[NbMaxCarac+1]; // Données membre qualifiée d'accès privé
float prixHT, tauxTVA; public: // Méthodes inline float PrixTTC() {return prixHT * (1+tauxTVA);} const char * Nom() {return nom;} // La donnée membre nom reste constante int FixeNom (const char *); // Méthode définie à l'extérieur de la classe
}; // Fin de la définition de la classe Produit
174 CHAPITRE VII ───────────────────────────────────────────────────
int Produit::FixeNom (const char * Texte) // La méthode FixeNom {// Copie au plus les NbMaxCarac premiers caractères de Texte dans le membre nom
strncpy (nom, Texte, NbMaxCarac); nom[NbMaxCarac] = '\0'; // Chaîne à copier trop longue return strlen(nom);
}
int main() { Produit P1;
P1.FixeNom("Chocolat économique Meunier par emballage de 100g"); cout << "P1 : " << P1.Nom() << endl ; P1.FixeNom("Baril 5kg Lessive Paic économique"); cout << "P1 : " << P1.Nom() << endl ; // Cout << "P1 : " << P1.nom << endl ; // Erreur de compilation : Produit::nom is not accessible // strcpy (P1.Nom(),"Baril 5kg Lessive Paic économique"); // Erreur de compilation : cannot convert 'const char *' to 'char *' return (1);
}
// Résultat P1 : Chocolat économique Meuni P1 : Baril 5kg Lessive Paic éc
4. METHODE
■ Règles d'utilisation
Une méthode peut :
• accéder et opérer sur toutes les données membres de la classe leur accès étant contrôlé à l'exécution à l'appel de la méthode.
• appeler toute méthode de la classe y compris elle même par récursion si elle n'est pas qualifiée inline,
• être d'accès public, privé ou protégé.
■ Méthode en ligne (inline)
Une méthode définie dans une classe est toujours inline.
■ Méthode définie à l'extérieur de la classe
• Une méthode, définie à l'extérieur d'une classe doit :
◊ comporter la définition explicite de sa portée,
◊ être déclarée dans la classe.
• Elle peut être définie en ligne si sa définition comporte le spécificateur inline.
CLASSES EN LANGAGE C++ ───────────────────────────────────────────────────
175
5. LE POINTEUR THIS
■ Définition
Dans une méthode non statique, le mot clé this représente un pointeur constant contenant l'adresse de l'instance par l'intermédiaire duquel elle a été invoquée, accessible par déréférenciation à partir de l'expression *this.
Le pointeur this est inaccessible à l'extérieur de la méthode.
� Exemple
#include <iostream.h> class Entiers { public: int i; void affiche(char * chaine)
{ cout << chaine << " this=" << this << " i=" <<this->i << endl;}; };
int main() {Entiers k,l; k.affiche("k : "); l.affiche("l : "); Entiers p; p.affiche("p : "); }
// Résultats k : this =0x50771c24 i=0 l : this =0x50771c22 i=0 p : this =0x50771c20 i=9615 // Initialisation fantaisiste
6. METHODE SPECIFIEE CONSTANTE
Une méthode spécifiée constante ne peut modifier l'instance qui l'invoque.
Seules les méthodes spécifiées constantes peuvent opérer sur des instances qualifiées constantes.
Tout argument qualifié const perd cette qualification dans l'interprétation de sa signature sauf l'instance courante (accessible par le pointeur this). Deux méthodes avec des paramètres identiques dont une qualifiée const peuvent donc être définies.
Synopsis type_résultat identificateur_de_méthode(...) const {corps de la méthode}
176 CHAPITRE VII ───────────────────────────────────────────────────
� Exemple
#include <iostream.h> // Troisième version de la classe Produit #include <string.h> const int NbMaxCarac = 25; enum {Taux1, Taux2}; // Entiers caractérisant les taux de TVA applicables class Produit { char nom[NbMaxCarac+1];
float tauxTVA; public: // Données membres publiques float PrixHT; float PrixTTC() const { return PrixHT * (1+tauxTVA);}
void MemeTVAque (Produit P) {tauxTVA = P.tauxTVA; } // Le taux de P est affecté à l'instance courante
// Prototypes const char * Nom() const; int FixeNom(const char *); void TVA( int); };
// La méthode Nom() qualifiée inline est définie à l'extérieur de la classe inline const char * Produit::Nom() const { return nom;}
// Validation du taux et affectation au membre de tauxTVA void Produit::TVA(int Taux) { switch (Taux)
{ case Taux1 : tauxTVA = 0.055; break; case Taux2 : tauxTVA = 0.196; break; default : cerr << "TVA inacceptable" << endl ; exit (1);
} }
// Copie au plus les NbMaxCarac premiers caractères de Texte dans le membre nom int Produit::FixeNom (const char * Texte) {strncpy (nom, Texte, NbMaxCarac); nom[NbMaxCarac] = '\0'; // Chaîne copiée trop longue
return strlen(nom); }
int main() { Produit P1 , P2;
P1.FixeNom("Chocolat Meunier 100g"); P1.PrixHT = 9; cout << "Prix HT de l'instance P1 : " << P1.PrixHT <<endl ; P1.TVA(Taux2); cout << "Prix TTC de l'instance P1 : " << P1.PrixTTC() <<endl ; P1.TVA(0); cout << "Prix TTC de l'instance P1 : " << P1.PrixTTC() <<endl ; P2.MemeTVAque (P1); P2.PrixHT = 18; cout << "Prix HT de l'instance P2 : " << P2.PrixHT <<endl ; cout << "Prix TTC de l'instance P2 : " << P2.PrixTTC() <<endl ; P2.TVA(4); // Message d'erreur à l'exécution (contrôle du taux) return (1);
}
// Résultat Prix HT de l'instance P1 : 9 Prix TTC de l'instance P1 : 10.764 Prix TTC de l'instance P1 : 9.495 Prix HT de l'instance P2 : 18 Prix TTC de l'instance P2 : 18.99 TVA inacceptable
CLASSES EN LANGAGE C++ ───────────────────────────────────────────────────
177
7. POINTEUR SUR LES MEMBRES D'UNE CLASSE
Les objets membres d'une classe sont accessibles par leur adresse.
� Exemple
#include <iostream.h> class Classe { public: void methode(int a){cout << a << endl ;}; };
void f() {Classe c, *pc=&c; void (Classe::*Pointeur_Methode)(int) =&Classe::methode;
// Quatre écritures pour un même résultat int i =3; c.methode(i); pc->methode(++i); (c.*Pointeur_Methode)(++i) ; (pc->*Pointeur_Methode)(++i);
}
int main() { void f(); f(); }
// Résultats 3 4 5 6
8. EXERCICE
On souhaite créer une bibliothèque de classes de nombres (entiers, réels, complexes, etc.) permettant de réaliser des opérations élémentaires sur ces derniers (saisie, affichage, addition, multiplication, division, soustraction, division modulo, permutation, etc.).
1°) Définir les objets membres d'une classe de nombres entiers (attributs, méthodes) publics ou privés correspondants. Instancier 2 nombres entiers dont on affichera la valeur initiale, la somme, le produit, la division entière, la division modulo et les permuter.
2°) Idem avec une classe de nombres réels et une classe de nombres complexes.
178 CHAPITRE VII ───────────────────────────────────────────────────
// Corrigé partiel #include <iostream.h> class Entiers { int valeur; public : void saisie() , affichage(char *) , addition(Entiers, Entiers) ; friend void permut(Entiers &, Entiers &); };
void Entiers::saisie() { cout << "valeur entière à saisir : " ; cin >> valeur; }
void Entiers::affichage(char * chaine) { cout << chaine << " : " << valeur << endl;}
void Entiers::addition(Entiers A, Entiers B) {valeur=A.valeur+B.valeur;}
void permut(Entiers & i, Entiers & j) { Entiers aux; aux.valeur=i.valeur; i.valeur=j.valeur; j.valeur=aux.valeur; }
int main() { Entiers A,B,C; A.saisie(); A.affichage("A"); B.saisie(); C.addition(A,B); C.affichage("somme :"); permut(B,C); B.affichage("B"); C.affichage("C"); }
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQUES
Les règles d'allocation mémoire des variables en langage C sont très permissives comme nous allons le voir. Elles permettent bien évidemment d'écrire des bons programmes mais il y a de nombreux pièges d'utilisation, corrigés dans les langages à objets (C++, Java, etc.) par les constructeurs et les destructeurs.
Sont ensuite présentées les évolutions sémantiques des règles d'allocation dynamique de mémoire dans les langages C et C++.
La gestion des objets statiques (variables, fonctions, méthodes) est ensuite étudiée.
1. L'INITIALISATION DES VARIABLES EN LANGAGE C
1.1 Classes de mémorisation Les différentes classes de mémorisation des variables sont définies à partir des qualificatifs correspondants :
auto pour les variables automatiques, static pour les variables statiques, register pour les variables registres, extern pour les variables externes.
La portée (visibilité) d'une variable est la partie du programme où elle est accessible, celle-ci pouvant être globale ou de locale.
1.2 Variables locales et globales • Une variable est interne à un bloc, à une fonction ou à un fichier si elle y est
définie. Elle y est externe sinon.
• Un programme en langage C est constitué d'objets externes (variables, fonctions).
• La portée d'une variable locale est limitée à un fichier, une fonction, ou à un bloc.
• Les variables globales externes, définies une seule fois à l'extérieur de toute fonction, sont accessibles par différentes fonctions externes, leurs valeurs étant conservées entre les différents appels.
• Une variable externe peut être déclarée dans chaque fonction l'utilisant par la déclaration facultative extern. Cette dernière n'est obligatoire que si les variables concernées n'ont pas encore été définies (référence en avant). Des variables externes peuvent être définies relativement à certaines fonctions.
CHAPITRE VIII
180 CHAPITRE VIII ───────────────────────────────────────────────────
� Exemple 1
On considère les différentes déclarations d'une variable externe i. Soient les fonctions f1, f2, f3, f4, définies avant la fonction main().
int i = 100; /* Variable globale pour toutes les fonctions */ void f1( void) /* Définition de f1 */ { printf("fonction f1 : ");
i++ ; /* Variable globale définie au début du fichier */ printf(" i = %d\n",i);
}
void f2(void) /* Définition de f2 */ { extern int i; /* Déclaration équivalente à la précédente */
printf("fonction f2 : "); i++ ; /* Variable globale définie au début du fichier */ printf(" i = %d\n",i);
}
void f3(void) /* Définition de f3 */ { extern i; /* Déclaration équivalente à la précédente */
printf("fonction f3 : "); i++ ; /* Variable globale définie au début du fichier */ printf(" i = %d\n",i);
}
int main(void) { /* Déclarations (prototype) de f1, f2, f3 */
void f1(void), f2(void), f3(void); printf("fonction main : i = %d\n",i); f1(); f2(); f3();
}
// Résultat fonction main : i = 100 fonction f1 : i = 101 fonction f2 : i = 102 fonction f3 : i = 103
f 1 f 2 f 3 m a i n
i = 1 0 0
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
181
� Exemple 2
i est globale à l'ensemble du fichier, j accessible dans les fonctions f2 et main.
int i = 100; /* Variable globale pour les fonctions f1, f2, main */ void f1(void) { printf("fonction f1 : ");
i++ ; /* Variable globale définie au début du fichier */ printf(" i = %d\n",i);
}
int j = 20; /* Variable globale pour f2, main, invisible dans f1 sans déclaration extern */ void f2(void) { extern int i,j;
printf("fonction f2 : "); i++ ; printf(" i = %d j = %d\n",i,j);
}
int main(void) { void f1(void), f2(void);
printf("fonction main : i = %d\n",i); f1(); f2();
}
// Résultats fonction main : i = 100 fonction f1 : i = 101 fonction f2 : i = 102 j = 20
� Exemple 3
j étant définie après la fonction f1 y est inaccessible (référence en avant).
int i = 100; /* Variable globale */ void f1(void) { printf("fonction f1 : ");
printf(" j = %d\n",j); /* Erreur car j est définie en avant */ }
int j = 20; void f2(void) { extern int i,j; /* Déclaration équivalente à la précédente*/
printf("fonction f2 : "); i++ ; /* Variable globale définie au début du fichier */ printf(" i = %d j = %d\n",i,j);
}
int main(void) { void f1(void), f2(void); printf("fonction main : i = %d\n",i); f1(); f2(); }
f 1 f 2 m a i n
i = 1 0 0 j = 2 0
182 CHAPITRE VIII ───────────────────────────────────────────────────
� Exemple 4
j défini comme variable externe dans la fonction f1 y est accessible.
int i = 100; /* Variable globale */ void f1(void) { extern j; /* Déclaration complémentaire */
printf("fonction f1 : "); i++ ; /* Variable globale définie au début du fichier */ printf(" j = %d\n",j);
}
int j = 20; void f2(void) { extern int i,j; /* Déclaration optionnelle */
printf("fonction f2 : "); i++ ; printf(" i = %d j = %d\n",i,j);
}
int main(void) { void f1(void), f2(void); printf("fonction main : i = %d\n",i); f1(); f2(); }
// Résultat fonction main : i = 100 fonction f1 : j = 20 fonction f2 : i = 102 j = 20
1.3 Initialisation des variables automatiques La portée d'une variable automatique est le bloc où elle est définie.
Toute variable automatique a une existence dynamique car créée à l'exécution du bloc et disparaissant à la fin de son exécution. Il faut donc impérativement l'initialiser pour éviter un comportement aléatoire d'un programme, la variable étant initialisée à chaque exécution à une valeur résiduelle laissée par le programme précédent.
C'est la classe de mémorisation par défaut.
1.4 Variables statiques Une variable statique peut être interne ou externe (donc locale ou globale).
■ Variable statique interne
Allouée à la compilation, locale à la fonction où elle a été définie, non réinitialisée entre chaque appel de cette dernière ce qui permet de conserver sa valeur entre deux appels son stockage étant permanent pendant l'exécution. C'est donc un point mémoire adressable, confidentiel et permanent.
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
183
� Exemple
Les chaînes de caractères apparaissant dans les spécifications de format de la fonction printf sont ainsi définies, leur identificateur pouvant être utilisé ailleurs et définissant alors une variable différente.
■ Variable statique externe
La portée d'une variable statique externe est le fichier où elle a été définie.
C'est une méthode de camouflage d'une variable, à la fois accessible des fonctions du fichier source et invisible de l'extérieur.
■ Fonction statique
Une fonction statique est invisible hors de son fichier de définition.
� Exemple
i est une variable globale statique, une variable locale statique (f1, f2), une variable externe globale statique (f3, f4, f5), une variable de type automatique.
static i=1
f4 f5f3
static i = 100 i=1
f1 main
static i=3
f2
static i = 100; /* Variable globale statique, de type int par défaut, visible dans f3, f4, f5*/ /* Aucune variable de même nom n'y est définie. */ void f1(void) { static i = 1; /* Variable statique locale définie dans f1 */
printf("fonction f1 : "); i++ ; printf(" i = %d\n",i); }
void f2(void) { static i = 3; /* Variable statique locale définie dans f2 */
printf("fonction f2 : "); i++ ; printf(" i = %d\n",i); }
void f3(void) { printf("fonction f3 : ");
i++ ; printf(" i = %d\n",i); /* Variable statique globale définie au début du fichier */ }
void f4(void) { extern int i; /* Déclaration équivalente à la précédente*/
printf("fonction f4 : "); i++ ; printf(" i = %d\n",i); /* Variable statique globale définie au début du fichier */
}
void f5(void) { extern i; /* Déclaration équivalente à la précédente*/
printf("fonction f5 : "); i++ ;printf(" i = %d\n",i); /* Variable statique globale définie au début du fichier */
}
184 CHAPITRE VIII ───────────────────────────────────────────────────
int main(void) { void f1(void), f2(void), f3(void), f4(void), f5(void);
int i; /* Variable automatique locale à main */ for (i = 0; i < 3; i++ ) { f1();f2();} printf(" résultats pour f3, f4, f5\n "); f3(); f4(); f5();
return(1); }
// Résultat fonction f1 : i = 2 fonction f2 : i = 4 fonction f1 : i = 3 fonction f2 : i = 5 fonction f1 : i = 4 fonction f2 : i = 6 résultats pour f3, f4, f5 fonction f3 : i = 101 fonction f4 : i = 102 fonction f5 : i = 103
1.5 Variables de type register Le type register est utilisé pour charger des variables dans des registres du processeur selon leur disponibilité ce qui optimise les temps d'exécution.
Rappelons que leur taille et leur nombre, spécifique pour tout processeur, détermine les types d'objets autorisés à y être chargés.
Cet objet est donc non portable en ce sens qu'il n'existe aucune fonction du langage permettant de connaître cette information, spécifique à chaque processeur.
2. L'INITIALISATION DES INSTANCES EN LANGAGE C++
En langage C++, deux méthodes permettent respectivement d'allouer et d'initialiser la mémoire puis d'en récupérer l'espace : le constructeur et le destructeur.
• Le constructeur est appelé pour définir et initialiser (implicitement ou explicitement) les données membres lors de toute instanciation.
• Le destructeur définit les opérations à effectuer lors de la restitution de la mémoire utilisée par un objet alloué par un constructeur.
Contrairement au langage C, la fonction main n'est pas la première à s'exécuter, les constructeurs devant l'être préalablement.
L'utilisation d'un constructeur permet une initialisation correcte des instances d'une classe contrairement à la classe de mémorisation par défaut (automatique) du langage C.
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
185
3. CONSTRUCTEUR
3.1 Constructeur implicite Le langage C++ fournit un constructeur par défaut appelé constructeur implicite.
Les données membres sont initialisées à une valeur indéterminée.
� Exemple
// Quatrième version de la classe Produit : utilisation du constructeur par défaut #include <iostream.h> enum {Taux1, Taux2}; // Taux de TVA utilisables class Produit { private : float tauxTVA; public: float PrixHT; // Seul le prix HT d'un produit peut varier float PrixTTC() const { return PrixHT * (1+tauxTVA);} // Méthode inline };
int main() { Produit P1; // L'instance P1 est initialisée avec le constructeur par défaut
P1.PrixHT = 10.5; // PrixHT est le seul attribut public initialisé explicitement. cout << "PrixHT = " << P1.PrixHT << "\tPrix TTC = " << P1.PrixTTC() << endl ;
}
// Résultat PrixHT = 10.5 Prix TTC = 10.5 // Taux=0 !!
3.2 Constructeur explicite
■ Définition
Un constructeur explicite est une méthode dont l'identificateur est celui de la classe et qui se substitue au constructeur implicite.
Il ne retourne rien donc ne comporte aucune type de retour.
■ Synopsis
Soit la classe C. Alors:
[C::]C(...){corps_de_la_méthode}
est une méthode constructeur de la classe.
■ Redéfinition du constructeur par défaut
Tout constructeur sans argument surcharge le constructeur par défaut. Il doit être cohérent avec l'objet à construire sur le plan sémantique.
■ Remarque
L'utilisation d’un constructeur explicite impose d'instancier les objets avec le nombre d'arguments défini dans ce dernier ce qui oblige les utilisateurs de la classe à instancier les objets d'une manière convenable.
186 CHAPITRE VIII ───────────────────────────────────────────────────
� Exemple et exercice
On considère la classe Produit. On souhaite exécuter la fonction main() suivante :
int main() { Produit P1("SAVON", 10, Taux2);
Produit P2("LIVRE DE POCHE", 25); cout << "Article P1 : Nom = " << P1.Nom() << " Prix HT = " << P1.PrixHT() ; cout << " Taux = " << P1.Taux() << " Prix TTC = " << P1.PrixTTC() ; cout << "\nArticle P2 : Nom = " << P2.Nom() << " Prix HT = " << P2.PrixHT() ; cout << " Taux = " << P2.Taux() << " Prix TTC = " << P2.PrixTTC() << endl;
// Produit P2; // Erreur de compilation : could not find a match for 'Produit::Produit() }
// Résultat Article P1 : Nom = SAVON Prix HT = 10 Taux = 0.196 Prix TTC = 11.96 Article P2 : Nom = LIVRE DE POCHE Prix HT = 25 Taux = 0.055 Prix TTC = 26.375 1°) Ecrire les méthodes void fixeNom(float), void prix(float), void tva(int)
ainsi que les méthodes inline
float PrixTTC() const const char * Nom() const const float PrixHT() const float Taux()
2°) Ecrire un constructeur de la classe qui fixe le taux de TVA par défaut à 5.5%. // Cinquième version de la classe Produit : constructeur explicite #include <iostream.h> #include <string.h> #include <stdlib.h> const int NbMaxCarac = 25; enum {Taux1, Taux2}; class Produit { private :
char nom[NbMaxCarac+1]; // Nom de l'instance float tauxTVA; // Taux de TVA float prixHT; // Prix HT // Prototypes des méthodes définies à l'extérieur à la classe void fixeNom(const char *); void prix(float); // Affecte une valeur valide à la donnée membre prixHT void tva(int); // Affecte une valeur valide à la donnée membre tauxTVA
public: // Méthodes inline float PrixTTC() const { return prixHT * (1+tauxTVA); } const char * Nom() const { return nom;} const float PrixHT() const { return prixHT;} const float Taux() const { return tauxTVA;} // Constructeur inline Produit(const char * Nom, float Prix, int TVA = Taux1) {fixeNom(Nom); prix(Prix); tva(TVA);}
}; // Fin de la classe Produit
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
187
void Produit::fixeNom (const char * Texte) { strncpy (nom, Texte, NbMaxCarac);
nom[NbMaxCarac] = '\0'; }
void Produit::prix(float Prix) // Affectation d'une valeur valide au membre prixHT {if (Prix < 1 || Prix > 1000) {cerr << "prix HT inacceptable" << endl ; exit(1);}
prixHT = Prix; }
void Produit::tva(int Taux) // Validation du taux de TVA {switch (Taux)
{ case Taux1 : tauxTVA = 0.055; break; case Taux2 : tauxTVA = 0.196; break; default : cerr << "\nTVA inacceptable" << endl ; exit(1); }
}
3.3 Constructeurs multiples Plusieurs constructeurs peuvent être définis dans une classe à la condition que leurs signatures respectives soient distinctes, le constructeur utilisé à l'exécution étant celui dont la signature est (la plus) adaptée.
� Exemple
Soit la classe Produit. On souhaite exécuter la fonction main() suivante :
int main() { Produit P1("SAVON", 7.5, Taux2); // 1er constructeur Produit P2("LIVRE DE POCHE 1 VOL",25); // 1er constructeur (taux par défaut) Produit Base; // 2ième constructeur Produit P3 ("LESSIVE PROMO"); // 3ième constructeur Produit P4; // 2ième constructeur P4 = "NOUVEAUTE"; // 3ième constructeur avec copie membre à membre cout << "\nArticle\tNom\t\t\tPrix HT\tTaux\tPrix TTC" ; cout << "\nP1\t" << P1.Nom() << "\t" << P1.PrixHT() << "\t" << P1.Taux(); cout << "\t" << P1.PrixTTC() ; cout << "\nP2\t" << P2.Nom() << "\t" << P2.PrixHT() << "\t" << P2.Taux() ; cout << "\t" << P2.PrixTTC() ; cout << "\nP3\t" << P3.Nom() << "\t" << P3.PrixHT() << "\t" << P3.Taux(); cout << "\t" << P3.PrixTTC() ; cout << "\nP4\t" << P4.Nom() << "\t" << P4.PrixHT() << "\t" << P4.Taux(); cout << "\t" << P4.PrixTTC() ; cout<< "\nBase\t" << Base.Nom() << "\t" << Base.PrixHT() << "\t" << Base.Taux(); cout << "\t" << Base.PrixTTC() ; }
// Résultat Article Nom Prix HT Taux Prix TTC P1 SAVON 7.5 0.196 8.97 P2 LIVRE DE POCHE 1 VOL 25 0.055 26.375 P3 LESSIVE PROMO 8.36 0.196 10 P4 NOUVEAUTE 8.36 0.196 10 Base TEMOIN 83.60 0.196 100
188 CHAPITRE VIII ───────────────────────────────────────────────────
1°) Ecrire les méthodes
void fixeNom(float), void prix(float), void tva(int)
ainsi que les méthodes inline
float PrixTTC() const , const char * Nom() const const float PrixHT() , const float Taux()
2°) Ecrire un constructeur (défaut) de la classe Produit fixant le taux de TVA à 5.5%.
3°) Ecrire un constructeur de la classe Produit qui fixe le nom d'un produit témoin dont le prix TTC est à 100 € avec un taux de TVA à 19.6%.
4°) Ecrire un constructeur de la classe Produit qui fixe par défaut le prix TTC de tout produit à 10 € avec un taux de TVA à 19.6%. #include <iostream.h> #include <string.h> #include <stdlib.h> const int NbMaxCarac = 25; enum {Taux1, Taux2};
class Produit { private : // Données membres char nom[NbMaxCarac+1]; float tauxTVA, prixHT; void fixeNom(const char *), prix(float), tva(int); // Prototypes des méthodes
public: // Méthodes inline float PrixTTC() const { return prixHT * (1+tauxTVA); } const char * Nom() const { return nom;} const float PrixHT() const { return prixHT;} const float Taux() const { return tauxTVA;}
Produit(const char * Nom, float Prix, int TVA = Taux1) // 1 : premier constructeur {fixeNom(Nom); prix(Prix); tva(TVA);}
Produit() // 2 : constructeur par défaut {fixeNom ("TEMOIN"); prixHT = 100/1.196; tva(Taux2);}
Produit(const char * Nom) // 3 : produit à 10 € {fixeNom(Nom); prixHT = 10/1.196; tva(Taux2); } };
// Méthodes void Produit::fixeNom (const char * Texte) { strncpy (nom, Texte, NbMaxCarac); nom[NbMaxCarac] = '\0'; }
void Produit::prix(float Prix) { if (Prix < 1 || Prix > 1000) {cerr << "prix HT inacceptable" << endl ; exit(1);} prixHT = Prix; }
void Produit::tva(int Taux) { switch (Taux)
{ case Taux1 : tauxTVA = 0.055; break; case Taux2 : tauxTVA = 0.196; break; default : cerr << "\nTVA inacceptable" << endl ; exit (1);
} }
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
189
3.4 Transtypage par appel d'un constructeur En langage C++, deux opérations de transtypage explicite sont définies : la forme traditionnelle du langage C et le transtypage par appel d'un constructeur, encore appelé transtypage fonctionnel du langage C++.
■ Synopsis du transtypage fonctionnel
Un nom de type suivi d'une expression entre parenthèses convertit l'expression conformément au type de retour spécifié par l'appel du constructeur implicite.
Quand expression est une liste, le transtypage s'effectue par l'appel d'un constructeur, la classe de l'objet devant être dotée du constructeur adéquat.
� Exemple 1
float f ; long i = (long) f ; // Forme traditionnelle i = long(f) ; // Forme fonctionnelle : appel du constructeur implicite
� Exemple 2
// Transtypage fonctionnel par appel du constructeur par défaut de la classe int #include <iostream.h> int main() { int i = int(1.2); cout << " i = " << i << endl ; i = int(); cout << " i = " << i << endl ; int j = int(); cout << " j = " << j << endl ; return (1); }
// Résultat i = 1 i = 0 j = 0
■ Conversions implicites
Les conversions implicites sont exécutées dès qu'existe un constructeur dont le premier argument est du même type que l'objet source. Dans l'exemple ci-dessous, le nombre situé à la droite de l'affectation est converti en un objet de la classe Entier.
� Exemple
#include <iostream.h> class Entier { public : int i; Entier(int j) {i=j;} // Transtypage de nombres entiers par appel du constructeur };
int main() { int j=2;
Entier e1(j), e2=j, e3(5) ; cout << " e1 = " << e1.i << " e2 = " << e2.i << " e3 = " << e3.i << endl;
}
190 CHAPITRE VIII ───────────────────────────────────────────────────
■ Le mot clé explicit
Le mot-clé explicit utilisé avant la déclaration du constructeur force la conversion explicite à partir d'un transtypage fonctionnel.
� Exemple
#include <iostream.h> class Entier {public : int i; explicit Entier(int j) {i=j; return ;} };
int main() {int j=6; // Entier e1; // Erreur Entier e1=(Entier) j; // e1.i=6 Entier e2=Entier(j); // e2.i=6 Entier e3=Entier(5); // e3.i=5 }
■ Remarque
Le type ne peut être composé dans la forme fonctionnelle (restriction syntaxique).
� Exemple
#include <iostream.h> int main() { char c = 'a', * p_c = &c; typedef int* p_int; p_int p_i; p_i = p_int(p_c); // OK // P_i = int* (p_c); // KO cout << " c = " << c << " *p_c = " << *p_c << " *p_i = " << *p_i << endl; }
// Résultat c = a *p_c = a *p_i = 97
4. DESTRUCTEUR
■ Destructeurs implicite et explicite
• Le destructeur est appelé quand une instance sort de la portée de la classe.
• Le destructeur par défaut libère la mémoire occupé par l'instance.
• Un destructeur ne peut être surdéfini son contexte d'utilisation étant inconnu.
• L'unique destructeur explicite, sans argument, d'une classe C est défini à partir de l'opérateur ~ selon la syntaxe :
[C::]~C(){...}
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
191
5. GESTION DYNAMIQUE DE LA MEMOIRE EN LANGAGE C
La gestion dynamique de la mémoire permet d'allouer dynamiquement une zone mémoire et de la restituer au système d'exploitation après usage.
■ Taille des objets : l'opérateur sizeof
L'opérateur sizeof retourne le nombre d'octets de la représentation interne de son opérande qui peut être un nom de type, l'identificateur d'un tableau, d'une fonction, d'une variable structurée, ou un objet typé.
Le type retourné par l'opérateur sizeof est défini par la constante size_t (type non signé intégral) dans le fichier stddef.h.
Synopsis : size_t sizeof(objet);
On peut ainsi déterminer la taille des types des objets de base d'un ordinateur donné.
� Exemple 1
#include <stdio.h> int main(void) {/* Utilisation de l'opérateur sizeof avec un processeur 16 bits */
char tab1[10]; int tab2[10]; float tab3[10]; short tab4[10]; unsigned tab5[10]; double tab6[10]; printf("sizeof(char) = %d\t",sizeof(char)); printf("sizeof(int) = %d\t",sizeof(int)); printf("sizeof(short)= %d\n",sizeof(short)); printf("sizeof(long) = %d\t",sizeof(long)); printf("sizeof(float)= %d\t",sizeof(float)); printf("sizeof(double)= %d\n",sizeof(double)); printf("sizeof(long double)= %d \t ",sizeof(long double)); printf("sizeof(tab1) = %d\t",sizeof(tab1)); printf("sizeof(tab2) = %d\n",sizeof(tab2)); printf("sizeof(tab3) = %d\t",sizeof(tab3)); printf("sizeof(tab4) = %d\t",sizeof(tab4)); printf("sizeof(tab5) = %d\n",sizeof(tab5)); printf("sizeof(tab6) = %d\t",sizeof(tab6));
}
// Résultat en octets sizeof(char) = 1 sizeof(int) = 2 sizeof(short) = 2 sizeof(long) = 4 sizeof(float) = 4 sizeof(double)= 8 sizeof(long double) = 8 sizeof(tab1) = 10 sizeof(tab2) = 20 sizeof(tab3) = 40 sizeof(tab4) = 20 sizeof(tab5) = 20 sizeof(tab6) = 80
Sur une processeur 32 bits, on aurait obtenu
sizeof(int) = 4
192 CHAPITRE VIII ───────────────────────────────────────────────────
� Exemple 2
#include <stdio.h> int main(void) { struct date { int jour; int mois; int années;} demain,*ptr;
printf("sizeof(struct date) = %d\n", sizeof(struct date)); printf("sizeof(demain) = %d\n",sizeof(demain)); printf("sizeof(*ptr) = %d\n", sizeof(*ptr)); printf("sizeof(ptr) = %d\n", sizeof(ptr));
}
// Résultat sizeof(struct date) = 6 sizeof(demain) = 6 sizeof(*ptr) = 6 sizeof(ptr) = 2
■ Mémoire occupée par un tableau
Soient le tableau mono-indice tab, * l'opérateur d'indirection, type(tab) le type des composantes du tableau. L'adresse du i-ème élément est obtenue par la formule :
tab[i] = *(tab[0] + i*sizeof(type(tab)))= *(tab + i*sizeof(type(tab)))
La définition char tab[4][2]; donne :
tab[i][j] = *(tab + i*sizeof(type(tab)) + j*sizeof(char))
■ Le pointeur générique
Les fonctions d'allocation dynamique de mémoire opèrent sur des objets dont le type effectif est déterminé à l'exécution du programme.
L'adresse de ces objets, de type prédéfini size_t, est décrite par le pointeur générique void * qui permet :
• au programmeur d'applications de définir des traitements qui s'appliqueront dynamiquement à l'exécution sur des objets dont le type sera déterminé par l'utilisateur.
• de comparer des pointeurs sur des objets de type différent tout pointeur pouvant être comparé au pointeur générique. Il est alors fondamental d'utiliser l'opérateur de transtypage pour convertir une adresse générique en un pointeur sur un objet d'un type déterminé.
■ Allocation dynamique et libération
La gestion des requêtes d'allocation dynamique de mémoire est intégrée à la bibliothèque standard du langage C.
• La fonction calloc fait une requête d'allocation dynamique d'une zone de mémoire pouvant contenir n items, chacun d'une taille t, initialisée à une valeur nulle.
• La fonction malloc retourne un pointeur sur le premier octet d'une zone allouée d'au moins n octets
• La fonction free libère l'espace en mémoire alloué par une des fonctions malloc, calloc ou realloc accessible à partir de l'adresse du bloc mémoire à libérer.
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
193
Synopsis #include <stdlib.h> void *calloc(size_t n, size_t t);
#include <malloc.h> void *malloc(size_t n);
#include <stdlib.h> void free(void *ptr);
� Exemple
int main(void) // Allocation dynamique d'un tableau {int i; float *p; p = (float *) malloc(100*sizeof(float)); // Allocation d'un tableau de 100 flottants for(i=0;i < 100; i++) *(p+i)=i; // Remplissage du tableau … free(p); // Libération }
6. GESTION DYNAMIQUE DE LA MEMOIRE EN C++
• Le langage C++ définit l'opérateur new de gestion de l'allocation dynamique qu'il est préférable d'utiliser à la place des fonctions traditionnelles de la bibliothèque C (malloc, calloc, realloc, etc...), en particulier sur des instances de classes.
• La fonction free (C) ou l'opérateur delete (C++) restitue l'espace alloué .
• Ces opérateurs sont appelés implicitement ou explicitement par les constructeurs et destructeurs.
■ L'opérateur d'allocation dynamique new
• L'opérateur new effectue l'allocation mémoire sans initialisation et appelle si nécessaire un constructeur d'objet. Il renvoie la constante NULL en cas d'échec.
• La définition d'un modèle d'objet se distingue d'une instanciation qui provoque toujours l'appel d'un constructeur.
• La définition du constructeur par défaut est nécessaire pour initialiser chaque composante d'un tableau d'instances.
• L'opérateur new retourne un pointeur typé sur la composante du tableau alloué.
• La définition de la taille du tableau n'est pas obligatoire pour l'utilisation standard mais peut le devenir pour une surdéfinition.
Synopsis new déclaration_de_type; // Un objet new classe; // Une instance de la classe new classe[Taille_du_Tableau]; // Un tableau d'instances de la classe new classe(valeur); // Initialisation par transtypage fonctionnel
194 CHAPITRE VIII ───────────────────────────────────────────────────
■ L'opérateur de libération delete
L'opérateur delete libère la mémoire alloué par l'opérateur new selon la syntaxe :
delete 0; // Autorisé et sans effet. delete P // Libération de l'espace alloué à l'instance pointée par P delete [] P // Libération de l'espace alloué au tableau d'instances P
■ Règles d'utilisation
• Les opérateurs new et delete d'allocation et de libération de la mémoire doivent être utilisés de préférence aux fonctions de la bibliothèque C malloc et free car ils garantissent un contrôle du type et une initialisation correcte.
• Il ne faut pas mélanger les fonctions et opérateurs d'allocation mémoire des langages C et C++, la gestion de la mémoire étant différente.
• Les opérateurs delete et delete [] ne doivent pas être utilisés sur une zone mémoire accessible avec le pointeur générique.
• Il faut utiliser l'opérateur delete avec les pointeurs retournés par l'opérateur new et l'opérateur delete [] avec ceux retournés par l'opérateur new [].
• L'opérateur new [] alloue la mémoire et crée les objets dans l'ordre croissant des adresses. L'opérateur delete [] les détruit dans l'ordre décroissant inverse.
� Exemple 1
// Allocation dynamique pour la donnée membre Nom avec l'opérateur new #include <iostream.h> #include <string.h> #include <stdlib.h> // Pour exit()
class Produit { char * nom; float prix;
public: Produit (const char * Nom, float Valeur) // Constructeur {nom = new char [strlen(Nom)+1];
if (nom == NULL) {cerr << "allocation impossible" << endl ; exit (1);} strcpy (nom, Nom); prix = Valeur;
}
void AfficheToi() const { cout << "Produit " << nom << " de prix " << prix << endl ;}
};
int main() {Produit P1("SAVON",7.5);
Produit * Ptr = &P1; Ptr->AfficheToi(); Ptr = new Produit("FARINE 1KG", 15.5); Ptr->AfficheToi(); // L'instruction Produit * Ptr2 = new Produit[100]; provoque l'erreur de compilation : // Cannot find default constructor to initialize array element of type 'Produit' return 1;
}
// Résultat Produit SAVON de prix 7.5 Produit FARINE 1KG de prix 15.5
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
195
� Exemple 2
Soit le constructeur de la classe Entier qui initialise tout Entier à 0. Définir
1°) un pointeur initialisé sur un Entier (int) indéfini,
2°) un pointeur initialisé sur un Entier (int) défini,
3°) un pointeur sur un tableau de la classe Entier initialisé à 0,
4°) un pointeur sur un tableau d'instances de la classe Entier non initialisé.
#include <iostream.h> #define Taille 10 class Entier { public: int nombre , *ptnombre; Entier(){nombre=0; ptnombre=(int *) NULL; } // Constructeur par défaut ~Entier(){cout << "destructeur" << endl; delete [] ptnombre ; } };
int main() { int *pi = new int; cout << "*pi=" << *pi << endl ; delete pi; int a = 25, *pl = &a; cout << "\t*pl=" << *pl << endl ; int *pj = new int(543); // Initialisation d'un pointeur sur la constante entière 543 cout << "\t*pj=" << *pj << endl ; delete pj;
int *tableau = new int[Taille]; // Constructeur implicite de la classe int et valeurs résiduelles int i; cout << "tableau : " << endl; for(i=0;i<Taille;i++) cout << tableau[i] << " "; cout << endl; delete [] tableau;
Entier *ptab= new Entier [Taille]; // Appel du constructeur explicite cout << "tableau ptab : constructeur explicite" << endl; for(i=0; i<Taille; i++) cout << (*ptab++).nombre << " ";
Entier Tab[Taille]; // Appel du constructeur explicite cout << "\ntableau Tab : constructeur explicite" << endl; for(i=0; i<Taille; i++) cout << Tab[i].nombre << " "; cout << endl ; cout << "tableau pk : constructeur int implicite" << endl; int *pk = new int [Taille] ; // Un tableau d'entiers non initialisés for(i=0; i< Taille;i++) cout << *pk++ << " "; cout <<endl ; }
// Résultat *pi=0 *pl=25 *pj=543 tableau : 543 0 0 3369 0 0 0 0 0 0 // Valeurs résiduelles tableau ptab : constructeur explicite 0 0 0 0 0 0 0 0 0 0 tableau Tab : constructeur explicite 0 0 0 0 0 0 0 0 0 0 tableau pk : constructeur int implicite 0 0 0 0 0 0 0 0 0 0 destructeur ... destructeur
196 CHAPITRE VIII ───────────────────────────────────────────────────
� Exemple 3
La chaîne de caractères doit être initialisée avec un caractère au moins.
// Classe Produit : version avec les opérateurs new et delete #include <iostream.h> #include <string.h> #include <stdlib.h> class Produit {private : char * nom; float prix; char * alloue(int LgrMem) {// Méthode privée d'allocation pour la donnée membre nom
char * Ptr = new char [LgrMem]; if (Ptr == NULL) {cerr << "plus de place en mémoire" << endl ; exit (1); } return Ptr;
}
public: Produit( const char * Nom, float Valeur) // Un constructeur {nom = alloue(strlen(Nom)+1);
strcpy (nom, Nom); prix = Valeur;
}
Produit() // Le constructeur par défaut {nom = alloue(1); nom[0] = '\0';
prix = 0; cout << "constructeur par défaut" << endl ;
}
void AfficheToi() const { cout << "Produit " << nom << " de prix " << prix << endl ; }
};
int main() { Produit P1("SAVON",7.5); // Appel du constructeur
P1.AfficheToi(); Produit *Ptr = new Produit[2]; // 2 instances et appel du constructeur par défaut for (int k=0; k<2; k++) Ptr[k].AfficheToi(); Produit TabProd[2]; // Idem cas précédent
}
// Résultat Produit SAVON de prix 7.5 constructeur par défaut constructeur par défaut Produit de prix 0 Produit de prix 0 constructeur par défaut constructeur par défaut
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
197
7. OBJETS STATIQUES EN LANGAGE C++
7.1 Variables statiques Une variable statique a, comme en C, des propriétés de rémanence et confidentialité.
• Accessible uniquement dans le fichier où elle est définie.
• Non détruite en sortie de bloc comme les variables automatique.
• Accessible dans une fonction, même après un retour d'appel.
� Exemple : analyser les résultats du programme suivant :
#include <iostream.h> int main() { int f(int); // Prototype for(int j=0; j<3; j++) {cout << "fonction main()" << endl; cout << " j = " << j << " f(j) = " << f(j) << endl; cout << "**********************" << endl; } }
int f(int i) { static int s=0 ; s++; cout << " fonction f " << endl; cout << " i = " << i << " s = " << s << endl ; return i+1; }
// Résultats fonction main() fonction f i = 0 s = 1 j = 0 f(j) = 1 ********************** fonction main() fonction f i = 1 s = 2 j = 1 f(j) = 2 ********************** fonction main() fonction f i = 2 s = 3 j = 2 f(j) = 3 **********************
7.2 Fonctions statiques
■ Fonction externe
Par défaut, une fonction définie dans un fichier peut être utilisée dans un autre à condition d'y être préalablement déclarée. La fonction est alors dite externe.
198 CHAPITRE VIII ───────────────────────────────────────────────────
■ Fonction statique
• Il peut être nécessaire de définir des fonctions locales à un fichier pour éviter des conflits d'identificateur (deux fonctions de même nom, même signature, dans deux fichiers différents) ou parce que la fonction est exclusivement d'intérêt local.
• Les langages C et C++ utilisent le qualificatif static, qui, en précédant la définition et les déclarations d'une fonction, la rend visible exclusivement dans ce fichier.
• La syntaxe d'utilisation des fonctions qualifiées static est identique à celle des fonctions traditionnelles.
� Exemple
static int locale1(void); // Déclaration d'une fonction statique /* Définition d'une autre fonction statique : */ static int locale2(int i, float j) { return i*i+j;}
7.3 Objets statiques d'une classe
■ Propriété des objets statiques
• Les objets statiques d'une classe peuvent être des données membres, des variables qualifiées statiques définies dans une méthode, des méthodes statiques.
• Les attributs statiques d'une classe sont communs à toutes ses instances.
7.4 Donnée membre statique
■ Propriétés
Une donnée membre qualifiée static :
• caractérise la classe et non ses instances,
• est accessible par tous ses objets,
• a des propriétés de rémanence et de protection identiques à celles d'une variable qualifiée statique en langage C.
■ Constructeur et données statiques
Les données statiques d'une classe n'étant pas spécifiques à une instance donnée, la norme interdit de les initialiser par un constructeur dont le rôle est l'initialisation dynamique des nouvelles instances.
■ Initialisation
L'initialisation des données statiques est réalisée à leur définition, toujours externe à la classe et spécifiée avec l'opérateur de résolution de portée.
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
199
� Exemple
#include <iostream.h> class test { public : static int i; // compteur d'instances test(){i++;}; }; int test::i=0; // Initialisation externe à la classe. int main(void) { test a; cout << test::i << endl; test b; cout << test::i << endl; } // Résultat 1 2
7.5 Variable statique d'une méthode
• Les variables statiques des méthodes doivent y être initialisées.
• Leur portée est réduite à celle du bloc où elles ont été définies.
• Elles caractérisent la classe, pas ses instances.
� Exemple
#include <iostream.h> class Test {public: int compte(void); }; int Test::compte(void) { static int nombre=0; nombre++; return nombre; }
int main(void) {Test objet1, objet2; cout << objet1.compte() << endl; // Affiche 1 cout << objet2.compte() << endl; // Affiche 2 return 0; }
� Exercice : analyser le programme suivant
#include <iostream.h> class Entiers { public: int i; Entiers() {i=0; cout << "this =" << this << endl;}; // Un constructeur void incremente() {i++; cout << "this =" << this << endl;}; void affiche(char * chaine ){cout << chaine << i << endl;}; };
int main() { void f(); f(); f(); }
200 CHAPITRE VIII ───────────────────────────────────────────────────
void f() { static Entiers k,l; k.affiche("k = "); k.incremente(); k.affiche("k = "); l.affiche("l = "); Entiers p; p.affiche("p = "); }
// Résultat this =0x6db70642 this =0x6db70644 k = 0 this =0x6db70642 k = 1 l = 0 this =0x6db71c30 p = 0 k = 1 this =0x6db70642 k = 2 l = 0 this =0x6db71c30 p = 0
7.6 Méthode statique Certaines méthodes peuvent n'opérer que sur des attributs statiques. Leur appel ne nécessite que le nom de leur classe.
■ Définition
• Une méthode statique, définie et qualifiée static, ne caractérise pas la classe. Son appel est identique à celui d'une méthode non statique.
• Une méthode qualifiée static peut être invoquée avec ou sans référence à un objet. Dans le premier cas, la partie gauche de l'expression objet.methode() n'est pas évaluée le pointeur this n'ayant alors pas de sens.
� Exemple 1
#include <iostream.h> class Entier { static int j;
public: static int set_value(void); };
int Entier::j=10; int Entier::set_value(void) {j++; // Légal. return j; }
int main() { cout << Entier::set_value(); cout << " " << Entier::set_value() << endl; }
// Résultat 11 12
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
201
� Exemple 2
#include <iostream.h> class Entier { static int i; public: static int set_value(void); };
int Entier::i=3; // Initialisation externe à la classe int Entier::set_value(void) { return i;}
int main(void) { int resultat=Entier::set_value(); cout << resultat << endl; // Résultat = 3 return 0; }
8. EXERCICES
� Exercice 1
1°) Créer une classe de nombres complexes avec un constructeur devant prendre en compte toutes les situations possibles d'initialisation de ses instances.
2°) Instancier quatre objets dans des situations d'initialisation différentes par un transtypage fonctionnel. On constatera que la situation peut être ambiguë.
#include <iostream.h> class complexe { public:
float reel; float imaginaire; complexe(double x=0, double y=0){reel = x; imaginaire =y;}// Constructeur inline
};
int main() // Initialisation par appel du constructeur et transtypage fonctionnel {// complexe z0 = {1,10}; initialisation traditionnelle interdite si constructeur défini
complexe z1(1.5, -10.78); // 2 flottants complexe z2(-1,3); // 2 entiers et transtypage fonctionnel, identique à // complexe z2= complexe(-1,3); complexe z3 = complexe(); // Valeur par défaut, mais // complexe z3(); // Interdit complexe z4 = complexe(-5); // Identique à complexe z4(-5, 0); complexe z5(0,-5); cout << "z1.reel = " << z1.reel << "\tz1.imaginaire = " << z1.imaginaire << endl ; cout << "z1.reel = " << z1.reel << "\tz1.imaginaire = " << z1.imaginaire << endl ; cout << "z2.reel = " << z2.reel << "\tz2.imaginaire = " << z2.imaginaire << endl ; cout << "z3.reel = " << z3.reel << "\tz3.imaginaire = " << z3.imaginaire << endl ; cout << "z4.reel = " << z4.reel << "\tz4.imaginaire = " << z4.imaginaire << endl ; cout << "z5.reel = " << z5.reel << "\tz5.imaginaire = " << z5.imaginaire << endl ;
}
// Résultat z1.reel = 1.5 z1.imaginaire = -10.78 z2.reel = -1 z2.imaginaire = 3 z3.reel = 0 z3.imaginaire = 0 z4.reel = -5 z4.imaginaire = 0 z5.reel = 0 z4.imaginaire = -5
202 CHAPITRE VIII ───────────────────────────────────────────────────
� Exercice 2
Les méthodes publiques (saisie et affichage) accèdent aux données privées.
#include <iostream.h> class complexe { float reel; float imaginaire; public: complexe(double x=0, double y=0){reel = x; imaginaire =y;} // Constructeur void affiche(char *), affiche(void), saisie(char *); };
void complexe::saisie(char * chaine) { cout << "Nombre complexe " << chaine << endl; cout << "saisir la partie reelle : " ; cin >> this->reel; cout << endl << "saisir la partie imaginaire : "; cin >> this->imaginaire; }
void complexe::affiche(char * chaine) { cout << "Nombre complexe " << chaine << endl; cout <<"partie reelle : "<< this->reel <<" partie imaginaire : "<<this->imaginaire<< endl; }
int main() // Initialisation avec appel du constructeur et transtypage fonctionnel {// complexe z0 = {1,10}; // Interdit si constructeur défini
void complexe::affiche(char *); void complexe::saisie(char *); complexe z1(1.5, -10.78); // 2 flottants complexe z2(-1,3); // 2 entiers et transtypage fonctionnel, identique à // complexe z2= complexe(-1,3); complexe z3 = complexe(); // Valeur par défaut, mais complexe z3(); interdit complexe z4 = complexe(-5); // Identique à complexe z4(-5, 0);
}
� Exercice 3
1°) Créer une classe de nombres complexes avec deux constructeurs. Il faudra redéfinir le constructeur par défaut pour éviter l'ambiguïté de l'exemple précédent.
2°) Instancier cinq objets de telle sorte que ces constructeurs soient appelés suite à un transtypage fonctionnel dans cinq situations d'initialisation différentes et qu'une situation ambiguë soit impossible.
#include <iostream.h> class complexe { public: float reel; float imaginaire; //constructeurs complexe() {reel =0; imaginaire = 0;} // Constructeur par défaut complexe(double x, // X = 0 impossible car ambiguïté avec le constructeur par défaut double y=0) // Nécessaire pour z4 {reel = x; imaginaire =y;} };
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
203
int main() { // Initialisation avec appel des constructeurs et transtypage fonctionnel
complexe z1(1,10), z2= complexe(-1,3), z3 = complexe(), z4(-5) , z5(0,-5); cout << "z1.reel = " << z1.reel << "\tz1.imaginaire = " << z1.imaginaire << endl ; cout << "z2.reel = " << z2.reel << "\tz3.imaginaire = " << z2.imaginaire << endl ; cout << "z3.reel = " << z3.reel << "\tz3.imaginaire = " << z3.imaginaire << endl ; cout << "z4.reel = " << z4.reel << "\tz4.imaginaire = " << z4.imaginaire << endl ; cout << "z5.reel = " << z5.reel << "\tz5.imaginaire = " << z5.imaginaire << endl ; return (1);
}
// Résultat z1.reel = 1 z1.imaginaire = 10 z2.reel = -1 z2.imaginaire = 3 z3.reel = 0 z3.imaginaire = 0 z4.reel = -5 z4.imaginaire = 0 z5.reel = 0 z5.imaginaire = -5
� Exercice 4
On souhaite initialiser des tableaux d'entiers.
1°) Définir une classe TableauEntier avec deux données membres représentant l'adresse et la taille en octet du tableau.
2°) Définir :
• le constructeur par défaut,
• le constructeur d'initialisation d'un tableau d'une taille donnée,
• le constructeur d'initialisation d'un tableau à partir d'un autre. Il faudra surdéfinir les opérateurs d'affectation et crochet.
• une procédure init, appelée par les constructeurs pour initialiser le tableau.
• le destructeur.
3°) Intégrer une fonction en ligne permettant de connaître la taille du tableau.
#include <iostream.h> #include <assert.h> const int TailleDef = 100; // Taille par défaut class TableauEntier { public:
TableauEntier(int Taille = TailleDef); // Constructeur de tableau non dimensionné (défaut) TableauEntier(const int*, int); // Constructeur d'un tableau dimensionné TableauEntier(const TableauEntier &); // Constructeur d'un tableau à partir d'un autre ~TableauEntier(){delete [] adresse; } // Destructeur TableauEntier& operator =(const TableauEntier&);// Opérateur d'affectation surdéfini int& operator [] ( int); // Surdéfinition de l'opérateur [] int getSize() {return Taille;} // Méthode inline protected : // Données internes void init (const int*, int); int Taille, *adresse; // Taille du tableau, adresse du tableau
};
204 CHAPITRE VIII ───────────────────────────────────────────────────
// Constructeur par défaut d'un tableau de dimension non définie // Allocation d'un tableau d'entier de Taille composantes (taille par défaut) TableauEntier::TableauEntier(int TailleDef) {init (0,TailleDef);} // Constructeur d'un tableau dont la dimension est fournie TableauEntier::TableauEntier(const int *tableau, int Taille) {init(tableau,Taille);} // Contructeur d'initialisation d'un tableau à partir d'un autre TableauEntier::TableauEntier(const TableauEntier &A) {init(A.adresse, A.Taille);} // Procédure init void TableauEntier::init(const int * tableau, int TailleDef) {adresse = new int[Taille=TailleDef]; assert(adresse !=0); // Traitement des exceptions for(int ix=0; ix < Taille; ++ix) adresse[ix]=(tableau !=0) ? tableau[ix] : 0; }
TableauEntier& TableauEntier::operator =(const TableauEntier&A) { if (this== &A) return *this; // Tableau lui même delete adresse; init(A.adresse, A.Taille); return *this; }
inline int& TableauEntier::operator [] ( int index) {return (adresse[index]);}
void swap(TableauEntier & tableau, int i, int j) { int tmp =tableau[i]; tableau[i]=tableau[j]; tableau[j]=tmp; }
int main() { int maTaille = 1024;
TableauEntier Tableau, A(maTaille), *pA=&Tableau, A2=Tableau, A3 ; cout << "Tableau.getSize() = " << Tableau.getSize() << "\n"; cout << "A.getSize() = " << A.getSize() << "\n"; cout << "(*pA).getSize() = " << (*pA).getSize() << "\n"; cout << "A2.getSize() = " << A2.getSize() << "\n";
A3=Tableau; cout << "A3.getSize() = " << A3.getSize() << "\n"; for(int i=0; i< maTaille;i++) Tableau[i]=i; cout << Tableau[1] << "\t" << Tableau[Tableau.getSize()] << "\n"; swap(Tableau, 1, Tableau.getSize()); cout << Tableau[1] << "\t" << Tableau[Tableau.getSize()] << "\n"; return 1;
}
// Résultat Tableau.getSize() = 100 A.getSize() = 100 (*pA).getSize() = 100 A2.getSize() = 100 A3.getSize() = 100 1 100 100 1
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
205
� Exercice 5
Un objet postal est décrit par son poids, la valeur de l'affranchissement, et son type (pli ordinaire ou recommandé). Si l'objet est recommandé, il faut indiquer sa valeur déclarée.
1°) Construire la classe ObjetPostal avec les attributs poids, valeur, recommande.
2°) Définir les méthodes aValeurDeclaree, poidsObjet, recommander permettant d'accéder aux données membres précédentes.
3°) Définir un constructeur d'un objet postal.
// Fichier SacPostal.C #include <iostream.h> class ObjetPostal { private :
int poids, valeur, recommande;
public: int tarif; int aValeurDeclaree() {return (valeur >0);} int poidsObjet() {return poids;} void recommander() {recommande =1 ;}
// Constructeurs ObjetPostal(int); ObjetPostal();
};
// Constructeurs par défaut et explicite ObjetPostal::ObjetPostal() {poids = 20 ; valeur =0; recommande =0; }
ObjetPostal::ObjetPostal(int p) {poids = p ; valeur =0; recommande =0;}
int main() {ObjetPostal x; // Appel du constructeur par défaut pour l'instance x
cout << "x.poidsObjet() = " << x.poidsObjet() << endl ; ObjetPostal y= 160; // Appel du constructeur explicite ObjetPostal z(160); // Identique au précédent cout << "y.poidsObjet() = " << y.poidsObjet() << endl ; cout << "z.poidsObjet() = " << z.poidsObjet() << endl ; return 1;
}
// Résultat x.poidsObjet() = 20 y.poidsObjet() = 160 z.poidsObjet() = 160
206 CHAPITRE VIII ───────────────────────────────────────────────────
� Exercice 6
La capacité d'un sac postal lui permet de contenir des instances de la classe ObjetPostal. Définir la classe SacPostal et les méthodes associées (constructeurs et destructeurs d'un sac).
#include "SacPostal.C" SacPostal::SacPostal(int cap) // Constructeur {capacite =cap; nbelts = 0; // Sac vide
sac = new ObjetPostal[cap]; // Allocation d'un tableau d'instances }
SacPostal::~SacPostal() // Destructeur { delete [capacite] sac;} // Restitution de l'espace mémoire utilisé par le tableau
int main() {ObjetPostal x;
ObjetPostal y= 160; SacPostal courrier(250); // 250 objets postaux dans le sac cout<<"courrier.nbelts = " << courrier.nbelts ; cout << "\tcourrier.capacité = " << courrier.capacite <<endl ;
}
// Résultat courrier.nbelts = 0 courrier.capacité = 250;
� Exercice 7
Analyser le résultat d'exécution du programme suivant. Conclusion sur le constructeur par défaut.
#include <iostream.h> #include <string.h> #include <stdlib.h> class Produit {private : char * nom; float prix; char * alloue(int LgrMem) {// Méthode privée d'allocation de la donnée nom
char * Ptr = new char [LgrMem]; if (Ptr == NULL) {cerr << "plus de place en mémoire" << endl ; exit (1); } return Ptr;
}
public: Produit( const char * Nom, float Valeur) // Un constructeur {nom = alloue(strlen(Nom)+1);
strcpy (nom, Nom); prix = Valeur; }
Produit() // Constructeur par défaut {nom = alloue(1);
nom[0] = '\0'; // Garantit le bon fonctionnement de la fonction AfficheToi prix = 0; cout << "appel du constructeur par défaut avec prix = " << prix << endl ;
}
~Produit() {delete [] nom;} // Destructeur d'un tableau d'instances
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
207
void ChangeNom(const char * NouveauNom) // Modification du nom { delete [] nom;
nom=alloue(strlen(NouveauNom+1)); strcpy(nom, NouveauNom);
}
void AfficheToi() const { cout << "Produit " << nom << " de prix " << prix << " €" << endl ; } };
int main(void ) { Produit P1("SAVON",7.5); Produit * Ptr = new Produit; // Un objet dynamique local à main() Ptr->ChangeNom("BROSSE A DENTS"); Ptr->AfficheToi();
{Produit P2("LIVRE DE POCHE 1 VOL",25);// P2 local au bloc P2.AfficheToi(); P2.ChangeNom("POCHE SIMPLE"); P2.AfficheToi(); } // L'instance P2 est détruite à la sortie du bloc
Ptr->AfficheToi(); // On affiche à nouveau Ptr delete Ptr; // Destruction de l'objet pointé par Ptr cout << "Allocation d'un tableau de 3 instances " << endl ; Ptr = new Produit[3]; // Un tableau de 3 instances for (int k=0; k<3; k++) {cout << "k= " << k << " : "; Ptr[k].ChangeNom( "K"); Ptr[k].AfficheToi(); }
delete Ptr; // Destruction du tableau de trois instances P1.ChangeNom("SAVON MENAGER"); P1.AfficheToi(); return (1); }
// Résultat appel du constructeur par défaut avec prix = 0 Produit BROSSE A DENTS de prix 0 € Produit LIVRE DE POCHE 1 VOL de prix 25 € Produit POCHE SIMPLE de prix 25 € Produit BROSSE A DENTS de prix 0 € Allocation d'un tableau de 3 instances appel du constructeur par défaut avec prix = 0 appel du constructeur par défaut avec prix = 0 appel du constructeur par défaut avec prix = 0 K= 0 : Produit K de prix 0 € K= 1 : Produit K de prix 0 € K= 2 : Produit K de prix 0 € Produit SAVON MENAGER de prix 7.5 €
208 CHAPITRE VIII ───────────────────────────────────────────────────
� Exercice 8
Analyser le résultat d'exécution du programme suivant. Conclusion sur le constructeur par défaut.
#include <iostream.h> #include <string.h> const int nbmaxcarac = 25; const float taux1 = 0.196, taux2 = 0.055; class Produit { private : char nom[nbmaxcarac+1]; float tauxTVA, prixHT;
public: void fixenom (char [] ), PrixHT (float), tva (float); float prixTTC() const { return prixHT * (1+tauxTVA);}; char * Nom() {return nom;}; float aff_prixHT() const { return prixHT;}; float taux() const { return tauxTVA;}
Produit (char param_nom[], float param_prixHT , float param_tauxtva=.196) {fixenom (param_nom);
PrixHT (param_prixHT); tva (param_tauxtva);
}
Produit (char * param_nom) {fixenom (param_nom);
tauxTVA=.196; prixHT=10/1.196;
}
Produit() {fixenom("Produit phare");
tauxTVA=.196; prixHT=10/1.196;
}
};
void Produit::fixenom (char * param_nom ) {strncpy(nom, param_nom, nbmaxcarac);
nom[nbmaxcarac] = '\0'; }
void Produit::PrixHT (float param_prixHT) { if ((param_prixHT < 0) | (param_prixHT > 10000))
{ cout << "prix errone" <<endl; prixHT = 0;} else
{prixHT = param_prixHT;} }
void Produit::tva (float param_tauxtva) {tauxTVA = param_tauxtva; }
INITIALISATION, ALLOCATION DYNAMIQUE, OBJETS STATIQ UES ───────────────────────────────────────────────────
209
main() { Produit P1("lait",2.5,taux2); Produit P2("voiture",6000,taux1);
Produit P3("voiture2",15000); Produit P4("sucre"); Produit P5; cout << P1.Nom()<<" prixHT "<<P1.aff_prixHT() << " taux "<< P1.taux() ; cout << " prixttc " << P1.prixTTC()<<endl; cout << P2.Nom()<<" prixHT "<<P2.aff_prixHT() << " taux "<< P2.taux() ; cout << " prixttc " << P2.prixTTC()<<endl; cout << P3.Nom()<<" prixHT "<<P3.aff_prixHT() << " taux "<< P3.taux() ; cout << " prixttc " << P3.prixTTC()<<endl; cout << P4.Nom()<<" prixHT "<<P4.aff_prixHT() << " taux "<< P4.taux() ; cout << " prixttc " << P4.prixTTC()<<endl; cout << P5.Nom()<<" prixHT "<<P5.aff_prixHT() << " taux "<< P5.taux() ; cout << " prixttc " << P5.prixTTC()<<endl;
}
// Résultat prix errone lait prixHT 2.5 taux 0.055 prixttc 2.6375 voiture prixHT 6000 taux 0.196 prixttc 7176 voiture2 prixHT 0 taux 0.196 prixttc 0 sucre prixHT 8.3612 taux 0.196 prixttc 10 Produit phare prixHT 8.3612 taux 0.196 prixttc 10
� Exercice 9
1°) Créer une classe de nombres entiers et une classe de nombres réels avec les caractéristiques suivantes :
• Chaque instance de la classe est décrite par un attribut appelés valeur.
• Les méthodes sont :
◊ instanciation à partir d'un ou plusieurs constructeurs,
◊ saisie d'une instance,
◊ affichage d'une instance,
◊ addition, multiplication, division de 2 instances opérant sur l'instance résultat.
• Ecrire une fonction amie de la classe, permettant d'en permuter deux instances. On essaiera les implémentations par adresse, valeur et référence.
2°) Afficher l'adresse de l'instance courante ainsi que son contenu (pointeur this).
3°) Mêmes questions avec une classe de nombres complexes dont chaque nombre est décrit par les attributs reel et imaginaire. Il faudra pouvoir instancier un nombre réel, complexe, imaginaire pur, à partir du constructeur par défaut ou par un appel explicite.
4°) Modifier si nécessaire les constructeurs pour instancier des tableaux (statiques ou dynamiques) de nombres.
210 CHAPITRE VIII ───────────────────────────────────────────────────
// Corrigé partiel #include <iostream.h> class Entiers { int valeur; public : void saisie() , affichage(char *) , addition(Entiers, Entiers) ; Entiers(int init=0) {valeur = init ; } // Constructeur friend void swap(Entiers &, Entiers &); };
void Entiers::saisie() {cout << "valeur entière à saisie : " ; cin >> valeur; }
void Entiers::affichage(char * chaine) {cout << chaine << " : " << valeur << endl;}
void Entiers::addition(Entiers A, Entiers B) {valeur=A.valeur+B.valeur;}
void swap(Entiers & i, Entiers & j) {Entiers aux; aux.valeur=i.valeur ; i.valeur=j.valeur ; j.valeur=aux.valeur; }
class Reels { float valeur; public : void saisie(void), affichage(void), addition(Reels, Reels), multiplication(Reels, Reels); friend void swap (Reels &, Reels &); Reels(float); // Constructeur };
Reels::Reels(float init = 0){valeur = init;}
void Reels::saisie(void) {cout << " valeur réelle à saisir "; cin >> valeur; cout << endl;}
void Reels::affichage(void) { cout<<"valeur = "<<valeur<<"\tthis = "<<this<<"\tthis -> valeur = "<<this->valeur<< endl;}
void Reels::addition(Reels x, Reels y) {valeur= x.valeur+y.valeur;}
void Reels::multiplication(Reels x, Reels y) {valeur= x.valeur*y.valeur;}
void swap(Reels & v, Reels & w) { Reels aux; aux = v; v = w ; w = aux; } // Equivalent aux instructions suivantes car l'affectation est surdéfinie par défaut // aux.valeur= v.valeur; v.valeur=w.valeur; w.valeur=aux.valeur;
int main(void) { Entiers A,B,C; Reels a , b(0), c(3e-2),d; A.saisie(); A.affichage("A"); B.saisie(); C.addition(A,B); C.affichage("somme :"); swap(B,C); B.affichage("B"); C.affichage("C");
a.affichage(); b.affichage(); c.affichage(); a.saisie(); b.saisie(); c.saisie(); a.affichage(); b.affichage(); c.affichage(); c.addition(a,b); c.affichage(); d.multiplication(a,b); d.affichage(); swap(c, d); c.affichage(); d.affichage(); }
SURDEFINITION DES OPERATEURS
1. GENERALITES ET SYNTAXE
■ Méthode opérateur
Tout opérateur du langage C++ étant implémenté sous la forme d'une méthode opérateur, il peut être surdéfini ce qui le généralise aux instances d'une classe
■ Exemple
L'opérateur + peut être surdéfini pour additionner deux nombres complexes.
■ Règles de syntaxe
La surdéfinition d'un opérateur prédéfini du langage est réalisée par une fonction dont la signature est constituée du mot clé operator suivi de son identificateur :
type_resultat operator opérateur_surdéfini(type argument,…)
La définition originelle des opérateurs sur les types de base ne peut être modifiée.
Il est interdit de définir de nouveaux opérateurs et symboles (par exemple **).
La précédence, le nombre d'opérandes (arité), la priorité, les règles d'associativité de l'opérateur restent celles de l'opérateur non surdéfini.
Les opérateurs = [] () et -> doivent être membres de la classe où ils sont surdéfinis.
Un opérateur peut être surdéfini dans une classe ou à l'extérieur, avec une syntaxe d'utilisation différente dans chaque cas. La version membre impose pour des raisons syntaxiques que l'argument de gauche soit une instance de la classe de l'opérateur.
■ Maximes associées à la surdéfinition des opérateurs
Ne jamais surdéfinir un opérateur dans un sens différent dont son sens "intuitif".
La sémantique de la version surdéfinie doit être compatible avec les types de base des opérandes.
■ Opérateurs autorisés
Tous les opérateurs du langage C++ peuvent être surdéfinis à l'exception des opérateurs :: . * ?: sizeof typeid static_cast dynamic_cast const_cast reinterpret_cast selon les règles suivantes :
CHAPITRE IX
212 CHAPITRE IX ───────────────────────────────────────────────────
arité opérateurs associativité binaire () [] -> gauche à droite unaire + - ++ -- ! ~ * & new new[] delete droite à gauche binaire * / % droite à gauche binaire * -> .* droite à gauche binaire + - droite à gauche binaire << >> droite à gauche binaire < <= >= > droite à gauche binaire == != droite à gauche binaire & droite à gauche binaire ^ droite à gauche binaire || droite à gauche binaire && droite à gauche binaire | droite à gauche binaire = += -= *= /= %= gauche à droite binaire , droite à gauche
2. SURDEFINITON D’UN OPERATEUR NON MEMBRE D'UNE CLASSE
Un des arguments d'un opérateur surdéfini non membre d'une classe doit en être une donnée membre d'accès public.
■ Syntaxe
Type_résultat operator <symbole_associé_à_l'opérateur> (liste_des_paramètres_formels_typés) {corps de la méthode associée à l'opérateur surdéfini}
� Exemple
// Surdéfinition de l'opérateur + pour des instances de la classe complexe #include <iostream.h> class complexe { public : float reel , im; complexe(float,float); }; // Fin de la classe complexe
complexe::complexe(float x=0 , float y = 0) {reel=x; im=y;}
const complexe operator + (const complexe z1, const complexe z2) // Opérateur surdéfini non membre de la classe { complexe aux; aux.reel =z1.reel+z2.reel; aux.im =z1.im+z2.im; return aux; }
int main() { complexe z1(1,10), z2(3,-1), z;
// Les données membres d'accès public sont accessibles par l'opérateur surdéfini. z = z1+z2; cout << "z.reel = " << z.reel << "\tz.im = " << z.im << endl; return (1);
}
// Résultat z.reel = 4 z.im = 9
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
213
3. SURDEFINITION D’UN OPERATEUR MEMBRE D'UNE CLASSE
■ Définition des opérateurs internes
Une deuxième technique de surdéfinition d'un opérateur consiste à le considérer comme une méthode de la classe sur laquelle il opère. Soient A et B deux opérandes et Opérateur la méthode opérateur surdéfinie. L'instruction
A Opérateur B a pour sémantique l'instruction A.Opérateur(B)
On en déduit que, défini dans une classe, l'opérateur surdéfini comporte toujours un argument de moins (le plus à gauche) que l'opérateur non surdéfini, l'objet qui traite le message étant un paramètre implicite de l'appel.
■ Type de retour
La méthode opérateur retourne l'instance courante (pointeur this) ou une instance temporaire selon les cas.
� Exemple 1
#include <iostream.h> class complexe // Addition surdéfinie pour des complexes { public:
float reel , im; complexe operator + (complexe z) {complexe aux; aux.reel =reel+z.reel; aux.im =im+z.im; return aux; }
};
int main() {complexe z1 = {1,10}, z2 = {3,-1}, z;
z = z1+z2; // En fait z1.+(z2) cout << "z.reel = " << z.reel << "\tz.im = " << z.im << endl ; return (1);
}
� Exemple 2
La méthode surdéfinie est inline, définie à l'extérieur de la classe.
#include <iostream.h> // Surdéfinition de l'opérateur définie à l'extérieur de la classe class complexe { public: float reel; float im;
complexe operator + (complexe z); };
inline complexe complexe::operator + (complexe z) {complexe aux; aux.reel =reel+z.reel; aux.im =im+z.im; return aux; }
int main() {complexe z1 = {1,10}, z2 = {3,-1}, z;
z = z1+z2; cout << "z.reel = " << z.reel << "\tz.im = " << z.im; return (1);
}
214 CHAPITRE IX ───────────────────────────────────────────────────
4. AMITIE ET LEVEE PARTIELLE DE L'ENCAPSULATION
La surdéfinition d'un opérateur non membre d'une classe nécessite l'accès à certaines de ses données membres privées donc la levée (partielle ou totale) de leur encapsulation.
■ Définition
• Une fonction, une (toutes les) méthode(s) d'une classe peu(ven)t être autorisée(s) à accéder à la partie privée d'une autre classe (donnée ou méthode) si elle y est (sont) déclarée(s) amie(s) par le qualificatif friend .
• Leur syntaxe d'utilisation est inchangée.
• Une fonction peut être amie de plusieurs classes.
• L'amitié n'est pas transitive l'amie de mon amie n'étant par défaut pas mon amie.
■ Synopsis
friend type_retour fonction_amie(liste type et arguments); friend type_retour [classe::]méthode_amie(liste type et arguments); friend class classe_amie;
� Exemple 1
class X { // Définition des entités amies des objets de la classe X
friend void f(int, float); // La fonction f friend void Y::g(char *, int); // La méthode g de la classe Y friend class Z; // Toutes les méthodes de la classe Z
};
� Exemple 2
La méthode mult de la classe matrice est amie de la classe vecteur.
class matrice { vecteur mult(vecteur); // Déclaration ... };
vecteur matrice::mult(vecteur x){…} // Définition
class vecteur { friend vecteur matrice::mult(vecteur); // Déclaration d'amitié dans la classe vecteur
... };
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
215
5. OPERATEURS RELATIONNELS
Tout opérateur relationnel retourne un type booléen (mot clé bool).
■ Opérations sur les chaînes de caractères
Dans la classe Chaine, les opérateurs de test d'identité et de comparaison de deux chaînes de caractères sont surdéfinis comme suit :
bool Chaine::operator ==(const Chaine &) const ; bool Chaine::operator <(const Chaine &) const ;
■ Définition d’une relation d’ordre
L'opérateur < surdéfini compare les modules de deux nombres complexes.
#include <iostream.h> #include <math.h> class complexe { float reel; float im;
friend complexe operator + (complexe, complexe); friend bool operator <(complexe, complexe); friend float module(complexe);
public : complexe (double, double); // Constructeur void affiche(char *); };
complexe::complexe (double x=0 , double y =0) {reel =x; im=y;}
void complexe::affiche(char * Texte) { cout << Texte << "partie réelle = "<<reel << "\tpartie imaginaire = " <<im<< endl;}
complexe operator + (complexe z1, complexe z2) {complexe aux; aux.reel =z1.reel+z2.reel; aux.im =z1.im+z2.im; return aux; }
bool operator < (complexe z1, complexe z2) { float module(complexe); return(module(z1) < module(z2));}
float module(complexe z) {return(sqrt(z.reel*z.reel+z.im*z.im));}
int main() {complexe z1(1,10), z2(3,-1), z;
z1.affiche("z1 : \t\t"); z2.affiche("z2 : \t\t"); z = z1+z2; z.affiche("z=z1 + z2 : \t"); if (z1 < z2) cout << " |z1| < |z2|"; else cout << "|z2| < |z1|" << endl; return (1);
}
// Résultat z1 : partie réelle = 1 partie imaginaire = 10 z2 : partie réelle = 3 partie imaginaire = -1 z=z1 + z2 partie réelle = 4 partie imaginaire = 9 |z2| < |z1|
216 CHAPITRE IX ───────────────────────────────────────────────────
� Exemple
// Classe Produit : l'opérateur surdéfini < compare les prix respectifs de deux produits #include <iostream.h> #include <string.h> #include <stdlib.h> class Produit { char * nom; float prix;
public: Produit (const char * Nom, float Valeur) // Constructeur { nom = new char [strlen(Nom)+1];
if (nom == NULL) {cerr << "allocation impossible" << endl ; exit(1); } strcpy (nom, Nom); prix = Valeur;
}
~Produit() {delete nom;} // Destructeur
// Surdéfinition de l'opérateur < bool operator < (const Produit P) const { return (prix < P.prix);}
}; // Fin de la définition de la classe Produit
int main() {Produit P1("SAVON PROMO",7.5);
Produit P2("SAVON MARSEILLE", 9.3); if (P1 < P2) // Inférieur équivalent à "moins cher que" cout << "PROMO BON MARCHE" << endl ; // if (P1 < 5.5) cout << "vraiment pas cher" << endl ; // Erreur de compilation : Illegal structure operation
}
// Résultat PROMO BON MARCHE
■ Remarque
On ne peut dans ce programme comparer une instance de la classe Produit à un nombre décimal le compilateur ne disposant pas de règles implicites de conversion.
6. OPERATEUR DE TRANSTYPAGE
■ Transtypage d'un objet typé vers le type classe
La règle du transtypage d'un objet d'un type donné vers un type classe apporte une solution à ce problème.
■ Syntaxe
Un constructeur de la classe C avec un argument unique d'un type donné T définit une règle de transtypage de l'objet de type T en une instance de la classe C.
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
217
� Exemple
// Classe Produit : surdéfinition d'opérateurs #include <iostream.h> #include <string.h> #include <stdlib.h> class Produit { char * nom;
float prix; void fixeNom (const char * Chaine) {nom = new char [strlen(Chaine)+1];
if (nom == NULL) {cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Chaine);
}
public: // Constructeurs Produit (const char * Nom, float Valeur) {fixeNom (Nom); prix = Valeur; }
Produit (float Montant) // Conversion d'un flottant en un Produit {fixeNom ("PRODUIT TEMOIN"); prix = Montant; }
~Produit() {delete nom; } // Destructeur
// Surdéfinition de l'opérateur < bool operator < (const Produit P) const { return (prix < P.prix);} }; // Fin de la définition de la classe Produit
int main() {Produit P1("SAVON PROMO",0.75);
Produit P2("SAVON MARSEILLE",0.93); if (P1 < P2) cout << "PROMO TRES BON MARCHE" << endl ; // Appel du constructeur Produit(float) if (P1 < .55) cout << "VRAIMENT PAS CHER" << endl ; else cout << "PRIX NORMAL" << endl ; // if (1.05 < P1) cout << "PROMO TROP CHERE" << endl ; // Illegal structure operation if (Produit(1.05) < P1) cout << "PROMO TROP CHERE" << endl ; // OK
}
// Résultat PROMO BON MARCHE PRIX NORMAL
■ Surdéfinition de l'opérateur de transtypage
L'opérateur de transtypage surdéfini permet des conversions entre des classes différentes de même sémantique.
� Exemple
Une chaîne de longueur variable est convertie en une chaîne de longueur fixe (un tableau de caractères) par surdéfinition de l'opérateur char const * (cet opérateur opère sur l'instance qui l'invoque).
Chaine::operator char const * (void) const;
218 CHAPITRE IX ───────────────────────────────────────────────────
■ Surdéfinition d'opérateur et fonction amie
La syntaxe impose d'utiliser un argument implicite dans la méthode surdéfinie du paragraphe précédent qui compare une instance de la classe Produit à un nombre décimal ce qui interdit l'opération réciproque.
Un opérateur ami, surdéfini, non membre de la classe, n'utilisant que des arguments explicites résout ce problème.
� Exemple
#include <iostream.h> #include <string.h> #include <stdlib.h>
class Produit { char * nom; float prix;
void fixeNom (const char * Chaine) { nom = new char [strlen(Chaine)+1];
if (nom == NULL) {cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Chaine);
}
friend bool operator < (const Produit , const Produit) ;
public: // Constructeurs et conversion d'un float en Produit Produit (const char * Nom, float Valeur) {fixeNom (Nom); prix = Valeur; } Produit (float Valeur) {fixeNom ("PRODUIT TEMOIN"); prix = Valeur; }
~Produit() {delete nom;}
};
bool operator < (const Produit P, const Produit Q) {return (P.prix < Q.prix);}
int main() {Produit P1("SAVON PROMO",2.05), P2("SAVON MARSEILLE", 1.63);
if (P2 < P1) cout << "PROMO PAS BON MARCHE" << endl ; if (P2 < 5.5) cout << "P2 PAS CHER" << endl ; if (1.5 < P2) cout << "PROMO TROP CHERE" << endl ; return (1);
}
// Résultat PROMO PAS BON MARCHE P2 PAS CHER PROMO TROP CHERE
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
219
7. OPERATEUR []
L'opérateur surdéfini [] retourne l'index d'une instance d'un tableau qu’un retour par référence permet de modifier.
� Exemple
Soit une classe de lignes de 80 caractères que le constructeur initialise par un unique caractère donné.
L'opérateur [] surdéfini permet d'accéder à un caractère d'une ligne. Quand il retourne une référence, il permet sa modification.
Version 1 #define TAILLE 80 #include <iostream.h> #include <stdlib.h> class Ligne // Une instance est une ligne de TAILLE caractères { private :
char t[TAILLE+1]; public: // Appel du constructeur Ligne(char C = ' '); // Ligne vide par défaut char & operator [](int Rang) // Position d'un caractère de la ligne {if (Rang < 1 || Rang > TAILLE)
{ cerr << "rang inacceptable pour une position de Ligne" << endl ; exit(1);} return t[Rang-1]; }
};
Ligne::Ligne(char C) {for (int k=0; k<TAILLE; k++) t[k]=C; t[TAILLE]='\0'; }
int main() {Ligne L, La('A');
cout << "La[3] = " << La[3] << endl ; // Modification du 3ième caractère l'opérateur surdéfini retournant une référence. La[3] = 'Z'; cout << "La[3] = " << La[3] << endl ; La[2] = La[3]; // L'affectation non surdéfinie fonctionne. cout << "La[2] = " << La[2] << endl ; return 1;
}
// Résultat La[3] = A La[3] = Z La[2] = Z
220 CHAPITRE IX ───────────────────────────────────────────────────
Version 2 #define TAILLE 80 #include <iostream.h> #include <stdlib.h> class Ligne // Une instance est une ligne de TAILLE caractères { private : char t[TAILLE+1]; public: Ligne(); // Constructeur par défaut
Ligne(char); // Constructeur général // Position d'un caractère de la ligne rang de 1 à TAILLE char & operator [](int Indice) { cout << "Opérateur surdéfini " << endl;
if (Indice < 1 || Indice > TAILLE) { cerr <<"rang inacceptable pour une position de Ligne"<< endl; exit(1);} return t[Indice-1];
} };
Ligne::Ligne() { cout << "Constructeur par défaut : " << endl; for(int k=0; k < TAILLE; k++) t[k]=' ';t[TAILLE]='\0'; }
Ligne::Ligne(char C) { cout << "Constructeur " << endl;
for (int k=0; k<TAILLE; k++) t[k]=C; t[TAILLE]='\0';
}
int main() { Ligne L, La('A');
cout << "La[3] = " << La[3] << endl ; La[3] = 'Z'; // Modification possible cout << "La[3] = " << La[3] << endl ; // l'opérateur surdéfini retournant une référence. La[2] = La[3]; cout << "La[2] = " << La[2] << endl ; char toto = La[3]; cout << "toto = " << toto << endl; char chaine[TAILLE]; // Opérateur [] non surdéfini chaine[5] = 'x'; cout << "chaine[5]=" << chaine[5] << endl; return 1;
}
// Résultat Constructeur par défaut : Constructeur Opérateur surdéfini La[3] = A Opérateur surdéfini Opérateur surdéfini La[3] = Z Opérateur surdéfini Opérateur surdéfini Opérateur surdéfini La[2] = Z Opérateur surdéfini toto = Z chaine[5]=x
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
221
8. CONSTRUCTEUR COPIE
Soient A et B deux instances d'une classe. L'exécution de l'instruction
A=B;
nécessite de surdéfinir l'affectation de A par B donc la copie membre à membre de l'instance B dans l'instance A, donc la copie membre à membre de chacun des attributs de l'instance B dans ceux de A.
■ Définition
Le constructeur copie est utilisé implicitement ou explicitement à la création de l'instance copie.
Son utilisation implicite peut provoquer des effets de bord.
� Exemple
#include <iostream.h> #include <string.h> #include <stdlib.h> class Produit { char * nom; float prix;
public: // Constructeur Produit (const char * Nom, float Valeur) {nom = new char [strlen(Nom)+1];
if (nom == NULL) {cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Nom); prix = Valeur;
}
~Produit() {delete nom;} // Destructeur
// Un problème causé par le constructeur copie implicite dans l'exécution // de l'opérateur surdéfini de comparaison (transmission par valeur). Deux solutions : // opérateur de comparaison (transmission par référence) ou constructeur copie explicite bool operator < (const Produit &P) const { return (prix < P.prix);}
void AfficheToi(char * Titre) const { cout << Titre << ": " << nom << ", " << prix <<endl ;}
}; // Fin de la classe Produit
int main() { Produit P1("SAVON PROMO",7.5), P2("SAVON MARSEILLE", 9.3);
P2.AfficheToi("P2"); if (P1 < P2) cout << "PROMO BON MARCHE" << endl ; Produit P3("YAOURT",7.6); P3.AfficheToi("P3"); P2.AfficheToi("P2");
}
// Résultat exact obtenu P2: SAVON MARSEILLE, 9.3 PROMO BON MARCHE P3: YAOURT, 7.6 P2: SAVON MARSEILLE, 9.3
// Résultat obtenu sans passage par référence dans l'opérateur de comparaison surdéfini … P2: YAOURT, 9.3
222 CHAPITRE IX ───────────────────────────────────────────────────
■ Interprétation
Le deuxième résultat d'exécution est faux pour les raisons suivantes :
• Lors de l'appel de l'opérateur surdéfini, la transmission par valeur provoque l'allocation d'une instance temporaire, locale à la fonction, copie de l'instance P2 où le constructeur copie par défaut recopie les pointeurs des attributs prix et nom. Le pointeur local est détruit à la sortie de la fonction appelée.
• Dans la fonction appelante, le pointeur accédant à la dernière donnée membre de P2 est réinitialisé à la fin de l'exécution de la méthode surdéfinie. C'est un bug d'exécution connu du langage.
■ Constructeur copie par défaut, constructeur copie explicite
Le constructeur copie par défaut ne copiant membre à membre que les pointeurs vers les attributs, il est préférable d'éviter son utilisation en le déclarant d'accès privé et de le réécrire en utilisant une transmission par référence.
Synopsis Identificateur_de_la_classe( Identificateur_de_la_classe & argument_formel)
Dans le programme suivant, l'opérateur surdéfini s'exécute sans problème avec des arguments transmis par valeur ou par référence.
� Exemple
#include <iostream.h> #include <string.h> #include <stdlib.h> class Produit { char * nom; float prix;
friend bool operator < (const Produit &, const Produit&);
public: // Constructeur Produit (const char * Nom, float Valeur) {nom = new char [strlen(Nom)+1];
if (nom == (char *) NULL) { cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Nom); prix = Valeur;
}
// Constructeur copie Produit (const Produit & Source) { cout << "constructeur copie" << endl; nom = new char [strlen(Source.nom)+1];
if (nom == (char *) NULL) { cerr << "allocation impossible" << endl ; exit (1);} strcpy (nom, Source.nom); prix = Source.prix;
}
~Produit() {delete nom;}
void AfficheToi(char * Titre) const { cout << Titre << ": " << nom << ", " << prix <<endl ;}
}; // Fin de la définition de la classe Produit
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
223
// Surdéfinition de l'opérateur < bool operator < (const Produit &P, const Produit & Q) { cout << "appel de l'opérateur surdéfini" << endl; return (P.prix < Q.prix);}
int main() {Produit P1("SAVON PROMO",0.75);
P1.AfficheToi("P1"); Produit Double1(P1); // L'instanciation provoque l’appel du constructeur copie Produit P2("SAVON MARSEILLE", 0.93); Produit Double2 = P2; // L'instanciation provoque l’appel du constructeur copie P2.AfficheToi("P2"); if (P1 < P2) cout << "PROMO BON MARCHE" << endl ;
Produit P3("YAOURT",0.76); P3.AfficheToi("P3"); P2.AfficheToi("P2"); Double2.AfficheToi("Double2");
}
// Résultat P1: SAVON PROMO, 0.75 constructeur copie constructeur copie P2: SAVON MARSEILLE, 0.93 appel de l'opérateur surdéfini PROMO BON MARCHE P3: YAOURT, 0.76 P2: SAVON MARSEILLE, 0.93 Double2: SAVON MARSEILLE, 0.93
9. AFFECTATION
L'opérateur d'affectation non surdéfini explicitement entre deux instances d'une même classe effectue l'opération membre à membre ce qui conduit à des problèmes d'allocation mémoire similaires à ceux rencontrés avec le constructeur copie implicite comme l'illustre le programme suivant :
// Opérateur d'affectation surdéfini utilisant le constructeur copie implicite #include <iostream.h> #include <string.h> #include <stdlib.h> class Produit { char * nom; float prix;
public: Produit (const char * Nom, float Valeur = 1) // Constructeur {nom = new char [strlen(Nom)+1];
if (nom == (char *) NULL) { cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Nom); prix = Valeur;
}
Produit (const Produit & Source) // Constructeur copie { cout <<"constructeur copie"<<endl;
nom = new char [strlen(Source.nom)+1]; if (nom == (char *) NULL) { cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Source.nom); prix = Source.prix;
}
224 CHAPITRE IX ───────────────────────────────────────────────────
~Produit() {delete nom;}
void AfficheToi(char * Titre) const {cout << Titre << ": " << nom <<", " << prix<<endl; } }; // Fin de la définition de la classe Produit
int main() {Produit P1("YAOURT",0.83), P2("SAVON PROMO",0.75);
Produit P3("CUVETTE",0.23) , P4("DISQUE COMPACT",12); P1.AfficheToi("produit P1"); P2.AfficheToi("produit P2"); P1 = P2; P1.AfficheToi("produit P1 après exécution de P1=P2"); P1 = P3 = P4; P1.AfficheToi("produit P1 après exécution de P1=P3=P4"); P3.AfficheToi("produit P3 après exécution de P1=P3=P4"); Produit P5 = "CAHIER"; P5.AfficheToi("P5"); // Constructeur Produit("CAHIER",1) P5 = "SUCRE 1 KG"; // Constructeur copie par défaut P5.AfficheToi("P5"); // Affectation non surdéfini P6=P5; // Appel du constructeur copie P6.AfficheToi("P6");
} // Fin de la fonction main
// Résultat produit P1: YAOURT, 0.83 produit P2: SAVON PROMO, 0.75 produit P1 après exécution de P1=P2: SAVON PROMO, 0.75 produit P1 après exécution de P1=P3=P4: DISQUE COMPACT, 12 produit P3 après exécution de P1=P3=P4: DISQUE COMPACT, 12 P5: CAHIER, 1 P5: SUCRE 1 KG, 1 constructeur copie P6: SUCRE 1 KG, 1 Segmentation Fault (core dumped) // Selon l'implémentation
■ Surdéfinition de l'opérateur d'affectation
La méthode opérateur d'affectation surdéfinie doit retourner une référence car c'est une Lvaleur. Elle s'écrit dans l'exemple précédent :
Produit & operator = (const Produit & Source) // Surdéfinition de l'opérateur = { // L'allocation mémoire est réinitialisée pour gérer l'accroissement de la chaîne
delete nom; nom = new char [strlen(Source.nom)+1];
if (nom == (char *) NULL) { cerr << "allocation impossible" << endl ; exit (1); } strcpy (nom, Source.nom); prix = Source.prix; return * this; // Pointeur sur l'instance de l'appel.
}
■ Forme canonique d'une classe
La classe idéale est dotée de constructeur et destructeur d'allocation dynamique de mémoire, d'un constructeur copie et d'un opérateur d'affectation surdéfinis.
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
225
10. INCREMENTATION ET DECREMENTATION
Ces deux opérateurs ont une sémantique différente selon leur utilisation en notation préfixée ou postfixée.
• En notation préfixée,
◊ l'incrémentation de l'objet est effectuée avant l'affectation,
◊ la fonction surdéfinie n'a pas d'argument et retourne une référence sur l'objet.
• En notation postfixée,
◊ l'incrémentation de l'objet est effectuée après l'affectation,
◊ la fonction surdéfinie utilise un argument de type int contenant sa valeur (initiale) d'appel et retourne la valeur (entière) de l'objet non incrémenté, sauvegardée dans une variable locale donc temporaire.
� Exemple
#include <iostream.h> class Entier { public: int i; Entier(int j=0) { i=j;} // Constructeur
Entier operator++(int i) // Surdéfinition de l'opérateur postfixé {Entier tmp(i); ++i; cout << "++ postfixé\t"; return tmp; // Retour de la valeur initiale non incrémentée }
Entier &operator++(void) // Surdéfinition de l'opérateur préfixé { ++i;
"++ préfixé\t" ; return *this;
} }; // Fin de la classe Entier
int main(void) {Entier a(1), c;
cout << "\ta.i = " << a.i << "\tc.i = " << c.i << endl; c=a++; // c = a; a++; cout << "a.i = " << a.i << " c.i = " << c.i << endl; c=++a; // a++ ; c = a; cout << "a.i = " << a.i << " c.i = " << c.i << endl;
}
// Résultat a.i = 1 c.i = 0 ++ postfixé a.i = 2 c.i = 1 ++ préfixé a.i = 3 c.i = 3
226 CHAPITRE IX ───────────────────────────────────────────────────
11. GESTION DYNAMIQUE DE LA MEMOIRE
■ Allocation
Les opérateurs d'allocation dynamique de mémoire new et new[] peuvent être surdéfinis et opérer sur un nombre variable d'arguments dont le premier définit l'adresse initiale de la zone allouée.
■ Restitution
Les opérateurs surdéfinis conjugués delete et delete[] peuvent opérer sur un ou deux arguments, le premier étant un pointeur générique (void *) sur l'objet à détruire, le deuxième quand il existe, de type size_t, définissant la taille de la mémoire à restituer.
■ Cas des tableaux
La taille d'un tableau doit être stockée avec ce dernier, souvent dans son en-tête ce qui rend impossible toute hypothèse sur la structure de la mémoire allouée. L'utilisation de l'opérateur delete[] avec comme unique argument l'adresse du tableau nécessite la mémorisation de sa taille dans le programme. Dans ce cas, le compilateur transmet à l'opérateur new[] la taille d'un objet de base multipliée par leur nombre.
� Exemple : détermination de la taille de l'en-tête des tableaux
#include <iostream.h> #define TAILLE 512 // Surdéfinition des operateurs d'allocation dynamique new[] et delete[] int tampon[TAILLE]; // Tampon de stockage du tableau. class Temp { char i[17]; // Indice nombre premier. public: static void * operator new[](size_t taille) // Opérateur new[] surdéfini {cout << "operateur new[] surdéfini" << endl; return tampon;}
static void operator delete[](void *p, size_t taille) // Opérateur delete surdéfini { cout << "Taille de l'en-tete : " << taille-(taille/sizeof(Temp))*sizeof(Temp)<< endl; return ; } };
int main(void) { delete[] new Temp[10]; return 0; }
// résultat operateur new[] surdéfini Taille de l'en-tete : 4
■ Remarque
L'instance courante ne peut être transmise aux opérateurs de gestion dynamique de la mémoire n'étant pas encore créée ou est déjà détruite lorsqu'ils s'exécutent.
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
227
12. CLASSE ENCAPSULEE OU IMBRIQUEE
■ Définition
Un objet d'une classe C peut référencer un objet d'une autre classe interne (classe encapsulée) ou externe (classe imbriquée).
■ Objets d'une classe externe
• Il est préférable de déclarer la classe imbriquée à l'extérieur et de définir les accès (privés, publics, amicaux) de ses données membres.
• Une donnée membre d'une classe imbriquée est par défaut hors de la portée de la classe englobante le contrôle d'accès s'y appliquant normalement.
• La liste des arguments imbriqués d'une méthode externe est introduite par le délimiteur : . L'ordre dans la liste est sans incidence.
• Les arguments des constructeurs des instances des classes membres sont spécifiés dans la définition du constructeur de la classe externe, pas dans les déclarations.
• Le constructeur d'une instance de la classe externe est appelé après ceux des instances membres, dans l'ordre de définition dans la classe. L'ordre inverse est appliqué à la destruction.
• Il peut être parfois nécessaire de définir un constructeur pour la classe externe dont le rôle est seulement d'assurer la transmission des arguments aux classes membres.
� Exemple
Dans la classe Interne, on définit un constructeur appelant une méthode d'affichage de l'attribut interne.
Dans la classe Externe, on définit une donnée membre locale et deux données imbriquées définies dans la classe Interne.
Le constructeur de la classe Externe appele celui de la classe Interne et une méthode d'affichage de l'attribut interne.
Classe C private
public
Imbriquée2
Imbriquée1
228 CHAPITRE IX ───────────────────────────────────────────────────
Externe afficher() int Externe_a Externe(int) Interne Interne_a, Interne_b
privatepublic
Interne afficher() int x Interne(int)
#include <iostream.h> class Interne { int x; public: Interne(int x) {Interne::x=x;afficher();} // Classe Interne : constructeur
void afficher() {cout << "classe Interne = " << x << endl ;} };
class Externe { int Externe_a; // Donnée membre définie dans la classe Interne Interne_b, Interne_c; // Données membres imbriquées public: // Prototypes Externe(int); // Constructeur de la classe Externe void afficher(); };
// Le constructeur de la classe Externe appelle celui de la classe Interne. // Interne_b, Interne_c sont initialisés par le constructeur Interne()
Externe::Externe(int v) : Interne_b(102), Interne_c(v/2)
// Initialisation de la donnée membre Externe_a {Externe_a=v; afficher();}
void Externe::afficher() { cout << "classe Externe = " << Externe_a <<endl ; }
int main() { Externe e(17);
Interne f(-10); // Transtypage fonctionnel Externe g(28);
}
// Résultat classe Interne = 102 classe Interne = 8 classe Externe = 17 classe Interne = -10 classe Interne = 102 classe Interne = 14 classe Externe = 28
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
229
13. DEREFERENCIATION, REFERENCE, SELECTION DE MEMBRE
L'opérateur de déréférenciation * permet de définir des classes d'objets sur lesquels des pointeurs peuvent opérer.
L'opérateur de référence & retourne une référence à l'objet sur lequel il opère.
L'opérateur de sélection de membre -> permet d'accéder à une classe encapsulée dans une autre. Il retourne un objet d'un type sur lequel il doit pouvoir à nouveau être appliqué (pointeur sur une variable structurée, union ou classe, etc.).
� Exemple
#include <iostream.h> class Encapsulee {public : int i;} objet; class Surcharge_operateurs // Classe Surcharge_operateurs des opérateurs surdéfinis { public : Encapsulee * operator -> (void) const { return &objet;} Encapsulee * operator & (void) const { return &objet;} Encapsulee & operator * (void) const { return objet;} };
void f(int i) {Surcharge_operateurs entree; entree->i=20; // Enregistre 20 dans objet.i cout << "i = " << i <<"\n" << " entree->i=" <<entree->i << endl; (*entree).i = 30; // Enregistre 30 dans objet.i. cout << " (*entree).i=" <<(*entree).i << endl; Encapsulee *p = &entree; p->i = 40; // Enregistre 40 dans objet.i. cout << " p->i=" <<p->i << endl; return ; }
int main() {void f(int); int a=1, b=200; f(a); f(b); } // Résultat i = 1 entree->i=20 (*entree).i=30 p->i=40 i = 200 entree->i=20 (*entree).i=30 p->i=40
230 CHAPITRE IX ───────────────────────────────────────────────────
14. OPERATEUR FONCTIONNEL
L'opérateur fonctionnel () peut être surdéfini ce qui est pratique vu de sa n-arité. Ainsi, il permet d'écrire matrice(i,j,k) ou matrice(i,j,k,l,m).
� Exemple
// surdéfinition de l’opérateur fonctionnel #include <iostream.h> #include <stdlib.h> class Complexe { float reel, imaginaire; public : Complexe(float x=0, float y=0) {reel = x; imaginaire = y;}
operator float (void) // Retourne la partir réelle d'un nombre complexe. {cout << "appel de ()" << endl; return reel;} };
int main() {Complexe z(2,3); cout << (z) << endl; }
// Résultat appel de () 2
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
231
15. EXERCICES
� Exercice 1
Analyser le programme suivant :
#include <iostream.h> #define TAILLE 10 class Complexes { float reel , imaginaire;
public: Complexes(double x=0, double y=0){reel = x; imaginaire =y;} void affiche(char *), affiche(int), saisie(char *), init( int);
};
void Complexes::affiche(char * chaine) { cout << chaine << " : partie reelle:" << reel << " partie imaginaire:" << imaginaire << endl; }
void Complexes::affiche(int k) { cout << "Z[" << k << "]" << "\t"; cout << "partie reelle :" << reel << " partie imaginaire : " << this->imaginaire << endl; }
void Complexes::saisie(char * chaine) { cout << chaine; cout << " : saisir la partie reelle :" ; cin >> this->reel; cout << endl << "saisir la partie imaginaire : "; cin >> imaginaire; }
void Complexes::init(int i) {reel=(float)i; imaginaire = -(float)i;}
typedef Complexes Complexe ;
int main() // Initialisation par appel du constructeur et transtypage fonctionnel { Complexe z1(1.5, -10.78), z2= Complexe(-1,3), z3 = Complexe(), z4 = Complexe(-5);
Complexe z5(0,-5), z6, Z[TAILLE]; z1.affiche("z1"); z2.affiche("z2"); z3.affiche("z3"); z4.affiche("z4"); z5.affiche("z5"); z6.saisie("z6"); z6.affiche("z6"); for (int i = 0; i < TAILLE ; i++) {Z[i].affiche(i); Z[i].ini t(i); Z[i].affiche(i);}
}
// Résultat z1 : partie reelle :1.5 partie imaginaire : -10.78 z2 : partie reelle :-1 partie imaginaire : 3 z3 : partie reelle :0 partie imaginaire : 0 z4 : partie reelle :-5 partie imaginaire : 0 z5 : partie reelle :0 partie imaginaire : -5 z6 saisir la partie reelle : 5 saisir la partie imaginaire : 6 z6 : partie reelle :5 partie imaginaire : 6 Nombre complexe Z[0] partie reelle :0 partie imaginaire : 0 Nombre complexe Z[0] partie reelle :0 partie imaginaire : -0 ... Nombre complexe Z[9] partie reelle :0 partie imaginaire : 0 Nombre complexe Z[9] partie reelle :9 partie imaginaire : -9
232 CHAPITRE IX ───────────────────────────────────────────────────
� Exercice 2 : classes imbriquées
Analyser le programme suivant :
#include <iostream.h> class C { public :
class IMBRIQUEE1 { public : int a; IMBRIQUEE1(int init=0){a=init;} void imprime(char * chaine) {cout << chaine << " Imbriquee 1 : a = " << a << endl;} } z1;
class IMBRIQUEE2 { public : int b; IMBRIQUEE1 f(IMBRIQUEE2); IMBRIQUEE2(int init=3) {b=init;} } z2;
IMBRIQUEE1 C::f(IMBRIQUEE2 z) {IMBRIQUEE1 A;
A.a=z.b; A.imprime(" f : " ); return (A);
}
void imprime(char * chaine, IMBRIQUEE1 z1, IMBRIQUEE2 z2) {z1=f(z2); cout << chaine << endl << "z1.a : " << z1.a << " z2.b : " << z2.b << endl;} };
int main() {C::IMBRIQUEE1 A,D,C::f(IMBRIQUEE2); C x ; C::IMBRIQUEE2 B; x.imprime("x", A, B); A.imprime("A :"); (D=A).imprime("D"); return 1; }
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
233
� Exercice 3 : variante de l’exercice 2
Analyser le programme suivant :
#include <iostream.h> class IMBRIQUEE1 { public : int a; IMBRIQUEE1(int init=0){a=init;} void imprime(char * chaine) {cout << chaine << " Imbriquee 1 : a = " << a << endl;} };
class IMBRIQUEE2 { public : int b; IMBRIQUEE2 f(IMBRIQUEE2); IMBRIQUEE2(int init=3) {b=init;} };
class C { public :
IMBRIQUEE1 z1; IMBRIQUEE2 z2; IMBRIQUEE1 f(IMBRIQUEE2); void imprime(char * chaine, IMBRIQUEE1 z1, IMBRIQUEE2 z2) {z1=f(z2); cout << chaine << endl << "z1.a : " << z1.a << " z2.b : " << z2.b << endl;}
};
IMBRIQUEE1 C::f(IMBRIQUEE2 z) { IMBRIQUEE1 A; A.a=z.b; A.imprime(" f : " ); return (A); }
int main() { IMBRIQUEE1 A, D, C::f(IMBRIQUEE2);
C x; IMBRIQUEE2 B; x.imprime("x", A, B); A.imprime("A :"); (D=A).imprime("D"); return 1;
}
// Résultat f : Imbriquee 1 : a = 3 x z1.a : 3 z2.b : 3 A : Imbriquee 1 : a = 0 D Imbriquee 1 : a = 0
234 CHAPITRE IX ───────────────────────────────────────────────────
� Exercice 4 : opérations matricielles
On considère les matrices de n lignes et m colonnes. Créer une classe de matrices avec constructeur, destructeur, opérateurs mathématiques usuels d’addition, de soustraction, de multiplication, d’affectation de matrices et de l’opérateur fonctionnel.
#include <iostream.h> class matrice { typedef double * ligne; ligne *lignes;
unsigned short int n, m; // Nombre de lignes et de colonnes.
public: // Constructeurs et destructeurs matrice(unsigned short int, unsigned short int); matrice(const matrice &); ~matrice(void);
// Opérateurs surdéfinis matrice & operator =(const matrice &); double & operator () (unsigned short int , unsigned short int); matrice operator +(const matrice &) const; matrice operator -(const matrice &) const; matrice operator *(const matrice &) const;
};
// Constructeur matrice::matrice(unsigned short int nl, unsigned short int nc) { n = nl; m = nc; lignes = new ligne[n];
for (unsigned short int i=0; i<n; i++)lignes[i] = new double [m]; }
// Constructeur copie matrice::matrice(const matrice &source) { m = source.m; n = source.n; lignes = new ligne[n];
for (unsigned short int i=0; i<n; i++) { lignes[i] = new double [m];
for (unsigned short int j=0; j<m; j++)lignes[i][j] = source.lignes[i][j]; }
}
// Destructeur matrice::~matrice(void) { unsigned short int i; for (i=0; i<n; i++) delete[] lignes[i]; delete[] lignes; }
// Surdéfinition de l’opérateur d’affectation matrice &matrice::operator =(const matrice &source) { if (source.n!=n || source.m!=m) // Vérification des dimensions.
{ unsigned short int i;for (i=0; i<n; i++) delete[] lignes[i]; delete[] lignes; // Détruit m = source.m; n = source.n; lignes = new ligne[n]; // Et réalloue. for (i=0; i<n; i++) lignes[i] = new double [m];
}
for (unsigned short int i=0; i<n; i++) // Copie for (unsigned short int j=0; j<m; j++) lignes[i][j] = source.lignes[i][j];
}
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
235
// Surdéfinition de l'opérateur () double &matrice::operator () (unsigned short int i , unsigned short int j) { return lignes[i][j]; }
// Addition de matrices matrice matrice::operator +(const matrice &m1) const { matrice tmp(n,m);
for (unsigned short int i=0; i<n; i++) for (unsigned short int j=0; j<m; j++) tmp.lignes[i][j] = lignes[i][j]+m1.lignes[i][j]; return tmp;
}
// Soustraction de matrices matrice matrice::operator -(const matrice &m1) const { matrice tmp(n,m);
for (unsigned short int i=0; i<n; i++) for (unsigned short int j=0; j<m; j++) tmp.lignes[i][j]=lignes[i][j]-m1.lignes[i][j];
return tmp; }
// Multiplication de matrices matrice matrice::operator *(const matrice &m1) const { matrice tmp(n,m1.m);
for (unsigned short int i=0; i<n; i++) for (unsigned short int j=0; j<m1.m; j++) { tmp.lignes[i][j]=0.; // Produit scalaire.
for (unsigned short int k=0; k<m; k++) tmp.lignes[i][j] += lignes[i][k]*m1.lignes[k][j]; }
return tmp; }
void imprimer(matrice m, const char * chaine, const int ligne, const int colonne) { cout << "matrice " << chaine << endl;
for(int i = 0; i < ligne; i++) { for(int j =0; j < colonne ; j++) cout << m(i,j) << " " ; cout << endl; }
}
int main() { int ligne, colonne;
cout << "lignes : " ; cin >> ligne ; cout << endl; cout << "colonnes : "; cin >> colonne ; cout << endl;
matrice m(ligne,colonne), n(ligne,colonne), p(ligne,colonne); for(int i=0;i<ligne;i++) for(int j=0;j<colonne;j++) m(i,j)=n(i,j)=i+j;
imprimer(m,"m , n",ligne,colonne); imprimer(m-n,"m-n",ligne,colonne); imprimer (m+n,"m+n",ligne,colonne); imprimer(m*n,"m*n",ligne,colonne);
}
236 CHAPITRE IX ───────────────────────────────────────────────────
� Exercice 5 : classes imbriquées et surdéfinition
On considère des vecteurs et des matrices.
1°) Définir une classe Matrice et une classe Vecteur dotées des constructeurs adéquats. Le constructeur de la classe Matrice appelle celui de la classe Vecteur.
2°) Ecrire un opérateur surdéfini de multiplication qui permette :
• de multiplier un vecteur par un scalaire
• de multiplier deux vecteurs entre eux.
• de multiplier une matrice par un scalaire.
• de multiplier une matrice par un vecteur ou un vecteur par une matrice.
• de multiplier deux matrice s entre elles.
// Corrigé partiel #include <iostream.h> class Vecteur { public: float *x; unsigned int Taille; Vecteur(){}
Vecteur(unsigned int a) { if(a!=0) { x= new float[a]; for(unsigned int i=0; i<a;i++) *(x+i)=0;
Taille = a; } else cout << "Problème de taille (0 non permis)"<< endl; }
Vecteur operator * (float a) { Vecteur aux(Taille); for(unsigned int i=0;i<Taille;i++) aux.x[i]=a*x[i]; return aux; }
Vecteur operator * (Vecteur A) { Vecteur aux(Taille); for(unsigned int i=0;i<Taille;i++) aux.x[i]=x[i]*(A.x[i]); return aux; } };
class Matrice: public Vecteur { public:
Vecteur *M; unsigned int nbLignes, nbCol; Matrice(unsigned int a,unsigned int b) { if ((a!=0) && (b!=0)) {M= new Vecteur[b]; for(unsigned int i=0;i<b;i++) *(M+i)=Vecteur(a);}
else cout << "Problème de taille" << endl; nbLignes = a; nbCol = b;
}
SURDEFINITION DES OPERATEURS ───────────────────────────────────────────────────
237
Matrice operator * (Vecteur A) { Matrice aux(A.Taille,1);
if (A.Taille == nbCol) { unsigned int i,j;
for (i=0;i<nbLignes;i++) { float temp=0;
for (j=0;j<nbCol;j++) temp+=M[j].x[i]*A.x[j]; aux.M[0].x[i]=temp; return aux;
} }
else { cout << "Multiplication impossible : taille incompatible !\n"; return aux; } }
};
#define LIGNES 3 #define COLONNES 5
int main() { int i,j,l;
Vecteur A(COLONNES ) , B(COLONNES ), C(COLONNES );
for(i=0; i<COLONNES ;i++) { A.x[i]=(float)i+1; B.x[i]=(float)10*i-2; } cout << "A" << " B" << endl;
for(i=0;i<COLONNES ;i++) cout << A.x[i] << " " << B.x[i] << endl; cout << endl; cout << "A*B" << endl;
for(i=0;i<COLONNES ;i++) cout << (A*B).x[i] << endl; cout << endl; cout << "10*A" << endl;
for(i=0;i<COLONNES ;i++) cout << (A*10).x[i] << endl; cout << endl;
Matrice M(LIGNES,COLONNES); // L'écriture n'est pas intuitive puisque les lignes viennent APRES les colonnes... M.M[0].x[0] = 2; M.M[1].x[0] = -2; M.M[2].x[0] = 8; M.M[0].x[1] = 1; M.M[1].x[1] = -15; M.M[2].x[1] = 24; M.M[0].x[2] = 10; M.M[1].x[2] = -5; M.M[2].x[2] = 1;
for(i=0; i< COLONNES;i++) C.x[i]=2+(float)i;
cout << "Matrice M: \n"; for(i=0;i<LIGNES;i++) { for(j=0;j<COLONNES;j++) cout << (M.M[j]).x[i] << " "; cout << endl;} cout << endl;
cout << "Vecteur C : " << endl; for(i=0;i< COLONNES; i++) cout << C.x[i] << endl; cout << endl;
cout << "M*C = \n"; for(i=0;i<LIGNES;i++) {cout << ((M*C).M[0]).x[i]; cout << endl; } return 1;
}
238 CHAPITRE IX ───────────────────────────────────────────────────
Exercice 6 : surdéfinition de l’opérateur d'affectation
Surdéfinir l'opérateur d'affectation dans une classe de nombres complexes.
#include <iostream.h> class Complexes { float reel, imaginaire; public : Complexes & operator = (const Complexes &z); Complexes(float x=0, float y=0) {reel=x; imaginaire=y;}
void afficher() {cout << reel << " " << imaginaire << endl;}
};
Complexes & Complexes::operator =(const Complexes & z) { reel=z.reel; imaginaire=z.imaginaire;}
typedef Complexes Complexe ;
int main() {Complexe z1, z2(2,3); z1=z2; z2.afficher();}
// Résultats 2 3
� Exercice 7
Reprendre les classes de nombres entiers, réels, complexes pour y intégrer les méthodes suivantes :
1°) Surdéfinition de l'addition et de la multiplication de 2 nombres de même nature.
2°) Instanciation d'un tableau de nombres. On vérifiera que les constructeurs sont utilisés à l'instanciation.
3°) Surdéfinir l'opérateur de multiplication pour effectuer la multiplication d'un complexe par un nombre entier ou flottant.
4°) Essayer de surdéfinir les opérateurs d'addition et d'affectation pour qu'ils opèrent sur des tableaux d'instances et constater que c'est difficile.
5°) Surdéfinir l'opérateur < pour comparer les modules de deux nombres (même type ou type différent (entier, réel, complexe)).
L'HERITAGE EN LANGAGE C++
1. DEFINITIONS
L'héritage permet de transmettre à une classe dérivée (fille, descendante) certaines propriétés d'une classe de base (mère, antécédente). Mathématiquement, c'est une relation d'inclusion, partielle ou totale entre classes.
Les propriétés des objets dérivés sont définies à partir de leur qualification d'accès de base et de la qualification d'héritage.
L'héritage d'une classe est simple et le graphe de la hiérarchie de classes un arbre. Plusieurs classes mères définissent un héritage multiple dont la représentation est un graphe sans cycle.
■ Syntaxe
Une classe dérivée est définie à partir de ses classes mères et de leur qualification d'héritage selon la syntaxe (identique avec les mots clés struct et union) suivante :
Classe_dérivée : [qualification_d_héritage] classe(s)_de_base {définition de la classe dérivée}
� Exemple 1
class C_mere1 {/* Classe mère 1 */ };[class C_mere2 {/* Classe mère 2 */ };][…]
class C_fille : public | protected | private C_mere1[, public | protected | private C_mere2 …] {/* Définition de la classe fille */};
� Exemple 2
Définir une classe de base avec deux données publiques B_a, B_b, une méthode publique d'addition int B_plus(int, int), une classe dérivée avec un champ D_c entier, une méthode D_plus d'addition du champ D_c aux champs dérivés qui appelle B_plus().
Définir une fonction f externe qui appelle les méthodes B_plus() et D_plus().
Basepublicint B_a, B_b;int B_plus();
Dérivéepublic :int D_c;int D_plus();
CHAPITRE X
240 CHAPITRE X ───────────────────────────────────────────────────
#include <iostream.h> class Base { public:
int B_a, B_b; int B_plus() {return B_a+B_b;}
}; // Fin classe Base
class Derivee : public Base { public:
int D_c; int D_plus() {return D_c+B_plus();}
}; // Fin classe Dérivée
int f() { Derivee d; // Initialisation
d.B_a=d.B_b=d.D_c=2; return d.D_plus()*d.B_plus(); // Accès aux méthodes dérivées et de base
}
int main() { cout << "f=" << f() << endl ;}
// Résultat f()=24
■ Redéfinition de méthode dans une classe dérivée
Une méthode de base peut être redéfinie (overriding) dans la classe dérivée.
� Exemple
#include <iostream.h> class Base { public:
int B_a, B_b; int plus() {return B_a+B_b;}
};
class Derivee : public Base { public:
int D_a; int plus(Base B_z) {return D_a+B_z.plus();}
}; // int plus() {return a+plus();} provoquerait une récursivité infinie. int main() { int plus(Base); // Prototype classe dérivée
int plus(); // Prototype classe de base Base B_x; B_x.B_a=2; B_x.B_b = 3; cout << "B_x.plus =" << B_x.plus() << endl ; Derivee D_y; D_y.D_a =15; cout << "D_y.plus=" << D_y.plus(B_x) << endl ; return 1;
}
// Résultat B_x.plus =5 D_y.plus=20
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
241
2. QUALIFICATIONS D'ACCES AUX OBJETS D'UNE CLASSE
Les accès aux membres d'une classe sont spécifiés par une qualification d'accès.
• Les objets d'accès public, qualifiés public, sont accessibles à tous.
• Les objets d'accès protégé, qualifiés protected, sont inaccessibles aux utilisateurs, accessibles aux développeurs de la classe et des classes dérivées.
• Les objets d'accès privé, qualifiés private, sont accessibles exclusivement au développeur de la classe.
Les différentes définitions peuvent être morcelées et désordonnées.
La qualification d'héritage affine les propriétés des objets dérivés.
3. QUALIFICATION D'HERITAGE
3.1 Qualifications d'héritage des objets dérivés
■ Définition
L'héritage public, private ou protected définit les règles d'accès des objets dérivés.
■ Synopsis
class B {...}; // Classe de base class Dl : public B {...}; // Héritage qualifié public class D2 : private B {...}; // Héritage qualifié privé class D3 : protected B {...}; // Héritage qualifié protégé class D4 : B {...}; // Héritage qualifié private par défaut struct D5 : B {...}; // Héritage qualifié public par défaut
public
private
protected
public(défaut)
private(défaut)
Base
D1
D2 D3D4
D5
3.2 Sémantique d'accès aux objets de base et dérivé s L'appartenance d'un objet à une classe implémente le concept HAS_A.
L'héritage public implémente le concept IS_A : tout objet dérivé est objet de base.
L'héritage privé ou protégé implémente le concept IS_A_KIND_OF : tout objet dérivé est une "sorte d'objet" de base presque pareil mais pas tout à fait identique ".
L'amitié est non transmissible par héritage et doit donc toujours être explicitée.
242 CHAPITRE X ───────────────────────────────────────────────────
■ Héritage qualifié public
• Les objets protégés et publics de la classe de base le restent dans la classe dérivée, y sont accessibles par ses fonctions membres et amies et sont dérivables.
• Les objets privés ne sont pas héritables.
Toute instance dérivée étant constituée des attributs publics de sa classe de base nécessite sa construction donc l'appel d'un constructeur de la classe mère.
Le transtypage implicite de la classe dérivée vers la classe de base est autorisé.
■ Héritage qualifié private
• Les objets protégés et publics de la classe de base sont privés dans la classe dérivée ne sont accessibles que par ses fonctions membres et amies, ne sont plus dérivables.
• Les objets privés ne sont pas héritables.
objetsprotectedpublic
objet private
objets private
Héritage private
Classe dérivée
Classe de base
Le transtypage implicite de la classe dérivée vers la classe de base ou réciproquement est interdit.
■ Héritage qualifié protected
• Les membres dérivés d'objets protégés ou publics sont protégés et dérivables.
• Les objets privés ne sont pas héritables.
objet objet public private protected
objet public protected
Héritage public
Classe de base
Classe dérivée
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
243
objetsprotectedpublic
objetsprotected
objetsprivate
Héritage protected
Classe dérivée
Classe de base
Le transtypage implicite de la classe dérivée vers la classe de base ou réciproquement est interdit.
accès de base qualif. d'héritage public protected private
public public protected private protected protected protected private private interdit interdit interdit
Tableau récapitulatif des qualifications d'accès des membres hérités
� Exemple
#include <iostream.h> class Base { public:
int Base_a; Base(): Base_a(0) {} // Constructeur par défaut Base(int A): Base_a(A) {} // Autre constructeur
};
class Derivee : public Base { public:
int Derivee_b; Derivee() : Derivee_b(0) {} // 2 constructeurs de la classe dérivée Derivee(int i, int j): Base(i), Derivee_b(j) {}
};
int main() { Base j, k(3);
cout << "j.Base_a = " << j.Base_a << " k.Base_a = " << k.Base_a ;
Derivee D, E(2,3); cout << "D.Base_a = " << D.Base_a <<" D.Derivee_b = " << D.Derivee_b ; cout << "E.Base_a = " << E.Base_a << " E.Derivee_b = " << E.Derivee_b << endl ;
}
// Résultat j.Base_a = 0 k.Base_a = 3 D.Base_a = 0 D.Derivee_b = 0 E.Base_a = 2 E.Derivee_b = 3
■ Requalification des qualifications d'accès dans les classes dérivées
Un objet dérivé peut être rendu public même si l'héritage est privé ou protégé par une redéfinition de la méthode de base dans la classe dérivée ou une requalification, selon les droits autorisés, de l'accès aux objets concernés.
244 CHAPITRE X ───────────────────────────────────────────────────
� Exemple
class list { public: void add(int), remove(int), print(); };
class linkedlist : private list // Héritage qualifié privé { protected : list::remove; // La méthode (private) remove requalifiée protected
public: list::add; // La méthode (private) add requalifiée public }; // La méthode print reste qualifiée private
class listpublic :void add(int;void remove(int), print();
Héritage private
linkedlistpublic : void add(int) // Requalifiéeprotected : void remove(int) // Requalifiéeprivate : print() // Défaut
■ Limites à la requalification
La requalification des droits permet d'assouplir les qualifications d'accès de certains membres de la classe dérivée des effets d'une dérivation privée ou protégée. La dispense ne porte que sur l'identificateur associé.
La requalification des droits d'accès ne peut pas être utilisé pour :
• Elargir les droits de la classe de base : privée vers protégée, protégée vers public,
• Restreindre les droits définis dans la classe de base en cas de dérivation public.
• Il est interdit de requalifier les fonctions et opérateurs surdéfinis.
� Exercice 1
La classe ProduitPerissable, dérivée de la classe Produit a les propriétés suivantes :
• Un produit périssable à une durée de vie limitée et son constructeur appelle celui de la classe de base.
• La méthode d'affichage dans la classe dérivée est redéfinie.
• Une méthode spécifique permettant d'afficher le prix et la durée, spécifique à la classe dérivée, y est également définie.
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
245
#include <iostream.h> #include <string.h> #include <stdlib.h> const int NbMaxCarac = 25;
class Produit // Version simplifiée pour l'introduction de l'héritage { protected : char nom[NbMaxCarac+1]; float prix; void fixeNom (const char * Texte);
public: Produit (char * Nom, float Prix) {fixeNom(Nom); prix = Prix;}
void AfficheToi() { cout << "Produit " << nom <<"\tprix: " << prix << endl;}
}; // Fin de la définition de la classe Produit
void Produit::fixeNom (const char * Texte) {strncpy (nom, Texte, NbMaxCarac); nom[NbMaxCarac] = '\0'; }
class ProduitPerissable : private Produit // Dérivation qualifiée private { private : int nombreDeJours; // Durée de conservation
public: ProduitPerissable (char * Nom, int Duree, float Prix) : Produit (Nom, Prix) {nombreDeJours = Duree;}
void AfficheToi() // Redéfinition de la fonction membre AfficheToi { Produit::AfficheToi(); // La fonction membre de la classe de base reste utilisable
cout << "\tvalidité: " << nombreDeJours << " jours" << endl ; }
void PrixEtDuree() // Spécifique à la classe dérivée { cout << prix <<"€, " << nombreDeJours << " jours" << endl ;}
}; // Fin de la définition de la classe ProduitPerissable
int main() { Produit P1("SAVON",7.5);
P1.AfficheToi(); ProduitPerissable P2("YAOURT", 15, 12.5); P2.AfficheToi(); // P2 a un comportement d'affichage propre à sa classe P2.PrixEtDuree();
return 1; } // Fin de main
// Résultat Produit SAVON prix: 7.5 Produit YAOURT prix: 12.5 validité: 15 jours 12.5€, 15 jours
246 CHAPITRE X ───────────────────────────────────────────────────
� Exercice 2 : l'héritage multiple
1°) Définir une classe fille des classes Mère1 et Mère2 dont le constructeur appelle celui de ses classes mères, une classe petite fille dont le constructeur appelle celui de sa mère.
Mère1 Mère2
affiche()
Petite fille
m1 m2affiche() affiche()
Fille
2°) La classe Mere1 est la classe Reels des réels, Mere2 celle des imaginaires purs Imaginaires et la classe Complexe en dérive. Instancier, saisir, afficher un nombre complexe à partir des méthodes redéfinies dans la classe Complexe. Y surdéfinir l'opérateur d'addition pour additionner des complexes, réels ou imaginaires purs.
#include <iostream.h> class mere1 { int m1;
public: mere1(int i =0) {cout << "constructeur mère 1 "<< endl; m1=i;} void affiche() {cout<< "m1=" << m1 ;}
};
class mere2 { int m2;
public: mere2(int i=0) {m2=i;} void affiche() {cout<< "m2=" << m2<< endl;}
};
class fille : mere1, mere2 { public :
fille( int i1=0, int i2=0): mere1(i1),mere2(i2) { cout << "constructeur fille" << endl; } void affiche() { mere1::affiche(); mere2::affiche(); }
};
class petitefille : fille { public: petitefille(int i1=0, int i2=0): fille (i1,i2){cout <<"constructeur petitefille" << endl;}
fille::affiche; };
int main() {fille f(2,3); f.affiche(); petitefille pf(4,5) ; pf.affiche();}
// Résultat constructeur mère 1 constructeur fille m1=2 m2=3 constructeur mère 1 constructeur fille constructeur petitefille m1=4 m2=5
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
247
■ Réponse à la 2ième question
#include <iostream.h> class Reels { public:
float Reel; Reels(float i=0) {cout << "constructeur Reel "<< endl; Reel=i;} void affiche() {cout<< "Reel=" << Reel << endl;}
};
class Imaginaires { public:
float Imaginaire; Imaginaires(float i=0) {Imaginaire=i;} void affiche() {cout<< "Imaginaire=" << Imaginaire<< endl;}
};
class Complexes : public Reels, public Imaginaires { public :
friend Complexes operator +(Complexes, Complexes); Complexes(float re=0,float im=0): Reels(re),Imaginaires(im) { cout << "constructeur Complexes" << endl; } void affiche(char * chaine) {cout << chaine; Reels::affiche(); Imaginaires::affiche(); }
};
Complexes operator +(Complexes z1, Complexes z2) { Complexes aux; aux.Reel=z1.Reel+z2.Reel; aux.Imaginaire=z1.Imaginaire+z2.Imaginaire;
return aux; }
class petitsComplexes : Complexes { public: petitsComplexes(float re,float im): Complexes (re,im) {cout << "constructeur petitsComplexes" << endl;}
Complexes::affiche; };
int main() {Complexes z1(2,3), z2; z1.affiche("\nz1\n");
petitsComplexes pf(4,5) ; pf.affiche("\npf\n"); (z1=z2+z1).affiche("\nz1=z2+z1\n"); Imaginaires i(8); i.affiche();
}
// Résultat constructeur Reel constructeur Complexes
z1 Reel=2 Imaginaire=3 constructeur Reel constructeur Complexes
pf Reel=4 Imaginaire=5 constructeur Reel constructeur Complexes
z1=z2+z1 Reel=2
Imaginaire=3 Imaginaire=8
248 CHAPITRE X ───────────────────────────────────────────────────
4. CONSTRUCTEUR DANS LES CLASSES DERIVEES
■ Ordre d'exécution des constructeurs
• Toute instance d'une classe dérivée ne peut être initialisée sans appel préalable d'un constructeur d'une classe de base. Il faut donc toujours appeler, de préférence explicitement, le constructeur de la classe de base avant celui de la classe dérivée.
• Lors d'appel implicite, le constructeur de la classe mère est d'abord l'explicite, puis le constructeur par défaut explicite, puis le constructeur par défaut implicite.
■ Syntaxe
L'opérateur de résolution de visibilité sépare, de la gauche vers la droite, le constructeur de la classe fille de celui la classe mère.
Classe_fille::Classe_fille: Classe_mere(arguments_constructeur_classe_fille)
� Exemple
#include <iostream.h> class Mere { int m_i;
public: Mere(int); // Constructeur ~Mere(void); // Destructeur };
Mere::Mere(int i) {m_i=i; cout << "Constructeur de la classe mère : m_i = " << m_i<< endl; }
Mere::~Mere(void) { cout << "Destructeur de la classe mère" << endl;}
class Fille : public Mere { public: Fille(void); ~Fille(void); };
Fille::Fille(void) : Mere(2) // Le constructeur de la classe fille appelle celui de la classe mère { cout << "Constructeur de la classe fille" << endl ;}
Fille::~Fille(void) // Le destructeur de la classe fille appelle celui de la classe mère { cout << "Destructeur de la classe fille " << endl;}
int main() {Mere A(1); Fille B; Mere C(5); }
// Résultat Constructeur de la classe mère : m_i = 1 Constructeur de la classe mère : m_i = 2 Constructeur de la classe fille Constructeur de la classe mère : m_i = 5 Destructeur de la classe mère Destructeur de la classe fille Destructeur de la classe mère Destructeur de la classe mère
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
249
5. HERITAGES MULTIPLES
5.1 Classe virtuelle
■ Position du problème
Soit la classe D héritant de deux classes mères B et C, filles d'une classe commune A selon la hiérarchie de classes suivante :
A
D
CB
Selon leur qualification d'héritage respective, les classes B et C héritent des données et méthodes publiques et protégées de A. La classe D hérite de données de B et C, donc des données héritables de A, dupliquées. Une solution (peu pratique et pas toujours efficace) est de spécifier le chemin de la hiérarchie de classes avec l'opérateur de résolution de portée (A::B::x, A::C::x,..., etc.).
■ Définition
Les objets membres d'une classe de base sont partagés par chaque classe dérivée où elle est déclarée virtuelle. Ainsi, les objets membres de la classe A déclarée virtuelle dans les classes B et C, ne sont transmis qu'une fois à la classe D.
■ Corollaire
L'unicité des données membres héritées d'une classe virtuelle par les différents chemins de la hiérarchie de classes est garantie quand l'héritage d'ordre 2 ou plus est multiple.
■ Syntaxe
La classe mère commune est qualifiée virtuelle dans ses classes filles, le spécificateur d'héritage étant précédé du mot clé virtual.
� Exemple
class A // La classe de base { protected : int Donnee; };
class B : virtual public A // Héritage qualifié public de la classe virtuelle A { protected : int Valeur_B; };
class C : virtual public A // A est encore virtuelle { protected : int valeur_C; }; // Nouvel attribut Donnee est acquit par héritage.
class D : public B, public C // L'attribut Donnee hérité de la classe virtuelle garanti unique. {...}; // La classe D
250 CHAPITRE X ───────────────────────────────────────────────────
5.2 Classe virtuelle et constructeurs
■ Construction explicite dans la classe dérivée
Le constructeur d'une classe dérivée d'une classe virtuelle au deuxième ordre ou plus ne pouvant appeler celui de la classe virtuelle ce dernier pouvant être invoqué à partir de différents chemins de la hiérarchie de classes, chaque classe dérivée construit explicitement ses objets hérités de la classe virtuelle.
■ Constructeur par défaut
Pour éviter l'appel du constructeur par défaut, un constructeur d'une classe de base virtuelle doit être appelé explicitement par celui de toute classe dérivée quel que soit son ordre, les données de la classe de base virtuelle y étant alors garanties uniques. Cette règle s'applique même si un constructeur est défini dans une autre classe mère dérivée de la classe de base virtuelle.
Reprenons l'exemple introductif avec l'hypothèse suivante : la classe D utilise les constructeurs des classes B et C qui tous deux appellent celui de la classe virtuelle A. Pour éviter que l'objet hérité de A ne soit construit plusieurs fois, le compilateur ignore les appels implicites aux constructeurs des classes virtuelles dans les classes dérivées, ces derniers devant être explicités à chaque niveau de la hiérarchie des classes.
■ Transtypage
L'utilisation de l'opérateur de transtypage dynamique dynamic_cast est impérative pour convertir un pointeur sur un objet d'une classe de base virtuelle en un pointeur sur un objet d'une de ses classes dérivées.
� Exemple
#include <iostream.h> class mere1 { int m1;
public: mere1(int i=18) {cout << "constructeur mère 1 "; m1=i; cout<< "m1=" << m1 << endl;} void affiche() {cout<< "m1=" << m1 << endl;}
};
class mere2 { int m2;
public: mere2(int i=0) {cout << "constructeur mère 2 "; m2=i;cout<< "m2=" << m2<< endl;} void affiche() {cout<< "m2=" << m2<< endl;}
};
class fille1 : mere1, virtual mere2 { public :
fille1(int i1=0,int i2=0): mere1(i1),mere2(i2) {cout << "constructeur fille1" << endl;} void affiche() {mere1::affiche(); mere2::affiche();}
};
L'HERITAGE EN LANGAGE C++ ───────────────────────────────────────────────────
251
class fille2 : virtual mere1, virtual mere2 { public :
fille2(int i1=0,int i2=0): mere1(i1),mere2(i2) {cout << "constructeur fille2" << endl;} void affiche() {mere1::affiche(); mere2::affiche();}
};
class petitefille : fille1 , fille2 { public:
petitefille(int i1,int i2): fille1(i1,i2),fille2(i1,i2){cout << "constructeur petitefille" << endl;} void affiche() {fille1::affiche(); fille2::affiche();}
}; int main() { fille1 f(1,2), g; f.affiche();g.affiche(); petitefille pf(4,8) ; pf.affiche(); }
// Résultat constructeur mère 2 m2=2 constructeur mère 1 m1=1 constructeur fille1 constructeur mère 2 m2=0 constructeur mère 1 m1=0 constructeur fille1 m1=1 m2=2 m1=0 m2=0 constructeur mère 2 m2=0 constructeur mère 1 m1=18 constructeur mère 1 m1=4 constructeur fille1 constructeur fille2 constructeur petitefille m1=4 m2=0 m1=18 m2=0
� Exercice
Reprendre la classe Complexe pour y intégrer les méthodes suivantes :
1°) Surdéfinition de l'addition et de la multiplication de 2 complexes.
2°) Surdéfinition des opérateurs d'addition et d'affectation pour qu'ils opèrent sur des tableaux d'instances.
3°) Surdéfinition de l'opérateur de multiplication pour multiplier un complexe par un nombre flottant. Idem avec un tableau de complexes.
4°) Définir la classe des nombres imaginaires purs, dérivée de la classe complexe qui utilisera les opérateurs d'addition et de multiplication de la classe complexe. Redéfinir la méthode d'affichage en testant les qualifications d'héritage public, privées.
5°) Idem avec la classe des nombres réels purs. Attention, c'est la classe complexe qui dérive des réels purs. Pas l'inverse. On va donc trouver deux héritages successifs…
METHODES VIRTUELLES, LIAISON DYNAMIQUE, POLYMORPHISME
1. METHODES VIRTUELLES ET LIAISON DYNAMIQUE
Sur le plan sémantique, les méthodes virtuelles sont différentes des classes virtuelles, bien que le mot-clé virtual soit également utilisé pour les définir.
■ Position du problème
Une méthode redéfinie appelée est toujours la plus proche dans la hiérarchie de classes. Son appel si elle est de niveau supérieur nécessite donc d'être explicite en rappelant le nom de sa classe avec l'opérateur de résolution de portée.
Cette règle est imparfaite : soit une classe B dérivée de A dont la méthode [A::]x appelle la méthode y redéfinie dans B. Que se passe-t-il lorsqu'un objet de B invoque [A::]x ? La méthode appelée étant membre de A appellera A::y dont la redéfinition dans B est alors inopérante.
Classede base Améthodes x y
Classe Bméthode y redéfinie
Une possibilité consiste à redéfinir la méthode x dans la classe dérivée B.
■ Liaison dynamique
La meilleure solution est que la méthode x appelle A::y (resp. B::y) quand elle est invoquée par une instance de A (resp. de la classe B), la liaison étant effectuée dynamiquement à l'exécution et non statiquement à la compilation.
CHAPITRE XI
254 CHAPITRE XI ───────────────────────────────────────────────────
■ Syntaxe
La méthode redéfinie dans la classe fille est spécifiée virtual dans la classe mère.
■ Méthode polymorphique
Une méthode virtuelle est invoquée par un objet de sa classe effective (mère ou fille).
Un tel comportement est dit polymorphique.
� Exemple
// Redéfinition d'une méthode de la classe de base #include <iostream.h> class DonneeBase { protected : int Numero; // Les données sont numérotées. int Valeur; // Et sont constituées d'une valeur entière public: virtual void Entre(void); // Méthode de saisie void MiseAJour(void); // Méthode de mise à jour };
void DonneeBase::Entre(void) { cout << "Numéro : "; cin >> Numero; cout << "Numéro : "<< Numero << endl << "Valeur : " ; cin >> Valeur; cout << "Valeur : " << Valeur << endl; return ; }
void DonneeBase::MiseAJour(void) {Entre(); return ;}
class DonneeDetaillee : public DonneeBase { int ValeurEtendue; // Les données détaillées ont en plus une valeur étendue. public: void Entre(void); // Redéfinition de la méthode de saisie };
void DonneeDetaillee::Entre(void) {DonneeBase::Entre(); cout << "Valeur étendue : "; cin >> ValeurEtendue; cout << "Valeur étendue : " << ValeurEtendue << endl; return ; }
int main(void) {DonneeBase A; A.Entre(); A.MiseAJour(); DonneeDetaillee B; B.Entre(); B.MiseAJour(); }
METHODES VIRTUELLES, LIAISON DYNAMIQUE, POLYMORPHIS ME ───────────────────────────────────────────────────
255
// Résultat Numéro : 5 Numéro : 5 Valeur : 8 Valeur : 8 Numéro : 6 Numéro : 6 Valeur : 2 Valeur : 2 Numéro : 5 Numéro : 5 Valeur : 5 Valeur : 5 Valeur étendue : 6 Valeur étendue : 6 Numéro : 6 Numéro : 6 Valeur : 5 Valeur : 5 Valeur étendue : 6 Valeur étendue : 6
■ Remarques
L'appel B.Entre est correct quand B est une instance de la classe DonneeDetaillee.
L'appel B.MiseAJour est faux si Entre n'est pas virtuelle.
2. METHODE VIRTUELLE PURE - CLASSE ABSTRAITE
2.1 Définitions Une méthode virtuelle pure (pure virtual method), également appelée méthode abstraite, est déclarée dans sa classe mère, définie dans une classe dérivée.
Une classe abstraite comporte au moins une méthode abstraite. Sa vocation n'est pas d'instancier des objets mais simplement d'être utilisée par ses classes dérivées.
L'utilisation de méthodes virtuelles pures et de classes abstraites permet de créer des classes de base décrivant des objets dérivées accessibles avec des pointeurs sur les objets de base, la méthode effective étant définie dans la classe dérivée.
Syntaxe Une méthode virtuelle pure (abstraite) a sa déclaration terminée par l'affectation nulle, donc après le mot-clé const pour les méthodes const et après l'éventuelle liste des exceptions autorisées.
� Exemple
virtual type_resultat identificateur_méthode_virtuelle_pure (liste_arguments_typés) =0;
256 CHAPITRE XI ───────────────────────────────────────────────────
2.2 Conteneur et objets polymorphiques
■ Définition
Un conteneur est un objet structuré pouvant en contenir d'autres, de type quelconque.
� Exemple : conteneur d'objets polymorphiques
• Un sac est un conteneur d'objets, non forcément uniques. Plusieurs occurrences d'un même objet peuvent y être placées simultanément.
• Le conteneur n'utilise que des pointeurs sur la classe mère abstraite.
• Deux objets identiques sont différenciés par un identifiant dont le choix est à leur charge. La classe abstraite dont ils dérivent est dotée d'une méthode le retournant. Définir deux fonctions permettant de mettre et de retirer une occurrence d'objet du sac ainsi qu'une fonction y détectant la présence d'une occurrence d'un objet. Les objets sont affichés par la méthode virtuelle pure de la classe abstraite print.
#include <iostream.h> class Object // Classe abstraite { unsigned long int h; // Identificateur de l'objet unsigned long int new_handle(); // Allocateur d'un nouvel identificateur public: Object(); // Constructeur virtual ~Object(); // Destructeur virtuel virtual void print() =0; // Méthode virtuelle pure unsigned long int handle() const; // Identification de l'objet }; unsigned long int Object::new_handle() // Allocateur d'un nouvel d'objet { static unsigned long int hc = 0; hc++; return hc; }
Object::Object() // Le constructeur de la classe Object {h = new_handle();} // Allocation d'un nouvel objet
Object::~Object() {} unsigned long int Object::handle() const { return h;} // Retourne l'identificateur de l'objet.
class Bag : public Object // Un sac pouvant en contenir un autre est implémenté sous la forme d'une liste chaînée { typedef struct baglist {baglist *next; Object *ptr; } BagList; BagList *head; // La tête de liste public: Bag(); // Constructeur ~Bag(); // Destructeur void print(); // Affichage du contenu du sac. bool has(unsigned long int) const; // Booléen vrai si le sac contient l'objet. bool is_empty() const; // Booléen vrai si le sac est vide. void add(Object &); // Ajout d'un objet dans le sac void remove(Object &); // Retrait d'un objet du sac Bag (const Bag &Source){}
METHODES VIRTUELLES, LIAISON DYNAMIQUE, POLYMORPHIS ME ───────────────────────────────────────────────────
257
Bag & operator = (const Bag &source) {BagList *tmp = head;
while (tmp != (BagList *) NULL) tmp = tmp->next; head = tmp; BagList *tmp2 = source.head; while (tmp2 != (BagList *) NULL) {this->add(*(tmp2->ptr)); tmp2 = tmp2->next;} return *this;
} };
Bag::Bag() : Object() {}
Bag::~Bag() // Destruction de la liste des objets {BagList *tmp = head; cout <<" Destructeur Bag" << endl; while (tmp != (BagList *) NULL) tmp = tmp->next; head = tmp; }
void Bag::print() // Affichage de la liste des objets. { BagList *tmp = head;
while (tmp != (BagList *)NULL) {tmp->ptr->print();tmp = tmp->next; } }
bool Bag::has(unsigned long int h) const // Recherche de l'objet. { BagList *tmp = head;
while(tmp != (BagList *)NULL && tmp->ptr->handle() != h) tmp = tmp->next; return (tmp != (BagList *)NULL);
}
bool Bag::is_empty() const { return (head==(BagList *)NULL);}
void Bag::add(Object &o) // Ajout d'un objet à la liste { BagList *tmp = new BagList; tmp->ptr = &o; tmp->next = head; head = tmp; }
void Bag::remove(Object &o) // Suppression d'un objet de la liste { BagList *tmp1 = head, *tmp2 = (BagList *)NULL;
while (tmp1 != (BagList *)NULL && tmp1->ptr->handle() != o.handle()) {tmp2 = tmp1; tmp1 = tmp1->next; } // Recherche de l'objet
if (tmp1!=(BagList *)NULL) // Suppression de la liste. { if (tmp2!=(BagList *)NULL) tmp2->next = tmp1->next; else head = tmp1->next; delete tmp1; }
}
class MonObjet : public Object { public : int entier; void print(); MonObjet(int donnee=0){entier=donnee;} };
class MonObjet2 : public Object { public : float reel; void print(); MonObjet2(float donnee=0){reel=donnee;} };
void MonObjet::print() {cout <<entier << endl;}
void MonObjet2::print() {cout <<reel << endl;}
258 CHAPITRE XI ───────────────────────────────────────────────────
int main() { Bag *MonSac = new Bag , *MonSac2 = new Bag; MonObjet a(1), b(5), c(8), d(10), e(3); cout << "On ajoute a=1 b=5 c=8 b=5 d=10 à sac n° 1" << endl; MonSac->add(a); MonSac->add(b); MonSac->add(c); MonSac->add(b); MonSac->add(d); MonSac->print(); cout << "On enleve b à sac n°1" << endl; MonSac->remove(b); MonSac->print(); cout << "On copie Sac 1 dans Sac 2 (copie inversée)" << endl; *MonSac2 = *MonSac; MonSac2->print(); cout << "On ajoute Sac 2 à Sac 1" << endl; MonSac->add(*MonSac2); MonSac->print(); delete MonSac; delete MonSac2;
Bag *TonSac = new Bag , *TonSac2 = new Bag; MonObjet2 A(1.4), B(5e-2), C(3.147), D(100.), E(3.); cout << "On ajoute A=1.4 B=5e-2 C=3.147 B=5e-2 D=10 à sac n°1" << endl; TonSac->add(A); TonSac->add(B); TonSac->add(C); TonSac->add(B); TonSac->add(D); TonSac->print(); cout << "On enleve B à sac n°1" << endl; TonSac->remove(B); TonSac->print(); cout << "On copie Sac 1 dans Sac 2 (copie inversée)" << endl; *TonSac2 = *TonSac; TonSac2->print(); cout << "On ajoute Sac 2 à Sac 1" << endl; TonSac->add(*TonSac2); TonSac->print(); delete TonSac; delete TonSac2; return 0; }
// Résultat On ajoute a=1 b=5 c=8 b=5 d=10 à sac n°1 10 5 8 5 1 On enleve b à sac n°1 10 8 5 1 On copie Sac 1 dans Sac 2 (copie inversée) 1 5 8 10 On ajoute Sac 2 à Sac 1 1 5 8 10 10 8 5 1 Destructeur Bag Destructeur Bag
METHODES VIRTUELLES, LIAISON DYNAMIQUE, POLYMORPHIS ME ───────────────────────────────────────────────────
259
■ Remarques
• La classe abstraite Object sert de cadre (frame) aux classes dérivées.
• La classe Bag stocke des objets dérivés avec les méthodes add et remove.
• La faculté d'interdire à une méthode virtuelle pure définie dans une classe dérivée d'accéder en écriture aux données de la classe de base et à celles de sa classe peut faire partie de ses prérogatives par la qualification const du pointeur this (pointeur constant sur un objet constant). Ainsi, dans l'exemple ci-dessous, la méthode virtuelle pure print qualifiée const permet l'accès uniquement en lecture à l'objet h. Cette méthode d'encapsulation coopérative détecte les erreurs de compilation et peut s'appliquer aux méthodes virtuelles non pures ou aux fonctions non virtuelles.
� Exemple
class Object { unsigned long int new_handle(void);
protected : // Héritage de la classe Object qualifié protected unsigned long int h; public: Object(void); // Le constructeur de la classe Object
virtual void print(void) const=0; // Méthode virtuelle pure qualifiée constante unsigned long int handle(void); // Méthode retournant le numéro d'identification de l'objet };
3. RECAPITULATION DES REGLES DE DERIVATION
■ Règle 1
Un objet dérivé est utilisable comme un objet d'une de ses classes mères.
■ Règle 2 : affectation d'instances entre classes fille et mère
• L'affectation d'une instance dérivée à une instance de base est autorisée les données des champs non définis dans la classe mère étant perdues.
• L'inverse est interdit les données de la classe fille non définies dans la classe mère ne pouvant être ni initialisées ni affectées.
■ Règle 3 : affectation de pointeur
• Les pointeurs des classes dérivées sont compatibles avec ceux des classes mères. Un pointeur d'une classe dérivée peut être affecté à un pointeur d'une de ses classes de base à condition d'être doté des qualifications d'accès convenables.
• Un objet dérivé, accédé avec un pointeur d'une de ses classes mères, est considéré comme un de ses objets et les données spécifiques à la classe dérivée deviennent momentanément inaccessibles, même si le mécanisme des méthodes virtuelles demeure, le destructeur de la classe de base étant virtuel.
• Un pointeur vers une classe de base peut être converti en celui d'une classe dérivée. L'utilisation d'un transtypage fonctionnel est alors préférable.
260 CHAPITRE XI ───────────────────────────────────────────────────
� Exemple
#include <iostream.h> class Mere { public: Mere(void); ~Mere(void); }; Mere::Mere(void) {cout << "Constructeur de la classe mère" << endl; return ;} Mere::~Mere(void) {cout << "Destructeur de la classe mère" << endl; return ;}
class Fille : public Mere { public: Fille(void); ~Fille(void); }; Fille::Fille(void) : Mere() {cout << "Constructeur de la classe fille" << endl; return ;} Fille::~Fille(void) {cout << "Destructeur de la classe fille" << endl; return ;}
Avec ces définitions, seule la première des deux affectations suivantes est autorisée :
int main() {Mere m; Fille f; m=f; } // F=m ; Erreur (ne compile pas).
Les mêmes règles sont applicables pour les pointeurs.
Mere *pm, m; Fille *pf, f; pf=&f; // Autorisé. pm=pf; // Autorisé. Les objets membres de la classe fille ne sont plus accessibles // Avec ce pointeur : *pm est un objet de la classe mère. // Pf=&m; // Illégal car transtypage nécessaire pf=(Fille *) &m; // Légal, mais dangereux les méthodes de la classe filles n'étant pas définies
■ Règle 4 : non transitivité de l'amitié par dérivation
• Par défaut, les relations d'amitié ne sont pas héritées. Soient une classe A amie d'une classe B et une classe C fille de B. La classe A n'est pas amie de la classe C.
• Cette règle s'applique également aux fonctions et méthodes amies.
■ Règle 5 : méthode virtuelle et constructeur
• Le mécanisme associé aux méthodes virtuelles est désactivé à l'exécution des constructeurs des objets de base pour éviter qu'une méthode virtuelle n'utilise une donnée sur un objet dérivé en cours d'instanciation (non encore initialisé).
• Une méthode virtuelle peut être appelée par un constructeur, la méthode effective étant celle de la classe de l'instance en cours de construction. Soit une classe (petite) fille A dérivée d'une classe B, définissant toutes les deux une méthode f dérivée d'une même méthode virtuelle. Son appel par le constructeur de B utilise B::f, même si l'objet instancié est membre de A.
■ Règle 6 : méthode virtuelle et destructeur
• L'opérateur (statique) delete recherchant le destructeur adéquat dans la classe la plus dérivée, il est fondamental de le déclarer virtuel. Invoqué dans une classe dérivée, il restitue la mémoire utilisée par l'objet dans sa totalité.
• Il est inutile de préciser la classe du destructeur celui-ci étant unique.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE)
En langage C, le préprocesseur permet de générer des objets (constante ou fonction) conformément à un modèle symbolique dont nous verrons que l'absence de contrôle par le compilateur de l'objet généré peut produire des erreurs d'exécution indétectables appelées effets de bord.
La génération d'objets contrôlés à la compilation à partir d'un modèle (template) décrivant toutes ses propriétés (attributs, traitement) élimine toutes ces inconvénients.
1. LE PREPROCESSEUR DU LANGAGE C
■ Directives de compilation
Le préprocesseur, appelé préalablement à la compilation, modifie le code source d'un programme à partir de directives dont les fonctions sont les suivantes : inclusion de fichiers source, substitution symbolique de mots, définition de zone de compilation conditionnelle.
■ L'opérateur #
Toute directive de compilation est préfixée par l'opérateur # pour être différenciée des instructions du langage. Elle ne suit pas la règle du ; (terminateur d'instructions).
Les principales directives sont les suivantes :
include <nom_fichier> ou "nom_fichier", define définition d'une objet symbolique, undef suppression d'une définition d'un objet symbolique, if compilation d'une partie du code si une condition est vraie, else compilation d'une partie du code si la condition précédente est fausse, endif fin du code compilé sous condition, elif compilation d'une partie du code si une condition est vraie, ifdef inclusion de code si un objet symbolique est défini, ifndef inclusion de code si un objet symbolique n'est pas défini.
■ Généralités sur les substitutions symboliques
Une unité syntaxique (token) est un texte indivisible traité par le préprocesseur représentant un objet symbolique (constante symbolique ou fonction symbolique) sous l'une des formes :
#define symbole suite_de_caractères #define symbole(liste_de_caractères) suite de caractères
CHAPITRE XII
262 CHAPITRE XII ───────────────────────────────────────────────────
1.1 Constante symbolique L'utilisation d'une constante symbolique, également appelée constante manifeste, a deux objectifs :
• meilleure lisibilité du code source. Ainsi, le symbole MAXFILES représente clairement le nombre maximum de fichiers alors qu'un simple nombre n'a pas de signification particulière.
• création d'un symbole dont la valeur peut être modifiée dans tout un programme suite à une simple modification de sa ligne de définition.
La portée d'une constante symbolique reste valide jusqu'à la prochaine définition #define du même symbole, où jusqu'à l'invalidation de la définition par la directive #undef symbole, où jusqu'à la fin du fichier source.
Une substitution symbolique représente l'action exécutée par le préprocesseur quand il substitue, dans tout ou partie du programme, un objet symbolique par l'unité syntaxique associée.
Une substitution de la forme
#define OUI 1
est appelée substitution du premier ordre.
� Description
Code source Code modifié après exécution du préprocesseur #define V 1 ... int main(void) int main(void) ... ... if(i == V) if(i == 1) Une substitution symbolique peut en utiliser une autre.
■ Constantes entières
Les constantes symboliques entières, de type short par défaut, peuvent être définies en format décimal (format par défaut), octal si la substitution commence par 0 (zéro), hexadécimal si la substitution commence par 0x ou 0X. Le type est unsigned si la substitution commence par u ou U.
Toute constante int dont la valeur excède les capacités par défaut de la machine devient automatiquement de type long, que l'on peut également définir explicitement. Il suffit de faire suivre la substitution du suffixe l ou L .
� Exemples
#define MAXLINE 10 /* Constante entière */ #define B 041 /* Constante octale de valeur décimale 33 */ #define HEXA 0XFF /* Constante hexadécimale de valeur décimale 255 */ #define HEXA 0xFF /* Constante hexadécimale de valeur décimale 255 */ #define LON 56897L /* Constante entière de type long */ #define LON 56897l /* Constante entière de type long */
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
263
■ Constantes réelles
Les constantes symboliques réelles sont de type double sous une des formes :
#define PI 3.1415 #define P 1E-6 #define P 1e-6 #define PI 0.31416e+2 #define ALPHA 31.45E-3
■ Constantes de type caractère
Une constante de type caractère est entouré par des simples quottes '. Trois substitutions sont possibles : directe, à partir du code ASCII en octal ('\ddd') ou en hexadécimal.
#define CAR 'b' #define CAR '\014' #define CAR '\0x0C'
■ Suppression d'une substitution symbolique
La directive undef permet la suppression d'une précédente substitution. Son utilisation sans argument supprime toutes les substitutions symboliques précédentes.
� Exemple
#define PI 3.14 ... #undef PI
1.2 Fonction symbolique La directive define définit des fonctions symboliques à partir de la syntaxe suivante :
#define symbole(liste_de_caractères) suite de caractères
La parenthèse ouvrante doit être accolée au symbole, le caractère suivant cette parenthèse faisant partie du mot substitué (même le caractère d'espacement).
■ Arguments d'une substitution fonctionnelle
Une substitution fonctionnelle a des arguments dont il n'est pas nécessaire de déclarer le type tant que les données restent cohérentes avec la substitution ce qui évite d'écrire des fonctions différentes selon les types de données et permet de décrire des objets génériques.
■ Effets de bord
L'utilisation systématique de parenthèses pour délimiter les identificateurs permettent d'éliminer certains effets de bord (cf. la fonction square ou le résultat du calcul de l'expression max(a++, b++ ) dans l'exemple ci-après, faux car le plus grand des deux nombres est évalué deux fois.
On constate également que les arguments de la fonction printf sont évalués de la droite vers la gauche et imprimés de la gauche vers la droite.
264 CHAPITRE XII ───────────────────────────────────────────────────
� Exemple
#define square(A) (A * A ) /* Faux */ #define carre(A) ((A) * (A)) /* Exact */ #define max(A,B) ( (A > B) ? A : B) /* Faux */ #define maxi(A,B) ( ((A) > (B)) ? (A) : (B) ) /* Exact */ int main(void) { int i = 2 , j = 3 ;
printf (" i = %d square(i) = %d carre(i) = %d\n",i,square(i), carre(i)); printf (" i+1 = %d square(i+1) = %d carre(i+1) = %d\n", i+1, square(i+1), carre(i+1)); printf (" i = %d j = %d maxi(i,j) = %d\n", i,j ,maxi(i,j)); printf (" i = %d j = %d max(i,j) = %d\n" , i,j, max(i,j)); /* Attention aux effets de bord : le max est évalué deux fois */ printf (" i = %d j = %d maxi(i++,j++) = %d, i = %d j = %d\n",i,j ,maxi(i++,j++),i,j); /* Ordre d'évaluation des opérandes de droite à gauche, impression de gauche à droite */ i = j = 4; printf(" i = %d j = %d max(i+1,j+1) = %d\n", i , j , max(i+1, j+1)); return(1);
}
// Résultat i = 2 square(i) = 4 carre(i) = 4 i+1 = 3 square(i+1) = 5 carre(i+1) = 9 i = 2 j = 3 maxi(i,j) = 3 i = 2 j = 3 max(i,j) = 3 i = 3 j = 5 maxi(i++,j++) = 4, i = 2 j = 3 i = 4 j = 4 max(i+1,j+1) = 5
2. LES MODELES GENERATEURS D'OBJET
En langage C++, les modèles d’objets permettent de résoudre deux problèmes résultant d’une mauvaise utilisation du préprocesseur : effets de bord, non vérification syntaxique du code source généré à partir des substitutions symboliques.
■ Principe de la génération d’instances en C++
Un modèle d'objet permet la génération d'instances de cet objet conformes à ses spécifications selon l’algorithme suivant : création à la compilation, lors de la première utilisation d'un modèle générateur, d’un objet conforme à ce dernier dont le(s) modèle(s) type(s) a(ont) été remplacé(s) par son (leur) type effectif, déterminé(s) implicitement à partir du contexte d'utilisation ou avec des arguments explicites.
Cette phase de la compilation est appelée instanciation des arguments modèles.
■ Les différents modèles
En langage C++, les objets modèles sont appelés objets génériques, objets paramétrés, objets template, ou template et peuvent générer des arguments, des fonctions, des classes, des méthodes avec les caractéristiques suivantes :
• Un modèle d'argument est un type paramétré ou une constante de type intégral.
• Un modèle de classe a des membres paramétrés (attributs et/ou méthodes).
• Les modèles de fonctions et de méthodes opèrent sur des arguments paramétrés.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
265
3. MODELE D'ARGUMENT
Syntaxe template <class|typename nom[=type] [, class|typename nom[=type] […]>
où nom est l'identificateur du type modèle.
■ Equivalence des mots clés class et typename
Dans ce contexte, l'un des mot-clé class ou typename définit un modèle de type.
� Exemple
template <class T, typename U> // Déclaration des modèles de type T, U
■ Interprétation
Les modèles de type T, U peuvent être instanciés à partir d'un quelconque type préalablement déclaré.
■ Modèle de type par défaut
Un modèle de type est défini par défaut quand son identificateur est suivi de l'opérateur d'affectation et d'un nom de type préalablement déclaré.
Sur le plan syntaxique, un type modèle avec une valeur par défaut impose d'en définir une à tous les types modèles qui le suivent dans la déclaration. La ligne suivante provoque donc une erreur de compilation :
template <class T=int, class V> // Int type modèle par défaut
� Exemple
#include <iostream.h> // Les types U, V, W sont paramétrés, W est de type char par défaut template <class U, class V, class W=char > void f(U argument1, V argument2, W argument3) { cout << "argument1 = "<< argument1 << " argument2 = " << argument2; cout << " argument3 =" << argument3 << endl; } int main() { int a=1;
double c=3.45; f(a,c,'c'); f(c,a,12);
}
//résultat argument1 = 1 argument2 =3.45 argument3 = c argument1 = 3.45 argument2 = 1 argument3 = 12
266 CHAPITRE XII ───────────────────────────────────────────────────
4. MODELE DE FONCTION, DE CLASSE, DE METHODE
La définition d'un modèle de fonction ou de classe peut être complétée par la déclaration d'un ou plusieurs modèle d'arguments (type ou constante) utilisés comme des types traditionnels ou des constantes locales à la fonction.
4.1 Modèle de fonction
■ Déclaration
La déclaration et la définition d'un modèle de fonction (fonction génératrice modèle) sont similaires à celle d'une fonction traditionnelle, celles-ci devant toutefois être précédées de la spécification template.
Syntaxe template <liste_types_paramétrés> type_retour fonction(type_paramétré1 argument_fonction[,…]);
Description • liste_types_paramétrés représente la liste des arguments paramétrés dont chacun
représente un type (sauf en cas d'instanciation explicite) ce qui permet l'identification à la compilation des types modèles et des types effectifs.
• argument_fonction représente un argument formel.
• type_retour représente le type de l'objet retourné qui peut être un des types modèles de la liste.
■ Définition
La définition d'un modèle de fonction est similaire à sa déclaration et comporte au choix :
• des modèles d'argument, utilisables comme les arguments traditionnels,
• des modèles de variables locales,
• des modèles de constante utilisables comme des variables locales qualifiées const.
� Exemple
// Définition d'un modèle de fonction template <class T> T min(T x, T y) { return x<y ? x : y;}
■ Accès public à une fonction modèle, opérateur surdéfini
La fonction min ainsi définie, d'accès public, peut être appelée depuis toute classe où l'opérateur < est surdéfini.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
267
� Exemple 1
#include <iostream.h> template <class T> T min(T x, T y) { return x<y ? x : y;}
int main(void) { int x=1, y=2; float a= -6.7, b=5.67; double c=4.56, d = 18.23; cout << min(x,y) << endl; // Instanciation avec des arguments de type int cout << min(a,b) << endl; // Instanciation avec des arguments de type float cout << min(c,d) << endl; // Instanciation avec des arguments de type double }
■ Classe, fonction modèle et amitié
• Une fonction modèle peut être amie de toute classe, éventuellement modèle.
• Toutes les instances d'une fonction modèle amie de la classe le sont.
� Exemple
#include <iostream.h> template <class T> // La fonction modèle min T min(T a, T b) {if (a < b) return a; else return b;}
class vecteur // La classe vecteur { int x,y; public : vecteur (int abs = 0, int ord = 0) // Le constructeur {x = abs; y = ord;} void affiche() {cout << x << " " << y << endl;}
// L'opérateur surdéfini est ami de la classe vecteur friend int operator < (vecteur, vecteur); };
int operator < (vecteur a, vecteur b) // L'opérateur < surdéfini { return a.x*a.x + a.y*a.y < b.x*b.x+ b.y*b.y;}
int main() { vecteur u(3,4), v(4,6), w;
w=min(u,v); cout << "min(u,v)= " ; w.affiche(); int a(1), b(3), c; cout << "min (a,b) = " << min(a,b) << endl; return 0;
}
// Résultat min(u,v)= 3 4 min(a,b)=1
■ Surdéfinition et fonction modèle
Une fonction modèle peut être surdéfinie par une fonction, éventuellement modèle.
L'ambiguïté entre une fonction instanciée à partir d'un modèle et une fonction traditionnelle est résolue par le choix de cette dernière.
268 CHAPITRE XII ───────────────────────────────────────────────────
4.2 Modèle de classe Un modèle de classe (classe modèle ou classe template) peut être considéré comme un modèle de type donc comme une métaclasse (modèle de modèles) dont les modèles caractérisent exclusivement le caractère générique de la classe.
■ Déclaration et définition
Le qualificatif template précède déclaration et définition d'un modèle de classe.
Syntaxe de la déclaration template <arguments_paramétrés> class|struct|union identificateur_classe_ modèle; où arguments_paramétrés représente la liste des arguments modèles utilisés par la classe modèle.
■ Méthode d'une classe modèle
Une méthode d'une classe modèle peut comporter des modèles de type dans sa liste d'arguments sans nécessité de déclaration, leur type effectif étant déterminé à l'instanciation de la classe.
■ Méthode modèle définie à l'extérieur d'une classe modèle
• Les méthodes d'un modèle de classe définies à l'extérieur de la classe y sont qualifiées template.
• Les modèles de type d'une méthode externe à une classe sont toujours spécifiés, à sa définition et déclaration, entre les opérateurs de comparaison, après l'identificateur de la classe.
Syntaxe template <arguments_ paramétrés> type_retour identificateur_classe_paramétré<argts_paramétrés>::méthode(liste_arguments) {…}
■ Classe modèle et amitié
Les classes modèles peuvent être dotées de fonctions amies, éventuellement modèles.
4.3 Modèle de méthode
■ Définition
Soit une classe, éventuellement modèle. Une méthode modèle opère sur un argument modèle au moins, à l'exception des destructeurs. Elle peut être définie dans ou à l'extérieur la classe.
■ Classe d'appartenance non modèle
La syntaxe d'appel est identique à celle d'un modèle de fonction.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
269
Classe A
public
privéadd(T)
� Exemple
#include <iostream.h> class A { int i;
public: template <class T> void addition(T); // Déclaration de la méthode modèle addition A(){i=3;} // Constructeur
};
template <class T> void A::addition(T valeur) { i=i+(( int) valeur); cout << i << endl; }
int main( void) { A objet;
objet.addition(1); objet.addition(-18.67); objet.addition(1e3); }
// Résultat 4 -14 986
■ Classe d'appartenance modèle
Deux spécifications du mot clé template sont nécessaires : une pour la classe, une pour la méthode. La définition d'un modèle de méthode dans une classe modèle est identique à celle d'un modèle de fonction. Il n'est pas obligatoire d'y indiquer les arguments modèles de la classe.
� Exemple
#include <iostream.h> template <class C=char> // Méthode modèle d'une classe modèle class string { public: template<class M> int compare(const M ); // Méthode modèle définie à l'extérieur de la classe modèle template<class M> // Constructeur modèle de la classe modèle string(const string<M> s) {/* … */} }; template <class C> template <class M> int string<C>::compare(const M s){return 1;}
270 CHAPITRE XII ───────────────────────────────────────────────────
5. INSTANCIATION D'UN MODELE
La définition d'une objet modèle ne génère pas de code exécutable en l'absence d'instanciation.
■ Définitions
• L'instanciation des modèles est effectuée à l'exécution.
• L'instanciation implicite des arguments modèles est effectuée à la première utilisation d'une fonction ou d'une classe modèle.
• Le type effectif d'un argument modèle est déterminé par son contexte d'utilisation.
5.1 Instanciation implicite d'un modèle de fonction
■ Contexte d'appel non ambigu
Considérons le programme suivant :
template <class T> T min(T x, T y) { return x<y ? x : y;}
int main(void) { // Instanciation implicite avec des arguments de type int
int i = min(2,3); ...
} L'appel de la fonction modèle min avec les arguments entiers 2 et 3, provoque l'instanciation implicite de la fonction dont l'argument modèle de type T est remplacé par le type effectif int.
■ Résolution de l'ambiguïté
L'appel ambigu d'une fonction modèle provoque une erreur de compilation qui peut être évitée par une surdéfinition adéquate.
� Exemple
La fonction modèle min ne peut pas être instanciée dans le cas suivant :
int i=min(2,3.0);
le compilateur ne pouvant déterminer le type effectif des arguments (int ou double).
L'ambiguïté est résolue par la spécification explicite des arguments modèles à l'appel.
min<int>(2,3.0)
ou mieux
min<double >(2,3.0)
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
271
� Exemple
#include <iostream.h> template <class T> T min(T x, T y) { return x<y ? x : y;}
int main(void) { cout << min(2,3)<< endl; cout << min(4.6,-56.) << endl; cout << min<int>(2,3.15) << endl; // Spécification explicite cout << min<double >(2,3.15) << endl; // Spécification explicite }
■ Syntaxe simplifiée
La syntaxe peut être simplifiée quand le compilateur peut déduire l'instanciation de tous les objets modèles à partir des définitions.
� Exemple
La déclaration ci-dessous, externe à la fonction main(), provoque une instanciation implicite de la fonction modèle min avec des arguments de type int :
template int min(int, int);
Programme #include <iostream.h> template <class T> T min(T x, T y) { return x<y ? x : y;}
template int min(int, int); // Spécification du type par défaut de l'instanciation
int main(void) { cout << min(2,3)<< endl;
cout << min(4.6,-56.) << endl; cout << min<double >(2,3.15) << endl;
}
5.2 Instanciation explicite d'un modèle
■ Définition
La définition explicite de tous les objets modèles est appelée instanciation explicite.
Syntaxe identificateur_de_l'objet_paramétré < type_paramétré> identificateur_instance
■ Valeur par défaut
Une invocation avec une liste de valeur vide (opérateurs de comparaison) provoque l'instanciation avec les valeurs par défaut.
272 CHAPITRE XII ───────────────────────────────────────────────────
� Exemple
template<class T = char>// Spécification du type par défaut d'une instanciation (char) class Chaine <T> {/* Définition de la classe Chaine */}; …
Chaine<> String; // L'instance String du type par défaut char de la classe Chaine
■ Efficacité du code généré
• L'instanciation d'un objet modèle n'est autorisée que si le type correspondant est défini. Ainsi, une instanciation ne peut s'effectuer qu'à partir d'un pointeur déréférencé sur une classe.
• Le compilateur ne générant que le code nécessaire à l'instanciation des objets modèles, seules les fonctionnalités effectivement utilisées de ce dernier génèrent du code dans le programme final.
5.3 Problèmes soulevés par l'instanciation d'un mod èle Le code source d'un objet modèle instancié dans plusieurs fichiers est compilé plusieurs fois ce qui provoque un accroissement de la taille de l'application et peut devenir rédhibitoire.
■ Conséquences
Pour l'éviter, les fichiers en-tête peuvent contenir leur déclaration et définition complète.
• Le nombre de fichiers sources tend alors à se réduire et la complexité des programme augmente ce qui est contraire à la philosophie traditionnelle de la séparation des déclarations et des définitions dans différents fichiers.
• Certains compilateurs imposent une instanciation explicite qui peut nuire à la portabilité de l'application.
• D'autres compilateurs génèrent des fichiers en-tête précompilés contenant le résultat de l'analyse des fichiers en-tête déjà traités ce qui peut imposer d'utiliser un unique fichier en-tête et accroît encore la complexité.
• D'autres encore gèrent une base de données des instances des objets modèles utilisées à l'édition de liens pour la résolution des références non satisfaites de la table des symboles.
• L'éditeur de liens peut être modifié pour regrouper les différentes instances d'un modèle donné.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
273
6. MODELE SPECIALISE
On peut définir une version spécifique d'un modèle avec des arguments paramétrés donnés.
■ Définition
• Une fonction ou une classe modèle peut être spécialisée par un jeu donné des arguments modèles.
• Deux types de spécialisation sont définis : les spécialisations partielles (certains arguments modèles ont une valeur fixée) et les spécialisations totales (tous les arguments modèles ont une valeur déterminée).
6.1 Spécialisation partielle d'un modèle (fonction ou classe)
■ Objectif
Une spécialisation partielle définit l'implémentation d'une fonction ou d'une classe modèle pour des valeurs fixées de certains de ses arguments modèles dont la nature peut varier (par exemple un pointeur) pour imposer au compilateur le choix de l'implémentation correspondante.
■ Syntaxe
La liste des modèles d'arguments, préalablement déclarés, est spécifiée entre les opérateurs de comparaison à la définition du modèle.
� Exemple
#include <iostream.h> // Exemple de spécialisation partielle template <class T1, class T2, int I> // Définition du modèle de classe A class A { public : T1 champ1;
A(){ cout << " 0 " << endl;} };
template <class T, int I> // Spécialisation 1 class A<T, T*, I> { public: A(){ cout << " 1 " << endl;}};
template <class T1, class T2, int I> // Spécialisation 2 class A<T1*, T2, I> { public: A(){ cout << " 2 " << endl;}};
template <class T> // Spécialisation 3 class A<T*, int, 5> { public: A(){ cout << " 3 " << endl;}};
template <class T1, class T2, int I> // Spécialisation 4 class A<T1, T2*, I> { public: A(){ cout << " 4 " << endl;}};
template <class T2, int I> // Spécialisation 5 class A<T2, int, I> { public: A(){ cout << " 5 " << endl;}};
274 CHAPITRE XII ───────────────────────────────────────────────────
int main(void) {A <int, float, 4> essai; A<float, int, 6> essai2; A<int, int* , 2> essai3; A<char *, double, 6> essai4; A<int, float*, 7> essai5; A<char *, int, 5> essai6; }
// Résultat 0 5 1 2 4 3
■ Règles de spécialisation des modèles de classes
• Le nombre des arguments modèles déclarés suivant le mot-clé template peut varier.
• Le nombre des arguments spécialisés doit rester constant (3 dans l'exemple précédent).
• Un argument modèle spécialisé ne peut être exprimé en fonction d'un autre argument modèle de la classe.
• Le type d'une des valeurs spécialisées ne peut dépendre d'un autre argument modèle.
• La liste des arguments de la spécialisation ne doit pas être identique à la liste implicite de la déclaration template correspondante.
• La déclaration de la liste des arguments modèles d'une spécialisation ne doit pas contenir des valeurs par défaut inutilisables.
� Exemple 1
template <int I, int J> struct B {}; template <int I> struct B<I, I*2> // Erreur ! Spécialisation incorrecte {};
� Exemple 2
template <class T, T t> struct C {}; template <class T> struct C<T, 1>; // Erreur! Spécialisation incorrecte!
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
275
6.2 Spécialisation totale d'un modèle de classe
Syntaxe La spécialisation totale impose de fournir une liste vide de arguments modèles entre les opérateurs de comparaison, après l'identificateur de la fonction ou de la classe modèle. La définition de cette fonction ou classe doit être précédée de l'instruction :
template <>
� Exemple 1
Soit la fonction min définie plus haut, utilisée sur la variable structurée Structure et devant comparer ses champs. Elle peut être spécialisée de la manière suivante :
#include <iostream.h> // Spécialisation totale struct Structure {int Clef; // Clef d'accès aux données. void *pData; // Pointeur sur les données. Structure(int a=10, void *p=(void*)NULL){Clef=a; pData=p;} };
template <class T> T min(T x, T y) { return x<y ? x : y;}
template <> Structure min<Structure>(Structure s1, Structure s2) {if (s1.Clef<s2.Clef) return s1; else return s2;}
int main() {int cle1=1, cle2=2; Structure Structure1(cle1,&cle1); Structure Structure2(cle2,&cle2), Structure3; cout << (min(Structure1, Structure2)).Clef << endl; cout << (min(Structure2,Structure3)).Clef<< endl; cout << (min(Structure2,Structure3)).pData<< endl; }
// Résultat 1 2 0x22ff70
■ Remarque
Certains compilateurs n'acceptent pas une liste vide des arguments template.
6.3 Spécialisation d'une méthode d'une classe modèl e La spécialisation partielle d'une classe modèle peut être assez lourde, en particulier si la structure de données qu'elle contient est identique dans les différentes versions spécialisées. Dans ce cas, il peut être plus simple de ne spécialiser que certaines méthodes ce qui permet d'éviter de redéfinir les données membres non concernées.
Syntaxe Une méthode est spécialisée par la définition de certains de ses arguments modèles.
276 CHAPITRE XII ───────────────────────────────────────────────────
� Exemple
#include <iostream.h> // Méthode spécialisée d'une classe modèle template <class T> class Item { public :
T item; Item(){item=(T)0;} // Constructeur par défaut Item(T); void set(T); T get(void) const; void print(void) const;
};
template <class T> Item<T>::Item(T i) // Constructeur {item = i;}
// Accesseurs template <class T> void Item<T>::set(T i) {item = i; cout << "set : item = " << item << endl; }
template <class T> T Item<T>::get(void) const { cout << "get : item " << item << endl; return item;}
template <class T> // Fonction modèle d'affichage void Item<T>::print(void) const { cout << "print paramétré : " << item << endl;}
template <> // Fonction d'affichage spécialisée pour le type int * et la méthode print void Item<int *>::print(void) const { cout << *item << endl;}
int main(void) { int a=8,b,*pa=&a;
float pi=3.1416; Item <int> entier; Item <float> flottant(pi); Item <int *> pointeur(pa);
entier.set(a); entier.get(); entier.print(); flottant.get(); flottant.print(); pointeur.get(); pointeur.print();
}
// Résultat set : item = 8 get : item 8 print paramétré : 8 get : item 3.1416 print paramétré : 3.1416 get : item 0xbffff9d4 8
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
277
7. METACLASSE MODELE (TEMPLATE TEMPLATE)
• Une classe modèle peut être interprétée comme un argument décrivant un modèle de type d'une autre classe et considérée comme une méta classe (classe de classes).
• Elle est qualifiée template dans la déclaration template et est appelée méta classe modèle (classe template template).
• L'instanciation par défaut d'un type classe modèle est autorisée.
Syntaxe template <class MetaClasse, …, template <class T> class M > où
MetaClasse est une métaclasse modèle par la classe modèle M T est un modèle de type de la classe modèle M, définie préalablement
� Exemple
#define TAILLE 4 #include <iostream.h> // Déclaration de méta classe modèle template <typename T> class Tableau { public: T tab[TAILLE];}; // classe modèle Tableau
template <class U, class V, template <typename T> class C = Tableau> // C : instance de la classe modèle T (Tableau par défaut) // d'un modèle de type d'une des méta classes U ou V
class Dictionnaire // Définition de la méta classe modèle Dictionnaire { public: C<U> Clef; // Création de la dépendance : Clef est un Tableau de type U C<V> Valeur; // Création de la dépendance : Valeur est un Tableau de type V };
int main(void) { Dictionnaire <int, short> Dico; // Clef, Tableau de type int, Valeur Tableau de type short
Dictionnaire <float , char > Dico2; Tableau <int> liste; //liste, instancié comme tableau d'entier Tableau <float> liste_flottant;
for (int i=0; i < TAILLE; i++) { liste_flottant.tab[i]=(float) 32*i;
Dico.Clef.tab[i]=i; // Gestion de la clé d'accès au dictionnaire Dico.Valeur.tab[i]=32*i; // Accès au dictionnaire Dico2.Clef.tab[i]=liste_flottant.tab[i]; // Accès par l'intermédiaire du tableau Dico2.Valeur.tab[i]= 'i';
} }
■ Interprétation
• La classe modèle Dictionnaire associe des données membres à une classe modèle.
• Les données membres sont décrites par les classes (conteneurs) modèles Clef et Valeur dont le type (classe) modèle est défini par le méta argument modèle C.
• Ce dernier décrit les modèles de type U et V instanciés par le modèle de type Tableau comme conteneur par défaut.
278 CHAPITRE XII ───────────────────────────────────────────────────
8. MODELES DE CONSTANTES
Syntaxe La déclaration d'un modèle de constante est effectuée selon la syntaxe :
template<type_argument_paramétré identificateur_argument_paramétré[=valeur_par_défaut][,…]>
Les arguments modèles peuvent être des types où des constantes.
■ Modèle de constante
Le type des modèles de constantes est l'un des suivants :
• type intégral (char , wchar _t, int, long, short, unsigned ) ou énuméré,
• pointeur ou référence (objet ou fonctions),
• pointeur sur membre.
� Exemple
// Déclaration d'arguments de type modèle de constante template <class T, int i, void (*f)(int)> // Modèle de type T, // Modèle de constante i de type int // Modèle de constante f de type pointeur sur une procédure avec un argument entier.
■ Remarque
Les arguments constants de type référence ne peuvent pas être initialisés avec une donnée (immédiate ou temporaire) lors de l'instanciation d'un modèle d'argument.
� Exemple
#include <iostream.h> template <int valeur> // Modèle de constante void f(void) { cout << "valeur = " << valeur << endl; }
int main(void) {f<5>(); // Affiche 5 const int a=8; f<a>(); // Affiche 8 }
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
279
9. GENERICITE ET METHODE VIRTUELLE
• Une méthode virtuelle ne peut pas être un modèle.
• Une méthode modèle avec le même identificateur qu'une méthode virtuelle d'une classe de base ne la surdéfinie pas.
• Une méthode virtuelle peut appeler une méthode modèle.
� Exemple
Une classe de base est dotée d'une méthode virtuelle f.
Une classe dérivée est dotée d'une méthode modèle f.
La méthode virtuelle appelle la méthode modèle et l'appel n'est pas récursif.
// Méthode modèle et méthode virtuelle #include <iostream.h> class B {public : virtual void f(int); // Méthode virtuelle };
void B::f(int entier) { cout << "B::f : " << entier << endl;}
class D : public B {public : template <class T> void f(T); // Méthode modèle ne surdéfinissant pas B::f(int) void f(int i) // Surdéfinition de la méthode virtuelle B::f(int) {f<>(i);} // Appel (non récursif) de la méthode modèle };
template <class T > void D::f(T type) {cout << "F paramétré : " << type << endl;}
int main(void) {B Base; Base.f(1); D Derive; Derive.f(2); }
// Résultat B::f : 1 F paramétré : 2
■ Méthodes modèles et traditionnelles
Soient deux méthodes modèles et non modèles de même signature. Cette dernière est toujours appelée sauf spécification explicite des arguments modèles entre les opérateurs de comparaison.
280 CHAPITRE XII ───────────────────────────────────────────────────
� Exemple 1
// Surdéfinition d'une méthode classique par une méthode modèle #include <iostream.h> struct A // Prototypes { void f(int); // Méthode classique template <class T> void f(T); // Modèle de méthode };
void A::f( int entier) // Définition de la méthode classique { cout << "A::f( int) = " << entier << endl;}
template <> // Définition de la méthode modèle void A::f<int>(int entier) { cout << "A::f<int>(int) = " << entier << endl;}
int main(void) {A a; a.f(1); // Appel de la version non modèle a.f<>(2); // Appel de la version modèle spécialisée }
// Résultat A::f( int) = 1 A::f<int>(int) = 2
� Exemple 2
// Surdéfinition d'une méthode non modèle par une méthode modèle #include <iostream.h> struct A { void f(int); template <class T> void f(T); };
// Méthode classique void A::f( int entier) { cout << "A::f(int) = " << entier << endl;}
// Méthode modèle template <class T> void A::f(T valeur) { cout << "A::f(T) = " << valeur << endl;}
int main(void) {A a; a.f(1); // Appel de la méthode classique a.f<char >('c'); // Appel de la méthode modèle instanciée a.f<>('c'); // Appel de la méthode modèle spécialisée a.f<>(2); // Appel de la méthode modèle spécialisée }
// Résultat A::f(int) = 1 A::f(T) = c A::f(T) = c A::f(T) = 2
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
281
10. MOT CLE TYPENAME
■ Sémantique
Nous avons vu que le mot-clé typename peut être utilisé pour introduire les modèles de type dans les déclarations template. Il indique également qu'un identificateur inconnu est un type utilisable dans la définition des modèles d'objet.
■ Syntaxe
typename identificateur
� Exemple
// Le mot-clé typename #include <iostream.h> class A { public: typedef int Y; // Y est un type défini dans la classe A };
template <class T> class X // La classe modèle X suppose que le modèle de type T définit un type Y { public: typename T::Y i; }; int main() {X<A> x; // La classe A permet d'instancier à partir de la classe modèle X x.i=3; cout << "x.i=" << x.i << endl; }
11. LA BIBLIOTHEQUE TEMPLATE
La bibliothèque STL (Standard Template Library), intégrée au langage C++, propose un grand nombre de modèles d'objets génériques ainsi que quelques algorithmes.
11.1 Objets de base La bibliothèque STL comporte essentiellement trois classes d'objets : les conteneurs, les itérateurs, les algorithmes et leurs allocateurs.
■ Conteneur
Un conteneur contient des éléments de type générique.
Plusieurs classes de conteneurs sont proposées : les tableaux (classes vector et deque), les listes (list), les piles (stack), les files (queue), les ensembles (set), les ensembles multiples (multiset), les cartes (map) et les multicartes (multimap).
■ Itérateur
Un itérateur permet de définir des opérations sur les conteneurs et peut être interprété comme un pointeur permettant d'accéder à ses éléments typés.
282 CHAPITRE XII ───────────────────────────────────────────────────
■ Algorithme
• Les algorithmes opèrent sur les données d'un conteneur avec des itérateurs.
• Le code modèle est réutilisable.
• Les algorithmes proposés n'utilisent pas de conteneur spécifique.
■ Allocateur
Un allocateur est un gestionnaire de mémoire utilisé par un conteneur comme dernier paramètre générique.
11.2 Vecteur et itérateur Un vecteur est un tableau d'objets dont deux références, une sur le début et une sur la fin de la collection, permettent de décrire la totalité. Sa taille peut augmenter dynamiquement.
La méthode begin retourne un itérateur sur le début du vecteur et end un itérateur sur sa fin.
� Exemple 1
Le programme suivant crée un tableau de 3 entiers et y stocke trois valeurs. La méthode copy utilise deux itérateurs (un sur le premier élément, un sur le dernier élément) pour délimiter puis envoyer un flot de données sur l'itérateur en sortie.
#include <vector> #include <iterator> #include <iostream> using namespace std; int main(void) {vector<int> myVector(3); // Instanciation d'un vecteur de 3 composantes myVector[0] = 2; myVector[1] = 4; myVector[2] = 6; copy(myVector.begin(),myVector.end(), ostream_iterator<int>(cout," ")); cout << endl; return 0; }
// Résulat 2 4 6
� Exemple 2
Le vecteur est créé avec trois composantes et la méthode push_back permet de modifier la taille du tableau par ajout de composante. Ainsi, les trois appels de la méthode push_back en modifient dynamiquement le nombre (jusqu'à 6).
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
283
#include <vector> #include <iterator> #include <iostream> using namespace std; int main(void) {vector<int> myVector(3);
myVector.push_back(3); myVector.push_back(4); myVector.push_back(5); myVector[1] += 2; copy(myVector.begin(),myVector.end(), ostream_iterator<int>(cout," ")); cout << endl; return 0;
}
// Résultat 0 2 0 3 4 5
■ Principales méthodes
iterator begin(); Retourne un itérateur sur le début de la collection.
iterator end(); Retourne un itérateur après le dernier élément de la collection.
void push_back(const T &); Dépose d'un élément en dernière position de la collection.
T &back(); Retourne le dernier élément de la collection. void pop_back(); Suppression du dernier élément de la
collection. iterator insert(iterator,const T &); Insertion d'un élément à une position
donnée. iterator erase(iterator); Suppression d'un élément à une position
donnée.
� Exemple
Saisie, tri, affichage de nombres entiers à partir des flux d'entrée/sortie.
#include <stdlib.h> #include <iostream> #include <fstream> #include <vector> #include <iterator> #include <algorithm> using namespace std;
int main(void) { typedef vector<int> vector_int;
ifstream source("input.txt"); vector<int> v; istream_iterator<int> start(source); istream_iterator<int> end; back_insert_iterator<vector_int> dest(v); copy(start,end,dest); sort(v.begin(),v.end()); copy(v.begin(),v.end(),ostream_iterator<int>(cout,"-")); cout << endl << "Fin du test !" << endl; return 0;
}
284 CHAPITRE XII ───────────────────────────────────────────────────
11.3 Les deques Un deque est un objet d'une collection pouvant être manipulé comme un vector par l'utilisation de la méthode push_front, (insertion en tête de la séquence).
■ Principales méthodes
iterator begin(); Retourne un itérateur sur le début de la collection.
iterator end(); Retourne un itérateur après le dernier élément de la collection ().
void push_back(const T &); Dépose d'un élément en dernière position. T &back(); Retourne le dernier élément de la collection. void pop_back(); Suppression du dernier élément de la
collection. void push_front(const T &); Dépose d'un élément en tête de la collection. T &front(); Retourne le premier élément de la collection. void pop_front(); Supprime le premier élément de la
collection. iterator insert(iterator,const T &); Insertion d'un élément à une position
donnée. iterator erase(iterator); Suppression d'un élément à une position
donnée.
12. FONCTIONS EXPORTEES
Les bibliothèques de modèles peuvent ne pas être fournies, la norme permettant de les compiler séparément et d'exporter les définitions des modèles dans des fichiers.
Syntaxe Les fonctions et classes modèles concernées sont exportées à partir du mot clé export.
■ Description
• Les définitions des fonctions et des classes exportées doivent être qualifiées export.
• L'exportation d'une classe modèle exporte toutes ses méthodes non qualifiées inline, toutes ses données statiques, toutes ses classes membres et toutes ses méthodes modèles non statiques.
• Une fonction modèle qualifiée inline, les fonctions et les classes modèles définies dans un espace de nommage anonyme ne peuvent pas être exportées.
� Exemple
export template <class T> void f(T); // Fonction dont le code n'est pas fourni
// Code exporté de f export template <class T> void f(T p) {…} // Corps de la fonction.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
285
13. EXERCICES
� Exercice 1 : le préprocesseur et la substitution fonctionnelle.
Le volume d'une sphère est calculé avec les directives #define et #include.
1°) Ecrire la fonction symbolique vol(r) qui appelle la fonction symbolique surf(r) qui appelle la fonction symbolique circonf(r). Définir π par une constante symbolique.
2°) Répartir le fichier source en 4 fichiers principal.c, vol.c, surf.c, circonf.c, pi.h.
principal.c vol.c surf.c circonf.c pi.h
� Exercice 2 : insuffisances du préprocesseur et modèle objet
1°) Définir la fonction symbolique square(x) (x*x) et l'utiliser sur des nombres entiers (courts, longs, avec ou sans signe), des nombres flottants, char et sur l'expression x+3.
2°) Définir un modèle de fonction carre(T) qui puisse opérer sur des objets d'un des types précédents et calculer le carré de l'expression entier+3. Conclusion.
#include<iostream.h> #define square(x) (x*x) // Effets de bords possibles
template<class T> T carre(T var) {return (var*var);}
int main() { char caract='u'; int entier=2; float flottant=3.2;
entier=square(entier); cout<< "entier="<<entier << "\tsquare(flottant) =" <<square(flottant); cout<<"\tsquare(entier+3)="<<square(entier+3)<<"\nsquare(caract):"<<square(caract) ; cout<<"\tcarre(3) = " << carre(3) << " carre(entier+3)= "<< carre(entier+3)<<endl;
}
// Résultats entier=4 square(flottant) =10.24 square(entier+3)=19 square(caract):13689 carre(3) = 9 carre(entier+3)= 49
� Exercice 3
Soit une classe modèle A contenant la méthode f(char *, T type). Instancier f.
#include <iostream.h> template <class T = double > class A { public: void f(char *,T type); };
template <class T> void A<T>::f(char *chaine, T objet) { cout << "A<T>::f(" << chaine << ")" << " objet = " << objet << " ";}
int main(void) { A< char > a; // Instanciation explicite de la classe modèle A<char >
char c='c'; a.f("char ", c); // Instanciation explicite de la méthode modèle A<char >::f() A<int> i; // Instanciation explicite de la classe modèle A<int> int b=25; i.f("int",b ); // Instanciation explicite de la méthode modèle A<int>::f() A<> d; // Instanciation par défaut de la méthode modèle A<double >::f() d.f("defaut",3.1416e0); return 0;
}
// Résultat A<T>::f(char ) objet = c A<T>::f(int) objet =25 A<T>::f(défaut) objet =3.1416
286 CHAPITRE XII ───────────────────────────────────────────────────
� Exercice 4 : instanciation explicite d'un objet modèle
Définir une classe modèle Pile constituée d'un tableau et de son pointeur de pile, d'un constructeur, d'un destructeur, de méthodes d'empilement, dépilement, et test de pile vide.
Instancier la pile avec des entiers (type par défaut), flottants, caractères.
#include <stdio.h> const int MAXSIZE = 128;
template<class Type = int> class Pile {Type Tableau[MAXSIZE]; int Pointeur_Pile; public: Pile(void) {Pointeur_Pile = 0;}; void push(Type in_data) {Tableau[Pointeur_Pile++] = in_data;}; Type pop(void) { return Tableau[--Pointeur_Pile];}; int vide(void) { return (Pointeur_Pile == 0);}; };
int main(void) { int x = 12, y = -7; float reel = 3.1415;
Pile<> int_Pile; // Instanciation avec le type int par défaut Pile<float> float_Pile; // Instanciation avec le type float Pile<char *> string_Pile; // Instanciation avec le type char *
char nom[] = "John Lee Hooker"; int_Pile.push(x); int_Pile.push(y); int_Pile.push(77);
float_Pile.push(reel); float_Pile.push(-12.345); float_Pile.push(100.01);
string_Pile.push("Première ligne"); string_Pile.push("Deuxième ligne"); string_Pile.push("Troisième ligne"); string_Pile.push(nom);
printf("Pile d'entiers ---> "); printf("%8d",int_Pile.pop());printf("%8d", int_Pile.pop()); printf("%8d\n",int_Pile.pop());
printf(" Pile de flottants ---> "); printf("%8.3f ", float_Pile.pop()); printf("%8.3f ", float_Pile.pop()); printf("%8.3f\n", float_Pile.pop()); printf("Chaînes de caractères\n");
do {printf("%s\n", string_Pile.pop());} while (!string_Pile.vide()); return 0;
}
// Résultat Pile d'entiers ---> 12 -7 77 Pile de flottants ---> 3.141 -12.345 100.010 Chaînes de caractères John Lee Hooker Troisième ligne Deuxième ligne Première ligne
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
287
� Exercice 5 : modèle de classe
Modéliser une classe de pile d'objets constitués en une liste chaînée avec les méthodes suivantes : constructeur, destructeur, empilement, dépilement d'une instance de la pile, test de pile vide, vidage de la pile, surdéfinition de l'opérateur d'affectation. #include <iostream.h> // Classe modèle de pile, liste chaînée allouée dynamiquement template <class T> class Stack { typedef struct stackitem {T Item; struct stackitem *Next; } StackItem; StackItem *Tete;
public: Stack(void); // Constructeurs Stack(const Stack<T> &); // Type effectif ("Stack<T>"). ~Stack(void); // Destructeur Stack<T> &operator =(const Stack<T> ); // Surdéfinition de l'opérateur d'affectation void push(T); T pop(void); // Empilement, dépilement bool is_empty(void) const; // Test de pile vide void flush(void); // Vidage de la pile
};
template <class T> Stack<T>::Stack(void) {Tete = NULL;}
template <class T> Stack<T>::Stack(const Stack<T> &Init) { Tete = NULL;
StackItem *tmp1 = Init.Tete, *tmp2 = NULL; while (tmp1!=NULL) { if (tmp2==NULL) {Tete= new StackItem; tmp2 = Tete;}
else {tmp2->Next = new StackItem; tmp2 = tmp2->Next;} tmp2->Item = tmp1->Item; tmp1 = tmp1->Next;
} if (tmp2!=NULL) tmp2->Next = NULL;
}
template <class T> Stack<T>::~Stack(void) {flush();}
template <class T> Stack<T> & Stack<T>::operator =(const Stack<T> Init) {flush(); StackItem *tmp1 = Init.Tete, *tmp2 = NULL; while (tmp1!=NULL) { if (tmp2==NULL) {Tete = new StackItem; tmp2 = Tete;} else {tmp2->Next = new StackItem; tmp2 = tmp2->Next;} tmp2->Item = tmp1->Item; tmp1 = tmp1->Next; }
if (tmp2!=NULL) tmp2->Next = NULL; return *this; }
template <class T> void Stack<T>::push(T Item) {StackItem *tmp = new StackItem; tmp->Item = Item; tmp->Next = Tete; Tete = tmp;}
template <class T> T Stack<T>::pop(void) {T tmp; StackItem *ptmp = Tete; if (Tete!=NULL) {tmp = Tete->Item; Tete = Tete->Next; delete ptmp;} return tmp; }
template <class T> bool Stack<T>::is_empty(void) const { return (Tete==NULL);}
template <class T> void Stack<T>::flush(void) {while (Tete!=NULL) pop();}
288 CHAPITRE XII ───────────────────────────────────────────────────
int main(void) { Stack <int> int_Pile; Stack <float> float_Pile[5]; int i, k;
k=int_Pile.is_empty(); (k==1) ? cout <<" k = " << k << " int_Pile_is_empty " << endl : cout <<" k = " << k << " int_Pile_is_not_empty " << endl;
int_Pile.push(k); k=int_Pile.is_empty(); (k==1) ? cout <<" k = " << k << " int_Pile_is_empty " << endl : cout <<" k = " << k << " int_Pile_is_not_empty " << endl;
int_Pile.flush(); k=int_Pile.is_empty(); (k==1) ? cout <<" k = " << k << " int_Pile_is_empty " << endl : cout <<" k = " << k << " int_Pile_is_not_empty " << endl;
for( i = 0; i < 5; i++ ) { int_Pile.push(i); float_Pile[i].flush(); float_Pile[i].push((float)i); }
for( i = 0; i < 5; i++ ) cout << " pop " << int_Pile.pop() << endl;
k=int_Pile.is_empty(); (k==1) ? cout <<" k = " << k << " int_Pile_is_empty " << endl : cout <<" k = " << k << " int_Pile_is_not_empty " << endl;
for(i=0; i < 5 ;i++) {k= float_Pile[i].is_empty(); (k==1) ? cout <<" k = " << k << " float_Pile_is_empty " << endl : cout <<" k = " << k << " float_Pile_is_not_empty " << endl; }
}
� Exercice 6 : le modèle objet et les classes dérivées
1°) Définir un modèle de classe Nombres pouvant opérer sur des nombres d'un type prédéfini quelconque avec les méthodes modèles suivantes : constructeur, destructeur, saisie, impression, addition et multiplication surdéfinies.
L'instancier à partir de nombres de type entier, réel…
2°) Construire une classe modèle de nombres complexes dérivée de la classe modèle Nombres ainsi que les méthodes de saisie et d'affichage correspondantes et l'instancier avec des nombres entiers, réels, complexes.
Les constructeurs, opérateurs d'addition et de multiplication seront redéfinis dans la classe dérivée.
3°) Définir un modèle de constructeur d'un tableau de nombres (classe mère) si nécessaire.
4°) Définir un constructeur d'un tableau de nombres complexes si nécessaire.
5°) Surdéfinir les opérateurs d'addition et de multiplication pour qu'ils opèrent sur des complexes, des tableaux de complexes.
LES MODELES GENERATEURS D'OBJETS (TEMPLATE) ───────────────────────────────────────────────────
289
// Corrigé partiel #include<iostream.h> template<class T> T carre(T var) {return (var*var);}
template <class Type= double> // Type double par défaut class Nombre {Type valeur;
public: // Constructeur et destructeur Nombre(Type a=0){valeur=a;} ~Nombre(){}
// Prototypes Nombre <Type> operator *(const Nombre <Type>); Nombre<Type>& operator = (const Nombre<Type>); template <class T> friend T carre(T);
void affiche(char * chaine) {cout << chaine << valeur << endl;} };
template <class Type> // Surdéfinition de l'opérateur d'affectation Nombre<Type> & Nombre<Type>::operator = (const Nombre<Type> y) {valeur=y.valeur; }
// Surdéfinition de l'opérateur * template <class Type> Nombre <Type> Nombre<Type>::operator *(const Nombre<Type> y) { return valeur*y.valeur; }
template <class Type= float> // La classe modèle Complexes dérive de la classe Nombre class Complexes : public Nombre<Type> { Type reel, imaginaire;
public: // Constructeur et destructeur Complexes(Type a=0, Type b=0){reel=a; imaginaire=b;} ~Complexes(){} // Prototypes Complexes <Type> operator *(const Complexes <Type>); Complexes<Type>& operator = (const Complexes<Type>); template <class T> friend T carre(T); void affiche(char * chaine) {cout << chaine << reel << " "<< imaginaire << endl;}
};
template <class Type> // Surdéfinition de l'opérateur d'affectation Complexes<Type> & Complexes<Type>::operator = (const Complexes<Type> y) { reel=y.reel; imaginaire=y.imaginaire; }
template <class Type> // Surdéfinition de l'opérateur * Complexes <Type> Complexes<Type>::operator *(const Complexes<Type> y) { Complexes<Type> aux;
aux.reel=reel*y.reel-imaginaire*y.imaginaire; aux.imaginaire= reel*y.imaginaire+imaginaire*y.reel; return aux;
}
// Définition des types "utilisateurs" Entier, Reel, Complexe typedef Nombre<int> Entier; typedef Nombre<float> Reel; typedef Complexes<> Complexe;
290 CHAPITRE XII ───────────────────────────────────────────────────
int main() { Entier entier1(10), entier2(20), entier3;
entier2.affiche("entier2 : "); entier3.affiche("entier3 : "); entier3=entier2; entier3.affiche("entier3 :"); entier1=entier2*entier3; entier1.affiche("entier1 : "); entier3=entier1*entier2; entier3.affiche("entier3 :"); carre(entier3).affiche("carre(entier3):"); Reel reel1(10.25), reel2(20.89), reel3; reel3.affiche("reel3 :"); reel3=reel2; reel3.affiche("reel3 :"); reel1=reel2*reel3; reel1.affiche("reel1 :"); carre(reel1).affiche("carre(reel1) : ");
Complexe z(5,8); Complexes<float > z1(0,1), z2; z.affiche("z : "); z1.affiche("z1 : "); (z1*z1).affiche("z1*z1="); return 1;
}
// Résultat entier2 : 20 entier3 : 0 entier3 :20 entier1 : 400 entier3 :8000 carre(entier3):64000000
reel3 :0 reel3 :20.89 reel1 :436.392 carre(reel1) : 190438
z : 5 8 z1 : 0 1 z1*z1= -1 0
ESPACES DE NOMMAGE
Les espaces de nommage sont des zones de déclaration d'identificateurs qui y sont regroupés pour éviter d'éventuels conflits d'identification entre différents modules d'une application. Par exemple, deux programmeurs définissant un même identificateur de classe dans deux fichiers différents risquent de provoquer un conflit à l'édition de lien ou à l'utilisation de fichiers sources communs.
Ce conflit est du à l'unicité de l'espace de nommage par défaut du langage C++, dont la portée est globale et dans lequel aucun conflit d'identificateur n'est autorisé. L'utilisation d'espaces de nommage non globaux est une solution à ce problème.
1. ESPACE NOMME ET ANONYME
1.1 Espace nommé Un identificateur associé à un espace de nommage est appelé espace de nommage nommé ou plus simplement espace de nommage, espace nommé, espace.
Syntaxe namespace identificateur_espace_noms {déclarations | définitions} identificateur_espace_noms est le nom de l'espace de nommage, déclarations et/ou définitions contiennent la liste des identificateurs le constituant.
■ Extension
Un espace peut être découpé en plusieurs zones, la première étant utilisée pour des déclarations, les suivantes pour des extensions.
La syntaxe d'une extension d'espace est identique à celle de sa déclaration.
� Exemple
namespace A // L'espace de nommage A { int i;} … namespace B // L'espace B { int i;} … namespace A // Extension de l'espace A { int j;}
Les identificateurs déclarés ou définis dans un espace ne doivent pas entrer en conflit et peuvent être surdéfinis. Ils sont accessibles par l'opérateur de résolution de portée.
CHAPITRE XIII
292 CHAPITRE XIII ───────────────────────────────────────────────────
� Exemple
#include <iostream.h> int i=1; // I global
namespace A { int i=2; // I redéfini dans l'espace A.
int j=i; // Référence A::i. }
int main(void) { cout << "i = " << i << endl;
cout << "A::i = " << A::i << endl; A::i=3; cout << "A::i = " << A::i << endl; return 0;
}
// Résultat i=1 A::i=2 A::i=3
■ Définition
Les fonctions et méthodes d'un espace définis à l'extérieur avec l'opérateur de résolution de portée imposent une déclaration préalable dans l'espace.
� Exemple
namespace A { int f(void); } // Déclaration de A::f
int A::f(void) // Définition externe de A::f { return 0;}
■ Définition récursive d'espace
• Un espace peut être défini dans un autre.
• Cette déclaration apparaissant au niveau le plus externe de l'espace conteneur, l'espace contenu ne peut être déclaré dans une fonction ou une classe.
� Exemple
namespace Conteneur // Un espace dans un autre { int i; // Conteneur::i. namespace Contenu {int j;} // Conteneur::Contenu::j }
1.2 Espace anonyme • Un espace anonyme est caractérisé par l'absence d'identificateur à sa déclaration.
• Ce type d'espace garantissant l'unicité de ses identificateurs peut être utilisé à la place du mot-clé static pour garantir l'unicité des objets d'un fichier.
• Un espace anonyme peut être déclaré dans un autre espace.
ESPACES DE NOMMAGE ───────────────────────────────────────────────────
293
� Exemple
namespace // Espace anonyme { int i;} // ::i est unique;
■ Portée d'un identificateur global
Un identificateur global est masqué par un identificateur local identique.
Dans des espaces nommés, l'accès est réalisé à partir de l'opérateur de résolution de portée. Il est impossible dans un espace anonyme.
� Exemple
#include <iostream.h> namespace // Ambiguïté entre espaces anonyme et nommé { int i = 10;} // Définition de ::i
void f(void) {i++;
cout << " f()::i = " << i << endl; } // Utilisation de ::i
namespace A { namespace
{ int i; int j;} // Définitions de A::i , A::j et initialisation implicite à 0
void g(void) { cout << "fonction g" << endl;
i++; // Résolution de l'ambiguïté entre ::i et A::i cout << " i = " << i << endl; A::i++; cout << " A::i = " << A::i << endl << " j = " << j << endl;
} // Fin de l'espace A } // Fin de l'espace anonyme global
int main(void) {f(); A::g(); } // Résultat f()::i = 11 fonction g i = 1 A::i = 2 j = 0
1.3 Alias d'espace de nommage Un alias d'espace permet d'accéder simplement à un espace lorsque sa dénomination est complexe. Il ne peut être en conflit avec d'autres identificateurs du même espace.
Syntaxe namespace identificateur_alias_espace = identificateur_espace_nommage;
294 CHAPITRE XIII ───────────────────────────────────────────────────
2. DECLARATION USING
2.1 Règles d'utilisation La déclaration using permet d'identifier un objet d'un espace sans spécifier ce dernier.
Syntaxe using identificateur;
identificateur décrit le chemin d'accès (espace et opérateur de résolution de portée).
� Exemple
namespace A { int i , j;} // Déclare A::i, A::j.
void f(void) { using A::i; // A::i accessible par l'alias i.
i=1; // Equivalent à A::i=1 j=1; // Erreur l'alias j n'étant pas défini !
}
■ Règles d'utilisation
• L'accès à un objet par un alias est identique à son accès usuel dans l'espace global.
• Un alias permet de référencer uniquement les identificateurs visibles à sa déclaration. Sa portée est réduite à l'espace où il est défini.
• Un alias peut être déclaré plusieurs fois quand les déclarations multiples sont licites (déclarations de variables ou de fonctions externes aux classes).
� Exemple
namespace A // Déclarations using multiples { int i;
void f(void); void f(void){}
}
namespace B { using A::i; // Déclaration de l'alias B::i, identique à A::i.
using A::i; // Légal : double alias de A::i. using A::f; // Déclare void B::f(void), fonction alias à A::f
}
int main(void) { B::f(); // Appelle A::f.
return 0; }
Dans un espace étendu après une déclaration using, un nouvel identificateur identique à celui d'un alias prédéfini n'est pas pris en compte.
ESPACES DE NOMMAGE ───────────────────────────────────────────────────
295
� Exemple
namespace A // Extension d'espace par une déclaration using { void f(int);} using A::f; // F est alias de A::f(int).
namespace A { void f(char ); } // F est toujours alias de A::f(int), pas de A::f(char )
void g() {f('a'); } // Appelle A::f(int), même si A::f(char ) existe
■ Conflit entre déclarations using et identificateurs locaux
• Quand plusieurs déclarations (locales et using) utilisent un même identificateur, ce dernier se rapporte à un objet unique ou représente une fonction surdéfinie.
• Une ambiguïté provoque une erreur de compilation contrairement à la directive using qui diffère la détection d'erreur à l'utilisation de l'identificateur ambigu.
� Exemple
namespace A { int i;
void f(int); }
void g(void) { int i; // Déclaration locale de i.
using A::i; // Erreur : i est déjà déclaré. void f(char ); // Déclaration locale de f(char ). using A::f; // Pas d'erreur, il y a surdéfinition de f. return ;
}
2.2 Héritage et déclaration using Une déclaration using utilisée dans la définition d'un objet membre d'une classe se réfère à une classe de base, l'identificateur associé devant y être accessible.
� Exemple
namespace A { float f;}
class Base { int i; public: int j; };
class Derivee : public Base { // using A::f; // Illégal : f n'est pas membre de la classe de base
// using Base::i; // Interdit : Base::i est d'accès privé public: using Base::j; // Légal. };
L'identificateur j est un synonyme de Base::j dans la classe Derivee.
296 CHAPITRE XIII ───────────────────────────────────────────────────
■ Requalification des droits d'accès
La déclaration using dans les classes dérivées peut rétablir des qualifications d'accès modifiés par un héritage à des membres de la classe de base. Elle doit porter sur une zone de déclaration de la classe de base où les d'accès sont qualifiés public.
� Exemple
class Base { public:
int i; };
class Derivee : private Base { public: using Base::i; // Rétablit l'accessibilité sur Base::i qui redevient public
// protected : using Base::i; // Interdit sauf requalification public préalable. };
■ Remarques
• Certains compilateurs interprètent différemment l'accessibilité de membres introduits avec une déclaration using qui selon leur grammaire permet de restreindre l'accessibilité des droits et non pas de les rétablir ce qui implique l'impossibilité de requalifier l'accessibilité de données restreintes par une qualification d'héritage, qui doit alors être défini de manière plus permissive, les accès étant ajustés cas par cas.
• Bien que cette interprétation soit licite sur le plan sémantique, les projets actuels de norme semblent indiquer qu'elle n'est pas correcte.
• Soit une fonction d'une classe de base, introduite dans une classe dérivée à partir d'une déclaration using, surdéfinie par une fonction de même signature définie dans la classe dérivée. Cette dernière surdéfinit, sans ambiguïté, la fonction de la classe de base.
3. DIRECTIVE USING
La directive using permet l'accès à tous les identificateurs d'un espace de nommage sans nécessité d'utiliser l'opérateur de résolution de portée.
Syntaxe using namespace identificateur_espace_nommage;
� Exemple
namespace A { int i; } // Déclare A::i *
void f(void) { using namespace A; // Tous les identificateurs de l'espace A sont accessibles
i=1; // Équivalent à A::i=1. }
ESPACES DE NOMMAGE ───────────────────────────────────────────────────
297
■ Portée
Les directives using sont valides à partir de la ligne où elles sont déclarées jusqu'à la fin du bloc de portée courante.
Un espace de nommage étendu après une directive using permet d'utiliser les identificateurs définis dans l'extension comme ceux qui y sont définis préalablement.
� Exemple
namespace A { int i;}
using namespace A; // Extension de l'espace de nommage
namespace A { int j;}
void f(void) { i=0; // Initialise A::i.
j=0; // Initialise A::j. return ;
}
■ Prévention des conflits
La définition d'identificateurs d'un espace par une directive using peut provoquer des conflits d'identificateur.
Aucune erreur n'est signalée sauf si un des identificateurs cause d'un conflit est utilisé.
� Exemple
namespace A; { int i;} // Définit A::i.
namespace B { int i; // Définit B::i.
using namespace A; // A::i et B::i sont en conflit. Aucune erreur n'apparaît. }
void f(void) { using namespace B;
i=2; // Erreur car ambiguïté. return ;
}
REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++
1. PRINCIPES SEMANTIQUES
■ Définitions
• En langage C++, une exception représente une interruption de l'exécution naturelle du programme résultant d'un événement ayant provoqué une erreur d'exécution suivie de l'activation d'un traitement permettant de rétablir un mode de fonctionnement cohérent du programme.
• La génération d'une exception stoppe donc l'exécution du programme et en transmet le contrôle à un gestionnaire d'exceptions qui l'attrape.
■ Remarques
• Le traitement des erreurs d'exécution est réalisé par le(s) gestionnaire(s) d'exceptions approprié(s).
• Une erreur d'exécution dans une fonction en provoque une terminaison anormale ainsi que celle anormale de la fonction appelante ce qui la propage dans la pile des fonctions appelantes jusqu'à ce qu'elle soit traitée ou jusqu'à la fin du programme.
■ Traitement traditionnel
En langage C, le code de retour d'une fonction indique à la fonction appelante si elle s'est correctement exécutée ce qui détermine le traitement ultérieur. Cette technique, lourde et délicate, nécessite de tester les codes de retour de chaque fonction.
Certains programmes gèrent le traitement des erreurs dans un code global de nettoyage, externe, qui n'est exécuté que si l'exécution se déroule sans erreur. Cette stratégie rend le programme moins structuré, car toutes les variables doivent être accessibles depuis le code de traitement des erreurs ce qui nécessite une portée globale. En outre, le traitement de code d'erreurs à valeurs multiples reste posé.
CHAPITRE XIV
300 CHAPITRE XIV ───────────────────────────────────────────────────
2. GENERATION ET TRAITEMENT D'UNE EXCEPTION
2.1 Principes de gestion des exceptions La gestion des erreurs d'exécution en langage C++ suit l'algorithme ci-après.
■ Algorithme de recherche du gestionnaire d'exception approprié
Une erreur d'exécution provoque la génération d'une exception, l'interruption de l'exécution et la recherche du gestionnaire d'exception approprié. Elle suit donc le même parcourt que celui de la remontée des erreurs à savoir :
• La première des fonctions appelantes de la pile contenant un gestionnaire d'exception approprié prend le contrôle et effectue le traitement de reprise.
◊ S'il est complet, le programme reprend son exécution normale.
◊ Dans le cas contraire, le gestionnaire d'exception peut stopper l'exécution du programme ou propager l'exception par sa relance pour rechercher dans la pile des fonctions appelantes le gestionnaire d'exception approprié.
• L'algorithme est donc récursif.
■ Gestion des variables automatiques
La norme garantit que tous les objets de classe de mémorisation automatique sont détruits lorsque l'exception qui remonte dans la pile sort de leur portée.
■ Corollaire
Quand les ressources sont encapsulées dans des classes avec destructeur(s), la remontée des exceptions provoque le nettoyage (utilisation du garbage collector).
2.2 Génération d'une exception
■ Génération d'une exception
Une erreur d'exécution est caractérisée par une exception typée lancée par la méthode throw qui crée un objet typé la caractérisant.
Synopsis throw(objet_typé_caractéristique);
■ Portée d'une exception
La portée du traitement associé à une exception donnée est définie par une zone du programme protégée des erreurs d'exécution.
Le bloc d'instructions la constituant est introduit avec la clause try.
Syntaxe try // Délimitation d'une zone de prise en compte d'une exception. {/* Code susceptible de générer une exception */ }
REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++ ───────────────────────────────────────────────────
301
2.3 Gestionnaire d'exception Tout gestionnaire d'exception, introduit par la méthode catch, suit le bloc try associé.
Syntaxe catch (type [&][objet_temporaire]) // Gestionnaire d'exception {/* Traitement de l'exception associée à la classe */ }
■ Gestionnaire d'exception(s) et constructeur copie
Les objets de classe de mémorisation automatique définis dans le bloc try et l'objet construit pour générer une exception étant détruit si l'exception provoque la sortie du bloc, le compilateur en effectue préalablement une copie pour le transférer au premier bloc catch susceptible de le recevoir ce qui peut nécessiter un constructeur copie.
■ Utilisation d'une transmission par référence
• La méthode catch peut transmettre ses arguments par valeur ou référence.
• L'utilisation d'une référence évite une copie de l'objet généré par l'exception et garantit que les modifications sont visibles dans les blocs catch des fonctions appelantes ou de portée supérieure, si l'exception est relancée après traitement.
■ Liste de gestionnaires d'exception(s)
• Plusieurs gestionnaires d'exception(s) peuvent être définis, chacun traitant l'exception dont l'objet associé est du type indiqué par son argument.
• Un objet temporaire peut être utilisé pour la transmission d'informations relatives à la nature de l'erreur. Son usage n'est pas obligatoire.
■ Gestionnaire universel
• Le gestionnaire d'exceptions universel (décrit par trois points de suspension dans sa clause catch) gère tout type d'exception.
• Une variable temporaire ne peut être associée à l'exception son type étant indéfini.
� Exemple
#include <iostream.h> // Utilisation des exceptions class Erreur // Exception associée à l'objet Erreur { public:
int cause; // Entier permettant de spécifier la cause de l'exception Erreur(int c) : cause(c) {} Erreur(const Erreur &source) : cause(source.cause) {} // Constructeur copie
};
class other {}; // Objet correspondant à toute autre exception
302 CHAPITRE XIV ───────────────────────────────────────────────────
int main(void) { int i; // Type de l'exception à générer.
cout << "Tapez 0 (une exception de type Erreur), 1 (une exception de type entier : )"; cin >> i; // Génération d'une exception cout << endl; try // Bloc de prise en charge des exceptions {switch (i) {case 0: {Erreur a(0); throw(a);} // Génére l'objet classe Erreur interrompant le code. case 1: {int a=1; throw(a);} // Exception de type entier. default: {other c; throw(c);} // C instancié puis lancé } } // Fin du bloc try
catch (Erreur &tmp) // Blocs de réception { cout << "Erreur! (cause " << tmp.cause << ")" << endl;}
catch (int tmp) // Traitement de l'exception de type int { cout << "Erreur int ! (cause " << tmp << ")" << endl;}
catch (...) // Traitement des autres types d'exception { cout << "Exception inattendue !" << endl;} return 0; }
// Résultat Tapez 0 (une exception de type Erreur), 1 (une exception de type entier) : Erreur int ! (cause 1) Tapez 0 (une exception de type Erreur), 1 (une exception de type entier) : Erreur! (cause 0) Tapez 0 (une exception de type Erreur), 1 (une exception de type entier) : Exception inattendue !
2.4 Propagation et relance d'une exception • Pour rétablir un état cohérent des données sur lesquelles elle opère, la fonction de
traitement des erreurs résultant d'une génération d'exception libère les ressources non encapsulées des objets de classe de mémorisation automatique.
• Elle relance ensuite l'exception (dont le parcours s'arrête dès le traitement effectif de l'erreur) pour l'exécution d'un traitement ultérieur par la fonction appelante.
• Une exception différente de l'exception reçue peut être générée comme dans le cas, toujours possible, où le traitement de l'erreur provoquerait lui-même une erreur.
• La méthode throw relance l'exception en cours de traitement.
Syntaxe throw() ;
Description L'exception relancée a comme argument l'objet généré à la précédente exception.
REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++ ───────────────────────────────────────────────────
303
3. GESTIONNAIRE D'EXCEPTIONS ELEMENTAIRES
La méthode std::terminate est appelée. Par défaut, elle appelle la fonction abort de la bibliothèque C qui provoque l'interruption de l'exécution sans la libération des ressources allouées pouvant ainsi occasionner des problèmes de gestion mémoire. La méthode std::set_terminate permet de masquer cet appel.
Synopsis set_terminate(void(*)(void));
� Exemple
#include <iostream.h> #include <exception> #include <stdlib.h> using namespace std; void mon_gestionnaire(void) { cout << "Exception non gérée précédemment reçue !" << endl;
cout << "Je termine le programme proprement..."<< endl; exit(-1);
}
int lance_exception(void) { throw 2;} // Exécution de la procédure mon_gestionnaire // { throw (double) 2;} // Message : Exception de type double reçue
int main(void) { set_terminate(&mon_gestionnaire); // Masquage de l'appel à la fonction abort
try {lance_exception();} catch(double d) {cout << "Exception de type double reçue : " <<d << endl;} return 0;
}
4. EXCEPTION TYPEE
■ Exceptions explicites
La méthode throw spécifie la liste explicite des exceptions typées qu'une fonction peut lancer, après son en-tête, par sa liste (de types) d'arguments.
� Exemple
int fonction(void) throw(int, double, erreur) // Liste des types d'exception autorisés (int, double, erreur) {/* Corps de la fonction */}
■ Exceptions implicites
Une exception non explicitée provoque une erreur d'exécution puis l'appel de la méthode std::unexpected, dont le comportement par défaut (terminaison impropre du programme) est similaire à celui de la méthode std::terminate.
304 CHAPITRE XIV ───────────────────────────────────────────────────
Une exception non autorisée termine l'exécution avec la méthode std::terminate. Ce dernier est modifié par le masquage de la fonction appelée par défaut par la méthode std::set_unexpected, dont l'argument pointe sur la procédure adéquate.
Une exception différente peut être relancée : si elle est autorisée, le programme reprend son cours à partir du gestionnaire correspondant. Sinon, le programme est terminé par la génération d'une exception de type std::bad_exception, déclarée comme suit dans le fichier en-tête correspondant :
class bad_exception : public exception { public:
bad_exception(void) throw(); bad_exception(const bad_exception &) throw(); bad_exception &operator =(const bad_exception &) throw(); virtual ~bad_exception(void) throw(); virtual const char *what(void) const throw();
};
� Exemple 1
#include <iostream> // Gestion de la liste des exceptions autorisées #include <exception> using namespace std; void mon_gestionnaire(void) { cout << "exception illégale lancée." << endl;
cout << "Relance d'une exception de type int." << endl; throw 2;
}
int f() throw(int) { static int i = 0;
if(i==0) { cout << "f : throw 5 " << endl; throw 5 ; } else {cout << "f : throw 5.2 " << endl; throw 5.2; } // Génération d'un flottant non prise en compte i++;
}
int main(void) { set_unexpected(&mon_gestionnaire);
try {cout << "f(0) " << endl; f();} catch (int i) {cout << "Exception de type int reçue : " << i << endl;} cout << "Relance de la fonction f()" << endl << "f() " << endl; f();
}
// Résultat f(0) f : throw 5 Exception de type int reçue : 5 Relance de la fonction f() f() f : throw 5 Abandon
REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++ ───────────────────────────────────────────────────
305
� Exemple 2
#include <iostream> // Gestion d'une liste d'exceptions autorisées #include <exception> using namespace std; void mon_gestionnaire(void) { cout << "exception illégale lancée et captée ." << endl; cout << "Relance d'une exception de type int." << endl; throw 2; }
void f(void) throw(int) { static int i = 0; i++; if(i==1) { cout << "f("<< i << ") : throw 5 " << endl; throw 5 ; } else { cout << "f("<< i << ") : throw 5.2 " << endl; throw 5.2 ; } }
void g(void) throw(float) { cout << "g() : throw float " << endl; throw 3.45 ; }
int main(void) {set_unexpected(&mon_gestionnaire);
try {f(); g();} catch (...) { cout << "Exception reçue " << endl << "Lancement de la fonction g()" << endl; g(); }
}
// Résultat f(1) : throw 5 Exception reçue Lancement de la fonction g() g() : throw float exception illégale lancée et captée . Relance d'une exception de type int. Erreur de segmentation
5. EXCEPTION ET CONSTRUCTEUR
• La génération d'une exception nécessitant la construction d'un objet typé la caractérisant, ce dernier contient des informations typées sur la nature des erreurs. Il est donc licite qu'un constructeur puisse traiter une erreur de construction.
• La génération d'une exception par un constructeur interrompt la construction de l'objet donc l'appel de son destructeur, d'où la nécessité d'intégrer la destruction des objets partiellement initialisés suite à son lancement. Cette règle n'est valide que pour des objets alloués dynamiquement, le comportement de l'opérateur delete étant modifié quand l'exception est générée dans un constructeur.
306 CHAPITRE XIV ───────────────────────────────────────────────────
■ Syntaxe
• Un bloc try intégré au constructeur lui permet de gérer des exceptions.
• Les blocs catch suivent sa définition et libèrent les ressources allouées par le constructeur préalablement à la génération de l'exception.
■ Règles d'utilisation
• L'exception doit être captée par le programme qui a provoqué la création de l'objet.
• Un constructeur constitué d'un bloc catch puis d'un bloc try provoque la relance de l'exception, le bloc catch associé détruisant les objets partiellement construits.
◊ Ce traitement est différent de celui du bloc catch simple où les exceptions ne sont pas relancées après traitement sauf relance explicite par la méthode throw.
◊ Un programme déclarant des objets globaux dont le constructeur peut lancer une exception à leur initialisation risque de mal se terminer si aucun gestionnaire d'exception ne peut la capter lors de la relance par la méthode catch.
• Lorsqu'un objet est construit par une allocation dynamique, l'opérateur delete procède à sa désallocation et son appel au traitement de l'exception est inutile.
� Exemple
• La création dynamique d'un objet A provoque une erreur d'initialisation et la génération d'une exception, traitée dans le bloc catch qui suit le constructeur.
• L'opérateur delete est appelé explicitement, le destructeur de l'objet A jamais.
#include <iostream> #include <stdlib.h> using namespace std; class A { char *pBuffer; int *pData;
public: A() throw(int); // Prototype du constructeur ~A(){ cout << "A::~A()" << endl;} // Destructeur static void *operator new(size_t taille) {cout << "new()" << endl; return malloc(taille);} static void operator delete(void *p) { cout << "delete" << endl; free(p);}
};
A::A() throw(int) // Constructeur susceptible de lancer une exception try // Bloc try { pBuffer = NULL; pData = NULL; cout << "Début du constructeur" << endl;
pBuffer = new char [256]; cout << "Lancement de l'exception" << endl; throw 2; // Code inaccessible : pData = new int;
} // Fin du bloc try
catch (int) {cout << "Je fais le ménage..." << endl; delete[] pBuffer; delete pData; }
int main(void) { try {A *a = new A;}
catch (...) {cout << "Aïe, même pas mal !" << endl;} return 0;
}
REPRISE DES ERREURS D'EXECUTION EN LANGAGE C++ ───────────────────────────────────────────────────
307
6. EXCEPTION ET ALLOCATION MEMOIRE
La norme impose de lancer une exception lorsque l'opérateur new ou new[] provoque un défaut de mémoire. Ces opérateurs se comportent alors de deux manières : retour d'un pointeur nul ou appel d'un gestionnaire d'erreur. Trois cas possibles :
• correction de l'erreur et retour à l'opérateur qui réitère sa requête.
• aucune action : l'opérateur achève le programme ou retourne à la fonction appelante par génèration de l'exception std::bad_alloc.
• le gestionnaire d'erreur est masqué par la méthode std::set_new_handler qui retourne l'adresse du gestionnaire d'erreur précédent. Rappelons que la méthode std::set_new_handler est déclarée dans le fichier en-tête new.
7. HIERARCHIE DES EXCEPTIONS
Les exceptions pouvant être dérivées, un gestionnaire d'exceptions peut les traiter par traitement d'un objet d'une de ses classes de base.
Les erreurs sont classifiées selon une hiérarchie de classesd'exceptions, l'écriture de traitements paramétrés utilisant des objets d'un certain niveau de cette dernière.
� Exemple
#include <iostream> using namespace std; // Classe de base des exceptions lors de manipulations de fichiers class ExRuntimeError {}; class ExFileError : public ExRuntimeError {}; // Classes des erreurs de manipulation des fichiers : class ExInvalidName : public ExFileError {}; class ExEndOfFile : public ExFileError {}; class ExNoSpace : public ExFileError {}; class ExMediumFull : public ExNoSpace {}; class ExFileSizeMaxLimit : public ExNoSpace {}; void WriteData(const char *szFileName) // Entrée/sortie sur un fichier { if (szFileName == NULL) throw ExInvalidName(); // Exemple d'erreur
else // Traitement de la fonction { throw ExMediumFull();} // Lancement d'une exception
}
void Save(const char *szFileName) {try {WriteData(szFileName);} // Traitement d'un erreur spécifique catch (ExInvalidName &) {cout << "Impossible de faire la sauvegarde" << endl;} // Traitement de toutes les autres erreurs en groupe catch (ExFileError &) {cout << "Erreur d'entrée / sortie" << endl;}
}
int main(void) { Save(NULL); Save("data.dat"); return 0; }
308 CHAPITRE XIV ───────────────────────────────────────────────────
■ Règles de gestion de la liste des exceptions
• La liste des exceptions gérées dans une fonction n'étant pas explicitée dans sa signature, elle n'apparaît pas dans celle des surdéfinitions. Elle est définie après les déclarations des méthodes qualifiées const et doit être placée avant l'affectation nulle dans les déclarations des fonctions virtuelles pures.
• Les exceptions n'étant pas gérées par le mécanisme standard de gestion des erreurs des langages C et C++, les tests de validité d'une opération doivent être explicites. Lancer une exception de report du traitement en cas d'échec peut être nécessaire.
• La norme spécifie que les exceptions générées par la machine hôte du programme ne sont pas obligatoirement portables dans les autres implémentations.
■ Retour sur le mot clé try
Quand une classe fille hérite d'une ou plusieurs classes mère, l'appel des constructeurs des classes de base doit être effectué au travers de la méthode try et de son premier bloc. Rappelons que les constructeurs des classes de base peuvent également générer des exceptions.
Syntaxe Classe::Classe try : Base(arguments) [, Base(arguments) [...]] {} catch(...)
TYPES DYNAMIQUES
1. IDENTIFICATION DYNAMIQUE D'UN TYPE
■ Position du problème
L'interprétation dynamique du type d'un objet (dérivé ou de base) pouvant être ambiguë, les objets polymorphiques intègrent des informations relatives à ce dernier ce qui garantit la validité des transtypages lors de dérivations ou à l'appel de méthodes virtuelles.
1.1 La classe type_info Les informations du type dynamique sont définies dans l'espace de nommage std comme suit :
#include <typeinfo> class type_info { public:
virtual ~type_info(); bool operator ==(const type_info &rhs) const; bool operator !=(const type_info &rhs) const; bool before(const type_info &rhs) const; const char *name() const; private : type_info(const type_info &rhs); type_info &operator =(const type_info &rhs);
};
• Les objets de la classe ne peuvent être copiés les opérateurs d'affectation et le constructeur copie surdéfinis étant d'accès privé.
• Les opérateurs de comparaison surdéfinis testent l'égalité de deux objets de la classe et permettent de comparer le type de deux expressions.
• Les informations de type des objets sont codées sous forme de chaînes de caractères dont une représente le type, accessible par la méthode name.
• La méthode before permet de définir une hiérarchie de types dans une hiérarchie de classes. Son utilisation est délicate l'ordre entre les différentes classes pouvant dépendre de l'implémentation.
CHAPITRE XV
310 CHAPITRE XV ───────────────────────────────────────────────────
1.2 L'opérateur typeid L'opérateur typeid accède au type dynamique d'une expression.
Syntaxe const type_info & typeid(expression)
L'objet retourné caractérise :
• le type statique d'un objet non polymorphique,
• le type dynamique d'un objet polymorphique sur lequel opère un pointeur ou une référence sur une classe mère de sa classe effective.
� Exemple
#include <iostream.h> #include <typeinfo> class Base { public: virtual ~Base(void){}; };
class Derivee : public Base { public:
virtual ~Derivee(void){}; };
int main(void) { Derivee* pd = new Derivee;
Base* pb = pd; Base* pB=new Base; const type_info &t1=typeid(*pd); // T1 qualifie le type de *pd. const type_info &t2=typeid(*pb); // T2 qualifie le type de *pb. cout << t1.name() << endl << t2.name() << endl; cout << typeid(*pb).before(typeid(*pd)) << endl; cout << typeid(*pB).before(typeid(*pd)) << endl; return 0 ;
}
// Résultat Derivee Derivee 0 1
■ Remarques
• Les objets t1 et t2 qualifient le type Derivee et sont identiques.
• Le type dynamique t2 accède au type effectif de l'objet pointé par pb.
• Les informations de type effectif sont différentes sur un pointeur et un pointeur déréférencé. Quand ce dernier est nul, l'opérateur typeid génère une exception dont l'objet est une instance de la classe bad_typeid, définie comme suit :
class bad_typeid : public logic { public:
bad_typeid(const char * what_arg) : logic(what_arg) {return ;} void raise(void) {handle_raise(); throw *this;}
};
TYPES DYNAMIQUES ───────────────────────────────────────────────────
311
2. TRANSTYPAGES EN LANGAGE C++
2.1 Généralités sur les opérateurs de transtypage
■ Rappels
• Les règles de dérivation garantissent, à d'utilisation d'un pointeur sur une classe, l'existence et l'appartenance de l'objet à la classe de base du pointeur ce qui permet de convertir un pointeur sur un objet de base en un pointeur sur un objet dérivé.
• Il est interdit d'utiliser un pointeur sur un objet de base pour initialiser un pointeur sur un objet dérivé, la grammaire du langage imposant un transtypage explicite.
■ Opérateurs de transtypage
Le langage C++ fournit un jeu d'opérateurs garantissant ces règles : transtypage dynamique, statique, de constante, de réinterprétation des données.
2.2 Transtypage dynamique
■ Définition
Le transtypage dynamique, implémenté par l'opérateur dynamic_cast, convertit une expression en un pointeur ou une référence d'une classe.
Syntaxe dynamic_cast<type>(expression)
où type désigne le type cible de l'expression à convertir.
■ Règles d'utilisation
• L'opérateur dynamic_cast contrôle la validité du transtypage.
• Il ne supprime pas les qualifications de constance (opérateur const_cast).
• Il permet de qualifier constant un type complexe.
• Il n'opère pas sur les types de base du langage à l'exception du pointeur générique.
• Le transtypage d'un pointeur ou d'une référence d'une classe dérivée vers une classe de base est effectué sans vérification dynamique par l'opérateur dynamic_cast, cette opération étant toujours autorisée.
� Exemple
Les instructions :
B *pb; // La classe B est dérivée de A A *pA=dynamic_cast<A *>(pB);
sont équivalentes aux instructions :
B *pb; A *pA=pB;
312 CHAPITRE XV ───────────────────────────────────────────────────
• Tout autre opération de transtypage doit opérer sur un type polymorphique.
• Le transtypage d'un pointeur vers un pointeur générique retourne l'adresse de l'objet le plus dérivé.
• Le transtypage d'un pointeur ou d'une référence d'un objet dérivé vers un pointeur ou une référence d'un objet dérivé d'ordre supérieur est effectué après vérification du type dynamique.
◊ Le pointeur nul est retourné si le type cible est un pointeur.
◊ Une exception de type bad_cast (Cf. ci-dessous) est générée quand l'expression caractérise un objet ou une référence.
• Lors d'un transtypage, aucune ambiguïté n'est autorisée pendant la recherche dynamique du type dans les héritages multiples même si la coexistence de plusieurs objets de même type est possible. Cette restriction mise à part, l'opérateur dynamic_cast parcourt une hiérarchie de classesverticalement (conversion d'un pointeur d'objet dérivé vers celui d'un objet dérivé d'ordre supérieur) ou horizontalement (conversion d'un pointeur d'objet vers celui d'un objet frère dans la hiérarchie de classes).
• L'opérateur dynamic_cast convertit un pointeur d'une classe virtuelle vers une classe fille sans permettre l'accès aux objets inaccessibles de la classe de base.
� Exemple
struct A { virtual void f(void) { return ;}; }; struct B : public virtual A {}; struct C : public virtual A, public B{}; struct D {virtual void g(void) { return ;};}; struct E : public virtual B, public C, public D {}; int main(void) { E e; // E contient deux objets dérivés de la classe B, un de la classe A
// Les objets dérivés des classes C et D sont frères. A *pA=&e; // Dérivation légale car objet dérivé de A unique. // C *pC=(C *) pA; // Illégal car A classe de base virtuelle. C *pC=dynamic_cast<C *>(pA); // Légal. Transtypage dynamique vertical. D *pD=dynamic_cast<D *>(pC); // Légal. Transtypage dynamique horizontal. B *pB=dynamic_cast<B *>(pA); // Légal return 0 ;
}
■ La classe bad_cast
La classe bad_cast est définie comme suit dans l'en-tête typeinfo :
class bad_cast : public exception { public:
bad_cast(void) throw(); bad_cast(const bad_cast&) throw(); bad_cast &operator =(const bad_cast&) throw(); virtual ~bad_cast(void) throw(); virtual const char * what(void) const throw();
};
TYPES DYNAMIQUES ───────────────────────────────────────────────────
313
2.3 Transtypage statique
Syntaxe L'opérateur static _cast effectue le transtypage statique d'objet non polymorphique.
static _cast<type>(expression)
où type désigne le type cible de l'expression valide à convertir.
Synopsis Construction d'un objet temporaire du type indiqué initialisé avec la valeur retournée par l'opérateur.
■ Règles
• Contrairement à dynamic_cast, l'opérateur static _cast permet d'effectuer les conversions entre des types autres que ceux des classes définies par l'utilisateur, sans vérification de la validité de la conversion.
• Quand l'expression est invalide, le transtypage ne peut être effectué qu'entre classe dérivée et classe de base.
• Toute valeur d'une expression peut être supprimée par conversion vers le type void.
• Un objet peut être requalifié const ou volatile.
• L'opérateur static _cast ne permet pas de supprimer la qualification const.
2.4 Transtypage de constance et de volatilité La suppression des qualificatifs const et volatile s'effectue avec l'opérateur const_cast
Syntaxe const_cast<type>(expression)
où type désigne le type cible de l'expression à convertir.
Synopsis • L'opérateur const_cast opère essentiellement sur une référence ou un pointeur.
• Le type cible à moins de contraintes que le type source qualifié const ou volatile.
• Il n'effectue pas les conversions effectuées par les autres opérateurs
• Il contrôle la validité du transtypage d'une référence en la convertissant en pointeur et en vérifiant que les attributs sont const ou volatile.
• Il n'opère pas sur des pointeurs de fonction.
314 CHAPITRE XV ───────────────────────────────────────────────────
2.5 Opérateur de réinterprétation des données L'opérateur de transtypage le plus délicat est reinterpret_cast.
Syntaxe reinterpret_cast<type>(expression)
où type désigne le type cible de l'expression à convertir.
Synopsis • L'opérateur reinterpret_cast ne permet pas de supprimer les qualificatifs const et
volatile.
• Il doit être symétrique : l'interprétation d'un type T1 comme type T2 puis la celle du résultat en type T1 doit redonner le type de l'objet initial.
• La conversion du type est effectuée sans vérification de la validité de l'opération.
� Exemple
Les instructions :
double f=2.3; int i=1, m; int & j=&m; j=reinterpret_cast<int &>(i); j++;
sont équivalentes aux instructions :
double f=2.3; int i=1; *(( int *) &f)=i;
ANNEXES
1. PROGRAMMATION OBJET ET APPEL SYSTEME
1.1 Appel système L'appel système est l'interface programmatique permettant à un processus utilisateur d'accéder aux ressources de la machine en exécutant en mode système une fonction du système d'exploitation qu'il protège donc par un mécanisme d'encapsulation.
Du point de vue de l'utilisateur, un appel système est similaire à l'appel d'une fonction de la bibliothèque C qui provoque un changement du mode d'exécution. L'interruption générée par l'appel système est traitée par le noyau du système d'exploitation dans le contexte du processus utilisateur appelant en trois phases :
Transfert des arguments et changement de contexte Transfert des arguments d'appel depuis la pile utilisateur vers la pile système ce qui permet au noyau de contrôler la validité de l'appel dans son espace propre.
Choix de l'appel et exécution Détermination de l'appel, contrôle de la validité de ses arguments, exécution.
Fin d'exécution et retour en mode utilisateur Deux situations :
• succès : le code de retour est nul et les informations sont transférées de la pile système à la pile utilisateur.
• échec : le code de retour vaut -1 et la variable errno retourne le code de l'erreur.
1.2 Cycle de vie d'un fichier
■ Définitions
Les définitions et opérations de base sur les fichiers sont les suivantes :
• Le fichier doit être créé, préalablement à toute opération (création).
• Lors de son utilisation, il est nécessaire d'initialiser certaines données (ouverture).
• Il est ensuite possible de lire ou d'écrire des données (lecture et écriture).
• Après utilisation, le fichier doit être refermé (fermeture).
• La suppression du fichier réalise sa destruction.
316 ANNEXES ───────────────────────────────────────────────────
■ Appels systèmes de gestion des fichiers
Les principaux appels système associés aux opérations précédentes sont :
creat création du fichier et gestion de ses droits d'accès open ouverture du fichier (lecture, écriture, mis à jour) write écriture read lecture lseek positionnement du pointeur close fermeture d'un fichier link création d'un lien (nom synonyme) unlink suppression d'un lien chmod modification des droits d'accès stat informations sur l'état d'un fichier access contrôle des droits d'accès.
Les appels système creat et open utilisent le nom externe du fichier (chemin d'accès ou pathname). Tous les autres utilisent son nom interne (descripteur du fichier).
La suppression d'un fichier est effective quand tous ses chemins d'accès sont détruits.
Le cycle de vie d'un fichier et les appels système correspondants sont résumés dans le schéma suivant :
open
close
creat link
write
read
lseek
unlink
1.3 Opérations sur les fichiers
■ Descripteur de fichier et ouverture
Un descripteur de fichier est défini à sa création ou à son ouverture permettant à l'utilisateur d'y accéder ensuite, après contrôle des droits d'accès.
Synopsis #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(char *chemin , int mode_ouverture, mode_t droits);
ANNEXE ───────────────────────────────────────────────────
317
Description chemin est le nom externe du fichier, de type quelconque. Pour les répertoire, seule l'ouverture en lecture est possible et il est recommandé d'utiliser l'appel opendir.
L'appel open peut être bloquant dans les situations suivantes :
• ouverture d'un tube nommé en écriture en l'absence de lecteur ou en lecture en absence de rédacteur,
• ouverture d'un terminal non prêt.
L'argument mode_ouverture permet de paramétrer le comportement des opérations de lecture/écriture ultérieures. Il est construit par disjonction bit à bit de constantes symboliques définies dans le fichier <fcntl.h> dont la fonction est de permettre l'exécution d'une opération spécifique ou de modifier le comportement de l'opération. Cette disjonction comporte exactement une des constantes suivantes
O_RDONLY si le fichier chargé est accessible en lecture seulement, O_WRONLY si le fichier chargé est accessible en écriture seulement, O_RDWR si le fichier chargé est accessible en écriture et en lecture.
Les constantes essentielles retenues par les normes POSIX et SYSTEM V sont :
O_TRUNC si le fichier existe, sa taille devient nulle. O_CREAT création d'un fichier ordinaire d'une taille vide s'il n'existe pas.
Un paramètre supplémentaire est alors nécessaire pour définir ses droits ultérieurs.
O_EXCL si le drapeau O_CREAT est positionné et que le fichier existe, retourne une erreur (-1)
O_NOCTTY le terminal courant ne sera pas le terminal de contrôle du processus,
O_APPEND écriture en mode ajout à la fin du fichier, O_NONBLOCK modifie l'opération d'ouverture bloquante en renvoyant un
message d'erreur avec la variable errno contenant la constante EAGAIN sauf dans le cas d'un tube nommé.
O_SYNC les opérations d'écriture en mode bloc sont synchrones (vidage du cache),
O_NDELAY les opérations normalement bloquantes ne le sont plus et renvoient un message d'erreur.
■ Création : appel creat
La création d'un fichier peut être réalisée avec l'appel système creat.
Synopsis #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int creat(const char *chemin, mode_t droits_acces);
Description chemin nom du fichier, droits_acces droits d'accès du fichier.
318 ANNEXES ───────────────────────────────────────────────────
■ Lecture et écriture
Les opérations de lecture et d'écriture sont réalisées, après l'ouverture du fichier, par les appels système read et write.
Synopsis size_t read(int fd, void* buf, size_t nbyte); size_t write(int fd, void* buf, size_t nbyte);
avec
fd le descripteur de fichier,
buf l'adresse du premier octet en mémoire de la zone où sera exécutée l'opération de lecture ou d'écriture,
nbyte le nombre de bytes à transférer en lecture ou en écriture.
Cas particuliers nbyte= 0 fin du fichier rencontrée. nbyte= -1 erreur en lecture ou en écriture.
■ Algorithme de lecture
Début retour -1 si erreur de paramètre d'appel s'il n'existe pas de verrou exclusif impératif en lecture alors
si la fin du fichier n'est pas atteinte, alors lecture depuis la position courante de nbyte caractères au plus (fin de fichier éventuellement atteinte) et retour du nombre d'octets effectivement lus sinon retour 0
sinon si le mode de lecture est bloquant (drapeaux O_NOBLOCK et O_NDELAY non positionnés), alors blocage du processus jusqu'à la suppression du verrou
sinon aucun caractère n'est lu et errno vaut EAGAIN is
Fin
■ Algorithme d'écriture
Début
retour -1 si erreur de paramètre d'appel s'il n'existe pas de verrou, exclusif, impératif ou partagé en écriture alors
écriture (asynchrone par défaut, synchrone avec le drapeau O_SYNC) depuis la position courante ou depuis la fin du fichier (drapeau O_APPEND) de nbyte caractères au plus et retour du nombre d'octets effectivement écrits.
sinon si aucun des drapeaux O_NOBLOCK et O_NDELAY n'est positionné
alors le processus est bloqué jusqu'à la suppression du verrou sinon aucun caractère n'est écrit et errno vaut EAGAIN
is Fin
ANNEXE ───────────────────────────────────────────────────
319
� Exemple 1
#define BUFSIZE 512 #include <stdio.h> int main(void) /* écriture sur la sortie standard du caractère saisi sur l'entrée standard */ { char buf[BUFSIZE];
int n; while (( n = read(0,buf,BUFSIZE)) >0) write(1,buf,n);
}
� Exemple 2 : écriture simplifiée de la commande UNIX de copie d'un fichier cp
#define BUFSIZE 1024 #define PMODE 644 #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> int main (int argc, char *argv[]) {int f1,f2,n;
char buf[BUFSIZE]; /* prototype */ void erreur( char *, char *); int open(char *, int, mode_t); if(argc != 3) erreur ("Usage : cp depuis vers", (char *) NULL); if((f1=open(argv[1],O_RDONLY,444))==-1)erreur("cp: ouverture impossible %s",argv[1]); if((f2 = creat(argv[2],PMODE)) == -1) erreur("cp : création impossible %s",argv[2]); while ((n = read(f1, buf, BUFSIZE)) > 0) { if (n==-1) exit(-1);
if (write(f2, buf, n) != n) erreur("cp : erreur en écriture", (char *) NULL); }
return(1); }
void erreur(char *s1, char *s2) {fprintf(stderr,s1,s2); printf("\n"); exit(-1); }
� Exemple 3
/* Utilisation des appels système read, write pour écrire une fonction getchar bufférisee */ #define BUFSIZE 512 #define CMASK 0377 struct _iobuf { char *_ptr; /* pointeur sur l'octet courant */
int _rcnt; /* nombre d'octets en lecture dans le tampon */ int _wcnt; /* nombre d'octets en écriture dans le tampon */ char *_base; /* adresse de base du début du tampon */ char _flag; /* mode d'ouverture du fichier */ char _file; /* numéro du fichier */ char _cbuff; /* tampon d'un caractère */ char _pad; /* tampon pour le nombre pair de bytes */
}
320 ANNEXES ───────────────────────────────────────────────────
#define putc(c,p) \ (--(p)->_wcnt>=0?((int)(* (p)->_ptr++= (c))) : _flsbf((c),p)) #define putchar(c) putc(c,stdout) int main(void) { int c;
while ((c = getchar()) != EOF) putchar(c); exit(0);
}
getchar() { static char buf [BUFSIZE ];
static char *bufp = buf; static int n = 0; if (n == 0)
{n = read(0,buf,BUFSIZE); bufp = buf; } return((--n >= 0) ? *bufp++ & CMASK : EOF); }
■ Fermeture
L'appel système close supprime la connexion entre un fichier ouvert et son descripteur assurant ainsi la fermeture des fichiers. Bien entendu, le fichier est réutilisable.
Synopsis #include <unistd.h> int close(int fd);
■ Accès aléatoire et déplacement dans un fichier
L'accès à un octet d'un fichier ordinaire est séquentiel (appel système read ou write), ou aléatoire avec l'appel système lseek.
Synopsis off_t int lseek(int fd, off_t offset , int origine);
Description La variable offset modifie le pointeur sur l'octet courant du fichier dont le descripteur est fd; la nouvelle position (en octets) est calculée de la façon suivante :
origine = 0 déplacement absolu et croissant par rapport à l'origine du fichier, origine = 1 déplacement relatif et croissant par rapport à la position courante, origine = 2 déplacement absolu et croissant par rapport à la fin du fichier (ajout).
� Exemple
lseek(fd,0L,2);
ANNEXE ───────────────────────────────────────────────────
321
2. LA GESTION OBJET DES ENTREES/SORTIES
2.1 L'objet fichier Sur les systèmes d'exploitation actuels, l'utilisateur a une vision uniforme des entrées sorties que celles-ci s'effectuent sur un fichier local ou distant, un périphérique, local ou distant, un IPC (outils de communications interprocessus), etc.
Un fichier représente donc un objet stocké localement ou à distance dont la sémantique d'utilisation impose de généraliser les opérations traditionnelles sur les fichiers locaux aux réseaux.
Les systèmes Unix et Windows distinguent sept classes de fichiers :
■ Fichier ordinaire
Un fichier ordinaire (ou encore normal, régulier, banalisé) est une suite finie d'octets sans organisation particulière.
■ Répertoire
Un répertoire (ou dossier) est un conteneur d'autres fichiers, d'un type quelconque.
La racine de l'arborescence est le conteneur racine qui permet d'associer le nom du fichier et sa position dans l'arborescence.
■ Fichiers standard
Trois fichiers utilisés pour la saisie et l'affichage, appelés fichiers standard sont ouverts au début de toute session de travail. Ce sont:
• le fichier standard d'entrée: le clavier,
• le fichier standard de sortie: l'écran,
• le fichier standard d'affichage des messages d'erreurs système : l'écran.
e n t r é e s t a n d a r d C o m m a n d e
s o r t i e s t a n d a r d
m e s s a g e s d ' e r r e u r s t a n d a r d
Il ne faut pas confondre les fichiers ordinaires et les fichiers standard.
dir1 fichier1, fichier2
dir2 fichier3
dir3 fichier4
322 ANNEXES ───────────────────────────────────────────────────
■ Fichier spécial
Un fichier spécial représente un périphérique fonctionnant dans un des modes suivants :
• mode b (bloc): supports magnétiques de stockage (disque, bande, etc.) dont les entrées/sorties sont gérées via le cache,
• mode c (caractère): tous les périphériques y compris les pécédents.
■ Tube nommé
Les tubes nommés (named pipe) permettent de gérer les accès concurrents.
■ Lien matériel
Le descripteur d'un fichier est appelé inode.
Un lien matériel est une association entre un chemin d'accès à un fichier donné dans un système de fichiers et son descripteur.
Un autre lien matériel sur ce dernier définit un deuxième chemin d'accès à ce fichier sans en faire de copie effective car il n'existe qu'une unique occurrence du fichier même si différents chemins d'accès permettent d'y accéder.
Le fichier est supprimé si le nombre de liens devient nul.
■ Lien symbolique
Un deuxième type de liens est défini: le lien symbolique. Ce dernier définit un chemin d'accès synonyme de celui du fichier (ordinaire ou répertoire). C'est une entrée d'un répertoire quelconque de l'arborescence décrite par inode contenant un nouveau chemin d'accès (absolu ou relatif) d'un inode existant (banalisé ou répertoire) d'un système de fichiers quelconque de l'arborescence.
On en déduit la représentation suivante :
/usr/local/toto
/usr/local/tutu
inode 2 liens physiques
inode1
inode 2
/home/prof/toto
/usr/local/toto
lien physique
lien symbolique
ANNEXE ───────────────────────────────────────────────────
323
Le lien symbolique généralise la notion de lien matériel à un système de fichiers quelconque. Toutefois, contrairement à un lien matériel, le déplacement ou la suppression d'un fichier ne modifie pas un lien symbolique sur ce fichier qui ne pointe sur rien si le fichier n'est pas remplacé ou qui pointe sur le nouveau fichier dont l'inode est identique au fichier antérieur.
■ Liens, réseaux locaux, Internet
Le protocole NFS a permis de généraliser la notion de lien symbolique sur des systèmes de fichiers distants ce qui est d'un intérêt considérable pour la transparence d'un fichier dans un réseau.
Le lien hypertexte est un lien symbolique sur le réseau Internet
■ Création et suppression
La commande ln permet de créer des liens et leur suppression est effectuée à partir de la commande usuelle de suppression d'un fichier rm.
2.2 Gestion des flux
■ Entrées/sorties de haut niveau et bibliothèque C
Les opérations d'entrées/sorties de haut niveau assurent le transfert des données entre le processus utilisateur et le noyau. Elles sont implémentées dans la bibliothèque standard du langage C sous forme de fonctions.
■ Entrées/sorties de bas niveau et appels système
Les opérations d'entrées/sorties de bas niveau assurent le transfert des données entre le noyau et le support physique. Elles sont implémentées sous la forme d'appels système.
324 ANNEXES ───────────────────────────────────────────────────
■ Entrées/sorties en mode bloc
Elles gèrent la lecture et l'écriture dans le cache d'une lecture anticipé (read ahead) et d'une écriture différée (delayed write), réalisées au niveau supérieur par les primitives de haut niveau et au niveau inférieur par les pilotes de périphérique en mode bloc. L'accès au périphérique est indépendant de l'application.
■ Entrées/sorties en mode caractère
L'application gérant elle-même le tampon d'entrées/sorties accède directement au fichier sans utiliser de cache et les pilotes gèrent les données en mode caractère.
■ Entrées/sorties en mode cru (entrées/sorties en mode raw)
L'application gère ses tampons et l'entrée/sortie, usuellement en mode bloc, n'utilise pas le cache.
■ Prise de communication (socket)
L'accès au réseau est réalisé via le descripteur de la prise de communication utilisée, accessibles comme les autres fichiers par les appels système adéquats.
2.3 Attributs de propriété et sécurité
■ Propriétaire
L'utilisateur propriétaire est celui auquel le fichier est rattaché et dont il peut modifier les caractéristiques de sécurité et d'accès. Son identité peut être modifiée par l'administrateur ou le (futur ex) propriétaire du fichier. C'est un acte irréversible pour l'ex propriétaire.
■ Groupe
• Un groupe est un ensemble d'utilisateurs dotés de privilèges communs.
• Un utilisateur peut appartenir à plusieurs groupes.
• Le groupe d'un fichier est modifiable (propriétaire ou administrateur).
• Le groupe de travail d'une session est modifiable par l'utilisateur.
■ Drapeaux de sécurité
Les protections d'accès d'un fichier sont définies par les drapeaux de sécurité, répartis en trois champs: un pour l'utilisateur propriétaire, un pour les membres de son groupe et un pour les autres utilisateurs, chacun constitué par trois drapeaux d'accès rwx. Leur signification varie selon le type du fichier (ordinaire ou répertoire).
r w x fichier ordinaire lecture écriture script suppression fichier exécutable modification répertoire listable modification traversable création, ajout suppression
ANNEXE ───────────────────────────────────────────────────
325
� Exemple
Un fichier ordinaire avec les droits rwxr-x—x peut être lu, écrit et exécuté respectivement par le propriétaire, lu et exécuté par les membres de son groupe et exécuté par les autres utilisateurs.
■ La commande chmod
Synopsis
chmod modif_autorisations_courantes fichier(s)
Cette commande de modification des attributs de sécurité d'un fichier est utilisée sous forme symbolique ou octale.
Utilisateur Opérateur Permissions u propriétaire + ajouter r g groupe - supprimer w o autres (others) = assigner x
� Exemple
chmod go-rx archive chmod g+w donnees
Les droits d'accès d'un fichier sont définis par l'interprétation (par 3) des 9 drapeaux en octal.
LECTURE = 4 ECRlTURE = 2 EXECUTlON = 1
644 -rw-r—-r-- droits par défaut pour fichier 755 drwxr-xr-x droits par défaut pour répertoire
■ Attributs par défaut
Le masque de création des drapeaux de sécurité est utilisé à la création des fichiers et des répertoires pour définir leurs attributs par défaut.
Les droits par défaut des fichiers ordinaires sont définis selon la règle:
droits_par_defaut=non(masque) et 666
Les droits par défaut des répertoires sont définis selon la règle:
droits_par_defaut=non(masque)
L'exécution de la commande umask indique la valeur par défaut du masque de création courant, souvent 022. Ainsi, avec ce masque, on obtient:
Fichiers ordinaires rw-r—r— Répertoires rwxr-xr-x
ce qui constitue des droits d'accès raisonnables.
Le masque de protection peut-être défini par l'administrateur ou redéfini par l'utilisateur dans son profil de démarrage. Il est fondamental pour l'administrateur de s'assurer d'une valeur par défaut acceptable du masque pour l'ensemble des utilisateurs (génération système, fichier de démarrage d'une session, etc.).
326 ANNEXES ───────────────────────────────────────────────────
2.4 Usurpation d'identité Le fichier contenant la liste des utilisateurs et leur mot de passe crypté appartient à l'administrateur avec les droits rw-------. Un utilisateur ne peut donc accéder directement à ce fichier, ni en lecture, ni en écriture.
Par contre, il peut y modifier son mot de passe par usurpation temporaire de l'identité du propriétaire de la commande réservée à cet usage dont le propriétaire est l'administrateur et dont le lecteur pourra constater que les droits sont rwsr-xr-x.
■ Le bit suid sur un fichier ordinaire
Le bit s est appelé bit sUID (set user id). Son positionnement autorise un utilisateur non propriétaire du fichier à usurper temporairement l'identité de son propriétaire pour l'exécuter avec les droits de ce dernier.
Le propriétaire réel d'un processus en cours d'exécution est l'utilisateur qui a lancé le processus, le propriétaire effectif étant le propriétaire du fichier exécutable qui en a défini les droits d'accès, autorisant, par le bit s d'autres utilisateurs à l'exécuter.
Synopsis chmod 4555 fichier_exécutable
Avec la commande précédente, le fichier exécutable a les droits r-sr-xr-x
■ Le bit suid sur un répertoire
Tout fichier créé dans ce répertoire hérite du groupe du répertoire si ce dernier a les droits drwxrwsr-x.
■ Le bit Sgid sur un fichier ordinaire
Le bit S, appelé bit SGID, est utilisé avec les fichiers exécutables d'un groupe, permettant à un utilisateur d'exécuter la commande avec les privilèges de ce dernier sans en être membre.
Synopsis chmod 2555 fichier_exécutable
Avec la commande précédente, le fichier exécutable a les droits r-xr-Sr-x
■ Précautions d'utilisation
Tous les fichiers avec l'un des bits s ou S sont sensibles car ils peuvent devenir des failles dans la sécurité du système. Il faut donc en minimiser l'utilisation et éviter que leur propriétaire ne soit l'administrateur par la création d'utilisateur dédié avec des droits limités par le bit suid.
Un fichier exécutable avec le bit suid ne doit pas être accessible en lecture, un pirate pouvant l'utiliser à l'insu de son propriétaire.
2.5 Protection des fichiers d'accès public Le bit de gluance (sticky bit), ou encore le bit t, a deux utilisations distinctes : l'une sur les fichiers et l'autre sur les répertoires.
ANNEXE ───────────────────────────────────────────────────
327
■ Fichier
Il permet le maintien de l'image du fichier en mémoire à la fin de son exécution, d'où son appellation. Les plus fréquents sont les éditeurs et les outils de production de logiciels : compilateurs, assembleurs, éditeur de liens.
■ Répertoire
Le bit t utilisé sur des répertoires d'accès publics (rwxrwxrwx) empêche un utilisateur de détruire des fichiers dont il n'est pas propriétaire.
■ Synopsis
chmod 1777 répertoire
Avec la commande précédente, le répertoire a les droits drwxrwxrwt
2.6 Listes de Contrôles d'Accès aux objets Les systèmes sécurisés actuels permettent de définir des listes de contrôle d'accès (LCA) ou ACL (Access Control List).
La norme POSIX P1006.3 définit les LCA comme un moyen formel d'exprimer des opérations sur des objets, selon l'identité de l'entité émettrice de la requête.
Une LCA est une entrée dans un fichier de la forme :
:type:principal:permissions
Le champ principal est le nom de l'entité à qui appliquer les droits.
Le champ type spécifie l'interprétation du champ principal (le propriétaire du fichier, un autre utilisateur (local ou distant), un groupe (local ou distant), etc).
Le champ permission est constitué des droits classiques RWX complété par les drapeaux CID, avec les interprétations :
C habilitation à modifier les ACL, I droits d'insertion de fichier dans un répertoire, D droits de destruction de fichier dans un répertoire.
� Exemples
root:/etc:RWXCID
Sur AIX (IBM), les ACL comportent les informations suivantes :
• attributs de sécurité spéciaux (bits s, S, t)
• permissions de base constituant les attributs traditionnels,
• permissions étendues, décrites sous la forme d'ACE (Access Control Entry), qui, pour chaque instance décrite, suivent le format :
opération type_d'accès [[info_utilisateur] [info_groupe]
328 ANNEXES ───────────────────────────────────────────────────
Opérations Trois types d'opérations sont supportés :
deny interdiction du type d'accès spécifié permit autorisation du type d'accès spécifié specify spécification du type d'accès
Types d'accès • L'utilisateur ou le groupe auquel s'appliquent les droits décrits est précédé du
suffixe u: ou g:.
• Une entrée est crée pour chaque utilisateur ou groupe.
• Plusieurs utilisateurs ou groupes peuvent être combinés dans une entrée.
� Exemple
attributes base permissions owner(philipp): rwx group(root): r-x other: r-- extended permissions: enabled deny rwx g:staff permit rwx u:root, g:staff permit r-x g:sysadm
■ Commandes associées
aclget affichage d'une ACL, aclput définition d'une ACL, acledit modification d'une ACL.
■ Test des droits d'accès
Synopsis #include <unistd.h> int access (char *chemin, int type_access);
Description Le paramètre est un disjonction logique (opérateur sur les bits |) des constantes :
R_OK accessible en lecture, W_OK accessible en écriture, X_OK accessible en exécution, F_OK test d'existence du fichier.
ANNEXE ───────────────────────────────────────────────────
329
2.7 Appel système de modification des attributs
■ Modification des droits d'accès
Synopsis #include <sys/stat.h> int chmod(const char* ref, mode_t mode_acces); int fchmod(int fd, mode_t mode_acces);
Description Le fichier référencé, par son descripteur ou par son nom reçoit les attributs définis par la variable mode_acces. Le propriétaire effectif du fichier doit être également le propriétaire du fichier ou l'administrateur.
� Exemple et exercice
Ecrire un programme qui illustre la différence des droits d'accès entre le propriétaire réel et le propriétaire effectif et qui modifie le propriétaire et le groupe d'un fichier.
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> char *chemin="essai"; void acces_autorise(void) { if(access(chemin,W_OK)) printf("accès en écriture sur %s interdit au propriétaire réel\n", chemin); else printf("accès en écriture sur %s autorisé au propriétaire réel\n",chemin); } int main(void) {mode_t mode; if(access(chemin,F_OK)) {printf("fichier %s inexistant\n",chemin); exit(0);} system("ls -l essai"); acces_autorise(); if(chown(chemin,200,100)==-1) perror("chown"); acces_autorise(); system("ls -l essai"); }
// Résultat -rwxr-xr-x 1 root other 21100 Mai 03 14:03 essai -rwxr-xr-x 1 user1 dba 21100 Mai 03 14:03 essai
■ Modification du propriétaire
Synopsis #include <sys/stat.h> int chown(const char* ref, uid_t uid, gid_t gid); int fchown(int fd, uid_t uid, gid_t gid);
Description Modification du propriétaire ou du groupe du fichier et suppression du bit s ou S.
330 ANNEXES ───────────────────────────────────────────────────
■ Création d'un inode associé à un fichier spécial
L'appel système à utiliser est variable selon le type du fichier créé.
Fichier ordinaire fichier spécial tube lien symbolique open mknod pipe symlink creat mkfifo
Synopsis #include <sys/stat.h> int mknod(const char *chemin, mode_t droits, dev_t majeur/mineur);
3. QUELQUES APPELS SYSTEME
Les appels systèmes peuvent différer selon les implémentations mais le respect de la norme doit garantir la portabilité des applications. Voici quelques appels systèmes :
■ Gestion des identificateurs
getpid identification du processus courant, getppid identification du processus père du processus actif, getuid identification du propriétaire du processus actif, geteuid identification du propriétaire effectif du processus actif, setuid modification du propriétaire d'un processus actif, seteuid modification du propriétaire effectif d'un processus actif.
■ Gestion de fichiers (compléments)
dup création d'un descripteur de fichier clone, pipe création d'un tube, chdir définition du répertoire de travail, chroot changement de la racine du système de fichiers, chown changement du propriétaire du fichier, stat, lstat, fstat information sur le fichier, link création d'un lien matériel, unlink suppression d'un lien matériel, mount montage d'un volume, umount démontage d'un volume, fcntl modification du comportement des appels systèmes,
■ Gestion et synchronisation des processus
fork création d'un processus, exec* remplacement du code d'un processus, exit terminaison d'un processus. wait attente du signal de fin d'un fils, pause attente d'un signal quelconque, signal action à exécuter à réception d'un signal, kill émission d'un signal à un processus.
ANNEXE ───────────────────────────────────────────────────
331
■ IPC
shmget création d'une zone de mémoire partagée, shmat attachement à un processus d'une zone de mémoire partagée, shmdt détachement d'un processus d'une zone de mémoire partagée, shmctl caractéristiques et destruction d'une zone de mémoire partagée. msgget création d'une file de messages, msgsnd émission d'un message vers une file, msgrcv lecture d'un message d'une file, msgctl opérations sur les files de messages. semget création d'un tableau de sémaphores, semop opérations sur un tableau de sémaphores, semctl opérations sur les files de messages.
4. GENERATION D'APPLICATIONS ET COMMANDE MAKE
On dispose de générateurs d'applications dont le code (source, compilé ou édité) peut provenir de différents langages de telle sorte que l'on puisse modifier un fichier (source, bibliothèque,...) sans avoir à recompiler l'ensemble. Ainsi, l'utilitaire sccs gère les différentes versions d'un même programme et l'utilitaire make, outil de développement et d'administration, génère des exécutables à partir de fichiers sources ou objets modifiés, en prenant en compte les aspects suivants :
• Graphe des dépendances des fichiers (sources, objets, exécutables) assurant la description des dépendances entre les différents fichiers utilisés,
• Prise en compte automatique de toute modification d'une dépendance,
• Description explicite ou implicite des actions de génération d'un applicatif.
■ Définitions
Le fichier cible, (ou plus simplement la cible) est le nom du fichier à générer.
La (les) dépendance(s) est (sont) le(s) fichier(s) nécessaire(s) (source(s) et/ou objet(s)) pour générer la cible.
L'action est la description des règles de production de la cible à partir de ses dépendances.
Le fichier (makefile ou Makefile) contient la description (explicite ou implicite) des règles de production de cibles à partir de leurs dépendances. On peut y définir des variables et des macros et l'algorithme utilisé garantit que seules sont exécutées les actions nécessaires.
■ Syntaxe d'écriture
cible : dépendance(s) action
Le caractère de tabulation est nécessaire devant la description de l'action.
332 ANNEXES ───────────────────────────────────────────────────
� Exemple
Soient les fichiers : /* fichier principal.c */ #include <stdio.h> void main(void) { float x,y;
float circonf(float), surface(float); x = circonf((float)100); y = surface((float)100); printf("circonférence = %6.4f , surface = %6.4f\n",x,y);
}
/* fichier pi.h */ #define PI 3.1416
/* fichier circonf.c */ #include "pi.h" float circonf( float r) { return((float) 2*PI*r);}
/* fichier surface.c */ #include "pi.h" float surface(float r) {return((float)PI*r*r);}
En appliquant la syntaxe d'écriture définie ci-dessus, on obtient :
circonf.o : circonf.c pi.h cc -c circonf.c
■ Règle de déclenchement
L'action a lieu si la cible n'existe pas ou si sa date de dernière modification est plus ancienne que celle d'une dépendance.
� Exemple
Avec les fichiers précédents, le graphe des dépendances est le suivant :
# Fichier Makefile (version 1) #cible résultat resultat : principal.o circonf.o surface.o #action cc principal.o circonf.o surface.o -o resultat
#cible principal.o principal.o : principal.c cc -c principal.c
circonf.o : circonf.c pi.h cc - c circonf.c
surface.o : surface.c pi.h cc -c surface.c
La scrutation est verticale, du bas vers le haut.
Si le fichier principal.o est supprimé après la création du fichier resultat, l'exécution de la commande make ne provoque que la création des cibles principal.o et resultat.
ANNEXE ───────────────────────────────────────────────────
333
■ Règles implicites
L'algorithme utilise des règles implicites. Ainsi, tout fichier objet ou exécutable est obtenu à partir d'un fichier source.c, ce qui s'écrit explicitement par :
* : *.o cc -o * *.o *.o : *.c cc -c *.c
En l'absence de fichier makefile, la commande
make bidon
lance la compilation et l'édition de lien du fichier bidon.c et affiche le message :
cc -o bidon bidon.c
Le fichier contenant la description des règles de production explicites est le fichier Makefile du répertoire courant (défaut) ou un fichier quelconque (option f).
� Exemple
Avec les règles implicites, le fichier Makefile se simplifie :
# fichier Makefile (version 2) resultat : principal.o circonf.o surface.o cc principal.o circonf.o surface.o -o resultat circonf.o : circonf.c pi.h cc - c circonf.c surface.o : surface.c pi.h cc -c surface.c
On peut croire que le fichier pi.h est une dépendance implicite puisque il est inclus par le préprocesseur dans les dépendances implicites circonf.c et surface.c ce qui est faux. Il suffit de modifier le fichier pi.h et le fichier Makefile de la façon suivante :
# fichier Makefile (version 2.2 erronée) resultat : principal.o circonf.o surface.o cc principal.o circonf.o surface.o -o resultat circonf.o : circonf.c cc - c circonf.c surface.o : surface.c cc -c surface.c
L'exécution de la commande make ne "voit pas" la modification du fichier pi.h. On peut se poser la même question avec les cibles circonf.o et surface.o avec les dépendances implicites circonf.c et surface.c. Soit le fichier Makefile :
# fichier Makefile (version 2.3) resultat : principal.o circonf.o surface.o cc principal.o circonf.o surface.o -o resultat circonf.o : pi.h cc - c circonf.c surface.o : pi.h cc -c surface.c
334 ANNEXES ───────────────────────────────────────────────────
Une modification d'une dépendance implicite (circonf.o) ou explicite (pi.h) de la cible circonf.c est détectée par la commande et les deux dernières actions sont conformes aux règles implicites. D'où :
# fichier Makefile (version 2.4) resultat : principal.o circonf.o surface.o cc principal.o circonf.o surface.o -o resultat circonf.o : pi.h surface.o : pi.h
■ Variables internes du fichier Makefile
Une variable interne peut être définie selon la syntaxe :
OBJ = principal.o circonf.o surface.o
Son contenu est obtenu par les caractères $() selon la syntaxe $(OBJ).
� Exemple
# fichier Makefile (version 3) : variables internes OBJ et EXEC OBJ = principal.o circonf.o surface.o EXEC = resultat $(EXEC) : $(OBJ) cc $(OBJ) -o $(EXEC) circonf.o : circonf.c pi.h surface.o : surface.c pi.h
■ Variable interne initialisée à l'appel
Une variable interne peut être initialisée à l'appel de la commande ainsi :
make "variable_interne=valeur_d'initialisation"
� Exemple
make "EXEC=application1"
■ Cible par défaut ou explicite
La cible est explicite ou, par défaut, la première rencontrée.
� Exemple
make circonf.o
■ Autres utilisations du fichier Makefile
N'importe quelle commande peut être utilisée.
� Exemple
listing : principal.c cat principal.c
ANNEXE ───────────────────────────────────────────────────
335
■ Variables internes prédéfinies
La commande make utilise les macros suivantes : # ligne de commentaire $@ nom de la cible courante suffixée $* nom de la cible courante non suffixée $< dépendance à l'origine de la cible $? dépendance dont la date est postérieure à celle de la cible.
■ Règles implicites et règles explicites
La règle utilisée est explicite ou par défaut implicite.
■ Règles de précédence
Supposons l'existence des fichiers fichier.c, fichier.s, fichier.f. La dépendance implicite de la cible fichier.o est le fichier suffixé par .c. La modification explicite des règles de précédence implicites est effectuée avec le mot clé .SUFFIXE.
Synopsis .SUFFIXE cible dépendance1 dépendance2...dépendancen
L'ordre de priorité d'évaluation des dépendances est de gauche à droite.
� Exemple
.SUFFIXE .o .f .s .c
Le suffixe de la dépendance d'une cible.o est par .f, par défaut .s, par défaut .c.
■ Règles d'inférence implicites
Les règles d'inférence définissent les règles de production de la cible à partir de ses dépendances. Ainsi, les règles d'inférences implicites pour le choix du compilateur ainsi que ses options de compilation sont :
CC=cc CFLAGS=-O
Les règles d'inférences implicites des fichiers objets, des bibliothèques et de leurs clés sont les suivantes
# les dépendances sont suffixée par.c, la cible est suffixée par.a .c .a:
# compilation du fichier source (dépendance $<) et création de la cible (fichier objet) $(CC) -c $(CFLAGS) $<
# règle de production de la bibliothèque (la cible) à partir de la clé (dépendance) ar rv $@ $*.o # nettoyage des fichiers objets rm $*.o
336 ANNEXES ───────────────────────────────────────────────────
■ Règles d'inférences de mise à jour de bibliothèques
Les parenthèses d'une dépendance indiquent que le nom intérieur est celui d'un fichier objet et que le nom extérieur est celui d'une bibliothèque le contenant. La mise à jour d'une bibliothèque est réalisée la règle de dépendance explicite :
libxx.a : libxx.a(mod.o) libxx.a(mod2.o)
La macro-instruction ar rv $@ $*.o est interprétée de la façon suivante :
la variable $@ est remplacée par libxx.a la variable $* est remplacée par la clé.
� Exemples
#fichier makefile version 4 EXEC = resultat OBJ = principal.o DIR = /home/philipp/biblio $(EXEC) : $(OBJ) lib2.a lib3.a cc -o $(EXEC) $(OBJ) -L$(DIR) -l2 -l3 lib2.a : lib2.a(surface.o) lib3.a : lib3.a(circonf.o) circonf.o : pi.h surface.o :pi.h #fichier makefile version 5 EXEC = resultat OBJ = principal.o DIR = /home/philipp/biblio BIB2 = 2 BIB3 = 3 LIB2 = lib2.a LIB3 = lib3.a $(EXEC) : $(OBJ) $(LIB2) $(LIB3) cc -o $(EXEC) $(OBJ) -L$(DIR) -l$(BIB2) -l$(BIB3) $(LIB2) : $(LIB2)(surface.o) $(LIB3) : $(LIB3)(circonf.o) circonf.o : pi.h surface.o : pi.h
■ Quelques options de la commande make
-i les codes d'erreurs sont ignorés -s exécution en mode silencieux -r ne tient pas compte des règles de dépendance par défaut -n affichage sans exécution de l'ensemble des commandes à effectuer pour la mise à
jour -p affichage des macros par défaut et de la liste des dépendances par défaut -d mode debug : affichage des informations relatives aux dates des fichiers.
■ La commande touch
La commande touch définit l'instant courant comme date de dernière modification d'un fichier ce qui fait croire à l'utilitaire make qu'un fichier vient d'être modifié.
ANNEXE ───────────────────────────────────────────────────
337
5. REGLES DE PRIORITE DES OPERATEURS
Ce tableau présente les priorités, par ordre décroissant des opérateurs du langage C++.
Opérateur Désignation :: résolution de portée [] accès aux composantes d'un tableau () appel de fonction type() transtypage fonctionnel explicite . sélection de membre -> sélection de membre par déréférenciation ++ post incrémentation — post décrémentation new allocation dynamique d'instance new[] allocation dynamique de tableau d'instances delete destruction d'instance d'objet créé dynamiquement delete[] destruction de tableaux d'instances créés dynamiquement ++ pré/post incrémentation — pré/post décrémentation * déréférenciation & référence + plus unaire - moins unaire ! négation logique ~ complément logique sizeof taille d'instance d'objet ou de type typeid identification de type (type) transtypage const_cast transtypage de constance dynamic_cast transtypage dynamique reinterpret_cast transtypage de réinterprétation static _cast transtypage statique .* sélection de membre par pointeur sur membre ->* sélection de membre par pointeur sur membre par déréférenciation * multiplication / division % division entière modulo + addition - soustraction << décalage à gauche (bit à bit) >> décalage à droite (bit à bit) < test d'infériorité > test de supériorité <= infériorité ou égalité >= supériorité ou égalité == identité logique != différence logique & et logique (champ de bits)
338 ANNEXES ───────────────────────────────────────────────────
Opérateur Désignation ^ ou exclusif logique (champ de bits) | ou inclusif logique (champ de bits) && et logique || ou logique ?: opérateur ternaire = opérateur d'affectation *= Opérateur composé : multiplication et affectation /= Opérateur composé : division et affectation %= Opérateur composé : modulo et affectation += Opérateur composé : addition et affectation -= Opérateur composé : soustraction et affectation <<= Opérateur composé : décalage à gauche et affectation >>= Opérateur composé : décalage à droite et affectation &= Opérateur composé : et logique et affectation |= Opérateur composé : ou inclusif logique et affectation ^= Opérateur composé : ou exclusif logique et affectation , Opérateur virgule
BIBLIOGRAPHIE
■ Langage C
C as a Second Language For Native Speakers of Pascal, Müldner and Steele, Addison-Wesley.
The C Programming Language, Brian W. Kernigham and Dennis M. Ritchie, Prentice Hall.
■ Langage C++
Programmer en C++, Claude Delanoy, Eyrolles
The C++ Programming Language, Bjarne Stroustrup, Addison-Wesley.
Working Paper for Draft Proposed International Standard for Information Systems — Programming Language C++, ISO.
Premières leçons de programmation : J. ARSAC - 1980 Cedic-Nathan
The science of programming : DIJKSTRA - 1981 Digue
Pascal - Manuel de l'utilisateur : Kathleen JENSEN - Niklaus WIRTH
The art of programming - Donald Knuth (1968)
Some notes on structure programming - Edger Dijkstra
A discipline of programming - Edger Dijkstra
The science of programming - D. Gries (1981)
■ Bibliothèque C / appels systèmes POSIX et algorithmique
Programmation système en C sous Linux, Christophe Blaess, Eyrolles.
Introduction à l'algorithmique, Thomas Cormen, Charles Leiserson, et Ronald Rivest, Dunod.
INDEX
r -, 85
--, 86
! !, 85 !=, 85
" ", 83
# #define, 261
% %, 85
& &, 88, 153 &&, 85
( (), 81, 153 () ? :, 86
* *, 85, 153 *this, 175
. .*, 153
/ /, 85 //, 152
[ [], 153
^ ^, 88
| |, 88 ||, 85
~ ~, 89
+ +, 85 ++, 86
< <, 85 <<, 89 <=, 85
= ==, 85
> >, 85 ->, 153 >=, 85 >>, 89
0 0, 83 0x, 262 0X, 262
A abstraction, 23, 24, 69, 139, 143, 148 Abstraction, 146 abstraction des données, 23 abstraction procédurale, 23
INDEX ───────────────────────────────────────────────────
341
accès aléatoire, 320 accès concurrents, 322 access, 316 Access Control Entry, 327 Access Control List, 327 ACE, 327 ACL, 327 acledit, 328 aclget, 328 aclput, 328 action, 44 ADA, 11 adressage, 60 adressage absolu, 60 adressage direct, 60 adressage immédiat, 60 adressage indexé, 62 adressage indirect, 61 adressage normal, 60 adressage par base et déplacement, 61 adressage par rapport à l'adresse courante, 62 adressage relatif, 61 adressage symbolique, 17 adresse, 96, 177 adresse absolue, 61 adresse de base, 61 adresse de retour, 65 adresse effective, 60 adresse réelle, 60 adresse symbolique, 79 affectation de pointeur, 259 Aggregation, 142 agrégat, 113, 142 agrégation, 142 Agrégation, 142 algorithme, 16, 19, 20, 21, 22, 23, 24, 26, 27, 33, 34, 35, 36, 37, 38, 40, 48, 52, 55, 59, 65, 67, 89, 130, 133, 135, 300, 331, 333 algorithmes, 282 Al-Khoarizmi, 19 allocateurs, 282 allocation de mémoire, 141 allocation dynamique, 148, 149, 156, 179, 192, 193, 224, 226, 306, 337 alphabet, 43 alphanumérique, 17 ambiguïté, 156, 160, 164, 202, 267, 270, 293, 295, 296, 297, 312 American Standard Code for Information Interchange, 54 analyse amortie, 21 analyse du cas pire, 21 analyse en moyenne, 21 analyse formelle, 16 analyse syntaxique, 40 analyseur syntaxique, 17, 92 appel de fonction, 95 appel système, 315 argc, 108 arguments effectifs, 95 argv, 108 arité, 211
Array, 145 ASCII, 54, 77, 80, 83, 263 assembleur de haut niveau, 70 assesseur, 172 attribut, 113, 139 attribut protégé, 173 attributs, 113 attributs de classe, 170 auto, 179 avertissement, 91
B Bag, 145 BASIC, 26 bibliothèque, 18 bibliothèques, 3, 15, 16, 17, 25, 40, 51, 52, 70, 71, 284, 335, 336 bit de contrôle, 54 bit de gluance, 326 bit de remplissage, 121 bit le plus signicatif, 55 bit S, 326 bit suid, 326 bits d'alignement, 113 bits de poids faible, 55 bits de poids fort, 55 Bjarne Stroustrup, 151 bloc, 72 bogue de l'an 2000, 14 bogues, 16 bool, 215, 256, 257, 287, 309 branchement, 64 branchement conditionnel, 11 Branchement conditionnel, 26 branchements multiples, 46 bug, 16
C cache, 322 calcul de la longueur d'une chaîne de caractères, 107 calcul effectif, 19 calloc, 192, 193 caractère d'échappement, 83 case, 47 cast, 81 cerr, 153 chaîne de caractères, 79, 83 champ, 113, 170 champ de bits, 121 champs, 141 char, 77, 79 chdir, 330 chemin d'accès, 316 chgrp, 324 chmod, 316, 325 chown, 324, 329, 330 chroot, 330 cible, 331 cin, 153 class, 153, 170, 173, 265
342 INDEX ───────────────────────────────────────────────────
classe, 3, 31, 41, 42, 63, 73, 74, 82, 113, 140, 141, 142, 143, 144, 145, 150, 155, 156, 158, 159, 165, 169, 170, 171, 172, 173, 174, 176, 177, 182, 184, 185, 186, 187, 188, 189, 190, 193, 195, 198, 199, 200, 201, 202, 203, 205, 206, 209, 211, 212, 213, 214, 215, 216, 217, 218, 219, 221, 222, 223, 224, 225, 227, 228, 229, 236, 238, 239, 240, 241, 242, 243, 244, 245, 246, 248, 249, 250, 251, 253, 254, 255, 256, 259, 260, 264, 266, 267, 268, 269, 270, 272, 273, 274, 275, 276, 277, 279, 281, 284, 285, 286, 287, 288, 291, 292, 295, 296, 300, 301, 302, 308, 309, 310, 311, 312, 313 classe abstraite, 141, 255 classe d'allocation, 179 classe de base, 141, 239 classe de classes, 268 classe de mémorisation, 74 classe dérivée, 141, 173, 239 classe encapsulée, 227 classe fille, 141, 239 classe générique, 264 classe imbriquée, 227 classe mère, 141, 239 classe modèle, 145 classe par défaut, 182 classe template, 268 classe Template, 145 classe virtuelle, 141, 249 classes d'objets, 170 clog, 153 close, 316, 320 codage, 53 code exécutable, 16 code mort, 18 code source, 16, 261, 262, 264, 331 coercition, 81 collection non ordonnée avec doublon, 145 collection non ordonnée sans doublon, 145 collection ordonnée avec doublon, 145 collection ordonnée et indexée, 145 comparaison de chaînes de caractères, 108 comparaison de pointeurs, 105 Compatibilité, 14 compilation, 16, 17, 82, 91, 92, 94, 144, 157, 165, 169, 172, 174, 182, 186, 194, 216, 259, 261, 264, 265, 266, 295, 331, 333, 335 compilation conditionnelle, 261 compilation séparée, 94 compilation simultanée, 94 complément à 1, 55 complément à 2, 55 complexité, 3, 12, 13, 16, 20, 22, 25, 33, 35, 36, 38, 44, 143, 148 comportement, 142 comportement asymptotique, 20, 21 composant logiciel, 15 concaténation de chaînes de caractères, 98 conditions limites, 16 confidentialité, 197 const, 78, 153, 157 constante, 262 constante de type caractère, 263 constante en virgule flottante, 263
constante entière, 262 constante manifeste, 262 constante symbolique, 152, 157, 261 constructeur, 141, 151, 184 constructeur copie, 221, 223 constructeur copie implicite, 221 constructeur explicite, 185 constructeur implicite, 185 constructeur par défaut, 150, 185, 188, 189, 193, 195, 196, 202, 203, 204, 205, 206, 207, 208, 209, 220, 248, 250 Container, 145 conteneur, 256, 281 conteneur d'objets, 145 conversion de pointeurs, 105 conversion explicite, 81 conversion implicite, 80, 189 couche objet, 139, 142 cout, 153, 154 creat, 316, 317, 330 création, 315, 317 cycle de vie d'un fichier, 316 cycle perpétuel, 12
D débogeur symbolique, 17 debugger, 17 déclaration, 43, 74, 156 déclaration du type du résultat d'une fonction, 94 déclaration struct, 114 déconstructeur, 143 default, 47 définition, 43, 74, 114, 156 définition d'une fonction, 91 définition littérale, 83 delayed write, 324 delete, 149, 150, 156, 193, 194, 195, 196, 203, 204, 206, 207, 212, 216, 217, 218, 221, 222, 224, 226, 234, 260, 287, 305, 306, 337 delete[], 194 délimiteur, 72, 83, 152, 227 délimiteur de fin de chaîne, 83 dépendances, 331 dépilement, 65 déplacement, 61 descripteur de fichier, 316 destructeur, 141, 143, 151, 184, 190, 195, 203, 206, 216, 217, 218, 221, 224, 234, 248, 259, 260, 286, 287, 288, 289, 300, 305, 306 destructeur explicite, 190 destructeur par défaut, 190 destruction, 315 Dijkstra, 11, 39 diviser pour régner, 24 Diviser pour régner, 24 do, 48 document technique de référence, 15 domaine de définition, 51 domaine de valeur, 51, 91 domaines de définition, 91 donnée alphanumérique, 54
INDEX ───────────────────────────────────────────────────
343
donnée membre, 170 donnée numérique, 54 donnée structurée, 113 données, 139 données membres, 113, 141, 173, 174, 184, 185, 188, 198, 203, 205, 212, 214, 227, 228, 249, 275, 277 double, 78, 79 double précision, 56 droits d'accès, 324 dup, 330 dynamic_cast, 250
E EBCDIC, 54, 77 écriture, 315 éditeur de texte, 16 Efficacité, 14 else...if, 46 émetteur, 140 empilement, 65 encapsulation, 139, 142, 143, 146, 148, 172, 214, 259 encapsulé, 172 enjoliveur de programmes, 17 enregistrement logique, 113 ensemble, 145 ensemble d'objets, 145 entrée standard, 71, 319 Entrées/sorties de bas niveau, 323 Entrées/sorties de haut niveau, 323 Entrées/sorties en mode bloc, 324 Entrées/sorties en mode caractère, 324 Entrées/sorties en mode cru, 324 Entrées/sorties en mode raw, 324 enum, 79, 125 énumération, 76, 152, 158 EOF, 83 Ergonomie, 14 erreur de troncature, 56 escape sequence, 83 exception, 151, 307 exec*, 330 exit, 330 explicit, 190 export, 284 expression, 42, 71 Extended Binary Coded Decimal Interchange Code, 54 Extensibilité, 14 extern, 156, 179
F fchown, 329 fcntl, 330 fermeture, 315, 320 fichier cible, 331 fichier en tête, 157 fichier ordinaire, 321 fichier répertoire, 321 fichier spécial, 322 fichier standard, 321
fichier standard d'affichage des diagnostics d'erreurs, 321 fichier standard de sortie, 321 fichier standard d'entrée, 321 FIFO, 66 file d'attente, 66 first in, first out, 66 float, 77, 79 flux, 153 fonction, 153 fonction 91, 127 fonction atoi, 127 fonction constructeur, 143 fonction de fonction, 92 fonction générique, 159 fonction générique amie, 267 fonction inline, 170 fonction membre, 141, 170 fonction récursive, 32, 33, 95 fonction sans argument, 93 fonction statique, 198 fonction statique, 183 fonction template, 266 for, 45 fork, 330 format libre, 153, 154 forme normalisée, 57 free, 192 friend, 214 fstat, 330 fstream.h, 155
G garbage collector, 143 Generalization, 141 généricité, 281 Généricité, 144, 279 getAttr, 172 geteuid, 330 getpid, 330 getppid, 330 getuid, 330 goto, 50 grammaire, 41 graphe des dépendances, 331 Gries, 11, 37, 339 group, 324 groupe, 324
H header, 157 héritage, 141, 239 héritage multiple, 151 héritage multiple, 239 Héritage multiple, 141 héritage qualifié private, 241 héritage qualifié protected, 241 héritage qualifié public, 241 héritage simple, 151, 239 hexadécimal, 17, 53, 54, 58, 83, 123, 262
344 INDEX ───────────────────────────────────────────────────
hiérarchie de classes, 141 Hiérarchie de classes, 141
I identificateur, 73 identificateur de fonction, 92 identificateur d'objet, 139 if...else, 46 ifstream, 155 implémentation, 142 impression des composantes d'un tableau, 86 inclusion de fichiers, 261 inconsistance, 24 indexation, 62 induction mathématique, 24 Inheritance, 141 initialisation, 184 initialisation d'un pointeur, 105 initialisation d'une variable structurée, 114 inode, 322, 323 instance, 170 instances, 140 instanciation, 141, 170 instanciation des paramètres génériques, 264, 270 instanciation du paramètre générique, 278 instanciation explicite, 271 instanciation implicite, 270 instruction, 72 instruction exécutable, 42, 156 instruction généralisée, 72 instruction non exécutable, 42 instruction symbolique, 159 instruction vide, 72 int, 77, 79 intégrité, 98 Intégrité, 14 interface d'accès, 143 interface d'objet, 143 interruption, 64 invariant, 24, 31, 78 invariant de boucle, 30, 78 iostream.h, 153 istream, 155 itérateur, 281 itération, 28, 29, 50
J Java, 3, 11, 23, 28, 39, 42, 44, 52, 69, 140, 179 jeu d'essais, 19
K Kernighan et Ritchie, 3, 71 kill, 330 Knuth, 11, 339
L LAC, 327 langage machine, 11, 16, 17, 19, 69
last in, first out, 66 Le bit s, 326 lecture, 315 Left value, 75 levée de l'encapsulation, 214 libération de la mémoire, 156 lien dynamique, 253 lien hypertexte, 323 lien matériel, 322, 323 Lien matériel, 322 lien symbolique, 322, 323 LIFO, 66 link, 316, 330 liste, 66, 145, 326 liste chaînée, 66, 67, 131, 132, 133, 287 liste d'énumération, 125 liste vide, 159 listes de contrôle d'accès, 327 ln, 323 long, 78, 79 long double, 78, 79 long int, 79 lseek, 316, 320 lstat, 330 Lvaleur, 75, 85, 118, 165, 224 Lvaleur, 75 L-valeur, 117 Lvaleur modifiable, 75 Lvalue, 75
M macro-définitions de type fonction, 261 macro-substitution du premier ordre, 262 main, 52 maintenance, 16 make, 331, 332, 333, 334, 335, 336 makefile, 331 Makefile, 331 malloc, 192, 193 message, 140, 154, 155, 176, 213 message d'erreur, 91 métaclasse, 268, 277 métalangage, 40 méthode, 23, 113, 139, 141, 170, 172 méthode abstraite, 255 méthode constructeur, 185 méthode des approximations successives, 15, 30 méthode inline, 172, 174 méthode multi-classes, 141 méthode opérateur, 211 méthode spécifiée constante, 175 méthode statique, 200 méthode virtuelle, 144, 253, 279 méthode virtuelle pure, 255 méthodes, 139 méthodes itératives, 24, 26, 28 mkfifo, 330 mknod, 330 mode bloc, 322 mode c, 322 modèle abstrait, 139
INDEX ───────────────────────────────────────────────────
345
modèle abstrait, 140 modificateur, 172 modulo, 85 most significant bit, 55 mount, 330 msgctl, 331 msgget, 331 msgrcv, 331 msgsnd, 331 multiplication égyptienne, 89, 127 mutable, 158
N new, 149, 150, 156, 157, 193, 194, 195, 196, 204, 206, 207, 212, 216, 217, 218, 221, 222, 223, 224, 226, 234, 236, 259, 287, 306, 307, 310, 337 niveau d'indirection, 61 noalias, 78 nom externe, 316 nom symbolique, 43 nombre entier non signé, 55 nombre entier signé, 55 nommage, 152, 158 nommage des agrégats, 152 non signé intégral, 191
O Object Interface, 143 objet, 139, 155 Objet, 139 objet de référence, 163 objet encapsulé, 143 objet générique, 144 objet modèle, 151 objet paramétré, 144, 145, 151 Objet polymorphique, 145 objet statique, 198 objet structuré, 113 objet template, 264 objets polymorphiques, 145, 256, 309 octal, 262 ofstream, 155 open, 316, 330 opendir, 317 opérateur, 42, 155 opérateur &, 163 opérateur *, 163 opérateur ~, 190 opérateur ->, 115 opérateur arithmétique, 41 opérateur binaire, 84 opérateur composé, 87 opérateur d'adresse, 163 opérateur de coercition, 71, 81 opérateur de décalage logique, 89 opérateur de déréférenciation, 96, 153, 163 opérateur de référence, 96, 153, 163, 229 opérateur de résolution de portée, 198, 249, 253, 291, 292, 293, 296 opérateur de résolution de visibilité, 156
opérateur de sélection de membre, 115, 170, 229 opérateur de transtypage, 71, 81 opérateur d'indirection, 96, 163, 192 opérateur fonctionnel, 230 opérateur logique, 41 opérateur modulo, 85 opérateur relationnel, 41 opérateur scope, 170 opérateur surdéfini, 211 opérateur ternaire, 84, 86 opérateur unaire, 84 opérateurs sur les champs de bits, 88 opération, 140 opération de décalage logique, 89 opération et logique, 88 opération ou exclusif, 88 opération ou logique, 88 opérations, 139 opérations sur les bits, 88 operator, 211 opérer, 139 ordre d'évaluation, 90 ostream, 155 ouverture, 315 overriding, 240 Overriding, 144
P padding, 121 paramètre template template, 277 Pascal, 28, 339 PASCAL, 3, 11, 40, 43, 339 pause, 330 picture element, 60 pile, 33, 38, 64, 65, 66, 75, 98, 99, 110, 111, 112, 146, 147, 148, 149, 150, 164, 173, 286, 287, 288, 299, 300 pile des adresses de retour, 65 pile des arguments, 110 pile d'exécution, 98, 99, 110, 164 pile logicielle, 64 pile matérielle, 64 pilotes, 324 pipe, 71, 322, 330 pixels, 60 pointeur, 63, 96 pointeur constant déréférencé, 98, 102, 109, 163 pointeur de pile, 64 pointeur déréférencé, 163, 165, 272, 310 pointeur générique, 192, 226 polymorphisme, 144, 159 POP, 65 POPD, 65 portabilité, 56, 69, 70, 126, 143 Portabilité, 14 portage, 16 portée, 160, 179 POSIX P1006.3, 327 post-incrémentation, 86 post-indexation, 62 pré-incrémentation, 86 pré-indexation, 62
346 INDEX ───────────────────────────────────────────────────
préprocesseur, 159 principe de localisation, 46 principe du partage, 24 printf, 101 printf, 111 Prise de communication, 324 private, 173, 241 problème calculatoire, 19 Problème du représentant de commerce, 22 problème indécidable, 22 problème NP-complet, 20 procédure, 51, 93 procédures, 139 profileur, 18 programmation dynamique, 24 programmation modulaire, 25 propriétaire, 324 propriétaire effectif, 326 propriétaire réel, 326 protected, 173, 241 Prototypage, 159 prototype, 43, 92, 94, 163 prototype de fonction, 160 prototype des fonctions sans argument, 93 public, 173, 241 pure virtual method, 255 PUSH, 65
Q qsort, 52 qualificatif, 78 qualification de l’héritage, 239 qualifications du contrôle d'accès, 241 Quick sort, 52
R ramasseur d'ordures, 143 read, 316 read ahead, 324 realloc, 192, 193 récepteur, 140 récursion, 24, 174 récursivité, 95, 115 Récursivité, 32, 35, 95 redéfinition d'une méthode, 144 réels DCB, 56 réels flottants, 56 réels virgule fixe, 56 référence, 16, 17, 139 référence, 163 référence en avant, 17, 172, 179, 181 référence externe, 17 référence interne, 17 register, 179, 184 registre de base, 61 registres, 74 règles de précédence, 335 règles de production, 331 règles d'inférence, 335 règles explicites, 335
règles implicites, 335 rémanence, 197 remontée, 33, 300 représentation interne, 23, 142 requalification, 244 résultat d'une fonction, 51, 93 return, 51, 93, 159 Réutilisabilité, 14 robustesse, 143 Robustesse, 14 rupture de séquence, 11, 45, 172 Rvaleur, 75, 85, 118 R-valeur, 117
S sac, 145 scanf, 101, 111 SCCS, 18 sélecteur d'objet membre, 153 sélecteur sur pointeur, 153 sémantique, 3, 16, 23, 26, 40, 69, 123, 159, 172, 185, 211, 213, 217, 225, 253, 296, 321 semctl, 331 semget, 331 semop, 331 Set, 145 setAttr, 172 seteuid, 330 setuid, 326, 330 shmat, 331 shmctl, 331 shmdt, 331 shmget, 331 short, 78, 79 short int, 79 signal, 330 signature, 140, 159, 160, 175, 187, 198, 279, 296, 308 signature d'une fonction, 160, 211 signatures, 143 signed, 78, 79 signed int, 79 signed long, 79 signed long int, 79 signed short, 79 signed short int, 79 simple précision, 56 Simula, 140 simulation, 16 size_t, 191 sizeof, 191, 212 Smalltalk, 11 SmallTalk, 140 sortie standard, 71, 319 soustraction de pointeurs, 105 spécialisation partielle, 273 spécialisation totale, 275 spécificateur const, 164 spécificateur d'accès, 173 spécificateur inline, 174 spécification, 14, 15, 25, 144, 266, 270, 271, 272, 279 stack pointer, 64
INDEX ───────────────────────────────────────────────────
347
Standart Template Library, 281 stat, 316, 330 static, 179, 198 stdarg.h., 111 stddef.h, 191 stdio.h, 49, 83, 84, 86, 87, 95, 98, 101, 107, 116, 117, 119, 120, 121, 126, 128, 131, 137, 147, 191, 192, 286, 332 sticky bit, 326 STL, 281 strcat, 98 strcmp, 108 streams, 153 struct, 79, 113, 153, 170, 173 structure, 113 structure de bloc, 72 structure de données, 64 structure de données abstraite, 23, 64, 113 substitution symbolique, 261, 262 suite de Fibonacci, 95 Suite de Fibonacci, 36 support, 16 surcharge d'un opérateur, 144 surcharge d'une fonction, 144 surcharge d'une méthode, 144 surcharge d'une procédure, 144 surdéfinition, 142, 144 surdéfinition de l'opérateur [], 219 surdéfinition de l'opérateur d'affectation, 223 surdéfinition des fonctions, 159 surdéfinition des opérateurs, 42 surdéfinition d'un opérateur, 144 surdéfinition d'une fonction, 144 surdéfinition d'une méthode, 144 surdéfinition d'une procédure, 144 switch, 47 symlink, 330 syntaxe, 40
T table, 64 table des symboles, 17, 272 tableau, 42, 79, 145, 153 tableau de dimension incomplète, 108 tableau de structures, 120 tableau variables structurées, 120 template, 144, 264 Terminateur, 72 test, 11, 45 test de fin de boucle, 28 test de régression, 18 tests de couverture, 18 this, 175, 200, 213 token, 72, 261 touch, 336 Tours de Hanoi, 37 transcodage, 53 translatable, 61 translation d'adresse, 61 transmission des arguments, 98 transmission des arguments par adresse, 101
transmission par adresse, 98, 163 transmission par référence, 99, 151, 163, 164, 221, 222, 301 transmission par valeur, 98, 163 transtypage, 81, 105, 109, 156, 189, 190, 192, 193, 201, 202, 203, 216, 217, 228, 231, 242, 243, 250, 259, 260, 311, 312, 313, 314, 337 transtypage explicite, 189 transtypage fonctionnel, 156 transtypage fonctionnel, 189 transtypage implicite, 242 transtypage par appel de fonction, 156 tube, 71, 317, 330 tube nommé, 322 typage, 142 type, 74 type abstrait, 23, 147, 148, 149 type agrégat, 76 type arithmétique, 76 type arithmétiques, 152 type booléen, 152 type composite, 76 type de base, 76 type de structure abstrait, 114 type dérivé, 77 type dérivés, 153 type du résultat d'une fonction, 91 type entier non signé, 76 type entier signé, 76 type énuméré, 76, 152, 158 type flottant, 76, 152 type fonction, 76 type générique, 264, 265, 268, 269 type incomplet, 77 type intégral, 77, 152 type majeur, 77 type non qualifié, 77 type objet, 77 type pointeur, 76 type prédéfini, 75 type scalaire, 76 type structuré, 76 type symbolique, 159 type synonyme, 126 type tableau, 76 type union, 76 typedef, 79, 126, 158 typename, 281 types abstraits, 142, 148 types de base, 77, 152, 211, 311
U umask, 325 umount, 330 undef, 263 union, 79, 113, 123, 153 unité syntaxique, 25, 111, 261, 262 Unix, 3, 23 unlink, 316, 330 unsigned, 78, 79 unsigned char, 79
348 INDEX ───────────────────────────────────────────────────
unsigned int, 79 unsigned long, 79 unsigned long int, 79 unsigned short, 79 unsigned short int, 79 utilisateur propriétaire, 324
V va_arg, 111 va_end, 111, 112 va_list, 111 va_start, 111 valeur par défaut, 161, 265 Valeur par défaut d'un type générique, 265 Validité, 14 variable, 42, 73 variable automatique, 179, 182 variable externe, 179 variable globale, 179 variable interne-statique, 182 variable locale, 179 variable pointeur, 96 variable qualifiée constante, 82 variable qualifiée constante de type caractère, 83 variable qualifiée constante en virgule flottante, 82 variable qualifiée constante entière, 82 variable register, 179 variable statique, 179, 182, 197 variable statique externe, 183 variable structurée, 113 variable symbolique, 43 variables d'instance, 141 variables structurées, 113, 114, 116, 120, 121, 123, 126 Vérifiabilité, 14 virtual, 253 visibilité, 179 void, 71, 79 void, 93 void, 93 volatile, 78 Von Neumann, 11
W wait, 330 while, 47 write, 316
Z zone, 113