1
C H A P I T R E
NOTIONS DE BASE
1
DÉFINITIONS GÉNÉRALES
1.1
2
DÉFINITIONS GÉNÉRALES
Le terme informatique (computer science) est un mot français désignant la science qui traite des données pour obtenir des résultats. Ce traitement est traditionnellement effectué à l’aide d’algorithmes. Un algorithme (algorithm) est une suite d’opérations à effectuer pour résoudre un problème (exemples 1.1 et 1.2). Exemple 1.1 Algorithme de résolution de l’équation ax+b = 0.
1 si a et b sont nuls, chaque nombre réel est solution et l’algorithme est terminé; 1 si a est nul et b non nul, l’équation est insoluble et l’algorithme est terminé; 2 si a est non nul, soustraire b à gauche et à droite du signe = et l’on obtient ax = –b; 3 diviser chaque membre par a et l’on obtient le résultat cherché qui est x = –b/a.
L’algorithmique est une discipline fondamentale de l’informatique qui traite de la conception, de la réalisation et de la vérification des algorithmes. Sa pratique est rendue difficile par la rigueur qu’il est nécessaire d’appliquer, rigueur souvent négligée par les néophytes, voire parfois par les concepteurs plus expérimentés. Exemple 1.2 Algorithme de mise en marche d’une voiture.
1 2 3 4
mettre la clé dans le démarreur; serrer le frein à main; mettre le levier des vitesses au point mort; répéter les opérations suivantes tant que le moteur ne tourne pas: • mettre la clé dans la position marche; • tourner la clé dans le sens des aiguilles d’une montre; • attendre quelques secondes; • si le moteur ne démarre pas, remettre la clé dans la position initiale; 5 enclencher la première vitesse; 6 desserrer le frein à main.
Lorsqu’un ordinateur doit exécuter un algorithme, celui-ci doit être exprimé dans un langage compréhensible par la machine. Ce langage appelé langage machine (machine code) est composé de suites de chiffres 0 et 1 appelés bits qui correspondent à des états distincts des circuits qui composent la machine. Or le programmeur ne peut pas exprimer des algorithmes complexes avec des 0 et des 1! Il va utiliser un langage plus proche d’une langue naturelle appelé langage de programmation (programming language). Une fois cet algorithme codé dans un langage de programmation, le programme source (source program) ainsi créé sera:
DÉFINITIONS GÉNÉRALES
3
• soit traduit complètement en langage machine par le compilateur
(compiler) pour permettre ensuite l’édition de liens; • soit directement interprété (interpreted), c’est-à-dire que chaque ligne de
code source est exécutée directement sans traduction préalable de tout le programme; cette ligne peut être traduite ou non en un langage proche du langage machine avant exécution. La compilation (compilation) est donc une étape supplémentaire mais a l’avantage de produire un programme en langage machine. Ce programme en langage machine peut exister aussi longtemps que nécessaire. Chaque fois que le programme doit être utilisé, il le sera directement, ce qui implique que la compilation n’est nécessaire qu’après chaque modification du programme source. L’interprétation (interpretation) est plus directe que la compilation, mais la traduction de chaque ligne a lieu lors de toutes les utilisations du programme. L’édition de liens (link) est la phase pendant laquelle les différentes parties composant un programme sont réunies de manière à former un programme exécutable. C’est au cours de cette opération que des composants préexistants, par exemple des éléments graphiques, sont placés dans cet exécutable. Ces composants sont souvent regroupés dans une (ou plusieurs) bibliothèque(s) (library). L’exécution (execution) d’un programme par un ordinateur consiste à exécuter les instructions machine les unes à la suite des autres. Elle sera nettement plus rapide dans le cas d’un programme compilé que pour un programme interprété. Le déverminage (debugging) est l’activité de récupération, diagnostic et correction des erreurs de conception (logique) du programme. En d’autres termes, durant cette activité on procède à la mise au point du programme. De manière générale, la programmation (programming) est l’art d’écrire des programmes (programs) en langage de programmation et constitue une autre discipline fondamentale de l’informatique. Un environnement de programmation (programming environment) ou de développement représente l’ensemble des outils logiciels (et parfois matériels) nécessaires à la conception, à la réalisation et à la maintenance d’applications complexes. Il faut enfin définir le terme implémentation (implementation) qui regroupe toutes les caractéristiques (d’un langage de programmation) propres à un environnement de programmation particulier. Parfois ce terme représente l’environnement lui-même.
LANGAGES DE PROGRAMMATION
1.2
4
LANGAGES DE PROGRAMMATION
1.2.1
Historique
Plusieurs centaines de langages de programmation ont été proposés au cours de l’évolution de l’informatique. La majorité d’entre eux s’appliquaient à des domaines très particuliers alors que d’autres se voulaient plus généraux. Leur succès ou leur échec tient autant à leurs qualités ou leurs défauts qu’à la période où ils apparurent sur le marché ou dans les universités. Le tableau 1.1 décrit par ordre chronologique d’apparition plusieurs langages de programmation parmi les principaux utilisés jusqu’à aujourd’hui ainsi que leur principal domaine d’application. Même s’ils ne sont pas cités explicitement, il faut également mentionner les langages d’assemblage appelés assembleurs (assemblers), utilisés surtout pour programmer au niveau de la machine. Finalement, il faut préciser qu’il existe encore d’autres catégories de langages informatiques destinés par exemple à la simulation de systèmes ou à la conception de circuits électroniques. Tableau 1.1 Principaux langages de programmation.
Domaine d’application
Remarques
Année
Langage
1955
Fortran
Calcul que
1960
Algol-60
Algorithmique
Premier langage dit structuré grâce à la notion de blocs.
1959
Lisp
Intelligence artificielle
Premier langage non impératif.
1961
Cobol
Applications commerciales
Langage verbeux, peu maniable, encore très utilisé en informatique de gestion.
1964
PL/1
Langage général
Complexe, ayant la prétention d’être universel et regroupant les spécificités de Cobol et Fortran, créé et utilisé chez IBM.
1965
Basic
«Travail à la maison»
Basique, simple d’utilisation mais peu adapté à la programmation structurée.
1970
Prolog
Intelligence artificielle
Langage non impératif.
1971
Pascal
Enseignement
Créé à l’Ecole polytechnique de Zurich, diffusé partout mais souffrant des différences présentes dans les nombreuses versions existantes.
scientifi-
Langage ancien, dont les versions plus récentes comportent encore des bizarreries héritées des années 50; normalisé actuellement sous l’appellation Fortran 98.
LANGAGES DE PROGRAMMATION
5
Tableau 1.1 (suite) Principaux langages de programmation.
Domaine d’application
Année
Langage
1972
C
Programmation système
Accès facile au matériel, largement diffusé; normalisé sous l’appellation ANSI-C.
1976
Smalltalk
Langage général
Programmation orientée objets, prototypage rapide.
1980
Modula-2
Programmation système
Descendant direct de Pascal, mêmes problèmes de versions différentes.
1983
Ada
Langage général
Riche, utilisé pour développer des systèmes complexes et fiables, ainsi que pour des applications temps réel critiques; normalisé sous l’appellation Ada 83.
1984
C++
Langage général
Successeur de C; permet la programmation orientée objets; normalisé par l’ISO en 1998.
1995
Ada 95
Langage général
Successeur d’Ada 83; ajoute la programmation orientée objets, par extension, etc; normalisé sous l’appellation Ada 95.
1996
Java
Internet
Semblable à C++, mais plus sûr; permet la programmation d’applications classiques mais aussi d’applets.
1.2.2
Remarques
Langage de programmation Ada
L’expérience montre que le premier langage de programmation appris est fondamental pour l’avenir d’un programmeur. En effet, les habitudes prises sont ancrées si profondément qu’il est très difficile de les modifier voire de s’en défaire! L’apprentissage de la programmation doit donc s’effectuer avec un langage forçant le programmeur à adopter de bonnes habitudes (sect. 1.3). Il est toujours plus facile d’évoluer vers de nouveaux langages lorsque ces bonnes habitudes sont acquises. Le langage Ada aide à l’apprentissage d’une bonne programmation en obligeant le programmeur à se soumettre à certaines contraintes et en lui fournissant une panoplie assez riche d’outils agréables à utiliser. Ces outils vont lui permettre de coder relativement simplement un algorithme même complexe et de refléter fidèlement sa structure. La version d’Ada présentée dans cet ouvrage est celle définie en 1995 [ARM] et normalisée ISO.
BONNES HABITUDES DE PROGRAMMATION
1.3
6
BONNES HABITUDES DE PROGRAMMATION
Le but de tout programmeur est d’écrire des programmes justes, simples, lisibles, fiables et efficaces. Pour la justesse, la simplicité et la lisibilité, les quelques points suivants sont fondamentaux (la liste est non exhaustive!): • Réfléchir et imaginer de bons algorithmes de résolution avant d’écrire la
première ligne du programme. • Une fois l’algorithme général trouvé, en écrire l’esquisse dans un
formalisme pseudo-formel, puis préciser cette esquisse pour finalement coder l’algorithme dans le langage de programmation choisi. • Lors du codage, choisir des noms parlants (utiliser des mnémoniques) pour
représenter les objets manipulés dans le programme, commenter chaque morceau du programme de manière explicative plutôt que descriptive et tester chaque module, procédure, fonction soigneusement. Pendant toute la démarche, adopter et appliquer systématiquement des conventions simples et cohérentes.
DE L’ANALYSE DU PROBLÈME À L’EXÉCUTION DU PROGRAMME
1.4
7
DE L’ANALYSE DU PROBLÈME À L’EXÉCUTION DU PROGRAMME
Voici une ébauche de marche à suivre pour la création de programmes à partir d’un problème donné: 1 bien lire l’énoncé du problème, être certain de bien le comprendre; 2 réfléchir au problème, déterminer les points principaux à traiter; 3 trouver un bon algorithme de résolution (sect. 1.7), l’écrire dans le formalisme choisi; 4 coder l’algorithme en un programme écrit sur papier (au moins pour son architecture principale); 5 introduire le programme dans l’ordinateur au moyen d’un éditeur de texte; 6 compiler le programme; 7 effectuer l’édition de liens du programme; 8 exécuter le programme, vérifier son bon fonctionnement par des tests significatifs. En cas d’erreurs de compilation, il faut les corriger avec l’éditeur de texte puis recommencer le point 6. Si le programme fonctionne mais donne des résultats faux, ou si l’exécution du programme se termine par un message d’erreur, cela signifie qu’il y a des fautes de logique. Il faut réfléchir, parfois longuement, trouver l’origine des fautes en particulier en s’aidant des outils de déverminage, modifier le pro-gramme en conséquence puis reprendre au point 6. Lorsque les programmes source dépassent une page, il serait judicieux de s’habituer à les concevoir et les tester par étapes. Cette façon de faire devient en effet indispensable lorsque la taille du code écrit dépasse, par exemple, deux cents lignes, ce qui représente encore un tout petit programme!
PROPRIÉTÉS D’UN PROGRAMME
1.5
8
PROPRIÉTÉS D’UN PROGRAMME
Lors de l’écriture d’une application, les propriétés énumérées ci-après devraient toujours guider le programmeur lors des choix qu’il devra inévitablement effectuer, que le programme soit simple ou compliqué. En effet, un code bien écrit sera toujours plus fiable, lisible, simple, juste et même efficace. Ces propriétés sont les suivantes: • La fiabilité (reliability) consiste à ce que les erreurs de programmation
soient détectées à la compilation ou à l’exécution afin que le programme n’ait jamais de comportement imprévu. Il est naturellement préférable que le plus d’erreurs possible soient signalées à la compilation de manière à diminuer la phase de test du programme, toujours très coûteuse en temps et en argent, ainsi que le code supplémentaire généré pour la vérification de contraintes liées à l’exécution de certaines instructions. Un langage fortement typé répond à ce critère de fiabilité. • La lisibilité (readability) permet de réduire la documentation associée au
programme, de simplifier la correction des erreurs et de faciliter les modifications futures. Un langage permettant la construction de structures de données et disposant de structures de contrôle répond à ce critère. Rappelons cependant que les habitudes du programmeur sont au moins aussi importantes que le langage! • La simplicité (simplicity) est un critère évident: ne jamais compliquer
lorsqu’il est possible de rester simple. • L’efficacité (efficiency) doit naturellement être suffisante afin que le
programme s’exécute rapidement, mais ne doit pas constituer l’idole à laquelle tout le reste est sacrifié. Une conception bien pensée, une bonne structuration du code sont beaucoup plus importantes que l’accumulation d’astuces subtiles permettant un gain de temps minime lors de l’exécution du programme. • Enfin, on appelle portabilité (portability) la propriété représentant
l’indépendance d’une application ou d’un langage de programmation par rapport à la machine utilisée. Cette propriété joue un rôle fondamental lors de l’évolution des programmes au cours du temps. Plus un logiciel est portable, moins il sera sensible aux changements de matériels utilisés.
EXEMPLE INTRODUCTIF
1.6
9
EXEMPLE INTRODUCTIF
Une bonne introduction à l’algorithmique consiste à étudier un problème simple (exemple 1.3) dont la résolution sera effectuée en respectant la marche à suivre (sect. 1.4), restreinte aux points 1 à 4. Pour trouver un algorithme de résolution, il faut appliquer une méthode et l’utiliser chaque fois qu’un problème de programmation doit être résolu. La méthode proposée ici est connue sous le nom de méthode de décomposition par raffinements successifs. Cet exemple va également permettre d’insister sur les bonnes habitudes de programmation et d’introduire les premières notions de programmation en langage Ada. Exemple 1.3 Problème simple.
Dessiner dans une fenêtre graphique une figure composée de deux formes géométriques, soit un carré et un triangle isocèle. De plus, le programme doit annoncer le début et la fin du dessin dans une fenêtre de texte.
Du fait de sa simplicité, la compréhension de ce problème est immédiate, après avoir précisé que la disposition des formes est libre. Le point 1 de la marche à suivre est fait.
MÉTHODE DE DÉCOMPOSITION PAR RAFFINEMENTS SUCCESSIFS
1.7
10
MÉTHODE DE DÉCOMPOSITION PAR RAFFINEMENTS SUCCESSIFS
Cette méthode est basée sur l’idée que, étant donné un problème à résoudre, il faut le décomposer en sous-problèmes de telle manière que: • chaque sous-problème constitue une partie du problème donné; • chaque sous-problème soit plus simple (à résoudre) que le problème donné; • la réunion de tous les sous-problèmes soit équivalente au problème donné. Il faut ensuite reprendre chaque sous-problème et le décomposer comme cidessus et recommencer jusqu’à ce que chaque sous-problème soit facile à résoudre. Une étape de cette suite de décompositions est appelée raffinement. Une telle méthode est efficace après avoir été utilisée plusieurs fois. Lorsque les problèmes deviennent suffisamment complexes pour que la découverte d’une solu-tion ne soit plus un processus trivial, il est indispensable de l’appliquer systéma-tiquement. Elle donne en général de bons algorithmes résolvant les problèmes posés. Mais, comme toute méthode, elle a cependant ses limites. Celleci devient inutilisable lorsque la complexité des problèmes est tout simplement trop grande pour que la suite de raffinements soit facilement exploitable.
APPLICATION À L’EXEMPLE INTRODUCTIF
1.8 1.8.1
11
APPLICATION À L’EXEMPLE INTRODUCTIF Considérations techniques
Selon le point 2 de la marche à suivre (sect. 1.4), il faut déterminer les points principaux à traiter. Le problème étant en partie géométrique, il faut tout d’abord savoir comment dessiner dans une fenêtre de l’écran d’un ordinateur. La documentation technique nous apprend que: • le système de coordonnées cartésiennes a son origine au point (0, 0) situé
en haut à gauche de la fenêtre graphique; • les axes sont disposés comme le montre la figure 1.1. Figure 1.1 Axes et coordonnées cartésiennes dans une fenêtre graphique.
(0,0)
Y
X
(X,Y)
Ceci étant établi, il faut connaître comment faire apparaître la fenêtre, dessiner un carré et un triangle, ou au moins des segments de droite. De même, il sera nécessaire de savoir comment écrire dans la fenêtre de texte. Ces renseignements constituent le point 2 de la marche à suivre. 1.8.2
Algorithme de résolution
L’algorithme de résolution (point 3, sect. 1.4) va être déterminé en utilisant la méthode décrite dans la section 1.7. Etant donné le problème initial, on en extrait les sous-problèmes: 1 annoncer le début du dessin; 2 ouvrir, faire apparaître la fenêtre de dessin; 3 dessiner le carré; 4 dessiner le triangle isocèle; 5 annoncer la fin du dessin. Ceci constitue le premier raffinement. Comme les points 1, 2 et 5 sont immédiatement traduisibles en Ada, ils seront laissés tels quels. Le raffinement
APPLICATION À L’EXEMPLE INTRODUCTIF
12
suivant est alors: 1 annoncer le début du dessin; 2 ouvrir, faire apparaître la fenêtre de dessin; 3 choisir la position du carré, i.e. celle du sommet en haut à gauche; 4 dessiner le carré avec les côtés parallèles aux axes de coordonnées; 5 choisir la position du triangle isocèle, i.e. celle du sommet gauche; 6 dessiner le triangle isocèle sur la pointe, avec la base parallèle à l’axe des x. 7 annoncer la fin du dessin. Afin de présenter un troisième raffinement, l’environnement Ada à disposition est supposé ne pas permettre le dessin d’un carré ou d’un triangle. La décomposition devient: 1 annoncer le début du dessin; 2 ouvrir, faire apparaître la fenêtre de dessin; 3 choisir la position du carré, i.e. celle du sommet en haut à gauche; 4 dessiner le côté supérieur depuis cette position; 5 dessiner le côté droit depuis l’extrémité droite du côté supérieur; 6 dessiner le côté inférieur depuis l’extrémité inférieure du côté droit; 7 dessiner le côté gauche depuis l’extrémité gauche du côté inférieur; 8 choisir la position du triangle isocèle, i.e. celle du sommet gauche; 9 dessiner la base depuis cette position; 10 dessiner le côté droit depuis l’extrémité droite de la base; 11 dessiner le côté gauche depuis l’extrémité inférieure du côté droit; 12 annoncer la fin du dessin. Cette suite de raffinements s’arrête ici. En effet, même si la norme ne définit aucune opération graphique, des outils supplémentaires présents dans (presque) tous les environnements Ada permettent de dessiner un segment depuis un point courant et d’afficher un mot. La suite d’opérations 1, 2, 3.1, 3.2.1, etc., constitue un algorithme de résolution du problème donné. 1.8.3
Codage de l’algorithme en Ada
Un premier essai de codage de l’algorithme (point 4, sect. 1.4) donne le programme Exemple_Essai_1 (exemple 1.4). Il est correct, simple, fonctionne parfaitement mais est assez mal écrit. En effet, l’art de la programmation doit obéir à des conventions de style telles que celles décrites dans [AUS 95]. En particulier, il faut savoir que: • un programme doit être lisible, ce qu’il n’est pas; • un programme doit être commenté, ce qu’il n’est pas; • le code doit refléter les décisions prises par le programmeur, ici la grandeur
des dessins et les points initiaux des dessins; • les nombres entiers peuvent signifier n’importe quoi! Il faut préciser leur
signification en leur substituant des noms parlants.
APPLICATION À L’EXEMPLE INTRODUCTIF
13
Exemple 1.4 Premier codage (maladroit) de l’algorithme obtenu. with Ada.Text_IO; use Ada.Text_IO; with Spider; use Spider; procedure Exemple_Essai_1 is begin Put_Line ( "Debut du dessin"); Init_Window ("Fenetre de dessin"); Move_To (30, 120); Line (50, 0); Line (0, 50); Line (– 50, 0); Line (0, – 50); Move_To (110, 120); Line (80, 0); Line (– 40, 40); Line (– 40, – 40); Put_Line ( "Fin du dessin"); end;
Malgré son apparente complexité, l’exemple 1.5 ci-après présente un programme source bien écrit et illustrant les points énumérés précédemment. Les qualités de style de ce programme résident en particulier dans: • les indications sur l’auteur, le but du programme (ici réduit au minimum),
• • • •
la date de création, les modifications éventuelles; d’autres informations générales pourraient bien entendu compléter cette introduction; la mention systématique de noms représentant des nombres; les explications décrivant chaque partie du programme; la cohérence dans le choix des identificateurs; la mise en page et l’alignement des lignes.
Il est vrai qu’en pratique, la production de logiciels dans des délais souvent extrêmement réduits conduit parfois à négliger l’effort de présentation du code. D’un autre côté, certaines entreprises imposent des normes strictes à respecter à la lettre. C’est le cas des industries pour lesquelles la qualité et la fiabilité du code produit est impérative comme dans le domaine spatial ou l’avionique par exemple. Exemple 1.5 Codage corrigé de l’algorithme obtenu. ------
Auteur: Dupont Jean But du programme: illustrer un codage soigne Date de creation: 1 octobre 1999 Date de modification: Raison de la modification:
APPLICATION À L’EXEMPLE INTRODUCTIF
14
-- Pour afficher du texte dans la fenetre de texte with Ada.Text_IO; use Ada.Text_IO; -- Pour lire les nombres entiers donnes par l'utilisateur with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; -- Pour travailler avec la fenetre de dessin with Spider; use Spider; -- Ce programme illustre un codage soigne en Ada; il dessine un -- carre et un triangle procedure Exemple_Bien_Fait is Droite Gauche Haut Bas
: : : :
constant constant constant constant
:= := := :=
1; –1; –1; 1;
-- Pour se deplacer d'une unite -- dans les quatre directions
-- Pour un trait vertical, le deplacement horizontal est nul A_L_Horizontale : constant := 0; -- Pour un trait horizontal, le deplacement vertical est nul A_La_Verticale : constant := 0; Cote_Carre : constant := 50; -- Longueur d'un cote du carre Abscisse_Carre : Integer; Ordonnee_Carre : Integer;
-- Abscisse et ordonnee du point -- initial de dessin du carre
Demi_Base : constant := 40;
-- Longueur de la demi-base du -- triangle
Abscisse_Triangle : integer; -- Abscisse et ordonnee du point Ordonnee_Triangle : integer; -- initial de dessin du triangle begin -- Exemple_Bien_Fait -- Presentation du programme a l'utilisateur Put ("Bonjour. Je vais dessiner un carre et un triangle "); Put_Line ("dans une fenetre de dessin."); Put_Line ("Debut du dessin..."); -- Pour pouvoir dessiner dans la fenetre de dessin Init_Window ("Fenetre de dessin"); -- L'utilisateur donne le point initial de dessin du carre Put ("Donnez l'abscisse du point initial du carre: "); Get (Abscisse_Carre); Put ("Donnez l'ordonnee du point initial du carre: "); Get (Ordonnee_Carre); -- Dessin du carre Move_To (Abscisse_carre, Ordonnee_Carre); Line (Cote_Carre * Droite, A_L_Horizontale); Line (A_La_Verticale, Cote_Carre * Bas); Line (Cote_Carre * Gauche, A_L_Horizontale); Line (A_La_Verticale, Cote_Carre * Haut); -- L'utilisateur donne le point initial de dessin du triangle Put ("Donnez l'abscisse du point initial du triangle:"); Get (Abscisse_Triangle); Put ("Donnez l'ordonnee du point initial du triangle:");
APPLICATION À L’EXEMPLE INTRODUCTIF
Get (Ordonnee_Triangle); -- Dessin du triangle. Move_To (Abscisse_Triangle, Ordonnee_Triangle); Line (2 * Demi_Base * Droite, A_L_Horizontale); Line (Demi_Base * Gauche, Demi_Base * Bas); Line (Demi_Base * Gauche, Demi_Base * Haut); -- Message de fin du dessin et du programme Put_Line ("Fin du dessin."); Put_Line ("Fin du programme."); end Exemple_Bien_Fait;
15
STRUCTURE D’UN PROGRAMME ADA
1.9
16
STRUCTURE D’UN PROGRAMME ADA Un programme Ada est composé de quatre parties principales: • La clause de contexte (context clause) qui sera développée par la suite (§
10.5.1). Cette clause doit contenir les outils Ada que le programmeur peut utiliser sans avoir à les construire lui-même, et indispensables au fonctionnement du programme. Ces outils sont regroupés en paquetages, notion présentée ultérieurement (chap. 11). Il est nécessaire de les indiquer pour que le compilateur puisse en tenir compte lors de la phase de vérification et de traduction en code machine. La mention de ces paquetages se fera de manière intuitive en suivant les exemples fournis. Pour le programme Exemple_Bien_Fait la clause de contexte est la suivante: with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Spider; use Spider;
• L’en-tête du programme, où est spécifié le nom du programme. Pour le programme Exemple_Bien_Fait l’en-tête est la suivante: procedure Exemple_Bien_Fait is
• La partie déclarative (declarative part), comprise entre l’en-tête et le mot reservé begin, contenant les déclarations (declarations) des objets
(constantes, variables, etc.) utilisés dans le programme. Ces objets représenteront des données traitées par le programme. La partie déclarative peut encore contenir d’autres déclarations comme des sous-programmes (chap. 4) ou encore des types (chap. 5). • Pour le programme Exemple_Bien_Fait la partie déclarative est la
suivante: Droite Gauche Haut Bas
: : : :
constant constant constant constant
:= := := :=
1; –1; –1; 1;
A_L_Horizontale : constant := 0; A_La_Verticale : constant := 0; Cote_Carre : constant := 50; Abscisse_Carre : Integer; Ordonnee_Carre : Integer; Demi_Base : constant := 40; Abscisse_Triangle : Integer; Ordonnee_Triangle : Integer;
Il faut noter que les déclarations sont groupées logiquement: d’abord les objets généraux pour le dessin, puis ceux concernant le carré, finalement ceux concernant le triangle.
STRUCTURE D’UN PROGRAMME ADA
17
• La partie instructions du programme, comprise entre les mots begin et end, appelée souvent corps (body). Elle contient les instructions
(statements) du programme, c’est-à-dire les actions à entreprendre sur les données. Parmi celles-ci et à titre d’exemple: Put_Line ("Debut du dessin..."); Line (Cote_Carre * Droite, A_L_Horizontale);
C’est dans cette partie que se trouve, sous forme codée, l’algorithme choisi pour résoudre le problème. Il faut encore relever le rappel du nom du programme après le end final. Finalement, il faut insister sur le fait qu’un programme doit être documenté, en particulier par des commentaires initiaux précisant le nom de l’auteur, la date de création, etc., comme suggéré dans l’exemple 1.5. Un guide méthodologique intitulé Style et qualité des programmes Ada 95 [AUS 95], destiné aux professionnels, fournit de nombreuses indications relatives aux différents aspects de l’écriture du code source (sect. 1.11).
CONSTITUANTS D’UN PROGRAMME
18
1.10 CONSTITUANTS D’UN PROGRAMME 1.10.1
Généralités
Un programme est écrit dans un langage de programmation. Ce langage est composé de mots, symboles, commentaires, etc. Ceux-ci sont groupés en phrases dont l’ensemble compose le programme. Les phrases obéissent à des règles et ces règles déterminent de manière absolument stricte si une phrase est correcte ou non, c’est-à-dire si cette phrase respecte la syntaxe (syntax) du langage. L’analogie avec les langues naturelles (français, allemand, etc.) est donc forte (fig. 1.2), la principale différence étant qu’une phrase française peut être formée de manière beaucoup moins rigoureuse et signifier néanmoins quelque chose. Or une phrase en Ada (ou tout autre langage de programmation) doit être absolument juste pour être comprise par le compilateur, sans quoi elle est obligatoirement rejetée! Un programme Ada est composé de phrases appelées unités syntaxiques (syntactic units). Elles sont elles-mêmes constituées de mots, symboles, etc. appelés unités lexicales (lexical units). Chaque unité lexicale est une suite de caractères appartenant au jeu de caractères Ada. Figure 1.2 Analogies avec la langue française.
mots et symbole de ponctuation Ada
est
un
langage
nom sujet
nom verbe article complément verbe
.
identificateurs et symboles Line (
A _ L a _ V e r t i c a l e,
id. de procédure
constante
appel de procédure
paramètre
1.10.2
Jeu de caractères en Ada
Cote_Carre * Bas constante
) ;
constante
paramètre
CONSTITUANTS D’UN PROGRAMME
19
Le jeu de caractères en Ada comporte les caractères décrits dans [ARM 2.1]. Cet ensemble est normalisé ISO sous le nom LATIN-1 et comporte les lettres majuscules et minuscules, les lettres accentuées, les chiffres, les symboles de ponctuation, etc. Il comprend comme sous-ensemble tous les caractères du code ASCII (§ 3.8.1). On distingue les caractères imprimables (printable characters), lisibles, comme ceux cités ci-dessus, des caractères non imprimables qui incluent les caractères de contrôle (control characters) interprétés de manière spéciale par l’implémentation (par exemple des caractères de contrôle comme le tabulateur horizontal (tab), le retour de chariot (return), le saut de ligne (line feed), etc.). 1.10.3
Unités lexicales en Ada
Diverses unités lexicales existent en Ada: • Les identificateurs (identifiers), comme par exemple A, Limite_100, Cote_Carre, Put, etc. Un identificateur est un mot composé de lettres, de
chiffres et du caractère _ et commençant obligatoirement par une lettre. S’il n’y a aucune limite théorique au nombre de caractères composant un identificateur, la norme Ada laisse à l’implémentation le soin de définir le nombre maximum de caractères significatifs, tout en précisant cependant que ce maximum doit être d’au moins deux cents caractères. Pour respecter les bonnes habitudes de programmation, les identificateurs doivent être parlants pour les lecteurs du programme! Les caractères significatifs servent à différencier deux identificateurs. Il faut relever que la casse (majuscule/ minuscule) n’est pas significative! Le langage contient des identificateurs prédéfinis (predefined identifiers) comme Integer, Float, Text_IO, Constraint_Error, etc., que le programmeur peut éventuellement redéfinir dans son programme, mais dont le but principal reste néanmoins d’être utilisés tels quels, dans les situations pour lesquelles ils ont été créés. L’annexe [ARM A.1] en donne la liste. • Les mots réservés (reserved words) qui sont des identificateurs restreints à un usage bien défini (toujours le même!), par exemple procedure, is, begin, etc. La liste est fournie dans [ARM 2.9]. • Les symboles (symbols) formés d’un unique caractère ou de deux caractères
accolés, comme < > = <= := + – , ;
la liste exhaustive se trouve dans [ARM 2.2]. • Les commentaires (comments), c’est-à-dire n’importe quels caractères imprimables typographiés après le symbole -- et ce jusqu’à la fin de la ligne
(fin de commentaire). Ils servent à expliquer le pourquoi et le comment des constructions auxquelles ils s’appliquent.
CONSTITUANTS D’UN PROGRAMME
20
• Les constantes numériques entières ou réelles ou plus simplement les
nombres comme 123 –36 24E3 12.3 –234.0E3 –0.3E–2
les sections 2.2 et 2.3 définiront précisément ces constantes. • Les constantes caractères ou plus simplement les caractères, c’est-à-dire
un seul caractère imprimable entre apostrophes comme 'a' '?' 'A' '='
la section 3.8 définira précisément ces constantes. • Les constantes chaînes de caractères ou plus simplement les chaînes de
caractères, c’est-à-dire une suite d’un ou de plusieurs caractères (éventuellement aucun) entre guillemets comme "abcd" "CARRE" "triangle de Pascal" "="
la section 9.2 définira précisément ces constantes. Il est parfois possible, voire pratique, d’accoler deux unités lexicales. Mais en règle générale, pour augmenter la lisibilité, deux unités lexicales seront séparées par au moins un espace, une tabulation ou une fin de ligne. 1.10.4
Unités syntaxiques et diagrammes syntaxiques
Les unités syntaxiques, comme les unités lexicales d’ailleurs, peuvent être décrites par des diagrammes appelés diagrammes syntaxiques. Un diagramme syntaxique (syntactic diagram) permet donc de décrire l’orthographe d’une unité lexicale ou, comme son nom l’indique, la syntaxe (grammaire) d’une phrase du programme. Toute phrase est donc composée d’une séquence d’unités lexicales. Il est ainsi possible de vérifier si une instruction d’un programme en cours d’écriture est correcte en vérifiant sa syntaxe à l’aide des diagrammes syntaxiques du langage, Ada en l’occurrence.
CONSTITUANTS D’UN PROGRAMME
21
Figure 1.3 Diagrammes syntaxiques définissant un identificateur et un chiffre. chiffre
0
1
2
3
4
5
identificateur
6
7
8
9
_ lettre
chiffre
lettre
Cette vérification est simple à réaliser; il suffit de parcourir le diagramme syntaxique dans le sens des flèches. Par exemple, la figure 1.3 montre un tel diagramme, permettant de vérifier qu’un «mot» est bien un identificateur. En suivant le diagramme syntaxique on rencontre une première bulle indiquant que le premier caractère d’un identificateur doit être une lettre. Si c’est le cas l’analyse peut continuer avec un choix: soit l’identificateur ne comporte qu’une lettre et le diagramme est quitté, soit il comporte d’autres caractères (lettres, chiffres ou _ ) et il faut alors suivre les autres chemins proposés par le diagramme et vérifier au fur et à mesure s’ils peuvent correspondre aux caractères constitutifs de la suite de l’identificateur. Si le diagramme est quitté, l’unité lexicale ou syntaxique analysée est correcte. Mais si l’on reste bloqué dans le diagramme sans possibilité d’en sortir, cela correspond à une erreur d’orthographe ou de syntaxe. Il existe trois sortes de bulles dans les diagrammes syntaxiques: les cercles, les rectangles à «coins» arrondis et ceux à angle droit. Si les coins sont à angle droit, cela signifie un renvoi à une entité syntaxique différente. Il faut ainsi se reporter à un autre digramme, pour continuer l’analyse et en revenir en cas de succès, et ainsi de suite. Si les coins sont arrondis ou si c’est un cercle, alors le contenu représente un symbole ou un mot réservé qui devra être présent tel quel dans le texte analysé. Les bulles de la figure 1.3 contenant les mots lettre et chiffre renvoient donc à deux autres diagrammes syntaxiques dont l’un des deux est donné dans la même figure. Par contre, celles contenant les chiffres de 0 à 9, ou le caractère _ montrent que ces caractères doivent apparaître tels quels dans un nombre, ou un identificateur.
MISE EN PAGE D’UN PROGRAMME
22
1.11 MISE EN PAGE D’UN PROGRAMME La mise en page d’un programme Ada est assez libre. Cependant, et pour des raisons évidentes de clarté et de lisibilité d’un programme, il existe des conventions de style qu’il faut respecter, décrites dans [AUS 95]. En particulier il est demandé: • de commenter chaque déclaration ou instruction importante en indiquant la
• • • •
raison de sa présence; ce commentaire se place avant ou à côté de la déclaration ou instruction; de choisir des identificateurs faciles à comprendre; d’assurer un degré élevé de systématique et de cohérence; d’effectuer une seule déclaration ou instruction par ligne; d’indenter, c’est-à-dire de décaler vers la droite les déclarations ou instructions contenues dans une autre déclaration ou instruction.
23
1.12 EXERCICES 1.12.1
Conception d’un algorithme
Ecrire un algorithme d’appel téléphonique à partir d’une cabine publique. 1.12.2
Déclarations et instructions
En Ada, peut-on mélanger des déclarations et des instructions dans une partie déclarative? Dans un corps? 1.12.3
Utilisation d’un diagramme syntaxique
Utiliser le diagramme syntaxique de la figure 1.3 pour déterminer si les termes suivants sont des identificateurs en Ada: Ada pRemiEr Découpe Bon_Appetit Ah__Ah Fin_ A+Tard Quoi?
POINTS À RELEVER
24
1.13 POINTS À RELEVER 1.13.1
En général • Un algorithme est une suite d’opérations à effectuer pour résoudre un
problème. • Saisie
du programme, compilation, édition de liens, exécution, déverminage constituent les phases de création de tout programme.
• Algol-60, Pascal, Modula-2, Ada, Ada 95 forment une famille de langages
de programmation. • C, C++, Java forment une autre famille de langages de programmation. • Le terme Ada est utilisé dans cet ouvrage pour nommer la version 1995 du
langage de programmation Ada normalisé la première fois en 1983. • Le respect des bonnes habitudes de programmation aboutit à des
programmes plus simples et plus lisibles. • Fiabilité, lisibilité, simplicité, justesse et efficacité des programmes sont
des propriétés parmi les plus importantes. • La méthode de décomposition par raffinements successifs aboutit à un
algorithme de résolution du problème initial. • Les unités lexicales constituent les mots du texte du programme alors que
les unités syntaxiques en forment les phrases. • Les diagrammes syntaxiques permettent de s’assurer que la syntaxe d’une
construction est correcte. • Les identificateurs nomment les entités d’un programme et doivent
toujours être parlants. • Les commentaires expliquent les raisons de la présence des constructions
auxquelles ils s’appliquent. 1.13.2
En Ada • Un programme principal Ada est constitué dans l’ordre de la clause de
contexte, l’en-tête, la partie déclarative et la partie instructions. • Les identificateurs sont composés d’une suite de lettres ou de chiffres,
éventuellement séparés par le caractère _ et doivent obligatoirement commencer par une lettre.
25
C H A P I T R E
NOMBRES ET ENTRÉES -SORTIES
2
26
NOMBRES ET ENTRÉES-SORTIES DE BASE
RAPPELS
2.1
27
RAPPELS Un programme Ada se compose de quatre parties: • • • •
la clause de contexte; l’en-tête; la partie déclarative; la partie instructions.
La partie déclarative contient les déclarations des objets (constantes, variables, etc.) représentant les données que l’on veut traiter. Le corps contient les instructions du programme, c’est-à-dire les traitements à effectuer sur ces données. Le présent chapitre porte sur la représentation de ces données, en se limitant pour l’instant à deux catégories de nombres: les nombres entiers et réels.
TYPES ENTIERS
2.2 2.2.1
28
TYPES ENTIERS Motivation
Les nombres entiers sont traditionnellement utilisés les premiers dans un cours d’apprentissage de la programmation. En effet, ils ne posent pas (ou peu) de problèmes. Le travail avec eux est naturel puisqu’il correspond à de l’arithmétique. Un réflexe doit cependant être acquis le plus vite possible, il est mis en évidence dans la note 2.1. NOTE 2.1 L’ensemble des nombres entiers n’est pas infini sur un ordinateur. Tous les nombres entiers ne sont évidemment pas utilisables sur un ordinateur; en effet, s’il existe en théorie une infinité de tels nombres, chacun d’entre eux occupe un ou quelques mots en mémoire et, de ce fait, l’ensemble des entiers représentables sur un ordinateur est fini. Généralement, les entiers sont représentés par 16 ou 32 bits, éventuel-lement 8 bits (§ 2.5.2). Les machines sur lesquelles ils occupent 64 bits ne sont encore pas très courantes.
2.2.2
Généralités
L’identificateur Integer est un identificateur prédéfini. Il signifie: «un objet (constante, variable, etc.) de type Integer contiendra un nombre entier et rien d’autre». Les variables Abscisse_Carre, Ordonnee_Carre, etc., du programme Exemple_Bien_Fait (§ 1.8.3) sont de type Integer. Le type Integer fait partie des types discrets (discrete types, [ARM 3.2]). Il faut cependant déjà mentionner qu’il existe d’autres types qu’Integer pour les entiers (§ 2.2.7). Mais tous les nombres entiers (non explicitement basés) doivent respecter la syntaxe décrite par la figure 2.1. Les constantes entières sont les nombres entiers eux-mêmes. Ces nombres peuvent être décimaux ou basés. Un nombre basé est un nombre écrit dans une base explicitement mentionnée, comprise entre 2 et 16. Le caractère _ est utile pour la lisibilité d’un nombre mais n’a aucune signification particulière.
TYPES ENTIERS
29
Figure 2.1 Diagrammes syntaxiques définissant un nombre entier décimal. Numéral _
chiffre chiffre
Exposant positif E Numéral e +
Nombre entier décimal
Numéral Exposant positif
Exemple 2.1 Nombres entiers décimaux ou basés. Nombres entiers décimaux: 10 12E6 12e6 12E+6 1_0 1_000 1_0E2 1E1_0 Le nombre entier 12 dans différentes bases (binaire, octale, décimale, hexadécimale): 2#1100# 2#0000_1100# 8#14# 10#12# 16#c# 16#C#
Les opérations arithmétiques possibles sur les valeurs entières se répartissent en deux catégories: les opérations unaires + – abs où
TYPES ENTIERS
30
• le symbole + représente l’identité et le symbole – l’opposé; • le mot réservé abs représente l’opération «valeur absolue»;
les opérations binaires + – * / ** rem mod où • le symbole + représente l’addition et le symbole – la soustraction; • le symbole * représente la multiplication et le symbole / la division
(entière); • le symbole ** représente l’exponentiation (exposant entier positif ou nul); • le mot réservé rem représente le reste de la division entière (euclidienne); notons les relations suivantes: A rem (–B) = A rem B et (–A) rem B = –(A rem B); • le mot réservé mod représente l’opération mathématique modulo qui ne sera pas détaillée ici, ni d’ailleurs les différences entre mod et rem; il suffit de préciser que mod est équivalent à rem si les deux opérandes sont positifs. Les opérations + – * / ** abs rem mod sont appelées opérateurs (operators), alors que les valeurs sur lesquelles ils opèrent sont les opérandes (operands). Les expressions (expressions) entières sont des combinaisons de ces opérations. L’évaluation (le calcul) d’une expression (expression evaluation) consiste à trouver la valeur (le résultat) de l’expression. Il faut alors prendre garde au fait qu’une telle expression n’est pas toujours calculable (note 2.2)! En effet, une division par zéro est impossible ou encore l’élévation d’un nombre à une trop grande puissance provoque un débordement de capacité (overflow), c’est-à-dire l’obtention d’une valeur non représentable sur la machine utilisée (note 2.1). Lorsque le calcul de l’expression n’est pas possible, une erreur sera générée à l’exécution du programme. NOTE 2.2 Attention aux dépassements de capacité. En Ada, le calcul d’une expression provoquera une erreur à l’exécution (en général Constraint_Error, § 6.3.2) si la valeur obtenue n’est pas représentable sur la machine ou si elle est hors de l’intervalle des valeurs du type utilisé. Une expression doit donc être conçue de manière à ne jamais provoquer de telles erreurs. Si cette garantie ne peut pas être obtenue (utilisation de valeurs données à l’exécution, par exemple par l’utilisateur), Ada offre un mécanisme de traitement des erreurs (sect. 6.3).
Exemple 2.2 Expressions entières. 2est une expression réduite à une seule constante de valeur 2; 3 + 4est une expression de valeur 7, l’opérateur est + et les opérandes sont les constantes
TYPES ENTIERS
31
3 et 4; –2est une expression de valeur –2; abs (–2)est une expression de valeur 2; 2 ** 8est une expression de valeur 256; 4 * 5est une expression de valeur 20; 4 / 2est une expression de valeur 2; 5 / 2est aussi une expression de valeur 2; 4 rem 2 et 4 mod 2sont deux expressions de valeur 0; 5 rem 2 et 5 mod 2sont deux expressions de valeur 1; 5 rem (–2)est une expression de valeur 1; (–5) rem 2est une expression de valeur –1; 2 + 3 * 4est une expression qui vaut 14 (sect. 2.4); (2 + 3) * 4est une expression qui vaut 20; Nombre + 1est une expression additionnant 1 à la valeur actuelle de la variable Nombre (dépassement de capacité possible); (Nombre + 10) / Nombreest une expression correcte ((dépassement de capacité possible lors de l’adition ou si Nombre vaut 0).
Les parenthèses permettent de définir des sous-expressions et un ordre d’évaluation de celles-ci. Mais les opérateurs sont également classés en niveaux de priorité (sect. 2.4). 2.2.3
Affectation
Le mot affectation (assignment) signifie «donner une valeur à». Parler d’affectation de 3 à la variable Nombre signifie que l’on veut que la variable Nombre prenne dès lors la valeur 3. Une variable est donc un objet dont la valeur peut être modifiée au cours de l’exécution d’un programme, alors qu’une constante voit sa valeur fixée lors de sa déclaration (§ 3.9.1). Toute affectation entre valeur et variable entières s’écrit (exemple 2.3): nom_de_variable_entière := expression_de_type_Integer;
La sémantique de n’importe quelle affectation s’exprime par un algorithme: 1 obtenir la valeur de tous les opérandes de l’expression; 2 calculer la valeur de l’expression; 3 remplacer le contenu de la variable par cette valeur. ATTENTION: lors de l’affectation, une erreur se produira si la variable ne peut pas accueillir la valeur calculée. Exemple 2.3 Affectations d’expressions entières à une variable entière. -- ... procedure Exemple_2_3 is
TYPES ENTIERS
32
Max : constant := 5; Nombre : Integer;
-- Une constante entiere -- Une variable entiere
begin -- Exemple_2_3 Nombre Nombre Nombre Nombre Nombre ...
2.2.4
:= := := := :=
5; Nombre + 4; (36 / 10) * 2; Nombre / 2; Max;
-------
Affecte Affecte Affecte Affecte Affecte Nombre
la la la la la
valeur valeur valeur valeur valeur
5 a Nombre 9 a Nombre 6 a Nombre 3 a Nombre de Max a
Dangers liés aux variables non initialisées
Soit le morceau de programme donné dans l’exemple 2.4. Exemple 2.4 Attention aux variables non initialisées. -- ... procedure Exemple_2_4 is Nombre : Integer; valeurs Resultat : Integer;
-- Deux variables entieres sans -- initiales definies
begin -- Exemple_2_4 Resultat := Nombre + 1; ...
Que vaut Resultat après l’affectation? La réponse est que Resultat a une valeur indéfinie (undefined value) car la valeur de Nombre n’était pas définie au moment de l’affectation. La variable non initialisée Nombre possédait en fait une valeur entière calculée à partir de la suite de bits du mot mémoire utilisé pour cette variable. L’état de ces bits dépend de l’état (électrique) de la mémoire! L’utilisation de variables déclarées mais non initialisées, c’est-à-dire ne possédant pas de valeur définie au moment de leur utilisation, est une erreur très courante. De plus, la détection de ces erreurs est difficile puisqu’il est possible que le programme s’exécute tout de même, en produisant évidemment n’importe quels résultats à commencer par des résultats corrects! Lors de l’utilisation de la valeur d’une variable, il faut donc toujours s’assurer que cette variable possède une valeur bien définie (note 2.3). NOTE 2.3 Toute variable doit être initialisée. Le programmeur qui utilise la valeur d’une variable, de quelque type que ce soit, doit être certain que
TYPES ENTIERS
33
cette valeur est toujours bien définie.
2.2.5
Attributs First, Last, Succ et Pred
Un attribut (attribute) en Ada est une construction offerte par le langage et permettant d’obtenir une certaine caractéristique d’une entité. Les attributs First et Last sont prédéfinis et donnent la première, respectivement la dernière valeur d’un intervalle ou d’une suite; les attributs Succ et Pred sont aussi prédéfinis et fournissent la valeur suivante, respectivement précédente d’une valeur donnée entre parenthèses (exemple 2.5). Exemple 2.5 Utilisation des attributs First, Last, Succ et Pred. Integer'First donne le nombre le plus petit des entiers du type Integer; Integer'Lastdonne le nombre le plus grand des entiers du type Integer; Integer'Succ(0)donne 1; Integer'Pred(0)donne –1; Integer'Succ(Nombre)donne la valeur Nombre+1; Integer'Pred(Nombre + 1)donne la valeur Nombre; Integer'Pred(Integer'Last)donne la valeur précédent le plus grand des
entiers.
2.2.6
Généralités sur les attributs
Comme mentionné auparavant, un attribut en Ada est un outil offert par le langage qui permet d’obtenir une caractéristique d’une entité. Il faut noter la syntaxe un peu surprenante qui utilise l’apostrophe pour indiquer que l’on a affaire à un attribut placé après celle-ci, ainsi que la présence obligatoire d’un identificateur avant l’apostrophe. Attention à la note 2.2! Par exemple Integer'Succ(Integer'Last) n’existe pas et le calcul de cette expression provoquera une erreur à la compilation du programme. Finalement, le calcul d’un attribut est l’une des opérations les plus prioritaires dans une expression. En particulier, un attribut est calculé avant n’importe quel opérateur. 2.2.7
Types Short_Integer et Long_Integer
Le langage Ada a la particularité de fournir différents types pour, dans notre cas, traiter les nombres entiers. La norme autorise une implémentation à offrir les
TYPES ENTIERS
34
types Short_Integer et Long_Integer avec les particularités suivantes: • le type Short_Integer a les mêmes caractéristiques qu’Integer mais l’intervalle [Short_Integer'First ; Short_Integer'Last] (ex-
primé en notation mathématique) est plus petit que celui représenté par [Integer'First ; Integer'Last]; • le type Long_Integer a les mêmes caractéristiques qu’Integer mais l’intervalle [Long_Integer'First ; Long_Integer'Last] (exprimé
en notation mathématique) est plus grand que celui représenté par [Integer'First ; Integer'Last]. Exemple 2.6 Valeurs possibles pour les nombres les plus petits et les plus grands des types entiers.
Avec Integer sur 16 bits:Integer'First vaut –2**15; Integer'Last vaut 2**15–1; Avec Short_Integer sur 8 bits:Short_Integer'First vaut –2**7; Short_Integer'Last vaut 2**7–1; Avec Long_Integer sur 32 bits:Long_Integer'First vaut –2**31; Long_Integer'Last vaut 2**31–1;
L’existence de ces différents types entiers permet donc, en choisissant l’un plutôt que l’autre, de limiter ou d’augmenter l’intervalle des nombres entiers utilisables pour une ou plusieurs valeurs. Mais il faut toujours se rappeler que le nombre de bits, donc l’intervalle des nombres représentés, attribués à ces différents types, dépend de l’implémentation. Enfin, le langage permet de définir d’autres types entiers (sect. 6.2).
TYPES RÉELS
35
2.3
TYPES RÉELS
2.3.1
Motivation
Les nombres réels sont indispensables dans les applications dites numériques de la programmation (programmes d’analyse numérique, de statistiques, de calcul par éléments finis, de régulation numérique, etc.). Dans la majorité des autres cas, les nombres réels sont peu fréquents. Leur utilisation, sans un minimum de précautions, peut s’avérer dangereuse du fait des erreurs de calcul provoquées par leur représentation en mémoire (§ 2.5.2). Comme pour les entiers, l’ensemble des nombres réels représentables sur un ordinateur est fini. Ces nombres réels vont être présentés sans trop de détails en commençant par les réels en virgule flottante et en laissant de côté (pour l’instant) les réels en virgule fixe. 2.3.2
Généralités
Float est un identificateur prédéfini. Il signifie «un objet (constante, variable, etc.) de type Float contiendra un nombre réel en virgule flottante et rien d’autre».
Les nombres réels (non explicitement basés) doivent respecter la syntaxe donnée à la figure 2.2. Le type Float fait partie des types numériques (numeric types, [ARM 3.2]). Les constantes réelles sont les nombres réels eux-mêmes. Ils peuvent être décimaux ou basés mais l’on ne décrira que les nombres décimaux (exemple 2.7). Le caractère _ s’utilise comme dans les nombres entiers, pour en faciliter la lecture. Figure 2.2 Diagrammes syntaxiques définissant un nombre réel décimal.
Nombre réel décimal
Numéral
.
Numéral Exposant
Exposant E Numéral e +
TYPES RÉELS
36
Exemple 2.7 Nombres réels décimaux (comparer avec l’exemple 2.1).
10.0
12.0E6
12.0e6
12.0E+6
1_0.0
1_000.0
1_0.0E2
1.0E1_0
12.0E–6
Les opérateurs arithmétiques possibles sur les valeurs réelles se répartissent en deux catégories: les opérateurs unaires + – abs où • le symbole + représente l’identité et le symbole – l’opposé; • le mot réservé abs représente l’opération «valeur absolue»;
et les opérateurs binaires + – * / ** où • le symbole + représente l’addition et le symbole – la soustraction; • le symbole * représente la multiplication et le symbole / la division; • le symbole ** représente l’exponentiation (exposant entier positif, négatif
ou nul). Les expressions réelles sont des combinaisons de ces opérations, comme illustré dans l’exemple 2.8. Exemple 2.8 Expressions réelles. 1.0est
une expression réduite à une seule constante de valeur 1.0 qui peut également s’écrire 1.0e0 ou 0.1e1 ou 0.1E+1 ou encore 10.0e–1; 2.0 ** (–8)est une expression de valeur 256–1; –3.0 + 4.0est une expression de valeur 1.0; 4.3 * 5.0e0est une expression de valeur 21.5; 4.0 / 2.0est une expression de valeur 2.0; 5.0 / 2.0est une expression de valeur 2.5 (comparer avec l’exemple 2.2); 2.0 + 3.0 * 4.0est une expression qui vaut 14.0 (sect. 2.4); 2.0 + (3.0 * 4.0)est une expression qui vaut 14.0; (2.0 + 3.0) * 4.0est une expression qui vaut 20.0.
TYPES RÉELS
2.3.3
37
Affectation
L’affectation s’effectue comme pour les entiers (§ 2.2.3): nom_de_variable_réelle := expression_de_type_Float;
2.3.4
Attributs First, Last et Digits
Comme pour les entiers, Ada offre des attributs pour les nombres réels. Les attributs First et Last ont la même signification que pour les entiers. L’attribut prédéfini Digits donne le nombre maximum de chiffres significatifs. Par exemple, et selon l’implémentation, Float'Digits peut donner la valeur 6. 2.3.5
Types Short_Float et Long_Float
De manière analogue aux entiers, la norme autorise une implémentation à offrir les types Short_Float et Long_Float avec les particularités suivantes: • le type Short_Float a les mêmes caractéristiques que Float mais le nombre de chiffres significatifs est plus petit que celui de Float; • le type Long_Float a les mêmes caractéristiques que Float mais le nombre de chiffres significatifs est plus grand que celui de Float, avec un
minimum de 11. L’intervalle des valeurs de ces deux types sera probablement également différent de celui de Float.
PRIORITÉ DES OPÉRATEURS ARITHMÉTIQUES
2.4
38
PRIORITÉ DES OPÉRATEURS ARITHMÉTIQUES
En mathématiques une question se pose avec une expression telle que 2+3*4: quel est le résultat de ce calcul? Est-ce 20 (addition de 2 et 3 puis multiplication par 4) ou 14 (ajouter 2 à 3*4)? Le même problème se pose en programmation. Pour le résoudre il faut tenir compte des priorités définies dans le langage (exemple 2.9). Celles propres à Ada sont, pour les opérateurs vus jusqu’ici et dans l’ordre de priorité décroissante: • • • •
les opérateurs ** et abs; les opérateurs binaires * / rem et mod ; les opérateurs unaires – et +; les opérateurs binaires – et +.
Dans chacun de ces groupes les opérateurs sont de même priorité. Lors de l’évaluation d’une expression comprenant des opérateurs de même priorité, ceuxci s’appliquent de gauche à droite. Exemple 2.9 Applications de la priorité des opérateurs. Integer'First + 10calcul de Integer'First puis addition de 10; 2 + 3 + 4donne 9 avec calcul de gauche à droite; 2 + 3 * 4donne 14 car la sous-expression 3*4 est d’abord évaluée (priorité
de * par rapport à + ); 2.0 * 3.0 / 4.0donne 1.5 car l’expression est calculée de gauche à droite (opérateurs de même priorité); 2 * 3 + 4 / 3donne 7 car la sous-expression 2*3 est d’abord évaluée puis la sous-expression 4/3 est calculée enfin l’addition des résultats partiels 6 et 1 est effectuée. Il est naturellement possible de préciser l’ordre d’évaluation (c’est-à-dire l’ordre de calcul des constituants) d’une expression en utilisant les parenthèses ( et ) comme en mathématiques. Les sous-expressions entre parenthèses sont alors calculées en priorité (exemple 2.10). Exemple 2.10 Parenthèses, expressions et priorités des opérateurs. (2 + 3) * 4 2+3 donne 5, multiplié par 4 donne 20; 2 + (3 * 4)3*4 donne 12, ajouté à 2 donne 14 (parenthèses inutiles); 3 * (1 + 2) + 41+2 donne 3, multiplié par 3 donne 9, ajouté à 4 donne 13; abs (–2.0)donne 2.0, avec les parenthèses indispensables; 3.0 * (–2.0)donne – 6.0, avec les parenthèses indispensables;
PRIORITÉ DES OPÉRATEURS ARITHMÉTIQUES
39
5.0 ** (–2)donne 25.0–1, avec les parenthèses indispensables; (2 ** 3) ** 4donne 4096, avec les parenthèses indispensables.
Les parenthèses des quatre derniers exemples sont indispensables pour respecter la syntaxe Ada car en écrivant par exemple 3.0*–2.0, l’ordre de priorité imposerait de calculer d’abord 3.0*–, ce qui n’a aucun sens!
REMARQUES RELATIVES AUX TYPES ENTIERS ET RÉELS
2.5
40
REMARQUES RELATIVES AUX TYPES ENTIERS ET RÉELS
2.5.1
Conversions de types
Il est interdit par la norme d’écrire des expressions composées d’un mélange d’opérandes entiers et réels. S’il est nécessaire de former de telles expressions, alors il faut décider si leur valeur sera entière ou réelle et utiliser des conversions explicites de type (exemple 2.11) entre Integer et Float pour s’assurer que chaque sous-expression (opérande opérateur opérande) est formée d’opérandes de même type. Une conversion explicite de type a la forme: nom_de_type ( expression )
où • le nom_de_type sert à convertir la valeur de l’expression en une valeur de
ce type. Exemple 2.11 Exemples de conversions explicites entre Integer et Float. Float ( Integer Float ( Integer Integer
5 )5 est converti en 5.0; ( 2.03 )2.03 est converti en 2; –800 ) / 2.5E2l’expression vaut –3.2; ( 3.5 ) 3.5 est converti en 4; ( –2.5 ) –2.5 est converti en –3.
La conversion d’une valeur réelle en une valeur entière est faite par arrondi vers l’entier le plus proche. 2.5.2
Valeurs entières et réelles utilisables en Ada
Les valeurs entières utilisables sont celles comprises dans l’intervalle [System.Min_Int;System.Max_Int] (en notation mathématique) dont les
deux bornes ont bien entendu une valeur dépendant de l’ordinateur et du compilateur Ada utilisé. Sans entrer maintenant dans les détails, il faut souligner que les constantes Min_Int et Max_Int sont mises à disposition par le paquetage prédéfini System (sect. 19.3). Tous les types entiers ont leur domaine de définition (§ 2.2.7) inclus dans l’intervalle ci-dessus. Les valeurs réelles en virgule flottante utilisables dépendent de la représentation en mémoire des nombres réels. Un tel nombre est enregistré sous forme d’une mantisse et d’un exposant, le tout implémenté généralement sur 32 ou 64 bits. Par exemple, le nombre 0.10012e13 est pour le programmeur constitué de 0.10012 pour la mantisse et de 13 pour l’exposant. Mais en réalité, le nombre de chiffres significatifs est en relation avec le nombre de bits dédiés à la représentation de la mantisse; les bits restants sont eux utilisés pour l’exposant.
REMARQUES RELATIVES AUX TYPES ENTIERS ET RÉELS
41
Dans tous les cas il faut se méfier lors de calculs avec les nombres réels, particulièrement si ces nombres sont grands ou petits. En effet, une addition telle que 1.0e45 + 1.0e–40 donne 1.0e45 ! RAPPEL: les décimales dont la position excède le nombre de chiffres significatifs ne signifient plus rien!
DIALOGUE PROGRAMME-UTILISATEUR
2.6
42
DIALOGUE PROGRAMME-UTILISATEUR
2.6.1
Motivation
La qualité du dialogue entre l’utilisateur et un logiciel est une propriété fondamentale d’une partie dudit logiciel appelée interface homme-machine (user interface). Ce dialogue est rendu possible par l’existence de périphériques spécialisés comme le clavier, la souris, le microphone, etc., qui permettent l’entrée d’informations dans la machine, et de périphériques comme l’écran, l’imprimante, le traceur ou les hauts-parleurs qui permettent la sortie de résultats ou de messages pour l’utilisateur. L’importance du dialogue réside dans le fait que plus celui-ci est compréhensible, cohérent et agréable, plus l’utilisateur acceptera ou aura envie d’utiliser le logiciel. Sans vouloir approfondir ici la notion de dialogue (cela ne fait pas partie d’une introduction à la programmation), il faut simplement mentionner qu’il consiste entre autres pour l’utilisateur en: • • • •
l’introduction des données; la commande du logiciel; la compréhension du déroulement des opérations; la compréhension des résultats obtenus;
et pour le programme, en: • • • • •
la demande des données nécessaires à son exécution; la production de résultats lisibles et clairement présentés; la quittance des opérations importantes effectuées; la mise en garde de l’utilisateur en cas de donnée erronée; la mise en garde de l’utilisateur en cas de commande erronée ou dangereuse.
Même s’il existe des logiciels spécialisés de conception et de réalisation d’interfaces homme-machine, la programmation d’un dialogue textuel en Ada se basera sur les outils de base que sont d’une part les deux opérations Get et Get_Line (§ 9.2.3) pour la lecture de données (nombres, caractères...), d’autre part Put et Put_Line pour l’affichage de messages ou de résultats. Ces quatre opérations sont mises à disposition dans des paquetages (sect. 10.2) prédéfinis, spécialisés dans les entrées-sorties de texte tels que Ada.Text_IO, les entréessorties d’entiers Ada.Integer_Text_IO ou de réels Ada.Float_Text_IO. Dans nos programmes et pour des raisons de simplicité, l’utilisation d’autres mécanismes indispensables à tout interface homme-machine actuel ou futur comme la souris, la reconnaissance vocale, les écrans tactiles, etc., sera complètement ignorée. 2.6.2
Lecture de nombres entiers ou réels
Pour effectuer la lecture d’une valeur entière ou réelle en respectant les
DIALOGUE PROGRAMME-UTILISATEUR
43
quelques principes cités au paragraphe 2.6.1, le programme doit, dans l’ordre: • • • • •
afficher un message clair à l’utilisateur pour lui indiquer ce qu’il doit faire; attendre que l’utilisateur ait introduit la valeur; lire la valeur; vérifier la validité de la valeur obtenue (§ 6.3.5); reprendre son exécution.
L’exemple 2.12 illustre l’utilisation des opérations d’entrées-sorties et réalise une ébauche de dialogue entre le programme et l’utilisateur. Il faut bien comprendre que ce dialogue obéit à un protocole fixé dans le programme et que l’utilisateur doit suivre. Exemple 2.12 Ebauche de dialogue entre un programme et son utilisateur. with Ada.Text_IO; with Ada.Integer_Text_IO; with Ada.Float_Text_IO; -- Calcul du volume d'un mur de briques procedure Ebauche_Dialogue is -- Nombre de briques en longueur Longueur_Mur : Integer; -- Nombre de briques en hauteur Hauteur_Mur : Integer; -- Dimensions d'une brique Longueur_Brique : Float; Largeur_Brique : Float; Hauteur_Brique : Float; -- Volume du mur de briques Volume : Float; -- Autres declarations... ... begin -- Ebauche_Dialogue -- Presentation du programme... ... -- Obtenir le nombre de briques formant le mur en longueur Ada.Text_IO.Put ( "Donnez le nombre de briques " ); Ada.Text_IO.Put ( "(longueur du mur): " ); Ada.Integer_Text_IO.Get ( Longueur_Mur ); -- Obtenir le nombre de briques formant le mur en hauteur Ada.Text_IO.Put ( "Donnez le nombre de briques " ); Ada.Text_IO.Put ( "(hauteur du mur): " ); Ada.Integer_Text_IO.Get ( Hauteur_Mur ); -- Obtenir les dimensions d'une brique Ada.Text_IO.Put ( "Donnez les dimensions d'une brique. " ); Ada.Text_IO.Put ( "La longueur: " );
DIALOGUE PROGRAMME-UTILISATEUR
Ada.Float_Text_IO.Get Ada.Text_IO.Put ( "La Ada.Float_Text_IO.Get Ada.Text_IO.Put ( "La Ada.Float_Text_IO.Get
44
( Longueur_Brique ); largeur: " ); ( Largeur_Brique ); hauteur: " ); ( Hauteur_Brique );
-- Calcul du volume du mur Volume := Longueur_Brique * Largeur_Brique * Hauteur_Brique * Float ( Longueur_Mur * Hauteur_Mur ); -- Montrer a l'utilisateur la valeur du volume du mur Ada.Text_IO.Put ( " Le volume du mur de briques vaut : " ); Ada.Float_Text_IO.Put ( Volume ); Ada.Text_IO.New_Line; ... end Ebauche_Dialogue;
Comment la lecture des dimensions d’une brique et du mur est-elle effectuée? L’exemple de la figure 2.3 va illustrer cette opération. Figure 2.3 Lecture de valeurs données par l’utilisateur.
programme
Ada.Float_Text_IO.Get(Longueur_Brique); Ada.Float_Text_IO.Get(Largeur_Brique); Ada.Float_Text_IO.Get(Hauteur_Brique);
ce que l’utilisateur a donné
30.0
20.0
15.5
Après l’exécution de Ada.Float_Text_IO.Get(Longueur_Brique); la variable Longueur_Brique vaut 30.0, Largeur_Brique et Hauteur_Brique sont indéfinies. Après l’exécution de Ada.Float_Text_IO.Get(Largeur_Brique); la variable Largeur_Brique vaut 20.0, Longueur_Brique garde sa valeur 30.0 et Hauteur_Brique est encore indéfinie. Après l’exécution de Ada.Float_Text_IO.Get(Hauteur_Brique); la variable Hauteur_Brique vaut 15.5, Longueur_Brique et Largeur_Brique conservent leur valeur. Les valeurs ont été lues par le programme en respectant l’ordre dans lequel elles
DIALOGUE PROGRAMME-UTILISATEUR
45
ont été données par l’utilisateur. Par ailleurs, les nombres doivent être séparés par un (ou plusieurs) espace(s) ou par une (ou plusieurs) fin(s) de ligne. Finalement les espaces et les fins de lignes sont sautés (ignorés) lorsque le prochain nombre à lire est précédé de tels séparateurs. Concernant l’écriture en Ada des instructions d’entrées-sorties, il faut remarquer la correspondance obligatoire entre le type de la variable et le nom du paquetage utilisé comme préfixe aux opérations Get et Put. Il serait en effet incorrect d’écrire l’une des instructions suivantes: Ada.Float_Text_IO.Get (Longueur_Mur); Ada.Integer_Text_IO.Get (Longueur_Brique); Ada.Float_Text_IO.Put (Longueur_Mur); Ada.Integer_Text_IO.Put (Longueur_Brique);
2.6.3
Passage à la ligne lors de la lecture
Il existe des situations où il est nécessaire de sauter tout ce que l’utilisateur a tapé au clavier (§ 6.3.5 par exemple) jusqu’à la fin de la ligne. L’opération Skip_Line effectue cette tâche en lisant tous les caractères restants de la ligne et en les éliminant, y compris la fin de ligne (fig. 2.4). Dans la pratique, l’utilisateur marque une fin de ligne en introduisant au clavier le caractère de contrôle de retour de chariot (§ 1.10.2). Figure 2.4 Elimination de valeurs données par l’utilisateur.
programme Ada.Float_Text_IO.Get(Longueur_Brique); Ada.Text_IO.Skip_Line; Ada.Float_Text_IO.Get(Largeur_Brique); Ada.Text_IO.Skip_Line; Ada.Float_Text_IO.Get(Hauteur_Brique);
ce que l’utilisateur a donné 30.0 10.0 5.0 1.0
20.0
15.5
Après l’exécution de Ada.Float_Text_IO.Get(Longueur_Brique); la variable Longueur_Brique vaut 30.0, Largeur_Brique et Hauteur_Brique sont indéfinies. Après l’exécution de l’instruction Ada.Text_IO.Skip_Line; le reste de la ligne a été sauté. Après l’exécution de Ada.Float_Text_IO.Get(Largeur_Brique); la variable Largeur_Brique vaut 10.0, Longueur_Brique garde sa valeur 30.0 et
DIALOGUE PROGRAMME-UTILISATEUR
46
Hauteur_Brique est encore indéfinie. Après l’exécution de l’instruction Ada.Text_IO.Skip_Line; le reste de la ligne a été sauté.
Après l’exécution de Ada.Float_Text_IO.Get(Hauteur_Brique); la variable Hauteur_Brique vaut 5.0, Longueur_Brique et Largeur_Brique conservent leur valeur. Le nombre 1.0 subsiste pour une éventuelle future lecture. 2.6.4
Affichage de messages sur l’écran
Un programme bien conçu affiche toujours des messages à l’écran indiquant à l’utilisateur qu’il faut introduire une donnée, qu’une opération est terminée, etc. En Ada, ces messages sont en fait des constantes du type String (sect. 9.2) appelées chaînes de caractères et toujours écrites entre guillemets (exemple 2.13). Exemple 2.13 Exemples de chaînes de caractères.
"ceci est une chaine de caracteres" "Attention: si un guillemet doit etre place dans une chaine" " de caracteres, il faut le dedoubler ainsi "" CQFD."
L’affichage d’un message sur l’écran, pour l’utilisateur, est maintenant facile: Ada.Text_IO.Put ( "Donnez les dimensions d’une brique " );
2.6.5
Complément sur le dialogue homme-machine
Tout bon dialogue avec l’utilisateur doit respecter les deux règles données dans la note 2.4. NOTE 2.4 Indication à l’utilisateur que son programme s’exécute. Tout programme doit annoncer le début de son exécution par un message à l’utilisateur. Celui-ci obtient donc la confirmation du début d’exécution du programme. Tout programme doit faire patienter l’utilisateur par un message d’avertissement lorsque le programme effectue des opérations nécessitant un temps relativement long, plusieurs secondes par exemple.
2.6.6
Affichage de valeurs entières ou réelles
L’opération Put s’utilise aussi pour afficher une valeur entière ou réelle à l’écran. Par exemple: Ada.Float_Text_IO.Put ( Volume );
-- Volume de type Float
DIALOGUE PROGRAMME-UTILISATEUR
47
Ada.Integer_Text_IO.Put ( Longueur_Mur );
-- Longueur_Mur de -- type Integer
S’il faut afficher plusieurs valeurs, il est nécessaire d’utiliser une opération Put par valeur. La notation scientifique est utilisée pour une valeur réelle (sauf en cas de mention explicite du contraire). 2.6.7
Mise en page du texte affiché par un programme
Toute valeur entière est affichée sur un certain nombre de positions. Ce nombre de positions dépend du type des valeurs et correspond au nombre maximum de chiffres possibles plus un. Par exemple, si le type Integer est sur 32 bits, la plus grande valeur de ce type, 2_147_483_647, est composée de 10 chiffres. N’importe quelle valeur de ce type Integer sera donc affichée sur 11 positions. De manière analogue, toute valeur réelle est écrite en notation scientifique avec 2 positions avant le point décimal, N positions après le point décimal où N est le nombre de chiffres significatifs du type et 3 positions pour l’exposant. Il est cependant possible de modifier cette mise en page. Pour les valeurs entières, il suffit de compléter l’opération Put avec le nombre de positions souhaitées. Par exemple: Ada.Integer_Text_IO.Put ( Longueur_Mur, 8 );
Pour les valeurs réelles, le procédé est le même, mais en donnant une suite de trois nombres pour, dans l’ordre, le nombre de positions avant le point décimal, après le point décimal et pour l’exposant. Par exemple: Ada.Float_Text_IO.Put ( Volume, 3, 9, 2 );
2.6.8
Passage à la ligne lors de l’affichage
Il est naturellement possible d’afficher une suite formée de messages et de valeurs entières ou réelles en utilisant plusieurs fois l’opération Put comme mentionné auparavant (§ 2.6.6). Mais tous ces textes et nombres vont être disposés les uns à la suite des autres! Le programmeur peut alors décider à quel moment il faut terminer la ligne affichée et passer à la suivante en utilisant l’opération New_Line. Par exemple: Ada.Text_IO.Put ( "Avec un mur d’une longueur de " ); Ada.Integer_Text_IO.Put ( Longueur_Mur ); Ada.Text_IO.New_Line; Ada.Text_IO.Put ( "Le volume du mur de briques vaut : " ); Ada.Float_Text_IO.Put ( Volume ); Ada.Text_IO.New_Line;
En supposant que l’utilisateur a donné 50 pour la longueur du mur et que le volume calculé vaut 1980.5, l’utilisateur verra donc s’afficher: Avec un mur d’une longueur de
50
DIALOGUE PROGRAMME-UTILISATEUR
Le volume du mur de briques vaut :
48
1.98050E+03
Il existe finalement la possibilité de contracter les deux opérations Put et New_Line par Put_Line lors de l’affichage d’un message (et uniquement dans ce cas). Par exemple, les deux instructions: Ada.Text_IO.Put ( "Le programme est maintenant termine!" ); Ada.Text_IO.New_Line;
seront souvent écrites comme suit: Ada.Text_IO.Put_Line ( "Le programme est maintenant termine!" );
2.6.9
Abréviations d’écriture dans un programme
Dans un programme Ada, l’utilisation intensive des préfixes comme Ada.Text_IO ou Ada.Integer_Text_IO diminue la lisibilité du programme. En effet, tout programmeur Ada sait que les opérations comme Get, Put ou New_Line viennent des paquetages d’entrées-sorties prédéfinis.
Il est possible d’omettre ces préfixes en ajoutant, dans la clause de contexte, des directives permettant au compilateur de traduire le code Ada comme si ces préfixes étaient mentionnés là où ils sont nécessaires. Ces notions seront développées dans le paragraphe mentionnant la notion de clause use (§ 10.5.2). L’exemple 2.14 présente le style d’écriture usuel pour les entrées-sorties, sans les préfixes. Exemple 2.14 Abréviations d’écriture pour les opérations d’entrées-sorties. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Ada.Float_Text_IO; use Ada.Float_Text_IO; -- ... procedure Dialogue is ... begin -- Dialogue ... -- Obtenir le nombre de briques formant le mur en longueur -Ada.Text_IO est sous-entendu comme prefixe des Put Put ( "Donnez le nombre de briques " ); Put ( "(longueur du mur): " ); -Ada.Integer_Text_IO est sous-entendu comme prefixe de Get Get ( Longueur_Mur ); -- Obtenir le nombre de briques formant le mur en largeur -Ada.Text_IO est sous-entendu comme prefixe des Put Put ( "Donnez le nombre de briques " ); Put ( "(hauteur du mur): " ); -Ada.Integer_Text_IO est sous-entendu comme prefixe de Get Get ( Hauteur_Mur );
DIALOGUE PROGRAMME-UTILISATEUR
49
-- Obtenir les dimensions d'une brique -Ada.Text_IO est sous-entendu comme prefixe des Put Put ( "Donnez les dimensions d'une brique " ); Put ( "(longueur largeur hauteur): " ); -Ada.Float_Text_IO est sous-entendu comme prefixe des Get Get ( Longueur_Brique ); Get ( Largeur_Brique ); Get ( Hauteur_Brique ); -- Calcul du volume du mur Volume := Longueur_Brique * Largeur_Brique * Hauteur_Brique * Float ( Longueur_Mur * Hauteur_Mur ); -- Montrer a l'utilisateur la valeur du volume du mur -Ada.Text_IO est sous-entendu comme prefixe de Put Put ( " Le volume du mur de briques vaut : " ); -Ada.Integer_Text_IO est sous-entendu comme prefixe de Put Put ( Volume ); -Ada.Text_IO est sous-entendu comme prefixe de New_Line New_Line; ... end Dialogue;
50
2.7 2.7.1
EXERCICES Utilisation des opérations arithmétiques
Ecrire un programme qui calcule la somme, la différence, le produit, la division et le reste de la division entre deux nombres entiers donnés par l’utilisateur du programme. 2.7.2
Utilisation des opérations arithmétiques
Ecrire un programme qui calcule le nombre minimum de billets et de pièces de monnaie nécessaires pour décomposer une somme d’argent donnée par l’utilisateur du programme. 2.7.3
Utilisation d’attributs
Ecrire un programme qui affiche les bornes des types Integer et Float, ainsi que le nombre de chiffres significatifs de Float. En faire de même avec d’autres types comme Short_Integer, Long_Float, etc. (§ 2.2.7 et 2.3.5) selon l’implémentation à disposition. 2.7.4
Correction d’erreurs
Trouver les erreurs de compilation puis, après correction, celles de logique conduisant à des résultats erronés dans le programme suivant: procedure Erreurs is Entier_1 : Integer; Reel_1 : Float := 5; begin -- Erreurs -- Valeur mediane des entiers Entier_1 := Integer'Last + Integer'First/2; Put("Valeur mediane des valeurs du type Integer", Entier_1); -- Quelques puissances Reel_1 := Reel_1**5 + (Entier_1+5)**3; Put("Un calcul de puissances:", Reel_1); New_Line; end Erreur;
POINTS À RELEVER
2.8 2.8.1
51
POINTS À RELEVER En général • L’intervalle des valeurs numériques entières ou réelles est toujours fini. • L’affectation consiste à donner, changer la valeur d’une variable. • Attention aux variables non initialisées qui sont toujours des sources
d’erreurs. • Les opérateurs possèdent tous une priorité définissant précisément l’ordre
d’évaluation d’une expression. • Les parenthèses peuvent servir à modifier l’ordre d’évaluation d’une
expression. • Lors de l’introduction de données par un utilisateur, un dialogue doit
s’instaurer entre le programme et lui-même. 2.8.2
En Ada • Les attributs doivent être utilisés chaque fois que c’est possible pour
faciliter la maintenance des programmes Ada. • Dans une expression, le mélange de valeurs entières et réelles est interdit. • Une conversion explicite de type peut transformer une valeur entière en son
équivalent réel et inversement (arrondi).
52
C H A P I T R E
SÉLECTION, ITÉRATION, AUTRES TYPES DE BASE
3
53
SÉLECTION, ITÉRATION, AUTRES TYPES DE BASE
INSTRUCTION IF SANS ALTERNATIVE
3.1
54
INSTRUCTION IF SANS ALTERNATIVE
3.1.1
Motivation
Les premiers programmes présentés s’exécutaient en séquence, c’est-à-dire une instruction après l’autre en suivant leur ordre d’écriture dans le corps du programme. Pour modifier cette situation, il est possible de programmer une sélection (un choix), c’est-à-dire la manière d’exécuter ou de ne pas exécuter une suite d’instructions. Cette possibilité est donnée par l’instruction if. 3.1.2
Instruction if sans alternative
Comment écrire un programme qui affiche tous les nombres impairs entre deux bornes ? Soit Nb_Courant le nombre examiné; déterminer s’il est pair ou impair peut être réalisé en considérant le reste de la division (entière) par 2. En effet, si l’expression abs Nb_Courant rem 2 vaut 0 alors Nb_Courant est pair, mais si abs Nb_Courant rem 2 vaut 1 alors Nb_Courant est impair (rem est l’opérateur donnant le reste de la division entière et abs la valeur absolue). Il est donc possible de n’afficher que les nombres impairs en effectuant une sélection en fonction de la valeur d’une condition. Cela s’écrit: if abs Nb_Courant rem 2 = 1 then -- Nb_Courant est-il impair? Put ( "Le nombre " ); Put ( Nb_Courant ); Put ( "est impair" ); New_Line; end if;
Le message Le nombre ... est impair n’est affiché que si la condition abs Nb_Courant rem 2 = 1 est vraie, i.e. lorsque Nb_Courant est impair. L’instruction if, appelée aussi énoncé conditionnel, s’utilise donc chaque fois qu’un choix entre différentes options doit être programmé. Sa forme générale (sans alternative) est: if condition then suite_d_instructions; end if;
où • if et then sont de nouveaux mots réservés; • la condition est en fait une expression dont le résultat d’évaluation doit
être soit vrai soit faux; une telle expression est appelée expression booléenne (§ 3.4.2). • la suite_d_instructions est exécutée si la condition est vraie; rien ne se passe si elle est fausse. Soient expression_1 et expression_2 deux expressions entières. Alors il est possible de construire des expressions booléennes selon l’exemple 3.1.
INSTRUCTION IF SANS ALTERNATIVE
55
Exemple 3.1 Exemples d’expressions booléennes. expression_1 expression_1 expression_1 expression_1 expression_1 expression_1
= expression_234+5 = 56–17 est vraie; /= expression_234+5 /= 56–17 est fausse; <= expression_21 <= 5 est vraie, de même < expression_25 < 3*3 est vraie; >= expression_25 >= 1 est vraie, de même > expression_21 > 5 est fausse.
que 1 <= 1; que 1 >= 1;
Les six symboles = /= <= < >= > sont appelés opérateurs de comparaison. Il faut encore relever que plusieurs conditions peuvent être combinées en une seule, par exemple: if abs Nb_Courant rem 2 = 1 and Nb_Courant > 20 then ...
Le mot réservé and est un opérateur booléen réalisant la fonction logique et. Tout ceci sera présenté avec la notion du type Boolean (sect. 3.4).
INSTRUCTION IF AVEC ALTERNATIVE
3.2
56
INSTRUCTION IF AVEC ALTERNATIVE
Comment modifier cet exemple de manière à afficher un message si Nb_Courant est impair et un autre message s’il est pair? Cela s’écrit: Put ( "Le nombre " ); Put ( Nb_Courant ); if abs Nb_Courant rem 2 = 1 then Put ( "est impair" );
-- Nb_Courant est-il impair? -- Nb_Courant est impair
else Put ( "est pair" );
-- Nb_Courant est pair
end if; New_Line;
La suite d’instructions comprise entre les mots réservés else et end if est exécutée si la condition est fausse, ici si Nb_Courant est pair. La forme générale de l’instruction if avec alternative est: if condition then suite_d_instructions_1; else suite_d_instructions_2; end if;
où • else est un nouveau mot réservé; • si la condition est vraie, alors la suite_d_instructions_1 est exécutée, sinon c’est suite_d_instructions_2 qui l’est.
INSTRUCTION IF GÉNÉRALISÉE
3.3
57
INSTRUCTION IF GÉNÉRALISÉE
Finalement, comment modifier une dernière fois notre exemple de manière à afficher un message variant selon la valeur de Nb_Courant? Ce message affichera simplement l’ordre de grandeur (unité, dizaine, centaine, millier ou plus) de Nb_Courant. Cela s’écrit: Nb_Courant := abs Nb_Courant;
-- Le nombre en valeur absolue
Put ( "Le nombre " ); Put ( Nb_Courant ); if Nb_Courant < 10 then Put ( "est une unite" );
-- Nb_Courant est-il une unite?
elsif Nb_Courant < 100 then Put ( "est une dizaine" );
-- Nb_Courant est-il une dizaine?
elsif Nb_Courant < 1000 then centaine? Put ( "est une centaine" ); elsif Nb_Courant < 10_000 then Put ( "est un millier" );
-- Nb_Courant est-il une
-- Nb_Courant est-il un millier?
else -- Nb_Courant est alors plus grand que 9999 Put ( "est plus qu’un millier" ); end if; New_Line;
L’exécution de cette instruction if généralisée commence par l’évaluation (le calcul) de l’expression Nb_Courant < 10. Si elle est vraie le message "est une unite" s’affiche et le reste de l’instruction est sauté. Si Nb_Courant < 10 est fausse, alors l’expression suivante (dans l’ordre d’écriture) Nb_Courant < 100 est évaluée. Si elle est vraie le message "est une dizaine" s’affiche et le reste de l’instruction est sauté. Sinon l’expression suivante est évaluée et ainsi de suite. Finalement, si toutes les expressions sont fausses, le message "est plus qu’un millier" placé après le mot réservé else est effectué. La forme générale de l’instruction if généralisée est: if condition_1 then suite_d_instructions_1; elsif condition_2 then suite_d_instructions_2; elsif condition_3 then suite_d_instructions_3; ... else autre_suite_d_instructions; end if;
INSTRUCTION IF GÉNÉRALISÉE
58
où • elsif est un nouveau mot réservé; • la suite_d_instructions_i est exécutée pour la première condition_i vraie dans l’ordre d’écriture et le reste de l’instruction if
est
sauté;
si
toutes
ces
conditions autre_suite_d_instructions qui l’est.
sont
fausses,
c’est
On appelle branche chacune des suites d’instructions précédée de la ligne contenant la condition. La branche commençant par else doit être la dernière et est optionnelle. Par conséquent, si toutes les conditions sont fausses et que cette branche n’est pas présente, aucune instruction de l’instruction if généralisée n’est exécutée.
LE TYPE BOOLEAN
3.4
59
LE TYPE BOOLEAN
3.4.1
Généralités
Boolean est un identificateur prédéfini. Il signifie «un objet (constante, variable, expression...) de type Boolean aura la valeur vrai ou faux et rien d’autre». Le type Boolean fait partie des types discrets (discrete types, [ARM
3.2]). Les constantes booléennes sont False et True, qui sont aussi des identificateurs prédéfinis et qui représentent respectivement les valeurs faux et vrai. Les opérations possibles sur les valeurs de type Boolean sont: and or xor not = /= <= < >= >
où • • • • •
and représente la fonction logique et; or représente la fonction logique ou (non exclusif); xor représente la fonction logique ou exclusif; not représente la fonction logique non; comme déjà mentionné (§ 3.1.2) = /= <= < >= et > représentent les
opérateurs de comparaison. Ces quatre nouveaux opérateurs sont appelés opérateurs logiques (ou opérateurs booléens). Les opérateurs de comparaison possèdent un sens bien défini car, par définition, le type Boolean est ordonné, avec False < True. 3.4.2
Expressions booléennes
Les expressions booléennes ou logiques s’écrivent comme présenté dans les exemples 3.1 et 3.2. Les parenthèses servent à préciser l’ordre d’évaluation. De plus, la priorité des opérateurs logiques est (dans l’ordre décroissant): not(le plus prioritaire) and or xor(même priorité entre eux)
Relevons qu’il semble alors possible de construire des expressions ambiguës comme Trouve and Present or Existe. Mais en fait une telle expression est interdite car Ada exige l’utilisation des parenthèses lorsque des opérateurs logiques différents (parmi and, or et xor) coexistent dans une expression. L’expression précédente doit donc s’écrire (Trouve and Present) or Existe ou alors Trouve and (Present or Existe). Exemple 3.2 Exemples d’expressions booléennes. Trouve : Boolean;déclaration
de deux variables booléennes;
LE TYPE BOOLEAN
60
Present : Boolean; Nombre : Integer;déclaration d’une variable entière; Trouve and Presentexpression vraie si Trouve et Present sont vrais; not Trouveexpression vraie si Trouve est faux; Trouve or Presentexpression vraie si Trouve ou Present est vrai; not (Trouve and Present)expression vraie si Trouve ou Present
est
faux; not Trouve or not Presentexpression équivalente à la précédente; Nombre = 0 or Nombre = 1expression vraie si Nombre vaut 0 ou 1; Trouve and Nombre = 0expression vraie si Trouve est vraie et si Nombre
vaut 0. A titre de synthèse, tous les opérateurs existant en Ada sont donnés dans le tableau 3.1, par ordre décroissant de priorité. Tableau 3.1 Opérateurs dans l’ordre décroissant de priorité.
3.4.3
Opérateurs
Classes d’opérateurs
** abs not
opérateurs prioritaires
* / rem mod
opérateurs multiplicatifs
+ –
opérateurs additifs unaires
+ – &
opérateurs additifs binaires
= /= <= < >= >
opérateurs de comparaison
and or xor
opérateurs logiques
Formes de contrôle en raccourci and then et or else
Sans pouvoir en donner maintenant l’utilité, il faut relever qu’il existe les formes de contrôle en raccourci and then et or else, qui ne sont pas des opérateurs, mais qui s’utilisent comme les opérateurs and et or, avec le même niveau de priorité. Soit l’expression booléenne expression_1 and then expression_2 (où expression_1 et expression_2 sont aussi booléennes). Expression_1 est d’abord calculée. Si elle est fausse, alors expression_2 n’est pas calculée et l’expression complète est aussi fausse. Si expression_1 est vraie alors l’expression complète aura la valeur de expression_2. Soit l’expression booléenne expression_1 or else expression_2 (où
LE TYPE BOOLEAN
61
expression_1 et expression_2 sont aussi booléennes). Expression_1 est d’abord calculée. Si elle est vraie, alors expression_2 n’est pas calculée et l’expression complète est aussi vraie. Si expression_1 est fausse alors l’expression complète aura la valeur de expression_2.
L’exemple 3.3 illustre l’utilisation des deux formes de contrôle en raccourci. Exemple 3.3 Exemples d’utilisation de formes de contrôle en raccourci. Trouve : Boolean := True;déclaration de deux variables booléennes; Present : Boolean := False; Present and then Trouveexpression fausse (car Present est fausse)
et
Trouve n’est pas évaluée; Trouve and then Presentexpression fausse (car Present est fausse); Trouve or else Presentexpression vraie (car Trouve est vraie)
et
Present n’est pas évaluée; Present or else Trouveexpression
vraie (car Trouve est vraie).
Il faut encore mentionner les tests d’appartenance in et not in (§ 6.1.5) qui donnent également un résultat booléen et qui possèdent le même niveau de priorité que les opérateurs de comparaison. Ces tests d’appartenance, comme les formes de contrôle en raccourci, ne sont pas des opérateurs car, contrairement aux opérateurs, il n’est pas possible de les surcharger (sect. 4.9). 3.4.4
Affectation
L’affectation s’effectue comme dans l’exemple 3.4. Exemple 3.4 Affectations d’expressions booléennes à une variable booléenne. -- ... procedure Exemple_3_4 is Trouve : Boolean; Present : Boolean := False; Nb_Courant : Integer := ...;
-- Deux variables booleennes -- Une variable entiere
begin -- Exemple_3_4 Trouve := True; -- Affecte True a Trouve Trouve := Trouve and Present; -- Affecte False a Trouve Trouve := Present; -- Affecte False a Trouve Trouve := Nb_Courant = 25;
...
-- Trouve obtient la valeur True -- si Nb_Courant vaut 25, False -- sinon
LE TYPE BOOLEAN
62
De manière générale, l’affectation s’écrit: nom_de_variable_booléenne := expression_de_type_Boolean;
3.4.5
Entrées-sorties
Comme pour tous les types simples, Ada offre la possibilité d’effectuer des entrées-sorties sur des valeurs de type Boolean. Sans en décrire ni expliquer le mécanisme utilisé (sect. 17.4), ces entrées-sorties sont disponibles sous les noms Put et Get après avoir effectué la déclaration suivante: package ES_Boolean is new Ada.Text_IO.Enumeration_IO ( Boolean ); Exemple 3.5 Entrées-sorties de valeurs booléennes. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_3_5 is Trouve : Boolean; -- Deux variables booleennes Present : Boolean := False; Nb_Courant : Integer := ...; -- Une variable entiere package ES_Boolean is new Ada.Text_IO.Enumeration_IO(Boolean); use ES_Boolean; -- § 2.6.9 begin -- Exemple_3_5 Put_Line (" Cet exemple..." ); Get (Trouve); Put (Trouve);
-- Lit une valeur booleenne -- Affiche la valeur lue
Put (Trouve and Present);
-- Affiche la valeur de -- l'expression booleenne -- Trouve and Present
Put (Nb_Courant = 25);
-- Affiche TRUE si Nb_Courant -- vaut 25, FALSE sinon
...
Comme suggéré dans la dernière instruction de l’exemple 3.5, les valeurs booléennes TRUE et FALSE sont toujours affichées en majuscules sur le nombre minimum de positions. A noter qu’il est possible de modifier ces aspects d’affichage [ARM A.10.10].
ITÉRATION: LA BOUCLE FOR
3.5
63
ITÉRATION: LA BOUCLE FOR
3.5.1
Motivation
Dans tout programme d’une certaine taille, des actions sont répétées plusieurs fois. Ici (fig. 3.1) le programmeur veut créer un dessin formé de dix triangles. Figure 3.1 La figure à dessiner.
Le lecteur remarque immédiatement que cela revient à dessiner dix fois le même triangle. On appelle itération (iteration) la répétition d’un groupe d’instructions. Cet exemple comporte donc dix itérations. 3.5.2
Généralités
La réalisation d’itérations se fait en utilisant des instructions appelées communément boucles (il y a trois boucles différentes en Ada). Le fait que le nombre d’itérations à effectuer soit calculable avant l’arrivée dans la boucle conduit à choisir l’instruction for. Cette instruction s’écrit: for I in 1..10 loop -- dessiner le triangle end loop;
où • I est une variable entière déclarée automatiquement.
Cette instruction for va s’exécuter de la manière suivante: la variable de boucle I va prendre successivement les valeurs 1, 2, 3, ..., 9 et 10 et, pour chacune de ces valeurs, l’action dessiner le triangle qui suit le mot réservé loop va s’effectuer. Un premier essai de programme complet est donné dans l’exemple 3.6. Exemple 3.6 Premier essai de programme pour dessiner les dix triangles. with Spider; use Spider;
-- Pour pouvoir dessiner
-- ... procedure Triangle_10_Premier_Essai is -- Nombre de triangles a dessiner Nombre_De_Triangles : constant := 10; X_Init : constant := 10; Y_Init : constant := 30;
-- Abscisse du point initial -- Ordonnée du point initial
ITÉRATION: LA BOUCLE FOR
64
begin -- Triangle_10_Premier_Essai -- Presentation du programme ... -- Pour pouvoir dessiner dans la fenetre de dessin Init_Window ("Fenetre de dessin"); -- Deplacement au point initial de dessin Move_To (X_Init, Y_Init); -- Dessiner les dix triangles for I in 1..Nombre_De_Triangles loop Line (40, 0); Line (–20, –20); Line (–20, 20);
-- Dessin de la base -- Dessin du cote a droite -- Dessin du dernier cote
end loop; end Triangle_10_Premier_Essai;
Une lecture attentive de ce programme fait apparaître un problème: les triangles seront tous dessinés au même endroit. Il faut donc changer de point initial avant chaque dessin. On remarque que pour le nième triangle, ce point a les coordonnées (N_Init+(n–1)*40, Y_Init). D’autre part, les nombres 20 et 40 sont à remplacer par une ou des constantes en vertu des bonnes habitudes de programmation. Le code source modifié en conséquence devient alors correct et l’on obtient l’exemple 3.7. Exemple 3.7 Programme de dessin des dix triangles. with Spider; use Spider;
-- pour pouvoir dessiner
-- ... procedure Triangle_10_Correct is -- Nombre de triangles à dessiner Nombre_De_Triangles : constant := 10; X_Init : constant := 10; -- Abscisse du point initial Y_Init : constant := 30; -- Ordonnée du point initial Base : constant := 40; -- Longueur de la base d'un triangle begin -- Triangle_10_Correct -- Presentation du programme ... -- Pour pouvoir dessiner dans la fenetre de dessin Init_Window ("Fenetre de dessin"); -- Dessiner les 10 triangles for I in 1..Nombre_De_Triangles loop -- Deplacement au point suivant Move_To (X_Init + (I – 1) * Base, Y_Init);
ITÉRATION: LA BOUCLE FOR
65
-- Dessin d'un triangle Line ( Base, 0 ); Line (–Base / 2, –Base / 2); Line (–Base / 2, Base / 2); end loop; end Triangle_10_Correct;
L’erreur commise dans le programme Triangle_10_Premier_Essai est classique. Il faut en effet toujours prendre garde à tenir compte de la variation de la variable de boucle. En général cette variable doit être utilisée au moins une fois dans les instructions répétées, sinon il y a de fortes chances qu’une erreur de logique se soit glissée dans le code source (note 3.1). Dans le programme Triangle_10_Correct, la variable de boucle I apparaît dans le calcul du point initial du dessin d’un triangle, point initial qui change à chaque itération! NOTE 3.1 Variable de boucle et itérations. La variable de boucle doit généralement être utilisée dans le corps de la boucle. Dans le cas contraire une erreur de logique est très probable.
La forme générale de l’instruction for est: for variable_de_boucle in intervalle loop suite_d_instructions; end loop;
avec • les trois nouveaux mots réservés for, in et loop; • variable_de_boucle déclarée automatiquement; • intervalle dont la forme la plus simple et la plus courante est expression_1 .. expression_2; • variable_de_boucle, expression_1 et expression_2 de type
discret (discrete types, [ARM 3.2]). D’autres formes d’intervalle seront décrites par la suite (§ 6.1.1). 3.5.3
Précisions sur l’utilisation de la boucle for
La boucle for doit être choisie chaque fois que le nombre d’itérations à effectuer est calculable avant l’arrivée dans la boucle (note 3.2). Ce nombre d’itérations sera égal à expression_2 – expression_1 + 1. La variable de boucle (appelée aussi variable de contrôle) est déclarée implicitement, du type des bornes de l’intervalle et n’existe que dans le corps de la
ITÉRATION: LA BOUCLE FOR
66
boucle for. Elle prendra successivement (au début de chaque itération) la valeur suivante dans l’intervalle. Il n’est pas possible de changer la valeur de la variable de boucle, par une affectation par exemple. Le compilateur détecterait une telle tentative et afficherait un message d’erreur. Si l’intervalle est nul, c’est-à-dire que l’expression_1 a une valeur supérieure à expression_2, la boucle n’est simplement pas effectuée. Si la valeur d’expression_1 ou d’expression_2 est modifiée par une itération, le nombre d’itérations ne change pas! Il est possible d’écrire: for variable_de_boucle in reverse intervalle loop suite_d_instructions; end loop;
Dans ce cas variable_de_boucle décroît expression_1; reverse est un mot réservé de plus.
de
expression_2
à
ITÉRATION: LA BOUCLE WHILE
3.6
67
ITÉRATION: LA BOUCLE WHILE
3.6.1
Généralités
La première construction permettant de répéter l’exécution d’une suite d’instructions est l’instruction for. L’emploi de cette construction est possible (et recommandé) lorsque le nombre d’itérations à effectuer est calculable avant l’arrivée dans la boucle. L’instruction while permet de répéter l’exécution d’une suite d’instructions un nombre de fois non calculable avant l’arrivée dans la boucle. Cette répétition s’effectue en fait tant qu’une condition (expression booléenne) est vraie. L’exemple 3.8 illustre l’utilisation d’une instruction while. Exemple 3.8 Traiter des nombres tant qu’un nombre particulier, ici 0, n’apparaît pas. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; -- ... procedure Exemple_3_8 is Nombre_Final : constant := 0;
-- Permet de terminer le -- traitement des nombres Nombre_Lu : Integer := 1; -- Nombre donne par l'utilisateur
begin -- Exemple_3_8 ... -- Presentation du programme -- Traiter les nombres de l'utilisateur. Attention, etre sur -- que Nombre_Lu a une valeur bien definie!
while Nombre_Lu /= Nombre_Final loop Put ( "Donnez le nombre suivant: Get ( Nombre_Lu );
" );
-- Traitement du nombre si ce n'est pas le dernier
if Nombre_Lu /= Nombre_Final then ...
end if; end loop; ...
La suite d’instructions comprise entre loop et end loop est répétée tant que la condition Nombre_Lu /= Nombre_Final est vraie, i.e. tant que Nombre_Lu est différent de 0. Le style de codage de cet exemple peut cependant être amélioré comme dans l’exemple 3.10. La forme générale de l’instruction while est: while expression_de_type_Boolean loop suite_d_instructions;
ITÉRATION: LA BOUCLE WHILE
68
end loop;
où • la suite_d_instructions contenue dans la boucle est exécutée tant que l’expression_de_type_Boolean est vraie.
3.6.2
Précisions sur l’utilisation de la boucle while
Toutes les variables appartenant à l’expression booléenne doivent avoir une valeur bien définie (note 2.3). Il faut s’assurer que l’expression booléenne devient fausse après un nombre fini d’itérations, faute de quoi le programme exécuterait l’instruction while indéfiniment. Si l’expression booléenne est initialement fausse, l’instruction while n’est pas effectuée.
ITÉRATION: LA BOUCLE GÉNÉRALE LOOP
3.7
69
ITÉRATION: LA BOUCLE GÉNÉRALE LOOP
3.7.1
Motivation
Les deux boucles vues jusqu’à présent permettent d’implanter tous les cas d’itérations. Mais le codage de certains d’entre eux sera plus facile et plus lisible avec une boucle où, par exemple, la condition de répétition de la boucle peut être placée au milieu (exemples 3.9 et 3.10) ou à la fin de la boucle, ou encore si l’algorithme prévoit des sorties multiples de la boucle. Exemple 3.9 Implantation d’un petit menu avec contrôle de la réponse de l’utilisateur. ... loop Put_Line Put_Line Put_Line Put_Line Put_Line
( ( ( ( (
"Votre choix parmi les quatre options:" ); "1. Calcul de la moyenne" ); "2. Calcul de la variance" ); "3. Calcul de l'ecart-type" ); "4. Quitter ce menu" );
Get ( Reponse );
-- On suppose que Reponse est une -- variable entiere
exit when Reponse = 4;
-- Sortie de la boucle
-- Traiter ci-dessous les options 1, 2 et 3 ... end loop; ...
Exemple 3.10 Exemple 3.8 récrit avec une boucle loop. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; -- ... procedure Exemple_3_10 is Nombre_Final : constant := 0; Nombre_Lu : Integer;
-- Permet de terminer le -- traitement des nombres
-- Nombre donne par l'utilisateur
begin -- Exemple_3_10 ... -- Presentation du programme -- Traiter les nombres de l'utilisateur. Dans cet exemple -- Nombre_Lu peut avoir une valeur quelconque lors de l’entree -- dans la boucle!
loop
ITÉRATION: LA BOUCLE GÉNÉRALE LOOP
70
Put ( "Donnez le nombre suivant: Get ( Nombre_Lu );
" );
exit when Nombre_Lu = Nombre_Final; -- Traitement du nombre puisque ce n'est pas le dernier ...
end loop; ...
3.7.2
Généralités
La forme générale de la boucle loop est très simple: loop suite_d_instructions end loop;
Sans rajouter de condition de sortie, une telle boucle loop s’exécuterait indéfiniment! C’est pourquoi l’instruction exit, permettant la sortie de la boucle, existe et est fortement liée à cette boucle. Sa forme générale est: exit when expression_booleenne;
Si l’expression_booleenne est vraie, l’instruction exit provoque la sortie de la boucle dans laquelle elle se trouve; sinon l’exécution de la boucle continue. Une instruction exit doit être placée parmi la suite d’instructions d’une boucle loop et il est autorisé d’en mettre plusieurs. Enfin il est possible, mais déconseillé, de placer une ou plusieurs instructions exit dans une boucle for ou while. 3.7.3 exit
Précisions sur l’utilisation des boucles et de l’instruction
La boucle loop (comme for et while d’ailleurs) peut comporter une étiquette (label) utile dans des cas tels que celui de deux boucles imbriquées: Externe: loop loop ... exit when B; ... exit Externe when C; ... end loop; ... end loop Externe;
-- Sortie de la boucle interne -- si B est vraie -- Sortie d’Externe si C est vraie
L’instruction exit; (sans condition) existe et provoque la sortie inconditionnelle de la boucle la plus interne qui la contient.
ITÉRATION: LA BOUCLE GÉNÉRALE LOOP
71
Enfin, la note 3.2 résume les critères de choix d’une boucle. NOTE 3.2 Choix d’une boucle parmi for, while et loop. Le choix d’une boucle ne s’effectue pas arbitrairement. Si le nombre d’itérations est calcu-lable avant l’arrivée dans la boucle, alors l’instruction for s’impose. Si l‘itération dépend d’une condition qui peut être initialement fausse, alors il faut utiliser une instruction while. Si aucune des deux hypothèses ci-dessus ne s’applique, alors prendre l’instruction loop en choisissant soigneusement le nombre et le lieu d’utilisation des instructions exit!
LE TYPE CHARACTER
3.8
72
LE TYPE CHARACTER
3.8.1
Généralités
Character est un identificateur prédéfini. Il signifie «un objet (constante, variable, expression...) de type Character contiendra un caractère et rien d’autre». Le type Character fait partie des types discrets (discrete types, [ARM
3.2]). Les constantes de type Character sont des caractères écrits entre apostrophes comme dans l’exemple 3.11. Exemple 3.11 Constantes caractères.
'A' 'a' '1' '?' ''' '+' '=' 'é' 'à' Ces constantes sont ordonnées selon un code, unique pour chaque caractère. Il existe plusieurs codes utilisés en informatique (ASCII, EBCDIC, LATIN-1...), mais en Ada le code utilisé est LATIN-1 [ARM A.1], composé de 256 valeurs numérotées à partie de 0, dont les 128 premières sont celles du code ASCII (American Standard Code for Information Interchange). Chaque caractère écrit entre apostrophes du code LATIN-1 est imprimable (§ 1.10.2), les caractères de contrôle peuvent être utilisés par le biais de l’attribut Val (§ 3.8.3). De plus, un paquetage appelé Ada.Characters.Latin_1 définit un identificateur de constante pour chaque caractère du code LATIN-1 [ARM A.3.3]. Les opérations possibles sur les valeurs de type Character sont les comparaisons (§ 3.1.2) = /= < <= > >= et la concaténation (§ 8.5.2). Les expressions se limitent aux constantes et variables caractères ainsi qu’aux fonctions et attributs à résultat de type Character. 3.8.2
Affectation
L’affectation s’effectue comme dans l’exemple 3.12. Exemple 3.12 Affectations d’expressions de type Character à une variable caractère. with Ada.Characters.Latin_1; -- ... procedure Exemple_3_12 is Espace : constant Character := ' '; Signe : Character; Lettre_Courante : Character;
-- Le caractere appele -- espace (space)
-- Deux variables caracteres
LE TYPE CHARACTER
73
begin -- Exemple_3_12 Signe := Espace; -- Affecte le caractere ' ' a Signe Lettre_Courante := 'A'; Lettre_Courante := Signe; Signe := Ada.Characters.Latin_1.Nul; -- Nul est le caractere ... -- de code 0
De manière générale l’affectation s’écrit: nom_de_variable_de_type_Character := expression_de_type_Character;
3.8.3
Attributs First, Last, Succ, Pred, Pos et Val
Les attributs First, Last, Succ et Pred ont la même signification que pour les entiers (§ 2.2.5). L’attribut prédéfini Pos donne la valeur (entière) du code du caractère auquel s’applique l’attribut. De manière symétrique, l’attribut prédéfini Val donne le caractère correspondant à une valeur entière comprise entre 0 et 255. Comme pour les entiers et les réels, une tentative d’utilisation d’un code de caractère en dehors de l’intervalle de définition produira une erreur à l’exécution du programme. Exemple 3.13 Utilisation des attributs First, Last, Succ, Pred, Pos et Val. Character'First donne le caractère de code 0 (nul); Character'Lastdonne 'ÿ', le caractère de code 255; Character'Pred('B')donne 'A'; Character'Succ('A')donne 'B'; Character'Pos('A')donne 65, le code de 'A'; Character'Val(65)donne 'A'; Character'Val(97)donne 'a'; Character'Val(32)donne ' '; Character'Val (Nombre)provoque une erreur à l’exécution
si Nombre a une
valeur hors de l’intervalle 0 .. 255.
3.8.4
Entrées-sorties
Comme pour tous les types simples, Ada offre la possibilité d’effectuer des entrées-sorties sur des valeurs du type prédéfini Character. Celles-ci sont directement mises à disposition par le paquetage prédéfini Ada.Text_IO. L’exemple 3.14 montre simplement une lecture et deux écritures de valeurs du type
LE TYPE CHARACTER
74
Character. Exemple 3.14 Entrées-sorties de caractères. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_3_14 is Lettre : Character;
-- Une variable caractere
begin -- Exemple_3_14 Put_Line (" Cet exemple..." ); Get (Lettre); Put (Lettre); Put ('A'); ...
-- Lit un caractere -- Ecrit le caractere lu -- Ecrit le caractere A
En lecture comme en écriture, les caractères tapés ou lus par l’utilisateur du programme se présentent sans apostrophes! Lesdites apostrophes ne sont nécessaires que dans le texte source du programme pour les distinguer d’un identificateur, d’un opérateur, etc. Le caractère lu par Get est le caractère suivant le dernier caractère lu par une précédente opération Get, en sautant les fins de lignes éventuelles.
RETOUR SUR LA PARTIE DÉCLARATIVE D’UN PROGRAMME
3.9
75
RETOUR SUR LA PARTIE DÉCLARATIVE D’UN PROGRAMME
3.9.1
Déclaration de constantes
Une déclaration de constantes a la forme principale suivante: suite_d_identificateurs : constant type := expression;
avec • la suite_d_identificateurs formée d’un ou de plusieurs identificateurs séparés
par des virgules; • le mot réservé constant qui permet la distinction entre les déclarations de constantes et de variables (§ 3.9.2). L’expression doit bien entendu être du même type que le type mentionné et peut être aussi compliquée que le désire l’auteur du programme. Comme son nom l’indique, une constante possède une valeur fixe définie par l’expression. Il existe une forme particulière de déclaration de constantes: suite_d_identificateurs : constant := expression_statique;
L’expression doit dans ce cas être statique (sect. 3.10) et d’un type entier ou réel. Une telle constante est appelée nombre nommé (named number). L’exemple 3.15 présente des déclarations de constantes et de nombres nommés. Exemple 3.15 Constantes et nombres nommés. -- Constantes Nombre_Maximum : constant Integer := 1000; Neper : constant Float := 2.718282; Sentinelle : constant Character := '.'; -- On suppose ci-dessous que Valeur est une variable entiere -- deja declaree et de valeur 9, l’expression vaut donc 2 Complique, Pas_Simple : constant Integer := (3 * Valeur – 1) / 10; -- Nombres nommes Pi : constant := 3.141592654; Zero, Nul, Rien: constant := 0; Pour_Cercle : constant := 2.0 * Pi; -- 2.0 * Pi est statique
Lorsque plusieurs constantes sont mentionnées dans la même déclaration comme Complique et Pas_Simple ou encore Zero, Nul et Rien de l’exemple 3.15, cette forme est en fait une abréviation. Elle remplace une suite équivalente de déclarations où seul un identificateur serait présent dans chacune d’entre elles, donc dans notre cas: Complique : constant Integer := (3 * Valeur – 1) / 10;
RETOUR SUR LA PARTIE DÉCLARATIVE D’UN PROGRAMME
76
Pas_Simple : constant Integer := (3 * Valeur – 1) / 10; Zero : constant := 0; Nul : constant := 0; Rien : constant := 0;
Cette remarque a son importance lorsque l’expression comporte une fonction (sect. 4.6). En effet, l’expression est calculée pour chaque identificateur, donc la fonction est appelée autant de fois qu’il y a d’identificateurs mentionnés dans la suite. 3.9.2
Déclaration de variables
Une déclaration de variables a la forme principale suivante: suite_d_identificateurs : type := expression;
avec • la suite_d_identificateurs formée d’un ou de plusieurs identificateurs séparés
par des virgules. L’expression, appelée valeur initiale (initial value), est optionnelle. Si elle est présente, elle doit bien entendu être du même type que le type mentionné et sert à donner une première valeur à la variable. Comme pour les constantes, une suite comportant plusieurs identificateurs est une abréviation et les mêmes règles s’appliquent. 3.9.3
Ordre et élaboration des déclarations
Le langage Ada ne fixe pas d’ordre particulier pour les déclarations. Il faut simplement que tout identificateur utilisé soit déjà déclaré. Mais les bonnes habitudes de programmation impliquent le regroupement des déclarations qui forment un tout logique. De plus l’usage voudrait que les constantes soient déclarées avant les variables dans un tel regroupement (exemple 3.16). Exemple 3.16 Groupements des déclarations dans une partie déclarative. -- Pour le traitement de donnees numeriques Nombre_Maximum : constant Integer := 1000; Nombre_Courant : Integer; Maximum_Courant : Integer; -- Pour le traitement de donnees textuelles Sentinelle : constant Character := '.' Lettre_Lue : Character; Nombre_lettres_Lues : Integer := 0;
A l’exécution du programme, toute déclaration est élaborée, c’est-à-dire qu’un emplacement mémoire est préparé, voire alloué, à son intention et que celui-ci
RETOUR SUR LA PARTIE DÉCLARATIVE D’UN PROGRAMME
77
contient les informations caractérisant l’objet déclaré. Par exemple, l’élaboration de la variable Nombre_Lettres_Lues de l’exemple 3.16 consiste en l’attribution d’un mot mémoire et l’introduction de la valeur initiale zéro dans ce mot mémoire. L’ordre d’écriture des déclarations fixe l’ordre dans lequel ces déclarations vont être élaborées à l’exécution.
RETOUR SUR LES EXPRESSIONS
78
3.10 RETOUR SUR LES EXPRESSIONS Une expression statique (static) est une expression calculée à la compilation du programme. Par extension on appelle statique toute entité dont les caractéristiques sont connues à la compilation (avant l’exécution du programme). Une expression dynamique (dynamic) est une expression dont la valeur n’est connue qu’à l’exécution du programme ou qui peut changer lors de cette exécution. Par extension on appelle dynamique toute entité dont les caractéristiques (ou l’existence) ne sont connues qu’à l’exécution du programme. Une expression qualifiée (qualified) consiste à préciser le type d’une expression par l’utilisation d’un identificateur de type (ou de sous-type, sect. 6.1) sous la forme donnée par le diagramme syntaxique de la figure 3.2. Cette forme d’expression est particulièrement utile pour préciser le type d’agrégats (§ 7.2.2 et 8.2.5) et lors de l’élaboration d’une variable dynamique avec valeur initiale (§ 15.2.3). Il faut aussi remarquer la présence discrète mais fondamentale de l’apostrophe qui distingue une expression qualifiée d’une conversion de type (§ 2.5.1). L’exemple 3.17 montre des cas simples d’expressions qualifiées. Figure 3.2 Diagramme syntaxique définissant une expression qualifiée. Expression qualifiée
identificateur
'
(
expression
)
Exemple 3.17 Expressions qualifiées. Integer'(10) le nombre 10 est ici du type Integer; Long_Integer'(10) le nombre 10 est ici du type Long_Integer; Float'(10.0) le nombre 10.0 est ici du type Float; Short_Float'(10.0) le nombre 10.0 est ici du type Short_Float; Character'('.') le caractere '.' est ici du type Character.
79
3.11 EXERCICES 3.11.1
Recherche des diviseurs d’un nombre
Ecrire un programme qui trouve tous les diviseurs d’un nombre entier donné par l’utilisateur du programme. 3.11.2
Décomposition d’un nombre en facteurs premiers
Ecrire un programme qui décompose un nombre entier donné par l’utilisateur du programme en son produit de facteurs premiers. 3.11.3
Solutions de l’équation du second degré
Ecrire un programme qui trouve les racines d’une équation du second degré dont les coefficients sont donnés par l’utilisateur du programme. La racine carrée est disponible dans le paquetage Ada.Numerics.Elementary_Functions sous le nom Sqrt. 3.11.4
Utilisation des caractères
Ecrire un programme qui compte le nombre d’imbrications des parenthèses dans une ligne de texte. 3.11.5
Utilisation des booléens
Ecrire un programme qui affiche la table de vérité d’expressions booléennes comme par exemple A and (B or C) où A, B et C sont des variables booléennes. 3.11.6
Petites statistiques sur une ligne de texte
Ecrire un programme qui affiche le nombre de lettres majuscules, de lettres minuscules et de chiffres d’une ligne de texte (ces trois groupes de caractères forment chacun un intervalle sans caractère «parasite» dans le code LATIN-1). 3.11.7
Triangle de Pascal
Ecrire le triangle de Pascal formé des coefficients du binôme de Newton. Le coefficient i du binôme
a b
n
vaut
n! --------------------------- , i! × ( n – i )!
i et n commençant à 0.
POINTS À RELEVER
80
3.12 POINTS À RELEVER 3.12.1
En général • Les caractères permettent des manipulations rudimentaires de textes. • Les caractères sont ordonnés selon un code dépendant du système utilisé. • Les déclarations devraient toujours être placées de manière à former des
groupes logiques. • Un objet statique est un objet dont les caractéristiques sont déjà connues à
la compilation. • Un objet dynamique est un objet dont les caractéristiques (ou l’existence)
ne sont connues qu’à l’exécution du programme. 3.12.2
En Ada • L’instruction if (sélection) permet l’exécution d’une branche d’instruc-
tions parmi plusieurs, éventuellement aucune, en fonction de la valeur d’expressions booléennes. • Les expressions booléennes s’utilisent en particulier dans l’instruction if et dans la boucle while. • Les entrées-sorties de valeurs booléennes nécessitent une déclaration
spéciale. • La boucle for s’utilise lorsque le nombre d’itérations est calculable avant
l’arrivée dans la boucle. Une fois ce nombre fixé, il ne varie plus. • La variable de contrôle de la boucle for est déclarée implicitement et n’existe que pour les instructions répétées par for. • La variable de contrôle change de valeur à chaque itération. Cette variation
est toujours de plus (ou moins) 1. • La boucle while s’utilise lorsque le nombre d’itérations n’est pas calcu-
lable avant l’entrée dans la boucle et si l’itération dépend d’une condition qui peut être fausse initialement. • La boucle généralisée loop s’utilise lorsque le nombre d’itérations n’est
pas calculable avant l’entrée dans la boucle et qu’il est nécessaire ou utile de quitter la boucle ailleurs qu’avant la première instruction qu’elle contient. • Attention à ne pas créer de boucle infinie avec while et surtout loop! • L’instruction exit permet de quitter n’importe quelle boucle et devrait s’utiliser essentiellement avec loop.
POINTS À RELEVER
81
• Il existe deux sortes de constantes, les nombres nommés et les constantes
proprement dites. • L’ordre des déclarations est libre dans une partie déclarative à condition
que tout identificateur utilisé soit déjà déclaré.
82
C H A P I T R E
PROCÉDU RES ET
4
83
PROCÉDURES ET FONCTIONS
MOTIVATION
4.1
84
MOTIVATION
Comment effectuer le dessin donné dans la figure 4.1 (sans les coordonnées des points, mises pour fixer les idées)? Figure 4.1 Exemple introductif. (10,10)
(110,10)
(10,110)
(110,110)
La décomposition (sect. 1.7) de ce problème simple conduit à quatre dessins de carrés identiques, disposés à quatre endroits différents. Un langage tel qu’Ada fournit un moyen de ne pas répéter quatre fois les mêmes instructions de dessin d’un carré. Ce moyen est une construction appelée procédure (procedure): -- Cette procedure dessine un carre procedure Dessiner_Carre is begin -- Dessiner_Carre -- Dessin d'un carre de 50 de cote (§ 1.8.3) Line ( 50, 0 ); Line ( 0, 50 ); Line ( –50, 0 ); Line ( 0, –50 ); end Dessiner_Carre;
Une procédure n’est pas exécutée hors de tout contexte. C’est le programme principal (ou une autre procédure) qui déclare la procédure et qui l’appelle. L’appel (call) de la procédure Dessiner_Carre provoque son exécution, donc l’exécution des quatre instructions Line. Cet appel est une instruction et s’écrit en utilisant le nom de la procédure comme par exemple: Dessiner_Carre;
MOTIVATION
85
Suite à ce premier exemple, les lecteurs attentifs et malins auront remarqué que l’instruction Line est en fait un appel de la procédure Line suivi de ce que l’on appelle des paramètres (sect. 4.3) Le programme complet réalisant le dessin proposé est donné dans l’exemple 4.1. Exemple 4.1 Réalisation du dessin au moyen d’une procédure. with Ada.Text_IO; use Ada.Text_IO; with Spider; use Spider; -- Ce programme trace le dessin de quatre carres procedure Dessin_Quatre_Carres is -- Les procedures se declarent dans la partie declarative ------------------------------------------------------------- Cette procedure dessine un carre procedure Dessiner_Carre is Cote : constant := 50;
-- Longueur du cote
begin -- Dessiner_Carre -- Dessin d'un carre de 50 de cote (§ 1.8.3) Line ( Cote, 0 ); Line ( 0, Cote ); Line ( – Cote, 0 ); Line ( 0, – Cote ); end Dessiner_Carre; -----------------------------------------------------------begin -- Dessin_Quatre_Carres -- Presentation du programme ... -- Pour pouvoir dessiner dans la fenetre de dessin Init_Window ("Fenêtre de dessin"); Move_To ( 10,10 ); Dessiner_Carre; point... Move_To ( 110,10 ); autres Dessiner_Carre; Move_To ( 110,110 ); Dessiner_Carre; Move_To ( 10,110 ); Dessiner_Carre; end Dessin_Quatre_Carres;
-- Deplacement au point (10,10) -- Dessin du carre depuis ce -- ... et de meme pour les trois
MOTIVATION
86
On remarquera la lisibilité de ce programme, simple il est vrai, grâce à l’introduction de Dessiner_Carre. Les procédures sont des constructions qui: • améliorent la lisibilité, donc aident à la compréhension d’un programme; • reflètent la décomposition d’un problème en sous-problèmes plus faciles à
analyser et à résoudre; • diminuent la taille des programmes, donc le temps de saisie (!); • réduisent les risques d’erreurs.
Une procédure peut être vue comme une boîte noire (black box). Le programmeur veut utiliser la boîte noire sans se préoccuper de sa structure interne. Ceci facilite l’écriture des programmes, en particulier ceux de grande taille.
STRUCTURE DES PROCÉDURES
4.2
87
STRUCTURE DES PROCÉDURES
Une procédure a la même structure qu’un programme principal (sect. 1.9), sauf la clause de contexte qui est absente. Si la partie déclarative et le corps sont structurellement identiques à ceux d’un programme principal, l’en-tête se compose (fig. 4.2) du mot réservé procedure suivi du nom de la procédure et, optionnellement, de paramètres (sect. 4.3) entre parenthèses (exemple 4.2). L’entête se termine avant le mot réservé is. Figure 4.2 Diagramme syntaxique de l’en-tête d’une procédure. En-tête de procédure procedure
identificateur
liste de paramètres
Liste de paramètres ;
in (
identificateur de paramètre
identificateur de type ou de sous-type
:
)
out ,
in out
:=
access
Exemple 4.2 En-têtes de procédure. -- En-tetes sans parametre procedure Dessiner_Carre procedure Presenter_Programme -- En-tetes avec parametres procedure Line (X, Y : in Integer) procedure Move_To (X, Y : in Integer := 0) procedure Lire_Nombre (Nombre : out Integer) procedure Calculer_Log (Nombre : in out Float)
expression
STRUCTURE DES PROCÉDURES
88
Un commentaire (but, description des paramètres, causes d’exceptions... [AUS 95]) doit accompagner chaque en-tête de procédure ou de fonction! Une procédure peut être déclarée dans une partie déclarative comme celle d’un programme principal, mais aussi dans celle d’une autre procédure (exemple 4.3) ou d’un paquetage (sect. 10.3 et 10.4). Ceci implique que la structure du programme et de ses procédures pourra refléter fidèlement les étapes de la décomposition d’un problème par raffinements successifs (sect. 1.7)! Exemple 4.3 Déclaration de procédures imbriquées. procedure Programme_Principal is -- Declarations du programme principal, dont la procedure Externe: ... -----------------------------------------------------------procedure Externe (...) is -- Declarations de la procedure Externe, dont la procedure -- Interne: ... ---------------------------------------------------------procedure Interne (...) is -- Declarations de la procedure Interne begin -- Interne ... -- Instructions de la procedure Interne end Interne; ---------------------------------------------------------begin -- Externe ... -- Instructions de la procedure Externe end Externe; -----------------------------------------------------------begin -- Programme_Principal ... -- Instructions du programme principal end Programme_Principal;
L’exécution des instructions d’une procédure, par contraction l’exécution d’une procédure, est commandée par l’exécution de l’instruction d’appel de la procédure. La figure 4.3, utilisant le programme Dessin_Quatre_Carres (sect. 4.1), illustre le mécanisme utilisé:
STRUCTURE DES PROCÉDURES
89
Figure 4.3 Exécution d’une procédure. procedure Dessiner_Carre is Cote : constant := 50; ap begin -- Dessiner_Carre pe l Line ( Cote, 0 ); Line ( 0, Cote ); Line ( – Cote, 0 ); Line ( 0, – Cote ); ur to end Dessiner_Carre; re
procedure Dessin_Quatre_Carres is begin -- Dessin_Quatre_Carres ... Move_To ( 10,10 ); Dessiner_Carre; Move_To ( 110,10 ); ... end Dessin Quatre Carres;
PARAMÈTRES DES PROCÉDURES
4.3
90
PARAMÈTRES DES PROCÉDURES
4.3.1
Paramètres d’entrée
La procédure Dessiner_Carre est bien utile pour dessiner un carré. Elle le serait plus encore si elle permettait le dessin d’un carré de n’importe quelle taille. Pour cela il faudrait que la valeur de la constante Cote soit donnée au moment de l’appel de la procédure. Ce mécanisme existe! Il faut transformer la constante Cote en un paramètre (parameter), en sachant que la déclaration des paramètres a lieu dans l’en-tête des procédures (fig. 4.2). La procédure Dessiner_Carre devient: -- Cette procedure dessine un carre dont la longueur du cote est -- passee en parametre procedure Dessiner_Carre ( Cote : in Integer ) is begin -- Dessiner_Carre -- Dessin d'un carre dont le cote vaut Cote Line ( Cote, 0 ); Line ( 0, Cote ); Line ( -Cote, 0 ); Line ( 0, -Cote ); end Dessiner_Carre;
L’instruction d’appel de la procédure est modifiée en spécifiant entre parenthèses la valeur (expression entière) qui doit être donnée au paramètre Cote (exemple 4.4). Il paraît naturel que le type du paramètre et le type de la valeur spécifiée doivent être identiques. Exemple 4.4 Appels de la procédure Dessiner_Carre avec paramètre. Dessiner_Carre (50);le carré aura 50 unités de côté; Dessiner_Carre (Nb_Entier);le carré aura Nb_Entier unités de côté; Dessiner_Carre((10 + Nombre) / 3);le carré aura un côté égal à la valeur
de (10 + Nombre) / 3.
PARAMÈTRES DES PROCÉDURES
91
Figure 4.4 Passage de paramètre en entrée. Dessiner_Carre ( 50 );
procedure Dessiner_Carre ( Cote : in Integer) is begin -- Dessiner_Carre Line ( Cote, 0 ); Line ( 0, Cote ); Line ( - Cote, 0 ); Line ( 0, - Cote ); end Dessiner_Carre;
4.3.2
Précisions concernant les paramètres de procédure
Les paramètres déclarés et utilisés dans les procédures sont appelés paramètres formels (formal parameters); les valeurs (constantes, variables, expressions) spécifiées à l’appel sont appelées paramètres effectifs (effective parameters). Dans la procédure Dessiner_Carre, la valeur représentant la longueur d’un côté est transmise de l’extérieur vers l’intérieur de la procédure (fig. 4.4) à l’appel de la procédure. On parle dans ce cas de paramètre d’entrée pour le paramètre Cote. Le mot réservé in est utilisé pour un paramètre d’entrée. Un tel paramètre est une constante utilisable à l’intérieur de la procédure uniquement. N’importe quel type peut être utilisé dans une déclaration de paramètre, mais seul un identificateur doit être mentionné. Le lecteur malin aura remarqué que, par exemple, l’instruction (§ 2.6.9) Put ( "Donnez les dimensions d'une brique " );
est l’appel d’une procédure Put avec le paramètre d’entrée "Donnez dimensions d'une brique ". 4.3.3
les
Paramètres de sortie
La procédure Dessiner_Carre va être encore modifiée afin de calculer la surface du carré en plus du dessin, et cette valeur sera fournie à l’extérieur de la procédure. Le mécanisme des paramètres va également permettre la transmission de la valeur de la surface. La procédure devient: -- Cette procedure dessine un carre dont la longueur du cote est -- passee en parametre. Elle calcule la surface qui est ensuite -- transmise par parametre a l'exterieur, a l'appelant procedure Dessiner_Carre ( Cote : in Integer;
PARAMÈTRES DES PROCÉDURES
92
Surface : out Integer) is begin -- Dessiner_Carre -- Dessin d'un carre dont le cote vaut Cote Line ( Cote, 0 ); Line ( 0, Cote ); Line ( -Cote, 0 ); Line ( 0, -Cote ); -- Calcul de la surface du carre Surface := Cote ** 2; end Dessiner_Carre;
La déclaration du paramètre Surface comprend le mot réservé out. Cela signifie que Surface est une variable utilisable à l’intérieur de la procédure et qu’elle permet de transmettre une valeur de l’intérieur vers l’extérieur. Cette valeur est transmise à la fin de l’exécution de la procédure (fig. 4.5). Un tel paramètre est appelé paramètre de sortie. Les paramètres effectifs doivent être ici (exemple 4.5) des variables (et non des expressions) puisqu’ils vont recevoir une valeur. Ici également les paramètres formels et effectifs doivent être de (n’importe quel) même type. Exemple 4.5 Appels corrects ou incorrects de la procédure Dessiner_Carre. Dessiner_Carre
(50,
Aire);est
un appel correct de la procédure
Dessiner_Carre ci-dessus (si Aire est de type Integer); Dessiner_Carre ( 50, 30 );est
un appel incorrect de la procédure
Dessiner_Carre ci-dessus (comment le nombre 30 pourrait-il «recevoir»
une valeur?).
Le fait que Dessiner_Carre calcule une surface est un peu artificiel. En effet son nom laisse croire à une simple procédure de dessin. Le calcul a été introduit dans le seul but d’illustrer un paramètre de sortie tout en conservant par ailleurs le reste de la procédure tel quel.
PARAMÈTRES DES PROCÉDURES
93
Figure 4.5 Passage de paramètre en sortie. Dessiner_Carre ( 50, Aire); procedure Dessiner_Carre ( Cote : in Integer; Surface : out Integer ) is begin -- Dessiner_Carre Line ( Cote, 0 ); Line ( 0, Cote ); Line ( - Cote, 0 ); Line ( 0, - Cote ); Surface := Cote ** 2; end Dessiner_Carre;
Le lecteur malin aura remarqué que, par exemple, l’instruction (§ 2.6.9): Get ( Longueur_Mur );
est l’appel d’une procédure Get avec un paramètre de sortie qui recevra une valeur entière donnée par l’utilisateur du programme. 4.3.4
Paramètres d’entrée et de sortie
Un paramètre d’entrée et de sortie permet de transmettre des valeurs de l’extérieur vers l’intérieur d’une procédure et inversement. Soit la procédure Majusculiser qui, comme son nom l’indique, transforme une lettre minuscule en majuscule: -- Cette procedure calcule la majuscule du caractere passe en -- parametre a condition qu'il soit une lettre, c'est-a-dire qu'il -- appartienne a l'intervalle [a..z]. Si ce n'est pas le cas, la -- procedure ne fait rien. procedure Majusculiser ( Caractere : in out Character ) is -- La difference entre le code d'une lettre minuscule et celui -- d'une majuscule est donnee par la difference des codes des -- lettres 'a' et 'A' Decalage : constant Integer := Character'Pos('a') Character'Pos('A'); begin -- Majusculiser if Caractere >= 'a' and Caractere <= 'z' then Caractere := Character'Val ( Character'Pos(Caractere) Decalage); end if;
PARAMÈTRES DES PROCÉDURES
94
end Majusculiser;
Au début de l’appel de la procédure Majusculiser, la valeur du paramètre effectif sera passée au paramètre formel Caractere. Puis la valeur de Caractere sera modifiée si c’est une lettre minuscule. A la sortie de la procédure (retour à l’appelant), la valeur de Caractere sera transmise au paramètre effectif (fig. 4.6). Comme dans le cas d’un paramètre de sortie, le paramètre effectif doit être ici une variable puisqu’il va recevoir une valeur. Les paramètres formels et effectifs doivent également être de (n’importe quel) même type. Figure 4.6 Passage de paramètre en entrée et en sortie. Majusculiser ( Lettre);
procedure Majusculiser ( Caractere : in out Character ) is Decalage : constant Integer := Character'Pos('a') - Character'Pos('A'); begin -- Majusculiser if Caractere in 'a' .. 'z' then -- § 6.1.5 Caractere := Character’Val(Character’Pos(Caractere) - Decalage); end if; end Majusculiser;
4.3.5
Remarques
Comme lors de l’affectation, une erreur se produira à l’exécution si le passage d’une valeur entre paramètres formel et effectif n’est pas possible, par exemple du fait de la violation d’une contrainte (§ 6.1.1). Si le mode de passage d’un paramètre n’est pas explicitement mentionné, alors ce paramètre sera implicitement considéré comme un paramètre d’entrée (in). Lorsqu’une procédure possède plusieurs paramètres ayant des modes de passage différents, et en l’absence d’autres conventions, il est conseillé de commencer par les paramètres d’entrée puis par les paramètres d’entrée et de sortie et de terminer par les paramètres de sortie. Il est également souhaitable de n’écrire qu’un seul paramètre par ligne de code source; cela augmentera la lisibilité de l’en-tête. La procédure Majusculiser pourrait (devrait) en fait être une fonction (sect. 4.6). De telles fonctions de manipulation de caractères existent dans le paquetage
PARAMÈTRES DES PROCÉDURES
95
prédéfini Ada.Characters.Handling [ARM A.3.2]. Dans la procédure Majusculiser, la condition du test devrait en fait s’écrire if Caractere in 'a'..'z' then (§ 6.1.5). Les paramètres notés comme access ne seront pas traités dans cet ouvrage. 4.3.6
Valeurs par défaut des paramètres d’entrée
Soit l’en-tête modifié de la procédure Dessiner_Carre (§ 4.3.1): procedure Dessiner_Carre ( Cote : in Integer := 50)
la présence d’une expression (constante 50) pour le paramètre Cote implique qu’il est possible d’appeler la procédure Dessiner_Carre sans mentionner de paramètre effectif (exemple 4.6)! Lors de l’appel, la valeur du paramètre Cote sera celle donnée par l’évaluation de l’expression (ici 50). L’expression joue donc le rôle de valeur par défaut (default value) du paramètre Cote. Exemple 4.6 Appels de la procédure Dessiner_Carre avec ou sans paramètre effectif. Dessiner_Carre (40);le
carré aura 40 unités de côté; la valeur par défaut
n’est ni calculée ni utilisée; carré aura 50 unités de côté, valeur donnée par le calcul de l’expression par défaut (constante 50).
Dessiner_Carre;le
Seuls les paramètres d’entrée peuvent avoir une valeur par défaut. Il faut donc obligatoirement mentionner un paramètre effectif pour tout paramètre formel d’entrée et de sortie ou de sortie uniquement. 4.3.7
Appel de procédure avec notation par position ou par nom
Les appels de procédure vus jusqu’ici utilisaient tous la notation par position pour les paramètres. La notation par position signifie que l’association paramètre formel-paramètre effectif est effectuée selon la position de chacun d’entre eux, en séparant les paramètres effectifs par une virgule. Il est possible et parfois nécessaire de préciser explicitement comment effectuer cette association. Dans ce cas il faut, toujours à l’appel, mentionner explicitement à quel paramètre formel correspond un paramètre effectif (exemple 4.7), ceci en utilisant le symbole => (flèche). Cette façon de faire s’appelle notation par nom. Exemple 4.7 Appels par position ou par nom. Dessiner_Carre ( 50, Aire );notation par position; Dessiner_Carre (Cote => 50,notation par nom. Surface => Aire );
PARAMÈTRES DES PROCÉDURES
96
En utilisant la notation par nom, il est possible de ne plus respecter l’ordre des paramètres formels. Finalement, le mélange des deux notations est possible en respectant le fait que, dès qu’un paramètre est noté par nom, alors tous les suivants doivent l’être aussi (exemple 4.8). Exemple 4.8 Appels par nom ou mélangés. Dessiner_Carre (Surface => Aire,ordre différent; Cote => 50 ); Dessiner_Carre (50, Surface => Aire );mélange.
4.3.8
Précisions sur le passage des paramètres
La copie d’une valeur entre paramètre effectif et paramètre formel (ou inversement) constitue ce qui est appelé le passage par valeur. Ceci signifie que la valeur du paramètre effectif ne change pas lors de l’exécution de la procédure sauf si, à la fin, la valeur du paramètre formel est recopiée dans le paramètre effectif. Le passage par référence entre paramètre effectif et paramètre formel n’est qu’une vue de l’esprit. Il faut en effet considérer que, dans ce cas, le paramètre formel n’est qu’un nouveau nom pour le paramètre effectif. Ceci signifie que, si par la suite la valeur du paramètre formel est modifiée, la valeur du paramètre effectif l’est de la même manière! La norme Ada laisse une certaine liberté à l’implémentation pour effectuer le passage des paramètres par valeur ou par référence. Mais elle impose cependant que les valeurs d’un type scalaire ou accès (scalar ou access types, [ARM 3.2]) soient toujours passées par valeur, alors que d’autres (non décrites ici) le sont obligatoirement par référence.
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS
4.4
97
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS
A la notion de procédure est étroitement associée celle de région. Une région est un nom donné à une structure comme une procédure, une fonction (sect. 4.6) ou un bloc (§ 6.3.5). Comme les procédures peuvent être emboîtées les unes dans les autres (sect. 4.2), des régions le seront également. Cette hiérarchie (appelée parfois structure de blocs) définit des niveaux (level) de régions: • le programme principal forme la région de niveau 1; • les procédures déclarées directement dans le programme principal forment
chacune une région de niveau 2; • les procédures déclarées dans des procédures de niveau 2 forment chacune
une région de niveau 3; • de manière générale, les procédures déclarées dans des procédures de
niveau N forment chacune une région de niveau N+1. Le numéro de niveau est appelé la profondeur (depth) du niveau. Une région de niveau 5 est plus profonde qu’une région de niveau 2. Exemple 4.9 Imbrication de régions. -- Debut region A procedure Imbrication is
-- 1
Max : constant := 10;
-- Trois identificateurs declares-
Lettre : Character; Nombre : Integer;
-- dans la region A-- 3 -- 4
- 2
------------------------------------------------------------- Debut region B procedure Niveau_2 (Caractere : in Character ) is-- 5 Nombre : Integer;
-- Deux declares dans la region B-
- 6 begin -- Niveau_2 ... end Niveau_2; -- Fin region B ------------------------------------------------------------- Debut region C procedure Aussi_Niveau_2 is -- 7 Nombre : Float;
-- Un seul dans la region C-- 8
----------------------------------------------------------- Debut region D procedure Niveau_3 (Caractere : in Character ) is-- 9 Nombre : Integer; - 10
-- Trois identificateurs declares-
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS
Ok : Boolean;
98
-- dans la region D-- 11
begin -- Niveau_3 ... end Niveau_3; -- Fin region D ---------------------------------------------------------begin -- Aussi_Niveau_2 ... end Aussi_Niveau_2; -- Fin region C -----------------------------------------------------------begin -- Imbrication ... end Imbrication; -- Fin region A
L’exemple 4.9 montre une hiérarchie à trois niveaux: le programme principal appelé Imbrication forme le niveau 1, les procédures Niveau_2 et Aussi_Niveau_2 constituent chacune une région et forment le niveau 2, finalement la procédure Niveau_3 forme une région de niveau 3. De plus on définit la notion de portée d’un identificateur qui est la zone de la région dans laquelle est déclaré un identificateur, zone commençant à sa déclaration et se terminant à la fin de la région. Enfin un identificateur est (directement ou indirectement) visible dans sa portée mais inexistant en dehors. Les questions qui se posent maintenant sont les suivantes: quels identificateurs a-t-on le droit d’utiliser dans une région donnée, et quels objets désignent-ils? Soit la liste des identificateurs déclarés dans l’exemple 4.9: • • • • • • • • •
Imbricationidentificateur (nom du programme) unique; Maxidentificateur (de constante) déclaré une fois; Lettreidentificateur (de variable) déclaré une fois; Nombreidentificateur (de variable) déclaré quatre fois; Okidentificateur (de variable) déclaré une fois; Caractereidentificateur (de paramètre) déclaré deux fois; Niveau_2identificateur (de procédure) déclaré une fois; Aussi_Niveau_2identificateur (de procédure) déclaré une fois; Niveau_3identificateur (de procédure) déclaré une fois.
Quelles sont à présent la portée et la visibilité de ces identificateurs dans les quatre régions de l’exemple 4.9? Le tableau 4.1 donne la réponse à cette
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS
99
interrogation. Tableau 1.2 Portée et visibilité des identificateurs.
Identificateur
Portée dans la région
A
B
C
D
Objet
Déclaré à la ligne
Imbrication
A
oui
oui
oui
oui
prog. princ.
1
Max
A
oui
oui
oui
oui
const. 10
2
Lettre
A
oui
oui
oui
oui
var. caract.
3
Nombre
A
oui
non*
non*
non*
var. entière
4
Nombre
B
non
oui
non
non
var. entière
6
Nombre
C
non
non
oui
non*
var. réelle
8
Nombre
D
non
non
non
oui
var. entière
10
Ok
D
non
non
non
oui
var. bool.
11
Caractere
B
non
oui
non
non
param. car.
5
Caractere
D
non
non
non
oui
param. car.
9
Niveau_2
B
oui
oui
oui
oui
procédure
5
Aussi_Niveau_ 2
C
oui
non
oui
oui
procédure
7
Niveau_3
D
non
non
oui
oui
procédure
9
Les identificateurs qui portent la mention oui pour une région R (R mis pour A, B, C ou D) sont visibles directement, c’est-à-dire utilisables tels quels dans la région R et désignent l’objet mentionné dans l’avant-dernière colonne. Les identificateurs qui portent la mention non pour une région R n’existent pas dans la région R et ne seront donc jamais utilisables dans la région R. Les identificateurs qui portent la mention non* pour une région R sont cachés par un identificateur identique déclaré dans ladite région ou dans une région englobante. Un identificateur caché est inutilisable tel quel dans la région R. Mais un identificateur caché peut être rendu visible indirectement, donc utilisable dans la région R, en préfixant son nom par le nom de la structure englobante et ainsi de suite jusqu’à la structure dans laquelle il a été déclaré et en séparant chaque nom par un point. On appelle nom développé une telle écriture. Par exemple, l’identificateur Nombre déclaré à la ligne 4 est caché dans la
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS
100
région B par le même identificateur Nombre déclaré à la ligne 6. Mais en écrivant Imbrication.Nombre dans la région B, l’on se réfère à l’identificateur déclaré à la ligne 4! Une conséquence de tout ceci est que plusieurs objets déclarés dans des régions différentes peuvent porter le même nom sans que l’on risque de les confondre. De plus il est toujours possible en Ada de préfixer un identificateur par le nom de la structure englobante. Donc les noms développés ainsi obtenus peuvent être assez complexes, comme: • Imbrication.Niveau_2.Nombre • Imbrication.Aussi_Niveau_2.Nombre • Aussi_Niveau_2.Niveau_3.Caractere
Pour l’instant, la déclaration de deux identificateurs identiques dans la même partie déclarative est interdite mais il existe quelques exceptions à cette règle (sect. 4.8).
IDENTIFICATEURS LOCAUX ET GLOBAUX
4.5
101
IDENTIFICATEURS LOCAUX ET GLOBAUX
Les deux définitions suivantes s’emploient également pour caractériser un identificateur (exemple 4.10): • un identificateur est local (local) à une région s’il est déclaré dans ladite
région; • un identificateur est global (global) à une région R s’il est déclaré dans une région englobant R et si R fait partie de la portée de cet identificateur. Exemple 4.10 Identificateurs locaux et globaux dans l’exemple 4.9.
Maxlocal au programme principal, global à toutes les procédures; Caractere (à la ligne 5)local à la procédure Niveau_2; Niveau_2local au programme principal, global aux procédures Aussi_Niveau_2 et Niveau_3; Nombre (à la ligne 8)local à la procédure Aussi_Niveau_2, global à la procédure Niveau_3.
Il faut insister sur le fait qu’un identificateur de procédure est local à la région englobant la procédure, alors qu’un paramètre de procédure est local à la région formée par la procédure. Il faut encore mentionner au lecteur un peu effrayé par cette terminologie que les notions ainsi définies sont conformes à une certaine logique. Avec un peu d’habitude toutes ces explications paraissent même partiellement superflues. Une bonne programmation respectera cependant toujours le principe de localité des déclarations (note 4.1). NOTE 4.1 Principe de localité des déclarations. En général, la déclaration d’un identificateur doit s’effectuer le plus localement possible. Le respect de cette règle permettra d’éviter absolument les variables globales nuisibles à la fiabilité des programmes (sect. 1.5), en particulier lors de l’adaptation du code.
FONCTIONS
4.6
102
FONCTIONS
Une fonction (function) est également une construction permettant de structurer les programmes. Tout ce qui a été dit pour les procédures est valable pour les fonctions (structure, déclaration, imbrication, etc.), exception faite pour les différences mentionnées dans les paragraphes qui suivent. Une fonction a pour but de calculer une valeur appelée résultat (result) ou valeur de retour (return value) de la fonction. L’en-tête d’une fonction (fig. 4.7) comprend le mot réservé function suivi du nom de la fonction et, optionnellement, de paramètres (sect. 4.3), puis du nouveau mot réservé return précédant le nom du type du résultat (exemple 4.11). Seuls des paramètres d’entrée sont autorisés dans l’en-tête d’une fonction. Figure 4.7 Diagramme syntaxique définissant l’en-tête d’une fonction. En-tête de fonction
function
identificateur
liste de paramètres
return
identificateur de type
Exemple 4.11 En-têtes de fonctions. -- En-tetes sans parametre function Nombre_Aleatoire return Float function Annee_Actuelle return Integer -- En-tetes avec parametres function Cube (X : in Integer) return Integer function Log_Naturel (Nombre : in Float := 2.71828) return Float function Est_Pair (Nombre : Integer) return Boolean
L’appel d’une fonction n’est pas une instruction (contrairement à l’appel de procédure) mais fait toujours partie d’une expression. Il consiste à nommer la fonction et à donner la liste des paramètres effectifs entre parenthèses si nécessaire. A l’appel de la fonction, les paramètres sont transmis puis elle est exécutée et son résultat (voir ci-après) est utilisé pour calculer l’expression à laquelle la fonction appartient (exemple 4.12). L’appel d’une fonction est plus prioritaire que n’importe quel opérateur ou attribut.
FONCTIONS
103
Exemple 4.12 Appels des fonctions de l’exemple 4.11. Variable_Reelle := Nombre_Aleatoire; Put ( Cube (Nombre) ); Un := Log_Naturel; Resultat := 2.0 * Log_Naturel (Variable_Reelle) / 5.7; if Pair ( Cube (Nombre) ) then ... end if;
Le résultat d’une fonction est obtenu grâce à l’instruction return. Cette instruction a la forme suivante: return expression;
où • l’expression doit être du même type que celui mentionné dans l’en-tête de la fonction, après le mot réservé return!
L’exécution de cette instruction termine celle de la fonction et le résultat de celle-ci est la valeur de l’expression. Plusieurs instructions return peuvent être placées à l’intérieur d’une fonction. Dans ce cas, la fonction se termine par l’exécution de la première d’entre-elles. L’exemple 4.13 illustre le corps d’une fonction. Exemple 4.13 Exemple complet de fonction. -- Calcule le cube du nombre passe en parametre function Cube (X : in Integer) return Integer is begin -- Cube return X ** 3; end Cube;
Une fonction doit impérativement se terminer par l’exécution d’une instruction return. Si ce n’est pas le cas et que l’exécution arrive à la fin de la fonction (au end final), une erreur survient et l’exception Program_Error (sect. 7.3) est levée.
SPÉCIFICATION ET CORPS DES SOUS-PROGRAMMES
4.7
104
SPÉCIFICATION ET CORPS DES SOUS-PROGRAMMES
On appelle traditionnellement sous-programme (subprogram, subroutine) une procédure ou une fonction. Pour des raisons qui deviendront plus tard impératives (sect. 10.3), il est possible de déclarer un sous-programme en deux parties (voir aussi note 4.2) nommées spécification (specification) et corps (body). Une spécification consiste en l’en-tête suivi immédiatement d’un point-virgule, alors que les sous-programmes complets vus jusqu’ici constituent en fait des corps, composés de l’en-tête suivi du mot réservé is, puis de la partie déclarative (éventuellement vide) et finalement des instructions encadrées par les mots réservés begin et end (exemple 4.14). Même si le langage Ada permet plus de souplesse il faut, pour simplifier, retenir que l’en-tête présent dans la spécification doit être strictement le même que celui du corps. Exemple 4.14 Spécifications et corps. -- Deux specifications... procedure Dessiner_Carre ( Cote : in Integer );-- Noter le ; function Cube ( X : in Integer ) return Integer;-- Noter le ; -- ... et les corps correspondants: -- Cette procedure dessine un carre dont la longueur du cote est -- passee en parametre procedure Dessiner_Carre ( Cote : in Integer ) is-- Noter le is begin -- Dessiner_Carre -- Dessin d'un carre dont le cote vaut Cote Line ( Cote, 0 ); Line ( 0, Cote ); Line ( –Cote, 0 ); Line ( 0, –Cote ); end Dessiner_Carre; -- Calcule le cube du nombre passe en parametre function Cube (X : in Integer) return Integer is-- Noter le is begin -- Cube return X ** 3; end Cube;
NOTE 4.2 Déclarations des spécifications et des corps des procédures et des fonctions. Comme l’exemple 4.14 pourrait le suggérer, le fait de déclarer d’abord toutes les spécifications puis tous les corps est une bonne habitude de programmation, augmentant la lisibilité d’un programme. Cependant le langage Ada laisse libre l’ordre des déclarations (§ 3.9.3).
SPÉCIFICATION ET CORPS DES SOUS-PROGRAMMES
105
SURCHARGE DES SOUS-PROGRAMMES
4.8
106
SURCHARGE DES SOUS-PROGRAMMES
Jusqu’à présent, il était interdit de déclarer, dans la même partie déclarative, deux identificateurs identiques, alors que cette possibilité est autorisée si les déclarations se trouvent dans deux parties déclaratives distinctes. Dans ce dernier cas la deuxième déclaration peut cacher la première (sect. 4.4). Or, à certaines conditions, deux ou plusieurs sous-programmes peuvent porter le même identificateur et être déclarés dans la même partie déclarative. Ces conditions doivent permettre de distinguer les sous-programmes entre eux. Comme leurs noms sont identiques, il faut autre chose pour permettre cette distinction. Deux sous-programmes sont distincts si leurs profils (profile) sont distincts. Le profil d’une procédure consiste en le nombre et l’ordre des types des paramètres. Le profil d’une fonction consiste en le nombre et l’ordre des types des paramètres ainsi que le type du résultat. Deux procédures sont donc distinctes si le nombre ou l’ordre des types des paramètres sont différents (les noms des paramètres, les modes de passage ou les valeurs par défaut ne jouent aucun rôle). De même, deux fonctions sont distinctes si le nombre ou l’ordre des types des paramètres, ou le type du résultat sont différents (ici aussi les noms des paramètres ou les valeurs par défaut ne jouent aucun rôle). La surcharge (overloading) est le fait de pouvoir déclarer des sous-programmes de même nom mais cependant distincts. Enfin, on appelle homologues des sous-programmes qui ne sont pas distincts. Des spécifications distinctes ou non sont données dans l’exemple 4.15. Exemple 4.15 Spécifications distinctes ou non. procedure P (
Param_1 : in T_Param_1; -- Procedure donnee Param_2 : out T_Param_2 ); ---------------------------------------------------------------procedure P; -- Distincte de la procedure donnee procedure P (
Param : in T_Param);
-- Egalement distincte
procedure P (
Param_1 : in T_Param_1; -- Egalement distincte Param_2 : out T_Param_2; Param_3 : in out T_Param_3);
-- Distincte si T_Param_1 est different de T_Param_2 procedure P ( Param_1 : in T_Param_2; Param_2 : out T_Param_1 ); -- Homologue de la procedure donnee procedure P ( Param_1 : in T_Param_1; Param_2 : in T_Param_2 ); ---------------------------------------------------------------function F ( Param : T_Param ) return T_Res;-- Fonction donnee ---------------------------------------------------------------function F return T_Res; -- Distincte de la fonction donnee -- Distincte si T_Res2 est different de T_Res
SURCHARGE DES SOUS-PROGRAMMES
107
function F ( Param : T_Param ) return T_Res2; -- Homologue de la fonction donnee function F ( Param : T_Param := ...) return T_Res;
Dans tous les cas, un sous-programmes surchargé doit être appelé sans ambiguïté. Cela signifie que le nombre, le type des paramètres effectifs et le type attendu pour le résultat d’une fonction doivent permettre au compilateur de déterminer quel sous-programme est appelé. Si aucun, ou plus d’un sous-programme correspond à l’appel, celui-ci est alors ambigu et le compilateur générera une erreur. Exemple 4.16 Appels de procédures surchargées. procedure P; -- Trois procedures surchargees procedure P ( I : in Integer := 0); procedure P ( I : in Float ); ... P ( 1 ); -- Deuxieme procedure appelee P ( 1.0 ); -- Troisieme procedure appelee P; -- Appel ambigu (erreur a la compilation)
Dans l’exemple 4.16, il est impossible d’appeler la première procédure puisque la notation P; pourrait signifier non seulement l’appel de la première procédure mais aussi l’appel de la deuxième avec utilisation de la valeur par défaut!
SURCHARGE DES OPÉRATEURS
4.9
108
SURCHARGE DES OPÉRATEURS
Le langage Ada permet de déclarer des fonctions-opérateurs qui surchargent certains opérateurs prédéfinis. Ces opérateurs surchargeables sont: • les opérateurs abs not + – (unaires); • les opérateurs and or xor mod rem = /= < <= > >= + – * / ** & (binaires).
Il faut cependant qu’une fonction surchargeant un opérateur unaire n’ait qu’un paramètre, alors qu’une fonction surchargeant un opérateur binaire doit en avoir exactement deux. Le «nom» de la fonction-opérateur est celui de l’opérateur entre guillemets. L’appel peut se faire sous la forme fonctionnelle, mais pour des raisons pratiques, comme pour l’opérateur surchargé (exemple 4.17). Exemple 4.17 Déclarations et appels de fonctions-opérateurs. -- Addition entre un entier et un reel function "+" ( I : Integer; F : Float ) return Float; function "+" ( F : Float; I : Integer ) return Float; -- Pour illustrer une fonction-operateur a un parametre function "abs" ( Valeur : Un_Type ) return Un_Type; X := X := X := X := reels V :=
Premiere fonction "+" appelee Premiere fonction "+" appelee Seconde fonction "+" appelee Operateur "+" predefini pour les
"+"( 3, 2.3 ); 3 + 2.3; 2.3 + 3; 2.0 + 3.0;
-----
abs Val;
-- Val de type Un_Type (sect. 5.1)
Pour être complet, remarquons que si l’opérateur d’égalité "=" est surchargé avec un résultat de type Boolean, alors l’opérateur d’inégalité "/=" est implicitement redéfini. Finalement l’opérateur d’inégalité "/=" peut être redéfini explicitement uniquement si le résultat de la fonction-opérateur qui le surcharge n’est pas Boolean.
COMPLÉMENTS
109
4.10 COMPLÉMENTS Comme mentionné auparavant (sect. 4.7), on nomme traditionnellement sousprogramme une construction comme une procédure ou une fonction qui, par sa structure, rappelle un programme. En Ada ledit programme est d’ailleurs une procédure (sect. 1.9) alors que c’est une fonction dans le langage C. Une instruction return (sans expression) peut être présente dans le corps d’une procédure. Dans ce cas, son exécution provoque simplement la fin de l’exécution de la procédure, comme si le end final avait été atteint.
110
4.11 EXERCICES 4.11.1
Dessin d’un paysage
Ecrire un programme qui dessine un paysage schématique où le dessin de chaque élément du paysage est effectué par une procédure. Il faudra donc créer, par exemple, des procédures comme Dessiner_Sapin, Dessiner_Rocher, Dessiner_Chalet, Dessiner_Fenêtre_Chalet, etc. 4.11.2
Calcul du nombre de jours entre deux dates
Ecrire un programme qui calcule le nombre de jours entre deux dates, en introduisant deux sous-programmes, le premier qui retourne le nombre de jours d’un mois donné et le second qui indique si une année est bissextile ou non en sachant qu’une année est bissextile si elle est divisible par 400, ou alors par 4 mais pas par 100. 4.11.3
Conversions d’unités de mesure
Ecrire des procédures de conversion d’unités de mesure anglo-saxonnes (pieds, pouces, livre, etc.) en unités SI (système international: mètre, kilo etc.) et inversement. 4.11.4
Conversions d’unités de mesure
Reprendre l’exercice 4.11.3 et transformer les procédures en fonctions. 4.11.5
Transformation de procédure en fonction
Transformer en fonction la procédure Majusculiser du paragraphe 4.3.4. 4.11.6
Notations par position et par nom
Soit la spécification de procédure suivante: procedure P (Par_Logique : in Boolean; Par_Entier : Integer; Par_Reel : in out Float; Par_Caractere : out Character);
Ecrire des appels de P en utilisant la notation par nom, puis celle par position, et enfin en mélangeant les deux. 4.11.7
Surcharge de sous-programmes
Parmi les spécifications suivantes, quelles sont celles qui se surchargent? procedure procedure procedure procedure procedure
P; P ( P ( P ( P (
E A E E
: : : :
in Integer); Integer); in out Integer); in Integer := 0);
111
procedure P ( procedure P ( procedure P ( procedure P (
E E S E S E S
: : : : : : :
out Integer); in Integer; out Float); in Float; out Float); in Integer; out Float);
POINTS À RELEVER
112
4.12 POINTS À RELEVER 4.12.1
En général • Les procédures et les fonctions constituent les sous-programmes. • Les sous-programmes servent à regrouper des instructions pour faciliter la
lisibilité des programmes et pour remplacer cette suite d’instructions par un seul appel. • Les paramètres de sous-programmes permettent aux sous-programmes de
produire des résultats qui dépendent de ces paramètres. • Les paramètres dits formels sont ceux déclarés dans l’en-tête d’un sous-
programme. • Les paramètres effectifs sont les valeurs transmises à un sous-programme
ou les variables qui recueilleront des valeurs calculées par le sousprogramme. • La portée d’un identificateur est la zone de texte dans laquelle l’iden-
tificateur est utilisable. • Un identificateur est local à une région s’il est déclaré à l’intérieur de la
région. • Un identificateur est global à une région s’il est déclaré hors de la région et
utilisable dans la région. • Il faudrait absolument respecter le principe de localité des déclarations.
4.12.2
En Ada • Les procédures peuvent comporter des paramètres d’entrée, d’entrée et de
sortie ou de sortie. • Les fonctions ne peuvent comporter que des paramètres d’entrée. • Une fonction retourne toujours un seul résultat qui peut être de n’importe
quel type. • Les paramètres formels d’entrée peuvent mentionner une valeur par défaut. • La notation par position, par nom ou un mélange des deux permet l’écriture
des paramètres effectifs dans l’appel d’un sous-programme. • Un identificateur visible directement peut être mentionné tel quel. • L’utilisation d’un identificateur caché nécessite un nom développé. • Un sous-programme peut être séparé en une spécification et un corps. • Il est possible de déclarer des opérateurs comme des fonctions.
POINTS À RELEVER
113
• Les sous-programmes et les opérateurs peuvent être surchargés. • L’instruction return termine l’exécution d’un sous-programme et fournit
le résultat dans le cas d’une fonction. • Les spécifications devraient toutes être déclarées avant les corps.
114
C H A P I T R E
T T É T
YPES, YPES NUMÉRA IFS,
5
115
TYPES, TYPES ÉNUMÉRATIFS, INSTRUCTION CASE
NOTION DE TYPE
5.1 5.1.1
116
NOTION DE TYPE Motivation
La connaissance des types et des instructions en Ada est relativement limitée jusqu’à présent. Quatre types de base (Integer, Character, Boolean, Float) et huit instructions (affectation, if, while, for, loop, exit, return et appel de procédure) ont été expliqués. Le chapitre en cours ainsi que les suivants présentent tous les autres types du langage Ada à savoir les types énumératifs, numériques, articles, tableaux, chaînes de caractères, pointeurs et fichiers ainsi que les instructions case, null, raise et l’instruction-bloc. Ceci va nous permettre de réaliser des programmes importants non seulement du fait de leur corps parfois volumineux mais aussi et surtout à cause des données complexes qu’ils vont manipuler. Les données ainsi que la manière de les représenter forment ce que l’on appelle les structures d’information ou structures de données (data structures) des programmes. 5.1.2
Généralités
Un type (type) est formé d’un groupe de valeurs et d’opérations possibles sur ces valeurs. Le tableau 5.1 donne quelques exemples de types prédéfinis. Tableau 5.1 Exemples de types et de leurs opérations.
Nom du type
Valeurs du type
Opérations possibles
Integer
tous les nombres entiers compris entre les bornes Integer'First et Integer'Last
affectation, addition, soustraction, comparaison, attributs, etc.
Character
tous les caractères compris entre les deux bornes Character'First et Character'Last
affectation, comparaison, attributs, etc.
Boolean
les deux valeurs False et True
affectation, comparaison, attributs, etc.
Lorsque l’on parle, par exemple, de «valeur de type Integer», cela veut dire «une valeur appartenant à une catégorie particulière de nombres entiers (Integer), valeur avec laquelle on peut additionner, soustraire, multiplier, diviser, prendre le reste de la division...». Comme déjà mentionné, les types permettent de structurer les données. Ils permettent aussi au compilateur de générer du code effectuant des vérifications de validité des données comme, par exemple, détecter le débordement de capacité
NOTION DE TYPE
117
lorsqu’un nombre entier devient trop grand. Mais pour le programmeur ils ont l’incomparable avantage de le forcer à respecter la règle des types. Celle-ci, particulièrement stricte en Ada, définit que doivent en particulier être du même type: • tous les opérandes d’une expression; • le type de l’expression et celui de la variable dans une instruction
d’affectation; • le type d’un paramètre formel et celui du paramètre effectif correspondant.
Cette règle permet au compilateur de détecter, avant l’exécution du programme, des erreurs commises par le programmeur appelées erreurs de sémantique. Une erreur de sémantique se produit lorsque la syntaxe d’une instruction est respectée mais que l’instruction est néanmoins incorrecte (exemple 5.1). Exemple 5.1 Erreurs de sémantique. if 3 + 4 then...3 + 4 n’est pas booléen; Variable_Entiere := 3.5;3.5 n’est pas entier; for I in 1..'9' loop... 1 et '9' ne sont pas du même type; Ada.Float_Text_IO.Put ( 10 );le type du paramètre formel (Float,
§ 2.6.6) est
différent du type du paramètre effectif (Integer). En plus des types prédéfinis, le programmeur peut créer ses propres types à partir de constructions syntaxiques adéquates. Pour pouvoir utiliser ses propres types il faut d’abord les déclarer dans une ou plusieurs parties déclaratives. Si chaque catégorie de type possède son style propre de déclaration, le début de la déclaration d’un type est commun à une majorité de types: type Nom_Du_Type is
Deux cas particuliers existent mais ne seront pas développés ici (les types tâches et objets protégés). L’exemple 5.2 présente une liste non exhaustive de déclarations de types. Exemple 5.2 Déclarations de types. -- Sect. 5.2 type Jours_De_La_Semaine is ( Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche ); -- Sect. 5.2 type Mois_De_L_Annee is (
Janvier, Fevrier, Mars, Avril, Mai, Juin, Juillet, Aout, Septembre, Octobre, Novembre, Decembre );
NOTION DE TYPE
type Date is record Jour : Integer; Mois : Mois_De_L_Annee; Annee : Integer; end record;
118
-- § 7.2.1
type Tableau is array (1..10) of Float; -- § 8.2.2 type Monnaie is new Float;
-- Sect. 19.2
type Pointeur is access Date;
-- Sect. 15.2
type Table is private;
-- Sect. 16.2
type Queue is limited private;
-- Sect. 16.9
task type Locomotive; -- Les deux cas particuliers protected type Tampon is ... end Tampon;
La notion de type va donc permettre de catégoriser les données traitées, ce qui implique une représentation plus agréable et un traitement plus sûr (règle des types!) de ces mêmes données. Par convention, on peut faire précéder le nom d’un type par le préfixe T_ ou le faire suivre par le suffixe _Type, ce qui permet de préciser que cet identificateur est le nom d’un type (et non celui d’une variable par exemple).
TYPES ÉNUMÉRATIFS
5.2
119
TYPES ÉNUMÉRATIFS
5.2.1
Motivation
Comment représenter les jours de la semaine par exemple? Jusqu’à présent, la seule façon de faire était de déclarer sept constantes de la manière suivante: Lundi : constant := 0; Mardi : constant := 1; Mercredi : constant := 2;
etc. Ceci peut conduire à des erreurs ou être fastidieux à déclarer. Un type énumératif va avantageusement résoudre ce genre de problèmes. 5.2.2
Généralités
Les types énumératifs (enumeration types) permettent de déclarer des valeurs en les énumérant sous la forme d’une suite de noms. Il est ainsi possible d’écrire: type T_Jours_De_La_Semaine is ( Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche );
Cette déclaration permet l’utilisation du type T_Jours_De_La_Semaine formé des valeurs Lundi, Mardi... Dimanche. L’exemple 5.3 présente deux autres types énumératifs. Exemple 5.3 Deux exemples traditionnels de types énumératifs. type T_Couleurs_Arc_En_Ciel is ( type T_Mois_De_L_Annee is (
Rouge, Orange, Jaune, Vert, Bleu, Indigo, Violet );
Janvier, Fevrier, Mars, Avril, Mai, Juin, Juillet, Aout, Septembre, Octobre, Novembre, Decembre );
Les constantes d’un type énumératif sont par définition les identificateurs énumérés entre les parenthèses. Les opérations possibles (en plus de l’affectation et du passage en paramètre) sur les valeurs d’un type énumératif sont les comparaisons = /= <= < >= >. En effet les valeurs d’un type énumératif sont ordonnées, codées selon leur ordre de déclaration. A chaque valeur énumérée correspond un numéro d’ordre (nombre entier). La première valeur porte le numéro 0, la seconde le numéro 1, etc. Dans l’exemple du type T_Jours_De_La_Semaine, Lundi porte le numéro 0, Mardi porte le numéro 1, etc. Donc: Lundi < Mardi < Mercredi < Jeudi < Vendredi < Samedi < Dimanche
Les expressions se limitent aux constantes, variables et attributs (§ 5.2.4)
TYPES ÉNUMÉRATIFS
120
applicables aux valeurs du type énumératif considéré. Finalement, les types énumératifs font partie des types discrets (discrete types, [ARM 3.2]). 5.2.3
Affectation
L’affectation se fait de manière habituelle, comme dans l’exemple 5.4. Exemple 5.4 Affectation de valeurs d’un type énumératif. -- ... procedure Exemple_5_4 is type T_Jours_De_La_Semaine is ( Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche ); Demain : T_Jours_De_La_Semaine; -- Trois variables de ce type Hier : T_Jours_De_La_Semaine; Jour : T_Jours_De_La_Semaine := Lundi; begin -- Exemple_5_4 Demain := Jour; -- Affecte Jour := Jeudi; -- Affecte Demain := T_Jours_De_La_Semaine'Succ Hier := T_Jours_De_La_Semaine'Pred ( ...
5.2.4
la valeur Lundi a Demain la valeur Jeudi a Jour ( Jour );-- § 5.2.4 Jour );-- § 5.2.4
Attributs First, Last, Succ, Pred, Pos et Val
Les attributs First, Last, Succ et Pred sont applicables aux valeurs d’un type énumératif, ce qui permet maintenant de préciser que ces attributs sont en fait applicables aux valeurs de n’importe quel type discret (discrete type, [ARM 3.2]). Comme pour les caractères, les attributs Pos et Val permettent de travailler avec les numéros d’ordre des valeurs énumérées. Mais les cas sont rares où une telle manipulation est indispensable! Quelques cas d’utilisation de ces attributs sont présentés dans l’exemple 5.5. Exemple 5.5 Utilisation des attributs First, Last, Succ, Pred, Pos et Val. T_Jours_De_La_Semaine'First donne la première valeur, à savoir Lundi; T_Jours_De_La_Semaine'Lastdonne Dimanche; T_Jours_De_La_Semaine'Pred (Mardi)donne Lundi; T_Jours_De_La_Semaine'Succ (Mardi)donne Mercredi; T_Jours_De_La_Semaine'Pos (Lundi)donne 0, le numéro d’ordre de la
valeur Lundi; T_Jours_De_La_Semaine'Val (6)donne Dimanche; T_Jours_De_La_Semaine'Pred (Lundi)provoque une erreur
de compilation;
TYPES ÉNUMÉRATIFS
121
T_Jours_De_La_Semaine'Val (Nombre)provoque
une erreur à l’exécution si
Nombre > 6.
5.2.5
Entrées-sorties
Comme pour tous les types simples, Ada offre la possibilité d’effectuer des entrées-sorties sur des valeurs énumérées. Sans en décrire ni expliquer le mécanisme utilisé (sect. 17.4), ces entrées-sorties sont disponibles, utilisables comme pour les booléens (§ 3.4.5) après avoir effectué la déclaration suivante: package ES_Jours is new Ada.Text_IO.Enumeration_IO ( T_Jours_De_La_Semaine ); Exemple 5.6 Entrées-sorties de valeurs énumérées. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_5_6 is type T_Jours_De_La_Semaine is ( Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche ); Jour : T_Jours_De_La_Semaine;
-- Une variable de ce type
package ES_Jours is new Enumeration_IO(T_Jours_De_La_Semaine); use ES_Jours;
-- § 2.6.9
begin -- Exemple_5_6 Put_Line (" Cet exemple..." ); Get (Jour); Put (Jour);
-- Lit un jour de la semaine -- Affiche en majuscules la valeur
lue Put (Mardi); Put (T_Jours_De_La_Semaine'Val(6)); ...
-- Affiche MARDI -- Affiche DIMANCHE
Comme suggéré dans l’exemple 5.6, les valeurs énumérées sont affichées en majuscules sur le nombre minimum de positions. Il est cependant possible de modifier ces aspects d’affichage [ARM A.10.10], à savoir le nombre de positions et la casse. 5.2.6
Types énumératifs, types Boolean et Character
Le type Boolean est en fait un type énumératif prédéfini: type Boolean is (False, True);
Ceci n’enlève rien aux notions présentées dans la section concernée (sect. 3.4)
TYPES ÉNUMÉRATIFS
122
mais permet simplement de préciser que tout ce qui s’applique aux types énumératifs est également valable pour le type Boolean. Le type prédéfini Character fait aussi partie des types énumératifs car il est possible de déclarer des caractères entre apostrophes comme valeurs énumérées. Cette dernière notion n’est cependant pas présentée dans cet ouvrage car, exception faite du type Character (et parfois Wide_Character, sect. 19.3), elle est peu utilisée dans la pratique. 5.2.7
Types énumératifs et instruction for
Un type énumératif peut être utilisé pour donner l’intervalle de variation d’une boucle for (§ 3.5.2). En utilisant le type T_Jours_De_La_Semaine ces trois formes sont équivalentes: • • • • •
for for ... end for
Jour in Lundi..Dimanche loop ... end loop; Jour in T_Jours_De_La_Semaine'First..Dimanche loop loop; Jour in T_Jours_De_La_Semaine loop ... end loop;
Cette dernière forme utilise le nom du type comme intervalle, ce qui signifie que l’intervalle comporte toutes les valeurs, de la première à la dernière. Cette forme d’intervalle (§ 6.1.1) permet d’assurer, même en cas de modification du type T_Jours_De_La_Semaine, que l’intention du programmeur est respectée ipso facto, c’est-à-dire que la variable de boucle prendra toujours toutes les valeurs du type énumératif.
INSTRUCTION CASE
5.3
123
INSTRUCTION CASE
5.3.1
Motivation
Les sections traitant de l’instruction if (sect. 3.1 à 3.3) ont montré comment programmer des sélections. Ici prend place la programmation d’un choix multiple, basé non pas sur les valeurs d’expressions booléennes comme l’instruction if généralisée (sect. 3.3) mais sur les valeurs d’un type discret. Cette sélection sera réalisé par l’instruction case. Comment poser une question à l’utilisateur du programme en supposant que celui-ci répond par un chiffre et que l’exécution du programme se poursuit en fonction de ce chiffre? Jusqu’à présent ce problème serait résolu par une instruction if généralisée. La façon suivante de procéder est plus lisible et plus sûre: Reponse : Integer; ... Get ( Reponse ); case Reponse is when 1 =>...;
-- Nombre donne par l'utilisateur -- Reponse donnee par l'utilisateur -- Traitement si Reponse est 1
when 2 | 3 =>...;
-- Traitement si Reponse est 2 ou 3
when 4 .. 6 =>...;
-- Traitement si Reponse est 4, 5 ou
when 7 .. 9 | 99 =>...;
-----
6
when others =>...;
Traitement si Reponse est entre 7 et 9, ou 99 Traitement si Reponse est autre chose qu'un nombre entre 1 et 9
ou 99 end case;
5.3.2
Forme générale de l’instruction case
La forme générale de l’instruction case est: case expression is when choix_1 => traitement_1;-- Premiere branche when choix_2 => traitement_2;-- Deuxieme branche when choix_3 => traitement_3;-- Troisieme branche ... when others => autre_traitement;-- Derniere branche end case;
avec • expression d’un type discret (discrete type, [ARM 3.2]); • un nouveau mot réservé when délimitant les branches de l’instruction; • les choix_n statiques (sect. 3.10), formés de valeurs ou d’intervalles
séparés par des barres verticales, valeurs et bornes d’intervalle du type de l’expression;
INSTRUCTION CASE
124
• les traitements_n composés d’une ou de plusieurs instructions; • le mot réservé others qui représente toutes les autres valeurs du type.
L’exécution d’une instruction case débute par l’évaluation de l’expression. Puis la branche (le traitement) correspondant à la valeur est choisie et exécutée. Puis le reste de l’instruction case est sauté et l’exécution se poursuit après la fin (end case) de l’instruction. 5.3.3
Compléments sur l’instruction case
Un choix (représentant une valeur possible de l’expression) ne peut apparaître qu’une seule fois. Si l’expression a une valeur ne correspondant à aucun des choix mentionnés explicitement, la branche commençant par when others, qui doit être la dernière, est exécutée. L’instruction null qui ne fait rien (!) peut être utilisée dans une branche où aucun traitement ne doit être effectué. L’instruction null n’est d’ailleurs pas restreinte à une telle branche mais est utilisable partout où une instruction est autorisée, et utile là où une instruction est exigée par le langage Ada mais où le programmeur ne veut rien faire. Si les choix mentionnés explicitement (autres que others) couvrent toutes les valeurs possibles de l’expression, alors la branche commençant par when others est optionnelle. Donc, si la variable Jour est du type T_Jours_De_La_Semaine (§ 5.2.2), alors la forme suivante est autorisée: case Jour is -- Appel d'une procedure when Lundi .. Vendredi => Travailler; -- Appel d'une autre procedure when Samedi => Faire_La_Fête; -- Ne rien faire le dimanche! when Dimanche => null; end case;
125
5.4
EXERCICES
5.4.1
Déclaration de types énumératifs
Déclarer des types énumératifs pour représenter: • les couleurs de l’arc-en-ciel; • les capitales des pays de l’Union européenne.
5.4.2
Utilisation de types énumératifs
Ecrire un programme qui affiche toutes les valeurs d’un type énumératif, ainsi que le prédécesseur, le successeur et la position de chacune d’entre elles. 5.4.3
Calcul du nombre de jours entre deux dates
Reprendre l’exercice 4.11.2 et modifier la solution de manière à ce que le sousprogramme qui retourne le nombre de jours d’un mois donné utilise le type T_Mois_De_L_Annee (§ 5.2.2) et une instruction case. 5.4.4
Petites statistiques sur une ligne de texte
Reprendre l’exercice 3.11.6 et modifier la solution de manière à ce que la catégorisation des caractères lus soit effectuée par une instruction case. 5.4.5
Mini-calculatrice
Ecrire un programme qui simule une mini-calculatrice en calculant des expressions formées d’opérandes réels, des opérateurs +, –, *, / et terminées par le symbole = comme 2.0 + 3.5 * 4.2 = par exemple. Pour des raisons de simplicité, la priorité des opérateurs peut être ignorée (l’expression est alors simplement calculée de gauche à droite).
POINTS À RELEVER
5.5 5.5.1
126
POINTS À RELEVER En général • Un type est formé d’un groupe de valeurs et d’opérations possibles sur ces
valeurs. • Les types servent à structurer les données et à permettre des vérifications à
la compilation comme à l’exécution des programmes. • La règle des types renforce la fiabilité des programmes en permettant la
détection d’erreurs par le compilateur. • Certains types sont prédéfinis, d’autres peuvent être construits par le pro-
grammeur. 5.5.2
En Ada • Un type énumératif permet l’énumération des valeurs (discrètes) du type. • Les types énumératifs et les types entiers forment les types discrets. • Les entrées-sorties de valeurs énumérées nécessitent une déclaration spé-
ciale. • L’instruction case (sélection) permet l’exécution d’une branche d’ins-
tructions parmi plusieurs, éventuellement aucune, en fonction de la valeur d’une expression discrète.
127
C H A P I T R E
SOUSTYPES, TYPES NUMÉRIQ
6
128
SOUS-TYPES, TYPES NUMÉRIQUES, EXCEPTIONS
SOUS-TYPES
6.1
129
SOUS-TYPES
6.1.1
Généralités
Un sous-type (subtype) permet d’appliquer une contrainte (constraint) à un type appelé type de base. Cette contrainte peut prendre différentes formes selon que le type de base est discret, réel ou autre. Pour l’instant, l’une des formes les plus utilisées, nécessaire pour les notions à venir est la contrainte d’intervalle, applicable à tout type discret [ARM 3.2]. Elle consiste à restreindre l’ensemble des valeurs possibles d’un tel type (valeurs qui forment en fait déjà un intervalle) en un intervalle plus petit (exemple 6.1). Un intervalle (range) s’écrit en respectant l’une des trois formes suivantes: borne_inf .. borne_sup identificateur_de_type_ou_sous_type attribut_range
où • borne_inf et borne_sup sont des expressions d’un type de base discret; • identificateur_de_type_ou_sous_type d’un type de base égale-
ment discret ; • attribut_range sera introduit plus loin (§ 8.2.7). Exemple 6.1 Exemples d’intervalles. 0..9valeurs entières de 0 à 9, le type de base est Integer; Lundi..Vendredivaleurs énumérées de Lundi à Vendredi,
le type de base est T_Jours_De_La_Semaine (§ 5.2.2); 'A'..'Z'lettres majuscules, le type de base est Character; Integer'First..–1nombres entiers négatifs, le type de base est Integer; I..J – 1nombres entiers de I à J–1, le type de base est un type entier (celui de I et J) avec le fait que, si I >= J, l’intervalle est vide; Val_1..Val_2les valeurs de Val_1 à Val_2, le type de base peut être n’importe quel type discret; Characterl’intervalle des 256 valeurs du type prédéfini Character (§ 3.8.1); T_Jours_De_La_Semainel’intervalle des 7 valeurs (noms des jours) du type T_Jours_De_La_Semaine (§ 5.2.2).
Une contrainte d’intervalle (sur un type discret) a la forme suivante: range intervalle
où • range est un nouveau mot réservé; • intervalle est soit borne_inf..borne_sup soit attribut_range
SOUS-TYPES
130
(identificateur_de_type_ou_sous_type est ici interdit). Il est maintenant possible de donner la forme générale de la déclaration d’un sous-type d’un type discret par le diagramme syntaxique de la figure 6.1. Figure 6.1 Diagramme syntaxique définissant un sous-type discret. Déclaration de sous-type discret
subtype
Identificateur
is
Type_de_base
; Contrainte d’intervalle
Exemple 6.2 Déclarations de sous-types de types discrets. subtype T_Chiffre is Integer range 0 .. 9; subtype T_Jours_Travail is T_Jours_De_La_Semaine range Lundi..Vendredi; subtype T_Majuscules is Character range 'A'..'Z'; subtype T_Negatifs is Integer range Integer'First .. –1; subtype T_Dynamique is Integer range 1 .. Max;-- Max, variable -- entiere subtype T_Entiers is Integer;
Les deux dernières déclarations de l’exemple 6.2 nécessitent des explications complémentaires. Le sous-type T_Dynamique obtient la borne supérieure par la valeur de la variable Max. Cette valeur est donc fournie à l’exécution et non à la compilation. Il faut donc s’assurer que Max ait une valeur bien définie avant la déclaration de T_Dynamique. De plus le sous-type T_Entiers, comme aucune contrainte n’est donnée, est en fait un synonyme d’Integer. Cette façon de faire peut être utilisée pour surnommer n’importe quel type. Il existe des situations, en relation avec l’utilisation de paquetages (sect. 10.5), où un tel surnommage est pratique pour changer le nom d’un type exporté. 6.1.2
Utilisation et utilité des sous-types
Un sous-type n’introduit donc pas un nouveau type mais permet de restreindre les valeurs utilisables d’un type. Un identificateur de sous-type peut être utilisé, en
SOUS-TYPES
131
général, là où un identificateur de type est permis. Exemple 6.3 Utilisation de sous-types de l’exemple 6.2. Chiffre : T_Chiffre;variable
dont les valeurs doivent se situer entre 0 et
9; Jour : T_Jours_Travail;variable
dont les valeurs doivent se situer entre
Lundi et Vendredi; T_Jours_Travail'Firstattribut de valeur Lundi; T_Jours_Travail'Lastattribut de valeur Vendredi; procedure P (Lettre : in T_Majuscules);Le paramètre
effectif devra
être une lettre majuscule.
L’attribution d’une valeur à un objet dont la déclaration comporte une contrainte, donnée par un sous-type par exemple, est autorisée si la valeur respecte la contrainte (exemple 6.4). Dans le cas contraire, une erreur (l’exception Constraint_Error, sect. 6.3) est générée à l’exécution du programme. Exemple 6.4 Attribution de valeurs à des objets dont la déclaration comporte une contrainte. Chiffre : T_Chiffre;deux variables contraintes par un sous-type; Jour : T_Jours_Travail; Nombre : Integer;une variable de type Integer; Lettre : Character;une variable de type Character; ... Jour := Mercredi;toujours correct; Nombre := Chiffre;toujours correct; Chiffre := Nombre;correct si la valeur de Nombre est entre 0 et 9, une erreur
est générée dans le cas contraire; Jour := Samedi;erreur générée, Samedi n’est pas jour de travail; P ( 'A' );toujours correct (P est declarée dans l’exemple 6.3); P ( Lettre );correct si la valeur de Lettre est une majuscule, une erreur est générée dans le cas contraire.
Le principal intérêt des sous-types réside dans le fait que leur utilisation permet la détection d’erreurs à l’exécution (note 6.1), lorsque des valeurs inappropriées sont attribuées à un objet (affectation à une variable, passage en paramètre, etc.). NOTE 4.3 Des erreurs à l’exécution sont moins graves que des résultats faux.
SOUS-TYPES
132
Il faut toujours avoir à l’esprit qu’un programme qui produit des résultats faux est plus difficile à corriger qu’un programme qui se termine brutalement par une erreur, surtout si l’apparition de l’erreur donne des indications précises sur l’endroit erroné du programme. L’arrivée d’erreurs, aussi frustrantes soient-elles, doit donc être considérée comme un symptôme moins grave que la production de résultats incertains.
Les sous-types imposent donc au programmeur une réflexion plus approfondie dans le choix des valeurs pour des variables et permettent la vérification à l’exécution des contraintes sur ces variables. Il faut préciser que cette vérification est effectuée par du code adéquat généré par le compilateur. 6.1.3
Sous-types prédéfinis
Il n’existe que deux sous-types prédéfinis dont la déclaration est la suivante: subtype Natural is Integer range 0 .. Integer'Last; subtype Positive is Integer range 1 .. Integer'Last;
Ces deux sous-types d’Integer s’utilisent lorsqu’un objet entier doit contenir une valeur positive ou nulle (Natural) ou strictement positive (Positive). 6.1.4
Attributs applicables aux sous-types
De manière générale, les attributs applicables à un type sont utilisables avec tout sous-type de ce type (exemple 6.5). Il faut simplement préciser que, si la valeur retournée par un attribut appliqué à un sous-type appartient au type de base, elle n’est pas soumise à la contrainte imposée par le sous-type. Exemple 6.5 Attributs et sous-types (voir exemple 6.2). T_Jours_Travail'Firstattribut de valeur Lundi; T_Jours_Travail'Succ(Vendredi)attribut de valeur Samedi; T_Jours_Travail'Pred(Jour)erreur générée à l’exécution si Jour vaut Lundi; T_Jours_Travail'Val(0)attribut de valeur Lundi; T_Jours_Travail'Val(6)attribut de valeur Dimanche; T_Chiffre'Pos(0)attribut de valeur 0; T_Negatifs'Lastattribut de valeur –1; T_Negatifs'Succ(T_Negatifs'Last)attribut de valeur 0.
Finalement, comme le nom d’un sous-type discret représente également l’intervalle des valeurs du sous-type, ce nom peut s’utiliser partout où un intervalle est autorisé (sauf dans une contrainte d’intervalle), en particulier dans les instructions for (sect. 3.5) et case (sect. 5.3) comme par exemple: -- La boucle s'effectuera cinq fois
SOUS-TYPES
133
for Jour in T_Jours_Travail loop ...; end loop; case Jour is -- Jour du type T_Jours_De_La_Semaine when T_Jours_Travail => ...; -- Travailler when Samedi | Dimanche => ...; -- Se reposer end case;
6.1.5
Tests d’appartenance
Il existe deux tests d’appartenance (ressemblant à deux opérateurs) in et not in, de priorité égale à celle des opérateurs de comparaison. Ils permettent de vérifier si une valeur appartient à un intervalle, bornes comprises (exemple 6.6). Le type de l’intervalle (ou du sous-type) doit être un type discret [ARM 3.2] alors que le résultat est naturellement de type Boolean. Comme in et not in ne sont pas des opérateurs, ils ne peuvent donc pas être surchargés (sect. 4.9). Exemple 6.6 Utilisation de tests d’appartenance. Jour in T_Jours_Travailvrai si Jour est un jour de travail; Jour not in T_Jours_Travailvrai si Jour est Samedi ou Dimanche; Nombre in T_Chiffrevrai si Nombre est un chiffre; Lettre in 'A'..'Z'vrai si Lettre est une majuscule; Indice in T_Ligne'Range§ 8.2.7.
TYPES NUMÉRIQUES
6.2
134
TYPES NUMÉRIQUES
6.2.1
Motivation
les types numériques prédéfinis en Ada (Integer, Float, etc.) suffisent pour les applications courantes. Ils ont l’avantage de correspondre, en général, à des valeurs et des opérations efficacement implémentées par le matériel. Ils ont par contre l’inconvénient de dépendre de l’implémentation, c’est-à-dire de voir leur intervalle de valeurs et leur précision, pour les types réels, varier d’une machine à l’autre ou selon les compilateurs. Cette situation se retrouve dans la grande majorité des langages de programmation. L’adaptation à un nouveau matériel d’une application utilisant les types prédéfinis peut de ce fait poser de nombreuses difficultés. Une des particularités intéressantes d’Ada est de permettre au concepteur de définir lui-même ses types numériques afin d’une part de choisir un bon équilibre entre efficacité et portabilité (sect. 1.5) et d’autre part de renforcer les contrôles du compilateur dans le but d’éviter de mélanger des données numériques, comme des euros et des dollars dans des calculs bancaires par exemple. 6.2.2
Types entiers signés
Toute implémentation d’Ada comprend le type prédéfini Integer. Mais comme mentionné auparavant (§ 2.2.7), d’autres types tels que Short_Integer ou Long_Integer peuvent également être fournis. Cependant tous sont dépendants de la machine utilisée. Pour renforcer la portabilité d’une application et compléter les contrôles du compilateur (note 6.2), Ada permet donc la déclaration de types entiers signés (exemple 6.7) de la manière suivante: type identificateur is range borne_inf .. borne_sup;
où • borne_inf et borne_sup sont des expressions entières statiques (sect.
3.10) appartenant à l’intervalle des nombres entiers disponibles sur la machine (§ 2.5.2). Exemple 6.7 Déclaration de types entiers signés. type type type type
T_Octet_Signe is range –128 .. 127; T_Indice_Signe is range 0 .. 1000; T_Euros is range –1E12 .. 1E12; T_Dollar is range –1E12 .. 1E12;
Ces types sont dits signés car la suite de bits implémentant toute valeur d’un tel type verra l’un de ses bits (souvent celui de poids le plus fort) interprété comme bit de signe.
TYPES NUMÉRIQUES
135
Les opérations possibles applicables à de tels types sont les mêmes que pour le type prédéfini Integer (§ 2.2.2) avec la particularité que le second opérande de l’opérateur arithmétique ** doit toujours être du sous-type Natural. L’affectation, le passage en paramètre et l’utilisation d’attributs s’effectuent également comme pour Integer. Pour réaliser des entrées-sorties, il faut avoir déclaré package ES_Entiers_Signes is new Ada.Text_IO.Integer_IO ( Id_Type_Entier_Signe );
où • ES_Entiers_Signes est le nom du paquetage d’entrées-sorties créé,
nom choisi par le programmeur; • Id_Type_Entier_Signe est l’identificateur du type entier signé.
Une fois cette déclaration effectuée et de manière analogue aux types énumératifs (§ 5.2.5), il est alors possible d’utiliser Get et Put pour lire et écrire des valeurs du type Id_Type_Entier_Signe. Le préfixe ES_Entiers_Signes doit être mentionné, sauf si une clause use ES_Entiers_Signes suit cette ligne de déclaration. Le lecteur malin aura réalisé que le nom développé Ada.Integer_Text_IO (sect. 2.6) vient d’une déclaration prédéfinie qui s’écrit package Ada.Integer_Text_IO is new Ada.Text_IO.Integer_IO ( Integer ); NOTE 6.1 Avantages apportés par l’utilisation de types entiers signés. L’un des avantages d’utiliser ses propres types entiers signés réside dans le fait que ceux-ci seront identiques dans n’importe quelle implémentation. Si la déclaration d’un tel type ne pouvait être réalisée sur une machine donnée, le compilateur refuserait ladite déclaration. Dans ce cas il faudrait modifier le type avec la conséquence que la portabilité de l’application en souffrirait. Le second avantage de l’utilisation de tels types se situe dans les contrôles du compilateur qui verifiera ainsi l’absence de mélange de valeurs entières de types différents.
Attention à ne pas confondre une déclaration de type entier signé avec une déclaration de sous-type entier (§ 6.1.1). 6.2.3
Types entiers non signés (types modulo)
Il est parfois pratique de travailler avec des entiers non signés, c’est-à-dire dont l’intervalle de définition va de zéro à une borne supérieure positive. C’est le cas lorsque l’on veut, par exemple, effectuer des calculs «modulo N». Ada permet la déclaration de types entiers non signés ou types modulo de la manière suivante: type identificateur is mod le_modulo;
TYPES NUMÉRIQUES
136
où • le_modulo est une expression entière positive statique (sect. 3.10), souvent
une puissance de 2; par définition, l’intervalle des valeurs du type est toujours 0 .. le_modulo – 1 (exemple 6.8). Exemple 6.8 Déclaration de types entiers non signés. type T_Octet is mod 256;valeurs de 0 à 255; type T_Mot is mod 65536;valeurs de 0 à 65535; type T_Indice is mod 1000;valeurs de 0 à 999.
Ces types sont dits non signés car la suite de bits implémentant toute valeur d’un tel type n’aura aucun bit interprété comme bit de signe. Les opérations applicables à de tels types sont les mêmes que pour le type prédéfini Integer (§ 2.2.2) avec la particularité que le second opérande de l’opérateur arithmétique ** doit toujours être du sous-type Natural. La principale caractéristique de ces types est qu’il ne peut jamais se produire de débordement de capacité car toute l’arithmétique est effectuée modulo Le_Modulo. De plus, comme c’est le cas pour l’arithmétique d’un processeur, les opérateurs and, or, xor et not sont applicables à des opérandes d’un type modulo, considérés alors comme des suites de bits. L’affectation, le passage en paramètre et l’utilisation d’attributs s’effectuent également comme pour Integer. Pour réaliser des entrées-sorties, il faut avoir déclaré package ES_Entiers_Non_Signes is new Ada.Text_IO.Modular_IO ( Id_Type_Entier_Non_Signe );
où • ES_Entiers_Non_Signes est le nom du paquetage d’entrées-sorties
créé, nom choisi par le programmeur; • Id_Type_Entier_Non_Signe est l’identificateur du type entier non
signé. De manière analogue aux types énumératifs (§ 5.2.5), il est alors possible d’utiliser Get et Put pour lire et pour écrire des valeurs d’un type comme Id_Type_Entier_Non_Signe. Le préfixe ES_Entiers_Non_Signes doit être mentionné, sauf si cette ligne de déclaration est suivie d’une clause use ES_Entiers_Non_Signes. 6.2.4
Types réels point-flottant
Toute implémentation d’Ada comprend le type point-flottant prédéfini Float.
TYPES NUMÉRIQUES
137
Mais comme mentionné auparavant (§ 2.3.5) d’autres types tels que Short_Float ou Long_Float peuvent également être disponibles. Encore une fois tous sont dépendants de la machine utilisée. Pour renforcer la portabilité d’une application mais aussi pour assurer la précision souhaitée dans les applications numériques (calcul scientifique), Ada permet la déclaration de types réels point-flottant (exemple 6.9) de la manière suivante: type identificateur is digits nb_chiffres;
où • nb_chiffres (statique) représente la précision désirée.
Notons tout de suite qu’il est encore possible de contraindre un tel type en définissant un intervalle statique (§ 6.1.1). Exemple 6.9 Déclaration de types point-flottant. type T_Reel_9 is digits 9; type T_Reel_12 is digits 12; type T_Unite_6 is digits 6 range 0.0 .. 0.999999;
Les opérations possibles applicables à de tels types sont les mêmes que pour le type prédéfini Float (§ 2.3.2). L’affectation, le passage en paramètre et l’utilisation d’attributs s’effectuent également comme pour Float. Pour réaliser des entrées-sorties, il faut avoir déclaré package ES_Reels_Point_Flottant is new Ada.Text_IO.Float_IO ( Id_Type_Reel_Point_Flottant );
où • ES_Reels_Point_Flottant est le nom du paquetage d’entrées-sorties
créé, nom choisi par le programmeur; • Id_Type_Reel_Point_Flottant est l’identificateur du type réel point-
flottant. De manière analogue aux types énumératifs (§ 5.2.5), il est alors possible d’utiliser Get et Put pour lire et pour écrire des valeurs d’un type comme Id_Type_Reel_Point_Flottant. Le préfixe ES_Reels_Point_Flottant doit être mentionné, sauf si cette ligne de déclaration est suivie d’une clause use ES_Reels_Point_Flottant. La réalisation d’applications numériques demanderait un approfondissement de l’utilisation des types réels point-flottant, ce qui dépasse l’objectif de ce texte. 6.2.5
Types réels point-fixe
TYPES NUMÉRIQUES
138
Un type réel point-fixe est utile s’il est nécessaire de travailler avec des nombres réels pour lesquels la différence entre deux nombres consécutifs est constante (er-reur absolue) comme lors du calcul de durées temporelles ou dans les applications comptables. Un tel type se déclare de la manière suivante (exemple 6.10): type identificateur is delta erreur range borne_inf .. borne_sup;
où • erreur (statique) représente l’erreur tolérée; • borne_inf et borne_sup sont des expressions réelles statiques. Exemple 6.10 Déclaration de types point-fixe. type T_Reel_01 is delta 0.1 range –1.0 .. 1.0; type T_Secondes is delta 0.001 range 0.0 .. 86_400.0;
Les opérations possibles applicables à de tels types sont les mêmes que pour le type prédéfini Float (§ 2.3.2) avec quelques particularités non détaillées ici. De manière un peu surprenante, il n’existe qu’un seul type point-fixe prédéfini nommé Duration (semblable au type T_Secondes), utilisé lorsqu’il est nécessaire de tenir compte du temps (applications temps réel). Les entrées-sorties sont possibles grâce au paquetage Ada.Text_IO.Fixed_IO utilisé de manière semblable à celle présentée pour les types point-flottant. Mentionnons encore qu’il existe des types réels point-fixe décimaux non traités dans ce texte. 6.2.6
Conversions entre types numériques
Comme introduit précédemment entre Integer et Float (§ 2.5.1), il est toujours possible de convertir explicitement (exemple 6.11) une valeur d’un type numérique en une valeur identique ou approchée d’un autre type numérique appelé type cible (target type). La valeur numérique résultant de la conversion doit bien évidemment respecter les contraintes du type (ou sous-type) cible. Exemple 6.11 Conversions entre types entiers. Octet_Signe : T_Octet_Signe; -- Quatre variables entieres Octet : T_Octet := 64; Billet : T_Euros; Valeur : Integer := 100; ... Octet_Signe := T_Octet_Signe ( Octet ); -- Valeur 64 convertie Billet := T_Euros ( Valeur ); -- Valeur 100 convertie Octet := T_Octet ( 3 * Billet ); -- Erreur a l'execution
TYPES NUMÉRIQUES
6.2.7
139
Types entier et réel universels
Les constantes numériques (les nombres et les nombres nommés) ne possèdent pas de type explicite, mais sont par convention d’un type (entier ou réel) appelé universel. Une valeur de type entier universel peut être utilisée partout où une valeur entière est autorisée, sans nécessiter de conversion explicite. Cette règle s’applique de manière similaire aux valeurs de type réel universel. Certains attributs, comme Pos (§ 5.2.4), Digits (§ 2.3.4), ou encore Length (§ 8.2.7) retournent un résultat entier également universel. 6.2.8
Remarque sur la portabilité d’une application
Il faut cependant noter qu’obtenir un programme entièrement portable en ce qui concerne l’utilisation des nombres demande un soin non seulement dans la déclaration des types numériques mais encore dans l’utilisation des opérateurs. En effet des résultats intermédiaires (valeurs d’expressions partielles) hors de l’intervalle de définition peuvent provoquer ou non une erreur à l’exécution (exemple 6.12). Exemple 6.12 Cas de non-portabilité d’une expression arithmétique. -- ... procedure Exemple_6_12 is type T_Octet is range –128 .. 127; Nombre : T_Octet; Temp : T_Octet;
-- Un type entier signe
-- Deux variables de ce type
begin -- Exemple_6_12 Temp := 100;
-- Toujours correct
Nombre := (Temp+Temp) / 2;
-- Affecte la valeur 100 à Nombre ou -- provoque une erreur a
l'execution -- car Temp+Temp est hors de T_Octet ...
Pour assurer une portabilité intégrale, il faudrait que le résultat de toute expression complète ou partielle soit toujours dans l’intervalle de définition du type utilisé.
EXCEPTIONS
6.3
140
EXCEPTIONS
6.3.1
Motivation
La notion d’erreur à l’exécution a déjà été rencontrée à plusieurs reprises. Dans la majorité des cas il s’agissait du dépassement d’une borne inférieure ou supérieure d’un intervalle. Or, très souvent, une erreur d’exécution dans une application provoque sa fin brutale avec messages d’erreurs, bombes, fichiers corrompus, etc. Mais un programme écrit en Ada peut récupérer et traiter des erreurs d’exécution si le concepteur de l’application l’a prévu, ceci parce que le langage met à disposition un mécanisme de gestion des erreurs appelées exceptions (exceptions). Ce paragraphe va montrer comment traiter ces erreurs. La déclaration et la levée explicite (par une instruction particulière) d’exceptions seront présentées plus loin (chap. 13). 6.3.2
Généralités
En Ada, il existe quatre exceptions prédéfinies: • Constraint_Error, qui est provoquée par toute violation de contrainte,
par exemple en cas de dépassement d’une borne inférieure ou supérieure d’un intervalle, ou en divisant par 0. C’est de loin l’exception qui survient le plus fréquemment! • Program_Error, qui survient lors de la violation d’une structure de contrôle comme l’arrivée sur le end final d’une fonction; • Storage_Error, qui apparaît lors d’un dépassement de mémoire; • Tasking_Error, qui ne sera pas détaillée ici.
Le fait qu’une erreur d’exécution se produise est appelé levée (raising) de l’exception (exemple 6.13). Exemple 6.13 Situations où l’exception Constraint_Error est levée. Fortune : Positive;trois déclarations (§ 6.1.3 et 5.2.2); Code : Integer := 10; Jour : T_Jours_De_La_Semaine := Dimanche; ... Fortune := 0;le nombre 0 n’appartient pas au sous-type Positive; Get ( Fortune );exception si l’utilisateur donne un nombre négatif ou nul; Fortune := System.Max_Int;exception si Positive’Last inférieur à
System.Max_Int (§ 2.5.2); Fortune := Fortune – 1;exception Fortune := Fortune / 0;division par 0;
si Fortune de valeur 1;
EXCEPTIONS
141
Jour := T_Jours_De_La_Semaine'le successeur de Dimanche n’existe pas; Succ ( Jour ); Jour := T_Jours_De_La_Semaine'pas de jour de numéro d’ordre égal à 10. Val ( Code );
6.3.3
Levée (automatique) et propagation d’une exception
Pendant l’exécution d’un programme, une exception peut être levée par une erreur dans une instruction. Dans les cas simples présentés dans l’exemple 6.14 l’exception, toujours Constraint_Error, provoque la fin du programme. En effet, lors de la levée d’une exception, les instructions restantes du corps sont abandonnées et, dans le cas d’un programme principal, celui-ci est quitté avec, en général, un message d’erreur qui dépend de l’implémentation. Exemple 6.14 Cas simples de levée de l’exception Constraint_Error. -- Premier exemple procedure Exemple_6_14_1 is Nombre : Positive; begin -- Exemple_6_14_1 ... Nombre := 0; Positive
-- 0 n'appartient pas au sous-type
... end Exemple_6_14_1; ---------------------------------------------------------------- Deuxieme exemple procedure Exemple_6_14_2 is Nombre : Integer := 0; procedure P ( Valeur : in Natural ) is ... end P; begin -- Exemple_6_14_2 ... P ( Nombre – 3 ); Natural
-- –3 n'appartient pas au sous-type
... end Exemple_6_14_2;
Une exception peut être levée non seulement lors d’une instruction mais aussi à l’élaboration (§ 3.9.3) d’une déclaration. L’exemple 6.15, qui n’est pas écrit de manière très structurée (!), va permettre d’illustrer ces deux situations et, à nouveau
EXCEPTIONS
142
et dans les détails, le phénomène de propagation d’une exception. Exemple 6.15 Levées d’exceptions. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; -- ... procedure Principal is Nombre : Integer; -- Nombre dont on calcule les puissances ------------------------------------------------------------- ... function Exposant_Maximum return Natural is Exposant : Natural; -- Exposant jusqu'auquel seront -- calculees les puissances begin -- Exposant_Maximum Put ( "Veuillez donner l'exposant maximum: " ); Get ( Exposant ); -- 1 Skip_Line; return Exposant; end Exposant_Maximum; ------------------------------------------------------------- ... procedure Afficher_Puissances ( Nombre : in Integer ) is Limite : Positive := Exposant_Maximum;-- 2 begin -- Afficher_Puissances Put_Line ( "Et voici la liste des puissances:" ); -- Affichage de toutes les puissances souhaitees for N in Natural'First..Limite loop if N rem 8 = 0 then -- Maximum huit puissances par ligne New_Line; end if; Put ( Nombre ** N ); -- 3 end loop; New_Line; end Afficher_Puissances; begin -- Principal -- Presentation du programme Put ( "Calcul des puissances d'un nombre entier jusqu'a " ); Put_Line ( "un exposant maximum." ); -- Lire le nombre dont on veut calculer les puissances Put ( "Veuillez donner ce nombre: " ); Get ( Nombre ); -- 4 Skip_Line; -- Affichage des puissances du nombre donne par l'utilisateur Afficher_Puissances ( Nombre );
EXCEPTIONS
143
end Principal;
Les lignes suivies d’un commentaire formé d’un seul numéro sont celles où une exception est susceptible d’être levée. En effet, si l’utilisateur donne un nombre hors de l’intervalle des valeurs du type Integer, Data_Error (sect. 12.5) sera levée à la ligne 4 car Nombre est du type Integer et si l’utilisateur donne un exposant maximum négatif, elle sera levée à la ligne 1 car Exposant est du soustype Natural. Si l’utilisateur donne un exposant nul, Constraint_Error sera levée à la ligne 2 car Limite est du sous-type Positive et si l’exposant est assez grand, elle sera levée à la ligne 3 car la valeur de l’expression Nombre ** N sera hors de l’intervalle des valeurs du type Integer et ne pourra donc pas être passée en paramètre à la procédure Put. Que devient l’exception après sa levée? Elle interrompt dans tous les cas le cours normal de l’exécution et provoque soit un «saut» à la fin du corps (sect. 4.7) contenant la ligne (cas 1, 3 et 4) soit l’abandon de la partie déclarative (cas 2). La suite dépend de la ligne ayant provoqué sa levée. En l’absence de tout traitement (§ 6.3.4), comme c’est le cas dans notre exemple, si l’exception provient de la ligne 4, alors le programme Principal se termine et l’environnement Ada va afficher un message d’erreur contenant entre autres le nom de l’exception. Si l’exception provient de la ligne 3, la procédure Afficher_Puissances se termine et l’exception est à nouveau levée au point d’appel de ladite procédure (ce qui provoquera la fin du programme principal comme cela vient d’être mentionné). Ce phénomène est appelé propagation (propagation) de l’exception. Si l’exception provient de la ligne 2, la procédure Afficher_Puissances se termine sans avoir commencé l’exécution de sa première instruction et l’exception est propagée au point d’appel dans le programme principal. Finalement, si l’exception provient de la ligne 1, Exposant_Maximum se termine sans retourner de valeur et l’exception est propagée au point d’appel, c’est-à-dire à la ligne 2. Il faut encore insister sur le fait qu’une exception est levée à l’exécution et que sa propagation est donc dynamique: l’exception remonte selon les appels de procédure ou de fonction jusqu’au programme principal (en l’absence de traitement). 6.3.4
Traitement d’une exception
Il n’est jamais agréable de voir une application se terminer brutalement par un message d’erreur. Le traitement (handling) d’une exception en Ada consiste à intercepter l’exception pour l’empêcher de remonter jusqu’au système d’exploitation et à rétablir la situation en supprimant ou en évitant la cause de l’erreur. Mais il faut toujours garder à l’esprit qu’il vaut mieux voir apparaître une erreur qu’obtenir des résultats faux à l’exécution d’une application (note 6.1). Il ne faut donc réagir et supprimer l’exception que si l’application est capable d’y faire face,
EXCEPTIONS
144
de retrouver un état cohérent. Le traitement d’une exception consiste à compléter la fin d’un corps de procédure, de fonction, ou encore de bloc (§ 6.3.5), immédiatement avant le end final, par une zone particulière appelée traite-exception (exception handler). Elle a la forme suivante, ressemblant à celle de l’instruction case (§ 5.3.2): exception when choix_1 => traitement_1;-- premiere branche when choix_2 => traitement_2;-- deuxieme branche ... when others => autre_traitement;-- derniere branche
avec • un nouveau mot réservé exception qui débute le traite-exception, après
la dernière instruction du corps et avant le end final; • les choix_n formés d’un ou de plusieurs identificateurs d’exception séparés par des barres verticales; • les traitements_n composés d’une ou de plusieurs instructions; • le mot réservé others qui représente tous les autres cas d’exceptions; cette branche est optionnelle. Lorsqu’une exception est levée dans les instructions du corps contenant un tel traite-exception, l’exécution est transférée dans la branche comportant le nom de l’exception si celle-ci est mentionnée explicitement ou dans la dernière branche si celle-ci est présente. Puis l’exécution se poursuit normalement dans le traitement de la branche et l’exception est éliminée. Si le nom de l’exception ne fait pas partie des choix et s’il n’y a pas de branche commençant par when others, l’exception est propagée, comme si le traite-exception n’existait pas. Il faut d’ailleurs noter que si l’exécution d’une instruction du traite-exception lève une exception (!), celle-ci est également propagée. Il n’est pas possible de revenir là où l’exception s’est produite, sauf par un artifice utilisant la structure de bloc, car le traitement dans une branche du traiteexception remplace le reste du corps et termine ainsi l’exécution du corps. Un exemple d’utilisation d’un bloc pour répéter une instruction ayant provoqué une exception est donné dans le paragraphe 6.3.5. Exemple 6.16 Modification de la fonction et de la procédure de l’exemple 6.15. -- ... function Exposant_Maximum return Natural is Exposant : Natural;
-- Exposant jusqu'auquel seront -- calculees les puissances
begin -- Exposant_Maximum Put ( "Veuillez donner l'exposant maximum: " );
EXCEPTIONS
145
Get ( Exposant ); Skip_Line;
-- 1
return Exposant; exception when Constraint_Error => Skip_Line; -- Eliminer les caracteres restants Put_Line ( "Exposant negatif, 1 arbitrairement." ); return 1; when others => Skip_Line; -- Eliminer les caracteres restants Put_Line ( "Exposant errone, 20 arbitrairement." ); return 20; end Exposant_Maximum; ------------------------------------------------------------- ... procedure Afficher_Puissances ( Nombre : in Integer ) is Limite : Positive := Exposant_Maximum;-- 2 begin -- Afficher_Puissances Put_Line ( "Et voici la liste des puissances:" ); -- Affichage de toutes les puissances souhaitees for N in Natural'First..Limite loop if N rem 8 = 0 then -- Maximum huit puissances par ligne New_Line; end if; Put ( Nombre ** N );
-- 3
end loop; New_Line; exception when Constraint_Error => Put_Line ( "Puissance trop grande. Fin de l'affichage" ); end Afficher_Puissances;
Avec les modifications de l’exemple 6.16, la levée de l’exception Constraint_Error à la ligne 1 provoque le transfert de l’exécution dans le traite-
exception de la fonction où un message d’avertissement est affiché, puis la fonction se termine en retournant la valeur arbitraire 20. Avec ces mêmes modifications, la levée de l’exception Constraint_Error à la ligne 3 provoque le transfert de l’exécution dans le traite-exception de la procédure où un message d’avertissement est affiché, puis le programme se termine normalement. Mais malgré ces modifications, l’exception Constraint_Error levée à la ligne 2 n’est pas traitée par le traite-exception placé à la fin de la procédure Afficher_Puissances. En effet, une exception levée par une déclaration d’un
EXCEPTIONS
146
sous-programme est toujours propagée au point d’appel du sous-programme, que celui-ci ait ou non un traite-exception. Pour éviter ce problème il faudrait naturellement remplacer Positive par Natural dans la déclaration. Finalement, il est intéressant de voir comment traiter les cas des lignes 1 et 4 si l’on veut que le programme s’exécute sans erreur, avec des valeurs autorisées. La solution va nécessiter l’utilisation de la notion de bloc. 6.3.5
Notion de bloc
Un bloc (block) est une instruction particulière ayant la forme suivante: id_bloc: declare partie_declarative; begin suite_d_instructions; exception branches_de_traitement; end id_bloc;
avec • id_bloc le nom du bloc; cet identificateur est optionnel; • un nouveau mot réservé declare débutant la partie déclarative du bloc, le
tout aussi optionnel; • la suite_d_instructions composée d’une ou de plusieurs instructions entre les mots réservés begin et end; • le traite-exception, lui aussi optionnel.
L’exécution d’un bloc consiste en l’élaboration des déclarations puis l’exécution de la suite d’instructions. Lorsque le bloc est terminé, les objets locaux disparaissent et les instructions qui suivent sont exécutées. Si une exception survient dans la suite d’instructions, elle peut être traitée par le traite-exception. Si elle survient dans une déclaration, elle est propagée à l’unité englobante. Comme c’est une instruction, un bloc peut donc s’utiliser partout où une instruction est permise. Un bloc peut être utilisé pour traiter localement une exception (exemple 6.17), ou encore pour déclarer des objets dont certaines caractéristiques ne sont connues qu’après les instructions précédant le bloc. Exemple 6.17 Modification de la fonction de l’exemple 6.16. -- ... function Exposant_Maximum return Natural is Exposant : Natural; begin -- Exposant_Maximum
-- Exposant jusqu'auquel seront -- calculees les puissances
EXCEPTIONS
147
loop -- Boucler si Constraint_Error ou une autre -- exception levee Put ( "Exposant maximum: " ); begin Get ( Exposant ); Skip_Line;
-- Bloc sans partie declarative -- Instructions du bloc
return Exposant; exception -- Traite-exception du bloc when Constraint_Error => Skip_Line; -- Eliminer les caracteres restants Put_Line ( "Exposant negatif. Recommencer." ); when others => Skip_Line; -- Eliminer les caracteres restants Put_Line ( "Exposant errone. Recommencer." ); end; end loop; end Exposant_Maximum;
L’exécution de la fonction va s’arrêter sur l’instruction Get pour que l’utilisateur donne un exposant. S’il se trompe (nombre négatif, trop grand ou contenant un caractère interdit), une exception est levée par l’instruction Get et provoque le transfert dans le traite-exception où l’une des deux branches est exécutée. Puis la boucle recommence et l’utilisateur donne à nouveau un exposant. Lorsque l’exposant est correct, la fonction se termine par l’instruction return Exposant. 6.3.6
Compléments
Il ne faut pas oublier que les paramètres d’entrée-sortie ou de sortie doivent être correctement affectés à la sortie d’une procédure, même si cette sortie se fait par l’exécution d’une branche d’un traite-exception. De même il faut veiller à terminer toute branche d’un traite-exception d’une fonction par une instruction return. L’utilisation des traite-exceptions doit se limiter aux cas exceptionnels! L’exemple ci-dessous doit être évité et écrit différemment: begin -- Debut d'un bloc Jour := T_Jours_de_La_Semaine'Succ ( Jour ); -- § 5.2.2 exception when Constraint_Error => Jour := T_Jours_de_La_Semaine'First; end;
Il est en effet facile de détecter le dernier jour de la semaine et de modifier Jour en conséquence sans utiliser d’exception. Il faut absolument éviter d’utiliser des traite-exceptions comportant la branche car de telles branches font disparaître les exceptions, ce qui rend la recherche des causes d’erreurs difficile lorsque l’utilisateur du programme se rend compte que celui-ci ne se comporte pas comme prévu.
when others => null;
EXCEPTIONS
148
En plus des quatre exceptions prédéfinies (§ 6.3.2), certains paquetages faisant partie de la norme définissent et exportent d’autres exceptions. C’est le cas du paquetage Ada.IO_Exceptions [ARM A.13] qui déclare huit exceptions relatives aux opérations d’entrées-sorties comme Put et Get par exemple.
149
6.4
EXERCICES
6.4.1
Utilisation de types et sous-types entiers signés
Soient les déclarations: subtype T_Sous_Entier is Integer range 1..20; type T_Millier is range -1000..1000; I : Integer := 0; M : T_Millier := T_Millier'Last;
Donner la valeur et le type des expressions ci-dessous, ou expliquer pourquoi certaines sont fausses: M / I + M 100
6.4.2
I 10 / T_Sous_Entier(I) 10 + T_Millier(I + 10) * (I + 1) - Integer(M) * T_Sous_Entier'Last
Utilisation de types entiers non signés
Soient les déclarations: type T_Huit is mod 8; H : T_Huit := 1;
Donner la valeur des expressions ci-dessous: T_Huit'Succ((H + 1) + 5) (H + 3) ** 4 not H H xor 2
Pourquoi l’expression et l’affectation ci-dessous sont-elles fausses? H + 10 H := 10;
6.4.3
Utilisation d’un test d’appartenance
Récrire la fonction Majusculiser du paragraphe 4.3.4 en utilisant un test d’appartenance. 6.4.4
Traitement d’exceptions
Ecrire un traite-exception qui affiche des messages en fonction des exceptions Constraint_Error et Program_Error.
POINTS À RELEVER
6.5 6.5.1
150
POINTS À RELEVER En général • Des erreurs à l’exécution sont graves, mais des résultats faux, ou qui
paraissent être corrects, le sont tout autant. 6.5.2
En Ada • Un sous-type restreint les valeurs du type de base en imposant une
contrainte. • Un intervalle est une première forme de contrainte. • Le nom d’un sous-type discret peut représenter l’intervalle restreint des
valeurs, sauf dans une contrainte d’intervalle. • Un identificateur de sous-type s’utilise partout où un identificateur de type
est possible. • Des attributs sont applicables aux sous-types. • Les types numériques comprennent les types entiers et les types réels. • Integer, Float sont des exemples particuliers, prédéfinis, de types
numériques. • Les types entiers peuvent être signés ou non. • N’importe quelle valeur numérique peut être convertie en une valeur d’un
autre type numérique. • Les entrées-sorties de valeurs numériques nécessitent une ou plusieurs
déclarations spéciales. • L’avantage d’utiliser ses propres types numériques réside dans le fait que
ceux-ci seront identiques dans n’importe quelle implémentation et dans les contrôles renforcés du compilateur. • Les exceptions permettent la gestion des erreurs d’exécution d’un
programme. • Constraint_Error est l’exception rencontrée le plus fréquemment et
signale qu’une contrainte a été violée. • Une exception est levée par l’exécution d’une instruction ou par l’élabo-
ration d’une déclaration. • Une exception peut être traitée dans un traite-exception placé à la fin d’un
corps. • En l’absence de traitement, une exception est propagée à la structure
appelante.
POINTS À RELEVER
151
• Attention aux valeurs des paramètres d’entrée et de sortie, ou de sortie,
ainsi qu’au résultat de fonction en cas de traitement d’exception dans un sous-programme. • Les exceptions sont faites pour les cas exceptionnels (!). • Les exceptions s’utilisent pour détecter des erreurs; il ne faut donc pas les faire disparaître simplement par la branche when others => null. • En Ada, un bloc est une instruction particulière formée d’une partie
déclarative optionnelle et d’un corps.
152
C H A P I T R E
ARTICLES SANS
7
153
ARTICLES SANS DISCRIMINANTS
MOTIVATION
7.1
154
MOTIVATION
Les données traitées jusqu’à présent sont toutes d’un type élémentaire scalaire (scalar types, [ARM 3.2]), c’est-à-dire d’un type définissant un ensemble de valeurs individuelles. Or il existe des informations constituées de plusieurs éléments (composantes) formant un tout comme par exemple: • les dates chronologiques (année, mois, jour); • les nombres complexes (parties réelles et imaginaires, ou module et
argument); • les fiches bibliographiques (titre du livre, auteur, date de parution, ISBN...); • les fiches personnelles (nom, prénom, âge, sexe, taille...).
La nature de ces composantes, souvent de types différents, conduit à utiliser une structure permettant la définition explicite de chacun de ces éléments: les articles. Selon les besoins et comme dans la réalité, les informations contenues dans un article pourront être manipulées comme un tout ou alors gérées individuellement. Par exemple, une fiche personnelle peut être imprimée ou archivée mais il est possible de modifier uniquement l’âge ou la taille d’une personne.
GÉNÉRALITÉS
7.2
155
GÉNÉRALITÉS
7.2.1
Types articles contraints
En Ada, un article (record) est formé d’un ou de plusieurs éléments appelés champs où chacun d’eux peut être de n’importe quel type. Un article doit être déclaré dans une partie déclarative en utilisant un type article visible (sect. 4.4), défini préalablement dans une (autre) partie déclarative. Un ou plusieurs champs peuvent comporter une valeur initiale, comme c’est aussi le cas pour les variables (§ 3.9.2). Exemple 7.1 Types articles contraints et déclarations d’articles. type T_Jour is range 1..31; type T_Date is
-- Pour traiter des dates
record Jour : T_Jour; Mois : T_Mois_De_L_Annee; Annee : Natural; end record; type T_Complexe is complexes
-- § 5.2.2
-- Pour traiter des nombres
record Partie_Reelle : Float; -- Champ sans valeur initiale Partie_Imaginaire : Float := 0.0; -- Champ avec valeur end record; -- initiale Noel : constant T_Date := ...; Paques : constant T_Date := ...;
-- Pour le 25 decembre et pour -- Paques; voir § 7.2.2 pour
la -- valeur de ces constantes Nombre : T_Complexe; complexe
-- Pour representer un nombre
-- Pour afficher une date procedure Afficher ( Date : in T_Date ); -- Fonction qui calcule et retourne la date du jour suivant function Jour_Suivant ( Date : in T_Date ) return T_Date;
Les types articles font partie des types composés (composite types, [ARM 3.2]). Seuls les types articles sans discriminants (contraints) seront exposés dans ce chapitre. Les types articles avec discriminants (non contraints) feront l’objet ultérieurement d’une présentation particulière (chap. 11). La forme générale d’un type article sans discriminants est la suivante: type identificateur is
GÉNÉRALITÉS
156
record suite_de_champs_1 : identificateur_de_type_ou_sous_type := expression_1; suite_de_champs_2 : identificateur_de_type_ou_sous_type := expression_2; suite_de_champs_3 : identificateur_de_type_ou_sous_type := expression_3; ... suite_de_champs_N : identificateur_de_type_ou_sous_type := expression_N; end record;
où • record est un nouveau mot réservé; • identificateur est le nom du type; • suite_de_champs_i est une liste d’identificateurs séparés par des virgules et
désignant les champs du type article; • identificateur_de_type_ou_sous_type donne le type du champ et éventuellement une contrainte; • expression_i est optionnelle et représente la valeur initiale du champ. 7.2.2
Expressions, agrégats et opérations sur les articles
Les expressions d’un type article sont réduites aux constantes, variables et paramètres de ce type. Si les variables et les paramètres n’amènent aucun commentaire particulier, ce n’est pas le cas des constantes qui nécessitent une valeur lors de leur déclaration. A cet effet, le langage Ada permet la construction de valeurs d’un type article, appelées agrégats (aggregates) et composées d’une valeur par champ de l’article. Les valeurs des champs (fig. 7.1) sont séparées par des virgules et le tout est mis entre parenthèses; ces valeurs sont simplement énumérées (notation par position), ou écrites en utilisant la notation par nom (l’ordre d’écriture est alors libre), ou en mélangeant ces deux façons de faire en commençant obligatoirement par la notation par position (exemple 7.2). Ces différentes possibilités rappellent celles de l’association entre paramètres formels et effectifs lors de l’appel d’un sous-programme (§ 4.3.7).
GÉNÉRALITÉS
157
Figure 7.1 Diagramme syntaxique définissant un agrégat d’article. Agrégat article
,
(
expression
)
, | identificateur de champ
=>
expression
others
=>
expression
Exemple 7.2 Agrégats d’articles. -- Agregats du type T_Date (probablement) ( 24, Novembre, 1997 ) -- Agregat par position ( 24, Novembre, Annee => 1997 ) -- Deux agregats, par position ( 24, Mois => Novembre, Annee => 1997 ) -- puis par nom ( Jour => 24, Mois => Novembre, Annee => 1997 )-- Deux agregats ( Mois => Novembre, Jour => 24, Annee => 1997 )-- par nom -- Agregats du type T_Complexe (probablement) ( 0.0, –1.0 ) ( 0.0, Partie_Imaginaire => –1.0 ) ( Partie_Reelle => 0.0, Partie_Imaginaire => –1.0 ) ( Partie_Reelle | Partie_Imaginaire => 0.0 ) ( others => 0.0 ) ( 2.0 * X, Partie_Imaginaire => X / 3.0 ) -- On suppose que X -- est une variable -- de type Float
Le compilateur doit toujours déterminer le type d’un agrégat en considérant les types articles visibles là où l’agrégat est présent. Pour éviter des erreurs de compilation, en particulier lorsque others est utilisé, il est toujours possible de qualifier l’agrégat, c’est-à-dire de préciser son type en le préfixant par le nom du type suivi d’une apostrophe (exemple 7.3), comme pour les expressions qualifiées (sect. 3.10).
GÉNÉRALITÉS
158
Exemple 7.3 Agrégats qualifiés. -- Agregats du type T_Date T_Date'( 24, Novembre, 1997 ) T_Date'( 24, Novembre, Annee => 1997 ) T_Date'( Mois => Novembre, Jour => 24, Annee => 1997 ) -- Agregats du type T_Complexe T_Complexe'( 0.0, –1.0 ) T_Complexe'( others => 0.0 )
Une constante d’article se déclare donc comme une constante d’un type scalaire, en utilisant un agrégat comme valeur de la constante. Notons au passage que les agrégats servent également à donner la valeur initiale d’une variable article ou la valeur par défaut d’un paramètre article (exemple 7.4). Les opérations possibles sur les articles (en plus de l’affectation et du passage en paramètre) sont l’égalité = et l’inégalité /= . 7.2.3
Affectation
L’affectation se fait de manière habituelle, comme dans l’exemple 7.4. Exemple 7.4 Affectation et passage en paramètre de valeurs d’un type article. -- ... procedure Exemple_7_4 is type T_Complexe is -- Pour traiter des nombres complexes record Partie_Reelle : Float; Partie_Imaginaire : Float; end record; I : constant T_Complexe := (0.0, 1.0); -- Une constante et Nombre_1 : T_Complexe; -- deux variables de Nombre_2 : T_Complexe := (1.0, 0.0); -- ce type -----------------------------------------------------------procedure Exemple (Z : in T_Complexe := (0.0, 0.0) ) is ... end Exemple; begin -- Exemple_7_4 Nombre_1 := I; Nombre_1 := Nombre_2; Nombre_2 := ( 3.5, 4.8 ); -- Qualification souhaitable mais pas indispensable Nombre_2 := T_Complexe'(others => 0.0);
GÉNÉRALITÉS
159
-- Qualification possible mais pas indispensable Exemple ( T_Complexe'(3.5, 4.8) ); Exemple ( (3.5, 4.8) ); Exemple ( I ); -- Utilisation de la valeur par defaut Exemple; ...
Il faut insister sur le fait que les champs d’un type article peuvent être de n’importe quel type y compris un type article. Mais dans le cas d’un type composé pour un champ [ARM 3.2] il faut alors que ce type soit contraint (§ 7.2.1 et 8.2.2) ou un type article à discriminants avec valeurs par défaut (sect. 11.3). Un article sans discriminants est toujours contraint. Enfin, une fonction peut retourner un article comme résultat. Il existe des attributs applicables à un article ou un type article mais leur utilisation est réservée à des situations particulières. Par contre, il n’y a pas d’entrées-sorties prédéfinies sur les articles. Tout agrégat doit être complet, c’est-à-dire qu’il doit contenir exactement une expression pour chaque champ. Cette règle est parfaitement naturelle puisqu’un agrégat représente un article avec une valeur pour chacun de ses champs! De plus, un agrégat formé d’une seule expression doit être écrit en utilisant la notation par nom.
ACCÈS AUX CHAMPS D’UN ARTICLE
7.3
160
ACCÈS AUX CHAMPS D’UN ARTICLE
Il est très souvent nécessaire d’accéder à un champ particulier d’un article et non à l’article complet. Pour réaliser cette opération, il s’agit de préfixer le nom du champ d’un article par le nom de l’article et de séparer les deux identificateurs par un point. Cette notation est identique à celle rencontrée dans d’autres contextes tout à fait différents, comme lorsqu’il fallait utiliser des noms développés pour rendre un objet visible directement (sect. 4.4). Le résultat de cette construction est un champ du type spécifié à la déclaration du champ, utilisable de manière absolument identique à une constante ou variable de ce type. Donc partout où une constante ou variable d’un type donné peut être utilisée, un champ de ce type peut l’être également. Exemple 7.5 Accès aux champs d’un article. with Ada.Float_Text_IO; use Ada.Float_Text_IO; -- ... procedure Exemple_7_5 is type T_Complexe is -- Pour traiter des nombres complexes record Partie_Reelle : Float; Partie_Imaginaire : Float := 0.0; end record; I : constant T_Complexe := (0.0, 1.0); Nombre_1 : T_Complexe; Nombre_2 : T_Complexe;
-- Une constante et -- deux variables de -- ce type
begin -- Exemple_7_5 -- Apres l'affectation Nombre_1 vaudra ( 0.0, 0.0 ) Nombre_1.Partie_Reelle := I.Partie_Reelle; -- Apres l'affectation Nombre_2 vaudra ( 0.0, 1.0 ) Nombre_2 := ( Nombre_1.Partie_Reelle, I.Partie_Imaginaire ); Get ( Nombre_1.Partie_Imaginaire ); -- Apres l'affectation Nombre_2 vaudra ( 1.0, 1.0 ) Nombre_2.Partie_Reelle := Nombre_1.Partie_Reelle + 1.0; Put ( Nombre_2.Partie_Reelle ); ...
161
7.4
EXERCICES
7.4.1
Déclaration de types articles contraints
Déclarer des types articles permettant de représenter: • un vecteur à quatre composantes; • une couleur par trois proportions, chacune d’une des couleurs fondamen-
tales Rouge, Vert et Bleu; • un véhicule par son poids, sa longueur, sa largeur et sa hauteur. 7.4.2
Agrégats articles
Ecrire des agrégats pour les types de l’exercice 7.4.1. Le type de certains d’entre eux est-il toujours défini? Utiliser la qualification si nécessaire. 7.4.3
Calcul du nombre de jours entre deux dates
Reprendre l’exercice 5.4.3 et modifier la solution de manière à utiliser le type T_Date (§ 7.2.1) pour implémenter les dates. 7.4.4
Opérations sur les nombres complexes
Ecrire les opérations d’addition, de soustraction, de multiplication et de division de nombres complexes en utilisant le type T_Complexe (§ 7.2.1).
POINTS À RELEVER
7.5 7.5.1
162
POINTS À RELEVER En général • Un article regroupe des déclarations de types identiques ou différents.
7.5.2
En Ada • Un agrégat article est une valeur d’un type article, composée d’une valeur
par champ. • Un identificateur d’un type ou sous-type article peut servir à qualifier
l’agrégat. • Un agrégat d’une seule valeur doit être écrit avec la notation pas nom. • L’affectation globale d’articles est autorisée, de même que les opérations
d’égalité et d’inégalité. • En utilisant la notation de nom développé, un champ d’article s’utilise
comme une variable du même type que le champ.
163
C H A P I T R E
TABLE
8
164
TABLEAUX
MOTIVATION
8.1
165
MOTIVATION
Dans de nombreuses situations le programmeur ressent le besoin de manipuler des informations plus complexes que de simples valeurs. Les articles (chap. 7) permettent le regroupement de données de types différents. Mais ceux-ci sont à proscrire si ce regroupement comprend plusieurs dizaines de valeurs de même nature. En effet, dans de tels cas, les langages de programmation classiques offrent tous la notion de tableau, particulièrement adaptée à des situations telles que: • le calcul vectoriel ou matriciel; • l’enregistrement et le traitement de valeurs comme des mesures effectuées
sur des appareillages externes à l’ordinateur; • la définition d’une application (au sens mathématique du terme) d’un
ensemble de valeurs dans un autre; • la gestion de structures simples telles que piles, listes, queues, etc. (chap. 14
et 15); • la gestion de structures complexes comme les tables, les arbres ou encore
les graphes; • la gestion des tampons d’entrées-sorties dans les systèmes d’exploitation.
Comme déjà mentionné, ces données ont la particularité d’être composées de plusieurs valeurs de même nature. En effet, un vecteur est un n-tuple de valeurs réelles, les mesures sont souvent de simples nombres réels ou encore un tampon d’entrée-sortie peut contenir des dizaines de caractères. Il faut alors comprendre que la constitution d’une telle donnée est complètement définie lorsque: • le nombre de valeurs composant cette donnée est connu; • le type de ces valeurs est fixé.
Ces caractéristiques conduisent à la réalisation de structures appelées tableaux.
GÉNÉRALITÉS
166
8.2
GÉNÉRALITÉS
8.2.1
Définitions
Un tableau (array) est formé d’un ou de plusieurs éléments (components, appelés aussi composantes) tous de même type. Un ou plusieurs indices (indexes) sont associés à un tableau et permettent d’identifier, de distinguer les éléments. Les indices sont souvent appelés les dimensions (dimensions) du tableau. Finalement on nomme longueur (length) d’une dimension la taille (nombre de valeurs) de l’intervalle correspondant. En Ada, les indices d’un tableau prennent toujours leur valeur dans des intervalles (§ 6.1.1). Les types tableaux font partie des types composés (composite types, [ARM 3.2]). On en distingue deux catégories: les types tableaux contraints et non contraints. 8.2.2
Types tableaux contraints
L’exemple 8.1 présente la déclaration d’un type tableau contraint T_Ligne et d’un tableau Ligne de ce type. Les éléments de Ligne sont indicés de 0 à 79 et contiendront chacun un caractère. Les tableaux peuvent naturellement être passés en paramètres de sous-programme (procédure Afficher) ou constituer le résultat d’une fonction comme Suite. Exemple 8.1 Types tableaux contraints et déclarations de tableaux. -- Type tableau pour traiter des lignes de 80 caracteres Max_Longueur_Ligne : constant := 80, type T_Ligne is array (0..Max_Longueur_Ligne – 1) of Character; Ligne
: T_Ligne;
-- Une ligne (de 80 caracteres)
-- Procedure qui affiche une ligne donnee en parametre procedure Afficher ( Ligne : in T_Ligne ); -- Fonction qui donne une ligne composee du caractere passe en -- parametre et repete 80 fois function Suite ( Caractere : Character ) return T_Ligne;
La forme générale d’un type tableau contraint (constrained) est la suivante: type identificateur is array ( intervalle_1, intervalle_2, ..., intervalle_N ) of identificateur_de_type_ou_sous_type;
où • identificateur est le nom du type, array et of de nouveaux mots réservés; • intervalle_i est un intervalle (§ 6.1.1) définissant le type et fixant les bornes
du ième indice;
GÉNÉRALITÉS
167
• identificateur_de_type_ou_sous_type donne le type des éléments et
éventuellement une contrainte. Concernant les types des indices et des éléments, il faut savoir que le type des indices doit être discret alors que le type des éléments peut être n’importe lequel sauf un type (ou sous-type) tableau non contraint ou un article à discriminants sans valeurs par défaut (§ 11.2.1). 8.2.3
Types tableaux non contraints
L’exemple 8.2 présente la déclaration de deux types tableaux non contraints T_Vecteur et T_Matrice ainsi que trois tableaux Vecteur_2, Vecteur_3 et Matrice. Contrairement au paragraphe précédent, les indices ne sont mentionnés que lors de la déclaration des tableaux, les types tableaux n’indiquant que le type (ou sous-type) de ces indices. Les paramètres de sous-programme ou le résultat d’une fonction peuvent naturellement être d’un type tableau non contraint. Exemple 8.2 Types tableaux non contraints et déclarations de tableaux. -- Type tableau pour traiter des vecteurs de n'importe quelle -- taille puisque les indices ne sont pas fixes par le type! type T_Vecteur is array (Integer range <>) of Float; Vecteur_2 : T_Vecteur (1..2); Vecteur_3 : T_Vecteur (–1..1);
-- Un vecteur a deux composantes -- Un vecteur a trois composantes
---------------------------------------------------------------- Type tableau pour traiter des matrices de n'importe quelle -- taille! type T_Matrice is array (Integer range <>, Integer range <>) of Float; Matrice : T_Matrice (1..2, 1..3); -- Une matrice a deux dimensions ---------------------------------------------------------------- Fonction qui retourne la norme de n’importe quel vecteur de -- type T_Vecteur function Norme ( Vecteur : in T_Vecteur ) return Float; ---------------------------------------------------------------- Procedure qui inverse la matrice donnee en parametre procedure Inverser ( Matrice : in out T_Matrice ); ---------------------------------------------------------------- Fonction qui redonne l'inverse de n’importe quelle matrice de -- type T_Matrice function Inverse ( Matrice : in T_Matrice ) return T_Matrice;
La forme générale d’un type tableau non contraint (unconstrained) est la suivante:
GÉNÉRALITÉS
168
type identificateur is array ( indice_indefini_1, ..., indice_indefini_N, ) of identificateur_de_type_ou_sous_type;
où • identificateur est le nom du type, array et of deux mots réservés; • indice_indefini_i est un intervalle particulier de forme générale identificateur_indice_de_type_ou_sous_type range <>
qui définit uniquement le type et les bornes minimale et maximale de l’indice; • identificateur_de_type_ou_sous_type donne le type des éléments et éventuellement une contrainte. Comme dans le cas des types tableaux contraints, le type des indices doit être discret alors que le type des éléments peut être n’importe lequel sauf un type (ou sous-type) tableau non contraint ou un article à discriminants sans valeurs par défaut (§ 11.2.1). 8.2.4
Sous-types tableaux
Il est possible et pratique de déclarer un ou plusieurs sous-types d’un type (de base) tableau non contraint. La forme générale d’une telle déclaration est la suivante: subtype identificateur is id_type_tableau (intervalle_1, ..., intervalle_N);
où • identificateur est le nom du sous-type; • id_type_tableau est le nom d’un type tableau non contraint; • intervalle_i est un intervalle (§ 6.1.1) fixant les bornes, et du type du ième
indice. Exemple 8.3 Types, sous-types et déclarations de tableaux. -- Type tableau contraint pour traiter des lignes de 80 caracteres Max_Longueur_Ligne : constant := 80; type T_Ligne is array (0..Max_Longueur_Ligne – 1) of Character; ---------------------------------------------------------------- Autre style equivalent mais preferable a la declaration -- precedente subtype T_Long_Ligne is Integer range 0..Max_Longueur_Ligne – 1; type T_Ligne_Bis is array ( T_Long_Ligne ) of Character; --------------------------------------------------------------Ligne : T_Ligne; -- Une ligne (de 80 caracteres) Ligne_Bis: T_Ligne_Bis; -- Une autre ligne (de 80 caracteres) subtype T_Ligne_40 is T_Ligne (0..39);
-- INTERDIT car T_Ligne est
GÉNÉRALITÉS
169
-- deja contraint! ------------------------------------------------------------------------------------------------------------------------------ Type tableau non contraint pour traiter des vecteurs de -- n'importe quelle taille! type T_Vecteur is array (Integer range <>) of Float; -- Sous-types tableau pour traiter des vecteurs a deux composantes Nombre_Composantes : constant := 2; --------------------------------------------------------------subtype T_Vecteur_2 is T_Vecteur (1..Nombre_Composantes); -- Autre style, preferable, pour la declaration precedente subtype T_2_Composantes is Integer range 1..Nombre_Composantes; subtype T_Vecteur_2_Bis is T_Vecteur ( T_2_Composantes ); ---------------------------------------------------------------- Quatre vecteurs a deux composantes du meme type T_Vecteur Vecteur_2 : T_Vecteur_2; Vecteur_2_Bis : T_Vecteur_2_Bis; Vecteur_2_Ter : T_Vecteur (0..1); Vecteur_2_Autre : T_Vecteur (T_2_Composantes); -- Fonction qui retourne la norme de vecteurs a deux composantes -- de type T_Vecteur function Norme_2_Composantes ( Vecteur : in T_Vecteur_2 ) return Float; ---------------------------------------------------------------- Fonction qui retourne la norme de tout vecteur de type T_Vecteur function Norme ( Vecteur : in T_Vecteur ) return Float; ---------------------------------------------------------------- Type tableau non contraint pour traiter des matrices de toute -- taille! type T_Matrice is array (Integer range <>, Integer range <>) of Float; -- Sous-types tableau pour traiter des matrices 2 x 3 et 4 x 4 subtype T_Matrice_2_3 is T_Matrice (1..2, 1..3); subtype T_Matrice_4_4 is T_Matrice (0..3, 0..3); ---------------------------------------------------------------- Une matrice constante 2 x 3 Matrice_2_3 : constant T_Matrice_2_3 := ( (0.0,1.0,2.0), (3.0,4.0,5.0) ); Matrice_4_4 : T_Matrice_4_4; -- Une matrice 4 x 4 Matrice_3_2 : T_Matrice (1..3, –1..0); -- Une matrice 3 x 2 ---------------------------------------------------------------- Fonction qui redonne l'inverse d'une matrice 2 x 3 de type -- T_Matrice function Inverse_2_3 ( Matrice : in T_Matrice_2_3 ) return T_Matrice_2_3; ---------------------------------------------------------------- Fonction qui rend l'inverse de toute matrice de type T_Matrice
GÉNÉRALITÉS
170
function Inverse ( Matrice : in T_Matrice ) return T_Matrice;
Parmi les différentes possibilités de déclarations de types et sous-types tableaux, certaines d’entre elles offrent une flexibilité et une sécurité maximales lors de la conception du code Ada (note 8.1). NOTE 8.1 Style déclaratif des types, sous-types et objets tableaux. En général, il faut préférer les types tableaux non contraints aux types tableaux contraints de manière à pouvoir créer des paramètres (de procédure, fonction, etc.) tableaux généraux où seuls des types non contraints sont indiqués, les bornes des paramètres étant déduites des paramètres effectifs (§ 8.2.5). De même, il est pratique de déclarer des sous-types tableaux contraints pour pouvoir ensuite déclarer des constantes, variables ou encore agrégats (§ 8.2.5) entièrement définis.
L’application de la note 8.1 est illustrée dans l’exemple 8.3 avec les types non contraints T_Vecteur et T_Matrice, les sous-types contraints T_Vecteur_2, T_Vecteur_2_Bis, T_Matrice_2_3 et T_Matrice_4_4, la (matrice) constante Matrice_2_3, les variables Vecteur_2, Vecteur_2_Bis et Matrice_4_4 et enfin les fonctions Norme et Inverse. Finalement, il ressort de la syntaxe qu’un sous-type tableau est en général contraint, c’est-à-dire que sa déclaration comporte la mention explicite des bornes inférieure et supérieure d’un intervalle. Une seule exception à cela: un sous-type tableau peut surnommer (§ 6.1.1) un type non contraint auquel cas il est aussi non contraint comme dans l’exemple suivant: subtype T_N_Tuple is T_Vecteur;
8.2.5
Expressions, agrégats et opérations sur les tableaux
Les expressions d’un type tableau quelconque comprennent les constantes, variables, paramètres et agrégats. Dans le cas des tableaux unidimensionnels, il existe en plus la notion de tranche de tableau (§ 8.5.1). Comme pour les articles il faut insister sur la création d’agrégats qui, ici, sont formés d’une expression par valeur d’indice (exemple 8.4). Le diagramme de la figure 8.1 ne l’indique pas mais un agrégat tableau doit être noté entièrement par position, ou entièrement par nom pour un indice donné. Le mélange des notations est ici interdit. Seule exception: others => est autorisé (mentionné à la fin de l’agrégat) si le reste est noté par position.
GÉNÉRALITÉS
171
Figure 8.1 Diagramme syntaxique définissant un agrégat de tableau. Agrégat tableau
,
(
expression
)
, |
valeur d’indice
=>
expression
=>
expression
intervalle
others
Exemple 8.4 Agrégats tableaux. -- Agregats du type T_Ligne ou T_Ligne_Bis (probablement) (0..Max_Longueur_Ligne-1 => ' ')-- 80 espaces, notation par nom (T_Long_Ligne => ' ') -- 80 espaces, notation par nom -- Agregats du type T_Vecteur (probablement) (0.0, 0.0, 1.0, 1.0) -- Vecteur a 4 composantes (0.0, 0.0) -- Vecteur a 2 composantes (1..6 => 0.0, 7 | 8 => 1.0, 9 => 1.0) -- Vecteur a 9 composantes -- Agregats du type T_Matrice (probablement) ( (0.0, 0.0), (1.0, 1.0) ) -- Matrice ( 1 => (0.0, 0.0), 2 => (1.0, 1.0) ) -- Matrice ( 1..3 => (0.0, 0.0) ) -- Matrice ( 1..3 => (1..2 => 0.0) ) -- Matrice
carree 2 x 2 carree 2 x 2 3 x 2 3 x 2
Les agrégats de tableaux peuvent paraître compliqués. Pour que le compilateur puisse vérifier leur validité et réserver de la mémoire pour les implémenter, il faut en effet toujours que leur longueur et leurs bornes soient bien définies (en particulier lorsque others est utilisé), ce qu’il faut constamment garder à l’esprit pour bien comprendre les explications qui suivent. Un agrégat est toujours contigu car c’est un tableau même si la syntaxe ne le montre pas explicitement! Comme pour les articles, le compilateur doit toujours connaître le type d’un
GÉNÉRALITÉS
172
agrégat tableau. Une bonne manière de procéder consiste en la qualification de l’agrégat (exemple 8.5) en le préfixant par un identificateur de type ou de sous-type suivi d’une apostrophe (sect. 3.10). Mais en plus il faut que l’intervalle de valeurs, pour chaque indice, soit défini et corresponde à celui du type de l’agrégat. Exemple 8.5 Agrégats qualifiés. -- Agregats du type T_Ligne T_Ligne'(0..Max_Longueur_Ligne – 1 => ' ') T_Ligne'(T_Long_Ligne => ' ') -- Agregats du type T_Vecteur T_Vecteur'(0.0, 0.0, 1.0, 1.0) T_Vecteur_2'(0.0, 0.0) T_Vecteur_2'( others => 0.0)
-- Vecteur a 4 composantes -- Vecteur a 2 composantes -- Vecteur a 2 composantes
-- Agregats du type T_Matrice T_Matrice'( (0.0, 0.0), (1.0, 1.0) ) -- Matrice carree 2 x 2 T_Matrice'( 1 => (0.0, 0.0), -- Matrice carree 2 x 2 2 => (1.0, 1.0) ) T_Matrice_2_3'( 1..2 => (0.0, 0.0, 0.0) ) -- Matrice 2 x 3
Les règles définissant la validité ou non d’un agrégat, sa longueur et ses bornes se basent sur l’agrégat lui-même ainsi que sur son contexte, c’est-à-dire la situation où l’agrégat est utilisé. A des fins de simplicité, seuls des cas simples vont être présentés. Pour plus de détails, la théorie complète peut être consultée dans [BAR 97]. Il faut préciser que lors d’une affectation, du passage en paramètre ou de l’attribution de la valeur par défaut, les bornes d’un agrégat n’ont pas besoin d’être identiques à celles de l’objet affecté, il y a conversion automatique des bornes de l’agrégat. Exemple 8.6 Agrégats tableaux, longueurs et bornes. -- Type tableau pour traiter des vecteurs de n'importe quelle -- taille type T_Vecteur is array (Integer range <>) of Float; ----------------------------------------------------------------- pour traiter des vecteurs a deux composantes Nombre_Composantes : constant := 2; subtype T_Vecteur_2 is T_Vecteur (1..Nombre_Composantes); ----------------------------------------------------------------- fonction qui retourne la norme de vecteurs a deux composantes function Norme_2_Composantes (Vecteur : in T_Vecteur_2) return Float; ----------------------------------------------------------------
GÉNÉRALITÉS
173
-- fonction qui retourne la norme de n'importe quel vecteur de type -- T_Vecteur function Norme (Vecteur : in T_Vecteur) return Float; ----------------------------------------------------------------- Dans ce qui suit, la longueur et la borne inferieure se -- rapportent a l'agregat ----------------------------------------------------------------- Affectation: longueur 2, borne inferieure 1 Vecteur_1 : constant T_Vecteur_2 := (1.0, others => 0.0); Vecteur_2 : T_Vecteur_2 := (1.0, 2.0); ----------------------------------------------------------------- Passage en parametre: longueur 2, borne inferieure 1 L1 : Float := Norme_2_Composantes ( (1.0, 2.0) ); ----------------------------------------------------------------- Passage en parametre: longueur 3, -borne inferieure Integer'First L2 : Float := Norme ( (1.0, 2.0, 3.0) ); ----------------------------------------------------------------- Passage en parametre: longueur 3, -borne inferieure 5 L2 := Norme ( (5 => 1.0, 6 => 2.0, 7 => 3.0) ); -- Qualification: longueur 2, borne inferieure 1 Vecteur_3 : T_Vecteur := T_Vecteur_2'(others =>0.0); Vecteur_3 := T_Vecteur_2'(1..2 => 0.0); L3 : Float := Norme ( T_Vecteur_2'(others => 0.0) ); ----------------------------------------------------------------- ATTENTION ------------ Utilisation d'agregats interdite Vecteur_4 : T_Vecteur := (others => 0.0);
-- Longueur inconnue
L4 : Float := Norme ( (1.0, others => 0.0) );
-- Longueur inconnue
-- Exception Constraint_Error levee Vecteur_3 := (1..4 => 1.0);
-- Longueurs differentes
Vecteur_3 := T_Vecteur_2'(2..3 => 1.0);
-- Bornes differentes
L3 := Norme_2_Composantes ( (1..3 => 0.0) );
-- Longueurs differentes
Comme certains agrégats de l’exemple 8.6 l’ont peut-être suggéré, les valeurs d’indice ou les intervalles de la notation par nom doivent être statiques, sauf dans un seul cas: celui où l’agrégat est de la forme (intervalle => expression). Ici l’intervalle peut comporter des bornes dynamiques; un intervalle vide (§ 8.2.8), qui définit alors un agrégat ne contenant aucune valeur, est donc possible. Une constante tableau se déclare donc en utilisant un agrégat comme valeur. Les agrégats servent également à donner la valeur initiale d’une variable tableau
GÉNÉRALITÉS
174
ou la valeur par défaut d’un paramètre tableau (exemple 8.6). Les opérations possibles sur les tableaux (en plus de l’affectation et du passage en paramètre) sont l’égalité = et l’inégalité /=. Il faut cependant relever que, lors du passage en paramètre (de sousprogramme) d’un tableau, le paramètre formel hérite des bornes du paramètre effectif si le paramètre formel est d’un type (ou sous-type) tableau non contraint; dans le cas contraire, les bornes sont fixées par le type (ou sous-type) contraint luimême. Comme l’héritage a lieu à l’exécution, la connaissance des bornes du paramètre formel ne peut se faire que par l’utilisation d’attributs comme First et Last (§ 8.2.7). A noter que les tableaux unidimensionnels comportent des opérations supplémentaires (sect. 8.5). 8.2.6
Affectation
L’affectation se fait de manière habituelle mais la longueur de l’expression (ici un tableau), pour chaque indice, doit être identique à celle de la variable sinon l’exception Constraint_Error sera levée (note 8.2). Par contre, les bornes correspondantes n’ont pas besoin d’être identiques (§ 8.2.5). NOTE 8.2 Rôle des longueurs lors de l’affectation de tableaux. Lors de l’affectation de tableaux, leurs longueurs doivent être identiques faute de quoi l’exception Constraint_Error sera levée (§ 6.3.2).
La figure 8.2 illustre l’affectation d’un tableau avec un agrégat, en rappelant que dans ce cas, l’agrégat est en fait un tableau. Figure 8.2 Affectation d’un tableau avec un agrégat. 1.0 2.0
Vecteur_2 := (1.0, 2.0);
agrégat
tableau Vecteur_2
8.2.7
Attributs First, Last, Length et Range
Les attributs First, Last, Length et Range sont applicables aux indices d’un tableau, ou d’un type ou sous-type tableau contraint. First(N) et Last(N) donnent la première, respectivement la dernière valeur d’indice de la dimension N. Length(N) fournit la longueur (§ 8.2.1) de la dimension N. Finalement, Range(N)
GÉNÉRALITÉS
175
représente l’intervalle lui-même des indices de la dimension N. Il faut noter que N doit être statique mais peut être omis. Dans un tel cas d’omission, ces attributs s’appliquent à la première dimension. Les types, sous-types, variables et fonctions utilisés dans l’exemple 8.7 sont déclarés dans les exemples 8.1 et 8.2 Exemple 8.7 Utilisation des attributs First, Last, Length et Range. T_Ligne’First(1)
donne la première valeur d’indice de la première (seule)
dimension, 0; T_Ligne’Firstcomme ci-dessus, 0; T_Vecteur_2’Lengthdonne le nombre
de valeurs de la première (seule)
dimension, 2; T_Vecteur_2’Rangedonne
l’intervalle des indices de la première (seule)
dimension, 1..2; T_Matrice_2_3’Lastdonne
la dernière valeur d’indice de la première
dimension, 2; T_Matrice_2_3’Last(2)donne
la dernière valeur d’indice de la seconde
dimension, 3; Ligne’First
donne la première valeur d’indice de la première (seule)
dimension, 0; Vecteur_2’Lastdonne
la dernière valeur d’indice de la première (seule)
dimension, 2; Matrice_2_3’Length(1)donne
le nombre de valeurs de la première
dimension, 2; Matrice_2_3’Range(2)donne
l’intervalle des indices de la seconde
dimension, 1..3; Inverse’First(1)donne
la première valeur d’indice de la première
dimension, 1.
8.2.8
Compléments sur les tableaux
Il faut insister sur le fait que les éléments d’un tableau peuvent être de n’importe quel type y compris un type ou sous-type tableau. Mais dans le cas d’un type composé [ARM 3.2], il faut alors qu’il soit un type article sans discriminant (chap. 7), un type tableau contraint (§ 8.2.2) ou un type article à discriminants avec valeurs par défaut (sect. 11.3). Un tableau doit toujours être contraint lors de son élaboration, c’est-à-dire que les bornes de toutes ses dimensions doivent avoir une valeur définie à sa création. Il n’y a pas d’entrées-sorties prédéfinies sur les tableaux, à l’exception des tableaux du type prédéfini String (§ 9.2.1).
GÉNÉRALITÉS
176
Tout agrégat doit être complet, c’est-à-dire qu’il doit contenir exactement une expression par valeur d’indice puisque c’est en fait un tableau! De plus, un agrégat formé d’une seule expression doit être écrit en utilisant la notation par nom. Il est possible de déclarer un tableau sans passer par une déclaration de type. Par exemple: Tampon : array ( 1..10 ) of Character;
Une telle déclaration devrait se rencontrer uniquement dans des cas particuliers, comme lorsque le tableau est utilisé dans une petite partie de code seulement. Un intervalle d’indice d’un type tableau peut être vide, c’est-à-dire qu’il n’y aura aucune valeur pour cet intervalle dans n’importe quel tableau de ce type. Un intervalle est vide si sa borne inférieure est plus grande que sa borne supérieure.
ACCÈS AUX ÉLÉMENTS D’UN TABLEAU
8.3
177
ACCÈS AUX ÉLÉMENTS D’UN TABLEAU
8.3.1
Accès à un élément particulier d’un tableau
Il est très souvent nécessaire d’accéder à un élément particulier d’un tableau et non au tableau complet. S’il est vrai qu’un agrégat pourrait parfois résoudre ce problème, il existe une manière de faire bien plus pratique et plus répandue. Il s’agit de faire suivre le nom du tableau par une valeur pour chaque indice mise entre parenthèses: identificateur (expression_1, ... , expression_N)
où • identificateur est le nom d’un tableau; • expression_i est la valeur pour l’indice de la ième dimension.
Le résultat de cette construction est un élément du type spécifié à la déclaration du tableau, utilisable de manière absolument identique à une constante ou variable de ce type (exemple 8.8). Donc partout où une constante ou variable d’un type donné peut être utilisée, un élément de tableau de ce type peut l’être également. Exemple 8.8 Accès aux éléments d’un tableau. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_8_8 is -- Type tableau pour traiter des lignes de 80 caracteres Max_Longueur_Ligne : constant := 80; type T_Ligne is array (0..Max_Longueur_Ligne – 1) of Character; -- Une ligne de 80 caracteres Ligne : T_Ligne := (T_Ligne'Range => ' '); -- Type tableau pour traiter des matrices de n'importe quelle -- taille type T_Matrice is array (Integer range <>, Integer range <>) of Float; -- Sous-type pour traiter des matrices 2 x 3 subtype T_Matrice_2_3 is T_Matrice ( 1..2, 1..3 ); Matrice : T_Matrice_2_3;
-- Une matrice 2 x 3
Indice : Natural := 0; Code : Natural;
-- Deux variables auxiliaires
begin -- Exemple_8_8 Ligne ( 0 ) := 'A'; -- 'A' est la valeur affectee Ligne ( Max_Longueur_Ligne – 1 ) := Ligne ( T_Ligne'First ); Ligne ( 5 * (Indice + 2) ) := 'B'; -- Affiche 'A'
ACCÈS AUX ÉLÉMENTS D’UN TABLEAU
178
Put ( Ligne (Indice) ); -- 65 est la valeur affectee Code := Character'Pos ( Ligne ( T_Ligne'Last ) ); Matrice ( 2, 2 ) := 0.0; Matrice ( 2, 3 ) := Matrice ( 2, 2 ) + 5.0; Matrice ( Matrice'First(1), Matrice'First(2) ) := 1.0; -- Levera l'exception Constraint_Error Ligne ( Max_Longueur_Ligne ) := Ligne ( Max_Longueur_Ligne – 1 ); ...
8.3.2
Remarque importante
Une tentative d’accéder à un élément de tableau inexistant (une valeur d’indice hors de l’intervalle de définition) provoque l’exception Constraint_Error à l’exécution du programme (note 8.3). Par exemple, l’utilisation de l’élément Ligne(expression) (exemple 8.8) générera cette exception si expression possède une valeur hors de l’intervalle des indices du tableau Ligne. NOTE 8.3 Accès à un élément inexistant d’un tableau. L’accès à un élément inexistant d’un tableau lèvera l’exception Constraint_Error. Comme les accès à des tableaux sont fréquents dans les programmes, il faut suspecter une telle erreur en cas d’apparition de cette exception.
CONVERSION ENTRE TYPES TABLEAUX
8.4
179
CONVERSION ENTRE TYPES TABLEAUX
Ada permet de convertir un tableau dans un type cible, autre que celui utilisé lors de la déclaration du tableau (exemple 8.9). Cette conversion, qui va laisser tel quel le contenu du tableau, permet de passer outre la règle des types (§ 5.1.2) lorsque la situation l’exige. Une conversion n’est cependant pas possible entre n’importe quels types tableaux. Il faut que: • les types aient le même nombre de dimensions; • les types d’indices (discrets) soient identiques ou convertibles (§ 6.2.6); • les types des éléments soient identiques.
Si le type cible est contraint, alors la longueur de chaque dimension du tableau doit être identique à la longueur de l’intervalle correspondant du type cible et les bornes du résultat sont celles du type cible (exemple 8.9). Si le type cible est non contraint, alors chaque intervalle (non nul) d’indice du tableau doit être compris dans l’intervalle correspondant du type cible et les bornes du résultat sont celles du tableau. Si l’une de ces règles est violée, Constraint_Error sera levée. Exemple 8.9 Conversion entre types tableaux. type T_Vecteur is array (Integer range <>) of Float; subtype T_Vecteur_10 is T_Vecteur (1..10); type T_Autre_Vecteur is array (Natural range <>) of Float; Vecteur : T_Autre_Vecteur (0..100); Vecteur_Test : T_Vecteur (–100..100); ... T_Vecteur ( Vecteur ) bornes T_Vecteur ( Vecteur(20..30) ) bornes T_Vecteur_10 ( Vecteur(20..29) ) bornes T_Autre_Vecteur ( Vecteur_Test )
de Vecteur, 0 et 100; de la tranche, 20 et 30; de T_Vecteur_10, 1 et 10; provoque Constraint_Error à l’exécution (–100..0 hors de
Natural); T_Vecteur_10 ( Vecteur(20..30) )
provoque Constraint_Error à l’exécution (longueurs différentes).
TABLEAUX UNIDIMENSIONNELS
8.5
180
TABLEAUX UNIDIMENSIONNELS
Toutes les notions présentées jusqu’ici s’appliquent naturellement aux tableaux unidimensionnels, c’est-à-dire aux tableaux ne comportant qu’un seul intervalle d’indice. Mais des opérations supplémentaires existent pour de tels tableaux. 8.5.1
Tranches de tableaux
Une tranche (slice) de tableau est un tableau partiel défini par un intervalle compris dans l’intervalle de définition des indices du tableau d’origine. Une tranche permet donc de manipuler une partie d’un tableau. Les indices, respectivement les éléments de la tranche sont du type des indices, respectivement des éléments du tableau d’origine. Les indices du premier et du dernier élément de la tranche forment les bornes du tableau représenté par la tranche. La longueur de la tranche est son nombre d’éléments. Une tranche de tableau se compose du nom du tableau suivi de l’intervalle d’indices définissant la tranche, intervalle mis entre parenthèses. Les bornes de l’intervalle sont des expressions statiques ou dynamiques du type requis. Exemple 8.10 Construction et utilisation de tranches de tableaux. -- On suppose que les types T_Ligne, T_Vecteur et la fonction Norme -- sont declares comme dans les exemples 8.1 et 8.2 Ligne : T_Ligne;
-- Une ligne de 80 caracteres
Nombre : constant := 10; Norme_Vecteur : Float;
-- Une constante et une variable -- auxiliaires
Vecteur : T_Vecteur ( 1..Nombre ); -- Un vecteur a 10 elements ... Ligne (0..9) := (0..9 => '*'); Ligne (Ligne'First+1..Nombre) := Ligne (Ligne'First..Nombre–1); Vecteur (Vecteur'Range) := (
0.0, 2.0, 2.0, 4.0, 5.0, others => 0.0 );
Vecteur (Nombre–2..Nombre) := Vecteur (Vecteur'First..3); Norme_Vecteur := Norme (Vecteur(Vecteur'First..Vecteur'Last/2));
Comme mentionné précédemment (§ 8.2.6) pour le cas général, l’affectation entre tranches de tableaux d’un même type est possible si la longueur de l’expression est identique à celle de la tranche affectée (note 8.4). Dans ce cas aussi les bornes correspondantes n’ont pas besoin d’être identiques. Attention à la fréquente confusion entre Ligne(0) et Ligne(0..0). Ligne(0) est un élément (le premier) du tableau Ligne, donc un caractère, alors que Ligne(0..0) est une tranche de type T_Ligne, donc un tableau, composée d’un seul élément.
TABLEAUX UNIDIMENSIONNELS
181
NOTE 8.4 Affectation de tranches de tableaux. L’affectation de tranches de tableaux est autorisée pourvu que la variable et l’expression (tableau) soient de même type. Mais leurs longueurs doivent être identiques, faute de quoi l’exception Constraint_Error sera levée.
8.5.2
Concaténation
Ada fournit l’opérateur binaire de concaténation (mise bout à bout) de tableaux (ou tranches) unidimensionnels d’un même type. Cet opérateur est noté & et possède la priorité des opérateurs binaires d’addition et de soustraction (§ 3.4.2). Le résultat est un tableau du même type (exemple 8.11) dont la longueur est la somme de celle des deux opérandes. La borne inférieure est celle de l’opérande de gauche si le type tableau est non contraint, sinon la borne inférieure est celle de l’intervalle de définition des indices du type tableau (contraint). La borne supérieure se calcule en additionnant les longueurs des opérandes à la borne inférieure, puis en soustrayant 1. Exemple 8.11 Concaténation de tableaux et de tranches. -- On suppose que les types T_Ligne et T_Vecteur sont declares -- comme dans les exemples 8.1 et 8.2 Ligne : T_Ligne; -- Une ligne de 80 caracteres Vecteur : T_Vecteur ( 1..10 ); -- Un vecteur a dix elements -- Un vecteur a cinq elements Demi_Vecteur : T_Vecteur ( 1..5 ) := (1..5 => 0.0); ... Vecteur := Demi_Vecteur & Demi_Vecteur; Demi_Vecteur := Vecteur (1..2) & (4..6 => 1.0); Vecteur := Demi_Vecteur & Vecteur (1..1) & Demi_Vecteur (1..4); Ligne (10..15) := Ligne (3..5) & Ligne (23..25); Ligne (0..5) := Ligne (3..5) & Ligne (23..25);
L’un ou les deux opérandes de l’opérateur & peuvent aussi être du type des éléments d’un type tableau (exemple 8.12). Si l’opérande de gauche est une telle valeur, la borne inférieure du résultat de la concaténation sera toujours celle de l’intervalle de définition des indices du type tableau de l’expression ainsi formée. Exemple 8.12 Concaténation d’éléments de tableaux. -- T_Ligne et T_Vecteur comme dans les exemples 8.1 et 8.2 Ligne : T_Ligne;
-- Une ligne de 80 caracteres
TABLEAUX UNIDIMENSIONNELS
182
Vecteur : T_Vecteur ( 1..10 ); -- Un vecteur a dix elements -- Un vecteur a cinq elements Demi_Vecteur : T_Vecteur ( 1..5 ) := (1..5 => 0.0); ... Demi_Vecteur := Demi_Vecteur (1..4) & 1.0; Vecteur := Demi_Vecteur & 1.0 & (4..7 => 0.0); Ligne (10..15) := '*' & (1..5 => '+'); Ligne (0..2) := 'O' & 'U' & 'I';
8.5.3
Comparaison
Les opérateurs d’égalité et d’inégalité sont utilisables avec tous les tableaux. Les autres opérateurs de comparaison <, <=, > et >= peuvent s’appliquer à des opérandes tableaux unidimensionnels dont les éléments sont d’un type discret. Le résultat de la comparaison se base sur l’ordre lexicographique fondé sur la relation d’ordre du type discret: les éléments sont comparés un à un jusqu’à épuisement de l’un des tableaux ou jusqu’à ce que l’une des comparaisons permette de répondre à la question posée (exemple 8.13). Exemple 8.13 Comparaisons de tableaux. String'('a','b') < ('a','b','c','d')expression String'('a','b') > ('a','b','c','d')expression (1,2) < (1,3,5,7)expression vraie; (2,1) < (1,3,5,7)expression fausse; (2,1) > (1,3,5,7)expression vraie; (2,1) > (2,1)expression fausse; (2,1) >= (2,1)expression vraie.
vraie; fausse;
La qualification par le type prédéfini String (§ 9.2.1) est nécessaire dans cet exemple car il existe un autre type prédéfini Wide_String (non décrit dans cet ouvrage) et, sans qualification, le compilateur rejetterait les deux comparaisons pour cause d’ambiguïté. 8.5.4
Opérations booléennes
Les opérateurs logiques (booléens) s’appliquent aux tableaux unidimensionnels dont le type des éléments est le type Boolean. L’application à un (not) ou à deux (and or xor) opérandes consiste à appliquer l’opérateur élément par élément de manière à construire ainsi le tableau résultat de l’opération (exemple 8.14). Notons que les deux opérandes tableaux des trois opérateurs binaires doivent donc avoir une longueur identique. De plus, les bornes du tableau résultat sont celles de
TABLEAUX UNIDIMENSIONNELS
l’unique opérande ou de l’opérande de gauche. Exemple 8.14 Opérations sur les tableaux de booléens. not (False, True, False)résultat (True, False, True); (True, True) and (False, True)résultat (False, True); (True, True) or (False, True)résultat (True, True); (True, True) xor (False, True)résultat (True, False).
183
184
8.6
EXERCICES
8.6.1
Déclaration de types tableaux contraints
Déclarer des types tableaux contraints permettant de représenter: • deux cents nombres réels; • un nombre complexe; • le nombre d’occurrences des lettres (sans distinction de casse) dans un
texte. 8.6.2
Déclaration de types tableaux non contraints
Déclarer des types tableaux non contraints permettant de représenter: • un ensemble de nombres réels; • un nom de personne.
8.6.3
Agrégats tableaux
Ecrire des agrégats pour les types des exercices 8.6.1 et 8.6.2. Le type de certains d’entre eux est-il toujours défini? Utiliser la qualification si nécessaire. 8.6.4
Tableaux en paramètre
Ecrire un sous-programme qui détermine les valeurs minimale et maximale d’un tableau contenant un ensemble de nombres réels (exercice 8.6.2). Même question pour un tableau contenant le nombre d’occurrences de lettres (exercice 8.6.1) 8.6.5
Opérations sur les vecteurs
Ecrire des sous-programmes réalisant la lecture, l’écriture, la somme et la différence de vecteurs du type T_Vecteur (§ 8.2.3). 8.6.6
Opérations sur les matrices carrées
Ecrire des sous-programmes réalisant la lecture, l’écriture, la somme, la différence et le produit de matrices carrées du type T_Matrice (§ 8.2.3). 8.6.7
Carrés latins
Ecrire un sous-programme qui vérifie si un carré de n2 cases contenant chacune un nombre entre 1 et n est latin. Un tel carré est dit latin si sur chaque ligne et chaque colonne, chaque nombre entre 1 et n est présent une et une seule fois. 8.6.8
Triangle de Pascal
Ecrire un programme qui calcule la nième ligne du triangle de Pascal en utilisant le fait que le ième élément de cette ligne se calcule en effectuant la somme des
185
éléments i–1 et i de la n–1ième (un seul tableau suffit pour résoudre cet exercice). Les quatre premières lignes sont les suivantes: 1 11 121 1331 8.6.9
Calcul du nombre de jours entre deux dates
Reprendre l’exercice 5.4.3 et utiliser un tableau en substitution de l’instruction case pour obtenir le nombre de jours d’un mois.
POINTS À RELEVER
8.7 8.7.1
186
POINTS À RELEVER En général • Un tableau est formé d’un ou de plusieurs éléments tous de même type. • Des indices permettent de numéroter les éléments. • Par une notation adéquate, un élément de tableau s’utilise comme une
variable du même type que l’élément. • Eviter à tout prix les débordements (tentatives d’accès) hors des tableaux.
8.7.2
En Ada • Un type tableau peut être contraint ou non. • Un tableau est lui toujours contraint. • On utilise en général des types tableaux non contraints. • Les sous-types tableaux sont en général contraints. • Un agrégat tableau est écrit entièrement par nom, ou entièrement par
position. • Comme pour les articles, tout agrégat tableau doit être complet. • L’affectation globale de tableaux est autorisée, de même que les opérations
d’égalité et d’inégalité. • Lors de l’affectation, les longueurs doivent être identiques sinon l’exception Constraint_Error sera levée. • Des attributs permettent de connaître les caractéristiques des indices. • Un tableau peut être déclaré sans passer par une déclaration de type. • Sous certaines conditions, des tableaux d’un type donné peuvent être
convertis en des tableaux d’un autre type. • Les tableaux unidimensionnels représentent un cas particulier souvent
rencontré dans la pratique. • Il existe des opérations spéciales dédiées à certains tableaux unidi-
mensionnels: tranches de tableaux, concaténation, comparaisons, opérations booléennes. • Ne pas confondre un élément de tableau et une tranche de tableau de
longueur 1.
187
C H A P I T R E
CHAÎNES DE
9
188
CHAÎNES DE CARACTÈRES
MOTIVATION
9.1
189
MOTIVATION
Le monde est ainsi fait que (presque) toute entité le composant possède un (ou plusieurs) nom(s) permettant de l’identifier, noms parfois complétés par des numéros mais il est rare que seul un code numérique soit utilisé. Quel sens aurait en effet la phrase «11 passe à 9 puis à 4» ? Notre imagination nous suggère une action de football, l’évolution des taux hypothécaires... Cet état de fait implique que tout traitement informatique comporte des manipulations de tels noms appelés chaînes de caractères. Formellement une chaîne de caractères (character string) est une suite d’un ou de plusieurs caractères accolés, éventuellement aucun (chaîne vide).
GÉNÉRALITÉS
9.2
190
GÉNÉRALITÉS
9.2.1
Le type prédéfini String
En Ada, le type prédéfini String permet de définir des chaînes de caractères. En fait, et plus précisément, ce type est le type tableau non contraint suivant: type String is array ( Positive range <> ) of Character;
Toutes les opérations applicables aux tableaux unidimensionnels (sect. 8.5) sont donc valables pour le type String (exemple 9.1). Les constantes constituent cependant un cas particulier. Si elles peuvent s’écrire comme des agrégats, il existe une syntaxe plus pratique: l’utilisation sous forme de texte délimité par des guillemets (§ 1.10.3). Mais, contrairement aux agrégats formés de n’importe quels caractères, ce texte ne peut contenir que des caractères imprimables (§ 1.10.2). Exemple 9.1 Utilisation de chaînes de caractères. -- ... procedure Exemple_9_1 is Bonjour : constant String := "Bonjour"; Au_Revoir : constant String := ('a','u',' ','r','e','v','o','i','r'); Chaine_Vide : constant String := ""; Taille_Message : constant := 20; Message : String (1..Taille_Message); begin -- Exemple_9_1 Message ( Bonjour'Range ) := Bonjour; -- Tranche de tableau Message ( 9 ) := 'e'; -- Acces aux elements de Message ( 10 ) := 't'; -- la chaine Message Message ( 1 .. Bonjour'Length + Au_Revoir'Length ):= Message ( Bonjour'Range ) & Au_Revoir; -- Concatenation ...
9.2.2
Attributs Image et Value
Les attributs définis pour les tableaux (§ 8.2.7) s’appliquent naturellement aux objets de type String. Mais deux attributs supplémentaires, Image et Value, s’utilisent souvent en relation avec les chaînes de caractères. L’attribut Image convertit une valeur d’un type scalaire [ARM 3.2] en une chaîne appelée image et formée des caractères représentant, textuellement, la valeur (la borne inférieure de cette chaîne vaut 1). Plus précisément: • l’image d’une valeur entière est la suite de caractères correspondante précé-
GÉNÉRALITÉS
191
dée d’un signe moins (valeur négative) ou d’un espace (valeur positive); • l’image d’une valeur réelle point-flottant est la suite de caractères correspondant à la valeur écrite en notation scientifique, précédée d’un signe moins (valeur négative) ou d’un espace (valeur positive); la transformation est similaire pour une valeur réelle point-fixe; • l’image d’un identificateur d’un type énumératif est la suite de caractères correspondante, en majuscules; • l’image d’un caractère imprimable est le caractère lui-même y compris les deux apostrophes alors que l’image d’un caractère de contrôle (§ 1.10.2) est un identificateur en majuscules dépendant de l’implémentation. L’attribut inverse d’Image s’appelle Value. Cet attribut convertit une chaîne de caractères constituant textuellement une valeur d’un type scalaire en la valeur correspondante de ce type en ignorant d’éventuels espaces suivant ou précédant la valeur dans la chaîne. Si la transformation n’est pas possible parce que le texte ne représente pas une valeur valide, l’exception Constraint_Error est générée. Dans les deux cas (exemple 9.2), le nom de l’attribut est précédé d’un identificateur de type ou sous-type scalaire définissant le type du paramètre (valeur transformée). Exemple 9.2 Utilisation des attributs Image et Value. Integer'Image( 12 )donne la chaîne " 12"; Integer'Image( –12 )donne la chaîne "–12"; Float'Image( 12.34 )donne la chaîne " 1.23400E+01"; T_Mois_De_L_Annee'Image( Mars )donne la chaîne "MARS" (§ 5.2.2); Character'Image( 'a' )donne la chaîne "'a'"; Integer'Value( "12" )donne le nombre entier 12; Integer'Value( " –12 " )donne le nombre entier –12; Float'Value( "1.234E+01" )donne le nombre réel 1.234E+01; T_Mois_De_L_Annee'Value( "MaRs" )donne l’identificateur Mars (§
5.2.2); Character'Value( "'a'" )donne le caractère 'a'; Natural'Value( "–12" )lèvera Constraint_Error.
9.2.3
Entrées-sorties
L’interaction de l’utilisateur avec le programme nécessite très souvent la saisie et l’affichage de chaînes de caractères. Dans ce but, Ada fournit quatre procédures prédéfinies dans le paquetage d’entrées-sorties Ada.Text_IO (sect. 19.3). Ces quatre procédures sont:
GÉNÉRALITÉS
192
• Put, qui affiche à l’écran une chaîne de caractères (de type String) passée
en paramètre. • Put_Line, qui affiche à l’écran une chaîne de caractères (de type String)
passée en paramètre, puis passe à la ligne suivante. • Get, qui lit une suite de caractères tapés au clavier et les place dans une variable de type String passée en paramètre. L’utilisateur doit introduire
(au moins) un nombre de caractères correspondant à la longueur du paramètre (§ 8.2.1) car l’appel à Get se termine lorsque le paramètre est complètement rempli. • Get_Line, qui lit une suite de caractères tapés au clavier et les place dans une variable de type String passée comme premier paramètre. L’utili-
sateur peut introduire (au moins) un nombre de caractères égal à la longueur du paramètre (§ 8.2.1) auquel cas l’appel à Get se termine lorsque le paramètre est complètement rempli; ou alors l’utilisateur donne moins de caractères que la longueur du paramètre et termine par une fin de ligne, ce qui provoque la fin de l’appel à Get. Dans les deux cas, un deuxième paramètre de sous-type Natural redonne la position (indice) du dernier caractère tapé par l’utilisateur. A titre de résumé, voici les en-têtes des procédures telles que définies dans Ada.Text_IO: procedure procedure procedure procedure
Put ( Item Put_Line ( Get ( Item Get_Line (
: in String ); Item : in String ); : out String ); Item : out String; Last : out Natural );
Exemple 9.3 Entrées-sorties de chaînes de caractères. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_9_3 is Bienvenue : constant String := "Bienvenue dans l'exemple 9.3!"; Taille : constant := 10; Chaine : String ( 1..Taille ); -- 10 caracteres au maximum Nombre_Car_Lus : Natural; begin -- Exemple_9_3 Put_Line ( Bienvenue ); Put ( "Lecture d'une chaine avec Get (tapez" & Integer'Image(Taille) & " caracteres au moins): " );-- 1 Get ( Chaine ); -- 2
GÉNÉRALITÉS
Skip_Line;
193
-- 3
Put_Line ( "Voici la chaine que vous avez tapee: " & Chaine ); Put_Line ( "Lecture d'une chaine avec Get_Line" ); Put ( " (terminez par une fin de ligne avant" & Integer'Image(Taille) & " caracteres): "); Get_Line ( Chaine, Nombre_Car_Lus ); -- 4 Put ( "Voici la chaine que vous avez tapee: " ); Put_Line ( Chaine(1..Nombre_Car_Lus) ); -- 5 ...
L’exemple 9.3 nécessite quelques commentaires pour les lignes numérotées de 1 à 5. L’appel de Put (ligne 1) illustre une utilisation pratique de l’attribut Image (§ 9.2.2) en conjonction avec l’opérateur de concaténation & (§ 8.5.2). L’utilisation de la procédure Skip_Line (§ 2.6.3) de la ligne 3 est nécessaire pour éviter que l’utilisation d’une fin de ligne après l’introduction de 10 caractères à la ligne 2 provoque la lecture d’une chaîne vide à la ligne 4! Enfin la tranche de tableau (sous-chaîne) de la ligne 5 contient les caractères tapés par l’utilisateur à la ligne 4. NOTE 9.1Longueur des chaînes de caractères lues au clavier. L’utilisation de chaînes de caractères contenant des éléments non définis ou obsolètes est une erreur fréquente, source de problèmes apparaissant aléatoirement, et souvent difficile à détecter. L’utilisation de tranches de tableau après lecture au clavier et contenant uniquement les caractères tapés permet d’éviter de tels problèmes à l’exécution du programme.
MANIPULATION DE CHAÎNES DE CARACTÈRES
9.3
194
MANIPULATION DE CHAÎNES DE CARACTÈRES
Le travail avec les chaînes de caractères nécessite souvent l’utilisation de tranches de tableau et de concaténations mais aussi la recherche ou le décompte de motifs, l’extraction ou la suppression de sous-chaînes, le remplacement de souschaînes par d’autres, la substitution systématique d’un caractère par un autre, etc. Un motif (pattern) est la chaîne de caractères particulière qu’il faut rechercher, compter, supprimer, etc., dans un texte. Les langages de programmation traditionnels offrent en général des outils de manipulation de chaînes de caractères permettant la réalisation d’opérations telles que celles mentionnées précédemment. Ada ne fait pas exception à cette règle et définit plusieurs paquetages (sect. 10.2) de traitement de chaînes de caractères [ARM A.4]. Le plus simple d’entre eux appelé Ada.Strings.Fixed [ARM A.4.3] permet le travail avec des chaînes de type String. Parmi les opérations disponibles dans ce paquetage, mentionnons quelques-unes d’entre elles parmi les plus classiques: • Index, fonction qui recherche un motif dans une chaîne et qui retourne la
position du premier caractère du motif dans la chaîne; • Count, fonction qui compte le nombre d’occurrences d’un motif dans la
chaîne; • Insert, fonction ou procédure qui insère un motif dans une chaîne à partir
d’une certaine position; • Delete, fonction ou procédure qui supprime une partie d’une chaîne; • Overwrite, fonction ou procédure qui substitue une partie d’une chaîne
par un motif à partir d’une certaine position. Exemple 9.4 Utilisation simple de quelques opérations du paquetage Ada.Strings.Fixed. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Ada.Strings.Fixed; use Ada.Strings.Fixed; -- ... procedure Exemple_9_4 is Texte : String := "Texte extra dans ce contexte"; Ext : constant String := "ext"; -- Deux motifs Tex : constant String := "tex"; begin -- Exemple_9_4 Put ( Index (Texte, Ext) ); Put ( Index (Texte, Tex) ); Put ( Index (Texte, "Tex") );
-- Affiche 2 -- Affiche 24 -- Affiche 1
-- Affiche 3, le nom developpe est indispensable (§ 10.5.2) Put ( Ada.Strings.Fixed.Count (Texte, Ext) );
MANIPULATION DE CHAÎNES DE CARACTÈRES
195
Insert (Texte, 12, "bleu ciel"); -- Texte vaut maintenant -- "Texte extrableu ciel dans ce" Delete (Texte, 12, Texte'Last); -- Texte vaut maintenant -- "Texte extra " Overwrite (Texte, 12, "-ordinaire");-- Texte vaut maintenant -- "Texte extra-ordinaire " ...
L’exemple 9.4 illustre quelques cas d’utilisation d’opérations disponibles dans le paquetage Ada.Strings.Fixed. La manipulation de chaînes de caractères par les sous-programmes du paquetage Ada.Strings.Fixed est cependant contrôlée. Certaines opérations ne sont pas autorisées si les chaînes traitées ne les permettent pas. Dans de tels cas des exceptions particulières seront levées [ARM A.4.1].
196
9.4 9.4.1
EXERCICES Calcul du nombre de jours entre deux dates
Reprendre l’exercice 8.6.9 et lire les dates sous forme de chaînes de caractères (jour, nom du mois, année) qu’il faut ensuite transformer en type T_Date (§ 7.2.1). 9.4.2
Analyse d’une ligne de texte
Ecrire un programme qui lit une ligne de texte (donnée par l’utilisateur) lue sous forme de chaîne de type String et qui affiche tous les mots de la ligne. 9.4.3
Analyse d’un texte
Ecrire un programme qui lit un texte (donné par l’utilisateur ligne après ligne, chacune lue sous forme de chaîne de type String), qui compte les mots de chaque ligne et qui affiche ces valeurs une fois tout le texte lu. 9.4.4
Tri d’un groupe de mots
Ecrire un programme qui lit des mots donnés par l’utilisateur et qui les affiche dans l’ordre lexicographique. Un algorithme de tri (pour classer les mots) peut être imaginé ou alors repris de la section 18.3 et adapté au problème. 9.4.5
Justification d’une ligne de texte
Ecrire un programme qui lit une ligne de texte (donnée par l’utilisateur) et qui, au choix de l’utilisateur, la justifie à gauche, à droite, ou encore à gauche et à droite. Un seul espace doit séparer les mots pour la justification à gauche ou à droite; les espaces doivent être répartis équitablement entre les mots pour la justification à gauche et à droite. On fixe arbitrairement à 80 le nombre de positions pour le placement des mots. 9.4.6
Utilisation du paquetage Ada.Strings.Fixed
Reprendre les exercices 9.4.2 et suivants et les résoudre en utilisant les outils mis à disposition par le paquetage Ada.Strings.Fixed (sect. 9.3). 9.4.7
Exercice récapitulatif
Ecrire une application qui gère (création, affichage, suppression) des adresses téléphoniques (nom, rue et numéro, ville, numéro de téléphone, adresse email).
POINTS À RELEVER
9.5 9.5.1
197
POINTS À RELEVER En général • Des outils de manipulation de chaînes de caractères sont très souvent
présents dans les langages de programmation. 9.5.2
En Ada • Le type tableau unidimensionnel prédéfini String permet la manipulation
de chaînes de caractères considérées comme des tableaux de caractères. • Des attributs permettent de transformer une valeur en la chaîne de
caractères équivalente et inversement. • Des entrées-sorties de valeurs de type String sont possibles grâce au paquetage Ada.Text_IO. • Les annexes du langage Ada offrent des paquetages de manipulation
d’autres chaînes de caractères. • Attention aux éléments non définis ou obsolètes d’une chaîne de caractères.
198
C H A P I T R E
PAQUETA GES SIMPLES ET
1 0
199
PAQUETAGES SIMPLES ET UNITÉS DE COMPILATION
MOTIVATION
200
10.1 MOTIVATION Au fil du développement des applications logicielles, la structuration du code source a pris de plus en plus d’importance. Les premiers rudiments ont consisté en des instructions équivalentes à la sélection (if, case) ou l’itération (while, loop). Par ailleurs des suites d’instructions ont été réunies en groupes dans des structures comme les sous-programmes (procedure, function) ou les blocs (begin...end). Ces notions constituaient l’état de l’art lors de l’apparition du langage Pascal [JEN 78]. Sous la pression du développement de logiciels de plus en plus complexes, des structures plus globales devinrent nécessaires. Il faut citer les modules de Modula-2 [WIR 83], les unités du populaire Turbo-Pascal [BOR 92] et bien entendu les paquetages de Ada. Parallèlement, les classes de Smalltalk [GOL 83] et maintenant de C++ [STR 91] ou Java [JAV 98] permettent de réunir des attributs (données) et des méthodes (sous-programmes); ces classes constituent également une manière de structurer les applications mais la programmation objet ne sera pas abordée dans cet ouvrage. En Ada, les paquetages constituent la pièce maîtresse des (grands) programmes et permettent non seulement de grouper des éléments de programmes (constantes, types, variables, exceptions (sect. 13.2), sous-programmes...) mais encore de contrôler l’accès à ces éléments. Certains d’entre eux s’utiliseront dans et hors de la structure formée par le paquetage, d’autres ne seront visibles qu’à l’intérieur. Finalement, certains types seront opaques (leur structure sera connue dans le paquetage et invisible à l’extérieur) mais néanmoins utilisables hors du paquetage. Les paquetages simples, par opposition aux paquetages génériques (sect. 17.2) ou aux paquetages avec types privés (sect. 16.2), sont décrits dans ce chapitre.
GÉNÉRALITÉS
201
10.2 GÉNÉRALITÉS En général, un paquetage (package) est constitué de deux parties disjointes et fondamentalement différentes: la spécification et le corps. Dans sa forme la plus simple, la spécification (specification) regroupe des déclarations visibles et utilisables dans et hors du paquetage alors que le corps (body) englobe des éléments connus uniquement à l’intérieur. Les déclarations utilisables à l’extérieur du paquetage sont appelées exportées. Il est important de noter dès maintenant que la durée de vie des variables déclarées dans un paquetage est celle du paquetage lui-même. Pendant l’exécution du programme, ces variables conservent donc leur valeur courante tant que le paquetage existe. Elles sont parfois appelées variables rémanentes. La spécification d’un paquetage peut être déclarée dans n’importe quelle partie déclarative; son corps doit alors figurer dans la même partie déclarative, ou, si la partie déclarative fait partie d’une spécification, dans le corps correspondant. Enfin, un paquetage peut devenir une unité de bibliothèque (sect. 10.6). Les éléments choisis pour faire partie d’un paquetage doivent toujours tendre à constituer un tout cohérent, à donner une solution fiable à un problème particulier. A ce titre, et plus que les sous-programmes, les paquetages constituent des briques élaborées, fondements de toute application d’une certaine envergure. La spécification fournit la description des outils de résolution que le concepteur d’une application peut utiliser chaque fois qu’il est confronté au problème résolu par le paquetage, alors que le corps implémente ces outils. L’exemple probablement le plus cité jusqu’à présent est le paquetage Ada.Text_IO (sect. 19.3) qui résout le problème des entrées-sorties de texte. En particulier, les procédures Put et Get appartiennent à sa spécification et permettent de lire ou d’écrire des caractères ou des chaînes. Leur déclaration et leur mise à disposition suffit au concepteur pour savoir comment effectuer ces lectures ou écritures. Il n’a en effet pas besoin de connaître leur implémentation, cachée dans le corps de Ada.Text_IO. Figure 10.1Mise à disposition des déclarations de la spécification de Ada.Text_IO.
Spécification de Ada.Text_IO package Ada.Text_IO is ... end Ada.Text_IO;
Corps de Ada.Text_IO package body Ada.Text_IO is ... end Ada.Text_IO;
with Ada.Text_IO; procedure Main is -- Ada.Text_IO, ou -- plus exactement -- sa specification, -- est utilisable -- dans Main ... end Main;
GÉNÉRALITÉS
202
Pour que le programme principal Main puisse effectuer des entrées-sorties de texte (fig. 10.1), il doit comporter une clause de contexte with Ada.Text_IO qui indique au compilateur que des éléments déclarés dans la spécification de Ada.Text_IO vont être utilisés dans ses déclarations et ses instructions (sect. 10.5). Par contre, le corps de Ada.Text_IO a automatiquement accès (sans clause with) aux déclarations de sa spécification. Chaque fois qu’un programme nécessite des entrées-sorties de texte, il pourra utiliser Ada.Text_IO qui n’existe qu’en un seul exemplaire. Les paquetages permettent donc la réutilisabilité (reusability) du code. Parfois, un paquetage ne comprend qu’une spécification. Il s’agit alors de fournir des déclarations de constantes, de types, de variables, etc., à l’exclusion de structures nécessitant un corps comme les sous-programmes ou encore les paquetages. A ce titre, les paquetages Ada.Characters.Latin_1 [ARM A.3.3] et Ada.IO_Exceptions (sect. 12.5) en constituent d’excellents exemples.
SPÉCIFICATION DE PAQUETAGE
203
10.3 SPÉCIFICATION DE PAQUETAGE La structure d’une spécification de paquetage n’introduit pas de réelle nouveauté dans la syntaxe utilisée. Elle est donnée par le diagramme de la figure 10.2. Figure 10.2 Syntaxe d’une spécification de paquetage simple. Spécification de paquetage simple package
nom
is
partie déclarative
end
; nom
Nom identificateur du paquetage nom du paquetage parent
.
NOTE 10.1Portée des identificateurs déclarés dans une spécification de paquetage. La spécification d’un paquetage peut contenir n’importe quelle déclaration sauf des corps. La portée (sect. 4.4) de ces déclarations va non seulement jusqu’à la fin de cette spécification mais encore englobe tout le corps et s’étend à toute unité précédée d’une clause de contexte (§ 10.5.1) mentionnant le nom du paquetage.
Lorsque les types privés seront présentés (sect. 16.2), une zone spéciale complètera et terminera la partie déclarative. Le nom du paquetage peut être un simple identificateur comme Spider (sect. 1.9) ou un nom développé (sect. 4.4) comme Ada.Strings.Fixed [ARM A.4.3] ou Ada.Text_IO. La raison nécessitant la présence de préfixes est en relation avec la notion de paquetage enfant (sect. 10.8). Exemple 10.1Spécification d’un paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels package Nombres_Rationnels is type T_Rationnel is -- Le type d'un nombre rationnel record Numerateur : Integer; Denominateur : Positive;-- Le signe est au numerateur end record;
SPÉCIFICATION DE PAQUETAGE
204
Zero : constant T_Rationnel := (0,1); -- Le nombre rationnel 0 -- Addition de deux nombres rationnels function "+" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Soustraction de deux nombres rationnels function "–" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Multiplication de deux nombres rationnels function "*" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Division de deux nombres rationnels function "/" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Puissance d'un nombre rationnel function Puissance ( X : T_Rationnel; Exposant : Natural) return T_Rationnel; -----------------------------------------------------------end Nombres_Rationnels;
L’exemple
10.1
présente
la
spécification
du
paquetage
Nombres_Rationnels. Elle met à disposition: • le type T_Rationnel pour la déclaration de nombres rationnels sous
forme d’articles composés de deux champs, le numérateur et le dénominateur du nombre; • une constante Zero représentant le nombre rationnel 0; • quatre fonctions-opérateurs permettant des calculs rudimentaires sur les
nombres rationnels; • une fonction Puissance retournant la puissance d’un nombre rationnel; la raison du choix de l’identificateur Puissance plutôt qu’une surcharge de
l’opérateur "**" sera donnée plus loin (§ 10.6.2). Il faut remarquer la présence de la spécification des fonctions et l’absence de leur corps car, comme déjà mentionné, une partie déclarative de paquetage ne peut pas contenir de corps. Il est donc ici indispensable de déclarer les fonctions (et les procédures) exportées en deux parties (sect. 4.7).
CORPS DE PAQUETAGE
205
10.4 CORPS DE PAQUETAGE 10.4.1
Structure d’un corps de paquetage
La structure d’un corps de paquetage ressemble syntaxiquement à celle d’une spécification. Elle est donnée par le diagramme de la figure 10.3. Un corps de paquetage, caractérisé par le nouveau mot réservé body, a pour but principal de fournir tous les corps des procédures, fonctions, etc., mentionnées dans sa spécification. Pour ce faire et si nécessaire, il est possible de déclarer n’importe quel élément dans sa partie déclarative, élément dont la portée (sect. 4.4) va de sa déclaration jusqu’au end final du corps. Finalement, un corps de paquetage peut utiliser toutes les déclarations de sa spécification. Figure 10.3 Syntaxe d’un corps de paquetage. Corps de paquetage package
body
nom
is
partie déclarative
end
begin
suite d’instructions
;
nom
Exemple 10.2 Corps du paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels package body Nombres_Rationnels is ------------------------------------------------------------- Addition de deux nombres rationnels function "+" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "+" return ( X.Numerateur * Y.Denominateur + Y.Numerateur*X.Denominateur, X.Denominateur * Y.Denominateur ); end "+"; ------------------------------------------------------------- Soustraction de deux nombres rationnels
CORPS DE PAQUETAGE
206
function "–" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "–" return ( X.Numerateur*Y.Denominateur – Y.Numerateur*X.Denominateur, X.Denominateur * Y.Denominateur ); end "–"; -- Multiplication de deux nombres rationnels function "*" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "*" return ( X.Numerateur * Y.Numerateur, X.Denominateur * Y.Denominateur ); end "*"; ------------------------------------------------------------- Division de deux nombres rationnels function "/" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "/" if Y.Numerateur > 0 then return (
-- Diviseur positif X.Numerateur * Y.Denominateur, X.Denominateur * Y.Numerateur );
elsif Y.Numerateur < 0 then return (
-- Changer de signe
– X.Numerateur * Y.Denominateur, X.Denominateur * abs Y.Numerateur);
else -- Division par zero! ...
-- Lever une exception (sect. 13.3)
end if; end "/"; ------------------------------------------------------------- Puissance d'un nombre rationnel function Puissance ( X : T_Rationnel; Exposant : Natural) return T_Rationnel is begin -- Puissance return ( X.Numerateur ** Exposant, X.Denominateur ** Exposant ); end Puissance; -----------------------------------------------------------end Nombres_Rationnels;
Le corps du paquetage Nombres_Rationnels (exemple 10.2) réalise les corps des quatre opérateurs arithmétiques de base sur des fractions et celui de la fonction Puissance, conformément aux déclarations de sa spécification. Aucune autre déclaration n’est nécessaire ou utile dans ce corps.
CORPS DE PAQUETAGE
207
Il faut cependant souligner que le paquetage Nombres_Rationnels (exemples 10.1 et 10.2), qui est un paquetage Ada parfaitement correct, est pour l’instant uniquement illustratif. En effet, le calcul avec des expressions formées de nombres rationnels et d’opérations mises à disposition par ce paquetage ne serait pas toujours exact. Par exemple, l’opérateur prédéfini "=" entre deux nombres rationnels égaux ne fournit pas toujours le résultat correct (cas des nombres rationnels non irréductibles). 10.4.2
Code d’initialisation d’un paquetage
Le corps d’un paquetage peut se terminer par une suite d’instructions située avant le end final et précédée du mot réservé begin placé après la dernière déclaration du corps. Il s’agit du code d’initialisation qui est exécuté avant tout appel aux éléments exportés du paquetage. Ce code est optionnel et permet, entre autres, d’initialiser des variables du paquetage si elles ne peuvent l’être à la déclaration, par exemple parce que leur valeur initiale n’est connue qu’après l’exécution d’un sous-programme.
UTILISATION D’UN PAQUETAGE
208
10.5 UTILISATION D’UN PAQUETAGE 10.5.1
Clauses de contexte
N’importe quel programme principal, et plus généralement toute unité de bibliothèque (§ 10.6.1) nécessitant l’utilisation d’un paquetage (ou d’une autre unité de bibliothèque) doit comporter une clause de contexte commençant par le mot réservé with suivi du nom du paquetage. Cette clause sert à indiquer au compilateur que le programme principal ou l’unité de bibliothèque va utiliser des déclarations exportées (sect. 10.2) du paquetage. Lorsque plusieurs paquetages doivent être indiqués, il faut les mentionner dans une ou plusieurs clauses de contexte, en séparant les noms par une virgule s’il y a lieu (fig. 10.4). Figure 10.4 Syntaxe d’une clause de contexte. Clause de contexte
with
nom
;
,
Dans le cas d’un paquetage enfant (sect. 10.8), le nom doit comporter tous les préfixes nécessaires, comme dans with Ada.Text_IO, Ada.Strings.Fixed; par exemple. Lorsqu’une clause de contexte mentionne un paquetage, tous les identificateurs déclarés dans sa spécification deviennent dès lors utilisables dans le programme principal ou l’unité de bibliothèque sous leur forme développée (sect. 4.4), c’est-àdire préfixés par le nom du paquetage (exemple 10.3). Exemple 10.3 Utilisation du paquetage Nombres_Rationnels. with Nombres_Rationnels; -- ... procedure Exemple_10_3 is -- Une constante rationnelle Une_Demi : constant Nombres_Rationnels.T_Rationnel := (1, 2); Nombre_1 : Nombres_Rationnels.T_Rationnel;-- Deux variables Nombre_2 : Nombres_Rationnels.T_Rationnel;
UTILISATION D’UN PAQUETAGE
209
begin -- Exemple_10_3 -- Affectations Nombre_1 := Une_Demi; Nombre_2 := Nombres_Rationnels.Zero; -- Addition de deux nombres rationnels Nombre_2 := Nombres_Rationnels."+" ( Nombre_1, (3, 12) ); -- Division de deux nombres rationnels Nombre_1 := Nombres_Rationnels."/" ( Nombre_1, Nombre_2 ); -- Puissance d’un nombre rationnel Nombre_2 := Nombres_Rationnels.Puissance ( Nombre_2, 2 ); -- L'acces aux champs (comme d'habitude) reste bien entendu -- possible Nombre_2.Numerateur := 5; ...
Une clause de contexte placée avant une spécification de paquetage s’applique aussi bien à cette spécification qu’au corps de ce paquetage. Par contre, une telle clause placée avant le corps n’est valable que pour ce corps. 10.5.2
Clauses use
Lors de l’utilisation des déclarations exportées, il peut être fastidieux de répéter systématiquement le nom du paquetage les définissant; de plus, l’appel de fonctions-opérateurs exportées devrait s’effectuer sous la forme fonctionnelle (sect. 4.9). Comme déjà mentionné (§ 2.6.9), il existe une clause permettant d’éviter de préfixer par le nom du paquetage et d’utiliser les fonctions-opérateurs comme des opérateurs habituels. Cette clause a la même syntaxe qu’une clause de contexte, en substituant à with le mot réservé use. Elle doit se trouver après la clause de contexte mentionnant le même paquetage, avant le programme principal ou l’unité de bibliothèque, ou dans une partie déclarative. Dès sa mention, les préfixes ne sont plus nécessaires (mais toujours autorisés), les identificateurs exportés sont alors rendus directement visibles (sect. 4.4). L’exemple 10.4 reprend l’exemple 10.3 sans la mention des préfixes, rendus inutiles par la présence d’une clause use. Exemple 10.4 Utilisation du paquetage Nombres_Rationnels et d’une clause use. with Nombres_Rationnels; use Nombres_Rationnels; -- ... procedure Exemple_10_4 is -- La clause use permet l'utilisation directe, sans prefixe, -- des declarations de la specification du paquetage
UTILISATION D’UN PAQUETAGE
210
-- Nombres_Rationnels Une_Demi : constant T_Rationnel := (1, 2); Nombre_1 : T_Rationnel; -- Le prefixe est cependant toujours possible Nombre_2 : Nombres_Rationnels.T_Rationnel; begin -- Exemple_10_4 -- Affectations Nombre_1 := Une_Demi; Nombre_2 := Zero; -- Addition de deux nombres rationnels Nombre_2 := Nombre_1 + (3, 12); -- Division de deux nombres rationnels Nombre_1 := Nombre_1 / Nombre_2; -- Puissance d’un nombre rationnel Nombre_2 := Puissance ( Nombre_2, 2 ); -- L'acces aux champs (comme d'habitude) reste possible Nombre_2.Numerateur := 5; ...
Une question surgit lorsque plusieurs paquetages sont utilisés et que le préfixage est rendu inutile via une ou plusieurs clauses use. Que se passe-t-il si un identificateur exporté par deux paquetages (au moins) est écrit sans préfixe? Plus généralement, que devient la visibilité des identificateurs en présence de clauses use? Deux règles précisent la situation [BAR 97]: • un identificateur déclaré dans une spécification de paquetage est rendu directement visible (sect. 4.4) par une clause use pourvu que, d’une part,
le même identificateur ne figure pas dans un autre paquetage mentionné avec une clause use, et que, d’autre part, cet identificateur ne soit pas déjà directement visible; sinon le préfixage est indispensable (exemple 10.5); • si tous les identificateurs en présence sont des sous-programmes ou des
valeurs énumérées (§ 5.2.2), alors ils se surchargent (exemple 10.5) et deviennent tous directement visibles; il est donc possible que des cas d’ambiguïté surviennent, qu’il faudra éliminer par exemple en préfixant chaque identificateur ambigu par un nom de paquetage. Exemple 10.5 Visibilité et clauses use. with Nombres_Rationnels; use Nombres_Rationnels; -- ... procedure Exemple_10_5 is -- La declaration de la constante Zero ci-dessous cache (sect.
UTILISATION D’UN PAQUETAGE
211
-- 4.4) celle de la specification de Nombres_Rationnels Zero : constant Natural := 0; -- Il faut donc utiliser le nom developpe pour acceder a la -- constante Zero du paquetage Nombre_1 : T_Rationnel := Nombres_Rationnels.Zero; ------------------------------------------------------------- Surcharge de la fonction-operateur de multiplication du -- paquetage Nombres_Rationnels function "*" (
X : Integer; Y : T_Rationnel) return T_Rationnel is ... end "*"; ------------------------------------------------------------
begin -- Exemple_10_5 -- Zero est la constante declaree ci-dessus, la premiere -- multiplication l'est aussi, la seconde provient du paquetage -- Nombres_Rationnels Nombre_1 := Zero * (Nombres_Rationnels.Zero * Nombre_1); ...
10.5.3
Clauses use type
L’utilisation systématique de clauses use aboutit à des codes sources contenant très peu de noms développés; cela constitue une avantage appréciable pour le programmeur qui n’a ainsi pas besoin de préfixer un grand nombre d’identificateurs. Par contre, la compréhension de tels codes source peut rapidement devenir difficile par le fait que la provenance des identificateurs est rendue obscure par l’absence de tout préfixe. A contrario, renoncer à l’utilisation des clauses use conduit parfois à une diminution de la lisibilité, voire à une certaine lourdeur du code du fait de la présence de tous les préfixes nécessaires à la visibilité des identificateurs ou des fonctions-opérateurs. En particulier, ces dernières doivent être utilisées sous leur forme fonctionnelle et non simplement et facilement comme des opérateurs. Heureusement, ce dernier inconvénient peut être facilement supprimé par l’utilisation d’une ou plusieurs clauses use type tout en évitant un usage immodéré des clauses use.
UTILISATION D’UN PAQUETAGE
212
Figure 10.5 Syntaxe d’une clause use type. Clause use type use
type
nom de type
;
,
L’utilisation d’une clause use type pour un type T rend directement visibles les fonctions-opérateurs déclarées dans la même spécification que T, sous réserve de la première règle applicable aux clauses use (§ 10.5.2) valable également pour les clauses use type. Par contre, le préfixage reste toujours indispensable pour tous les identificateurs exportés (exemple 10.6). Exemple 10.6 Utilisation du paquetage Nombres_Rationnels et d’une clause use type. with Nombres_Rationnels; use type Nombres_Rationnels.T_Rationnel; -- La clause use type permet l'utilisation directe, sans prefixe, -- des fonctions-operateurs associees au type T_Rationnel -- ... procedure Exemple_10_6 is -- Trois declarations Une_Demi : constant Nombres_Rationnels.T_Rationnel := (1, 2); Nombre_1 : Nombres_Rationnels.T_Rationnel; Nombre_2 : Nombres_Rationnels.T_Rationnel; begin -- Exemple_10_6 -- Affectations Nombre_1 := Une_Demi; Nombre_2 := Nombres_Rationnels.Zero; -- Addition de deux nombres rationnels Nombre_2 := Nombre_1 + (3, 12); -- Division de deux nombres rationnels Nombre_1 := Nombre_1 / Nombre_2; -- Puissance d’un nombre rationnel Nombre_2 := Nombres_Rationnels.Puissance ( Nombre_2, 2 ); ...
UNITÉS DE COMPILATION ET UNITÉS DE BIBLIOTHÈQUE
213
10.6 UNITÉS DE COMPILATION ET UNITÉS DE BIBLIOTHÈQUE 10.6.1
Généralités
Comme déjà relevé, les paquetages forment les briques de base des (grands) programmes Ada. D’un point de vue pratique, comment de tels programmes sontils compilés? La question n’est pas dénuée d’intérêt lorsque leur taille atteint plusieurs dizaines ou centaines de milliers de lignes en code source! Leur compilation en un seul bloc nécessiterait plusieurs heures, même avec la rapidité des processeurs actuels. Il faut donc absolument disposer de facilités de compilation séparée (separate compilation) dans le but de soumettre au compilateur des parties de code compilables pour elles-mêmes appelées unités de compilation (compilation units). Les programmes principaux, les spécifications des paquetages et les corps des paquetages constituent des unités de compilation de même que les sous-programmes dans certains cas particuliers. L’ensemble des unités compilées pour elles-mêmes forment une bibliothèque (library) et sont alors appelées unités de bibliothèque (library units). Dans la pratique, la bibliothèque peut être constituée simplement par un répertoire particulier du système d’exploitation regroupant les unités de bibliothèque, ou encore grâce à un fichier spécial souvent appelé projet et contenant entre autres des liens vers lesdites unités de bibliothèque. Toute unité de compilation peut être précédée d’une ou plusieurs clauses de contexte afin de permettre sa compilation propre. Une telle clause présente avant la spécification d’un paquetage est valable également pour son corps, mais il est possible et conseillé de la placer avant le corps uniquement, si elle n’est pas requise dans la spécification, ceci afin de respecter le principe de localité des déclarations (sect. 4.5). Une clause de contexte va donc servir à mentionner toutes les unités de bibliothèque utilisées dans l’unité de compilation qui suit. L’ordre de compilation des constituants d’un programme doit respecter le fait qu’une unité de bibliothèque ne peut pas être compilée tant que toutes les unités énumérées dans la ou les clauses de contexte de l’unité ne sont pas présentes dans la bibliothèque. 10.6.2
Sous-unités
Lorsque les unités de compilation atteignent une taille importante, il existe une possibilité supplémentaire de compilation séparée en procédant à l’extraction des corps présents dans l’unité de compilation afin de les compiler pour eux-mêmes. A la place des corps, ou plus exactement à la place de tout le code commençant par is et se terminant au end final, il faut indiquer au compilateur, par le mot réservé separate, que ce code va se compiler séparément (exemple 10.7). Cette déclaration particulière s’appelle corps souche (body stub).
UNITÉS DE COMPILATION ET UNITÉS DE BIBLIOTHÈQUE
214
Exemple 10.7 Corps souche Puissance dans le corps du paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels package body Nombres_Rationnels is ---------------------------------------------------------------- Addition de deux nombres rationnels function "+" (X, Y : T_Rationnel ) return T_Rationnel is ... end "+"; ---------------------------------------------------------------- Soustraction de deux nombres rationnels function "–" (X, Y : T_Rationnel ) return T_Rationnel is ... end "–"; ---------------------------------------------------------------- Multiplication de deux nombres rationnels function "*" (X, Y : T_Rationnel ) return T_Rationnel is ... end "*"; ---------------------------------------------------------------- Division de deux nombres rationnels function "/" (X, Y : T_Rationnel ) return T_Rationnel is ... end "/"; ---------------------------------------------------------------- Puissance d'un nombre rationnel sous forme de corps souche function Puissance ( X : T_Rationnel; Exposant:Natural) return T_Rationnel is separate; --------------------------------------------------------------end Nombres_Rationnels;
Le corps extrait est nommé sous-unité (subunit). Il se présente sous la forme du corps complet, précédé d’une clause separate indiquant de quelle unité de compilation, appelée unité parent (parent unit), provient ce corps. Cette sous-unité représente également une unité de compilation. Elle va ainsi pouvoir être compilée pour elle-même (c’est l’objectif visé) et devenir une unité de bibliothèque. Figure 10.6 Syntaxe d’une clause separate. Clause separate
separate
(
nom de l’unité parent
)
UNITÉS DE COMPILATION ET UNITÉS DE BIBLIOTHÈQUE
215
Exemple 10.8 Sous-unité Puissance de l’unité parent Nombres_Rationnels. separate (Nombres_Rationnels) ici! function Puissance (
-- Pas de point-virgule X : T_Rationnel; Exposant : Natural) return
T_Rationnel is begin -- Puissance return ( X.Numerateur ** Exposant, X.Denominateur ** Exposant ); end Puissance;
La fonction Puissance est la seule qui peut devenir une sous-unité (exemple 10.8) dans le corps du paquetage Nombres_Rationnels. En effet, les fonctionsopérateurs ne peuvent pas être des unités de bibliothèque. NOTE 10.2 Visibilité des identificateurs dans une sous-unité. Les identificateurs visibles dans une sous-unité sont ceux qui sont visibles là où le corps souche est placé, et ceux rendus visibles par les clauses de contexte mentionnées pour la sous-unité.
La note 10.2 précise les règles de visibilité applicables à une sous-unité. Enfin, si une sous-unité nécessite une ou plusieurs clauses de contexte, celles-ci se placent avant la clause separate.
CONCEPTION D’UN PAQUETAGE
216
10.7 CONCEPTION D’UN PAQUETAGE La conception et la réalisation d’un bon paquetage pose plus de difficultés que les exemples simples présentés. Le risque de créer une structure fourre-tout qui offre tout et «n’importe quoi» est réel, en particulier pour le concepteur débutant. L’ébauche du paquetage Nombre_Rationnels (exemples 10.1 et 10.2) va être complétée pour illustrer les notions conduisant à la création d’un paquetage cohérent et fournissant les outils de base pour le calcul avec les nombres rationnels. Un paquetage bien conçu doit satisfaire les critères généraux suivants: • il doit fournir une solution d’un problème bien défini et, si possible, de • • • •
faible complexité; il doit constituer une brique réutilisable, voire facilement extensible; la spécification doit être cohérente, former un tout; la spécification doit être simple, et faciliter la tâche de l’utilisateur du paquetage et non celle de son concepteur; le corps doit réaliser complètement et précisément la spécification.
Le fait de limiter le paquetage au calcul avec des nombres rationnels respecte les deux premiers critères. La spécification de l’exemple 10.9 répond aux deux suivants. Mais il faut attendre le corps pour voir si le dernier critère est satisfait. Exemple 10.9 Spécification cohérente du paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels. Tous -- les nombres traites sont irreductibles package Nombres_Rationnels is type T_Rationnel is -- Le type d'un nombre rationnel record Numerateur : Integer; Denominateur : Positive;-- Le signe est au numerateur end record; -- Le nombre rationnel 0 Zero : constant T_Rationnel := (0, 1); ------------------------------------------------------------- Construction d'un nombre rationnel function "/" ( Numerateur : Integer; Denominateur : Positive ) return T_Rationnel; ------------------------------------------------------------- Addition de deux nombres rationnels function "+" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Soustraction de deux nombres rationnels function "–" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------
CONCEPTION D’UN PAQUETAGE
217
-- Multiplication de deux nombres rationnels function "*" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Division de deux nombres rationnels function "/" ( X, Y : T_Rationnel ) return T_Rationnel; ------------------------------------------------------------- Puissance d'un nombre rationnel function "**" ( X : T_Rationnel; Exposant : Natural) return T_Rationnel; ------------------------------------------------------------- Comparaisons de deux nombres rationnels function "=" ( X, Y : T_Rationnel ) return Boolean; function "<" ( X, Y : T_Rationnel ) return Boolean; function "<=" ( X, Y : T_Rationnel ) return Boolean; function ">" ( X, Y : T_Rationnel ) return Boolean; function ">=" ( X, Y : T_Rationnel ) return Boolean; -----------------------------------------------------------end Nombres_Rationnels;
Cette spécification est cohérente parce qu’elle ne traite que des nombres rationnels, parce qu’un seul type permet de déclarer de tels nombres et que les fonctions fournies constituent la base indispensable pour le calcul avec eux. Elle est simple et facilite son utilisation par le choix uniforme des en-têtes des opérations. La déclaration systématique de fonctions-opérateurs permet, en introduisant une clause use type (§ 10.5.3), l’écriture de nombres et d’expressions sous la forme fractionnaire habituelle. L’exemple 10.10 présente le corps du paquetage Nombres_Rationnels. Il implémente les corps de toutes les opérations appartenant à la spécification ainsi que trois fonctions nécessaires à ces opérations, les fonctions P_P_M_C, P_G_C_D et Irreductible. Ces dernières sont invisibles à l’extérieur du paquetage puisqu’elles sont déclarées uniquement dans le corps. Exemple 10.10 Corps complet du paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels. Tous -- les nombres traites sont irreductibles. A relever la presence -- des trois fonctions locales P_P_M_C, P_G_C_D et Irreductible. package body Nombres_Rationnels is ------------------------------------------------------------- Construction d'un nombre rationnel function "/" ( Numerateur : Integer; Denominateur : Positive ) return T_Rationnel is
CONCEPTION D’UN PAQUETAGE
218
begin -- "/" return ( Numerateur, Denominateur ); end "/"; ------------------------------------------------------------- Pour l'addition et la soustraction afin de rendre les -- numerateur et denominateur les plus petits possibles function P_P_M_C ( X, Y : Positive ) return Positive is Multiple_X : Positive := X; Multiple_Y : Positive := Y;
-- Pour les multiples
begin -- P_P_M_C while Multiple_X /= Multiple_Y loop
-- PPMC trouve?
if Multiple_X < Multiple_Y then Multiple_X := Multiple_X + X;
-- Multiple suivant
else Multiple_Y := Multiple_Y + Y;
-- Multiple suivant
end if; end loop; return Multiple_X;
-- C'est le PPMC
end P_P_M_C; -- Addition de deux nombres rationnels function "+" ( X, Y : T_Rationnel ) return T_Rationnel is Le_P_P_M_C : Positive := P_P_M_C ( X.Denominateur, Y.Denominateur ); begin -- "+" return ( X.Numerateur * ( Le_P_P_M_C/X.Denominateur ) + Y.Numerateur * ( Le_P_P_M_C/Y.Denominateur ), Le_P_P_M_C ); end "+"; ------------------------------------------------------------- Soustraction de deux nombres rationnels function "–" ( X, Y : T_Rationnel ) return T_Rationnel is Le_P_P_M_C : Positive := P_P_M_C ( X.Denominateur, Y.Denominateur ); begin -- "–" return ( X.Numerateur * ( Le_P_P_M_C/X.Denominateur ) – Y.Numerateur * ( Le_P_P_M_C/Y.Denominateur ), Le_P_P_M_C ); end "–"; ------------------------------------------------------------- Pour la reduction en un nombre rationnel irreductible par -- la fonction Irreductible function P_G_C_D ( X, Y : Positive ) return Positive is Diviseur_X : Positive := X;
-- Pour les soustractions
CONCEPTION D’UN PAQUETAGE
219
Diviseur_Y : Positive := Y; begin -- P_G_C_D while Diviseur_X /= Diviseur_Y loop
-- PGCD trouve?
if Diviseur_X > Diviseur_Y then Diviseur_X := Diviseur_X – Diviseur_Y; else Diviseur_Y := Diviseur_Y – Diviseur_X; end if; end loop; return Diviseur_X;
-- C'est le PGCD
end P_G_C_D; ------------------------------------------------------------- Rendre un nombre rationnel irreductible apres -- multiplication et division de deux nombres rationnels function Irreductible ( X : T_Rationnel) return T_Rationnel is Le_P_G_C_D : Positive; begin -- Irreductible if X.Numerateur = 0 then return (0, 1); else Le_P_G_C_D := P_G_C_D (abs X.Numerateur, X.Denominateur); return (X.Numerateur / Le_P_G_C_D, X.Denominateur / Le_P_G_C_D ); end if; end Irreductible; ------------------------------------------------------------- Multiplication de deux nombres rationnels. Le resultat est -- un nombre rationnel irreductible function "*" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "*" return Irreductible ((
X.Numerateur * Y.Numerateur, X.Denominateur * Y.Denominateur ));
end "*"; ------------------------------------------------------------- Division de deux nombres rationnels. Le resultat est un -- nombre rationnel irreductible function "/" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "/" if Y.Numerateur > 0 then return Irreductible((
-- Diviseur positif X.Numerateur * Y.Denominateur, X.Denominateur * Y.Numerateur ));
elsif Y.Numerateur < 0 then
-- Changer de signe
return Irreductible((– X.Numerateur * Y.Denominateur,
CONCEPTION D’UN PAQUETAGE
220
X.Denominateur * abs Y.Numerateur)); else -- Division par zero! ...
-- Lever une exception (sect. 13.3)
end if; end "/"; ------------------------------------------------------------- Puissance d'un nombre rationnel function "**" (X : T_Rationnel; Exposant : Natural) return T_Rationnel is begin -- "**" return ( X.Numerateur ** Exposant, X.Denominateur ** Exposant ); end "**"; ------------------------------------------------------------- Comparaisons entre deux nombres rationnels: egalite function "=" ( X, Y : T_Rationnel ) return Boolean is begin -- "=" return X.Numerateur * Y.Denominateur = X.Denominateur * Y.Numerateur; end "="; -- Comparaisons entre deux nombres rationnels: inferieur function "<" ( X, Y : T_Rationnel ) return Boolean is begin -- "<" return X.Numerateur * Y.Denominateur < X.Denominateur * Y.Numerateur; end "<"; ------------------------------------------------------------- Comparaisons entre deux nombres rationnels: inferieur ou -- egal function "<=" ( X, Y : T_Rationnel ) return Boolean is begin -- "<=" return X.Numerateur * Y.Denominateur <= X.Denominateur * Y.Numerateur; end "<="; ------------------------------------------------------------- Comparaisons entre deux nombres rationnels: superieur function ">" ( X, Y : T_Rationnel ) return Boolean is begin -- ">" return X.Numerateur * Y.Denominateur > X.Denominateur * Y.Numerateur; end ">"; ------------------------------------------------------------- Comparaisons entre deux nombres rationnels: superieur ou -- egal
CONCEPTION D’UN PAQUETAGE
221
function ">=" ( X, Y : T_Rationnel ) return Boolean is begin -- ">=" return X.Numerateur * Y.Denominateur >= X.Denominateur * Y.Numerateur; end ">="; -----------------------------------------------------------end Nombres_Rationnels;
Les opérations de comparaison pourraient générer des erreurs de débordement (Constraint_Error) provoquées par l’utilisation des produits croisés, et la division par un nombre rationnel nul lèvera une exception (sect. 13.3). Tous les cas où une exception peut être levée doivent être précisés (note 10.3). NOTE 10.3 Documentation accompagnant un paquetage. Un mode d’emploi doit accompagner tout paquetage. Il contiendra en particulier les règles d’utilisation des opérations exportées par la spécification ainsi que les sources possibles des erreurs générées lors de l’exécution de tout sous-programme du paquetage.
Il faut enfin relever un point fondamental qui justifiera une présentation détaillée des types privés (sect. 16.2). La transparence du type T_Rationnel, c’est-à-dire la connaissance de sa structure hors du paquetage, peut conduire à une utilisation erronée d’objets de ce type. Par exemple, si R et S sont deux variables rationnelles, rien n’interdit au programmeur d’écrire l’agrégat logiquement faux (R.Numerateur + S.Numerateur, R.Denominateur + S.Denominateur)
qui ne correspond à aucune opération sur des nombres rationnels. Ces erreurs, involontaires, doivent être détectées si possible à la compilation. Les types privés (sect. 16.2) empêchent de telles erreurs et augmentent la fiabilité (sect. 1.5) d’un paquetage qui doit naturellement être maximale.
PAQUETAGES ENFANTS
222
10.8 PAQUETAGES ENFANTS 10.8.1
Généralités
Soit un paquetage P correctement conçu, et même déjà compilé. Il est donc utilisable par n’importe quelle unité de bibliothèque. L’évolution de ces unités au cours du temps (corrections, modifications, extensions, etc.) peut nécessiter des adaptations du paquetage P, aussi bien de sa spécification que de son corps. En particulier, l’adjonction de nouvelles fonctionnalités à P nécessitera la recompilation de P et de toutes les unités qui l’importent, même si ses nouvelles fonctionnalités ne sont pas utiles pour certaines d’entre elles. Pour étendre le paquetage P d’origine sans les inconvénients cités ci-dessus, il existe la notion de paquetage enfant (child package). Il s’agit de laisser le paquetage P tel quel et le compléter par un paquetage supplémentaire dont la spécification et le corps complètent la spécification du paquetage P. Cette manière de procéder implique que les unités utilisant uniquement P, donc pas le nouveau paquetage enfant, n’auront pas besoin d’être recompilées! Naturellement celles qui importent le paquetage enfant devront l’être. Dans ce contexte, le paquetage P est appelé paquetage parent (parent package). Le nom d’un enfant est obligatoirement un nom développé (sect. 4.4) composé du nom du paquetage parent comme préfixe et d’un identificateur adéquat caractérisant l’enfant. De tels noms mentionnés précédemment (sect. 10.3) désignent donc des paquetages enfants! Exemple 10.11 Spécification d’un paquetage enfant Nombres_Rationnels.Utilitaires. -- Ce paquetage complete le calcul avec les nombres rationnels package Nombres_Rationnels.Utilitaires is ------------------------------------------------------------- Valeur absolue d'un nombre rationnel function "abs" ( X : T_Rationnel ) return T_Rationnel; -- Multiplication d'un nombre rationnel par un nombre entier function "*" ( N : Integer; X : T_Rationnel ) return T_Rationnel; function "*" (
X : T_Rationnel; N : Integer ) return T_Rationnel;
------------------------------------------------------------- Division d'un nombre rationnel par un nombre entier function "/" ( X : T_Rationnel; N : Integer ) return T_Rationnel; -----------------------------------------------------------end Nombres_Rationnels.Utilitaires;
PAQUETAGES ENFANTS
223
L’exemple 10.11 montre la définition de la spécification du paquetage enfant Nombres_Rationnels.Utilitaires. Celle-ci met à disposition quatre fonctions-opérateurs pour des calculs complémentaires sur les nombres rationnels. Quant à l’exemple 10.12, il présente, lui, le corps de ce paquetage enfant. Exemple 10.12 Corps du paquetage enfant Nombres_Rationnels.Utilitaires. -- Ce paquetage complete le calcul avec les nombres rationnels package body Nombres_Rationnels.Utilitaires is ------------------------------------------------------------- Valeur absolue d'un nombre rationnel function "abs" ( X : T_Rationnel ) return T_Rationnel is begin -- "abs" return ( abs X.Numerateur, X.Denominateur ); end "abs"; ------------------------------------------------------------- Multiplication d'un nombre rationnel par un nombre entier function "*" ( N : Integer; X : T_Rationnel ) return T_Rationnel is begin -- "*" return ( N * X.Numerateur, X.Denominateur ); end "*"; -----------------------------------------------------------function "*" ( X : T_Rationnel; N : Integer ) return T_Rationnel is begin -- "*" return ( N * X.Numerateur, X.Denominateur ); end "*"; ------------------------------------------------------------- Division d'un nombre rationnel par un nombre entier function "/" ( X : T_Rationnel; N : Integer ) return T_Rationnel is begin -- "/" if N > 0 then -- Diviseur positif return ( X.Numerateur, N * X.Denominateur ); elsif N < 0 then
-- Diviseur negatif, le signe est au -- numerateur return ( – X.Numerateur, abs N * X.Denominateur );
else -- Division par zero! ... -- Lever une exception (sect. 13.3) end if; end "/";
PAQUETAGES ENFANTS
224
-----------------------------------------------------------end Nombres_Rationnels.Utilitaires;
Les multiplications et la division par un nombre entier retournent des nombres rationnels non irréductibles! Il faudrait les réduire par l’utilisation de la fonction Irreductible, soit placée dans la partie visible de Nombres_Rationnels ou alors déclarée une deuxième fois dans le corps du paquetage enfant Nombres_Rationnels.Utilitaires, solution insatisfaisante dans les deux cas car cette opération est évidemment purement interne. Une solution élégante à ce problème passe par l’utilisation d’un enfant privé (sect. 16.7). 10.8.2
Visibilité entre paquetages parents et enfants
Les éléments de la spécification d’un paquetage parent sont directement visibles (sect. 4.4) dans la spécification et le corps d’un paquetage enfant. Les éléments du corps du paquetage parent ne sont jamais visibles pour le paquetage enfant. On peut donc se représenter le paquetage enfant comme s’il était déclaré entre la spécification et le corps du paquetage parent (fig. 10.7 et 16.2). Figure 10.7 Visibilité entre paquetages parent et enfant.
Paquetage enfant
Paquetage parent package Parent is ... end Parent;
package Parent.Enfant is ... end Parent.Enfant;
package body Parent is ... end Parent;
package body Parent.Enfant is ... end Parent.Enfant;
Un paquetage enfant peut être parent d’un autre paquetage enfant et ainsi de suite. Un paquetage parent peut avoir plusieurs paquetages enfants. On obtient alors une hiérarchie de paquetages qui devraient illustrer la décomposition naturelle des fonctionnalités. Du point de vue de la visibilité, les éléments de la spécification d’un paquetage parent sont directement visibles dans la spécification et le corps de tous ses paquetages enfants, petits-enfants, etc. 10.8.3
Utilisation d’un paquetage enfant
PAQUETAGES ENFANTS
225
Comme pour un paquetage parent, toute unité de bibliothèque doit mentionner le paquetage enfant dans une clause de contexte (§ 10.5.1) afin d’accéder à sa spécification. Une clause use peut également s’appliquer au nom d’un paquetage enfant pour éviter de préfixer les éléments de sa spécification. Une particularité très pratique consiste en le fait qu’une clause de contexte pour l’utilisation d’un paquetage enfant comprend implicitement une telle clause pour son paquetage parent ainsi que pour tous ses ancêtres! Par contre, cette règle ne s’applique pas aux clauses use. L’exemple 10.13 présente quelques cas d’utilisation d’un paquetage enfant. Exemple 10.13 Paquetages parent, enfant, clauses de contexte et clauses use. -- Premier exemple with Nombres_Rationnels.Utilitaires; -- Implique automatiquement with Nombres_Rationnels procedure Exemple_10_13 is use Nombres_Rationnels.Utilitaires; ...
-- Visibilite directe sur les elements de Utilitaires
---------------------------------------------------------------- Deuxieme exemple with Nombres_Rationnels.Utilitaires; use Nombres_Rationnels; procedure Exemple_10_13 is use Utilitaires; ...
-- Visibilite directe sur les elements des deux -- specifications
---------------------------------------------------------------- Troisieme exemple with Nombres_Rationnels.Utilitaires; package body Nombres_Rationnels is -- La specification d'un enfant peut s'utiliser dans le corps du -- parent ... -- Quatrieme exemple with Nombres_Rationnels.Utilitaires; package Nombres_Rationnels.Autre_Enfant is -- La specification d'un enfant peut s'utiliser dans la -- specification d'un autre enfant ... ---------------------------------------------------------------- Cinquieme exemple with Nombres_Rationnels.Utilitaires; package body Nombres_Rationnels.Autre_Enfant is
PAQUETAGES ENFANTS
226
-- La specification d'un enfant peut s'utiliser dans le corps -- d'un autre enfant ...
10.8.4
Remarques
Fort de la connaissance de la notion de paquetage enfant et pour obtenir une excellente structuration, le paquetage Nombres_Rationnels devrait être disjoint en un paquetage parent plus concis (le type T_Rationnel, la fonction de construction et les quatre opérations arithmétiques de base) et un nouveau paquetage enfant contenant les autres éléments (la constante Zero et les opérations de comparaison). La compilation d’un paquetage enfant ne peut se faire que lorsque la spécification de son parent a été introduite dans la bibliothèque. Bien entendu la spécification d’un paquetage enfant se compile avant le corps. Il existe des règles particulières pour les paquetages enfants appelés publics ou privés (sect. 16.6 et 16.7). Cette distinction deviendra nécessaire lorsque les notions de types privés et de parties privées (sect. 16.2) auront été présentées. Pour terminer, il faut mentionner que la norme Ada comporte de nombreux paquetages prédéfinis ou offerts dans les annexes du langage (sect. 19.3).
227
10.9 EXERCICES 10.9.1
Spécification de paquetages
Ecrire la spécification de paquetages de gestion de: • • • •
10.9.2
nombres complexes avec les opérations de base (exercice 7.4.4); vecteurs avec les opérations de base (exercice 8.6.5); matrices carrées avec les opérations de base (exercice 8.6.6); fiches personnelles (nom, prénom, état civil, sexe, etc.) avec les opérations de création, d’affichage et de destruction d’une fiche. Corps de paquetages
Ecrire le corps des paquetages de l’exercice 10.9.1. 10.9.3
Utilisation de paquetages
Réaliser des programmes de test des opérations fournies par les paquetages des exercices 10.9.1 et 10.9.2. 10.9.4
Paquetage avec variable rémanente
Ecrire la spécification et le corps d’un paquetage de génération des nombres de Fibonacci. L’unique opération exportée sera Prochain_Nombre. Ces nombres se calculent par la formule Fn = F n – 1 + Fn – 2 avec F0 = 0 et F 1 = 1 . 10.9.5
Paquetage avec variable rémanente
Ecrire la spécification et le corps d’un paquetage de génération de nombres entiers pseudo-aléatoires. Les opérations exportées seront Initialiser et Prochain_Nombre. Voici un algorithme de génération connu: Test := 16807 * (Seed rem 127773) – 2836 * (Seed/127773) si Test > 0 alors Seed := Test sinon Seed := Test + M fin si Nouveau_Nombre := Réel(Seed)/Réel(M) M vaut 2**31–1 et Seed est initialisé à 117. Tous les nombres sont des entiers dans l’intervalle –2**31..2**31–1, la conversion finale en nombres réels est nécessaire pour normaliser entre 0.0 et 1.0. 10.9.6
Paquetages enfants
Ecrire la spécification et le corps de paquetages enfants dont les parents sont les paquetages des exercices 10.9.1 et 10.9.2. Plus précisément, les opérations mises à disposition par les enfants sont: • la puissance, l’obtention de la partie réelle et celle de la partie imaginaire
228
pour les nombres complexes; • la norme et le produit scalaire pour les vecteurs; • la puissance et la multiplication par un nombre réel pour les matrices; • l’obtention du nombre total de fiches et la recherche d’une fiche en fonction d’un nom particulier pour le paquetage de gestion de fiches personnelles; un algorithme de recherche peut être imaginé ou alors repris du paragraphe 14.4.8 et adapté au problème. 10.9.7
Utilisation de paquetages enfants
Réaliser des programmes de test des opérations fournies par les paquetages enfants de l’exercice 10.9.6.
POINTS À RELEVER
229
10.10 POINTS À RELEVER 10.10.1 En général • Les paquetages et leurs équivalents dans d’autres langages de program-
mation forment, comme les sous-programmes, un constituant de base des (grands) programmes. • La réalisation d’un bon paquetage, ou de leurs équivalents dans d’autres
langages de programmation, doit respecter des règles de conception. 10.10.2 En Ada • La spécification fournit les éléments exportés utilisables hors du paquetage. • Le corps contient les éléments propres au paquetage, invisibles à l’ex-
térieur. • La spécification des sous-programmes exportés se déclare dans la spéci-
fication des paquetages, leur corps dans le corps des paquetages. • Les éléments d’un paquetage, en particulier les variables, ont la même
durée de vie que le paquetage. • Le code d’initialisation est exécuté avant toute utilisation d’un paquetage. • Une clause de contexte rend visibles, utilisables, un ou plusieurs paque-
tages. • Une clause use rend directement visibles les éléments exportés d’un pa-
quetage. • Une clause use type appliquée à un type T rend directement visibles les
fonctions-opérateurs associées à T et déclarées dans la même spécification que T. • Une unité de compilation est une structure compilable pour elle-même. • Une unité de bibliothèque est une unité de compilation déjà compilée. • Une sous-unité est un corps extrait d’une unité de compilation, transformé
en une unité de compilation pour elle-même. Le corps-souche est ce qui reste dans l’unité de compilation originale après l’extraction. • Un paquetage enfant est une extension, un complément d’un paquetage
appelé paquetage parent. • Les clauses de contexte possèdent quelques particularités pour des paque-
tages enfants. • Les annexes du langage Ada définissent de nombreux paquetages pré-
définis.
POINTS À RELEVER
230
231
C H A P I T R E
ARTICLES À
1 1
232
ARTICLES À DISCRIMINANTS
MOTIVATION
233
11.1 MOTIVATION Les types articles vus précédemment (chap. 7) ont tous une structure statique: leurs champs sont définis à la déclaration du type et n’ont aucune dépendance entre eux en dehors de la volonté du concepteur de les associer en un seul type. D’autre part, si un tableau constitue l’un de ces champs, il doit avoir ses intervalles d’indices déterminés à la compilation. Or les données regroupées dans un article peuvent comporter des dépendances naturelles. Par exemple, l’information concernant la fiche personnelle d’un individu verra sa forme dépendre partiellement du sexe de la personne. Les discriminants sont des champs qui influencent la structure des articles dans lesquels ils sont utilisés. A ce titre ils constituent une sorte de paramètres des types articles. Mentionnons également que la notion de discriminant se rencontre également dans d’autres contextes (types tâches et types protégés) non présentés dans cet ouvrage.
GÉNÉRALITÉS
234
11.2 GÉNÉRALITÉS 11.2.1
Types articles à discriminants
Un discriminant est un champ (§ 7.2.1) délimité par une paire de parenthèses et localisé entre l’identificateur d’un type article et le mot réservé is. Cette particularité de placement définit à elle seule qu’un tel champ est un discriminant. De plus, le type d’un discriminant doit être discret ou accès [ARM 3.2] mais cette dernière possibilité ne sera pas développée ici. Il est possible de placer plus d’un discriminant dans la paire de parenthèses les délimitant. Dans ce cas, chaque déclaration est séparée de la suivante par un pointvirgule. A l’intérieur du type article, un ou plusieurs champs (éventuellement aucun...) formeront le contenu (exemple 11.1). Parmi ceux-ci, certains peuvent dépendre d’un discriminant: ce sont les tableaux dont l’une des bornes au moins est donnée par le discriminant seul (une expression est ici interdite). Notons qu’une autre forme de dépendance est fournie dans les types articles à parties variantes (sect. 11.4). Exemple 11.1 Types articles à discriminant et déclarations d’articles. type Intervalle is range 0 .. 100; type T_Exemple (Discriminant : Intervalle) is record -- Exemple symbolique Champ_1 : Integer; Champ_2 : String (1..Discriminant); -- Sect. 9.2 Champ_3 : Float; end record; type T_Degre is range 0 .. 100; type T_Coefficients is array (T_Degre range <>) of Integer; type T_Polynome (Degre : T_Degre) is record -- Pour traiter des polynomes -- Coefficients des termes Coefficients : T_Coefficients (T_Degre'First..Degre); end record; Exemple : T_Exemple (5); -- Contient un tableau de 5 elements Quadratique : T_Polynome (2); -- Un polynome de degre 2 -- Pour afficher n'importe quel polynome procedure Afficher ( Polynome : in T_Polynome );
En l’absence d’une valeur par défaut pour un discriminant (sect. 11.3), la déclaration d’un article (constante ou variable) à discriminant doit comporter une valeur pour chacun de ses discriminants (exemple 11.1), valeur donnée entre parenthèses ou par une valeur initiale (agrégat). Un tel article est dit contraint (par
GÉNÉRALITÉS
235
le discriminant), par analogie avec la déclaration d’un tableau. Le type T_Polynome va être utilisé pour illustrer le travail avec des articles dont un champ est un tableau dépendant d’un discriminant [BAR 97], [MOL 92]. Par convention, un polynôme classique comme P(x) = a0 + a1x + a2x2 + ... + anxn avec an /= 0 si n /= 0 va être réalisé sous la forme d’un article de type T_Polynome dont le discriminant représentera le degré du polynôme et les éléments du tableau Coefficients implémenteront les coefficients a0...an selon la convention Coefficient(i) = ai. 11.2.2
Sous-types articles à discriminants
Il est possible de déclarer des sous-types d’un type (de base) article à discriminants (exemple 11.2). La forme générale d’une telle déclaration est la suivante: subtype identificateur is id_type_article (valeur_discr_1, ..., valeur_discr_N);
où • identificateur est le nom du sous-type; • id_type_article est le nom d’un type article à discriminant; • valeur_discr_i fixe la valeur du ième discriminant. Exemple 11.2 Sous-types et articles à discriminants. -- Pour utiliser des polynomes de degres 2 et 5 subtype T_Polynome_2 is T_Polynome (2); subtype T_Polynome_5 is T_Polynome (5); Quadratique : T_Polynome_2; Polynome_Degre_5 : T_Polynome_5;
11.2.3
-- Un polynome de degre 2 -- Un polynome de degre 5
Agrégats, expressions et opérations
Les agrégats d’article à discriminants (exemple 11.3) sont semblables à ceux des articles simples (§ 7.2.2). Les valeurs attribuées aux discriminants se situent au début de l’agrégat si la notation par position est utilisée. La seule subtilité réside dans les agrégats dynamiques, où la valeur d’un discriminant et une borne du tableau (au moins) sont donnés par une variable. Exemple 11.3 Déclarations de polynômes et agrégats. -- Le polynome 3 – 2x + 5x2 Polynome_1: T_Polynome (2) := (2, (3, –2, 5) ); -- Le polynome x + 7x4 declare de deux manieres equivalentes
GÉNÉRALITÉS
236
Polynome_2: T_Polynome (4) := (4, (0, 1, 0, 0, 7) ); Polynome_3: T_Polynome := (4, (0, 1, 0, 0, 7) ); -- Le polynome constant –6 Polynome_4: constant T_Polynome (0) := ( Degre => 0, Coefficients => (0 => –6) ); -- Le polynome 1+x+x2+...+xn Polynome_5: T_Polynome := (N, (0..N => 1) );-- N de type T_Degre -- et voir § 8.2.5
Il faut rappeler qu’il est toujours possible de qualifier l’agrégat, c’est-à-dire de préciser son type en le préfixant par le nom du type suivi d’une apostrophe. Les expressions d’un type article à discriminants sont réduites aux constantes, variables et paramètres de ce type. Les opérations possibles sur les articles (en plus de l’affectation et du passage en paramètre) sont l’égalité = et l’inégalité /=. 11.2.4
Affectation
L’affectation se fait de manière habituelle, mais en précisant que la valeur d’un discriminant donnée à la déclaration d’une variable ne peut plus être modifiée par la suite. L’expression et la variable présentes dans une opération d’affectation devront donc comporter les mêmes valeurs pour tous les discriminants correspondants, sinon l’exception Constraint_Error sera levée. L’exemple 11.4 illustre l’affectation et le passage en paramètres d’articles à discriminants. Exemple 11.4 Affectation de valeurs d’un type article à discriminants. -- ... procedure Exemple_11_4 is type T_Degre is range 0 .. 100; type T_Coefficients is array (T_Degre range <>) of Integer; type T_Polynome (Degre : T_Degre) is record -- Pour traiter des polynomes -- Coefficients des termes Coefficients : T_Coefficients (T_Degre'First..Degre); end record; -- Une constante et deux variables de ce type Deux_Plus_X : constant T_Polynome := (1, (2, 1)); Polynome_1 : T_Polynome (1); Polynome_2 : T_Polynome := ( Degre => 2, Coefficients => (2, –4, 1)); ------------------------------------------------------------
GÉNÉRALITÉS
237
-- Pour afficher n'importe quel polynome procedure Afficher ( Polynome : in T_Polynome ) is ... end Afficher; -----------------------------------------------------------begin -- Exemple_11_4 -- Trois affectations correctes car meme valeur de discriminant Polynome_1 := Deux_Plus_X; Polynome_2 := (Polynome_2.Degre, (1, –2, 1)); Polynome_2 := T_Polynome'(2, (1, –2, 1));
11.2.5
Afficher ( Deux_Plus_X ); Afficher ( Polynome_2 );
-- Affichage de deux polynomes
Polynome_2 := Polynome_1; ...
-- Provoque Constraint_Error -- (discriminants differents)
Compléments sur les articles à discriminants
Comme déjà mentionné, une expression, et non seulement une constante, peut donner la valeur d’un discriminant (exemple 11.5). Par contre, un discriminant seul peut être utilisé comme borne d’un champ tableau. Comme suggéré dans l’exemple 11.4, l’accès à un champ discriminant s’effectue comme pour un champ habituel, en préfixant le nom du discriminant par le nom de l’article et en séparant les deux identificateurs par un point. Il existe des attributs applicables à un article ou un type article à discriminants. Le plus utile est l’attribut Constrained (§ 11.3.3). Lors d’un passage en paramètre, la valeur du discriminant d’un paramètre formel comme Polynome (exemple 11.4) est obtenue de celle du paramètre effectif correspondant, quel que soit le mode de passage (in, out, in out) du paramètre. Si le paramètre formel est d’un type ou sous-type contraint, les valeurs des discriminants des paramètres formel et effectif doivent être identiques, sinon l’exception Constraint_Error sera levée à l’appel du sous-programme. Une fonction peut retourner un article à discriminant comme résultat (exemple 11.5). Exemple 11.5 Illustration de quelques-unes des remarques ci-dessus. -- Expression comme valeur de discriminant Polynome_1 : T_Polynome (3 * 4 + N) -- N entier ---------------------------------------------------------------- Fonction a resultat de type T_Polynome function Polynome_Nul return T_Polynome is begin -- Polynome_Nul
GÉNÉRALITÉS
238
return (0, (0 => 0) ); end Polynome_Nul; ---------------------------------------------------------------- Correspondance entre valeurs de discriminants, N entier egal -- a 3 sinon Constraint_Error Polynome_2 : T_Polynome (3) := (N, (1..N => 1) );
VALEURS PAR DÉFAUT DES DISCRIMINANTS
239
11.3 VALEURS PAR DÉFAUT DES DISCRIMINANTS 11.3.1
Motivation
L’inconvénient majeur des articles à discriminants examinés jusqu’ici réside dans l’impossibilité de modifier la contrainte de discriminant, c’est-à-dire la valeur donnée lors de la déclaration de tels articles. En introduisant une expression (valeur) par défaut pour un discriminant lors de la déclaration du type article, ce type devient non contraint et la liberté existe de contraindre ou non un article de ce type lors d’une déclaration de variable, en mentionnant explicitement une valeur ou, au contraire, en laissant la valeur de l’expression par défaut donner la valeur du discriminant. Le fait d’utiliser la valeur de l’expression par défaut conduit à créer un article non contraint dont les champs, y compris les discriminants, peuvent être modifiés par la suite. La modification de la valeur d’un discriminant d’un article non contraint n’est cependant possible que par une affectation globale de l’article, au moyen d’un agrégat par exemple (exemple 11.6). Cette manière de faire assure le maintien de la cohérence entre la valeur du discriminant et les champs qui en dépendent. Exemple 11.6 Discriminants avec valeur par défaut. -- ... procedure Exemple_11_6 is type T_Degre is range 0 .. 100; type T_Coefficients is array (T_Degre range <>) of Integer; type T_Polynome (Degre : T_Degre:= 0) is-- 0 est la valeur -- par defaut record Coefficients : T_Coefficients (T_Degre'First..Degre); end record; -- Une constante et une variable contrainte de ce type Deux_Plus_X : constant T_Polynome := (1, (2, 1)); Polynome_1 : T_Polynome (1); -- Deux variables de ce type, non contraintes Polynome_2 : T_Polynome := ( Degre => 2, Coefficients => (2, –4, 1)); Polynome_3 : T_Polynome; begin -- Exemple_11_6 -- Toutes les affectations suivantes sont correctes Polynome_1 := Deux_Plus_X; Polynome_2 := (Polynome_2.Degre, (1, –2, 1)); Polynome_2 := T_Polynome'(2, (1, –2, 1)); Polynome_3 := Polynome_1; Polynome_3 := (Polynome_2.Degre – 1, Polynome_2.Coefficients (0..Polynome_2.Degre –
VALEURS PAR DÉFAUT DES DISCRIMINANTS
240
1)); ...
Si un article comprend plus d’un discriminant, alors il doit être complètement contraint (aucune expression par défaut) ou non contraint (tous les discriminants ont une expression par défaut). Finalement un paramètre formel d’entrée-sortie ou de sortie d’un type article à discriminants est contraint si le paramètre effectif l’est; un paramètre formel d’entrée est de toute manière toujours constant. 11.3.2
Remarque importante
Quel serait l’effet de remplacer le sous-type T_Degre par Natural dans la déclaration du type T_Polynome avec expression par défaut (§ 11.3.1)? Un compilateur Ada réservera en général une place mémoire permettant d’implanter le tableau Coefficients, quelle que soit la valeur par défaut. Le nombre d’octets nécessaires sera donc de Natural'Last multiplié par la taille d’un entier, ce qui représente beaucoup (trop) de mémoire et provoquera donc très vraisemblablement l’exception Storage_Error, exception qui est levée lorsque la place mémoire devient insuffisante lors de l’exécution d’un programme. 11.3.3
Attribut Constrained
L’attribut Constrained qui s’applique entre autres à un article à discriminants rend une valeur booléenne vraie si l’article est contraint, fausse sinon. En faisant référence à l’exemple 11.6, on obtiendrait Polynome_1'Constrained = True, Polynome_2'Constrained = False et Polynome_3'Constrained = False. Exemple 11.7 Utilisation de l’attribut Constrained. -- Cette procedure normalise le polynome P, c'est-a-dire qu'elle -- assure que le polynome rendu dans P, de degre n, a son terme -- anxn non nul. -- Il faut cependant que le parametre effectif soit non contraint. procedure Normaliser ( P : in out T_Polynome ) is Degre_Polynome : Natural := P.Degre; -- Degre du polynome begin -- Normaliser -- Normalisation possible? if not P'Constrained then -- Chercher le plus grand coefficient non nul while Degre_Polynome > 0 and then P.Coefficients (Degre_Polynome) = 0 loop Degre_Polynome := Degre_Polynome – 1; end loop;
VALEURS PAR DÉFAUT DES DISCRIMINANTS
P := ( Degre_Polynome, P.Coefficients (T_Degre'First..Degre_Polynome) ); end if; end Normaliser;
241
ARTICLES À PARTIES VARIANTES
242
11.4 ARTICLES À PARTIES VARIANTES 11.4.1
Motivation
Il n’est pas rare qu’un groupe d’informations formant un tout cohérent, typiquement le contenu d’un article, comporte une partie commune à tous les exemplaires de ces groupes mais que ces exemplaires se distinguent les uns des autres par la présence ou l’absence de certaines informations n’appartenant pas à cette partie commune. Par exemple, les informations nécessaires pour décrire une personne comportent des indications indépendantes du sexe (taille, poids, couleur des cheveux...) mais d’autres ne sont opportunes que pour l’un des deux sexes (présence ou absence de barbe, femme enceinte ou non...). Les fenêtres des interfaces utilisateurs actuelles en sont un autre exemple. Parmi les informations communes, l’on trouvera les dimensions et la position, mais les fenêtres se classent en plusieurs catégories: celles qui possèdent un titre, les fenêtres graphiques ou textuelles, celles prévues pour un dialogue modal, les avertissements, etc. Une telle fenêtre s’implémentera directement par un article à discriminants avec partie variantes. 11.4.2
Généralités
Une partie variantes est une structure permettant de déclarer les champs particuliers de certains articles et ressemblant fortement à la syntaxe de l’instruction case. La forme générale d’une partie variantes est: case discriminant is when choix_1 => suite_de_decl_de_champs_1;-- Premiere branche when choix_2 => suite_de_decl_de_champs_2;-- Deuxieme branche when choix_3 => suite_de_decl_de_champs_3;-- Troisieme branche ... when others => autre_suite_de_decl_de_champs;-- Derniere branche end case;
avec • discriminant d’un type discret et déclaré comme discriminant d’article; • les choix_n statiques (sect. 4.10), formés de valeurs ou d’intervalles
séparés par des barres verticales, valeurs et bornes d’intervalle du type du discriminant; • les suite_de_decl_de_champs_n composées d’une ou de plusieurs déclarations de champs (éventuellement aucune); • le mot réservé others qui représente toutes les autres valeurs possibles du discriminant. Un type article à discriminant peut donc comporter maintenant, comme dernière déclaration, une partie variantes qui permettra de déclarer des objets de ce
ARTICLES À PARTIES VARIANTES
243
type (exemple 11.8) et dont la structure variera en fonction de la valeur du discriminant. Exemple 11.8 Types articles à partie variantes et déclarations d’articles. Max : constant := 80;
-- Longueur maximum d'une ligne
type T_Genre_Fenetre is (
Confirmation, De_Taille_Variable, Avertissement);
type T_Fenetre ( Genre : T_Genre_Fenetre:= De_Taille_Variable ) is record Abscisse : Natural; Ordonnee : Natural; Longueur : Natural;
-- Position de la fenetre -- Dimensions de la fenetre en
pixels Largeur : Natural; case Genre is
-- Partie variantes
when Confirmation => Texte_Affiche : String ( 1..Max );-- Selon le texte, Reponse_OK : Boolean; -- confirmer ou non when De_Taille_Variable => Variation_X : Integer := 0; -- Variations par rapport Variation_Y : Integer := 0; -- aux dimensions d’origine when Avertissement => Texte_Avertissement : String ( 1..Max ); end case; end record; Edition : T_Fenetre ( De_Taille_Variable ); -- Article contraint Graphique : T_Fenetre; -- Article non contraint Erreur_NT : T_Fenetre ( Avertissement ); -- Article contraint
11.4.3 Expressions, agrégats et opérations sur les articles à parties variantes Les expressions, agrégats et opérations sur les articles à parties variantes sont semblables à ce qui existe pour les articles à discriminants sans partie variantes. La seule différence concerne les agrégats: les valeurs des discriminants doivent y être statiques puisqu’un agrégat joue le rôle d’une constante (exemple 11.9). Il est bien clair que seules des valeurs pour la branche correspondant à la valeur du discriminant seront mentionnées dans l’agrégat. Exemple 11.9 Déclarations d’articles à partie variantes et d’agrégats. -- Edition est un article contraint
ARTICLES À PARTIES VARIANTES
244
Edition : T_Fenetre ( De_Taille_Variable ) := (De_Taille_Variable, 0, 0, 600, 400, 0, 0); -- Graphique est non contraint malgre la valeur initiale Graphique : T_Fenetre := (De_Taille_Variable, 0, 0, 600, 400, 10, 20); -- Agregat tableau (§ 8.2.5) pour le texte d'avertissement Erreur_NT : T_Fenetre ( Avertissement ) := (Avertissement, 0, 0, 600, 400, "Profil inconnu" & (15..80=>' '));
11.4.4
Affectation
L’affectation se fait de manière habituelle, en rappelant que la valeur d’un discriminant donnée à la déclaration d’une variable (et non par une valeur initiale) ne peut plus être modifiée par la suite. L’expression et la variable présentes dans une opération d’affectation doivent donc comporter les mêmes valeurs pour tous les discriminants correspondants, sinon l’exception Constraint_Error sera levée. Si les discriminants comportent une valeur par défaut et que cette valeur est utilisée lors de la déclaration d’un article (comme pour Graphique dans l’exemple 11.8), la valeur de tout l’article, y compris les discriminants, est modifiable globalement. 11.4.5
Compléments sur les articles à parties variantes
Un choix (représentant une valeur possible du discriminant) ne peut apparaître qu’une seule fois. Si le discriminant reçoit une valeur ne correspondant à aucun des choix mentionnés explicitement, la branche commençant par when others, qui doit être la dernière, est choisie. Si les choix mentionnés explicitement (autres que others) couvrent toutes les valeurs possibles de l’expression, alors la branche commençant par when others est optionnelle. Le mot réservé null doit être utilisé dans une branche où aucune déclaration n’est effectuée. L’accès aux champs d’une branche se fait comme pour un champ habituel (sect. 7.3). Mais seuls les champs de la branche correspondant à la valeur du discriminant sont utilisables. Une tentative d’accès à un champ d’une autre branche lèvera Constraint_Error. Les identificateurs de tous les champs d’un article (avec ou sans partie variantes) doivent être différents. Une partie variantes peut être emboîtée dans une autre pourvu qu’elle se situe
ARTICLES À PARTIES VARIANTES
à la fin de la branche qui l’englobe.
245
246
11.5 EXERCICES 11.5.1
Chaînes de caractères de longueur variable
Déclarer un type article contraint à discriminants T_Chaine_Contraint pour représenter des chaînes de caractères de longueur variable mais d’au maximum 256 caractères. En faire de même mais avec un type article à discriminants T_Chaine_Non_Contraint cette fois non contraint. 11.5.2
Chaînes de caractères de longueur variable
Déclarer une variable du type T_Chaine_Contraint et une autre du type T_Chaine_Non_Contraint (exercice 11.5.1) puis leur affecter successivement à chacune les chaînes "Bonjour" et "Au revoir". 11.5.3
Paquetage de chaînes de caractères de longueur variable
Ecrire un paquetage de gestion de chaînes de caractères du type T_Chaine_Contraint et un autre pour le type T_Chaine_Non_Contraint
(exercice 11.5.1). Les opérations à disposition seront la lecture au clavier, l’affichage à l’écran, la concaténation, l’obtention de la longueur et les opérateurs de comparaison. 11.5.4
Matrices carrées
Reprendre le paquetage de gestion de matrices carrées créé dans les exercices 10.9.1 et 10.9.2, et transformer le type tableau T_Matrice en un type article à discriminants où le discriminant représente l’ordre de la matrice. Modifier en conséquence le reste (corps) du paquetage, ainsi que le programme de test de l’exercice 10.9.3. 11.5.5
Fiches personnelles
Reprendre le paquetage de gestion de fiches personnelles créé dans les exercices 10.9.1 et 10.9.2, et transformer à votre guise le type des fiches en un type article à discriminants avec partie variantes (les champs sexe et état civil sont de bons candidats pour devenir des discriminants). Modifier en conséquence le reste (corps) du paquetage, ainsi que le programme de test de l’exercice 10.9.3.
POINTS À RELEVER
247
11.6 POINTS À RELEVER 11.6.1
En Ada • Les types articles à discriminants permettent d’obtenir des articles de même
type mais de constitution variable. • Un discriminant est un champ jouant un rôle particulier. • Un discriminant peut servir à préciser la valeur initiale d’un champ, une
borne d’un champ tableau, ou à introduire une partie variantes. • Un type article à discriminant peut être contraint ou non. • Un article à discriminant d’un type non contraint peut être contraint ou non. • La valeur d’un discriminant ne peut être modifiée que par une modification
globale de tous les champs de l’article. • Seuls les champs définis peuvent être accédés, sinon Constraint_Error
sera levée.
248
C H A P I T R E
FICHIER
1 2
249
FICHIERS
MOTIVATION
250
12.1 MOTIVATION Les structures de données que nous avons déjà vues ont un point commun: elles résident toutes dans la mémoire principale de l’ordinateur. Ceci signifie que l’effacement (volontaire ou non!) de la mémoire provoque la destruction de ces structures et de leur contenu, ainsi d’ailleurs que celle du programme les utilisant. De plus, il peut être nécessaire de conserver des données à la fin de l’application qui les a créées, ceci en prévision d’une utilisation future. Par exemple, l’utilisation d’un traitement de texte conduit à la manipulation de contenus lus et écrits sur supports magnétiques ou autres. Ces quelques considérations nous amènent à introduire la notion de fichier.
NOTION DE FICHIER
251
12.2 NOTION DE FICHIER Hors du monde informatique un fichier est une collection de fiches; chacun de nous a peut-être manipulé un fichier dans une bibliothèque, une administration, etc. Ces fichiers se caractérisent par un nombre quelconque de fiches en général toutes de même aspect. Pour trouver une fiche particulière, il faut les parcourir une à une ou utiliser une clé d’accès si le fichier est trié selon cette clé (ordre alphabétique, ordre de cotation de livres...). En informatique, on définit qu’un fichier (file) est une structure composée de données dont le nombre n’est pas connu a priori et qui réside en mémoire secondaire. L’accès à un élément (à une donnée) du fichier peut se faire • séquentiellement (sequential access), c’est-à-dire en parcourant le fichier
élément par élément depuis le début jusqu’au moment où, par exemple, un élément cherché est trouvé; • directement (direct access), en donnant la position d’un élément; • selon une clé (access by key), chaque valeur de la clé désignant un élément particulier et permettant donc d’obtenir l’élément désiré; mais le traitement de ce dernier type d’accès dépasse les objectifs de cet ouvrage. Comme les fichiers sont conservés en mémoire secondaire (disques et bandes magnétiques, disquettes, cassettes, disques compacts, DVD...), ils subsistent tant que cette mémoire n’est pas effacée ou endommagée. Chaque fichier est désigné par un nom et possède des attributs tels que date de création, taille, icône, extension... Ils se répartissent en deux catégories: • les fichiers (de) texte (text files, appelés aussi imprimables) contenant des
caractères et susceptibles d’être lus par un être humain, édités, imprimés... (sect. 12.3); • les fichiers binaires (binary files), qui contiennent du code binaire représentant chaque élément (sect. 12.4); ces fichiers ne doivent être manipulés que par des programmes! La différence entre les deux catégories peut être illustrée par un exemple simple. Un fichier binaire contenant la valeur 75 comportera un mot (supposons 2 octets) formé des bits 00000000 01001011 représentant l’entier 75, alors que dans un fichier texte ce même nombre sera écrit comme les deux caractères '7' et '5' accolés, implémentés par les codes (LATIN-1, § 3.8.1) 00110111 et 00110101. En Ada comme dans d’autres langages, on distingue les termes fichier (file object) et fichier externe (external file). Un fichier est une variable d’un programme destinée à désigner un fichier externe. Un fichier externe est un objet extérieur au programme, conservé en mémoire secondaire et désigné par un nom. Il est fréquent, dans le langage parlé, d’appeler indistinctement fichier les deux types d’entités.
FICHIERS TEXTE
252
12.3 FICHIERS TEXTE 12.3.1
Généralités
Les fichiers texte constituent un cas particulier des fichiers séquentiels car ils sont formés d’éléments bien connus: les caractères. Chacun a déjà manipulé de tels fichiers: un programme source (Ada ou autre) est en fait un fichier texte! Les caractères contenus dans un fichier texte sont organisés en lignes, chacune terminée par une fin de ligne. Après la dernière ligne, le fichier se termine; cet endroit particulier est appelé fin de fichier et représente une limite à ne jamais dépasser lors de la lecture des caractères sous peine de voir levée l’exception End_Error (sect. 12.5). En Ada, le traitement des fichiers texte s’effectue grâce au paquetage prédéfini Ada.Text_IO (sect. 19.3) dans lequel sont définis les types, sous-types, procédures, fonctions et exceptions nécessaires. Les notions de base pour la manipulation d’un fichier seront donc présentées en utilisant Ada.Text_IO. Un fichier texte se déclare comme une variable habituelle (exemple 12.1), en utilisant le type File_Type qui est particulier dans le sens où sa structure est cachée à l’utilisateur. De tels types (privés) feront l’objet d’une présentation ultérieure (sect. 16.2). Il suffit d’indiquer ici qu’ils permettent essentiellement la déclaration de variables et de paramètres. La seule opération permise sur les fichiers est le passage en paramètre. L’affectation est en effet interdite entre des fichiers (car ils sont d’un type limité, sect. 16.9). Par conséquent il est impossible de déclarer une constante d’un type fichier. Les expressions se réduisent aux variables d’un type fichier. Comme dans le cas des tableaux, l’intérêt principal des fichiers réside en l’utilisation de leurs éléments (§ 12.3.3). Il faut encore préciser que les deux paragraphes précédents s’appliquent à tous les fichiers et pas seulement aux fichiers texte. Exemple 12.1 Déclarations de fichiers comme variables et paramètres. with Ada.Text_IO; -- ... procedure Exemple_12_1 is Original : Ada.Text_IO.File_Type; -- Deux variables fichiers Copie : Ada.Text_IO.File_Type; -- texte use Ada.Text_IO; File_Type
-- Pour eviter de prefixer le type
------------------------------------------------------------- Cree une copie de Source, copie designee par Destination procedure Dupliquer ( Source: in File_Type;
FICHIERS TEXTE
253
Destination: in File_Type) is begin -- Dupliquer ... end Dupliquer; -----------------------------------------------------------begin -- Exemple_12_1 ...
12.3.2
Création, ouverture et fermeture d’un fichier texte
En Ada un fichier texte peut être lu, écrit ou complété à la fin. On dit que le fichier est utilisé en mode lecture, en écriture ou en adjonction. La lecture (read) consiste à consulter le contenu sans le changer, alors que l’écriture (write) permet de créer un nouveau contenu (en effaçant l’ancien s’il existait). Finalement l’adjonction (append) laisse le contenu présent tel quel mais permet de rajouter des informations à la fin du fichier. L’ouverture (open) d’un fichier consiste entre autres à associer une variable fichier à un fichier externe et à choisir le mode d’utilisation (lecture, écriture, adjonction) du fichier. Ceci se fait par la procédure Create si le fichier est nouveau ou par Open s’il existe déjà. Ces deux procédures ont les en-têtes (semblables) suivantes: procedure Create (
File Mode Name Form
: : : :
in in in in
out File_Type; File_Mode := Out_File; String := ""; String := "" );
procedure Open (
File Mode Name Form
: : : :
in in in in
out File_Type; File_Mode; String; String := "" );
avec • File le fichier créé s’il est nouveau ou simplement ouvert s’il existait déjà; • Mode le mode d’utilisation (d’accès) du fichier, par défaut en écriture lors
de la création; • File_Mode un type énumératif déclaré dans Ada.Text_IO et fournissant les valeurs énumérées In_File (lecture), Out_File (écriture) et Append_File (adjonction); • Name le nom du fichier externe désigné par File; • Form un paramètre permettant de fixer des propriétés du fichier externe
comme par exemple les droits d’accès. La valeur par défaut du paramètre Name de la procédure Create permet la création d’un fichier temporaire qui sera automatiquement effacé à la fin du
FICHIERS TEXTE
254
programme. Par ailleurs, la valeur par défaut du paramètre Form permet l’utilisation des options courantes de l’implémentation, souvent celles du système d’exploitation utilisé. La fermeture (close) d’un fichier consiste en la suppression de l’association (réalisée à l’ouverture) entre un fichier et un fichier externe. Elle est effectuée par la procédure Close: procedure Close ( File : in out File_Type );
avec • File le fichier qui doit être fermé.
C’est au plus tard lors de la fermeture d’un fichier que son contenu est modifié en fonction des opérations réalisées sur le fichier. 12.3.3
Accès aux éléments d’un fichier texte
Les procédures Put et Get, utilisées pour la lecture et l’écriture de données d’un type scalaire [ARM 3.2], de même que celles fournissant d’autres services comme Put_Line, Get_Line, New_Line ou Skip_Line sont utilisables avec n’importe quel fichier texte. Il suffit de rajouter le nom de la variable fichier comme premier paramètre pour que l’opération s’effectue sur le fichier (exemple 12.2). Comme déjà mentionné il faut cependant disposer des paquetages d’entréessorties adéquats. Exemple 12.2 Lecture et écriture dans un fichier texte. with Ada.Text_IO; use Ada.Text_IO; with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; with Ada.Float_Text_IO; use Ada.Float_Text_IO; -- ... procedure Exemple_12_2 is Fichier_Traite : File_Type;
-- Une variable fichier texte
Titre : String ( 1..10 );
-- Une variable chaine de -- caracteres
Lettre : Character; Nb_Entier : Integer; Nb_Reel : Float;
-- Une variable caractere -- Une variable entiere -- Une variable reelle
L : Natural;
-- Pour un appel correct a Get_Line
begin -- Exemple_12_2 -- Creation et ouverture du fichier a ecrire appele texte.txt Create ( File => Fichier_Traite, Name => "texte.txt" ); Put_Line ( Fichier_Traite, "Titre" ); -- Ecriture sur une ligne Put ( Fichier_Traite, 'O' ); Put ( Fichier_Traite, 123 ); Put ( Fichier_Traite, 'N' );
-- Ecriture d'un caractere -- Ecriture d'un entier -- Ecriture d'un caractere
FICHIERS TEXTE
255
New_Line ( Fichier_Traite );
-- Passage a la ligne
Put ( Fichier_Traite, 3.14 ); New_Line ( Fichier_Traite );
-- Ecriture d'un reel -- Passage a la ligne
Close ( Fichier_Traite );
-- Fermeture du fichier
-- Ouverture du meme fichier, cette fois-ci en lecture Open ( File => Fichier_Traite, Mode => In_File, Name => "texte.txt" ); -- Lecture du titre (L vaut 5) Get_Line ( Fichier_Traite, Titre, L); Get ( Fichier_Traite, Lettre );
--Get ( Fichier_Traite, Nb_Entier ); --Get ( Fichier_Traite, Lettre ); --Skip_Line ( Fichier_Traite ); --
Lecture (O) Lecture (123) Lecture (N) Passage
d'un caractere d'un entier d'un caractere a la ligne
Get ( Fichier_Traite, Nb_Reel ); Skip_Line ( Fichier_Traite );
-- Lecture d'un reel (3.14) -- Passage a la ligne
Close ( Fichier_Traite ); ...
-- Fermeture du fichier
L’exemple 12.2 est naïvement écrit car le traitement des fichiers texte est plus structuré que cela (§ 12.3.4). Son seul but est d’illustrer l’utilisation des procédures d’entrées-sorties de base lorsqu’elles s’appliquent à un fichier texte. Figure 12.1 Le fichier texte.txt créé par le programme de l’exemple 12.2.
Titre O123N 3.14000E+00
12.3.4
Traitement d’un fichier texte
Le traitement d’un fichier texte au sens le plus général consiste en la lecture, l’utilisation du contenu et l’écriture d’un (nouveau) fichier texte, la lecture et l’écriture s’effectuant séquentiellement, un élément après l’autre sans saut ni retour en arrière. Il faut rappeler que ce contenu est formé de lignes de caractères (terminées par une fin de ligne) et, qu’après la dernière ligne, le fichier se termine (fin du fichier). Le traitement des caractères consiste en fait à les lire ou à les écrire comme des caractères individuels, comme des groupes constituant une ou plusieurs chaînes de caractères ou encore à les interpréter comme des mots ou des nombres.
FICHIERS TEXTE
256
Par exemple, la suite –12E3 arbitrairement choisie forme une séquence de cinq caractères individuels ou une chaîne de cinq caractères, mais elle peut aussi être interprétée différemment comme le nombre entier –12 suivi d’une lettre et finalement d’un deuxième nombre entier, ou encore former un seul nombre entier négatif. C’est la signification que l’on désire donner au contenu qui déterminera la manière de lire (ou d’écrire) l’information d’un fichier texte. Habituellement une lecture correcte doit tenir compte des fins de lignes (si celles-ci jouent un rôle particulier) et de la fin du fichier afin de ne pas essayer de lire au-delà, ce qui provoquerait inévitablement une erreur. Pour cela le programmeur peut utiliser les deux fonctions booléennes End_Of_Line et End_Of_File définies dans Ada.Text_IO. Elles ont les en-têtes: function End_Of_Line ( File : in File_Type ) return Boolean; function End_Of_File ( File : in File_Type ) return Boolean;
avec • File le fichier traité, ouvert en lecture.
La fonction End_Of_Line retourne la valeur True si une fin de ligne précède le prochain caractère à lire, False dans tous les autres cas. La fonction End_Of_File retourne la valeur True si la fin de fichier est atteinte, False sinon. Pour illustrer l’utilisation de ces fonctions, l’exemple 12.3 présente un algorithme typique de lecture caractère à caractère d’un fichier texte. Exemple 12.3 Lecture caractère à caractère d’un fichier texte. with Ada.Text_IO; use Ada.Text_IO; -- ... procedure Exemple_12_3 is Fichier_Traite : File_Type; Lettre : Character;
-- Une variable fichier texte -- Une variable caractere
begin -- Exemple_13_3 -- Ouverture du fichier a lire Open ( File => Fichier_Traite, Mode => In_File, Name => "texte.txt" ); -- Fin de fichier? while not End_Of_File ( Fichier_Traite ) loop -- Fin de ligne? while not End_Of_Line ( Fichier_Traite ) loop Get ( Fichier_Traite, Lettre ); -- Lecture d'un caractere ...;
-- Traitement du caractere lu
end loop; Skip_Line ( Fichier_Traite ); end loop;
-- Passage a la ligne
FICHIERS TEXTE
Close ( Fichier_Traite ); ...
257
-- Fermeture du fichier
La suite des caractères lus et traités est donnée par la figure 12.2. Lors de l’écriture dans un fichier texte, c’est naturellement le passage à la ligne qui termine une ligne et la fermeture du fichier qui provoque la fin du fichier par l’intermédiaire du système d’exploitation. Figure 12.2 Caractères lus par le programme de l’exemple 12.3 ( représente un espace).
T i t r e O1 2 3 N3 . 1 4 0 0 0 E + 00
12.3.5
Remarques sur l’utilisation de Ada.Text_IO
Le paquetage Ada.Text_IO offre quelques facilités pour des applications interactives. Il s’agit des procédures Get_Immediate et Look_Ahead. La procédure Get_Immediate permet de lire le caractère suivant immédiatement, contrairement à Get qui nécessite l’introduction d’une fin de ligne au clavier pour fournir le caractère. Elle possède deux formes: procedure Get_Immediate ( procedure Get_Immediate (
Item: out Character ); Item: out Character; Available: out Boolean );
avec • Item le caractère lu immédiatement; • Available qui indique s’il existe un caractère à lire immédiatement.
La première forme fait attendre le programme si aucun caractère n’est disponible. La seconde retourne la valeur False dans Available si aucun caractère n’est disponible et l’exécution du programme se poursuit. La procédure Look_Ahead permet d’obtenir une copie du caractère suivant sans éliminer l’original de la suite de caractères à lire. Un tel caractère sera donc encore présent pour une future lecture. Elle a l’en-tête: procedure Look_Ahead (
Item : out Character; End_Of_Line : out Boolean );
avec • Item le caractère obtenu si End_Of_Line est faux; • End_Of_Line qui est vrai si la fin de ligne est atteinte. Dans ce cas Item
n’a pas de valeur définie.
FICHIERS TEXTE
258
Finalement, il faut encore effectuer les remarques suivantes: • Il existe deux fichiers courants, l’un ouvert en lecture et l’autre en écriture.
En fait ces deux fichiers représentent par défaut le clavier (lecture) et l’écran (écriture). Des opérations comme Put et Get, sans paramètre fichier, s’appliquent à ces fichiers. • Les entrées-sorties textuelles s’effectuent de la même manière sur des fichiers qu’avec le clavier et l’écran. Des formatages (§ 2.6.7) sont par conséquent aussi possibles sur des valeurs écrites dans les fichiers texte. • Il existe les notions de numéros de ligne et de colonne qui ne seront pas développées ici. • Plusieurs exceptions prédéfinies peuvent être générées lors de l’utilisation de fichiers textes ou binaires. Elles sont décrites plus loin (sect. 12.5).
FICHIERS BINAIRES
259
12.4 FICHIERS BINAIRES 12.4.1
Généralités
Comme déjà mentionné (sect. 12.2), les fichiers binaires comprennent tous les fichiers non textuels. Leur contenu peut être vu comme une suite de bits constituant les éléments du fichier placés les uns après les autres. Comme pour les fichiers texte, la fin du fichier se situe après le dernier élément. En Ada, le traitement des fichiers binaires s’effectue grâce aux paquetages génériques prédéfinis Ada.Sequential_IO (sect. 19.3) pour un accès séquentiel (sect. 12.2) et Ada.Direct_IO (sect. 19.3) pour un accès direct (sect. 12.2), paquetages dans lesquels sont définis les types, sous-types, procédures, fonctions et exceptions nécessaires. Une fois encore les notions de base pour la manipulation d’un fichier binaire seront présentées de façon intuitive. Un fichier se déclare comme une variable, en utilisant le type File_Type dont la structure est cachée à l’utilisateur. De tels types (privés) feront l’objet d’une présentation ultérieure (§ 16.2.1). Il suffit de rappeler ici qu’ils permettent essentiellement la déclaration de variables et de paramètres. Tout ceci est identique à la situation rencontrée pour les fichiers texte. Mais la manipulation d’un fichier binaire nécessite la connaissance du type des éléments du fichier. Celle-ci est obtenue grâce au fait que les deux paquetages Ada.Sequential_IO et Ada.Direct_IO sont génériques (sect. 17.2). Ils permettent de créer des paquetages non génériques par des déclarations mentionnant le type des éléments (exemple 12.4) et semblables à celles nécessaires pour disposer des entrées-sorties sur des types numériques (sect. 6.2) ou énumératifs (§ 5.2.5). Pour un fichier séquentiel, les éléments peuvent être de n’importe quel type alors qu’il existe une restriction pour ceux des fichiers à accès direct (§ 12.4.7). Exemple 12.4 Déclarations de fichiers comme variables et paramètres. with Ada.Sequential_IO; with Ada.Direct_IO; -- ... procedure Exemple_12_4 is
-- Pas de use sur un paquetage qui -- est generique (sect. 17.2)
type T_Mois_De_L_Annee is (Janvier, Fevrier, Mars, Avril, Mai, Mai, Juin, Juillet, Aout, Septembre, Octobre, Novembre, Decembre); type T_Jour is range 1..31; type T_Date is record Jour : T_Jour; Mois : T_Mois_De_L_Annee; Annee : Integer;
FICHIERS BINAIRES
260
end record; -- Pour utiliser des fichiers binaires sequentiels d'entiers package Entiers_Seq_IO is new Ada.Sequential_IO ( Integer ); use Entiers_Seq_IO; -- Pour utiliser des fichiers binaires sequentiels de dates package Dates_Dir_IO is new Ada.Direct_IO ( T_Date ); use Dates_Dir_IO; -- Deux variables fichiers (prefixe necessaire, § 10.5.2) Fichier_Entiers : Entiers_Seq_IO.File_Type; Fichier_Dates : Dates_Dir_IO.File_Type; -- Lit le fichier d'entiers Mesures procedure Lire (Mesures : in Entiers_Seq_IO.File_Type) is begin ... end Lire; -- Ecrit le fichier de dates Dates procedure Ecrire (Dates : in Dates_Dir_IO.File_Type) is begin ... end Ecrire; begin -- Exemple_12_4 ...
La seule opération permise sur les fichiers binaires est le passage en paramètre. L’affectation est en effet interdite entre ces fichiers (car ils sont d’un type limité, sect. 16.9). Par conséquent, il est impossible de déclarer une constante d’un type fichier. Les expressions se réduisent aux variables d’un type fichier. Comme dans le cas des tableaux l’intérêt principal des fichiers binaires réside en l’utilisation de leurs éléments (§ 12.4.3 et 12.4.5). 12.4.2
Création, ouverture et fermeture d’un fichier binaire
En Ada, un fichier binaire peut toujours être lu ou écrit. Il peut encore être complété à la fin s’il est séquentiel ou à la fois lu et écrit s’il est en accès direct. On dit que le fichier est ouvert respectivement en mode lecture, en écriture, en adjonction ou en lecture-écriture. La lecture (read) consiste à consulter le contenu sans le changer, alors que l’écriture (write) permet de créer un nouveau contenu (en effaçant l’ancien s’il existait). L’adjonction (append) laisse le contenu présent tel quel mais permet de rajouter des informations à la fin du fichier. Finalement la lecture-écriture (read-write) permet de lire, de modifier ou d’ajouter des informations n’importe où dans le fichier. Comme expliqué pour les fichiers texte, l’ouverture (open) d’un fichier binaire consiste entre autres à associer une variable fichier à un fichier externe et à choisir le mode d’utilisation (accès séquentiel ou
FICHIERS BINAIRES
261
direct, lecture, écriture, adjonction, lecture-écriture). Ceci se fait par la procédure Create si le fichier est nouveau ou par Open s’il existe déjà. Ces deux procédures ont les en-têtes (semblables): procedure Create (
File Mode Name Form
: : : :
in in in in
out File_Type; File_Mode := ...; String := ""; String := "" );
procedure Open (
File Mode Name Form
: : : :
in in in in
out File_Type; File_Mode; String; String := "" );
avec • File le fichier créé s’il est nouveau ou simplement ouvert s’il existait déjà; • Mode le mode d’utilisation du fichier; la valeur par défaut est Out_File pour Ada.Sequential_IO et Inout_File pour Ada.Direct_IO lors
de la création; • File_Mode un type énumératif fournissant les valeurs In_File (lecture), Out_File (écriture) et Append_File (adjonction) s’il est déclaré dans Ada.Sequential_IO et In_File (lecture), Inout_File (lectureécriture) et Out_File (écriture) s’il est déclaré dans Ada.Direct_IO; • Name le nom du fichier externe désigné par File; • Form un paramètre permettant de fixer des propriétés du fichier externe. La valeur par défaut du paramètre Name de Create permet la création d’un fichier temporaire qui sera automatiquement effacé à la fin du programme alors que celle du paramètre Form permet l’utilisation des options courantes de l’implémentation, souvent celles du système d’exploitation utilisé. La fermeture (close) d’un fichier binaire, comme pour un fichier texte, consiste en la suppression de l’association (réalisée à l’ouverture) entre un fichier et un fichier externe. Elle est effectuée par la procédure Close: procedure Close ( File : in out File_Type );
avec • File le fichier qui doit être fermé.
C’est au plus tard lors de la fermeture d’un fichier que son contenu est modifié en fonction des opérations réalisées sur le fichier. 12.4.3
Accès aux éléments d’un fichier binaire séquentiel
Les procédures Read et Write sont déclarées dans Ada.Sequential_IO et s’utilisent pour la lecture et l’écriture séquentielles de données du type des éléments du fichier. Elles ont les en-têtes: procedure Read ( File : in File_Type; Item : out Element_Type );
FICHIERS BINAIRES
262
procedure Write ( File : in File_Type; Item : in Element_Type );
avec • File le fichier séquentiel; • Item l’élément lu ou écrit. Exemple 12.5 Lecture et écriture dans un fichier binaire séquentiel. with Ada.Sequential_IO; -- ... procedure Exemple_12_5 is -- Pour utiliser des fichiers binaires sequentiels formes de -- dates, les types sont declares comme dans l'exemple 12.4 package Dates_Seq_IO is new Ada.Sequential_IO ( T_Date ); use Dates_Seq_IO; Fichier_Dates : File_Type;
-- Une variable fichier binaire
Date : T_Date;
-- Une date reelle
begin -- Exemple_12_5 -- Creation et ouverture du fichier a ecrire Create ( File => Fichier_Dates, Name => "dates.dat" ); Date := Write ( Write ( Write ( Write (
(1, Avril, 1958); Fichier_Dates, Date ); -- Ecriture de quelques dates Fichier_Dates, (26, Decembre, 1964) ); Fichier_Dates, (7, Septembre, 1991) ); Fichier_Dates, (28, Juillet, 1998) );
Close ( Fichier_Dates );
-- Fermeture du fichier
-- Ouverture du même fichier pour le relire Open ( File => Fichier_Dates, Mode => In_File, Name => "dates.dat" ); Read Read Read Read
( ( ( (
Fichier_Dates, Fichier_Dates, Fichier_Dates, Fichier_Dates,
Date Date Date Date
Close ( Fichier_Dates ); ...
); ); ); );
-----
Lecture des quatre dates dans l'ordre selon lequel elles viennent d'etre ecrites
-- Fermeture du fichier
L’exemple 12.5 illustre simplement l’utilisation des procédures Read et Write. Un traitement moins trivial d’un fichier binaire séquentiel est donné ci-après. 12.4.4
Traitement d’un fichier binaire séquentiel
Le traitement d’un fichier binaire séquentiel au sens le plus général consiste en la lecture, l’utilisation du contenu et l’écriture d’un (nouveau) fichier binaire. Le fichier se termine ici aussi après le dernier élément (fin du fichier). Une lecture correcte doit tenir compte de la fin du fichier afin de ne pas essayer de lire au-delà,
FICHIERS BINAIRES
263
ce qui provoquerait une erreur. Pour cela le programmeur peut utiliser la fonction booléenne End_Of_File définie dans Ada.Sequential_IO. Elle a l’en-tête: function End_Of_File ( File : in File_Type ) return Boolean;
avec • File le fichier traité, ouvert en lecture.
La fonction End_Of_File retourne la valeur True si la fin de fichier est atteinte, False sinon. Pour illustrer l’utilisation de cette fonction, l’exemple 12.6 présente un algorithme typique de traitement de fichiers binaires séquentiels et la figure 12.3 montre le résultat de cet algorithme. Exemple 12.6 Copie d’un fichier binaire séquentiel. with Ada.Sequential_IO; -- ... procedure Exemple_12_6 is -- Pour utiliser des fichiers binaires sequentiels formes de -- dates, les types sont declares comme dans l'exemple 12.4 package Dates_Seq_IO is new Ada.Sequential_IO ( T_Date ); use Dates_Seq_IO; Original : File_Type; Copie : File_Type; Date : T_Date; element
-- Deux variables fichiers binaires -- sequentiels -- Une variable pour la copie d'un
begin -- Exemple_12_6 -- Ouverture du fichier a lire et creation de la copie Open ( File => Original, Mode => In_File, Name => "dates.dat" ); Create ( File => Copie, Name => "copie de dates.dat" ); while not End_Of_File ( Original ) loop -- Fin de fichier? Read ( Original, Date ); -- Lecture d'une date Write ( Copie, Date ); -- Ecriture d'une date end loop; Close ( Original ); Close ( Copie ); ...
-- Fermeture des fichiers
Figure 12.3 Suite des dates copiées par le programme de l’exemple 12.6.
1 Avril 1958 26 Decembre 1964 7 Septembre 1991
FICHIERS BINAIRES
264
28 Juillet 1998
Lors de l’écriture dans un fichier binaire, c’est naturellement la fermeture du fichier qui provoque la fin du fichier par l’intermédiaire du système d’exploitation. 12.4.5
Accès aux éléments d’un fichier binaire à accès direct
Des procédures Read et Write sont aussi déclarées dans Ada.Direct_IO et s’utilisent pour la lecture et l’écriture de données du type des éléments du fichier. Ces opérations peuvent être séquentielles mais aussi, et c’est l’intérêt de cet accès, s’effectuer en fonction de la valeur d’un index. Cet index représente un nombre entier entre 1 et une borne maximale imposée par l’implémentation; il est du soustype Positive_Count construit sur le type de base Count. Toutes ces déclarations appartiennent à Ada.Direct_IO et sont définies ainsi: type Count is range 0 .. implementation_defined; subtype Positive_Count is Count range 1 .. Count'Last; procedure Read ( File : in File_Type; Item : out Element_Type ); procedure Read ( File : in File_Type; Item : out Element_Type; From : in Positive_Count ); procedure Write ( File : in File_Type; Item : in Element_Type ); procedure Write ( File : in File_Type;
Item : in Element_Type; To : in Positive_Count
);
avec • • • •
File le fichier à accès direct; Item l’élément lu ou écrit; From la valeur de l’index, c’est-à-dire la position de l’élément à lire; To la valeur de l’index, c’est-à-dire la position d’écriture de l’élément.
Dans tous les cas, l’index est augmenté de 1 après une lecture ou une écriture. Comme la valeur de l’index est arbitraire, il est possible d’effectuer des lectures ou des écritures n’importe où dans un fichier à accès direct! De plus, l’index peut être modifié sans passer par Read ou Write en utilisant la procédure Set_Index ou consulté par la fonction Index. Elles ont les en-têtes: procedure Set_Index (
File : in File_Type; To : in Positive_Count ); function Index ( File : in File_Type ) return Positive_Count;
avec • File le fichier à accès direct; • To la nouvelle valeur de l’index. Cette valeur peut dépasser la position du
dernier élément du fichier (§ 12.4.7)!
FICHIERS BINAIRES
12.4.6
265
Traitement d’un fichier binaire à accès direct
Toutes les notions présentées pour le traitement des fichiers binaires séquentiels sont identiques pour les fichiers à accès direct. L’exemple 12.7 et la figure 12.4 les illustrent en présentant l’algorithme de copie de l’exemple 12.6 modifié de manière à ce que la copie contienne les éléments de l’original mais dans l’ordre inverse. La fonction Size (§ 12.4.7) qui donne le nombre d’éléments d’un fichier est utilisée dans cet exemple. Exemple 12.7 Copie d’un fichier binaire à accès direct en inversant l’ordre des éléments. with Ada.Direct_IO; -- ... procedure Exemple_12_7 is -- Pour utiliser des fichiers binaires a acces direct formes -- de dates, les types sont declares comme dans l'exemple 12.4 package Dates_Dir_IO is new Ada.Direct_IO ( T_Date ); use Dates_Dir_IO; -- Deux variables fichiers a acces direct Original : File_Type; Copie : File_Type; Date : T_Date; element
-- Une variable pour la copie d'un
begin -- Exemple_12_7 -- Ouverture du fichier a lire et creation de la copie Open ( File => Original, Mode => In_File, Name => "dates.dat" ); Create ( File => Copie, Name => "copie de dates.dat" ); -- Copier tous les elements for No_Element_Courant in reverse 1 .. Size( Original ) loop -- Placer l'index sur l'element No_Element_Courant Set_Index ( Original, No_Element_Courant ); Read ( Original, Date ); Write ( Copie, Date );
-- Lecture d'une date -- Ecriture d'une date
end loop; Close ( Original ); Close ( Copie ); ...
-- Fermeture des fichiers
FICHIERS BINAIRES
266
Figure 12.4 Lecture et écriture des dates par le programme de l’exemple 12.7.
Après l’ouverture de dates.dat et la création de copie de dates.dat: 1 avril 195826 decembre 19647 septembre 1991 28 juillet 1998
Index de dates.dat Index de copie de dates.dat Après la première instruction Set_Index( Original, No_Element_Couran 1 avril 195826 decembre 19647 septembre 1991 28 juillet 1998
Index de dates.dat
Après la première itération:
1 avril 195826 decembre 19647 septembre 1991 28 juillet 1998
Index de copie de dates.dat
Index de dates.d
28 juillet 1998
Après la deuxième itération: 1 avril 195826 decembre 19647 septembre 1991 28 juillet 1998
Index de dates.dat Index de copie de dates.dat 28 juillet 19987 septembre 1991
Et ainsi de suite pour les deux dernières itérations.
La fin de la copie se produit lorsque le premier élément du fichier Original a été lu. Il faut noter que la lecture s'effectue en fonction de l'index alors que l'écriture est purement séquentielle. 12.4.7 direct
Compléments sur l’utilisation des fichiers binaires à accès
Lorsqu’une déclaration telle que package Dates_Dir_IO is new Ada.Direct_IO ( T_Dates );
est effectuée, il faut que le type mentionné, ici T_Dates, ne soit pas un type tableau non contraint (§ 8.2.3) ni un type article à discriminants sans valeurs par défaut (sect. 11.3). En fait, cette restriction est la même que pour le type des éléments d’un
FICHIERS BINAIRES
267
tableau (§ 8.2.8). De plus, il existe la fonction Size qui a l’en-tête: function Size ( File : in File_Type ) return Count;
Elle retourne le nombre d’éléments du fichier à accès direct File. Comme le positionnement de l’index après la fin du fichier est possible, à cet endroit une lecture lèvera l’exception End_Error (sect. 12.5) alors qu’une écriture créera un élément bien constitué. De plus, entre celui-ci et l’élément qui était le dernier, le fichier est complété par des éléments de contenu non défini. Le fichier ne peut de ce fait jamais contenir de trous. Un fichier à accès direct ressemble donc à un tableau dont on peut augmenter la taille. 12.4.8
Compléments sur l’utilisation des fichiers
Pour n’importe laquelle des trois catégories de fichiers, le paquetage correspondant founit d’autres outils comme les procédures Delete et Reset ou les fonctions Is_Open, Mode et Name. La procédure Delete ferme le fichier et l’efface de la mémoire secondaire. Elle possède l’en-tête: procedure Delete ( File : in out File_Type );
avec • File le fichier à effacer.
La procédure Reset positionne («rembobine») le fichier de manière à ce que la lecture recommence au premier élément pour les modes lecture et lecture-écriture, ou que l’écriture se fasse au début pour les modes lecture-écriture et écriture, ou finalement que l’écriture reprenne après le dernier élément pour le mode adjonction. Elle possède deux formes: procedure Reset ( File : in out File_Type ); procedure Reset ( File : in out File_Type; Mode : in File_Mode);
avec • File le fichier à repositionner; • Mode le nouveau mode du fichier.
La deuxième forme permet de changer le mode du fichier, par exemple pour relire les éléments d’un fichier qui viennent d’être écrits. Elle aurait donc pu être utilisée dans les exemples 12.2 et 12.5. Par ailleurs, pour un fichier à accès direct, Reset a aussi pour effet de positionner l’index à la valeur 1. La fonction Is_Open est vraie si le fichier File est ouvert, fausse sinon. Elle a l’en-tête: function Is_Open ( File : in File_Type ) return Boolean;
La fonction Mode redonne le mode d’ouverture du fichier File. Elle possède l’en-tête: function Mode ( File : in File_Type ) return File_Mode;
FICHIERS BINAIRES
268
La fonction Name fournit le nom du fichier externe connecté au fichier File. Elle a l’en-tête: function Name ( File : in File_Type ) return String;
Il existe encore d’autres opérations qui ne seront pas abordées dans cet ouvrage.
EXCEPTIONS LORS DE L’UTILISATION DES FICHIERS
269
12.5 EXCEPTIONS LORS DE L’UTILISATION DES FICHIERS Toutes les opérations de traitement des fichiers ou de leurs éléments peuvent générer des exceptions. Celles-ci sont toutes déclarées dans le paquetage Ada.IO_Exceptions et surnommées (sect. 19.1) dans tous les paquetages d’entrées-sorties. Cela signifie que leurs identificateurs sont utilisables de la même manière que tous ceux déclarés dans Ada.Text_IO, Ada.Sequential_IO ou encore Ada.Direct_IO. Le bref résumé qui suit [BAR 97] donne une idée générale des circonstances provoquant la levée de ces exceptions: Status_Errorle fichier est ouvert alors qu’il devrait être fermé ou viceversa; Mode_Errorle fichier n’a pas le bon mode, par exemple In_File au lieu de Out_File; Name_Errorune erreur est apparue dans le nom externe de fichier passé en paramètre de Create ou Open; Use_Errordiverses raisons (!); paramètre Form inacceptable, ordre d’impression sur un périphérique d’entrée, etc.; Device_Errorle dispositif physique est en panne ou n’est pas connecté; End_Errortentative de lecture au-delà de la fin de fichier; Data_ErrorRead ou Get ne peut pas interpréter les données comme valeurs du type désiré; Layout_Errorun problème de formatage dans Ada.Text_IO, ou bien Put écrit trop de caractères dans une chaîne. Le manuel de référence [ARM A.6-A.13] donne, pour chaque opération d’entrée-sortie, les exceptions levées en fonction de l’erreur commise.
270
12.6 AUTRES PAQUETAGES D’ENTRÉES-SORTIES Il faut encore mentionner, à titre informatif et sans approfondissement, l’existence de paquetages très spécifiques comme Ada.Streams.Stream_IO et Ada.Storage_IO. Le paquetage Ada.Streams.Stream_IO permet de traiter séquentiellement des fichiers hétérogènes considérés comme des flots, c’est-à-dire comme des suites d’octets. Ada.Storage_IO permet, lui, les lectures et écritures directes en mémoire.
EXERCICES
271
12.7 EXERCICES 12.7.1
Traitement d’un fichier texte
Reprendre les exercices 9.4.2 à 9.4.4 et adapter les solutions de manière à lire les lignes dans un fichier texte. 12.7.2
Majuscules et minuscules
Modifier (si nécessaire) la casse des caractères d’un fichier texte de manière à respecter les règles habituelles en présence de symboles de ponctuation. On suppose qu’il n’existe pas de noms propres dans le texte. 12.7.3
Dates et fichier texte
Soit un fichier texte formé de lignes contenant chacune une date (numéro de jour, nom du mois, année). Récrire le fichier en faisant précéder chaque date par le nom du jour (on peut partiellement utiliser la solution de l’exercice 8.6.9). 12.7.4
Justification d’un fichier texte
Reprendre l’exercice 9.4.5 et adapter la solution de manière à justifier toutes les lignes d’un fichier texte. 12.7.5
Fiches personnelles et fichier binaire
Adapter la solution de l’exercice 11.5.5 (ou des exercices 10.9.1 et 10.9.2) de manière à utiliser un fichier binaire (séquentiel ou à accès direct) pour lire et écrire les fiches. 12.7.6
Existence d’un fichier
Ecrire une fonction Is_Created qui sera vraie si le fichier externe (dont le nom est passé en paramètre) existe, fausse sinon. Indication: ouvrir le fichier et traiter l’exception Name_Error.
POINTS À RELEVER
272
12.8 POINTS À RELEVER 12.8.1
En général • Les fichiers servent à conserver l’information, essentiellement sur support
externe. • Les fichiers se répartissent en deux catégories: les fichiers binaires et les
fichiers texte (appelés aussi fichiers ASCII). • Les accès séquentiel, direct et selon une clé sont les accès aux fichiers les
plus courants. • La création, l’ouverture, la lecture, l’écriture et la fermeture sont des
opérations communes à tous les fichiers. 12.8.2
En Ada • L’accès aux fichiers texte s’effectue par le paquetage Ada.Text_IO. • Il ne faut jamais dépasser la fin d’un fichier texte sinon l’exception End_Error sera levée. • L’accès aux fichiers binaires s’effectue par les paquetages Ada.Sequential_IO et Ada.Direct_IO mais cet accès nécessite une
ou plusieurs déclarations spéciales. • L’adjonction est une opération supplémentaire pour les fichiers binaires
séquentiels. • La lecture-écriture est une opération supplémentaire pour les fichiers
binaires à accès direct. • Le paquetage Ada.IO_Exceptions regroupe les exceptions en relation
avec le traitement des fichiers.
273
C H A P I T R E
CRÉATIO N ET TRAITEM
1 3
274
CRÉATION ET TRAITEMENT D’EXCEPTIONS
RAPPELS ET MOTIVATION
275
13.1 RAPPELS ET MOTIVATION La notion d’exception a été abordée relativement tôt (sect. 6.3) afin d’expliquer les phénomènes issus d’erreurs lors de l’exécution d’un programme. Les concepts de levée (automatique), de propagation et de traitement d’exceptions ont été présentés en détails. On rappellera simplement que: • une quelconque erreur d’exécution provoquera la levée d’une exception qui
interrompra le cours normal du déroulement du programme; • une exception peut être traitée dans un traite-exception placé après la
dernière instruction d’un sous-programme, d’un bloc, d’un code d’initialisation de paquetage, d’une tâche, d’un corps d’entrée ou d’une instruction accept; ces trois derniers cas sont mentionnés à titre indicatif puisque n’appartenant pas à la matière traitée dans cet ouvrage; • si une exception n’est pas traitée et supprimée par un traite-exception, elle
est propagée à l’appelant et levée à nouveau au point d’appel. L’intérêt de cette notion réside en la possibilité de prévoir et de programmer des traitements aboutissant au rétablissement de la situation en cas d’erreur ou, au moins, explicitant les causes de l’apparition de telles erreurs. Or, compter sur des exceptions prédéfinies (§ 6.3.2 et sect. 12.5) pour détecter et réagir à des situations exceptionnelles mais connues, est une mauvaise pratique en programmation. En effet, comment s’assurer que la levée d’une telle exception est provoquée par une situation prévue et non par autre chose qui s’est mal passé? Une bonne habitude consiste à traiter les situations exceptionnelles prévisibles en créant ses propres exceptions et en les levant lorsque c’est nécessaire. En écrivant des traite-exceptions appropriés pour ces exceptions, il est alors possible de réagir précisément et de manière adéquate en fonction de l’erreur survenue.
DÉCLARATION D’UNE EXCEPTION
276
13.2 DÉCLARATION D’UNE EXCEPTION Une exception peut être déclarée (exemple 13.1) dans n’importe quelle partie déclarative sous la forme générale suivante: suite_d_identificateurs : exception;
avec • la suite_d_identificateurs formée d’un ou de plusieurs identi-
ficateurs d’exceptions séparés par des virgules; • exception le mot réservé utilisé ici pour donner la nature de l’identificateur. Les règles de visibilité sont les mêmes pour une exception que pour n’importe quelle constante ou variable. Exemple 13.1 Déclaration d’une exception. -- Ce paquetage permet le calcul avec les nombres rationnels. -- La specification est completee par la declaration d'une -- exception package Nombres_Rationnels is type T_Rationnel is -- Le type d'un nombre rationnel record Numerateur : Integer; Denominateur : Positive; -- Le signe est au numerateur end record; -- Le nombre rationnel 0 Zero : constant T_Rationnel := (0, 1); -- Exception levee si division par 0 Division_Par_0 : exception; -----------------------------------------------------------... -- Autres declarations (sect. 10.7) end Nombres_Rationnels;
LEVÉE ET PROPAGATION D’UNE EXCEPTION
277
13.3 LEVÉE ET PROPAGATION D’UNE EXCEPTION Si la propagation (§ 6.3.3) s’effectue de manière identique pour n’importe quelle exception (prédéfinie ou non), la levée d’une exception non prédéfinie ne peut pas avoir lieu automatiquement mais doit être effectuée par une instruction particulière, l’instruction raise. Sa forme générale est: raise nom_d_exception;
où • nom_d_exception désigne l’exception levée.
L’exécution de cette instruction termine la suite d’instructions en cours et provoque la levée de l’exception mentionnée. N’importe quelle exception visible peut être levée par l’instruction raise. L’emploi de cette instruction est illustré dans l’exemple 13.2. Exemple 13.2 Levée explicite d’une exception par l’instruction raise. -- Ce paquetage permet le calcul avec les nombres rationnels. Le -- corps de l'operateur de division est complete pour lever une -- exception en cas de division par 0 package body Nombres_Rationnels is ... -- Premiers operateurs (sect. 10.7) -- Division de deux nombres rationnels. Le resultat est un -- nombre rationnel irreductible function "/" ( X, Y : T_Rationnel ) return T_Rationnel is begin -- "/" if Y.Numerateur > 0 then return Irreductible(
-- Diviseur positif X.Numerateur * Y.Denominateur, X.Denominateur * Y.Numerateur );
elsif Y.Numerateur < 0 then return Irreductible( Y.Denominateur),
-- Changer de signe
X.Numerateur * (– X.Denominateur * (–Y.Numerateur)
); else -- Division par zero! raise Division_Par_0;
-- Lever l'exception
end if; end "/"; -----------------------------------------------------------... -- Autres operations (sect. 10.7) end Nombres_Rationnels; -- Ce paquetage complete le calcul avec les nombres rationnels. -- Le corps de l'operateur de division est complete pour lever une
LEVÉE ET PROPAGATION D’UNE EXCEPTION
278
-- exception en cas de division par 0 package body Nombres_Rationnels.Utilitaires is ... -- Premiers operateurs (§ 10.8.1) ------------------------------------------------------------- Division d'un nombre rationnel par un nombre entier function "/" ( X : T_Rationnel; N : Integer ) return T_Rationnel is begin if N > 0 then
-- Diviseur positif
return ( X.Numerateur, N * X.Denominateur ); elsif N < 0 then
-- Le signe au numerateur
return ( – X.Numerateur, abs N * X.Denominateur ); else -- Division par zero! raise Division_Par_0;
-- Lever l'exception
end if; end "/"; -----------------------------------------------------------... -- Autres operations (§ 10.8.1) end Nombres_Rationnels.Utilitaires;
L’exemple 13.2 illustre parfaitement l’utilité des exceptions: que doit retourner une fonction de division si le diviseur est nul? L’exception levée dans une telle fonction ne peut être traitée que par l’appelant. C’est parfaitement cohérent puisque c’est l’appelant qui a transmis le diviseur nul à la fonction, à lui de rétablir la situation s’il le peut! Un cas intéressant est représenté par la propagation d’une exception hors de sa portée (sect. 4.4), par exemple si elle est propagée hors d’une procédure où elle était (maladroitement) déclarée. Lors de la sortie de sa portée, l’exception continue d’exister mais perd son identificateur et devient anonyme; la seule façon de la traiter est d’utiliser le choix others dans un traite-exception! Inutile de préciser que ce genre de cas est à éviter absolument.
TRAITEMENT D’UNE EXCEPTION
279
13.4 TRAITEMENT D’UNE EXCEPTION Le traitement d’une exception est identique pour n’importe quelle exception. Mais la présentation des notions de base (§ 6.3.4) va être complétée par quelques aspects pratiques. Tout d’abord il existe la forme simplifiée raise; sans mention explicite d’exception. Cette forme ne peut être utilisée que dans un traite-exception et sert à lever à nouveau l’exception dont le traitement est en cours. Cela permet de passer par le traite-exception, par exemple pour afficher un message ou pour quitter le plus correctement possible la structure actuelle, et de laisser l’exception se propager. L’exemple 13.3 présente cette situation. Exemple 13.3 Utilisation de l’instruction raise sans mention explicite d’exception. ... exception when Constraint_Error => ...
-- Preparer la sortie de la structure actuelle
raise; -- Lever Constraint_Error a nouveau pour la propager -- On suppose que E1 et E2 sont deux exceptions when E1 | E2 => Put_Line ("Exception levee dans cette structure"); raise; -- Lever l'exception qui a provoque l'arrivee dans le -- traite-exception, c'est-a-dire E1 ou E2 end ...;
Le paquetage Ada.Exceptions [ARM 11.4.1], à ne pas confondre avec Ada.IO_Exceptions, permet d’obtenir des précisions concernant l’exception en cours grâce à trois fonctions qui retournent chacune une chaîne de caractère de type String. Ces trois fonctions sont les suivantes: • Exception_Name, qui rend le nom de l’exception courante; • Exception_Message, qui produit un message d’une ligne en relation
avec l’exception; • Exception_Information, qui fournit le nom de l’exception, le message
et d’autres informations. Le message et les autres informations dépendent de l’implémentation et doivent servir à identifier la cause et l’endroit de la levée de l’exception. Ces trois fonctions nécessitent chacune un paramètre permettant d’accéder à l’exception. Sans expliquer complètement le mécanisme, il suffit de dire qu’une branche d’un traite-
TRAITEMENT D’UNE EXCEPTION
280
exception peut comporter une sorte de paramètre qui contiendra l’identité de l’exception survenue et qui pourra être transmis aux fonctions. De cette manière, un traite-exception peut produire des traces (exemple 13.4), c’est-à-dire des informations textuelles que le programmeur va utiliser pour comprendre les raisons de l’exception. Une telle branche va toujours comporter un choix (§ 6.3.4) mais complété de la manière suivante: when identificateur : choix => traitement;-- branche avec parametre when identificateur : others => traitement;-- branche others avec parametre
L’identificateur représente le paramètre et contiendra l’identité de l’exception qui a provoqué l’arrivée dans la branche. Sa portée se limite à sa branche de déclaration. Exemple 13.4 Production de traces dans un traite-exception with Ada.Exceptions; ... exception when Constraint_Error => ...
-- Comme pour l'exemple 13.3
-- Preparer la sortie de la structure actuelle
raise; -- Lever Constraint_Error a nouveau pour la propager -- On suppose que E1 et E2 sont deux exceptions when Erreur : E1 | E2 => -- Erreur est implicitement -- declaree ici -- Utilisation de la fonction Exception_Name Put ("Exception "); Put ( Ada.Exceptions.Exception_Name(Erreur) ); Put_Line (" levee dans cette structure"); -- Utilisation de la fonction Exception_Message Put_Line ("Informations supplementaires:"); Put_Line ( Ada.Exceptions.Exception_Message(Erreur) ); -- Utilisation de la fonction Exception_Information Put_Line ("Informations completes:"); Put_Line ( Ada.Exceptions.Exception_Information(Erreur) ); raise; -- Lever l'exception qui a provoque l'arrivee dans le -- traite-exception, c'est-a-dire E1 ou E2 end ...;
281
13.5 COMPLÉMENTS Le code d’initialisation d’un paquetage peut comporter un traite-exception. Celui-ci pourra traiter les exceptions levées dans ce code, mais en aucun cas celles provoquées par l’un des corps contenus dans le corps du paquetage. Une exception déclarée dans un sous-programme récursif n’est pas recréée à chaque appel récursif mais existe en un seul exemplaire. Les exceptions sont des entités spéciales car il est impossible de les manipuler comme les autres objets d’un programme Ada. Les seules opérations possibles sont celles décrites dans ce chapitre et à la section 6.3.
EXERCICES
282
13.6 EXERCICES 13.6.1
Paramètre incorrect
Reprendre le sous-programme de l’exercice 8.6.4 et lever une exception dans le cas où le tableau contenant les nombres réels et passé en paramètre est vide. Fautil déclarer l’exception dans le sous-programme ou à l’extérieur de celui-ci? Une exception prédéfinie fait-elle l’affaire? 13.6.2
Carrés latins
Reprendre le sous-programme de l’exercice 8.6.7 et lever une exception pour le cas où une ligne ou une colonne contient un nombre hors de l’intervalle 1..n. Fautil déclarer l’exception dans le sous-programme ou à l’extérieur de celui-ci? Une exception prédéfinie fait-elle l’affaire? 13.6.3
Division par zéro
Reprendre le paquetage de gestion de nombres complexes (exercices 10.9.1 et 10.9.2) et introduire une exception pour le cas de division par zéro. Introduire ce cas dans le programme de test correspondant (exercice 10.9.3). 13.6.4
Multiplication par zéro
Reprendre le paquetage enfant de gestion de vecteurs (exercice 10.9.6) et introduire une exception pour le cas de produit scalaire avec un vecteur nul. Introduire ce cas dans le programme de test correspondant (exercice 10.9.3).
POINTS À RELEVER
283
13.7 POINTS À RELEVER 13.7.1
En général • La déclaration et le traitement d’exceptions se retrouvent dans d’autres
langages de programmation, par exemple C++ et Java. 13.7.2
En Ada • Le programmeur peut déclarer et traiter ses propres exceptions. • L’instruction raise permet de lever explicitement n’importe quelle ex-
ception. • Un traite-exception peut comprendre l’utilisation de fonctions exportées du paquetage Ada.Exceptions afin de produire de l’information sur l’ex-
ception en cours.
284
C H A P I T R E
STRUCTU RES LINÉAIR ES
1 4
285
STRUCTURES LINÉAIRES SIMPLES ET STATIQUES
MOTIVATION
286
14.1 MOTIVATION L’étude des structures linéaires simples comme les files, listes, piles et queues, appartient traditionnellement aux bases de l’analyse et de la programmation et ceci pour de multiples raisons parmi lesquelles: • elles font partie des structures de données (data structures) simples; • elles impliquent l’étude d’algorithmes de recherche (searching); • sous leur forme dynamique, elles impliquent l’utilisation des pointeurs
(pointers). On appelle structure de données un support organisé permettant la représentation en mémoire de données simples ou complexes. L’étude de ces structures ainsi que des algorithmes associés (recherche, tri, parcours, etc.) forme l’une des matières les plus fondamentales dans la formation en programmation. A ce titre, ce chapitre se veut également une introduction à ces notions. Les articles et les tableaux sont des structures de données linéaires simples. Ils peuvent d’ailleurs aussi servir à implémenter certaines autres structures linéaires dans leur forme statique (§ 14.4.1 et 14.5.1). La présence de ce chapitre se justifie comme préparation au chapitre suivant consacré aux pointeurs. Même s’il n’apporte aucun nouveau mécanisme Ada, il contient des situations typiques d’utilisation des tableaux et permet de comprendre les notions de listes, queues et piles indépendamment des pointeurs. Le lecteur, après présentation de ces concepts, peut parfaitement sauter ce chapitre dès la section 14.3 et continuer sa lecture au chapitre 15. Cependant, la définition d’une pile statique (§ 14.5.2) sera utilisée à la section 17.3 et celle d’une queue statique (§ 14.4.2) dans le chapitre 18.
LISTES, FILES
287
14.2 LISTES, FILES 14.2.1
Généralités
Les listes (lists) ou files (les deux termes sont synonymes) sont formées d’éléments successifs, classés ou non mais toujours ordonnés, dont le nombre peut être borné ou illimité. Une file d’attente devant un guichet ou la liste de passage à un examen en sont de bons exemples. Pour la suite le terme liste sera utilisé car c’est le plus répandu des deux. En informatique, une liste sert principalement à représenter une collection d’informations qui peuvent ou doivent se suivre. Des adresses postales peuvent constituer une liste si on les considère une par une; ces mêmes adresses classées par ordre alphabétique du nom de famille forment une liste dite triée. Si le nombre maximal d’adresses est connu, cette liste pourra être implantée sous une forme statique par un tableau. Sinon l’usage des pointeurs conduira à une version dynamique. Les termes statique, respectivement dynamique dénotent une entité connue à la compilation, respectivement seulement à l’exécution (sect. 3.10). On appelle tête (head), respectivement queue (tail) le premier, respectivement le dernier élément d’une liste. Comme les éléments sont toujours ordonnés, l’élément suivant (next element) et l’élément précédent (previous element) d’un élément donné sont définis (sauf pour la tête et la queue). L’élément considéré (traité) à un moment donné est appelé élément courant (current element). Comme pour les tableaux le nombre d’éléments s’appelle longueur (length) de la liste. Finalement, une liste vide (empty) est une liste ne comportant aucun élément. Cette dernière définition n’est pas sans importance car une liste vide doit souvent être traitée de manière particulière pour éviter l’apparition d’erreurs à l’exécution des programmes! Manipuler des listes consiste plus précisément à: • insérer (insert) une information, c’est-à-dire la rajouter à la liste; • supprimer (delete) ou extraire (extract) une information, l’éliminer de la
liste; modifier (modify) un élément, changer l’information présente; consulter (get) un élément, c’est-à-dire lire l’information présente; savoir si la liste est vide (empty); rechercher (search) une information particulière, savoir si elle fait partie de la liste; • parcourir (traverse) la liste, c’est-à-dire effectuer un traitement sur chacun de ses éléments. • • • •
Ces opérations sont les plus simples et forment la base du traitement des listes même si cette énumération est loin d’être exhaustive. A noter que l’endroit où un élément sera rajouté ou supprimé joue un rôle important dans la manipulation de certaines listes.
LISTES, FILES
14.2.2
288
Gestion, manipulations de base
Comment réaliser les opérations sur les listes? La recherche et le parcours ne posent pas de problèmes particuliers; la recherche consiste à retrouver un élément (éventuellement plusieurs) dont on connaît une partie de l’information et le parcours effectue un traitement sur tous les éléments. Par contre, l’insertion, la suppression, la modification et la consultation nécessitent la mention de la position où va s’effectuer l’opération. Or rien n’est défini dans le cas général d’une liste; l’insertion par exemple pourrait avoir lieu en tête, en queue, au «milieu», selon un critère comme l’ordre lexicographique sur une partie de l’information, etc. Le choix systématique de la même position particulière pour chacune de ces quatre opérations conduit à la gestion de listes caractérisées par un nom typique. En effet: • si l’insertion se passe en queue et la suppression en tête (ou inversement),
alors la liste s’appelle une queue (queue); cela conduit au fait que le premier élément mis en queue en sera le premier extrait; cette gestion est connue sous l’adage «premier arrivé, premier servi» (first in, first out abrégé FIFO) et possède donc la propriété de conserver l’ordre des éléments placés et retirés d’une queue; • si l’insertion et la suppression ont lieu toutes les deux en tête (ou en queue),
alors la liste s’appelle une pile (stack); le premier élément empilé sera le dernier désempilé; l’adage «dernier arrivé, premier servi» (last in, first out abrégé LIFO) s’applique à ce type de gestion; • si l’insertion s’effectue selon un critère d’ordre et la suppression en tête,
alors la liste s’appelle une queue de priorité (priority queue). La queue correspond donc à la situation d’attente devant un guichet; la personne située en tête de la queue la quitte après avoir dialogué avec l’employé(e) au guichet; les personnes qui arrivent se placent à la fin de la queue. Le terme queue possède maintenant deux significations: il peut désigner une liste gérée de cette manière ou simplement le dernier élément d’une liste quelconque. Une pile se manipule comme une pile d’assiettes qui sont prises et remises toujours en tête, tout en haut des assiettes restantes. C’est pour cette raison que l’accès à une pile se fait au même endroit appelé communément sommet (top). La queue d’une pile ne joue aucun rôle spécial dans ce type de liste. La gestion d’une file d’impression sur une imprimante s’effectue très souvent selon une queue de priorité où les fichiers à imprimer sont classés par ordre croissant de taille. Le fichier imprimé puis éliminé est toujours pris en tête (c’est le plus petit) alors qu’un nouveau fichier est placé dans la file en fonction de sa taille. Il existe naturellement d’autres manières de gérer des listes. Seuls les deux premiers cas seront développés dans ce chapitre. La structure de paquetage va être
LISTES, FILES
utilisée pour réaliser des solutions de gestion de queues et piles.
289
LISTES STATIQUES
290
14.3 LISTES STATIQUES Une liste statique est donc une structure de données statique (puisque sa longueur maximale est connue à la compilation) et prête à contenir une suite d’informations. Comment cette structure peut-elle être définie? Une bonne façon de faire est de déclarer un article comme dans l’exemple 14.1 où les informations sont supposées du type T_Info. Le type T_Liste_Statique permet la déclaration (création) d’une ou de plusieurs listes formées des informations (dans le tableau Contenu), de leur Longueur et de deux indicateurs désignant leur Tete et leur Queue. Les informations sont numérotées par T_Numerotation. Ces listes seront toutes initialement vides car leur longueur vaut 0 à la déclaration. Exemple 14.1 Définition d’une liste statique. Longueur_Max : constant := 100; -- Longueur maximum d'une liste subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Liste_Statique is record Contenu : T_Contenu;
-- Pour une liste statique -- Contenu de la liste
-- Longueur de la liste Longueur : T_Longueur := T_Longueur'First; Tete : T_Numerotation; Queue : T_Numerotation; end record;
-- Tete et Queue de la liste
QUEUES STATIQUES
291
14.4 QUEUES STATIQUES 14.4.1
Définition et création
Une queue statique voit donc sa longueur maximale connue à la compilation. La définition de cette structure (exemple 14.2) est presque identique à celle de l’exemple 14.1 pour les listes quelconques, les informations sont toujours supposées du type T_Info. Exemple 14.2 Définition d’une queue statique. Longueur_Max : constant := 100; -- Longueur maximum d'une queue subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Indic is Integer range 1..Longueur_Max + 1; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Queue_Statique is record
-- Pour une queue statique
-- Contenu de la queue Contenu : T_Contenu; -- Longueur de la queue Longueur : T_Longueur := T_Longueur'First; Tete : T_Pos_Indic := T_Pos_Indic'First;-- Tete et queue Queue : T_Pos_Indic := T_Pos_Indic'First;-- de la queue end record;
Le type T_Queue_Statique défini dans l’exemple 14.2 permet la déclaration (création) d’une ou de plusieurs queues formées des informations existantes (dans le tableau Contenu), de sa Longueur et de deux indicateurs désignant leur Tete et leur Queue. Leurs informations sont numérotées par T_Numerotation. Ces queues seront toutes initialement vides car leur longueur vaut 0 à la déclaration et la première information sera placée dans le premier élément. De plus et pour des raisons pratiques, Queue désignera le prochain élément de tableau à utiliser pour une insertion alors que Tete indiquera la prochaine information à extraire. Ceci explique les valeurs initiales pour ces deux indicateurs (fig. 14.1) ainsi que l’intervalle T_Pos_Indic des valeurs possibles.
QUEUES STATIQUES
292
Figure 14.1 Définition schématique d’une queue statique initialement vide. Queue statique Contenu 1
2
3
4
5
6
...
Longueur_Max
Longueur = 0 Tete
Queue
Une queue statique peut maintenant être créée par une simple déclaration de variable de type T_Queue_Statique. 14.4.2
Manipulations de base
Les opérations de base pour une queue (sect. 14.2) s’implémentent sans difficulté particulière sauf l’insertion (§ 14.4.3). La spécification d’un paquetage de gestion de queues statiques est donné dans l’exemple 14.3. Le corps de ce paquetage Queues_Statiques contiendra uniquement les corps des opérations énumérées dans la spécification, corps présentés dans les paragraphes qui suivent. Exemple 14.3 Spécification d’un paquetage de gestion de queues statiques. -- Bases de gestion de queues statiques package Queues_Statiques is type T_Info is ...;
-- Depend de l'application
Longueur_Max : constant :=100; -- Longueur maximum d'une queue subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Indic is Integer range 1..Longueur_Max + 1; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Queue_Statique is record
-- Pour une queue statique
-- Contenu de la queue Contenu : T_Contenu; -- Longueur de la queue Longueur : T_Longueur := T_Longueur'First; Tete : T_Pos_Indic := T_Pos_Indic'First;-- Tete et queue Queue : T_Pos_Indic := T_Pos_Indic'First;-- de la queue end record; Queue_Vide : exception; Queue_Pleine : exception;
-- Levee si la queue est vide -- Levee si la queue est pleine
QUEUES STATIQUES
293
------------------------------------------------------------- Insere Info en queue de La_Queue. Leve Queue_Pleine si -- l'insertion est impossible (la queue contient deja -- Longueur_Max elements) procedure Inserer ( La_Queue : in out T_Queue_Statique; Info : in T_Info ); ------------------------------------------------------------- Supprime l'information (en tete de La_Queue) qui est rendue -- dans Info. Leve Queue_Vide si la suppression est impossible -- (la queue est vide) procedure Supprimer ( La_Queue : in out T_Queue_Statique; Info : out T_Info ); ------------------------------------------------------------- Change l'information en tete de Queue. Leve Queue_Vide si la -- modification est impossible (la queue est vide) procedure Modifier ( La_Queue : in out T_Queue_Statique; Info : in T_Info ); ------------------------------------------------------------- Retourne l'information en tete de La_Queue. Leve Queue_Vide -- si la consultation est impossible (la queue est vide) function Tete ( La_Queue : T_Queue_Statique ) return T_Info; ------------------------------------------------------------- Retourne True si La_Queue est vide, False sinon -----------------------------------------------------------function Vide ( La_Queue : T_Queue_Statique ) return Boolean; -- Retourne True si l'information Info est presente dans la -- queue, False sinon -----------------------------------------------------------function Recherche ( La_Queue : T_Queue_Statique; Info : T_Info ) return Boolean; ------------------------------------------------------------- Effectue un traitement sur tous les elements de la queue procedure Parcourir ( La_Queue : in out T_Queue_Statique ); -----------------------------------------------------------end Queues_Statiques;
14.4.3
Insertion
Pour l’insertion, les informations sont placées les unes à la suite des autres, en commençant par occuper le premier élément (fig. 14.2).
QUEUES STATIQUES
294
Figure 14.2 Queue statique après une, puis cinq insertions. Queue statique après une insertion Contenu 1
2
3
4
5
6
...
Longueur_Max
Info 1 Longueur = 1 Tete
Queue
Queue statique après cinq insertions Contenu 1
2
3
4
5
Info 1
Info 2
Info 3
Info 4
Info 5
6
...
Longueur_Max
Longueur = 5 Tete
Queue
Que faire lorsque le dernier élément du Contenu est utilisé? Si le Contenu est complet, toute tentative d’insertion doit lever l’exception Queue_Pleine pour en avertir l’auteur (fig. 14.3). Figure 14.3 Tentative d’insertion après déjà Longueur_Max insertions. Queue statique Contenu 1
2
3
4
5
6
...
Info 1
Info 2
Info 3
Info 4
Info 5
Info 6
...
Q
Tete
e_ ueu
Ple
Longueur_Max ...
...
...
...
Info 100
ine Longueur = Longueur_Max
Queue
Si la queue n’est pas pleine parce que des suppressions ont eu lieu, et sans parler encore de gestion circulaire (§ 14.4.11), l’utilisation d’une structure statique nécessite parfois le déplacement des informations présentes, par exemple de manière à ce que l’information de tête soit placée dans le premier élément (fig. 14.4). La procédure d’insertion (exemple 14.4) est donc quelque peu compliquée du fait de
QUEUES STATIQUES
295
ces déplacements, nécessaires lorsque le dernier élément est occupé par une information. Figure 14.4 Insertion après Longueur_Max insertions et trois suppressions. Queue statique avant l’insertion Contenu 1
2
3
4
5
6
...
Info 4
Info 5
Info 6
...
Longueur_Max Info 100
Longueur = Longueur_Max - 3
Tete
Queue
Queue statique avant l’insertion mais après déplacement Contenu 1
2
3
4
Info 4
Info 5
Info 6
...
5
6
...
Longueur_Max Info 100
Longueur = Longueur_Max - 3
Tete
Queue
Queue statique après l’insertion Contenu 1
2
3
4
Info 4
Info 5
Info 6
...
5
6
...
Longueur_Max Info 100 Info 101
Longueur = Longueur_Max - 2
Tete
Queue
Exemple 14.4 Procédure d’insertion d’une information dans une queue statique. -- Insere Info en queue de La_Queue. Leve Queue_Pleine si l'inser-- tion est impossible (la queue contient deja Longueur_Max
QUEUES STATIQUES
296
-- elements) procedure Inserer (
La_Queue : in out T_Queue_Statique; Info : in T_Info ) is
begin -- Inserer -- Cas si la queue est pleine if La_Queue.Longueur = Longueur_Max then raise Queue_Pleine; end if; -- Queue pas pleine => faut-il deplacer les informations et -- les indicateurs? Oui si le dernier element est utilise if La_Queue.Queue = T_Pos_Indic'Last then -- Deplacer les informations La_Queue.Contenu ( La_Queue.Contenu'First .. La_Queue.Contenu'First + La_Queue.Longueur–1 ) := La_Queue.Contenu (La_Queue.Tete..La_Queue.Queue–1);-- 1 -- Remettre les indicateurs au bon endroit La_Queue.Queue := T_Pos_Indic'First + La_Queue.Longueur; La_Queue.Tete := T_Pos_Indic'First; end if; -- Inserer l'information en queue La_Queue.Contenu ( La_Queue.Queue ) := Info; -- L'element courant est maintenant occupe La_Queue.Queue := La_Queue.Queue + 1; -- Une information de plus dans la queue La_Queue.Longueur := La_Queue.Longueur + 1; end Inserer;
Il peut arriver qu’il n’y ait pas d’information à déplacer et que l’indicateur Queue désigne la position après le dernier élément. Les deux indicateurs sont alors
remis à leur valeur initiale et l’instruction 1 effectue une affectation de tranches de tableau vides (§ 8.5.1). 14.4.4
Suppression
Pour la suppression, les informations sont extraites les unes après les autres, en commençant par l’information de tête (fig. 14.5). Lorsque la dernière information du Contenu a été extraite, toute tentative de suppression doit lever l’exception Queue_Vide pour en avertir l’auteur (fig. 14.6). La réalisation de la procédure de suppression est alors facile et illustrée dans l’exemple 14.5.
QUEUES STATIQUES
297
Figure 14.5 Queue statique après cinq insertions et deux suppressions. Queue statique Contenu 1 2
3
4
5
6
Info 3
Info 4
Info 5
...
Longueur_Max
Longueur = 3 Tete
Queue
Figure 14.6 Tentative de suppression alors que la queue est vide. Queue statique Contenu 1
2
3
4
5
e ueu
Q
6
_ Vi
...
Longueur_Max
de
Longueur = 0
Tete Queue
Exemple 14.5 Procédure de suppression d’une information dans une queue statique. -- Supprime l'information (en tete de La_Queue) qui est rendue -- dans Info. Leve Queue_Vide si la suppression est impossible (la -- queue est vide) procedure Supprimer (
La_Queue : in out T_Queue_Statique; Info : out T_Info ) is
begin -- Supprimer -- Cas si la queue est vide if La_Queue.Longueur = T_Longueur'First then raise Queue_Vide; end if; -- Recuperer l'information en tete Info := La_Queue.Contenu ( La_Queue.Tete ); -- L'element courant est maintenant libre La_Queue.Tete := La_Queue.Tete + 1;
QUEUES STATIQUES
298
-- Une information de moins dans la queue La_Queue.Longueur := La_Queue.Longueur – 1; end Supprimer;
14.4.5
Modification
La modification d’une information n’est possible qu’en tête de la queue, par définition de celle-ci (fig. 14.7). Lorsque la queue est vide, toute tentative de modification doit lever l’exception Queue_Vide pour en avertir l’auteur (fig. 14.8). Finalement, la réalisation de la procédure de modification est facile et illustrée dans l’exemple 14.6. Figure 14.7 Modification de l’information de tête dans une queue statique.
Queue statique Contenu 1
2
3
4
5
Info 3
Info 4
Info 5
6
...
Longueur_Max
Longueur = 3 Information à modifier Tete
Queue
Figure 14.8 Tentative de modification alors que la queue est vide. Queue statique Contenu 1
2
3
eu Qu
4
5
ide e_V
6
...
Longueur = 0
Tete Queue
Longueur_Max
QUEUES STATIQUES
299
Exemple 14.6 Procédure de modification d’une information dans une queue statique. -- Change l'information en tete de La_Queue. Leve Queue_Vide si la -- modification est impossible (la queue est vide) procedure Modifier ( La_Queue : in out T_Queue_Statique; Info : in T_Info ) is begin -- Modifier -- Cas si la queue est vide if La_Queue.Longueur = T_Longueur'First then raise Queue_Vide; end if; -- Modifier l'information en tete La_Queue.Contenu ( La_Queue.Tete ) := Info; end Modifier;
14.4.6
Consultation
La consultation d’une information n’est possible qu’en tête de la queue, par définition de celle-ci. Cette opération est donc très proche de la modification, ce qui permet de donner directement la fonction adéquate dans l’exemple 14.7. Exemple 14.7 Fonction de consultation d’une information dans une queue statique. -- Retourne l'information en tete de La_Queue. Leve Queue_Vide si -- la consultation est impossible (la queue est vide) function Tete ( La_Queue : T_Queue_Statique ) return T_Info is begin -- Tete -- Cas si la queue est vide if La_Queue.Longueur = T_Longueur'First then raise Queue_Vide; end if; -- Retourner l'information en tete return La_Queue.Contenu ( La_Queue.Tete ); end Tete;
14.4.7
Queue vide
Le fait qu’une queue soit vide implique que tous les éléments insérés ont été extraits, ou éventuellement que la queue n’a pas encore servi. Cette information est très souvent utilisée dans les algorithmes qui mettent en œuvre une ou plusieurs queues. La fonction correspondante est présentée dans l’exemple 14.8.
QUEUES STATIQUES
300
Exemple 14.8 Fonction retournant l’état (vide/non vide) d’une queue statique. -- Retourne True si La_Queue est vide, False sinon function Vide ( La_Queue : T_Queue_Statique ) return Boolean is begin -- Vide -- La queue est vide si sa longueur vaut 0 return La_Queue.Longueur = T_Longueur'First; end Vide;
14.4.8
Recherche
Une recherche est très différente des opérations précédentes. S’il existe de nombreuses méthodes de recherche, la recherche séquentielle (sequential search) va seule être réalisée. Une recherche séquentielle consiste à examiner les informations les unes après les autres en commençant par la tête de la queue et à stopper: • soit parce que l’information cherchée a été trouvée dans la queue; • soit parce que toutes les informations ont été examinées sans succès.
Une recherche donnera donc comme résultat une valeur binaire (information trouvée ou non). Parfois la position de l’information trouvée est aussi donnée comme résultat. L’algorithme de recherche séquentielle consiste donc essentiellement en une boucle avec les deux conditions de sortie mentionnées ci-dessus. L’exemple 14.9 donne la fonction réalisant cette opération. Exemple 14.9 Fonction de recherche d’une information dans une queue statique. -- Retourne True si l'information Info est presente dans la queue, -- False sinon function Recherche ( La_Queue : T_Queue_Statique; Info : T_Info ) return Boolean is Position : T_Pos_Indic := La_Queue.Tete;-- Pour acceder a -- chaque information begin -- Recherche -- Tant que la queue n'est pas atteinte et l'information pas -- trouvee while Position < La_Queue.Queue and then Info /= La_Queue.Contenu ( Position ) loop Position := Position + 1; -- Passer a l'information suivante end loop; -- Si la queue n'est pas atteinte, l'information a ete trouvee return Position < La_Queue.Queue; end Recherche;
QUEUES STATIQUES
301
La présence de la forme de contrôle en raccourci and then (§ 3.4.3) est le seul point délicat de cet exemple; elle est nécessaire pour éviter de consulter le Contenu si la Position est hors de celui-ci. A noter que le cas d’une queue vide ne provoque cette fois aucune exception; la recherche d’une information dans une queue vide aboutit immédiatement et simplement à la valeur False (information introuvable), ce qui est conforme à la logique et à la pratique courante. 14.4.9
Parcours
Le parcours (traverse) d’une structure consiste à appliquer un même traitement à tous les éléments de cette structure. Le résultat de l’application du traitement à chaque élément forme le résultat du parcours. Selon le traitement, les éléments ou la structure peuvent être modifiés ou non. Le parcours d’une queue semble facile à réaliser. Mais puisque le traitement peut être différent à chaque fois qu’un parcours s’effectue, comment réaliser ces changements dans une seule procédure de parcours? Il faut connaître la notion de généricité (chap. 17 et 18) ou de type accès à sous-programme (§ 15.8.2) pour obtenir une réponse satisfaisante à ce problème. L’exemple 14.10 présente une procédure réalisant un parcours où l’on suppose qu’une procédure Traiter effectue le traitement voulu. Exemple 14.10 Procédure de parcours d’une queue statique. -- Effectue un traitement sur tous les elements de la queue. Le -- traitement est suppose fait dans la procedure interne Traiter procedure Parcourir ( La_Queue : in out T_Queue_Statique ) is procedure Traiter ( Info : in out T_Info ) is ... end Traiter; begin -- Parcourir -- Prendre les elements les uns apres les autres for Position in La_Queue.Tete .. La_Queue.Queue – 1 loop Traiter ( La_Queue.Contenu (Position) ); -- Traitement end loop; end Parcourir;
Comme pour la recherche et pour les mêmes raisons, le cas d’une queue vide ne provoque aucune exception. 14.4.10 Exemple d’utilisation d’une queue statique Soit un bar à boissons qui suit les festivals musicaux d’été (jazz à Montreux, Paléo à Nyon, etc.). Les festivaliers assoiffés doivent d’abord payer leur boisson à une caisse unique et reçoivent un ticket numéroté attestant la somme payée. La caisse et le bar proprement dit sont reliés par un petit système informatique qui,
QUEUES STATIQUES
302
entre autres, permet aux barmen de préparer aussitôt les boissons payées apparaissant à l’écran, accompagnées des numéros des tickets. Les barmen servent les boissons selon l’ordre d’apparition des numéros afin de ne provoquer aucun mécontentement. Pour la programmation du logiciel du système informatique, une queue statique a été utilisée. Elle contient à tout moment les boissons payées mais non encore distribuées avec les numéros correspondants. L’information est donc constituée du type de boisson, de la contenance et du numéro associé (exemple 14.11). Exemple 14.11 Le type T_Info pour le bar à boissons. type T_Info is record Boisson : Integer; Contenance : Float; Numero : Integer; end record;
-- Pour simplifier -- En litres -- Numero du ticket correspondant
On suppose que ce type T_Info a complété la spécification de l’exemple 14.3. Les caissiers et les barmen utilisent le logiciel pour gérer leurs boissons. Pour vendre une boisson, la procédure Vendre_Boisson est utilisée alors que pour distribuer une boisson, c’est la procédure Distribuer_Boisson qui s’exécute. La queue et les corps de ces procédures sont décrites dans l’exemple 14.12. Exemple 14.12 Gestion rudimentaire des boissons du bar. with Queues_Statiques;
-- But de l'exemple
procedure Systeme_Gestion is -- Squelette pour presenter la queue et les deux procedures -- La queue des boissons vendues mais pas encore distribuees Queue_Boissons : Queues_Statiques.T_Queue_Statique; ... ------------------------------------------------------------- Permet d'enregistrer une boisson vendue procedure Vendre_Boisson is -- Type , contenance et numero de la boisson vendue Info_Boisson : Queues_Statiques.T_Info; begin -- Vendre_Boisson ... -- Accueillir le client, lui demander ce qu'il veut -- Enregistrer sa commande Queues_Statiques.Inserer ( Queue_Boissons, (Boisson, Contenance, Numero) ); end Vendre_Boisson;
QUEUES STATIQUES
303
-- Pour la distribution d'une boisson procedure Distribuer_Boisson is -- Type , contenance et numero de la boisson distribuee Info_Boisson : Queues_Statiques.T_Info; begin -- Distribuer_Boisson -- Connaitre la boisson du prochain client Queues_Statiques.Supprimer (Queue_Boissons, Info_Boisson); -- Preparer la boisson, appeler le client grace au numero ... end Distribuer_Boisson; -----------------------------------------------------------... begin -- Systeme_Gestion ... end Systeme_Gestion;
Il faut que les barmen distribuent les boissons suffisamment vite pour éviter que la queue devienne pleine et que la procédure Inserer lève l’exception Queue_Pleine. 14.4.11 Queues statiques gérées circulairement L’insertion dans une queue statique peut être considérablement simplifiée si la queue est gérée circulairement, c’est-à-dire si le tableau Contenu est pris comme un anneau: le premier élément est le successeur du dernier. Il n’y a alors plus besoin de décaler les informations puisque le concept de dernier élément n’existe plus! Les autres opérations sont très similaires à celles présentées. Ce cas particulier de gestion circulaire, très utilisé dans la pratique, ne sera pas plus détaillé ici.
PILES STATIQUES
304
14.5 PILES STATIQUES 14.5.1
Définition et création
Une pile statique voit donc sa longueur maximale connue à la compilation. La définition de cette structure est presque identique à celle d’une queue (§ 14.4.1), les informations sont toujours supposées du type T_Info. Exemple 14.13 Définition d’une pile statique. Longueur_Max : constant := 100;
-- Longueur maximum d'une pile
subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Sommet is Integer range 0..Longueur_Max; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Pile_Statique is
-- Pour une pile statique
record -- Contenu de la pile Contenu : T_Contenu; -- Longueur de la pile Longueur : T_Longueur := T_Longueur'First; -- Sommet de la pile Sommet : T_Pos_Sommet := T_Pos_Sommet'First; end record;
Le type T_Pile_Statique défini dans l’exemple 14.13 permet la déclaration (création) d’une ou de plusieurs piles formées des informations existantes (dans le tableau Contenu), de sa Longueur et d’un seul indicateur désignant leur Sommet. Leurs informations sont numérotées par T_Numerotation. Ces piles seront toutes initialement vides car leur longueur vaut 0 à la déclaration. Par choix arbitraire, la première information sera placée dans le premier élément. De plus et pour des raisons pratiques, Sommet désignera l’élément de tableau contenant l’information située au sommet de la pile (§ 14.2.2). Ceci explique la valeur initiale pour cet indicateur ainsi que l’intervalle T_Pos_Sommet des valeurs possibles (fig. 14.9).
PILES STATIQUES
305
Figure 14.9 Définition schématique d’une pile statique initialement vide. Pile statique Contenu 1
2
3
4
5
6
...
Longueur_Max
Longueur = 0
Sommet
Une pile statique peut être représentée horizontalement ou verticalement. Pour des raisons de composition des pages de cet ouvrage, la forme horizontale sera en général celle choisie. Une pile statique peut maintenant être créée par une simple déclaration de variable de type T_Pile_Statique. 14.5.2
Manipulations de base
Les opérations de base pour une pile (sect. 14.2) s’implémentent sans difficulté particulière et, sauf l’insertion, sont très semblables à celles d’une queue. La spécification d’un paquetage de gestion de piles statiques est donné dans l’exemple 14.14. Exemple 14.14 Spécification d’un paquetage de gestion de piles statiques. -- Bases de gestion de piles statiques package Piles_Statiques is type T_Info is ...;
-- Depend de l'application
Longueur_Max : constant := 100; -- Longueur maximum d'une pile subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Sommet is Integer range 0..Longueur_Max; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Pile_Statique is
-- Pour une pile statique
record Contenu : T_Contenu;
-- Contenu de la pile
-- Longueur de la pile Longueur : T_Longueur := T_Longueur'First; -- Sommet de la pile
PILES STATIQUES
306
Sommet : T_Pos_Sommet := T_Pos_Sommet'First; end record; Pile_Vide : exception; Pile_Pleine : exception;
-- Levee si la pile est vide -- Levee si la pile est pleine
------------------------------------------------------------- Insere Info au sommet de Pile. Leve Pile_Pleine si l'inser-- tion est impossible (la pile contient deja Longueur_Max -- elements) procedure Empiler ( Pile : in out T_Pile_Statique; Info : in T_Info ); ------------------------------------------------------------- Supprime l'information (au sommet de Pile) qui est rendue -- dans Info. Leve Pile_Vide si la suppression est impossible -- (la pile est vide) procedure Desempiler ( Pile : in out T_Pile_Statique; Info : out T_Info ); ------------------------------------------------------------- Change l'information du sommet de Pile. Leve Pile_Vide si la -- modification est impossible (la pile est vide) procedure Modifier ( Pile : in out T_Pile_Statique; Info : in T_Info ); -- Retourne l'information du sommet de Pile. Leve Pile_Vide si -- la consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile_Statique) return T_Info; ------------------------------------------------------------- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile_Statique) return Boolean; ------------------------------------------------------------- Retourne True si l'information Info est presente dans la -- pile, False sinon function Recherche ( Pile : T_Pile_Statique; Info : T_Info) return Boolean; ------------------------------------------------------------- Effectue un traitement sur tous les elements de la pile procedure Parcourir ( Pile : in out T_Pile_Statique ); -----------------------------------------------------------end Piles_Statiques;
Le corps de ce paquetage Piles_Statiques contiendra uniquement les corps des opérations énumérées dans la spécification, corps présentés dans les paragraphes qui suivent. 14.5.3
Insertion
Les informations sont placées à la suite les unes des autres, en commençant par occuper le premier élément (fig. 14.10). Insérer une information est communément
PILES STATIQUES
307
appelé empiler (push). Figure 14.10 Pile statique après cinq insertions. Pile statique Contenu 1
2
3
4
5
Info 1
Info 2
Info 3
Info 4
Info 5
6
...
Longueur_Max
Longueur = 5 Sommet
Lorsque le dernier élément du Contenu est utilisé, toute tentative d’insertion doit naturellement lever l’exception Pile_Pleine pour en avertir l’auteur (fig. 14.11). La réalisation de la procédure d’insertion est alors facile et illustrée dans l’exemple 14.15. Figure 14.11 Tentative d’insertion après déjà Longueur_Max insertions. Pile statique Contenu 1 Info 1
2
3
4
5
6
...
Info 2
Info 3
Info 4
Info 5
Info 6
...
P
_P ile
lei
Longueur_Max ...
...
...
...
Info 100
ne Longueur = Longueur_Max
Sommet
Exemple 14.15 Procédure d’insertion d’une information dans une pile statique. -- Insere Info au sommet de Pile. Leve Pile_Pleine si l'insertion -- est impossible (la pile contient deja Longueur_Max elements) procedure Empiler ( Pile : in out T_Pile_Statique; Info : in T_Info ) is begin -- Empiler -- Cas si la pile est pleine if Pile.Longueur = Longueur_Max then
PILES STATIQUES
308
raise Pile_Pleine; end if; -- L'element suivant va etre occupe Pile.Sommet := Pile.Sommet + 1; -- Inserer l'information au sommet Pile.Contenu ( Pile.Sommet ) := Info; -- Une information de plus dans la pile Pile.Longueur := Pile.Longueur + 1; end Empiler;
14.5.4
Suppression
Pour la suppression, les informations sont extraites les unes après les autres, en commençant par l’information au sommet (fig. 14.12). Supprimer une information est communément appelé désempiler (pop). Lorsque la dernière information du Contenu a été désempilée, toute tentative de suppression doit lever l’exception Pile_Vide pour en avertir l’auteur (fig. 14.13). La réalisation de la procédure de suppression est alors facile et illustrée dans l’exemple 14.16. Figure 14.12 Pile statique après cinq insertions et deux suppressions. Pile statique Contenu 1
2
3
Info 1
Info 2
Info 3
4
5
6
...
Longueur = 3
Sommet
Longueur_Max
PILES STATIQUES
309
Figure 14.13 Tentative de suppression alors que la pile est vide. Pile statique Contenu 1
2
3
4
5
Pil
id e e_V
6
...
Longueur_Max
Longueur = 0
Sommet
Exemple 14.16 Procédure de suppression d’une information dans une pile statique. -- Supprime l'information (au sommet de Pile) qui est rendue dans -- Info. Leve Pile_Vide si la suppression est impossible (la pile -- est vide) procedure Desempiler (Pile : in out T_Pile_Statique; Info : out T_Info ) is begin -- Desempiler -- Cas si la pile est vide if Pile.Longueur = T_Longueur'First then raise Pile_Vide; end if; -- Supprimer l'information au sommet Info := Pile.Contenu ( Pile.Sommet ); -- Le sommet devient l'element precedent Pile.Sommet := Pile.Sommet – 1; -- Une information de moins dans la pile Pile.Longueur := Pile.Longueur – 1; end Desempiler;
14.5.5
Modification
La modification d’une information n’est possible qu’au sommet de la pile, par définition de celle-ci (fig. 14.14).
PILES STATIQUES
310
Figure 14.14 Modification de l’information au sommet dans une pile statique. Pile statique Contenu 1
2
3
Info 1
Info 2
Info 3
4
5
6
...
Longueur_Max
Longueur = 3 Information à modifier Sommet
Lorsque la pile est vide, toute tentative de modification doit lever l’exception Pile_Vide pour en avertir l’auteur (fig. 14.15). Figure 14.15 Tentative de modification alors que la pile est vide. Pile statique Contenu 1
2
3
4
5
Pil
id e e_V
6
...
Longueur_Max
Longueur = 0
Sommet
La réalisation de la procédure de modification est aussi facile et elle est illustrée dans l’exemple 14.17. Exemple 14.17 Procédure de modification d’une information dans une pile statique. -- Change l'information du sommet de Pile. Leve Pile_Vide si la -- modification est impossible (la pile est vide) procedure Modifier ( Pile : in out T_Pile_Statique; Info : in T_Info ) is begin -- Modifier -- Cas si la pile est vide if Pile.Longueur = T_Longueur'First then raise Pile_Vide; end if; -- Modifier l'information du sommet
PILES STATIQUES
311
Pile.Contenu ( Pile.Sommet ) := Info; end Modifier;
14.5.6
Consultation
La consultation d’une information n’est possible qu’au sommet de la pile, par définition de celle-ci. Cette opération est donc très proche de la modification, ce qui permet de donner directement la fonction adéquate dans l’exemple 14.18. Exemple 14.18 Fonction de consultation d’une information dans une pile statique. -- Retourne l'information du sommet de Pile. Leve Pile_Vide si la -- consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile_Statique ) return T_Info is begin -- Sommet -- Cas si la pile est vide if Pile.Longueur = T_Longueur'First then raise Pile_Vide; end if; -- Retourner l'information du sommet return Pile.Contenu ( Pile.Sommet ); end Sommet;
14.5.7
Pile vide
Le fait qu’une pile soit vide implique que tous les éléments insérés ont été extraits, ou éventuellement que la pile n’a pas encore servi. Cette information est très souvent utilisée dans les algorithmes qui mettent en œuvre une ou plusieurs piles. La fonction correspondante est présentée dans l’exemple 14.19. Exemple 14.19 Fonction retournant l’état (vide/non vide) d’une pile statique. -- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile_Statique ) return Boolean is begin -- Vide -- La pile est vide si sa longueur vaut 0 return Pile.Longueur = T_Longueur'First; end Vide;
PILES STATIQUES
14.5.8
312
Recherche
La recherche séquentielle dans une pile est semblable à celle appliquée pour une queue (§ 14.4.8) en commençant par le sommet de la pile. L’exemple 14.20 donne la fonction réalisant cette opération. Exemple 14.20 Fonction de recherche d’une information dans une pile statique. -- Retourne True si l'information Info est presente dans la pile, -- False sinon function Recherche (
Pile : T_Pile_Statique; Info : T_Info ) return Boolean is
Position : T_Pos_Sommet := Pile.Sommet; -- Pour acceder a -- chaque information begin -- Recherche -- Tant qu'il reste des elements et l'information pas trouvee while Position >= Pile.Contenu'First and then Info /= Pile.Contenu ( Position ) loop -- Passer a l'information suivante Position := Position – 1; end loop; -- L'information a ete trouvee si Position designe un element -- de la pile return Position >= Pile.Contenu'First; end Recherche;
La présence de la forme de contrôle en raccourci and then (§ 3.4.3) est le seul point délicat de cet exemple; elle est nécessaire pour éviter de consulter le Contenu si la Position est hors de celui-ci. A noter que le cas d’une pile vide ne provoque cette fois aucune exception; la recherche d’une information dans une pile vide aboutit immédiatement et simplement à la valeur False (information introuvable), ce qui est conforme à la logique et à la pratique courante. 14.5.9
Parcours
Le parcours d’une pile est semblable à celui appliqué à une queue (§ 14.4.9) en commençant par le sommet de la pile, avec la même remarque concernant le traitement à effectuer. L’exemple 14.21 présente une procédure réalisant un parcours où l’on suppose qu’une procédure Traiter effectue le traitement voulu. Exemple 14.21 Procédure de parcours d’une pile statique. -- Effectue un traitement sur tous les elements de la pile. Le -- traitement est suppose fait dans la procedure interne Traiter
PILES STATIQUES
313
procedure Parcourir ( Pile : in out T_Pile_Statique ) is procedure Traiter ( Info : in out T_Info ) is ... end Traiter; begin -- Parcourir -- Prendre les elements les uns apres les autres for Position in reverse Pile.Contenu'First .. Pile.Sommet loop Traiter ( Pile.Contenu (Position) ); -- Traitement end loop; end Parcourir;
Comme pour la recherche et pour les mêmes raisons, le cas d’une pile vide ne provoque aucune exception. 14.5.10 Exemple d’utilisation d’une pile statique Parmi toutes les applications de l’utilisation d’une pile statique, l’empilement des objets locaux lors de l’appel d’un sous-programme est l’une des plus intéressantes. En simplifiant la réalité qui est assez complexe, il s’agit de gérer une pile dont les éléments sont des blocs de mémoire formés de paramètres et de variables locales, avec un bloc par sous-programme en cours d’exécution. A chaque appel de sous-programme, un nouveau bloc est empilé alors qu’à chaque sortie, le bloc du sous-programme quitté est désempilé. Sans donner plus d’explications, cette façon de faire facilite la gestion de la mémoire et l’accès aux objets locaux. L’exemple 14.22 montre une suite d’appels de procédures et les figures 14.16 et 14.17 les empilements correspondants. Exemple 14.22 Suite d’appels de procédures. procedure P1 ( I : in Integer ) is J : Integer; begin -- P1 ... end P1; procedure P2 is A, B: Float; begin -- P2 ... P1; ... end P2;
PILES STATIQUES
314
Figure 14.16 Empilement du bloc correspondant à l’appel de P2 (exemple 14.22). Appel de P2: place libre dans la pile
sommet de la pile emplacement de B emplacement de A
}
bloc de P2
blocs des appels des sous-programmes précédant l’appel de P2
Figure 14.17 Empilement du bloc correspondant à l’appel de P1 (exemple 14.22). Appel de P1: place libre dans la pile sommet de la pile emplacement de J emplacement de I emplacement de B emplacement de A
} }
bloc de P1
bloc de P2
blocs des appels des sous-programmes précédant l’appel de P2
315
14.6 EXERCICES 14.6.1
Déclaration d’une queue statique
Pourquoi la borne supérieure du sous-type T_Pos_Indic (exemple 14.2) vautelle Longueur_Max + 1? 14.6.2
Queue statique circulaire
Récrire les déclarations de l’exemple 14.2 pour une queue statique circulaire et adapter la procédure d’insertion (§ 14.4.3). 14.6.3
Paquetage enfant pour les queues statiques
Ecrire un paquetage enfant du parent Queues_Statiques exportant les opérations suivantes: copier une queue, appondre une (copie d’une) queue à une autre, savoir si une queue est pleine, et supprimer tous les éléments d’une queue. 14.6.4
Paquetage enfant pour les piles statiques
Ecrire un paquetage enfant du parent Piles_Statiques similaire à celui demandé dans l’exercice 14.6.3.
POINTS À RELEVER
316
14.7 POINTS À RELEVER 14.7.1
En général • Une structure de données est un support organisé permettant la
représentation en mémoire de données simples ou complexes. • Une liste est une structure de données linéaire, statique ou dynamique. Les
queues et les piles sont des cas simples de listes. • Insérer, supprimer, modifier, consulter, rechercher, parcourir constituent
les opérations de base sur les listes. • Les queues sont gérées selon la politique FIFO, les piles selon la politique
LIFO. • Il faut faire attention lorsqu’une liste est vide ou pleine; certaines opérations
peuvent alors provoquer des erreurs. 14.7.2
En Ada • Une liste statique est implémentée par un article. • Une queue statique comporte quatre éléments: le contenu sous forme de
tableau, la longueur et les indicateurs de tête et de queue. • Une pile statique comporte trois éléments: le contenu sous forme de
tableau, la longueur et l’indicateur du sommet.
317
C H A P I T R E
TYPES
1 5
318
TYPES ACCÈS
MOTIVATION
319
15.1 MOTIVATION Les pointeurs sont difficiles à appréhender, en particulier pour les programmeurs néophytes. Les pièges apparaissant lors de leur manipulation sont souvent déconcertants. Une comparaison tirée de [BAR 97] illustre parfaitement la situation: «Manipuler les pointeurs, c’est un peu comme jouer avec le feu. Le feu est sans doute l’outil le plus important pour l’homme. Utilisé avec précaution, le feu est considérablement utile; mais quel désastre s’il n’est plus contrôlé!». Cependant, l’usage très largement répandu des pointeurs rend indispensable leur présentation et leur maîtrise. Les types accès permettent la déclaration de pointeurs en Ada.
TYPE ACCÈS
320
15.2 TYPE ACCÈS 15.2.1
Motivation
Les structures de données statiques ont l’avantage d’une réalisation simple et d’un accès en général efficace en temps processeur. Leur principal inconvénient réside dans leur nature statique, impossible en effet d’augmenter leur taille lors de l’exécution puisque celle-ci est fixée à la compilation; on parle ici d’allocation statique de mémoire (static memory allocation, note 15.1). Il est vrai que le langage Ada permet de déterminer la taille d’un tableau à l’exécution grâce aux types tableaux non contraints (§ 8.2.3), voire de faire varier celle-ci dans certaines limites en utilisant des types articles à discriminants avec valeurs par défaut (sect. 11.3). Mais, même avec ces quelques propriétés, la taille (maximale) reste une frontière infranchissable. L’allocation dynamique de mémoire (dynamic memory allocation) permet d’éliminer cette frontière. Il s’agit d’une idée toute simple: permettre au programme de se réserver, de s’allouer des portions de mémoire lorsqu’il en a besoin. Mais il existe évidemment aussi une limite à cette technique, c’est la quantité de mémoire disponible de l’ordinateur utilisé, d’où l’importance de prendre en compte les possibilités de restitution, de libération de mémoire (memory deallocation) lorsque certaines portions deviennent inutilisées. Cette restitution peut être automatique, par exemple s’il existe un ramasse-miettes (garbage collector) associé au programme en exécution. En l’absence d’un tel outil, le programme lui-même peut assurer la libération de la mémoire. L’allocation dynamique de mémoire est cependant une technique de bas niveau, dans le sens où le programmeur qui l’utilise doit travailler avec des portions de mémoire, comprendre que celles-ci sont définies par une adresse et une taille. De manière générale, dans les langages de programmation, la gestion des adresses mémoire (la taille ne pose aucun problème) s’effectue par des pointeurs (pointers), c’est-à-dire par des variables qui contiendront des adresses mémoire. NOTE 15.1 Variables et allocation de mémoire. Il faut absolument comprendre que toute déclaration de variable dans une partie décla-rative est traduite par le compilateur en code réalisant, à l’exécution, l’allocation d’une place mémoire suffisamment grande pour cette variable. Cette règle est donc aussi valable pour une variable pointeur, la place mémoire allouée dans ce cas permettant de contenir une adresse.
15.2.2
Généralités
Un type pointeur en Ada se déclare au moyen du mot réservé access, d’où la terminologie des types accès pour désigner ces types pointeurs. Mais, avec ce mot
TYPE ACCÈS
321
réservé, la déclaration du type n’est pas complète. Il faut encore indiquer quel contenu les variables pointeurs de ce type vont repérer, ceci en effet pour permettre au compilateur les vérifications (de type) classiques lors d’affectation ou de passage en paramètre par exemple. La forme la plus simple d’une déclaration de type accès est: type identificateur_type_acces is access ident_de_type_ou_sous_type;
avec • identificateur_type_acces le nom du type accès défini; • ident_de_type_ou_sous_type le type ou le sous-type du contenu des
portions mémoire qui seront repérées par les variables du type identificateur_type_acces; ce type ou sous-type s’appelle type ou sous-type pointé. Exemple 15.1 Déclaration de types pointeurs sur des types habituels. type T_Tableau is array (1..9) of Integer; -- Un type tableau type T_Article is -- Un type article record Nombre : Integer; Tab : T_Tableau; end record; type T_Pt_Integer is access Integer;
--type T_Pt_Float is access Float; --type T_Pt_Tableau is access T_Tableau;--type T_Pt_Article is access T_Article;---
Un type pointeur sur le type pointe Integer Un type pointeur sur le type pointe Float Un type pointeur sur le type pointe T_Tableau Un type pointeur sur le type pointe T_Article
Dans l’exemple 15.1, le type T_Pt_Integer signifie que les variables de ce type contiendront une adresse (c’est un type accès) d’une portion mémoire prévue pour un nombre du type pointé Integer. De manière similaire, les types T_Pt_Float, T_Pt_Tableau et T_Pt_Article signifient que les variables de ces types contiendront une adresse d’une portion mémoire prévue pour un nombre du type pointé Float, un tableau du type pointé T_Tableau et un article du type pointé T_Article. Ces portions mémoire portent le nom de variables pointées puisque désignées, repérées par un pointeur. Il existe une seule constante pointeur appelée null qui, affectée à une variable pointeur, indique que cette variable ne repère aucune variable pointée. Une variable pointeur se déclare comme toute autre variable et possède la particularité d’avoir automatiquement la valeur initiale (valeur par défaut) null.
TYPE ACCÈS
322
Les opérations possibles (en plus de l’affectation et du passage en paramètre) sur les pointeurs consiste en l’utilisation de l’allocateur new (§ 15.2.3) et l’accès à la variable pointée (dereference). Les expressions se limitent aux constantes, variables, attributs applicables aux pointeurs ainsi qu’à l’utilisation de l’allocateur new. Finalement, les types pointeurs font partie des types simples (elementary types, [ARM 3.2]). 15.2.3
Allocateur new et affectation
Comme déjà mentionné (note 15.1), la place mémoire pour une variable pointeur est prévue par le compilateur comme pour toute autre variable de n’importe quel type. Mais la création d’une variable pointée s’effectue à l’exécution (c’est le but visé) par un allocateur de mémoire (memory allocator), utilitaire présent dans toute application nécessitant l’allocation dynamique de mémoire. En Ada, le mot réservé new désigne cet allocateur qui s’utilise comme une fonction-opérateur et de deux manières différentes: new identificateur_de_type_ou_sous_type
ou new expression_qualifiee
avec • l’identificateur_de_type_ou_sous_type qui indique le type ou
sous-type (pointé) de la variable pointée créée; • l’expression_qualifiee (sect. 3.10) qui non seulement indique le type
ou sous-type (pointé) de la variable pointée créée mais encore lui donne la valeur initiale placée entre les parenthèses. Exemple 15.2 Utilisation de l’allocateur new avec les types de l’exemple 15.1. new new new new new
Integerune variable pointée du type Integer est créée; Floatune variable pointée du type Float est créée; T_Tableauune variable pointée du type T_Tableau est créée; T_Articleune variable pointée du type T_Article est créée; Integer'(10)une variable pointée du type Integer est créée
et sa
valeur initiale est 10; new Float'(10.0)une variable pointée du type Float est créée et sa valeur initiale est 10.0; new T_Tableau'(others=>0)une variable pointée du type T_Tableau est créée et sa valeur initiale est (others=>0); new T_Article'(1,(others=>0))une variable pointée du type T_Article
TYPE ACCÈS
323
est créée et sa valeur initiale est (1, (others=>0)); Puisque l’allocateur new s’utilise comme une fonction, quel est le résultat retourné? Ce résultat comprend l’adresse de la variable pointée créée puisqu’il faudra naturellement pouvoir accéder à cette variable. Et pour cela, cette adresse devra être placée dans une variable pointeur du bon type par une affectation, comme dans l’exemple 15.3. C’est ainsi qu’une variable pointeur repérera, désignera, pointera (tous ces termes sont synonymes) une variable pointée. Exemple 15.3 Affectation du résultat de l’allocateur new. -- On utilise les types de l'exemple 15.1 Pt_Integer : T_Pt_Integer; -- Une variable pointeur sur Integer Pt_Float : T_Pt_Float; -- Une variable pointeur sur Float Pt_Tableau : T_Pt_Tableau; -- Une variable pointeur sur T_Tableau Pt_Article : T_Pt_Article; -- Une variable pointeur sur T_Article ... -- Utilisation de l'allocateur: Pt_Integer := new Integer; -- Pt_Integer repere la variable -- pointee creee Pt_Float := new Float'(10.0); -- Pt_Float repere la variable -- pointee creee de valeur initiale -- 10.0 Pt_Tableau := new T_Tableau;
-- Pt_Tableau repere la variable -- pointee creee
Pt_Article := new T_Article'(1,(others=>0)); -- Pt_Article repere -- la variable pointee creee de -- valeur initiale (1,(others=>0))
15.2.4
Représentation schématique
L’expérience montre que le travail avec les pointeurs est facilité, pour les néophytes, par la pratique de schémas illustrant la gestion des pointeurs et des variables pointées (fig. 15.1). Ces schémas comprennent des rectangles pour les variables et des flèches qui symbolisent les adresses des variables pointées; l’extrémité initiale d’une telle flèche indique où est enregistrée l’adresse alors que l’extrémité terminale désigne la variable pointée possédant cette adresse. La valeur particulière null est représentée par une barre oblique.
TYPE ACCÈS
324
Figure 15.1 Représentation schématique des déclarations et affectations de l’exemple 15.3. Déclaration des pointeurs: Pt_Integer
Pt_Tableau
Pt_Float
Pt_Article
Affectation des pointeurs: Pointeurs
Variables pointées
Pt_Integer ? Pt_Float 10.0 Pt_Tableau ?
?
?
?
?
?
0
0
0
0
0
?
?
?
0
0
Pt_Article 1
0
15.2.5
0
Accès aux variables pointées
L’accès à une variable pointée ne peut se faire que par l’intermédiaire d’une variable pointeur, ou indirectement par une autre variable pointée (§ 15.3.1). Cet accès peut se faire sur l’entier de la variable pointée ou sur l’un de ses composants si elle est de type composé (tableau ou article). L’accès à l’entier de la variable pointée (attention à la note 15.2) s’effectue en ajoutant le suffixe all à la variable pointeur qui la repère. L’accès à un composant est réalisé par la notation habituelle d’accès à ce composant, la variable pointeur (suivie ou non de all) jouant alors le rôle de préfixe. L’expression que constitue cet accès est naturellement du type de la variable pointée ou du composant accédé. NOTE 15.2 Accès et valeur null. Soit une variable pointeur de valeur null. L’exception Constraint_Error sera levée si cette variable
TYPE ACCÈS
325
est utilisée pour une tentative d’accès à une variable pointée. Cette erreur est très fréquente dans les programmes comportant des pointeurs et écrits par des néophytes.
Exemple 15.4 Accès aux variables pointées (les types sont définis dans l’exemple 15.1). with Ada.Integer_Text_IO; use Ada.Integer_Text_IO; procedure Exemple_15_4 is -- Quatre variables pointeurs Pt_Integer : T_Pt_Integer := new Integer; Pt_Float : T_Pt_Float := new Float'(10.0); Pt_Tableau : T_Pt_Tableau := new T_Tableau; Pt_Article : T_Pt_Article := new T_Article'(1, (others=>0)); Entier : Integer;
-- Une variable auxiliaire
begin -- Exemple_15_4 -- Acces aux variables pointees creees par l'allocateur Pt_Integer.all := 5; -- 1 Entier := 3 * Pt_Integer.all – 1; -- 2 Put (Pt_Integer.all); -- 3 Pt_Tableau.all := (1..4 =>0, 5|7|9 => 1, others => 2);-- 4 -- Acces aux composants Pt_Tableau (1) := 5; -- 5 Pt_Tableau.all (1) := 5; -- Identique a l'instruction 5 for I in Pt_Tableau.all'Range loop -- 6 Put ( Pt_Tableau (I) ); end loop; Pt_Article.Nombre := 10; -- 7 Pt_Article.all.Nombre := 10; -- Identique a l'instruction 7 Pt_Article.Tab := Pt_Tableau.all; -- 8 for I in Pt_Article.Tab'Range loop Put ( Pt_Article.Tab (I) ); end loop; ...
-- 9
Afin de bien comprendre ce mécanisme d’accès, l’effet des différentes instructions de l’exemple 15.4 est représenté dans la figure 15.2.
TYPE ACCÈS
326
Figure 15.2 Effet des instructions de l’exemple 15.4. Pointeurs
Variables pointées
Instruction 1: Pt_Integer 5 Instruction 2: la variable Entier reçoit la valeur 14 Instruction 3: affichage de la valeur 5 Instruction 4: Pt_Tableau 0
0
0
0
1
2
1
2
1
5
0
0
0
1
2
1
2
1
0
0
0
0
0
0
0
0
0
1
2
1
2
1
Instruction 5: Pt_Tableau
Instruction 6: affichage successif des valeurs 5 0 0 0 1 2 1 2 1 Instruction 7: Pt_Article 10
0
0
Instruction 8: Pt_Article 10
5
0
Instruction 9: affichage successif des valeurs 5 0 0 0 1 2 1 2 1
15.2.6
Affectation
L’affectation entre variables pointeurs s’effectue comme d’habitude; la seule
TYPE ACCÈS
327
contrainte réside dans le fait que la variable pointeur et l’expression doivent être du même type pointeur. Seul fait marquant, il ne faut pas confondre affectation de pointeurs et affectation de variables pointées. L’affectation d’un pointeur remplace l’adresse contenue dans la variable affectée, alors que l’affectation d’une variable pointée change la valeur de la variable pointée affectée sans modifier son adresse. Exemple 15.5 Affectation de pointeurs et de variables pointées -- On utilise les types de l'exemple 15.1 Pt_Integer : T_Pt_Integer := new Integer'(5);-- Trois variables -- pointeurs Autre_Pt_Integer : T_Pt_Integer; Pt_Float : T_Pt_Float; -- 1,situation initiale ... Autre_Pt_Integer := Pt_Integer; -- 2 Pt_Integer.all := 1; -- 3 -- que vaut Autre_Pt_Integer.all? -- 4 Autre_Pt_Integer := new Integer; -- 5 Autre_Pt_Integer.all := Pt_Integer.all; -- 6 Pt_Float := Pt_Integer; Pt_Float.all := 0.0;
-- 7,erreur de type -- 8,leve l'exception -- Constraint_Error
L’effet des lignes numérotées de 1 à 6 de l’exemple 15.5 est illustré dans la figure 15.3 afin de visualiser ce qui se passe. L’instruction 7 est non compilable car les deux variables pointeurs sont de types différents. L’instruction 8 pourra s’exécuter mais provoquera Constraint_Error car le pointeur Pt_Float possède la valeur null (§ 15.2.2) et ne désigne donc aucune variable pointée! 15.2.7 Pièges dans l’utilisation de pointeurs et de variables pointées L’instruction 8 de l’exemple 15.5 permet de relever que l’affectation, et plus généralement toute tentative de modification de pointeurs ou de variables pointées, recèle des pièges dans lesquels tout programmeur est tombé à plusieurs reprises. Ces pièges sont d’autant plus méchants qu’en général le compilateur ne les détecte pas car ils surgissent uniquement lors de l’exécution du code. Parmi ces pièges classiques, les plus répandus sont la tentative d’accès à une variable pointée alors qu’elle n’existe pas (note 15.2) comme dans l’instruction 8 de l’exemple 15.5, la confusion entre affectation de pointeurs, instruction 2 du même exemple, et de variables pointées, instruction 6 du même exemple également, et finalement la création de variables pointées inaccessibles (note 15.3).
TYPE ACCÈS
328
Figure 15.3 Effet des lignes 1 à 6 de l’exemple 15.5. Pointeurs
Variables pointées
Ligne 1: les trois déclarations Pt_Integer 5 Autre_Pt_Integer
Pt_Float
Instruction 2: Pt_Integer 5
Autre_Pt_Integer
Instruction 3: Pt_Integer 1
Autre_Pt_Integer
Ligne 4: Autre_Pt_Integer.all vaut aussi 1 car c’est la même variable pointée que Pt_Integer.all Instruction 5: Autre_Pt_Integer ? Instruction 6: Autre_Pt_Integer 1
TYPE ACCÈS
329
NOTE 15.3 Variables pointées inaccessibles. Une variable pointée devient inaccessible si elle n’est plus référencée, repérée par une autre variable. De ce fait elle ne sera plus jamais utilisable par le programme et occupera inutilement de la place en mémoire. Cet état de fait découle toujours d’une ou de plusieurs erreurs de programmation si l’information contenue dans la variable inaccessible devait encore servir à l’application. De plus, ce gaspillage de mémoire peut conduire dans le pire des cas à un blocage partiel ou total de cette application.
Une situation typique d’une variable pointée devenue inaccessible (exemple 15.6) est illustrée dans la figure 15.4. La similitude avec le début de l’exemple 15.5 est frappante et illustre le piège que recèle l’affectation de pointeurs. Exemple 15.6 Création de variables inaccessibles. type T_Pt_Integer is access Integer; Pt_Integer : T_Pt_Integer := new Integer'(5); Autre_Pt_Integer : T_Pt_Integer; -- 1, situation initiale ... Pt_Integer := Autre_Pt_Integer; -- 2
Figure 15.4 Situation aux lignes 1 et 2 de l’exemple 15.6. Pointeurs Ligne 1: les deux déclarations
Variables pointées
Pt_Integer 5 Autre_Pt_Integer
Instruction 2: Pt_Integer
Autre_Pt_Integer
variable pointée devenue inaccessible
5
TYPE ACCÈS
330
Il faut absolument éviter les pièges posés par les pointeurs. De plus, en cas de bizarreries lors de l’exécution d’un programme, il faut se rappeler que ces pièges existent. Un examen très soigneux de l’usage des pointeurs dans le code source s’impose. 15.2.8
Types accès et contraintes
Jusqu’à présent, tous les types accès présentés étaient définis sur des types contraints. Mais un type tableau non contraint (§ 8.2.3) ou un type article à discriminants (§ 11.2.1) peut être utilisé comme type pointé dans la définition d’un type accès. Il faut alors simplement se souvenir que c’est à l’élaboration d’une variable que la ou les contraintes nécessaires sont données. Ici, c’est donc lors de l’utilisation de l’allocateur new qu’il faudra se préoccuper de ces contraintes (exemple 15.7). Les règles régissant une variable pointée d’un type tableau non contraint ou article à discriminants sans valeur par défaut sont les mêmes que pour une variable statique ordinaire. Par contre, le ou les discriminants d’une variable pointée d’un type article à discriminants avec valeur par défaut ne peuvent plus changer une fois cette variable pointée créée. Exemple 15.7 Type accès et types tableau non contraint ou article à discriminant. subtype T_Intervalle is Integer range 0..100; -- Discriminant sans valeur par defaut type T_Article_Sans_Val_Def (Taille : T_Intervalle) is record Nombre : Integer; Tab : String (1..Taille); end record; -- Discriminant avec valeur par defaut type T_Article_Avec_Val_Def (Taille : T_Intervalle := 10) is record Nombre : Integer; Tab : String (1..Taille); end record; -- Trois types acces type T_Pt_String is access String; -- String est non contraint type T_Pt_Article_Sans_Val_Def is access T_Article_Sans_Val_Def; type T_Pt_Article_Avec_Val_Def is access T_Article_Avec_Val_Def; -- Trois variables pointeurs Pt_String : T_Pt_String; Pt_Article_Sans_Val_Def : T_Pt_Article_Sans_Val_Def; Pt_Article_Avec_Val_Def : T_Pt_Article_Avec_Val_Def;
TYPE ACCÈS
331
-- Differents cas d'utilisation de l'allocateur: -La contrainte est necessaire Pt_String := new String (1..10); Pt_Article_Sans_Val_Def := new T_Article_Sans_Val_Def (10); -La contrainte, necessaire, est donnee par la valeur initiale Pt_String := new String'("Bonjour"); -- Remarquer l'apostrophe Pt_Article_Sans_Val_Def := new T_Article_Sans_Val_Def'(5, 10, "Hello"); -La contrainte est possible mais pas necessaire Pt_Article_Avec_Val_Def := new T_Article_Avec_Val_Def (10); -Pas de contrainte, mais le discriminant ne pourra cependant -pas varier Pt_Article_Avec_Val_Def := new T_Article_Avec_Val_Def; Pt_Article_Avec_Val_Def := new T_Article_Avec_Val_Def'(5, 10, "Hello");
UTILISATION CLASSIQUE DES TYPES ACCÈS
332
15.3 UTILISATION CLASSIQUE DES TYPES ACCÈS 15.3.1
Motivation
Une question légitime se pose après la présentation des notions de base des types accès: comment créer des structures de taille variable? Jusqu’à présent l’allocation dynamique offre une certaine souplesse pour l’utilisation de la mémoire mais tous les objets présentés dans les exemples ont néanmoins une taille fixée à leur élaboration. L’idée principale à la base des structures dynamiques consiste à chaîner, relier les variables pointées entre elles de manière à ce que chacune d’elles soit repérée par une autre variable pointée au moins, à l’exception d’une (ou de plusieurs) de ces variables qui est (sont) désignée(s) par une variable pointeur particulière. Ainsi, grâce à cette variable pointeur, l’accès à une (première) variable pointée est possible puis, par l’intermédiaire du chaînage il est possible d’accéder à une autre variable pointée et ainsi de suite. La structure dynamique est alors composée de l’ensemble des variables pointées dont le nombre est quelconque; la taille de cette structure peut donc varier au cours de l’exécution par création ou destruction de variables pointées. Comme les variables pointées ne seront plus repérées uniquement par des pointeurs mais aussi par d’autres variables pointées, elles seront appelées de manière usuelle variables dynamiques (dynamic variables). Cette appellation sera dorénavant utilisée en lieu et place de variables pointées. Par ailleurs, le chaînage entre deux variables dynamiques est habituellement dénommé lien (link) entre ces variables. Ce lien est aussi un pointeur. 15.3.2
Généralités
Une variable dynamique comporte au moins une information et un lien. L’information est propre à l’application, alors que le lien sert à accéder à une autre variable dynamique. Du fait de leur structure en champs de types différents, les types articles sont donc appropriés pour implémenter une variable dynamique. De manière générale, une variable dynamique se présente sous la forme suivante: type T_Variable_Dynamique is record Information : T_Information;-- Un ou plusieurs champs pour l'information -- geree dans la structure dynamique Liens : T_Lien;-- Un ou plusieurs liens pour la realisation de -- la structure dynamique end record;
Dans sa forme la plus simple, une variable dynamique ne comporte qu’un seul lien (fig. 15.5) et le chaînage de telles variables permet de gérer des listes
UTILISATION CLASSIQUE DES TYPES ACCÈS
333
dynamiques (dynamic lists). Comme une variable dynamique est toujours repérée par un pointeur, le type de ce pointeur est le même que celui du lien. Pour une telle liste ce pointeur est naturellement appelé tête (head). Figure 15.5 Représentation schématique d’une liste dynamique de trois éléments. Tete Information 1
Information 2
Information 3
Il existe bien d’autres structures dynamiques comme les multilistes, les arbres, les graphes, etc. Leur étude dépasse les objectifs de cet ouvrage.
LISTES DYNAMIQUES
334
15.4 LISTES DYNAMIQUES 15.4.1
Définition
Une liste dynamique est donc une structure de longueur variable et prête à contenir une suite d’informations. Elle peut être définie sous forme d’une collection de variables dynamiques où chacune d’elles est un article et où les informations sont supposées du type T_Info. L’exemple 15.8 montre une tentative de définition des types nécessaires et du pointeur de tête. Exemple 15.8 Définitions nécessaires pour la réalisation d’une liste dynamique. type T_Lien is access T_Element;
-- ATTENTION...
type T_Element is record Information : T_Info; Suivant : T_Lien; end record;
-- Pour une variable dynamique
type T_Liste_Dynamique is record Tete : T_Lien; Queue : T_Lien; end record;
-- L'information, sur un seul champ -- Pour reperer l'element qui suit -- Pour une liste dynamique -- Tete et queue de la liste
Le type T_Liste_Dynamique (exemple 15.8) devrait permettre la gestion d’une ou de plusieurs listes formées d’une suite d’informations enregistrées dans des variables dynamiques de type T_Element et de deux pointeurs désignant leurs Tete et Queue. Ces listes seront toutes initialement vides puisqu’un pointeur possède la valeur null à sa création. Mais le compilateur va refuser le type T_Lien car T_Element n’est pas encore déclaré. Comment faire puisque si T_Element est déclaré le premier, c’est alors T_Lien qui n’est pas encore connu? 15.4.2
Prédéclaration d’un type pointé
La dépendance mutuelle de l’exemple 15.8 peut être brisée par une prédéclaration du type pointé T_Element (exemple 15.9), prédéclaration limitée au nom du type et, éventuellement, à ses discriminants. Exemple 15.9 Définitions complètes et correctes pour la réalisation d’une liste dynamique. type T_Element; -- Predeclaration type T_Lien is access T_Element; -- Le type acces type T_Element is record Information : T_Info;
-- Pour une variable dynamique -- L'information, sur un seul champ
LISTES DYNAMIQUES
Suivant : T_Lien; end record; type T_Liste_Dynamique is record Tete : T_Lien; Queue : T_Lien; end record;
335
-- Pour reperer l'element qui suit -- Pour une liste dynamique -- Tete et queue de la liste
La prédéclaration, appelée aussi déclaration incomplète, et la déclaration complète du type T_Element doivent se situer dans la même partie déclarative, à une exception près (§ 16.2.1). De plus, entre la prédéclaration et la déclaration complète, l’identificateur T_Element ne peut s’utiliser que comme type pointé. Si le type pointé possède des discriminants, ils peuvent figurer ou non dans la prédéclaration. Cette distinction, utile si le type pointé est privé (§ 16.2.1), peut être ignorée ici. 15.4.3
Gestion, manipulations de base
Les généralités mentionnées pour les listes (sect. 14.2) s’appliquent évidemment aux listes dynamiques. Comme pour les listes statiques, seules les queues et les piles dynamiques vont être étudiées.
QUEUES DYNAMIQUES
336
15.5 QUEUES DYNAMIQUES 15.5.1
Définition et création
La définition de cette structure est identique à celle de l’exemple 14.2 pour les listes quelconques. Exemple 15.10 Définition d’une queue dynamique. type T_Element;
-- Predeclaration
type T_Lien is access T_Element; type T_Element is
-- Le type acces
-- Pour une variable dynamique
record Information : T_Info; Suivant : T_Lien;
-- L'information, sur un seul champ -- Pour reperer l'element qui suit
end record; type T_Queue_Dynamique is
-- Pour une queue dynamique
record Tete : T_Lien; Queue : T_Lien;
-- Tete et queue de la queue
end record;
Le type T_Queue_Dynamique défini dans l’exemple 15.10 permet la déclaration (création) d’une ou de plusieurs queues formées d’une suite d’informations enregistrées dans des variables dynamiques de type T_Element et de deux pointeurs désignant leur Tete et leur Queue. Ces queues seront toutes initialement vides car Tete et Queue ont la valeur null à la déclaration (fig. 15.6). Figure 15.6 Définition schématique d’une queue dynamique initialement vide. Queue dynamique Tete Queue
Une queue dynamique peut maintenant être créée par une simple déclaration de variable de type T_Queue_Dynamique. 15.5.2
Manipulations de base
QUEUES DYNAMIQUES
337
Les opérations de base pour une queue dynamique s’implémentent sans difficulté particulière. Comme dans la variante statique, elles consistent plus précisément à: • • • • • • •
insérer une information en queue; supprimer l’information de tête; modifier l’information présente en tête de la queue; consulter l’élément de tête; savoir si la queue est vide; rechercher une information dans la queue; parcourir la queue, c’est-à-dire effectuer un traitement sur chacun de ses éléments.
La spécification d’un paquetage de gestion de queues dynamiques est donné dans l’exemple 15.11. Le corps de ce paquetage Queues_Dynamiques contiendra uniquement les corps des opérations énumérées dans la spécification, corps présentés dans les paragraphes qui suivent. Exemple 15.11 Spécification d’un paquetage de gestion de queues dynamiques. -- Bases de gestion de queues dynamiques package Queues_Dynamiques is type T_Info is ...;
-- Depend de l'application
type T_Element;
-- Predeclaration
type T_Lien is access T_Element; type T_Element is record
-- Le type acces
-- Pour une variable dynamique
Information : T_Info; -- L'information sur un seul champ Suivant : T_Lien; -- Pour reperer l'element qui suit end record; type T_Queue_Dynamique is -- Pour une queue dynamique record Tete : T_Lien; Queue : T_Lien;
-- Tete et queue de la queue
end record; Queue_Vide : exception; -- Levee si la queue est vide ------------------------------------------------------------- Insere Info en queue de La_Queue procedure Inserer ( La_Queue : in out T_Queue_Dynamique; Info : in T_Info ); ------------------------------------------------------------- Supprime l'information (en tete de La_Queue) qui est rendue -- dans Info. Leve Queue_Vide si la suppression est impossible -- (la queue est vide)
QUEUES DYNAMIQUES
338
procedure Supprimer ( T_Queue_Dynamique;
La_Queue : in out Info : out T_Info );
------------------------------------------------------------- Change l'information en tete de La_Queue. Leve Queue_Vide -- si la modification est impossible (la queue est vide) procedure Modifier ( La_Queue : in T_Queue_Dynamique; Info : in T_Info ); ------------------------------------------------------------- Retourne l'information en tete de La_Queue. Leve Queue_Vide -- si la consultation est impossible (la queue est vide) function Tete ( La_Queue : T_Queue_Dynamique ) return T_Info; ------------------------------------------------------------- Retourne True si La_Queue est vide, False sinon function Vide ( La_Queue : T_Queue_Dynamique ) return Boolean; ------------------------------------------------------------- Retourne True si l'information Info est presente dans la -- queue, False sinon function Recherche ( La_Queue : T_Queue_Dynamique; Info : T_Info ) return Boolean; ------------------------------------------------------------- Effectue un traitement sur tous les elements de la queue procedure Parcourir ( La_Queue : in T_Queue_Dynamique ); -----------------------------------------------------------end Queues_Dynamiques;
15.5.3
Insertion
Pour l’insertion, les informations sont chaînées, reliées les unes aux autres, selon la politique FIFO (§ 14.2.2). Il faut mettre en évidence les pointeurs Tete et Queue parce que, tels qu’ils sont utilisés, l’insertion et la suppression vont être faciles à réaliser (fig. 15.7). Figure 15.7 Queue dynamique après trois insertions. Queue dynamique Tete Queue
Information 1
Information 2
Information 3
QUEUES DYNAMIQUES
339
Exemple 15.12 Procédure d’insertion d’une information dans une queue dynamique. -- Insere Info en queue de La_Queue procedure Inserer ( La_Queue : in out T_Queue_Dynamique; Info : in T_Info ) is Pt_Nouveau : T_Lien := new T_Element'(Info, null);-- 1 begin -- Inserer -- Placer le nouvel element apres celui en queue; il faut -- distinguer si la queue est vide ou non if La_Queue.Tete = null then -- Queue vide La_Queue.Tete := Pt_Nouveau; else
-- 2 -- Queue non vide
La_Queue.Queue.Suivant := Pt_Nouveau; -- 3 end if; -- Modifier le chainage; le nouvel element devient celui en -- queue La_Queue.Queue := Pt_Nouveau; -- 4 end Inserer;
-- 5
La procédure Inserer (exemple 15.12) est plus simple que dans le cas statique (§ 14.4.3), mais elle mérite néanmoins quelques explications. L’insertion du premier élément est un cas particulier car le pointeur Tete ne repère encore aucune variable dynamique; il faut donc simplement lui donner l’adresse du premier élément. Dans le cas d’une queue non vide, le nouvel élément doit suivre (politique FIFO) le dernier existant, ce qui explique que ce nouvel élément est chaîné derrière l’élément de queue. Puis, dans tous les cas, l’élément de queue est le nouvel élément. Les figures 15.8 et 15.9 illustrent les deux cas mentionnés.
QUEUES DYNAMIQUES
340
Figure 15.8 Insertion d’un élément à partir d’une queue dynamique vide.
Ligne 1: création d’un nouvel élément Tete Queue Pt_Nouveau Information 1
Instruction 2: Tete
Information 1
Queue Pt_Nouveau
L’instruction 3 ne s’exécute pas Instruction 4: Tete
Information 1
Queue Pt_Nouveau
Ligne 5: Tete Queue
Information 1
QUEUES DYNAMIQUES
341
Figure 15.9 Insertion d’un élément à partir d’une queue dynamique non vide (un élément existant).
Instruction 1: création d’un nouvel élément Tete
Information 1
Queue Pt_Nouveau Information 2
L’instruction 2 ne s’exécute pas Instruction 3: Tete
Information 1
Information 2
Information 1
Information 2
Information 1
Information 2
Queue Pt_Nouveau
Instruction 4: Tete Queue Pt_Nouveau
Ligne 5: Tete Queue
QUEUES DYNAMIQUES
15.5.4
342
Suppression
Pour la suppression, les informations sont extraites les unes après les autres, en commençant par l’information de tête (fig. 15.19). Figure 15.10 Queue dynamique après N insertions et N-1 suppressions. Queue dynamique Tete
Information N
Queue
Lorsque la dernière variable dynamique a été supprimée de la queue, toute tentative de suppression doit lever l’exception Queue_Vide (fig. 15.11). Figure 15.11 Tentative de suppression alors que la queue dynamique est vide. Queue dynamique
Tete de Queue _ Vi eue u Q
La réalisation de la procédure de suppression est illustrée dans l’exemple 15.13. La place mémoire de la variable dynamique éliminée de la queue devrait être récupérée (sect. 15.7), ce qui n’est pas explicitement réalisé dans cet exemple. Un ramasse-miettes serait ici très utile. Exemple 15.13 Procédure de suppression d’une information dans une queue dynamique. -- Supprime l'information (en tete de La_Queue) qui est rendue -- dans Info. Leve Queue_Vide si la suppression est impossible (la -- queue est vide) procedure Supprimer ( La_Queue : in out T_Queue_Dynamique; Info : out T_Info ) is begin -- Supprimer -- 1 -- Cas si la queue est vide if La_Queue.Tete = null then raise Queue_Vide;
QUEUES DYNAMIQUES
343
end if; -- Recuperer l'information en tete Info := La_Queue.Tete.Information; -- L'element de tete est l'element supprime, et celui qui -- le suit est dorenavant en tete. La place memoire n'est -- pas recuperee La_Queue.Tete := La_Queue.Tete.Suivant; -- 2 -- Cas du dernier element supprime if La_Queue.Tete = null then La_Queue.Queue := null;
-- 3
end if; end Supprimer;
Cette procédure supprime l’élément de tête de la queue (fig. 15.12). Lorsque le dernier élément a été éliminé (fig. 15.13), les pointeurs Tete et Queue retrouvent la valeur initiale null. Les variables enlevées de la queue deviennent inaccessibles, ce qui peut être toléré puisque l’information contenue n’est plus utile. Figure 15.12 Suppression d’un élement à partir d’une queue dynamique de longueur 2. Ligne 1:
Tete
Information 1
Information 2
Information 1
Information 2
Queue
Instruction 2:
Tete Queue
L’instruction 3 ne s’exécute pas
QUEUES DYNAMIQUES
344
Figure 15.13 Suppression du dernier élement d’une queue dynamique. Ligne 1: Tete
Information 2
Queue
Instruction 2: Tete
Information 2
Queue Instruction 3: Tete
Information 2
Queue
15.5.5
Modification
La modification d’une information n’est possible qu’en tête de la queue, par définition de celle-ci (fig. 15.14). Figure 15.14 Modification de l’information de tête dans une queue dynamique. Queue dynamique Information à modifier
Tete Queue
Information 1
Information 2
QUEUES DYNAMIQUES
345
Lorsque la queue est vide, toute tentative de modification doit lever l’exception Queue_Vide pour en avertir l’auteur (fig. 15.15). Figure 15.15 Tentative de modification alors que la queue dynamique est vide. Queue dynamique
Tete de _ Vi Queue eue u Q
La réalisation de la procédure de modification est facile, illustrée dans l’exemple 15.14. Exemple 15.14 Procédure de modification d’une information dans une queue dynamique. -- Change l'information en tete de La_Queue. Leve Queue_Vide si la -- modification est impossible (la queue est vide) procedure Modifier ( La_Queue : in T_Queue_Dynamique; Info : in T_Info ) is begin -- Modifier -- Cas si la queue est vide if La_Queue.Tete = null then raise Queue_Vide; end if; -- Modifier l'information en tete La_Queue.Tete.Information := Info; end Modifier;
15.5.6
Consultation
La consultation d’une information n’est possible qu’en tête de la queue, par définition de celle-ci. Cette opération est donc très proche de la modification, ce qui permet de donner directement la fonction adéquate dans l’exemple 15.15. Exemple 15.15 Fonction de consultation d’une information dans une queue dynamique. -- Retourne l'information en tete de La_Queue. Leve Queue_Vide si -- la consultation est impossible (la queue est vide)
QUEUES DYNAMIQUES
346
function Tete ( La_Queue : T_Queue_Dynamique ) return T_Info is begin -- Tete -- Cas si la queue est vide if La_Queue.Tete = null then raise Queue_Vide; end if; -- Retourner l'information en tete return La_Queue.Tete.Information; end Tete;
15.5.7
Queue vide
Le fait qu’une queue soit vide implique que tous les éléments insérés ont été extraits, ou éventuellement que la queue n’a pas encore servi. Cette information est très souvent utilisée dans les algorithmes qui mettent en œuvre une ou plusieurs queues. La fonction correspondante est présentée dans l’exemple 15.16. Exemple 15.16 Fonction retournant l’état (vide/non vide) d’une queue dynamique. -- Retourne True si La_Queue est vide, False sinon function Vide (La_Queue : T_Queue_Dynamique) return Boolean is begin -- Vide -- La queue est vide si le pointeur de tete ne repere aucun -- element return La_Queue.Tete = null; end Vide;
15.5.8
Recherche
L’algorithme de recherche séquentielle (§ 14.4.8) s’applique également aux queues dynamiques. L’exemple 15.17 donne la fonction réalisant cette opération. Exemple 15.17 Fonction de recherche d’une information dans une queue dynamique. -- Retourne True si l'information Info est presente dans la queue, -- False sinon function Recherche ( La_Queue : T_Queue_Dynamique; Info : T_Info ) return Boolean is -- Repere l'element courant Pt_Courant : T_Lien := La_Queue.Tete; begin -- Recherche -- Tant que la queue n'est pas depassee et l'information pas
QUEUES DYNAMIQUES
347
-- trouvee while Pt_Courant /= null and then Info /= Pt_Courant.Information loop -- Passer a l'element suivant Pt_Courant := Pt_Courant.Suivant; end loop; -- Si la fin de la queue n'est pas depassee, l'information a -- ete trouvee return Pt_Courant /= null; end Recherche;
La présence de la forme de contrôle en raccourci and then (§ 3.4.3) est le seul point délicat de cet exemple; elle est nécessaire pour éviter de consulter l’Information si Pt_Courant ne pointe sur aucun élément. A noter que le cas d’une queue vide ne provoque cette fois aucune exception; la recherche d’une information dans une queue vide aboutit immédiatement et simplement à la valeur False (information introuvable), ce qui est conforme à la logique et à la pratique courante. 15.5.9
Parcours
Le parcours d’une queue dynamique est très semblable à celui présenté pour une queue statique (§ 14.4.9). L’exemple 15.18 présente une procédure réalisant un tel parcours où l’on suppose qu’une procédure Traiter effectue le traitement voulu. Exemple 15.18 Procédure de parcours d’une queue dynamique. -- Effectue un traitement sur tous les elements de la queue. -- Le traitement est suppose fait dans la procedure interne -- Traiter procedure Parcourir ( La_Queue : in T_Queue_Dynamique ) is -- Repere l'element courant Pt_Courant : T_Lien := La_Queue.Tete; procedure Traiter ( Info : in out T_Info ) is ... end Traiter; begin -- Parcourir -- Prendre les elements les uns apres les autres while Pt_Courant /= null loop -- Effectuer le traitement Traiter ( Pt_Courant.Information ); -- Passer a l'element suivant Pt_Courant := Pt_Courant.Suivant; end loop;
QUEUES DYNAMIQUES
348
end Parcourir;
Comme pour la recherche et pour les mêmes raisons, le cas d’une queue vide ne provoque aucune exception. 15.5.10 Exemple d’utilisation d’une queue dynamique Lors de la demande d’impression d’un fichier sur une imprimante partagée, cette impression n’est pas toujours réalisée immédiatement (l’imprimante peut être déjà occupée à imprimer un texte). La demande est alors mise en queue, en général derrière toutes les demandes en cours et non encore satisfaites. Or la queue de ces demandes est gérée dynamiquement car le nombre maximal de demandes d’impression simultanées n’est pas quantifiable. Une gestion statique de la queue pourrait aboutir à des messages d’avertissement aux utilisateurs de l’imprimante alors que leur demande d’impression semble parfaitement acceptable. De tels messages risqueraient de ne pas donner une bonne image du logiciel de gestion de l’imprimante...
PILES DYNAMIQUES
349
15.6 PILES DYNAMIQUES 15.6.1
Définition et création
La définition de la structure de pile dynamique ressemble fortement à celle d’une queue dynamique (§. 15.5.1). La seule différence réside en l’absence du deuxième champ pointeur du type T_Pile_Dynamique puisqu’une pile n’est accédée qu’en un seul endroit: au sommet. Exemple 15.19 Définition d’une pile dynamique. type T_Element;
-- Predeclaration
type T_Lien is access T_Element; type T_Element is
-- Le type acces
-- Pour une variable dynamique
record Information : T_Info; Suivant : T_Lien;
-- L'information, sur un seul champ -- Pour reperer l'element qui suit
end record; type T_Pile_Dynamique is
-- Pour une pile dynamique
record Sommet : T_Lien;
-- Sommet de la pile
end record;
Le type T_Pile_Dynamique défini dans l’exemple 15.19 permet la déclaration (création) d’une ou de plusieurs queues formées d’une suite d’informations enregistrées dans des variables dynamiques de type T_Element et d’un pointeur désignant leur Sommet. Ces piles seront toutes initialement vides car Sommet a la valeur null à la déclaration (fig. 15.16). Figure 15.16 Définition schématique d’une pile dynamique initialement vide. Pile dynamique
Sommet
Le champ pointeur Suivant repère en fait l’élément au-dessous de lui si l’on imagine les éléments de la pile empilés les uns sur les autres comme une pile d’assiettes. Par contre, pour les représentations schématiques, une pile est représentée horizontalement, comme une liste.
PILES DYNAMIQUES
350
L’utilisation d’un article à un seul champ pour le type T_Pile_Dynamique se justifie si la définition de ce type doit être étendue avec des champs supplémentaires, par exemple la longueur de la pile. Dans le cas contraire, une définition simplifiée comme celle de l’exemple 15.20 est tout à fait adéquate. Dans l’idée de faciliter l’extension de la définition, celle de l’exemple 15.19 sera conservée pour la suite. Exemple 15.20 Définition simplifiée d’une pile dynamique. type T_Element;
-- Predeclaration
-- Le type d'une pile dynamique type T_Pile_Dynamique is access T_Element; type T_Element is
-- Pour une variable dynamique
record Information : T_Info;
-- L'information, sur un seul -- champ
Suivant : T_Pile_Dynamique;
-- Pour reperer l'element qui -- suit
end record;
Une pile dynamique peut maintenant être créée par une simple déclaration de variable de type T_Pile_Dynamique. 15.6.2
Manipulations de base
Les opérations de base (sect. 14.2) pour une pile dynamique s’implémentent sans difficulté particulière et sont très semblables à celles d’une queue. La spécification d’un paquetage de gestion de piles dynamiques est donné dans l’exemple 15.21. Exemple 15.21 Spécification d’un paquetage de gestion de piles dynamiques. -- Bases de gestion de piles dynamiques package Piles_Dynamiques is type T_Info is ...;
-- Depend de l'application
type T_Element;
-- Predeclaration
type T_Lien is access T_Element;-- Le type acces type T_Element is record Information : T_Info; Suivant : T_Lien;
-- Pour une variable dynamique -----
L'information sur un seul champ Pour reperer l'element qui suit
PILES DYNAMIQUES
351
end record; type T_Pile_Dynamique is record Sommet : T_Lien;
-- Pour une pile dynamique -- Sommet de la pile
end record; Pile_Vide : exception;
-- Levee si la pile est vide
------------------------------------------------------------- Insere Info au sommet de Pile procedure Empiler ( Pile : in out T_Pile_Dynamique; Info : in T_Info ); ------------------------------------------------------------- Supprime l'information (au sommet de Pile) qui est rendue -- dans Info. Leve Pile_Vide si la suppression est impossible -- (la pile est vide) procedure Desempiler ( Pile : in out T_Pile_Dynamique; Info : out T_Info ); ------------------------------------------------------------- Change l'information du sommet de Pile. Leve Pile_Vide si la -- modification est impossible (la pile est vide) procedure Modifier ( Pile : in T_Pile_Dynamique; Info : in T_Info ); ------------------------------------------------------------- Retourne l'information du sommet de Pile. Leve Pile_Vide si -- la consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile_Dynamique ) return T_Info; ------------------------------------------------------------- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile_Dynamique ) return Boolean; ------------------------------------------------------------- Retourne True si l'information Info est presente dans la -- pile, False sinon function Recherche ( Pile : T_Pile_Dynamique; Info : T_Info ) return Boolean; -- Effectue un traitement sur tous les elements de la pile procedure Parcourir ( Pile : in T_Pile_Dynamique ); -----------------------------------------------------------end Piles_Dynamiques;
Le corps de ce paquetage Piles_Dynamiques contiendra uniquement les corps des opérations énumérées dans la spécification, corps présentés dans les paragraphes qui suivent. 15.6.3
Insertion
Pour l’insertion les informations sont chaînées, reliées les unes aux autres,
PILES DYNAMIQUES
352
selon la politique LIFO (§ 14.2.2). Une fois le premier élément placé, chaque nouvel élément vient en tête et repousse les éléments existants. Insérer une information est appelé empiler (push). La figure 15.17 présente l’état d’une pile après trois insertions, alors que la procédure d’insertion est illustrée dans l’exemple 15.22. Figure 15.17 Pile dynamique après trois insertions. Pile dynamique Sommet
Information 3
Information 2
Information 1
Exemple 15.22 Procédure d’insertion d’une information dans une pile dynamique. -- Insere Info au sommet de Pile procedure Empiler ( Pile : in out T_Pile_Dynamique; Info : in T_Info ) is -- Pour reperer le nouvel element Pt_Nouveau : T_Lien := new T_Element'(Info, null);-- 1 begin -- Inserer Pt_Nouveau.Suivant := Pile.Sommet;
-- 2
Pile.Sommet := Pt_Nouveau;
-- 3
end Empiler;
-- 4
Après sa création, le nouvel élément devra précéder (politique LIFO) le premier existant, ce qui explique que le sommet actuel de la pile est chaîné au nouvel élément. Puis le nouvel élément devient l’élément au sommet. Les figures 15.18 et 15.19 illustrent l’empilement de deux éléments, la première fois dans une pile vide, la seconde fois lorsque la pile possède déjà un élément.
PILES DYNAMIQUES
353
Figure 15.18 Insertion d’un élément à partir d’une pile dynamique vide.
Ligne 1, création d’un nouvel élément:
Sommet
Pt_Nouveau Information 1
Instruction 2: pas de changement sur le schéma précédent
Instruction 3: Sommet
Information 1
Pt_Nouveau
Ligne 4: Sommet
Information 1
PILES DYNAMIQUES
354
Figure 15.19 Insertion d’un élément à partir d’une pile dynamique non vide (un élément existant). Instruction 1, création d’un nouvel élément: Sommet
Information 1
Pt_Nouveau Information 2
Instruction 2: Sommet
Information 1
Pt_Nouveau Information 2
Instruction 3: Sommet
Information 1
Pt_Nouveau Information 2
Ligne 4: Sommet
Information 2
Information 1
PILES DYNAMIQUES
355
Le code de la procédure Empiler donnée dans l’exemple 15.22 peut (et doit) être condensé en une seule instruction (exemple 15.23). La forme développée a été présentée pour illustrer graphiquement les étapes de l’insertion. Exemple 15.23 Procédure condensée d’insertion d’une information dans une pile dynamique. -- Insere Info au sommet de Pile procedure Empiler ( Pile : in out T_Pile_Dynamique; Info : in T_Info ) is begin -- Empiler Pile.Sommet := new T_Element'(Info, Pile.Sommet); end Empiler;
15.6.4
Suppression
Pour la suppression, les informations sont extraites les unes après les autres, en commençant par l’information au sommet (fig. 15.20). Supprimer une information est communément appelé désempiler (pop). Figure 15.20 Pile dynamique après N insertions et N-3 suppressions. Pile dynamique Sommet
Information 3
Information 2
Information 1
Lorsque la dernière information de la pile a été désempilée, toute tentative de suppression doit lever l’exception Pile_Vide pour en avertir l’auteur (fig. 15.21). Cette situation est en fait plus généralement celle de la pile vide. Figure 15.21 Tentative de suppression alors que la pile est vide. Pile dynamique
P
Sommet de _ Vi ile
PILES DYNAMIQUES
356
La réalisation de la procédure de suppression est alors facile, illustrée dans l’exemple 15.24. La place mémoire de la variable dynamique éliminée de la queue devrait être récupérée (sect. 15.7), ce qui n’est pas explicitement réalisé dans cet exemple (fig. 15.22); un ramasse-miettes serait ici très utile. Exemple 15.24 Procédure de suppression d’une information dans une pile dynamique. -- Supprime l'information (au sommet de Pile) qui est rendue dans -- Info. Leve Pile_Vide si la suppression est impossible (la pile -- est vide) procedure Desempiler ( Pile : in out T_Pile_Dynamique; Info : out T_Info ) is begin -- Desempiler -- 1 -- Cas si la pile est vide if Pile.Sommet = null then raise Pile_Vide; end if; -- Recuperer l'information au sommet Info := Pile.Sommet.Information; -- L'element au sommet est supprime, celui qui le suit est -- dorenavant au sommet; la place memoire n'est pas recuperee Pile.Sommet := Pile.Sommet.Suivant; -- 2 end Desempiler;
Figure 15.22 Suppression d’un élément à partir d’une pile dynamique de longueur 2. Ligne 1: Sommet
Information 2
Information 1
Information 2
Information 1
Instruction 2:
Sommet
Le cas particulier de suppression du dernier élément ne pose aucune difficulté; le champ pointeur Sommet retrouve simplement la valeur initiale null.
PILES DYNAMIQUES
15.6.5
357
Modification
La modification d’une information n’est possible qu’au sommet de la pile, par définition de celle-ci (fig. 15.23). Lorsque la pile est vide, toute tentative de modification doit lever l’exception Pile_Vide pour en avertir l’auteur (fig. 15.24). Enfin, la réalisation de la procédure de modification est facile, illustrée dans l’exemple 15.25. Figure 15.23 Modification de l’information au sommet dans une pile dynamique. Pile dynamique
Sommet
Information 3
Information 2
Information 1
Information à modifier
Figure 15.24 Tentative de modification alors que la pile dynamique est vide. Pile dynamique
P
Sommet de _ Vi ile
Exemple 15.25 Procédure de modification d’une information dans une pile dynamique. -- Change l'information du sommet de Pile. Leve Pile_Vide si la -- modification est impossible (la pile est vide) procedure Modifier ( Pile : in T_Pile_Dynamique; Info : in T_Info ) is begin -- Modifier -- Cas si la pile est vide if Pile.Sommet = null then raise Pile_Vide; end if;
PILES DYNAMIQUES
358
-- Modifier l'information du sommet Pile.Sommet.Information := Info; end Modifier;
15.6.6
Consultation
La consultation d’une information n’est possible qu’au sommet de la pile, par définition de celle-ci. Cette opération est donc très proche de la modification, ce qui permet de donner directement la fonction adéquate dans l’exemple 15.26. Exemple 15.26 Fonction de consultation d’une information dans une pile dynamique. -- Retourne l'information du sommet de Pile. Leve Pile_Vide si la -- consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile_Dynamique ) return T_Info is begin -- Sommet -- Cas si la pile est vide if Pile.Sommet = null then raise Pile_Vide; end if; -- Retourner l'information du sommet return Pile.Sommet.Information; end Sommet;
15.6.7
Pile vide
Le fait qu’une pile soit vide implique que tous les éléments insérés ont été extraits, ou éventuellement que la pile n’a encore pas servi. Cette information est très souvent utilisée dans les algorithmes mettant en œuvre une ou plusieurs piles. La fonction correspondante est présentée dans l’exemple 15.27. Exemple 15.27 Fonction retournant l’état (vide/non vide) d’une pile dynamique. -- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile_Dynamique ) return Boolean is begin -- Vide -- La pile est vide si le pointeur du sommet ne repere aucun -- element return Pile.Sommet = null; end Vide;
PILES DYNAMIQUES
15.6.8
359
Recherche
L’algorithme de recherche séquentielle (§ 14.4.8) s’applique également aux piles dynamiques. L’exemple 15.28 donne la fonction réalisant cette opération. Exemple 15.28 Fonction de recherche d’une information dans une pile dynamique. -- Retourne True si l'information Info est presente dans la pile, -- False sinon function Recherche ( Pile : T_Pile_Dynamique; Info : T_Info ) return Boolean is -- Repere l'element courant Pt_Courant : T_Lien := Pile.Sommet; begin -- Recherche -- Tant que la queue n'est pas depassee et l'information pas -- trouvee while Pt_Courant /= null and then Info /= Pt_Courant.Information loop -- Passer a l'element suivant Pt_Courant := Pt_Courant.Suivant; end loop; -- Si le dernier element n'a pas ete depasse, l'information a -- ete trouvee return Pt_Courant /= null; end Recherche;
La présence de la forme de contrôle en raccourci and then (§ 3.4.3) est le seul point délicat de cet exemple; elle est nécessaire pour éviter de consulter l’Information si Pt_Courant ne pointe sur aucun élément. A noter que le cas d’une pile vide ne provoque cette fois aucune exception; la recherche d’une information dans une pile vide aboutit immédiatement et simplement à la valeur False (information introuvable), ce qui est conforme à la logique et à la pratique courante. 15.6.9
Parcours
Le parcours d’une pile est semblable à celui appliqué à une queue (§ 14.4.9) en commençant par le sommet de la pile, avec la même remarque concernant le traitement à effectuer, à savoir que ce traitement peut ou non modifier les éléments ou la structure de la pile. L’exemple 15.29 présente une procédure réalisant un parcours où l’on suppose qu’une procédure Traiter effectue le traitement voulu.
PILES DYNAMIQUES
360
Exemple 15.29 Procédure de parcours d’une pile dynamique. -- Effectue un traitement sur tous les elements de la pile. Le -- traitement est suppose fait dans la procedure interne Traiter procedure Parcourir ( Pile : in T_Pile_Dynamique ) is -- Repere l'element courant Pt_Courant : T_Lien := Pile.Sommet; procedure Traiter ( Info : in out T_Info ) is ... end Traiter; begin -- Parcourir -- Prendre les elements les uns apres les autres while Pt_Courant /= null loop -- Effectuer le traitement Traiter ( Pt_Courant.Information ); -- Passer a l'element suivant Pt_Courant := Pt_Courant.Suivant; end loop; end Parcourir;
Comme pour la recherche et pour les mêmes raisons, le cas d’une pile vide ne provoque aucune exception. 15.6.10 Exemple d’utilisation d’une pile dynamique Parmi toutes les applications simples de l’utilisation d’une pile dynamique, l’inversion des éléments d’une suite est probablement l’une des plus connues. Il s’agit simplement de prendre tous les éléments et de les restituer dans l’ordre inverse. L’exemple 15.30 montre comment cet algorithme fonctionne en supposant que les éléments (de type T_Info) présents dans un tableau de type T_Suite sont passés en paramètre de la procédure Inverser. L’inversion est réalisée grâce à la gestion LIFO (§ 14.2.2) de la pile. Exemple 15.30 Inversion des éléments d’une suite grâce à une pile dynamique. -- Inverse l'ordre des elements de Suite procedure Inverser ( Suite : in out T_Suite ) is -- Pour l'inversion Pile : Piles_Dynamiques.T_Pile_Dynamique; begin -- Inverser -- Prendre les elements les uns apres les autres for Position in Suite'Range loop -- Empiler l'element courant Piles_Dynamiques.Empiler ( Pile, Suite (Position) ); end loop;
PILES DYNAMIQUES
-- Retirer les elements dans l'ordre inverse for Position in Suite'Range loop -- Desempiler un element Piles_Dynamiques.Desempiler ( Pile, Suite (Position) ); end loop; end Inverser;
361
RESTITUTION DE LA MÉMOIRE
362
15.7 RESTITUTION DE LA MÉMOIRE A chaque appel de l’allocateur new, une nouvelle portion mémoire (variable dynamique) est réservée pour l’application. Comme les sections et paragraphes précédents l’ont montré, cette mémoire est utilisée par exemple pour construire des structures de données dynamiques. Il se pose alors la question de la libération (restitution) de cette mémoire lorsque celle-ci n’est plus utilisée. Une fois l’application terminée, le système d’exploitation a naturellement la charge de récupérer toute la mémoire dont l’application a eu besoin. Mais en cours d’exécution, il peut être pratique de réutiliser des portions mémoire devenues inaccessibles afin de permettre la création de nouvelles structures sans besoins supplémentaires en place mémoire. En fait, cette récupération peut s’effectuer à plusieurs niveaux. Tout d’abord, la place mémoire, occupée par toutes les variables dynamiques désignées par des pointeurs d’un type accès donné, est restituée lorsque l’exécution quitte la portée (sect. 4.4) de ce type accès. Le système s’en occupe lui-même, et il n’y a aucun risque d’obtenir des pointeurs fantômes, c’est-à-dire des pointeurs qui repèrent des variables dynamiques dont l’emplacement mémoire est déjà réutilisé pour autre chose! En effet, tous les pointeurs du type accès auront également cessé d’exister lorsque la portée du type sera abandonnée. Une variable dynamique devenue inaccessible (§ 15.2.7) peut être récupérée par le système en vue de la réutilisation, dans le même programme, de la place mémoire occupée. On appelle ramasse-miettes (garbage collector) la partie du système chargée de cette tâche. La norme Ada permet mais n’impose pas qu’une implémentation fournisse un tel ramasse-miettes. Finalement, le programmeur dispose d’un outil pour gérer lui-même la récupération de la place mémoire occupée par une variable dynamique. Il s’agit d’une unité de bibliothèque (§ 10.6.1), en fait une procédure générique (sect. 17.5) prédéfinie appelée Ada.Unchecked_Deallocation et qui s’utilise comme le montre l’exemple 15.31. Exemple 15.31 Utilisation de la procédure générique Ada.Unchecked_Deallocation. with Ada.Unchecked_Deallocation; -- ... procedure Exemple_15_31 is type T_Element is ...; type T_Pt_Element is access T_Element;
-- Pas de use
-- Un type quelconque -- Le type acces
-- Utilisation de la procedure Ada.Unchecked_Deallocation en -- creant une procedure de liberation de la memoire adaptee aux -- types de cet exemple procedure Liberer is
RESTITUTION DE LA MÉMOIRE
363
new Ada.Unchecked_Deallocation (T_Element, T_Pt_Element); Pt_Element : T_Pt_Element;
-- Une variable auxiliaire
begin -- Exemple_15_31 -- Creation d'une variable dynamique Pt_Element := new T_Element; -- Utilisation de la variable dynamique ... -- Restitution de la memoire occupee par la variable dynamique Liberer ( Pt_Element ); ...
La création de la procédure Liberer ressemble fortement à l’opération nécessaire pour effectuer des entrées-sorties sur des valeurs énumérées par exemple (§ 5.2.5) où le nom du type énumératif est placé entre les parenthèses. Ici, il faut mentionner d’abord le nom du type des variables dynamiques, puis le nom du type accès. Après l’exécution de la procédure Liberer, le pointeur Pt_Element obtient la valeur null. Attention cependant à ne pas tomber dans le piège des pointeurs fantômes comme illustré dans l’exemple 15.32. Dans de tels cas, l’exécution du programme devient imprévisible... Exemple 15.32 Création volontaire d’un pointeur fantôme. with Ada.Unchecked_Deallocation;
-- Pas de use
-- ... procedure Exemple_15_32 is type T_Element is ...; type T_Pt_Element is access T_Element;
-- Un type quelconque -- Le type acces
-- Utilisation de la procedure Ada.Unchecked_Deallocation en -- creant une procedure de liberation de la memoire adaptee aux -- types de cet exemple procedure Liberer is new Ada.Unchecked_Deallocation (T_Element, T_Pt_Element); Pt_Element : T_Pt_Element;
-- Une variable auxiliaire
Fantome : T_Pt_Element;
-- Va devenir un pointeur fantome
begin -- Exemple_15_32 -- Creation d'une variable dynamique Pt_Element := new T_Element; -- Utilisation de la variable dynamique ... -- Les deux pointeurs reperent la meme variable dynamique Fantome := Pt_Element; -- Restitution de la memoire occupee par la variable dynamique Liberer ( Pt_Element );
RESTITUTION DE LA MÉMOIRE
364
...
Dès l’exécution de Liberer, Fantome repère une variable dynamique dont l’emplacement mémoire a peut-être déjà été réutilisé par le système. Il ne se passe rien de problématique si une nouvelle adresse (ou la valeur null) est ensuite affectée à Fantome, ce qui est couramment réalisé dans la pratique. Par contre, si le programme essaie d’accéder via Fantome à la variable dynamique qui n’existe plus, son comportement n’est plus prédictible.
AUTRES ASPECTS DES TYPES ACCÈS
365
15.8 AUTRES ASPECTS DES TYPES ACCÈS 15.8.1
Types accès généralisés
A l’instar d’autres langages, Ada permet la déclaration de pointeurs généralisés sur des objets statiques à condition que ces objets soient prévus pour cela. Des constantes ou des variables de n’importe quel type simple ou composé, de même que des éléments de tableaux ou des champs d’articles, peuvent ainsi être référencées pourvu que leur déclaration comporte le mot réservé aliased qui indique au compilateur que l’accès à l’entité déclarée va se faire également par un pointeur. Ces entités sont de ce fait appelées aliasées. La déclaration d’un type accès généralisé (permettant la création de pointeurs généralisés sur des objets statiques) se caractérise par la présence de l’un des mots réservés all ou constant placé après access. Le mot réservé all permet de lire et de modifier la variable référencée par le pointeur alors que constant ne permet que la lecture de la constante ou de la variable référencée par le pointeur. L’attribut Access, appliqué à un identificateur de constante ou variable aliasée, donne l’adresse de cette constante ou variable, adresse qui peut alors être affectée à un pointeur généralisé (exemple 15.33). Il faut bien entendu que les types concordent lors de cette affectation. Exemple 15.33 Déclaration de types accès généralisés et de pointeurs de ces types. -- Deux types acces generalises type T_Pt_Gen_Integer is access all Integer; type T_Pt_Gen_String is access constant String; -- aliased permet de referencer les objets qui suivent par des -- pointeurs generalises Entier : aliased Integer := 5; Deux : aliased constant Integer := 2; Bonjour : aliased constant String := "Bonjour"; -- Deux pointeurs generalises, de valeur initiale null (mise -- automatiquement pour tous les pointeurs) Pt_Gen_Integer : T_Pt_Gen_Integer; Pt_Gen_String : T_Pt_Gen_String; ... -- Utilisation de l'attribut Access pour obtenir l'adresse des -- objets Pt_Gen_Integer := Entier'Access; -- Pt_Gen_Integer reference -- Entier Pt_Gen_Integer.all := 8;
-- Entier vaut maintenant 8
Pt_Gen_Integer := Deux'Access;
-- INTERDIT, Deux est une -- constante
Pt_Gen_String := Bonjour'Access;
-- Pt_Gen_String reference -- Bonjour
AUTRES ASPECTS DES TYPES ACCÈS
Pt_Gen_String (1) := 'b'; ...
366
-- INTERDIT, Bonjour est une -- constante
Lorsqu’elle est référencée par un pointeur généralisé, une constante ou variable aliasée peut donc être accédée soit directement par son identificateur, soit indirectement par le pointeur qui la repère. Afin d’éviter des cas de pointeurs fantômes (§ 15.7.1), c’est-à-dire ici des pointeurs généralisés qui désignent des objets statiques qui n’existent plus, la durée de vie de tout objet aliasé doit être au moins aussi longue que celle du type accès généralisé. La norme Ada précise quelques points supplémentaires non abordés ici. La création de paquetages d’interface (sect. 19.3) entre des bibliothèques de sousprogrammes écrits en langage C et des applications Ada est une des raisons de l’existence des types accès généralisés. En effet, la notion de pointeur sur des objets statiques s’utilise couramment en C. 15.8.2
Types accès à sous-programme
Ada permet la déclaration de pointeurs sur des sous-programmes. La déclaration d’un type accès à sous-programme se caractérise par la présence d’une spécification de sous-programme sans nom (!) après le mot réservé access. Des pointeurs d’un tel type permettent l’appel de tout sous-programme possédant le même profil (sect. 4.8). L’attribut Access, appliqué à un identificateur de sous-programme, donne l’adresse de ce sous-programme, adresse qui peut alors être affectée à un pointeur à sous-programme (exemple 15.34). Il faut cependant que les profils concordent pour que cette affectation soit acceptée par le compilateur. Exemple 15.34 Déclaration de types à sous-programmes et de pointeurs de ces types. -- Deux types acces a sous-programme type T_Pt_Procedure is access procedure; -- Pas de parametre type T_Pt_Fonction is access function (F:Float) return Float; procedure P is begin ... end P; -- Deux pointeurs a sous-programme Pt_Procedure : T_Pt_Procedure; Pt_Fonction : T_Pt_Fonction; -- Pour l'appel de la fonction Nombre_Reel : Float; ... -- Utilisation de l'attribut Access pour l'adresse des sous-- programmes Pt_Procedure := P'Access; -- Pt_Procedure reference P
AUTRES ASPECTS DES TYPES ACCÈS
367
Pt_Procedure.all;
-- Appel de la procedure P
-- Pt_Fonction reference la fonction logarithme du paquetage -- de bibliotheque Ada.Numerics.Elementary_Functions -- [ARM A.5.2] Pt_Fonction := Ada.Numerics.Elementary_Functions.Log'Access; Nombre_Reel := Pt_Fonction (10.0); -- Appel de la fonction Log
Lorsqu’il est référencé par un pointeur à sous-programme, un sous-programme peut donc être appelé soit directement par son identificateur, soit indirectement par le pointeur qui le référence. Les appels s’effectuent comme présentés dans l’exemple 15.34 en précisant que le mot réservé all doit être présent si l’appel ne comporte pas de paramètres effectifs comme avec Pt_Procedure.all, alors qu’il peut être omis en présence d’un ou de plusieurs paramètres comme pour Pt_Fonction(10.0). L’exemple 15.35 présente, lui, le passage en paramètre d’un sous-programme grâce à un type accès à sous-programme. Afin d’éviter les pointeurs fantômes (§ 15.7.1), c’est-à-dire ici des pointeurs à sous-programme qui désignent des sous-programmes qui n’existent plus, la durée de vie d’un sous-programme doit être au moins aussi longue que celle du type accès à sous-programme. Exemple 15.35 Sous-programmes en paramètre grâce à un type accès à sous-programme. with Ada.Numerics.Elementary_Functions; use Ada.Numerics.Elementary_Functions; -- Illustre le passage en parametre de sous-programmes procedure Exemple_15_35 is type T_Pt_Fonction is access function (F:Float) return Float; ------------------------------------------------------------- Calcule la valeur approchee de l'integrale entre deux bornes -- de la fonction passee en parametre, sans aucune verification procedure Integrer (
Pt_Fonction : in T_Pt_Fonction; Borne_Inf : in Float; Borne_Sup : in Float; Nombre_Pas : in Integer; Aire : out Float ) is
-- Pour l'avance entre les deux bornes X : Float := Borne_Inf; Pas : Float := (Borne_Sup – Borne_Inf) / Float(Nombre_Pas); begin -- Integrer Aire := 0.0; for I in 1..Nombre_Pas loop -- Aire d'un rectangle de largeur Pas et de longueur -- moyenne entre la valeur de la fonction en X et en X+Pas
AUTRES ASPECTS DES TYPES ACCÈS
368
Aire := Aire + Pas * ((Pt_Fonction(X) + Pt_Fonction(X+Pas)) / 2.0); X := X + Pas;
-- Abscisse suivante
end loop; end Integrer; -----------------------------------------------------------Integrale : Float;
-- Pour le resultat du calcul
begin -- Exemple_15_35 -- Quelques exemples d'appel Integrer ( Sqrt'access, 0.0, 1.0, 100, Integrale ); Integrer ( Log'access, 1.0, 10.0, 10, Integrale ); Integrer ( Exp'access, –1000.0, 0.0, 10_000, Integrale ); ...
La norme Ada précise quelques points supplémentaires peu ou pas abordés ici. Le passage de sous-programmes en paramètres, la création d’interfaces-utilisateurs graphiques ainsi que la création de paquetages d’interface (sect. 19.3) entre des bibliothèques de sous-programmes écrits en langage C et des applications Ada sont des raisons de l’existence des types accès à sous-programmes.
369
15.9 EXERCICES 15.9.1
Représentation schématique
En considérant les déclarations des types T_Lien et T_Element de l’exemple 15.9 et les variables El_1 : T_Element; Lien_1 : T_Lien; Lien_2 : T_Lien;
et en supposant que le type des informations (T_Info) est Integer, représenter schématiquement l’effet des instructions suivantes: Lien_1 := new T_Element'(10, Lien_2); Lien_1 := new T_Element'(8, Lien_1); Lien_2 := Lien_1; Lien_1.all := El_1; El_1.Suivant := Lien_2;
15.9.2
Représentation schématique
Représenter schématiquement chaque étape de la suite d’opérations suivantes sur une queue dynamique initialement vide et formée d’éléments entiers: • insérer successivement les nombres 18, 24, –5 et 2; • modifier la valeur de tête par le nombre 6; • rechercher la valeur –5, puis la valeur 3; • supprimer tous les éléments de la queue. 15.9.3
Chaînes de longueur variable
Soient les déclarations suivantes: type T_Pt_String is access String; function "+" ( Chaine : String ) return T_Pt_String is begin return new String'(Chaine); end "+";
En utilisant la fonction-opérateur "+" ci-dessus, déclarer un tableau Calcio (de type T_Pt_String pour ses éléments) contenant les chaînes "Juventus", "Milan", "Inter", "Fiorentina", "Napoli", "Parma" et "Sampdoria". Constater qu’il s’agit d’un tableau d’éléments de longueurs différentes! Cet exercice est inspiré de [BAR 97]. 15.9.4
Chaînes de longueur variable
Ecrire la fonction-opérateur "-" inverse de "+" (exercice 15.9.3) qui rend la chaîne repérée par le pointeur (de type T_Pt_String) passé en paramètre. Cet exercice est également inspiré de [BAR 97].
370
15.9.5
Fiches personnelles et liste dynamique
Adapter la solution de l’exercice 12.7.5 de manière à utiliser une liste dynamique pour stocker les fiches en mémoire. Le fichier binaire servira toujours à sauvegarder les fiches lorsque l’utilisateur le jugera nécessaire. 15.9.6
Paquetage enfant pour les queues dynamiques
Ecrire un paquetage enfant du parent Queues_Dynamiques mettant à disposition les opérations suivantes: copier une queue, appondre une queue à une autre, savoir si une queue est pleine, et supprimer tous les éléments d’une queue. 15.9.7
Paquetage enfant pour les piles dynamiques
Ecrire un paquetage enfant du parent Piles_Dynamiques similaire à celui demandé dans l’exercice 15.9.6. 15.9.8
Types accès à sous-programme
Ecrire un sous-programme qui trouve un zéro d’un polynôme de degré impair (ou d’une autre fonction) par dichotomie dans un intervalle contenant le zéro.
POINTS À RELEVER
371
15.10 POINTS À RELEVER 15.10.1 En général • Attention lorsqu’une liste est vide ou pleine; certaines opérations peuvent
alors provoquer des erreurs. • Un pointeur est une variable (statique) dont le contenu est toujours une
adresse mémoire. • Une variable dynamique (ou pointée) est une variable créée à l’exécution
par un allocateur de mémoire. • L’accès à une variable dynamique s’effectue toujours par l’intermédiaire
d’un pointeur ou d’une autre variable dynamique. • L’utilisation de variables dynamiques est facilitée par des représentations
schématiques. • La manipulation des pointeurs comporte des pièges à éviter absolument, en
particulier lors d’une tentative d’accès si le pointeur mentionné possède la valeur null. • Attention aux variables dynamiques devenues inaccessibles. • Les pointeurs et les variables dynamiques permettent d’implanter les
structures de données, comme les listes, sous leur forme dynamique. • Il faut être attentif au problème de restitution de la mémoire.
15.10.2 En Ada • Les types accès permettent de manipuler des pointeurs. • Une prédéclaration de type est nécessaire lors de la définition d’une
structure de données dynamique. • Les types accès généralisés permettent d’obtenir des pointeurs sur des
objets statiques. • Les types accès à sous-programme permettent d’obtenir des pointeurs sur
des sous-programmes.
372
C H A P I T R E
TYPES PRIVÉS, TYPES
1 6
373
TYPES PRIVÉS, TYPES LIMITÉS, UNITÉS ENFANTS
MOTIVATION
374
16.1 MOTIVATION La notion de type privé est indissociable de la notion de paquetage. S’il est possible de concevoir un paquetage sans type privé, la déclaration d’un type privé ne peut s’effectuer que dans un paquetage. Comme vu précédemment (sect. 10.2), la spécification déclare toutes les entités exportées alors que le corps implémente certaines de ces entités en cachant leur réalisation aux utilisateurs du paquetage. Mais dans tous les exemples présentés jusqu’ici, la structure des types exportés était complètement visible, avec comme conséquence une diminution de la fiabilité de ces paquetages puisque, intentionnellement ou par maladresse, tout utilisateur a la possibilité d’agir directement sur les constituants des objets de ces types, sans passer obligatoirement par les opérations mises à disposition. De plus, tout changement dans la structure même du type pourrait demander de nombreuses modifications des unités utilisatrices, avec le coût que cela peut représenter et les risques d’introduction d’erreurs supplémentaires. L’introduction d’un type privé dans la spécification d’un paquetage va permettre d’éliminer tous ces inconvénients et de transformer le paquetage en une structure de boîte noire (black box), c’est-à-dire en une pièce de programme dont l’intérieur est invisible et dont l’utilisation passe obligatoirement et uniquement par les opérations mises à disposition. En fait, avec cette définition, un sous-programme est déjà une sorte de boîte noire rudimentaire. Dans d’autres langages de programmation, de tels types sont appelés opaques. Enfin, en plus de ces arguments en faveur de la présentation de cette notion, ce chapitre va également servir comme préparation à la notion de généricité (chap. 17 et 18).
GÉNÉRALITÉS
375
16.2 GÉNÉRALITÉS Un type privé (private type) n’a rien de mystérieux! Il s’agit d’offrir un type dont la réalisation est placée dans une zone de la spécification appelée partie privée (private part) et inaccessible de l’extérieur du paquetage malgré sa présence dans la spécification pour des raisons de compilation. L’exemple des nombres rationnels (sect. 10.7) va être utilisé pour la présentation de ces modifications. 16.2.1
Spécification d’un paquetage avec type et partie privés
La spécification (partielle) du paquetage de calcul avec les nombres rationnels est rappelée dans l’exemple 16.1. Elle est formée de la déclaration du type T_Rationnel, de la constante Zero, de la fonction de construction d’un nombre rationnel et des opérations de calcul et de comparaison. La transformation du type T_Rationnel en un type privé va influencer la déclaration de la constante et donner tout son sens à la fonction de construction. La version avec le type T_Rationnel privé est donnée dans l’exemple 16.2. Exemple 16.1 Spécification (partielle) du paquetage Nombres_Rationnels. -- Ce paquetage permet le calcul avec les nombres rationnels package Nombres_Rationnels is type T_Rationnel is -- Le type d'un nombre rationnel record Numerateur : Integer; Denominateur : Positive;-- Le signe est au numerateur end record; Zero : constant T_Rationnel := (0, 1); -- Le nombre rationnel 0 ------------------------------------------------------------- Construction d'un nombre rationnel function "/" ( Numerateur : Integer; Denominateur : Positive ) return T_Rationnel; ------------------------------------------------------------- Autres operations (sect. 10.7) ... -----------------------------------------------------------end Nombres_Rationnels;
Exemple 16.2 Spécification (partielle) du paquetage Nombres_Rationnels avec type privé. -- Ce paquetage permet le calcul avec les nombres rationnels package Nombres_Rationnels is type T_Rationnel is private;
-- Le type prive
GÉNÉRALITÉS
Zero : constant T_Rationnel;
376
-- Une constante differee
------------------------------------------------------------- Construction d'un nombre rationnel function "/" ( Numerateur : Integer; Denominateur : Positive ) return T_Rationnel; ------------------------------------------------------------- Autres operations (sect. 10.7) ... -----------------------------------------------------------private type T_Rationnel is -- La declaration complete du type record Numerateur : Integer; Denominateur : Positive; -- Le signe est au numerateur end record; Zero : constant T_Rationnel := (0, 1); -- La constante complete end Nombres_Rationnels;
Comme mentionné préalablement, la spécification a été complétée par une partie déclarative privée située à la fin et commençant par le nouveau mot réservé private. Dans cette partie déclarative, n’importe quelle déclaration (sauf un corps naturellement) est possible, mais on doit au moins y trouver les déclarations complètes du ou des types privés et celles de la ou des constantes différées. Entre la définition privée du type et sa déclaration complète, il existe des restrictions concernant son utilisation puisque sa structure n’est pas encore connue. L’utilisation du type est dans ce cas possible uniquement comme type d’une constante différée, comme identificateur dans une déclaration de type ou de soustype et finalement comme type d’un paramètre de sous-programme ou du résultat d’une fonction. Ces restrictions sont respectées dans la spécification de Nombres_Rationnels (exemple 16.2) Une constante différée est, comme son nom l’indique, une constante dont l’identificateur est déclaré dans la partie visible (non privée) de la spécification et dont la déclaration complète est située dans la partie privée. Cette déclaration retardée est rendue nécessaire parce qu’il n’est possible de donner la valeur à la constante qu’une fois le type privé complètement défini. Comme pour un type privé, avant sa déclaration complète, une constante différée est soumise à des restrictions d’utilisation. Elle ne peut servir en effet que dans l’expression d’une valeur par défaut d’un paramètre d’entrée de sous-programme 16.2.2 Corps d’un paquetage avec type et partie privés dans sa spécification
GÉNÉRALITÉS
377
La présence d’un type privé n’affecte en rien la visibilité dans le corps du paquetage. Puisque la déclaration complète se place dans la spécification, la structure du type est utilisable dans le corps du paquetage, ainsi que toute autre entité de la partie privée. Le corps du paquetage Nombres_Rationnels est donc a priori (sect. 16.4) identique à celui de l’exemple 10.10.
UTILISATION D’UN PAQUETAGE AVEC TYPE PRIVÉ
378
16.3 UTILISATION D’UN PAQUETAGE AVEC TYPE PRIVÉ Un paquetage avec type privé s’importe avec une clause de contexte comme n’importe quel autre paquetage. Mais comme le type est privé, les unités utilisatrices du paquetage ne connaissent pas la structure du type. En fait, les seules opérations mises à disposition pour un type privé sont les suivantes: • l’affectation; • l’égalité et l’inégalité; • les opérations définies dans la partie visible de la spécification du
paquetage qui contient la déclaration du type privé, ou encore celles exportées de ses paquetages enfants publics (sect. 16.6). L’exemple 16.3 illustre l’utilisation de ces opérations. Exemple 16.3 Utilisation du paquetage Nombres_Rationnels avec type privé. with Nombres_Rationnels; use type Nombres_Rationnels.T_Rationnel;
-- § 16.2.1 -- § 10.5.3
-- ... procedure Exemple_16_3 is -- Une constante rationnelle (remarquer l'utilisation de / ) Une_Demi : constant Nombres_Rationnels.T_Rationnel := 1 / 2; Nombre_1 : Nombres_Rationnels.T_Rationnel;-- Deux variables Nombre_2 : Nombres_Rationnels.T_Rationnel; begin -- Exemple_16_3 -- Affectations Nombre_1 := Une_Demi; Nombre_2 := Nombres_Rationnels.Zero; -- Addition de deux nombres rationnels Nombre_2 := Nombre_1 + 3 / 12; -- Division de deux nombres rationnels Nombre_1 := Nombre_1 / Nombre_2; -- Comparaison de deux nombres rationnels if Nombre_1 > Nombre_2 then ... end if; -- L'acces aux champs est interdit puisque le type est prive Nombre_2.Numerateur := 5; ...
CONCEPTION D’UN PAQUETAGE AVEC TYPE PRIVÉ
379
16.4 CONCEPTION D’UN PAQUETAGE AVEC TYPE PRIVÉ La conception d’un paquetage avec type privé suit les mêmes règles que celles énoncées pour les paquetages simples (sect. 10.7). Mais, toujours parce que le type est privé, il faut en général penser à introduire des opérations pour construire un objet de ce type, pour obtenir les valeurs qui composent un tel objet et, éventuellement, prévoir des entrées-sorties pour des valeurs de ce même type. Dans l’exemple du paquetage Nombres_Rationnels, il existe la fonction "/" qui crée un nombre rationnel à partir d’un couple de nombres entiers. Mais il est ensuite impossible d’obtenir les numérateur et dénominateur d’un tel nombre! Afin que le paquetage soit cohérent, il faut donc rajouter deux fonctions à la spécification pour obtenir celle de l’exemple 16.4, ce qui amènera à la modification correspondante du corps. Le choix des fonctions s’impose puisqu’on désire obtenir les deux composants d’un nombre rationnel sans modifier la valeur dudit nombre. Pour des raisons de simplicité, aucune opération d’entrée-sortie ne sera par contre ajoutée au paquetage. Mais leur introduction ne poserait aucun problème particulier. Exemple 16.4 Spécification cohérente (partielle) de Nombres_Rationnels avec type privé. -- Ce paquetage permet le calcul avec les nombres rationnels package Nombres_Rationnels is type T_Rationnel is private; -- Le type d'un nombre rationnel Zero : constant T_Rationnel; -- Le nombre rationnel 0 Division_Par_0 : exception; -- Exception si division par 0 ------------------------------------------------------------- Construction d'un nombre rationnel function "/" ( Numerateur : Integer; Denominateur : Positive ) return T_Rationnel; ------------------------------------------------------------- Numerateur d'un nombre rationnel function Numerateur ( X : T_Rationnel ) return Integer; ------------------------------------------------------------- Denominateur d'un nombre rationnel function Denominateur ( X : T_Rationnel ) return Positive; ------------------------------------------------------------- Autres operations (sect. 10.7) ... private ... -- Pas de changement end Nombres_Rationnels;
CONCEPTION D’UN PAQUETAGE AVEC TYPE PRIVÉ
380
Exemple 16.5 Corps (partiel) du paquetage Nombres_Rationnels avec type privé. package body Nombres_Rationnels is ------------------------------------------------------------- Numerateur d'un nombre rationnel function Numerateur ( X : T_Rationnel ) return Integer is begin -- Numerateur return X.Numerateur; end Numerateur; ------------------------------------------------------------- Denominateur d'un nombre rationnel function Denominateur ( X : T_Rationnel ) return Positive is begin -- Denominateur return X.Denominateur; end Denominateur; ------------------------------------------------------------- Autres corps (sect. 10.7) ... end Nombres_Rationnels;
TYPES PRIVÉS ET DISCRIMINANTS
381
16.5 TYPES PRIVÉS ET DISCRIMINANTS La déclaration complète d’un type privé peut comporter un type à discriminant comme un type article (§ 11.2.1). Dans ce cas, la définition privée peut mentionner ou non le discriminant, selon la manière avec laquelle les objets de ce type vont être manipulés. 16.5.1
Définition privée avec mention du discriminant
Si la définition privée mentionne le discriminant sans valeur par défaut, alors un objet de ce type doit être contraint par une valeur pour le discriminant. En présence d’une valeur par défaut, la contrainte est optionnelle comme pour les types articles à discriminants (§ 11.3.1). Dans les deux cas, cela permet de conserver masquée la structure du type tout en permettant la construction d’objets de taille différente. L’exemple 16.6, adapté de l’exemple 14.3, permet la déclaration de queues de longueurs différentes. Exemple 16.6 Spécification d’un paquetage de gestion de queues statiques privées. -- Bases de gestion de queues statiques privees package Queues_Statiques is type T_Info is ...;
-- Depend de l'application
-- Pour une queue statique de longueur Taille type T_Queue_Statique ( Taille : Positive ) is private; ------------------------------------------------------------- Operations sur une queue statique (§ 14.4.2) ... -----------------------------------------------------------private type T_Contenu is array ( Positive range <>) of T_Info; type T_Queue_Statique ( Taille : Positive ) is record -- Contenu de la queue Contenu : T_Contenu ( 1 .. Taille ); Longueur : Natural := 0; Tete : Positive := 1; Queue : Positive := 1; end record;
-- Longueur de la queue -- Tete de la queue -- Queue de la queue
end Queues_Statiques;
Il est maintenant possible de déclarer les queues suivantes: Tampon : Queues_Statiques.T_Queue_Statique ( 100 ); Longue_Queue : Queues_Statiques.T_Queue_Statique ( 10_000 ); Petite_Queue : Queues_Statiques.T_Queue_Statique ( 10 );
TYPES PRIVÉS ET DISCRIMINANTS
16.5.2
382
Définition privée sans mention du discriminant
Si la définition privée ne mentionne pas le discriminant, alors la déclaration complète doit comporter une valeur par défaut pour le discriminant. Cela permet de conserver masquée la structure du type tout en permettant la création d’objets de taille variable. L’exemple 16.7 reprend le cas des polynômes (§ 11.3.1) sous forme de paquetage. Exemple 16.7 Spécification d’un paquetage de gestion de polynomes privés. -- Bases de gestion de polynomes prives package Polynomes is type T_Degre is range 0 .. 100; -- Pour les coefficients des termes d'un polynome type T_Coefficients is array (T_Degre range <>) of Integer; -- Pour une polynome de degre inferieur ou egal a 100 type T_Polynome is private; ------------------------------------------------------------- Creation d'un polynome function Creer ( Coefficients : in T_Coefficients ) return T_Polynome; ------------------------------------------------------------- Autres operations sur un polynome ... -----------------------------------------------------------private type T_Polynome (Degre : T_Degre := 0) is -- Expression -- par defaut record Coefficients : T_Coefficients (T_Degre’First..Degre) := (T_Degre'First..Degre => 0); end record; end Polynomes;
Il est maintenant possible de déclarer les polynômes suivants, tous initialement nuls: Pol_Const : Polynomes.T_Polynome; Parabole : Polynomes.T_Polynome; Grand : Polynomes.T_Polynome;
Comme ces polynômes ne sont pas contraints (sect. 11.3), il sera possible de les modifier en utilisant l’opération Creer du paquetage, comme par exemple: Pol_Const := Polynomes.Creer ( (0 => –6) ); -- Le polynome -- constant –6
TYPES PRIVÉS ET DISCRIMINANTS
383
Parabole := Polynomes.Creer ( (3, –2, 5) ); -- 3 – 2x + 5x2 Grand := Polynomes.Creer ( (0..N => 1) ); -- 1+x+x2+...+xn
A titre d’illustration, le corps de la fonction Creer serait le suivant: function Creer ( Coefficients : in T_Coefficients ) return T_Polynome is begin -- Creer return ( Coefficients'Last, Coefficients ); end Creer;
La valeur du discriminant (le degré du polynôme) est obtenue par la borne supérieure des indices du paramètre; cela implique que ledit paramètre doit être un tableau de coefficients de borne inférieure égale à zéro pour que la valeur fournie par l’attribut Last corresponde effectivement au degré du paramètre effectif. Cette convention est respectée dans les trois exemples précédents.
PAQUETAGES ENFANTS PUBLICS
384
16.6 PAQUETAGES ENFANTS PUBLICS Lors de la présentation des paquetages, la notion d’enfant a été introduite (sect. 10.8). Maintenant que les paquetages peuvent comporter des types privés, il est nécessaire de revenir sur les enfants. En fait, les paquetages enfants se répartissent en deux catégories: les enfants publics et les enfants privés. Le paquetage enfant Nombres_Rationnels.Utilitaires (§ 10.8.1) est un exemple de paquetage enfant public car il n’est pas déclaré comme privé (sect. 16.7). Un tel paquetage possède la même structure qu’un paquetage parent, à savoir une spécification, avec optionnellement une partie privée, et un corps. Les règles énoncées préalablement (sect. 10.8) pour les paquetages enfants s’appliquent en fait à tous les paquetages enfants publics, avec ou sans partie privée. Figure 16.1 Visibilité entre paquetages parent et enfant public.
Paquetage parent
Paquetage enfant public
package Parent is ... private ... end Parent;
package Parent.Enfant is ... private ... end Parent.Enfant;
package body Parent is ... end Parent;
package body Parent.Enfant is ... end Parent.Enfant;
Les seules précisions à donner concernent la visibilité (fig. 16.1 et 16.2). Dans la partie privée et le corps de l’enfant, toutes les déclarations de la spécification du parent, partie privée comprise, sont utilisables sans préfixe. Par contre, les déclarations de la partie privée du parent sont invisibles dans la partie visible (non privée) de l’enfant.
PAQUETAGES ENFANTS PUBLICS
385
Figure 16.2 Vue schématique de la visibilité entre paquetages parent et enfant public. package Parent is ... package Parent.Enfant is ... private ... private ... end Parent.Enfant; end Parent;
package body Parent is ... ... ... ... package body Parent.Enfant is ... ... ... end Parent.Enfant; end Parent;
PAQUETAGES ENFANTS PRIVÉS
386
16.7 PAQUETAGES ENFANTS PRIVÉS Dans une hiérarchie de paquetages, les enfants publics permettent d’offrir des fonctionnalités supplémentaires aux unités utilisatrices des paquetages parents. Les paquetages enfants privés (spécification précédée de private) n’offrent aucune fonctionnalité supplémentaire mais servent à décomposer de manière structurée les opérations internes, invisibles hors des paquetages parents. Figure 16.3 Visibilité des paquetages parents dans les enfants privés.
Paquetage parent
Paquetage enfant privé
package Parent is ... private ... end Parent;
private package Parent.Enfant is ... private ... end Parent.Enfant;
package body Parent is ... end Parent;
package body Parent.Enfant is ... end Parent.Enfant;
Un enfant privé n’est visible que pour le corps de son parent et pour tous les enfants de son parent à l’exclusion des spécifications des enfants publics (fig. 16.3 et 16.4). Finalement, la spécification et le corps d’un paquetage enfant privé peuvent aussi accéder à la partie privée de tous ses ancêtres. Figure 16.4 Vue schématique de la visibilité entre paquetages parent et enfant privé.
package Parent is ... private ... private package Parent.Enfant is ... private ... end Parent.Enfant; end Parent;
package body Parent is ... ... ... package body Parent.Enfant is ... ... ... ... end Parent.Enfant; end Parent;
PAQUETAGES ENFANTS PRIVÉS
387
Exemple 16.8 Spécification du paquetage enfant privé Nombres_Rationnels.Operations_Internes. -- Ce paquetage fournit des operations internes de calcul avec les -- nombres rationnels private package Nombres_Rationnels.Operations_Internes is ------------------------------------------------------------- Pour l'addition et la soustraction afin de rendre le -- resultat le plus petit possible function P_P_M_C ( X, Y : Positive ) return Positive; ------------------------------------------------------------- Pour la reduction en un nombre rationnel irreductible function P_G_C_D ( X, Y : Positive ) return Positive; ------------------------------------------------------------- Rendre un nombre rationnel irreductible function Irreductible ( X : T_Rationnel) return T_Rationnel; -----------------------------------------------------------end Nombres_Rationnels.Operations_Internes;
Les opérations mises à disposition par le paquetage enfant privé Nombres_Rationnels.Operations_Internes (exemple 16.8) sont destinées uniquement à la réalisation de certaines des fonctionnalités des paquetages Nombres_Rationnels et Nombres_Rationnels.Utilitaires. Il est donc parfaitement logique d’utiliser un paquetage enfant privé pour les implémenter. Après l’acquisition de ces notions nouvelles, la réalisation d’un ensemble d’unités de calcul et de traitement des nombres rationnels doit être revue par rapport à la proposition faite préalablement (sect. 10.7 et 10.8). La figure 16.5 montre une décomposition qui tire parti de toutes les notions maintenant connues.
PAQUETAGES ENFANTS PRIVÉS
388
Figure 16.5 Hiérarchie d’unités de traitement des nombres rationnels. Spécification de Nombres_Rationnels
Spécification de Nombres_Rationnels.Utilitaires
type T_Rationnel zero, addition, construction, soustraction, multiplication, division, comparaisons, numerateur, denominateur
Corps de Nombres_Rationnels addition, construction, soustraction, multiplication, division, comparaisons, numerateur, denominateur
valeur absolue, puissance, multiplication par un nombre entier, division par un nombre entier Spécification de Nombres_Rationnels.Operations_Internes P_G_C_D, P_P_M_C, Irreductible Corps de Nombres_Rationnels.Utilitaires valeur absolue, puissance, multiplication par un nombre entier, division par un nombre entier Corps de Nombres_Rationnels.Operations_Internes
visibilité directe visibilité grâce à une clause de contexte
P_G_C_D, P_P_M_C, Irreductible
Le corps du paquetage Nombres_Rationnels.Operations_Internes contiendra uniquement les corps des opérations exportées (exemple 10.10). Par contre, cette nouvelle décomposition rendra nécessaire l’utilisation de la clause de contexte with Nombres_Rationnels.Operations_Internes avant le corps du paquetage enfant Nombres_Rationnels.Utilitaires si nécessaire. Dans ce corps, il sera alors possible et facile de rendre les nombres rationnels (calculés) irréductibles par l’utilisation de la fonction Irreductible exportée du paquetage
PAQUETAGES ENFANTS PRIVÉS
enfant privé Nombres_Rationnels.Operations_Internes.
389
REMARQUES FINALES SUR LES UNITÉS ENFANTS
390
16.8 REMARQUES FINALES SUR LES UNITÉS ENFANTS Une unité enfant (publique ou privée) peut être en fait n’importe quelle unité de bibliothèque. Autrement dit, non seulement un paquetage mais encore une procédure, une fonction, une unité générique (sect. 18.4) peuvent être les enfants d’un paquetage parent et former une hiérarchie. Dans le cas des nombres rationnels, les trois fonctions P_G_C_D, P_P_C_M et Irreductible pourraient être extraites du paquetage enfant les contenant (Nombres_Rationnels.Operations_Internes) et transformées en unités enfants. L’exemple 16.9 montre la fonction P_G_C_D rendue privée. Exemple 16.9 Fonction P_G_C_D rendue privée. private function Nombres_Rationnels.P_G_C_D ( X, Y : Positive ) return Positive; function Nombres_Rationnels.P_G_C_D ( X, Y : Positive ) return Positive is Diviseur_X : Positive := X; Diviseur_Y : Positive := Y;
-- Pour les soustractions
begin -- P_G_C_D while Diviseur_X /= Diviseur_Y loop
-- PGCD trouve?
if Diviseur_X > Diviseur_Y then Diviseur_X := Diviseur_X – Y; else Diviseur_Y := Diviseur_Y – X; end if; end loop; return Diviseur_X;
-- C'est le PGCD
end Nombres_Rationnels.P_G_C_D;
En guise de conclusion, il faut souligner que les notions de paquetage et d’unités enfants publiques et privées garantissent le caractère évolutif des composants logiciels pour leurs utilisateurs et permettent une conception très fine pour leurs concepteurs. Une présentation plus complète et très détaillée des unités enfants peut être trouvée dans [BAR 97].
TYPES LIMITÉS
391
16.9 TYPES LIMITÉS 16.9.1
Motivation
L’opération d’affectation permet de remplacer la valeur d’une variable par le résultat du calcul d’une expression. Parmi tous les cas particuliers existants, celui où l’expression est réduite à la présence d’une seule variable permet en fait d’effectuer une copie de la valeur de cette variable. Or il existe des situations où ce cas particulier d’affectation peut produire des effets non désirés. Par exemple, si l’on considère une queue (ou une pile) dynamique (sect. 15.5 et 15.6), l’affectation du contenu d’une variable du type T_Queue_Dynamique ne va pas copier toute la queue, mais uniquement le contenu de la variable, c’est-à-dire les valeurs (adresses) des pointeurs Tete et Queue. Le résultat de l’affectation ne sera donc qu’une copie de pointeurs, autrement dit la même queue sera désignée par deux variables différentes de type T_Queue_Dynamique (§ 15.5.1). Si l’intention du programmeur était d’effectuer la copie de toute la queue, alors cette affectation conduira à des erreurs à l’exécution. Ada permet de se protéger contre de telles erreurs par le biais des types limités, qui empêchent l’utilisation de l’opération d’affectation. 16.9.2
Généralités
Un type limité (limited type) est un type article pour lequel la déclaration mentionne qu’il est limité par l’emploi du (nouveau) mot réservé limited placé immédiatement après le mot réservé is. Cette mention empêche toute affectation d’objets d’un tel type et interdit l’utilisation des opérations prédéfinies d’égalité et d’inégalité. Exemple 16.10 Déclarations d’un type limité. type T_Date is limited record Jour : T_Jour; Mois : Mois_De_L_Annee; Annee : Integer; end Date;
L’exemple 16.10 illustre simplement une première et peu fréquente utilisation du mot réservé limited. Il est bien clair qu’un tel type T_Date reste utile car l’accès aux champs d’une date n’est pas restreint par la limitation (globale) du type. Dans l’exemple des queues dynamiques, si le type T_Queue_Dynamique était limité, une affectation globale serait ainsi détectée et interdite par le compilateur. Mais l’intérêt majeur de la notion de type limité réside dans son application aux
TYPES LIMITÉS
392
types privés. Il devient alors possible de créer des paquetages qui exportent des types dont l’utilisation est strictement restreinte aux opérations exportées par le paquetage ou l’un de ses enfants publics, avec comme conséquence un niveau de fiabilité du paquetage encore augmenté. En plus de l’utilisation des opérations exportées, la déclaration de variables et de paramètres d’un type limité privé est possible. Exemple 16.11 Spécification (partielle) d’un paquetage de gestion de queues dynamiques limitées privées. -- Bases de gestion de queues dynamiques limitees privees package Queues_Dynamiques is type T_Info is ...;
-- Depend de l'application
type T_Queue_Dynamique is limited private;-- Pour une queue -- dynamique Queue_Vide : exception; -- Levee si la queue est vide ------------------------------------------------------------- Insere Info en queue de La_Queue procedure Inserer ( La_Queue : in out T_Queue_Dynamique; Info : in T_Info ); ------------------------------------------------------------- Autres operations (§ 15.5.2) ... private type T_Element;
-- Predeclaration
type T_Lien is access T_Element;
-- Le type acces
type T_Element is -- Pour une variable dynamique record Information : T_Info; -- L'information sur un seul champ Suivant : T_Lien; -- Pour reperer l'element qui suit end record; type T_Queue_Dynamique is -- Pour une queue dynamique record Tete : T_Lien; -- Tete et queue de la queue Queue : T_Lien; end record; end Queues_Dynamiques;
Dans l’exemple 16.11, le type T_Queue_Dynamique est limité privé. Dans les unités utilisatrices du paquetage Queues_Dynamiques, il sera donc possible de déclarer des queues dynamiques et d’utiliser les opérations d’insertion, de suppression, etc., comme dans l’exemple 16.12. Il faut relever à ce propos que la déclaration d’une variable de type T_Queue_Dynamique est initialement vide car
TYPES LIMITÉS
393
tout pointeur est toujours initialisé à la valeur null! Exemple 16.12 Utilisation d’une queue dynamique limitée privée. -- ... with Queues_Dynamiques; procedure Exemple_16_12 is Queue : Queues_Dynamiques.T_Queue_Dynamique; Info : Queues_Dynamiques.T_Info := ...; -- Info initialisee procedure P (Queue: in Queues_Dynamiques.T_Queue_Dynamique) is ... end P; begin
-- Exemple_16_12
Queues_Dynamiques.Inserer ( Queue, Info ); P ( Queue ); if Queues_Dynamiques.Vide ( Queue ) then ... end if; ...
Pour tous les types en Ada, il est possible de surcharger les opérateurs prédéfinis d’égalité et d’inégalité. Pour les types limités, la définition des opérateurs d’égalité et d’inégalité est autorisée sous la forme habituelle des fonctions-opérateurs "=" et "/=" avec les mêmes règles que précédemment (sect. 4.9). Par contre, il est interdit de redéfinir l’affectation. Il faut dans ce cas créer une procédure de copie qui simule ladite affectation, tout ceci dans la spécification du paquetage contenant le type limité privé ou dans celle de l’un de ses enfants publics. Bien entendu, les opérations d’affectation, d’égalité et d’inégalité restent utilisables partout où la structure du type est visible, comme par exemple dans le corps du paquetage et dans celui de ses enfants. 16.9.3
Remarques sur les types limités • Le critère de décision entre le choix de limiter ou non un type privé réside
dans la volonté d’interdire ou de permettre l’affectation. • Comme l’affectation est interdite, une déclaration de constante ou une
valeur initiale pour une variable sont impossibles. • Des tableaux d’élements limités ou des articles comportant un champ limité
sont autorisés. Les tableaux ou les articles sont alors eux-mêmes limités.
394
16.10 EXERCICES 16.10.1 Types privés Supposons que le type T_Queue_Statique de l’exemple 16.6 soit déclaré comme suit: type T_Queue_Statique ( Taille : Positive := 100 ) is private;
Déclarer alors quelques queues, en utilisant parfois la valeur par défaut. 16.10.2 Paquetage avec type privé Reprendre et modifier le paquetage Piles_Statiques (§ 14.5.2 et suivants) en transformant le type T_Pile_Statique en type privé. 16.10.3 Types limités privés Reprendre et modifier les deux paquetages Queues_Dynamiques (§ 15.5.2 et suivants) et Piles_Dynamiques (§ 15.6.2 et suivants) en transformant les types T_Queue_Dynamique et T_Pile_Dynamique en types limités privés. 16.10.4 Types privés ou limités privés Pourquoi l’exercice 16.10.2 précise-t-il que les types doivent être limités? Que pourrait-il arriver s’ils ne l’étaient pas? 16.10.5 Paquetages parent et enfants Reprendre le problème des fiches personnelles et des opérations associées (exercices 10.9.1, 10.9.2, 10.9.6, 11.5.5, 12.7.5 et 15.9.5) et concevoir une hiérarchie de paquetages parent et enfants (publics et/ou privés) ainsi qu’un type privé (ou limité privé) pour représenter une fiche.
POINTS À RELEVER
395
16.11 POINTS À RELEVER 16.11.1 En général • Les types privés d’Ada possèdent des équivalents dans d’autres langages de
programmation; ils sont parfois appelés types opaques. • Empêcher le plus possible les erreurs de l’utilisateur est une propriété
fondamentale des langages modernes, concept que les types opaques réalisent. 16.11.2 En Ada • Les types privés permettent d’augmenter la fiabilité d’un paquetage. • La partie privée est une partie déclarative dans laquelle doivent se situer les
déclarations complètes des types privés. • Entre la déclaration privée et la déclaration complète d’un type, il existe des
restrictions sur son utilisation. • Une constante différée est une constante dont la valeur est donnée lors de
la déclaration complète. • Un type privé peut avoir des discriminants, visibles ou non. • Les paquetages enfants peuvent être publics ou privés. • Les types limités empêchent l’utilisation de l’affectation et des opérations
prédéfinies d’égalité et d’inégalité. • Les opérations sur les valeurs d’un type limité privé sont celles visibles
dans les spécifications du paquetage de déclaration du type ainsi que de ses enfants publics.
396
C H A P I T R E
BASES DES UNITÉS
1 7
397
BASES DES UNITÉS GÉNÉRIQUES
MOTIVATION
398
17.1 MOTIVATION Au cours des chapitres précédents, certaines incertitudes sont apparues dans les structures ou les mécanismes présentés. Il s’agissait parfois d’un type qui dépendait de l’utilisation faite d’un paquetage (§ 15.5.2) ou d’une procédure de traitement (§ 15.5.9). Dans les deux cas, les informations nécessaires à la levée de ces incertitudes sont extérieures au paquetage concerné; elles dépendent de l’utilisation qu’une unité externe en fera. Or le fonctionnement du paquetage est complètement indépendant de la nature du type ou du corps de la procédure. Un mécanisme tel que la généricité (genericity) permet l’écriture d’unités génériques (paquetages ou sous-programmes), structures paramétrables à la compilation ou à l’exécution dont les paramètres sont spécifiés par les unités utilisatrices. Il est donc possible d’écrire des paquetages ou des sous-programmes plus facilement réutilisables puisque, rendus génériques, ils pourront s’adapter à des emplois différents sans nécessiter de réécriture. Les paquetages d’entrées-sorties de base comme Ada.Text_IO.Integer_IO (§ 6.2.2), Ada.Text_IO.Modular_IO (§ 6.2.3), Ada.Text_IO.Float_IO (§ 6.2.4), Ada.Text_IO.Enumeration_IO (§ 5.2.5), ou encore la procédure Ada.Unchecked_Deallocation (sect. 15.7) sont des exemples d’unités génériques prédéfinies.
PAQUETAGES GÉNÉRIQUES ET INSTANCIATIONS
399
17.2 PAQUETAGES GÉNÉRIQUES ET INSTANCIATIONS Un paquetage devient générique dès que le nouveau mot réservé generic précède le début de sa spécification. Mais l’intérêt essentiel de cette transformation réside en la possibilité de le paramétrer dans le but déjà mentionné de l’utiliser à plusieurs reprises en fournissant chaque fois des valeurs différentes pour ces paramètres. Ceux-ci, appelés paramètres formels génériques, se placent dans une zone nommée partie formelle générique et délimitée par les mots réservés generic et package. Ces paramètres peuvent être des valeurs, des types, des sous-programmes ou encore des paquetages. Leur diversité donne au mécanisme de généricité tout son intérêt et sa puissance. Figure 17.1 Partie formelle générique d’un paquetage. Partie formelle générique generic Paramètres génériques
Le diagramme syntaxique de la figure 17.1 montre qu’un paquetage générique peut n’avoir aucun paramètre. Cette possibilité est particulièrement utile si le paquetage est un enfant (sect. 18.4) d’un paquetage parent générique. Exemple 17.1 Parties formelles génériques avec ou sans paramètres. generic package Sans_Parametre is
-- Specification d'un paquetage -- generique sans parametre -- Parties visible et privee ... end Sans_Parametre; --------------------------------------------------------------generic -- Valeurs en parametres, sect. 17.3 Longueur : in Positive; Taille : Natural := 80; Caractere_Courant : in out Character; -- Types en parametres, sect. 17.4 et 18.1 type T_Mois is (<>); type T_Entier is range <>; type T_Tableau is array (T_Mois range <>) of Integer; type T_Pointeur is access T_Tableau; type T_Inconnu is private;
PAQUETAGES GÉNÉRIQUES ET INSTANCIATIONS
400
type T_Indefini (<>) is private; type T_Vraiment_Inconnu is limited private; -- Sous-programmes en parametres, sect. 18.2 with procedure Imprimer ( Tableau : in T_Tableau ); with function Nombre_Jours ( Mois : T_Mois ) return Natural is <>; -- Paquetage en parametre, cas non traite dans cet ouvrage with package Entrees_Sorties is new Ada.Text_IO.Enumeration_IO (<>); package Avec_Parametres is
-- Specification d'un paquetage -- generique avec parametres
-- Parties visible et privee ... end Avec_Parametres;
L’exemple 17.1 est uniquement illustratif. Il présente simplement quelques formes de paramètres (ou aucune) placés avant une spécification. Celle-ci ainsi que le corps correspondant possèdent une structure identique à celle d’un paquetage non générique. Les paramètres formels génériques sont visibles dans tout le paquetage ainsi que dans ses enfants. La seule restriction réside dans le fait que les paramètres génériques ne sont pas considérés comme statiques et par conséquent inutilisables là où un objet statique est requis, par exemple dans les choix d’une instruction case ou dans un intervalle de définition d’un type entier. Un paramètre générique peut être utilisé dans la définition d’un autre paramètre générique sauf si la restriction précédente l’interdit. L’utilisation d’un tel paquetage nécessite la déclaration d’un paquetage habituel, non générique, à partir du paquetage générique. Cette opération (exemple 17.2), qui permet en plus d’associer les paramètres génériques effectifs aux paramètres formels, s’appelle instanciation (instantiation). L’association des paramètres effectifs aux paramètres formels dépend du genre de paramètre et s’écrit comme une liste de paramètres effectifs (notation par nom ou par position) lors de l’appel d’un sous-programme (§ 4.3.7). On appelle paquetage instancié (instantiated package) le paquetage non générique résultant d’une instanciation. Un paquetage générique joue le même rôle que le plan d’une maison. Un tel plan ne sert qu’à construire des maisons qui, elles, sont réelles et servent à l’habitation. Les paquetages génériques permettent de créer des paquetages instanciés qui, comme les maisons, sont les entités utilisables. Exemple 17.2 Exemples d’instanciations de paquetages génériques. package E_S_Entiers is new Ada.Text_IO.Integer_IO ( Integer );
PAQUETAGES GÉNÉRIQUES ET INSTANCIATIONS
401
package E_S_Jours is new Ada.Text_IO.Enumeration_IO ( T_Jours_De_La_Semaine ); package Exemple_Simple is new Sans_Parametre; package Exemple is new Avec_Parametres ( Longueur => 30, Caractere_Courant => Lettre, ... );
Un paquetage générique est souvent une unité de bibliothèque. Dans ce cas il doit d’abord être compilé, puis importé par une clause de contexte dans toute unité utilisatrice. Sinon, il doit être déclaré comme un paquetage non générique (sect. 10.2). Comme un paquetage générique a pour seul but de permettre la création de paquetages instanciés, il est interdit d’utiliser une clause use ou use type avec un tel paquetage. Ces clauses sont naturellement et logiquement possibles pour un paquetage instancié. De plus, il est possible de compiler une instanciation pour elle-même, ce qui créera une nouvelle unité de bibliothèque, par exemple: with Ada.Text_IO; package Entier_IO is new Ada.Text_IO.Integer_IO ( Integer );
C’est de cette manière qu’ont été mis à disposition les paquetages prédéfinis Ada.Integer_Text_IO et Ada.Float_Text_IO. Finalement, par convention dans cet ouvrage, le nom d’un paquetage générique se terminera par le suffixe _G.
VALEURS COMME PARAMÈTRES GÉNÉRIQUES
402
17.3 VALEURS COMME PARAMÈTRES GÉNÉRIQUES Les valeurs constituent le cas de paramètre générique le plus simple. Il s’agit simplement de paramétrer un paquetage avec une constante (mode in) ou de partager la valeur d’une variable entre le paquetage instancié et l’unité utilisatrice (mode in out). Ce dernier cas, peu fréquent, ne sera pas plus développé ici. Naturellement, les types des paramètres formels et effectifs devront être identiques. L’utilisation d’une constante comme paramètre (formel) générique peut être illustrée grâce, par exemple, au paquetage de gestion de piles statiques (§ 14.5.2). Celui-ci comportait une longueur maximum constante Longueur_Max candidate idéale à la transformation en paramètre générique. Le paquetage obtenu (exemple 17.3) permettra donc la déclaration et la gestion de piles statiques de longueur constante définie lors de chaque instanciation. Exemple 17.3 Squelette d’un paquetage générique de gestion de piles statiques. -- Paquetage generique de piles statiques generic Longueur_Max : in Natural;
-- Longueur maximum d'une pile
package Piles_Statiques_G is type T_Info is ...;
-- Depend de l'application
subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Sommet is Integer range 0..Longueur_Max; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Pile_Statique is
-- Pour une pile statique
record -- Contenu de la pile Contenu : T_Contenu; -- Longueur de la pile Longueur : T_Longueur := T_Longueur'First; -- Sommet de la pile Sommet : T_Pos_Sommet := T_Pos_Sommet'First; end record; -- Autres declarations (§ 14.5.2) ... end Piles_Statiques_G; package body Piles_Statiques_G is ... end Piles_Statiques_G;
VALEURS COMME PARAMÈTRES GÉNÉRIQUES
403
Un paquetage instancié se déclare en fournissant une valeur pour la constante générique: package Piles_200 is new Piles_Statiques_G ( 200 ); package Grandes_Piles is new Piles_Statiques_G ( 1000 );
Comme pour un paramètre d’entrée de sous-programme, un paramètre constante générique peut comporter une valeur par défaut (exemple 17.4). Dans ce cas il n’est pas nécessaire de donner de paramètre effectif lors de l’instanciation. Exemple 17.4 Valeur par défaut et instanciation. -- Paquetage generique de piles statiques generic Longueur_Max : in Natural := 100; -- Longueur maximum -- d'une pile package Piles_Statiques_G is -- Declarations (exemple 17.3) ... end Piles_Statiques_G; ... -- Corps de Piles_Statiques_G -- Instanciations du paquetage generique package Piles_100 is new Piles_Statiques_G; package Piles_80 is new Piles_Statiques_G ( 80 );
Le corps du paquetage Piles_Statiques_G est identique à celui du paquetage non générique Piles_Statiques (§ 14.5.3 et suivants). Les paquetages instanciés Piles_200, Grandes_Piles, Piles_100 et Piles_80 s’utilisent comme n’importe quel paquetage non générique.
TYPES COMME PARAMÈTRES GÉNÉRIQUES
404
17.4 TYPES COMME PARAMÈTRES GÉNÉRIQUES 17.4.1
Généralités
Les types constituent un cas des plus intéressants. Grâce à ce genre de paramètres génériques, il va être possible de constituer des paquetages de gestion de structures indépendantes du type des informations contenues dans ces structures. De tels paquetages fourniront des opérations sur des valeurs dont les types seront fournis par les unités utilisatrices des paquetages! Les paquetages de gestion de queues et de piles constituent de bons exemples où le type des informations n’a aucune influence sur les opérations mises à disposition. A l’intérieur des paquetages génériques et de tous leurs enfants, la structure précise des types passés comme paramètres est inconnue mais les opérations globales qui leur sont associées sont possibles, comme certains attributs par exemple. Cette généralisation n’est cependant pas absolue dans le sens que, comme Ada reste un langage fortement typé, le compilateur aura besoin d’un minimum d’informations sur les types passés comme paramètres afin d’effectuer les vérifications nécessaires. C’est une des raisons pour lesquelles ces types sont groupés en catégories englobant ceux de mêmes caractéristiques. Seules les catégories des types discrets et numériques vont être discutées ici. Le chapitre 18 présentera les autres. Lors de l’instanciation, des sous-types peuvent jouer le rôle de paramètres effectifs pourvu que leur type de base soit compatible avec le paramètre formel. Dans ce cas, les contraintes fournies par ces sous-types s’appliquent dans le paquetage instancié. 17.4.2
Types discrets comme paramètres génériques
Les types discrets sont mentionnés comme paramètres génériques (exemple 17.5) grâce à la forme syntaxique suivante: type T_Discret is (<>);
Le paramètre effectif spécifié à l’instanciation doit naturellement être d’un type ou sous-type discret. Exemple 17.5 Type discret comme paramètre générique et instanciation. -- ... generic type T_Discret is (<>); package Exemple_17_5_G is -- Retourne le successeur de Valeur, circulairement -- (l'attribut Succ reste naturellement applicable au type -- T_Discret) function Successeur (Valeur : T_Discret) return T_Discret; ------------------------------------------------------------
TYPES COMME PARAMÈTRES GÉNÉRIQUES
405
-- Autres declarations de la specification ... end Exemple_17_5_G; -- Corps de Exemple_17_5_G package body Exemple_17_5_G is ... end Exemple_17_5_G; -- Instanciations du paquetage generique (§ 5.2.2 et 6.1.1) package Jours_Semaine is new Exemple_17_5_G (T_Jours_De_La_Semaine); package Jours_Travail is new Exemple_17_5_G (T_Jours_Travail);
Le paquetage Enumeration_IO, interne à Ada.Text_IO (sect. 19.3), est un autre exemple mentionnant un type énumératif comme paramètre générique. 17.4.3
Types numériques comme paramètres génériques
La déclaration des types numériques comme paramètres génériques ressemble à celle rencontrée lors d’une déclaration habituelle (à noter l’utilisation de <>): type T_Entier is range <>; type T_Modulaire is mod <>;
---type T_Flottant is digits <>; -type T_Fixe is delta <>; --
Pour un type Pour un type (modulaire) Pour un type Pour un type
entier signe entier non signe reel point-flottant reel point-fixe
Les paramètres effectifs correspondants doivent naturellement correspondre au genre des paramètres formels. Les paquetages génériques internes à Ada.Text_IO (sect. 19.3) fournissent des exemples d’utilisation de telles déclarations.
SOUS-PROGRAMMES GÉNÉRIQUES
406
17.5 SOUS-PROGRAMMES GÉNÉRIQUES Comme les paquetages, les sous-programmes peuvent devenir génériques et tout ce qui s’applique aux paquetages (instanciation...) reste valable pour les sousprogrammes. La seule contrainte est qu’un sous-programme générique doit être déclaré en une spécification et un corps puisqu’une partie (formelle) générique précède toujours une spécification et non un corps. L’exemple 17.6 présente une fonction générique qui retourne le maximum de deux valeurs réelles point-flottant. Exemple 17.6 Fonction générique de calcul d’une valeur maximum. -- Fonction generique de recherche du maximum de deux valeurs -- reelles point-flottant generic type T_Reel is digits <>; function Maximum_G (
Valeur_1 : T_Reel; Valeur_2 : T_Reel ) return T_Reel;
function Maximum_G (
Valeur_1 : T_Reel; Valeur_2 : T_Reel ) return T_Reel is
begin -- Maximum_G if Valeur_1 > Valeur_2 then return Valeur_1; else return Valeur_2; end if; end Maximum_G; -- Instanciations de la fonction generique (§ 6.2.4) function Maximum is new Maximum_G ( Float ); function Maximum is new Maximum_G ( T_Reel_12 );
A propos de la fonction de l’exemple 17.6, il faut préciser qu’il existe les attributs Max et Min [ARM ANNEXE K] applicables à n’importe quel type scalaire, attributs qui retournent les valeurs maximale et minimale d’un couple de valeurs. Pour clore cette section, il faut citer la procédure générique prédéfinie Ada.Unchecked_Deallocation (sect. 15.7) et la fonction générique prédéfinie Ada.Unchecked_Conversion [ARM 13.9].
PAQUETAGES GÉNÉRIQUES ET EXCEPTIONS
407
17.6 PAQUETAGES GÉNÉRIQUES ET EXCEPTIONS Le paquetage Nombres_Rationnels peut être rendu générique, par exemple en transformant le type du numérateur et du dénominateur en paramètre générique. Qu’advient-il alors de l’exception exportée de ce paquetage (exemple 17.7)? Exemple 17.7 Squelette du paquetage générique de gestion de nombres rationnels. -- Ce paquetage generique permet le calcul avec les nombres -- rationnels generic type T_Entier is range <>; package Nombres_Rationnels_G is type T_Rationnel is private; Division_Par_0 : exception;
-- Exception levee si division -- par 0
------------------------------------------------------------- Autres declarations et operations (sect. 16.4) ... private ... end Nombres_Rationnels_G;
Un exemplaire de l’exception Division_Par_0 sera créé dans chaque paquetage instancié de Nombres_Rationnels_G. Cette situation normale peut conduire à un traitement d’exception où chaque exemplaire de l’exception doit être écrit en nom développé avec le nom du paquetage instancié comme préfixe puisqu’une exception ne peut pas être surchargée. Cette obligation de préfixage, si elle se révèle ennuyeuse, peut être évitée en créant un paquetage non générique englobant la déclaration de Division_Par_0 et le paquetage générique, expurgé de cette même déclaration. Le traitement sera ainsi possible quel que soit le paquetage instancié dont l’une des opérations a provoqué la levée de l’exception. L’exemple 17.8 modifie l’exemple 17.7 de manière à déclarer l’exception Division_Par_0 hors du paquetage générique Nombres_Rationnels_G. Exemple 17.8 Paquetage englobant une déclaration d’exception et un paquetage générique. -- Ce paquetage rend possible le traitement de l'exception -- Division_Par_0 independamment des instanciations package Nombres_Rationnels_Complets is Division_Par_0 : exception;
-- Exception levee si division -- par 0
PAQUETAGES GÉNÉRIQUES ET EXCEPTIONS
408
-----------------------------------------------------------generic type T_Entier is range <>; package Nombres_Rationnels_G is type T_Rationnel is private; ---------------------------------------------------------- Autres declarations et operations (sect. 16.4) ... private ... end Nombres_Rationnels_G; -----------------------------------------------------------end Nombres_Rationnels_Complets;
TYPES DE DONNÉES ABSTRAITS
409
17.7 TYPES DE DONNÉES ABSTRAITS Il est intéressant de terminer ce chapitre par une application particulièrement utile non seulement de la généricité mais encore des possibilités offertes par les paquetages et les unités enfants. Il s’agit ici de mettre en œuvre la notion de type de données abstrait. Un type de données abstrait (abstract data type) est un ensemble logique formé d’un type définissant la structure voulue ainsi que d’opérations disponibles sur les valeurs de ce type. Par définition, la composition exacte de la structure est inconnue, cachée à l’utilisateur d’où l’adjectif abstrait qualifiant le type de données, et cela implique que les opérations proposées sont les seules disponibles pour ce type. La création d’un type de données abstrait, souvent abrégé ADT, s’effectue tout d’abord indépendamment de tout langage de programmation. Il faut simplement décider quelle structure mettre en œuvre et lui donner un nom (le nom du type), puis définir les opérations nécessaires, voire utiles, permettant la gestion de la structure et de ses éléments. On parle alors d’encapsulation de la structure de données et des opérations. Figure 17.2 Types de données abstraits de nombre rationnel et de pile.
T_Nombre_Rationnel Construire Additionner Soustraire Multiplier Diviser Numérateur Denominateur
T_Pile Créer Effacer Empiler Désempiler Sommet Vide
La figure 17.2 montre une première tentative de définition de deux types de données abstraits sous une forme schématique simple séparant le nom du type des opérations. Si dans ces exemples les opérations sont énumérées arbitrairement, la conception propre d’un ADT doit obéir en fait à quelques règles simples. Tout d’abord, un ADT doit être suffisant, si possible minimal et éventuellement complet. Un ADT est suffisant s’il met à disposition toutes les opérations nécessaires à une application donnée. Il est minimal s’il ne comporte pas d’opérations redondantes et complet si toutes les opérations possibles raisonnables sont présentes. Dans la figure 17.2, les deux types de données abstraits sont
TYPES DE DONNÉES ABSTRAITS
410
probablement suffisants pour des applications simples; ils sont assurément minimaux, à condition que l’opération Effacer ne corresponde pas à effectuer Désempiler un certain nombre de fois; ils ne sont de loin pas complets puisque des opérations comme Copier ou Parcourir (pour une pile) ne sont pas présentes. Dans la pratique, la minimalité est parfois négligée pour des raisons de facilité et d’attractivité pour l’utilisateur. Par ailleurs, les opérations sont toujours classées en trois catégories: les constructeurs, les sélecteurs et les itérateurs. Un constructeur (constructor) est une opération agissant sur la structure complète et dont l’action modifie cette structure. Un sélecteur (selector) est une opération qui fournit une information sur la structure dans sa globalité ou sur l’un de ses éléments, sans aucune modification. Enfin, un itérateur (iterator) agit sur tous les éléments de la structure, avec ou sans leur modification. La figure 17.3 présente un type de données abstrait de pile suffisant, dont les opérations sont classées dans les trois catégories précitées. Il n’est pas absolument minimal car certaines opérations (Effacer, Sommet) pourraient être substituées par d’autres (Désempiler). Par contre, leur présence offre un confort nettement accru pour l’utilisateur. Figure 17.3 Type de données abstrait de pile.
Nom du type
T_Pile
Constructeurs
Créer Effacer Empiler Désempiler Copier
Sélecteurs
Sommet Vide Cardinalité
Itérateur
Parcourir
La mise en œuvre de tels types de données abstraits est agréablement simplifiée en Ada non seulement par l’utilisation des paquetages génériques mais aussi par l’existence des types privés et limités exportés des paquetages. Et grâce aux
TYPES DE DONNÉES ABSTRAITS
411
paquetages enfants, il sera aussi facile d’étendre, de compléter la réalisation d’un ADT si une extension de celui-ci est devenue nécessaire. Exemple 17.9 Spécification de la réalisation d’un type de données abstrait de pile dynamique. -- Bases de gestion de piles dynamiques generiques generic type T_Info is (<>); package A_D_T_Piles_Dynamiques_G is -- Le nom du type de donnees type T_Pile is limited private; Pile_Vide : exception;
-- Levee si la pile est vide
------------------------------------------------------------- Constructeurs du type de donnees abstrait ------------------------------------------------------------- Cree une Pile vide procedure Creer ( Pile : out T_Pile ); ------------------------------------------------------------- Insere Info au sommet de Pile procedure Empiler ( Pile : in out T_Pile; Info : in T_Info ); -- Supprime l'information (au sommet de Pile) qui est rendue -- dans Info. Leve Pile_Vide si la suppression est impossible -- (la pile est vide) procedure Desempiler ( Pile : in out T_Pile; Info : out T_Info ); ------------------------------------------------------------- Supprime tous les elements de Pile procedure Effacer ( Pile : in out T_Pile ); ------------------------------------------------------------- Copie Pile_Source dans Pile_Destination procedure Copier ( Pile_Source : in T_Pile; Pile_Destination : out T_Pile ); ------------------------------------------------------------- Selecteurs du type de donnees abstrait ------------------------------------------------------------- Retourne l'information du sommet de Pile. Leve Pile_Vide -- si la consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile ) return T_Info; ------------------------------------------------------------- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile ) return Boolean; ------------------------------------------------------------- Retourne le nombre d'elements de Pile function Cardinalite ( Pile : T_Pile ) return Natural; ------------------------------------------------------------
TYPES DE DONNÉES ABSTRAITS
412
-- Iterateur du type de donnees abstrait ------------------------------------------------------------- Effectue un traitement sur tous les elements de la pile procedure Parcourir ( Pile : in T_Pile ); -----------------------------------------------------------private type T_Element; type T_Lien is access T_Element; type T_Element is
-- Pour un element de la pile
record Information : T_Info; -- L'information sur un seul champ Suivant : T_Lien; -- Pour reperer l'element qui suit end record; type T_Pile is
-- Pour une pile dynamique
record Sommet : T_Lien;
-- Sommet de la pile
end record; end A_D_T_Piles_Dynamiques_G;
L’exemple 17.9 réalise un type de données abstrait de pile dynamique. Le paquetage A_D_T_Piles_Dynamiques_G est générique pour que la structure de pile soit indépendante du type des informations gérées bien que cette indépendance soit encore restreinte, ici à un type discret. De plus, la procédure Parcourir comporte toujours une procédure Traiter interne définie une fois pour toutes (sect. 10.7) et ne permet pas la définition d’un itérateur flexible. Le chapitre 18 permettra d’obtenir toute la généralité nécessaire grâce aux types privés génériques et aux sous-programmes paramètres génériques. Il faut encore noter que le type exporté T_Pile est limité pour que toute copie de pile s’effectue par l’intermédiaire de la procédure Copier. Cependant, la présence et l’utilité de la procédure Creer est discutable puisqu’en Ada, tout pointeur est automatiquement initialisé à la valeur null. En guise de conclusion à ce chapitre, il faut relever que les types de données abstraits se situent entre la programmation structurée modulaire classique et la programmation orientée objet non abordée dans cet ouvrage.
413
17.8 EXERCICES 17.8.1
Sous-programme générique
Ecrire une fonction générique Est_Pair_G qui indique si un nombre entier est pair ou non. Le type entier est le paramètre générique. 17.8.2
Type de données abstrait
Concevoir un type de données abstrait de queue en précisant les catégories d’opérations comme présenté dans la figure 17.3. 17.8.3
Réalisation d’un type de données abstrait
Réaliser le type de données abstrait de queue (exercice 17.8.2) de manière statique par un paquetage générique. Le type des informations ainsi que la taille des queues seront des paramètres génériques. 17.8.4
Instanciations
Instancier plusieurs fois la fonction de l’exercice 17.8.1 et le paquetage de l’exercice 17.8.3.
POINTS À RELEVER
414
17.9 POINTS À RELEVER 17.9.1
En général • La généricité permet l’indépendance de structures de données par rapport à
leur contenu. 17.9.2
En Ada • Une unité générique est une sorte de moule permettant la création de
paquetages ou de sous-programmes instanciés. • Les unités génériques favorisent la réutilisabilité du code source. • Une unité générique est toujours déclarée en deux parties: la spécification
et le corps. • Des valeurs, des types, des sous-programmes et des paquetages peuvent
être passés comme paramètres génériques. • Les paramètres formels peuvent dépendre des paramètres précédents. • Les paramètres formels ne sont jamais statiques. • Un nouvel exemplaire d’une exception déclarée dans un paquetage
générique est créé avec le même nom à chaque instanciation. • Les types de données abstraits représentent une manière de structurer
certains constituants d’une application.
415
C H A P I T R E
UNITÉS GÉNÉRIQ UES: NOTIONS
1 8
416
UNITÉS GÉNÉRIQUES: NOTIONS AVANCÉES
TYPES COMME PARAMÈTRES GÉNÉRIQUES
417
18.1 TYPES COMME PARAMÈTRES GÉNÉRIQUES Dans le chapitre précédent les types discrets et numériques ont été présentés comme paramètres génériques. Cette section complète cette présentation avec les types tableaux, accès et privés. 18.1.1
Types tableaux comme paramètres génériques
Les types tableaux constituent également une catégorie de paramètres génériques. Les deux façons les plus simples mais assez contraignantes de les écrire sont les suivantes: type T_Tableau_Contraint is array (T_Indice) of T_Element; type T_Tableau_Non_Contraint is array (T_Indice range <>) of T_Element;
Mais l’inconvénient de cette manière de faire réside dans le respect des types des indices et des éléments pour le paramètre effectif. Il sera donc bien plus pratique de passer ces deux types comme paramètres génériques également, comme dans l’exemple 18.1. Exemple 18.1 Types tableaux comme paramètres génériques et instanciation. -- ... generic type T_Indice is (<>); discret
-- Les indices sont d'un type
type T_Element is range <>;
-- Les elements sont d'un -- type entier
type T_Tableau_Contraint is array (T_Indice) of T_Element; type T_Tableau_Non_Contraint is array (T_Indice range <>) of T_Element; package Exemple_18_1_G is ... end Exemple_18_1_G; -- Corps de Exemple_18_1_G package body Exemple_18_1_G is ... end Exemple_18_1_G; -- Instanciation de Exemple_18_1_G. On suppose declares les types -type T_Contraint is array (1..10) of Integer; -type T_Non_Contraint is array (Integer range <>) of Integer; package Tableaux is new Exemple_18_1_G ( Integer, Integer, T_Contraint, T_Non_Contraint);
Les paramètres effectifs, ici aussi, doivent correspondre en tous points aux
TYPES COMME PARAMÈTRES GÉNÉRIQUES
418
paramètres formels. 18.1.2
Types accès comme paramètres génériques
Toutes les formes de types accès se retrouvent dans la panoplie des paramètres génériques. Les notations suivantes sont donc autorisées (sect. 15.2 et 15.8): type T_Pt_Type is access T_Type; type T_Pt_Type_Gen is access constant T_Type; type T_Pt_Type_Gen_All is access all T_Type; type T_Pt_Procedure is access procedure ...; type T_Pt_Fonction is access function ...;
Dans tous les cas, les paramètres effectifs correspondants doivent être semblables. Les types pointés seront souvent également spécifiés comme paramètres génériques. 18.1.3
Types privés comme paramètres génériques
Les types privés constituent le genre de paramètres génériques le plus intéressant. Ils permettent de paramétrer un paquetage générique par un type quelconque non limité et soit contraint, soit non contraint mais alors avec valeur par défaut. La notation est identique à celle des types privés exportés d’un paquetage: type T_Prive is private;
Il faut immédiatement insister sur le fait que, comme la structure du type T_Prive est inconnue dans le paquetage générique, l’utilisation du type T_Prive
dans ce paquetage est semblable à l’utilisation d’un type privé comme T_Rationnel à l’extérieur du paquetage Nombres_Rationnels (sect. 16.4). Exemple 18.2 Type privé comme paramètre générique et instanciations. -- Bases de gestion de queues statiques generiques generic Longueur_Max : in Natural := 100; -- Longueur maximale d'une -- queue (sect. 17.3) type T_Info is private; -- Fourni par l'unite -- utilisatrice package Queues_Statiques_G is subtype T_Longueur is Integer range 0..Longueur_Max; subtype T_Numerotation is Integer range 1..Longueur_Max; subtype T_Pos_Indic is Integer range 1..Longueur_Max + 1; type T_Contenu is array ( T_Numerotation ) of T_Info; type T_Queue_Statique is record -- Contenu de la queue Contenu : T_Contenu;
-- Pour une queue statique
TYPES COMME PARAMÈTRES GÉNÉRIQUES
419
-- Longueur de la queue Longueur : T_Longueur := T_Longueur'First; -- Tete de la queue Tete : T_Pos_Indic := T_Pos_Indic'First; -- Queue de la queue Queue : T_Pos_Indic := T_Pos_Indic'First; end record; ------------------------------------------------------------- Autres declarations (§ 14.4.2) ... end Queues_Statiques_G; -- Corps de Queues_Statiques_G package body Queues_Statiques_G is ... end Queues_Statiques_G; -- Instanciations du paquetage generique (§ 7.2.1 et 16.4) package Queues_Entiers is new Queues_Statiques_G (T_Info => Integer); package Queues_Dates is new Queues_Statiques_G (80, T_Date); package Queues_Nombres_Rationnels is new Queues_Statiques_G (T_Info => Nombres_Rationnels.T_Rationnel); package Queues_De_Queues is new Queues_Statiques_G (T_Info => Queues_Dates.T_Queue_Statique);
Le corps du paquetage générique Queues_Statiques_G (exemple 18.2) est identique à celui du paquetage non générique Queues_Statiques (§ 14.4.3 et suivants). Les instanciations montrent différentes possibilités pour les paramètres, du type Integer au type article T_Date pour les deux cas simples. Les types privés T_Rationnel et T_Queue illustrent le fait qu’un type privé peut naturellement jouer le rôle du paramètre effectif d’un paramètre formel générique correspondant. Un type privé comme paramètre générique est le seul moyen permettant de spécifier un type article comme paramètre effectif. 18.1.4 Paramètres génériques constitués de types privés avec discriminants La règle qui empêche un type non contraint (sans valeur par défaut) d’être mentionné comme paramètre effectif d’un paramètre formel générique privé vient du fait suivant: il serait alors impossible, dans le paquetage, de donner l’indispensable contrainte lors de la déclaration d’un objet de ce type (puisque tout objet doit être contraint); ceci est donc contradictoire avec le fait que le paramètre formel est déclaré comme privé, donc de structure inconnue. Cette restriction peut être levée si le paramètre formel générique mentionne la
TYPES COMME PARAMÈTRES GÉNÉRIQUES
420
présence de discriminants inconnus par la notation: type T_Prive_Avec_Discr_Inconnus (<>) is private;
Dans ce cas, n’importe quel type non limité est possible comme paramètre effectif mais, à l’intérieur du paquetage générique, il ne sera pas possible de déclarer des objets non initialisés. L’initialisation obligatoire fournira alors les valeurs des discriminants ou les contraintes nécessaires aux objets déclarés. Un problème subsiste cependant: comment obtenir la valeur initiale d’un objet puisque le type est privé? La réponse à cette question nécessite le passage de constantes ou de fonctions comme paramètres génériques. L’exemple 18.3 qui illustre la mention d’un type privé avec discriminants inconnus sera donc repris ultérieurement (sect. 18.2). Exemple 18.3 Type privé à discriminants inconnus comme paramètre générique. -- Gestion d'un tampon generique de type quelconque non limite generic type T_Info (<>) is private; Valeur_Initiale : in T_Info;
-- Pour la valeur initiale -- du tampon
package Tampon_Non_Limite_G is procedure Nouvelle_Valeur (Info : in T_Info); function Valeur_Actuelle return T_Info; end Tampon_Non_Limite_G; package body Tampon_Non_Limite_G is Tampon : T_Info := Valeur_Initiale;
-- La valeur initiale -- donne la contrainte -----------------------------------------------------------procedure Nouvelle_Valeur (Info : in T_Info) is begin -- Nouvelle_Valeur Tampon := Info; end Nouvelle_Valeur; -----------------------------------------------------------function Valeur_Actuelle return T_Info is begin -- Valeur_Actuelle return Tampon; end Valeur_Actuelle; -----------------------------------------------------------end Tampon_Non_Limite_G;
Pour être complet, mais sans entrer dans les détails, il est possible de mentionner un type privé avec discriminants connus comme par exemple:
TYPES COMME PARAMÈTRES GÉNÉRIQUES
421
type T_Prive_Avec_Discr_Connu (Discr : T_Discr) is private;
Le type mentionné comme paramètre effectif doit alors comporter un discriminant du type ou sous-type T_Discr, avec ou sans valeur par défaut (le paramètre formel ne mentionne jamais de valeur par défaut). 18.1.5
Types limités privés comme paramètres génériques
Un type privé mentionné comme paramètre générique peut être limité par la notation: type T_Limite_Prive is limited private;
Tout ce qui a été présenté pour les types privés non limités (§ 18.1.3 et 18.1.4) s’applique aussi pour de tels paramètres qui, puisqu’ils sont limités, interdisent l’affectation et les opérations d’égalité et d’inégalité prédéfinies sur les objets de ce type. N’importe quel type est possible comme paramètre effectif pourvu qu’il ne soit pas non contraint sans valeur par défaut. Mais en utilisant la notation introduisant des discriminants inconnus type T_Limite_Prive (<>) is limited private;
n’importe quel type sans restriction peut être mentionné comme paramètre effectif! Cependant, il devient impossible de déclarer des constantes ou variables rémanentes (sect. 10.2) de type T_Limite_Prive dans le paquetage. 18.1.6 Types privés comme paramètres génériques et comme types exportés Tous les exemples vus jusqu’à présent illustraient des paquetages dans lesquels le type exporté, s’il existait, était visible, ceci pour ne pas induire de confusion pour le lecteur entre un type privé comme paramètre générique et ce type exporté. Mais il est naturellement possible et souhaitable de construire des paquetages génériques dont le ou les types exportés sont privés (exemple 18.4), voire limités privés. Exemple 18.4 Types privés comme paramètre générique et comme type exporté. -- Bases de gestion de queues statiques generiques generic type T_Info is private;
-- Fourni par l'unite utilisatrice
package Queues_Statiques_G is -- Pour une queue statique de longueur Taille type T_Queue_Statique ( Taille : Positive ) is private; ------------------------------------------------------------- Operations sur une queue statique (§ 14.4.2) sauf -- Parcourir dont le cas est traite plus loin (sect. 18.3) ... private
TYPES COMME PARAMÈTRES GÉNÉRIQUES
422
type T_Contenu is array ( Positive range <>) of T_Info; type T_Queue_Statique ( Taille : Positive ) is record -- Contenu de la queue Contenu : T_Contenu ( 1 .. Taille ); Longueur : Natural := 0;
-- Longueur de la queue
Tete : Positive := 1; Queue : Positive := 1;
-- Tete de la queue -- Queue de la queue
end record; end Queues_Statiques_G;
La partie privée de ce dernier exemple ne contient plus de déclarations de soustypes utilisés dans les champs du type T_Queue_Statique contrairement aux précédents paquetages de gestion de queues statiques. En effet, la taille des queues est ici fournie par un discriminant d’article ne permettant pas la création de ces sous-types.
SOUS-PROGRAMMES COMME PARAMÈTRES GÉNÉRIQUES
423
18.2 SOUS-PROGRAMMES COMME PARAMÈTRES GÉNÉRIQUES Les paquetages de gestion de queues et de piles (chap. 14 et 15) comportent une procédure Traiter (alors interne) qui constitue une excellente candidate pour devenir un paramètre générique. En effet, et plus généralement, des traitements sont souvent nécessaires dans un paquetage (générique), traitements connus dans l’unité utilisatrice mais pas (complètement) dans le paquetage. Le passage de sousprogrammes en paramètres génériques donne une solution à ces problèmes. La forme la plus simple pour la réalisation de ce mécanisme est donnée par le diagramme syntaxique de la figure 18.1. Figure 18.1 Diagramme syntaxique de sous-programme comme paramètre générique. Procédure comme paramètre générique (forme simple) with
procedure
identificateur
liste de paramètres
;
fonction comme paramètre générique (forme simple)
with
function
identificateur
liste de paramètres
return
Id. type
;
Il faut noter la présence du mot réservé with qui permet de distinguer un sousprogramme paramètre générique d’un sous-programme générique lui-même (sect. 17.5 et 18.3)! L’exemple 18.5 illustre la présence d’un tel paramètre. Exemple 18.5 Fonction d’initialisation comme paramètre générique. -- Gestion d'un tampon generique de type quelconque non limite, -- voir l'exemple 18.3 generic type T_Info (<>) is private; with function Valeur_Initiale return T_Info; package Tampon_Non_Limite_G is procedure Nouvelle_Valeur (Info : in T_Info); function Valeur_Actuelle return T_Info; end Tampon_Non_Limite_G;
SOUS-PROGRAMMES COMME PARAMÈTRES GÉNÉRIQUES
424
package body Tampon_Non_Limite_G is Tampon : T_Info := Valeur_Initiale; ...
-- Utilisation du -- parametre generique
end Tampon_Non_Limite_G;
L’instanciation (exemple 18.6) d’un paquetage comportant un ou plusieurs sous-programmes comme paramètres génériques s’effectue comme toute autre instanciation, en spécifiant un sous-programme dont le profil et le mode de passage de ses paramètres correspond à ceux du paramètre formel. Exemple 18.6 Instanciations du paquetage générique Tampon_Non_Limite_G. function Valeur_Nulle return Float is begin -- Valeur_Nulle return 0.0; end Valeur_Nulle; function Chaine_Nulle return String is begin -- Chaine_Nulle return ""; end Chaine_Nulle; ... package Tampon_Reel is new Tampon_Non_Limite_G (Float, Valeur_Nulle); package Tampon_Chaine is new Tampon_Non_Limite_G (String, Chaine_Nulle);
Une autre application plus connue des sous-programmes comme paramètres génériques consiste à résoudre le problème du traitement des informations contenues dans des structures comme les queues et les piles. En effet, le parcours de telles structures (§ 15.4.9 par exemple) implique l’application d’une procédure Traiter à chaque élément de la structure. Or, si le type de ces informations est lui-même un paramètre générique (sect. 17.4 et 18.1), il devient ainsi naturel de transmettre cette procédure également comme paramètre générique. Exemple 18.7 Traitement des informations contenues dans une queue statique générique. -- Bases de gestion de queues statiques generiques generic type T_Info is private; with procedure Traiter ( Info : in out T_Info ); package Queues_Statiques_G is
SOUS-PROGRAMMES COMME PARAMÈTRES GÉNÉRIQUES
425
-- Pour une queue statique de longueur Taille type T_Queue_Statique ( Taille : Positive ) is private; ------------------------------------------------------------- Operations sur une queue statique (§ 14.4.2) ... -----------------------------------------------------------private ... end Queues_Statiques_G;
Il faut cependant relever que l’instanciation ne permettra de spécifier qu’une seule procédure de traitement. Cette limitation pourra néanmoins être éliminée par la transformation de la procédure Parcourir, qui utilise Traiter, en procédure générique (sect. 18.3). Finalement, il existe la possibilité de mentionner des valeurs par défaut pour les sous-programmes paramètres génériques en complétant leur déclaration comme dans les cas suivants: with procedure Traiter ( Info : in out T_Info ) is <>; with function Valeur_Initiale return T_Info is <>;
Si aucun paramètre effectif n’est donné lors de l’instanciation, la valeur par défaut consistera à utiliser une procédure ou fonction de même nom dont le profil et les modes de passage des paramètres sont identiques, procédure ou fonction qui doit être unique et visible à l’endroit de l’instanciation. Un exemple intéressant d’une telle valeur par défaut consiste à écrire une procédure de tri générique, applicable à un tableau d’éléments d’un type quelconque. Cette procédure est présentée dans la section traitant des sous-programmes génériques (sect. 18.3). Pour être complet, il faut encore indiquer qu’il existe une deuxième forme de valeur par défaut non explicitée dans cet ouvrage: with procedure Traiter ( Info : in out T_Info ) is Afficher; with function Valeur_Initiale return T_Info is Valeur_Nulle;
RETOUR SUR LES SOUS-PROGRAMMES GÉNÉRIQUES
426
18.3 RETOUR SUR LES SOUS-PROGRAMMES GÉNÉRIQUES Cette section va compléter l’exemple simple donné précédemment (sect. 17.5) par la présentation de deux cas plus complexes. Le premier réside dans la généralisation des procédures Parcourir examinées lors de la présentation des structures linéaires. Il s’agit ici de les transformer en procédures génériques comportant une seul paramètre générique constitué de la procédure Traiter. La solution ainsi définie (exemple 18.8) est meilleure que celle donnée auparavant (exemple 18.7) car plusieurs procédures de parcours vont pouvoir être instanciées pour chaque instanciation de l’un des paquetages de gestion de ces structures. Exemple 18.8 Traitement des informations contenues dans une queue statique générique. -- Bases de gestion de queues statiques generiques generic type T_Info is private; package Queues_Statiques_G is -- Pour une queue statique de longueur Taille type T_Queue_Statique ( Taille : Positive ) is private; ------------------------------------------------------------- Operations sur une queue statique (§ 14.4.2) ... -----------------------------------------------------------generic with procedure Traiter ( Info : in out T_Info ); procedure Parcourir_G ( La_Queue : in out T_Queue_Statique ); -----------------------------------------------------------private ... end Queues_Statiques_G; -- Corps de Queues_Statiques_G package body Queues_Statiques_G is ... end Queues_Statiques_G; -- Instanciations du paquetage et de la procedure Parcourir_G. Le -- type T_Date est defini au paragraphe 6.2.1 package Queues_Dates is new Queues_Statiques_G ( T_Date ); -- On suppose que la procedure Afficher affiche une date procedure Afficher_Queue is new Queues_Dates.Parcourir_G ( Afficher ); -- On suppose que la procedure Plus_Un_Jour incremente une date -- d'un jour procedure Mise_A_Jour is new Queues_Dates.Parcourir_G ( Plus_Un_Jour );
RETOUR SUR LES SOUS-PROGRAMMES GÉNÉRIQUES
427
Cette section se termine par une procédure de tri générique (exemple 18.9) dont la réalisation regroupe l’utilisation de notions maintenant connues. L’algorithme de tri choisi consiste à trouver le plus petit élément contenu dans un tableau et à le placer en première position, par échange avec l’élément qui s’y trouvait, puis à recommencer pour le tableau amputé de son premier élément et ainsi de suite. Cet algorithme est connu sous le nom de tri par sélection (selection sort). Exemple 18.9 Procédure de tri générique. -- Procedure generique de tri d'une table selon l'algorithme de -- tri par selection generic type T_Indice is (<>); -- Les indices sont de type discret, type T_Element is private; -- les elements les plus generaux -- possibles type T_Table is array (T_Indice range <>) of T_Element; with function "<" (X, Y : T_Element ) return Boolean is <>; procedure Trier_G ( Table : in out T_Table ); procedure Trier_G ( Table : in out T_Table ) is Tampon : T_Element; -- Pour la permutation Indice_Plus_Petit : T_Indice;-- Pour le plus petit element begin -- Trier_G for I in Table'First .. T_Indice'Pred ( Table'Last ) loop Indice_Plus_Petit := I; -- Rechercher le plus petit parmi ceux restants for J in T_Indice'Succ ( I ) .. Table'Last loop if Table ( J ) < Table ( Indice_Plus_Petit ) then Indice_Plus_Petit := J;-- Un nouveau plus petit trouve end if; end loop; -- Permuter le plus petit element trouve avec le premier Tampon := Table ( I ); Table ( I ):= Table ( Indice_Plus_Petit ); Table ( Indice_Plus_Petit ) := Tampon; end loop; end Trier_G;
L’opérateur "<" doit être mentionné comme paramètre formel puisque le type des éléments du tableau à trier est privé. L’utilisation d’une valeur par défaut à l’instanciation est pratique dans tous les cas où le type des éléments est ordonné. A
RETOUR SUR LES SOUS-PROGRAMMES GÉNÉRIQUES
428
contrario, il faut fournir une fonction de comparaison ad hoc. Des instanciations avec des types prédéfinis peuvent s’écrire: procedure Trier is new Trier_G ( Integer, Character, String ); procedure Trier is new Trier_G ( Integer, Float, T_Vecteur, "<");
Ces deux procédures instanciées se surchargent! La première triera un tableau de caractères, la seconde un vecteur (§ 8.2.3). La fonction de comparaison utilisée est l’opérateur "<" sur les caractères dans la première instanciation (valeur par défaut) alors qu’il s’agit de l’opérateur "<" disponible avec le type Float dans la seconde, opérateur mentionné cette fois explicitement.
UNITÉS DE BIBLIOTHÈQUE ET ENFANTS GÉNÉRIQUES
429
18.4 UNITÉS DE BIBLIOTHÈQUE ET ENFANTS GÉNÉRIQUES Une spécification de paquetage ou de sous-programme générique, de même que son corps peuvent être compilés pour eux-mêmes et former une nouvelle unité de bibliothèque. Mais la généricité peut aussi être associée à la notion d’unité enfant (sect. 16.6 et suivantes). En effet, les enfants d’un parent non générique peuvent ou non être génériques, alors que ceux d’un parent générique le sont obligatoirement. Les règles définissant l’instanciation sont les suivantes: • si l’unité parent n’est pas générique, un enfant générique peut être instancié
partout où il est visible; • si l’unité parent est générique, un enfant peut être instancié sans restriction
à l’intérieur du parent, mais ne pourra l’être à l’extérieur que lorsque son parent sera lui-même instancié; une clause de contexte pour l’enfant sera alors nécessaire. Exemple 18.10 Squelette du paquetage parent générique de gestion de nombres rationnels. -- Ce paquetage generique permet le calcul avec les nombres -- rationnels generic type T_Entier is range <>; package Nombres_Rationnels_G is type T_Rationnel is private; -- Autres declarations et operations (sect. 16.4) ... private ... end Nombres_Rationnels_G; package body Nombres_Rationnels_G is -- Corps des operations exportees (sect. 16.4) ... end Nombres_Rationnels_G;
L’exemple 18.11 illustre la définition d’un paquetage enfant générique en reprenant le cas des nombres rationnels (sect. 16.4). Des opérations d’entréessorties sur des valeurs de type T_Rationnel constituent le paquetage enfant. Ainsi il est possible de travailler avec des nombres rationnels en choisissant ou non d’effectuer des lectures ou des écritures de tels nombres. Le parent est d’abord rappelé dans l’exemple 18.10, puis le corps de Nombres_Rationnels_G.ES_G est donné dans l’exemple 18.12.
UNITÉS DE BIBLIOTHÈQUE ET ENFANTS GÉNÉRIQUES
430
Exemple 18.11 Spécification du paquetage enfant générique Nombres_Rationnels_G.ES_G. -- Ce paquetage enfant generique permet les entrees-sorties de -- nombres rationnels generic package Nombres_Rationnels_G.ES_G is ------------------------------------------------------------- Lecture d'un nombre rationnel procedure Get ( X : out T_Rationnel ); ------------------------------------------------------------- Ecriture d'un nombre rationnel procedure Put ( X : in T_Rationnel ); -----------------------------------------------------------end Nombres_Rationnels_G.ES_G;
Exemple 18.12 Corps du paquetage générique Nombres_Rationnels_G.ES_G. -- Ce paquetage generique permet le calcul avec les nombres -- rationnels with Ada.Text_IO; use Ada.Text_IO; package body Nombres_Rationnels_G.ES_G is package Entier_IO is new Integer_IO ( T_Entier ); use Entier_IO; -- Evite de prefixer Get et Put sur les entiers ------------------------------------------------------------- Lecture d'un nombre rationnel procedure Get ( X : out T_Rationnel ) is begin -- Get Put Get Put Get
("Numerateur: "); ( X.Numerateur ); ("Denominateur: "); ( X.Denominateur );
end Get; ------------------------------------------------------------- Ecriture d'un nombre rationnel procedure Put ( X : in T_Rationnel ) is begin -- Put Put ( X.Numerateur, 1); -- Minimum de positions [ARM A.10.6] Put ( " /" ); Put ( X.Denominateur, 1); end Put; -----------------------------------------------------------end Nombres_Rationnels_G.ES_G;
UNITÉS DE BIBLIOTHÈQUE ET ENFANTS GÉNÉRIQUES
431
Finalement, les instanciations peuvent être effectuées en commençant par celle du paquetage parent comme mentionné dans l’une des règles du début de cette section: package Nombres_Rationnels is new Nombres_Rationnels_G ( Integer ); package ES_Nombres_Rationnels is new Nombres_Rationnels.ES_G;
Il faut noter que les instanciations d’unités enfants génériques peuvent devenir des unités de bibliothèque (§ 10.6.1). Comme tous les enfants d’un parent générique sont aussi génériques, il est probable que nombre d’entre eux, y compris Nombres_Rationnels_G.ES_G, n’auront pas de paramètres génériques propres car l’utilisation des paramètres génériques du parent est autorisée.
RETOUR SUR LES TYPES DE DONNÉES ABSTRAITS
432
18.5 RETOUR SUR LES TYPES DE DONNÉES ABSTRAITS L’exemple 18.13 réalise un type de données abstrait de pile de la manière la plus générale possible. Le type T_Info est privé pour que le type des informations gérées soit le plus général possible. Comme illustré précédemment (sect. 18.3), la procédure Parcourir est maintenant générique, avec la procédure Traiter comme paramètre, ce qui va permettre de créer plusieurs itérateurs différents pour une seule instanciation du paquetage. Le lecteur peut comparer le paquetage ci-dessous avec celui de l’exemple 17.9. Exemple 18.13 Spécification de la réalisation d’un type de données abstrait de pile dynamique. -- Bases de gestion de piles dynamiques generiques generic type T_Info is private; package A_D_T_Piles_Dynamiques_G is -- Le nom du type de donnees type T_Pile is limited private; Pile_Vide : exception;
-- Levee si la pile est vide
------------------------------------------------------------- Constructeurs du type de donnees abstrait ------------------------------------------------------------- Cree une Pile vide procedure Creer ( Pile : out T_Pile ); ------------------------------------------------------------- Insere Info au sommet de Pile procedure Empiler ( Pile : in out T_Pile; Info : in T_Info ); ------------------------------------------------------------- Supprime l'information (au sommet de Pile) qui est rendue -- dans Info. Leve Pile_Vide si la suppression est impossible -- (la pile est vide) procedure Desempiler ( Pile : in out T_Pile; Info : out T_Info ); ------------------------------------------------------------- Supprime tous les elements de Pile procedure Effacer ( Pile : in out T_Pile ); ------------------------------------------------------------- Copie Pile_Source dans Pile_Destination procedure Copier ( Pile_Source : in T_Pile; Pile_Destination : out T_Pile ); ------------------------------------------------------------- Selecteurs du type de donnees abstrait ------------------------------------------------------------- Retourne l'information du sommet de Pile. Leve Pile_Vide
RETOUR SUR LES TYPES DE DONNÉES ABSTRAITS
433
-- si la consultation est impossible (la pile est vide) function Sommet ( Pile : T_Pile ) return T_Info; ------------------------------------------------------------- Retourne True si Pile est vide, False sinon function Vide ( Pile : T_Pile ) return Boolean; ------------------------------------------------------------- Retourne le nombre d'elements de Pile function Cardinalite ( Pile : T_Pile ) return Natural; ------------------------------------------------------------- Iterateur du type de donnees abstrait ------------------------------------------------------------- Effectue un traitement sur tous les elements de la pile generic with procedure Traiter ( Info : in out T_Info ); procedure Parcourir ( Pile : in T_Pile ); -----------------------------------------------------------private type T_Element; type T_Lien is access T_Element; type T_Element is -- Pour un element de la pile record Information : T_Info; -- L'information sur un seul champ Suivant : T_Lien; -- Pour reperer l'element qui suit end record; type T_Pile is record Sommet : T_Lien; end record; end A_D_T_Piles_Dynamiques_G;
-- Pour une pile dynamique -- Sommet de la pile
434
18.6 EXERCICES 18.6.1
Sous-programme générique
Ecrire une fonction générique Maximum qui détermine la valeur maximale d’un groupe de valeurs dont le type est un paramètre générique discret. En faire de même pour un paramètre générique de type numérique, de type article, de type accès (les valeurs considérées sont ici celles des variables pointées) et enfin de type privé. 18.6.2
Type de données abstrait
Concevoir un type de données abstrait d’ensemble (au sens mathématique du terme) en précisant les catégories d’opérations comme présenté dans la figure 17.3. 18.6.3
Réalisation d’un type de données abstrait
Réaliser le type de données abstrait d’ensemble (exercice 18.6.1) dont les éléments sont d’un type discret. 18.6.4
Réalisation d’un type de données abstrait
Réaliser le type de données abstrait d’ensemble (exercice 18.6.1) de manière aussi générale que possible.
POINTS À RELEVER
435
18.7 POINTS À RELEVER 18.7.1
En général • La généricité se rencontre dans des langages comme Ada et C++.
18.7.2
En Ada • Les types privés constituent un cas intéressant de paramètre générique par
la généralité qu’ils apportent. • Les unités enfants peuvent aussi être génériques.
436
C H A P I T R E
SURNOMS, TYPES DÉRIVÉS, ANNEXES
1 9
437
SURNOMS, TYPES DÉRIVÉS, ANNEXES PRÉDÉFINIES
SURNOMMAGE
438
19.1 SURNOMMAGE Sans constituer un point absolument fondamental du langage Ada, le surnommage (renaming) possède quelques avantages méritant une présentation dans cette section. Grâce à cette notion, il est possible de: • donner un autre nom, c’est-à-dire déclarer un surnom, à un objet, une
exception, un paquetage, une unité générique ou un sous-programme; • fournir un corps à un sous-programme dont la spécification a déjà été
déclarée. Un objet est surnommé par la forme de déclaration suivante: id_d_objet : id_de_type_ou_sous_type renames objet_surnomme;
avec • • • •
id_d_objet le surnom déclaré; id_de_type_ou_sous_type le type ou le sous-type du surnom; renames un nouveau mot réservé indiquant le surnommage; objet_surnomme l’objet surnommé.
L’objet surnommé peut être une constante, une variable, un élément d’un objet composé ou une tranche de tableau. Le surnom s’applique à l’objet identifié au moment du surnommage comme pour la variable Reponse de l’exemple 19.1. De manière peut-être surprenante, une éventuelle contrainte mentionnée par le soustype n’a aucune influence sur le surnom. Celui-ci est simplement un nouveau nom pour un objet et de ce fait hérite de toutes les caractéristiques de l’objet surnommé. Exemple 19.1 Surnommage d’objets. -- Voir la section 10.7 pour le paquetage Nombres_Rationnels et -- la constante Zero Zero : Nombres_Rationnels.T_Rationnel renames Nombres_Rationnels.Zero; -- Voir l'exemple 18.6 pour le paquetage Tampon_Chaine et la -- variable Tampon Tampon_Caracteres : String renames Tampon_Chaine.Tampon; -- § 7.2.1 pour l'article Noel Jour : Integer renames Noel.Jour; -- § 9.2.3 pour la tranche Chaine (1..Nombre_Car_Lus) Reponse : String renames Chaine (1..Nombre_Car_Lus);
Le surnommage d’un composant ou d’une tranche de tableau est soumis à des restrictions non mentionnées ici car elles ne concernent que quelques cas particuliers. L’intérêt de surnommer de tels objets réside principalement dans une meilleure lisibilité et une simplification du code.
SURNOMMAGE
439
Une exception est surnommée (exemple 19.2) selon la forme de déclaration suivante: id_d_exception : exception renames exception_surnommee;
avec • id_d_exception le surnom déclaré; • exception_surnommee l’exception surnommée. Exemple 19.2 Surnommage d’exceptions. -- Voir la section 10.7 pour le paquetage Nombres_Rationnels Div_Par_0 : exception renames Nombres_Rationnels.Division_Par_Zero; -- Voir la section 12.5 pour le paquetage Ada.IO_Exceptions Fin_Fichier : exception renames Ada.IO_Exceptions.End_Error;
Un paquetage (non générique) est surnommé (exemple 19.3) selon la déclaration suivante: package id_de_paquetage renames paquetage_surnomme;
avec • id_de_paquetage le surnom déclaré; • paquetage_surnomme le paquetage surnommé. Exemple 19.3 Surnommage de paquetages. -- Voir la section 10.7 pour le paquetage Nombres_Rationnels package Fractions renames Nombres_Rationnels; -- Voir la section 12.5 pour le paquetage Ada.IO_Exceptions package Erreurs_ES renames Ada.IO_Exceptions;
Une unité générique est surnommée (exemple 19.4) par l’une des trois formes de déclaration suivantes: generic package id_de_paquetage renames paquetage_generique; generic procedure id_de_procedure renames procedure_generique; generic function nom_de_fonction renames fonction_generique;
avec • id_de_paquetage, id_de_procedure et nom_de_fonction les surnoms déclarés où nom_de_fonction peut être un identificateur ou un
opérateur; • paquetage_generique le paquetage générique surnommé;
SURNOMMAGE
440
• procedure_generique la procédure générique surnommée; • fonction_generique la fonction ou l’opérateur générique surnommé. Exemple 19.4 Surnommage d’unités génériques. -- Voir la section 17.6 pour le paquetage Nombres_Rationnels_G generic package Fractions_G renames Nombres_Rationnels_G; -- Voir la section 18.3 pour la procedure Trier_G generic procedure Sort_G renames Trier_G; -- [ARM 13.9] pour la fonction Ada.Unchecked_Conversion generic function Conversion_Brute renames Ada.Unchecked_Conversion;
Pour les paquetages, comme pour les exceptions et les unités génériques, le surnommage permet de simplifier la notation en raccourcissant les noms. Le surnommage de sous-programmes (non génériques) permet non seulement de créer un surnom d’un sous-programme mais encore de fournir un corps à une spécification de sous-programme, dans un corps de paquetage par exemple. Dans les deux cas, la forme de déclaration est donnée par l’un des diagrammes syntaxiques de la figure 19.1. Figure 19.1 Diagrammes syntaxiques de surnommage de sous-programmes. Surnommage de procédure procedure
identificateur de surnom
liste de paramètres
renames
id de procédure
return
id de type ou sous-type
Surnommage de fonction ou de fonction-opérateur function
surnom de fonction ou d’opérateur
liste de paramètres
renames
nom de fonction ou d’opérateur
SURNOMMAGE
441
Effectuer le surnommage d’un sous-programme (exemple 19.5) représente une opération moins triviale que dans le cas des exceptions, des paquetages ou encore des unités génériques. En effet, certaines règles doivent être respectées: • lors de la définition d’un surnom d’un sous-programme, le nombre, le type,
le mode de passage des paramètres et le type du résultat (dans le cas d’une fonction ou d’une fonction-opérateur) du surnom doivent être identiques à ceux du sous-programme surnommé; • lors de la fourniture du corps d’une spécification, non seulement la règle
précédente s’applique aussi, mais il faut encore que les contraintes des (éventuels) sous-types des paramètres et du résultat soient les mêmes. Les noms des paramètres peuvent donc être librement choisis. Par contre, il faut mentionner que les valeurs par défaut apparaissant dans la définition d’un surnom ainsi que leur présence ou absence se substituent à ce qui existait dans le sousprogramme surnommé lors de l’utilisation du surnom. A contrario, les contraintes sur les paramètres du sous-programme surnommé ou le résultat de la fonction surnommée s’appliquent aussi au surnom, quelles que soient celles présentes dans la définition de ce surnom. Exemple 19.5 Surnoms de sous-programmes. -- § 4.3.4 procedure Up ( Caractere : in out Character ) renames Majusculiser; -- [ARM A.5] function Sqrt (X : Float) return Float renames Ada.Numerics.Elementary_Functions.Sqrt; -- Voir la section 10.3 function "**" (X : Nombres_Rationnels.T_Rationnel; Exp : Natural) return Nombres_Rationnels.T_Rationnel renames Nombres_Rationnels.Puissance; -- Voir la section 10.7 use Nombres_Rationnels; function Creer (
Numerateur : Integer; Denominateur : Positive ) return T_Rationnel renames "/";
Parmi les avantages du surnommage des sous-programmes, citons la levée d’ambiguïtés lors d’utilisation de clauses use (§ 10.5.2), l’utilisation de fonctions sous la forme de fonctions-opérateurs ou encore comme alternative aux clauses use type (§ 11.5.3).
SURNOMMAGE
442
Les unités enfants (paquetages ou sous-programmes) peuvent naturellement aussi être surnommées. Par exemple, le paquetage d’entrées-sorties Ada.Text_IO est surnommé en Text_IO dans la norme Ada 95 pour des raisons de compatibilité avec la norme précédente Ada 83. Le surnommage d’un type peut s’effectuer grâce à un sous-type sans contrainte (§ 6.1.1). Finalement et pour être complet, il est possible de surnommer des valeurs énumérées ainsi que certains attributs comme Succ et Pred.
OPÉRATIONS PRIMITIVES ET TYPES DÉRIVÉS
443
19.2 OPÉRATIONS PRIMITIVES ET TYPES DÉRIVÉS Une opération primitive (primitive operation) est une opération disponible sur les valeurs d’un type et appartenant à l’une des catégories suivantes: • les opérations prédéfinies comme l’affectation, l’égalité prédéfinie,
certains attributs; • les sous-programmes déclarés dans une spécification de paquetage et comportant un paramètre ou un résultat de ce type alors que ce type est une déclaration de la spécification du paquetage; • les opérations primitives héritées de son parent si le type est dérivé. Par exemple, les opérateurs arithmétiques et ceux de comparaison sont des opérations primitives des types Integer et Float (conceptuellement, ces types sont déclarés dans un paquetage virtuel appelé Standard, sect. 19.3). Pour le type T_Rationnel (sect. 10.7), toutes les fonctions de la spécification du paquetage Nombres_Rationnels sont des opérations primitives. La notion d’opération primitive est utilisée principalement en relation avec les types dérivés. Un type dérivé (derived type) est une nouveau type possédant toutes les caractéristiques d’un type existant appelé type parent (parent type). Un type dérivé (exemple 19.6) est donc fondamentalement différent d’un sous-type sans contrainte (§ 6.1.1). Il hérite automatiquement une copie des valeurs et des opérations primitives de son type parent mais il est naturellement possible de définir d’autres opérations sur les valeurs de ce type, voire redéfinir certaines opérations héritées. Lors de la dérivation, des contraintes peuvent s’ajouter au nouveau type de la même manière que lors de la création d’un sous-type, ou alors en dérivant directement d’un sous-type. Exemple 19.6 Types dérivés et contraintes. type type type type
T_Euro is new Integer; -- Integer est le type parent T_Franc is new Integer range –1E12 .. 1E12; T_Positif is new Positive; T_Nouvelle_Date is new T_Date; -- § 7.2.1
L’intérêt de la dérivation de type réside d’une part dans la création de types différents permettant d’éviter le mélange accidentel de valeurs (vérification à la compilation, exemple 19.7) et d’autre part dans la réalisation de la déclaration complète d’un type privé. Exemple 19.7 Mélange accidentel de valeurs. Euro : T_Euro := 0;
OPÉRATIONS PRIMITIVES ET TYPES DÉRIVÉS
444
Franc : T_Franc; Dix : constant Integer := 10; ... Euro := Euro + 1; -- Possible car 1 est un entier Euro := Euro + Dix; -- INTERDIT car types differents Franc := Euro; -- INTERDIT car types differents
Comme exemple d’implémentation d’un type privé, voici un paquetage d’attribution d’identités privées (exemple 19.8), réalisées sous forme de numéros entiers positifs. Exemple 19.8 Paquetage dont le type privé est réalisé par un type dérivé. -- Fournit une identite differente a chaque appel de la fonction -- Nouvelle_Identite package Attribution_Identites is type T_Identite is private; function Nouvelle_Identite return T_Identite; private type T_Identite is new Natural; end Attribution_Identites; package body Attribution_Identites is Identite_Courante : T_Identite := 0; -- Variable remanente ------------------------------------------------------------- Fournit une identite differente a chaque appel function Nouvelle_Identite return T_Identite is begin -- Nouvelle_Identite Identite_Courante := Identite_Courante + 1; return Identite_Courante; end Nouvelle_Identite; -----------------------------------------------------------end Attribution_Identites;
Le parent d’une dérivation peut être lui-même un type dérivé. La répétition de cette situation peut conduire à l’existence d’une ou de plusieurs hiérarchies arborescentes de types dérivés les uns des autres dans une application. Dans une telle hiérarchie la conversion explicite d’une valeur de l’un des types en la même valeur d’un autre type de la hiérarchie est possible en utilisant le nom du type cible (exemple 19.9), comme c’était déjà le cas pour les types numériques (§ 6.2.6) ou pour les tableaux (sect. 8.4).
OPÉRATIONS PRIMITIVES ET TYPES DÉRIVÉS
445
Exemple 19.9 Mélange de valeurs par conversion explicite. Euro : T_Euro := 0; Franc : T_Franc; Dix : constant Integer := 10; ... Euro := Euro + T_Euro (Dix); -- Possible car Dix est converti Franc := T_Franc (Euro); -- Possible car Euro est converti
Enfin, les types dérivés représentent la base de la programmation orientée objets en Ada, et réalisent en particulier le concept d’héritage.
UNITÉS PRÉDÉFINIES
446
19.3 UNITÉS PRÉDÉFINIES Toutes les unités prédéfinies font partie d’une bibliothèque composée du paquetage Standard, parent de trois paquetages principaux nommés Ada, System et Interfaces. Toutes les autres unités prédéfinies sont des enfants de ces trois paquetages. Une description précise de cette bibliothèque nécessiterait plusieurs dizaines de pages, raison pour laquelle seules les idées générales de son contenu vont être présentées, accompagnées des références à la norme Ada. Le paquetage Standard [ARM A.1] déclare entre autres les types de base Integer, Boolean, Float, Character, String, Duration, les opérations sur ces types et les quatre exceptions Constraint_Error, Program_Error, Storage_Error et Tasking_Error. Une clause de contexte et une clause use pour ce paquetage sont implicitement présentes avant toute unité de compilation. Dans ce qui suit, seuls les identificateurs des unités sont utilisés pour des raisons de lisibilité du texte. Dans la pratique de la programmation, il est bien entendu que cette mention des seuls identificateurs est réalisable par des clauses use. Le paquetage Ada représente la racine de la première hiérarchie. Il ne contient pas de déclaration. Son premier fils, le paquetage Calendar [ARM 9.6], offre des opérations de traitement de dates et de durées. Il est aussi possible d’obtenir l’instant courant par la fonction Clock. Ce paquetage permet donc la datation, ainsi que l’évaluation de durées de traitement particulièrement utiles lors de la réalisation de programmes dits temps réel parce qu’ils doivent respecter des contraintes temporelles. Les caractères peuvent être manipulés par les paquetages Handling et Latin_1 [ARM A.3], le paquetage Characters, lui-même vide, jouant simplement le rôle de leur parent. Le paquetage Handling offre des opérations comme la transformation en minuscules ou majuscules ainsi que des fonctions déterminant si un caractère est une lettre, un chiffre, un caractère de contrôle, etc. Latin_1 définit essentiellement des constantes nommant les 256 éléments du jeu de caractères LATIN-1. Command_Line [ARM A.15] offre la possibilité à un programme Ada d’accéder aux arguments de la commande qui l’a invoqué et de donner une valeur au code de retour si ce dernier est défini dans l’environnement d’exécution [BAR 97].
Le petit paquetage Decimal [ARM F.2] fournit un ensemble de constantes et une procédure générique de division, applicables aux valeurs réelles décimales non traitées dans cet ouvrage. Il doit être disponible si l’annexe F (sect. 19.4) est offerte par l’implémentation. Les trois paquetages Direct_IO [ARM A.8.4], Exceptions [ARM 11.4.1] et IO_Exceptions [ARM A.13] ont déjà fait l’objet d’une présentation dans les sections 12.4, respectivement 13.4 et 12.5.
UNITÉS PRÉDÉFINIES
447
Le paquetage Numerics [ARM A.5] définit les célèbres constantes mathématiques π et e. Ses enfants [ARM A.5.1] Elementary_Functions et Generic_Elementary_Functions offrent des opérations trigonométriques de base soit sous forme générique, soit par une instanciation réalisée avec le type Float. Les paquetages Discrete_Random et Float_Random [ARM A.5.2] offrent des générateurs de nombres pseudo-aléatoires, Discrete_Random permet d’obtenir une valeur pseudo-aléatoire comprise dans un type discret passé en paramètre alors que pour Float_Random, la valeur est toujours comprise dans l’intervalle 0.0..1.0. Dans les deux cas, les suites de valeurs générées peuvent ou non être reproductibles. Les derniers enfants de Numerics sont définis dans l’annexe G (sect. 19.4) et concernent le traitement des nombres complexes. Les deux paquetages Sequential_IO [ARM A.8.1] et Storage_IO [ARM A.9] ont également déjà fait l’objet d’une présentation dans les sections 12.4, respectivement 12.6. De même, le paquetage Streams_IO [ARM A.12.1] a été mentionné dans la section 12.6. Son parent, Streams [ARM 13.13.1], permet la création et la gestion de flots (streams), c’est-à-dire de fichiers formés d’éléments hétérogènes. De plus amples informations sont données dans [BAR 97]. Le paquetage Strings [ARM A.4.1] est le parent de tous les paquetages enfants de traitement de chaînes de caractères. Il définit uniquement des entités glo-bales comme des constantes caractères, des exceptions et des types. La raison de la présence de plusieurs enfants réside dans la diversité de la gestion des chaînes de caractères. Les trois paquetages Bounded [ARM A.4.4], Fixed [ARM A.4.3] et Unbounded [ARM A.4.5] fournissent des opérations comme celles présentées pour Fixed dans la section 9.3. Le paquetage Bounded gère des chaînes ayant une longueur maximale dont seule une partie est utilisée en permanence; Fixed est prévu pour des chaînes de taille fixe; enfin Unbounded s’occupe des chaînes de longueur quelconque, non bornée. Les paquetages Maps [ARM A.4.2] et Constants [ARM A.4.6] permettent d’effectuer des correspondances entre caractères ou ensembles de caractères, comme par exemple entre minuscules et majuscules, ou encore entre une lettre accentuée et son équivalent sans accent. Ces correspondances facilitent énormément la recherche de motifs (patterns), c’est-àdire de textes particuliers, dans des chaînes de caractères. Le très connu paquetage Text_IO [ARM A.10.1] offre des opérations d’entrées-sorties textuelles classiques. Il fournit directement la lecture et l’écriture de caractères de type Character ou de chaînes de type String ainsi que des traitements sur les lignes, les colonnes et les pages d’un fichier texte. Par la définition de paquetages internes génériques, il permet également la création de paquetages instanciés pour la lecture et l’écriture d’entiers signés (instanciation de Integer_IO), d’entiers non signés (instanciation de Modular_IO), de nombres réels en virgule flottante (instanciation de Float_IO), en virgule fixe (instanciation de Fixed_IO) ou encore décimaux (instanciation de Decimal_IO),
UNITÉS PRÉDÉFINIES
448
et enfin de valeurs énumérées (instanciation de Enumeration_IO). Le paquetage enfant Complex_IO [ARM G.1.3] est défini dans l’annexe G (sect. 19.4) et traite la lecture et l’écriture de nombres complexes alors que Editing [ARM F.3.3] provient, lui, de l’annexe F (sect. 19.4). Finalement, Text_Streams [ARM A.12.3] réalise des entrées-sorties sur des flots et permet le mélange de texte et de binaire. La fonction générique Unchecked_Conversion [ARM 13.9] est un utilitaire de bas niveau et convertit sans aucune vérification la suite de bits représentant une valeur d’un type en la même suite binaire mais interprétée selon un autre type. Les deux types sont a priori quelconques mais l’implémentation peut imposer des contraintes raisonnables comme par exemple un nombre de bits identiques pour les valeurs de ces types. La procédure générique Unchecked_Deallocation [ARM 13.11.2] a été décrite dans la section 15.7 lors de la présentation des types accès. Pour terminer l’examen des enfants du paquetage Ada, il reste à relever l’existence de paquetages identiques à Text_IO et ses enfants mais applicables à des caractères de type Wide_Character occupant 16 bits. Ils sont reconnaissables à leur nom qui est systématiquement préfixé par Wide_. Le paquetage Interfaces [ARM B.2] constitue la racine de la deuxième hiérarchie et définit des types entiers signés et non signés propres à l’implémentation utilisée ainsi que des opérations de décalage et de rotation de bits pour les types entiers non signés. Ses trois enfants permettent l’interfaçage entre des unités Ada et d’autres, écrites dans les langages C, COBOL et Fortran. Enfin, le paquetage System [ARM 13.7] et ses enfants forment la dernière hiérarchie et définissent des caractéristiques dépendantes de l’implémentation comme le plus grand intervalle pour des nombres entiers, la précision maximale des nombres réels ou encore la taille d’un mot mémoire. Leur contenu dépasse largement les objectifs de cet ouvrage.
ANNEXES PRÉDÉFINIES SPÉCIALISÉES
449
19.4 ANNEXES PRÉDÉFINIES SPÉCIALISÉES La norme Ada propose, en plus de la partie obligatoire (core) du langage, c’està-dire l’ensemble du langage que tous les compilateurs Ada doivent pouvoir compiler, six annexes dédiées à des classes d’application bien précises. Comme pour la section précédente, un examen approfondi des annexes nécessiterait non seulement plusieurs dizaines de pages d’explications mais aussi des connaissances préliminaires particulières pour plusieurs d’entre elles. Seules les fonctionnalités générales vont donc être présentées, accompagnées des références à la norme Ada. Il faut encore préciser que toutes les annexes spécialisées sont facultatives, une implémentation peut parfaitement n’en supporter aucune. L’annexe Programmation des systèmes [ARM ANNEXE C] offre principalement l’accès au code machine, le traitement des interruptions, des pragmas pour l’accès concurrent à des variables partagées et des paquetages pour l’identification de tâches. L’annexe Systèmes temps réel [ARM ANNEXE D] précise l’utilisation de priorités, l’ordonnancement et le contrôle direct de tâches; elle fournit un paquetage de temps monotone croissant, définit les restrictions conduisant à des applications temps réel simplifiées et impose des conditions sur l’instantanéité des effets d’une instruction abort. Grâce à l’annexe Systèmes répartis [ARM ANNEXE E], il est possible de décomposer un programme en partitions destinées à s’exécuter sur un ou plusieurs processeurs et d’assurer la communication entre elles. L’annexe Systèmes d’informations [ARM ANNEXE F] permet l’interfaçage des programmes Ada avec des programmes COBOL. Dans l’annexe Numérique [ARM ANNEXE G], des paquetages de gestion de nombres complexes sont définis, ainsi que les exigences pour la précision de l’arithmétique des nombres réels en virgule flottante et en virgule fixe. Enfin, l’annexe Fiabilité et sécurité [ARM ANNEXE H] propose plusieurs pragmas visant à obtenir entre autres une meilleure sécurité à l’exécution et une analyse facilitée du code source.
450
19.5 EXERCICES 19.5.1
Surnoms
Déclarer des surnoms pour les entités suivantes: • le type T_Queue_Statique de l’exemple 16.6 (attention au piège); • la fonction Creer de l’exemple 16.7; • le paquetage enfant Nombres_Rationnels.Operations_Internes
de l’exemple 16.8; • la tranche de tableau Chaine(1..Nombre_Car_Lus) de l’exemple 9.3.
19.5.2
Opérations primitives
Enumérer le plus d’opérations primitives possible pour les types • • • • •
19.5.3
Boolean; T_Jours_De_La_Semaine (§ 5.2.2); T_Vecteur et T_Matrice (exemple 8.3); T_Polynome (exemple 11.1); T_Queue_Statique (exemple 14.3).
Types dérivés
Dériver les types ci-dessous et énumérer leurs opérations primitives: • • • • •
Boolean; T_Polynome (exemple 11.1); T_Queue_Statique (exemple 14.3); T_Queue_Dynamique (exemple 16.11); T_Rationnel (fig. 16.5)
POINTS À RELEVER EN ADA
451
19.6 POINTS À RELEVER EN ADA • Le surnommage peut améliorer la lisibilité du code source. • Grâce au surnommage, il est possible de fournir un corps de sous-
programme correspondant à une spécification, déclarée par exemple dans la spécification d’un paquetage. • Les types dérivés permettent d’éviter des erreurs dues à des mélanges de
valeurs. • La définition complète d’un type privé peut être fournie par une dérivation
de type. • Les unités prédéfinies ainsi que les annexes spécialisées constituent le
pendant des bibliothèques prédéfinies existant dans d’autres langages de programmation.