You are on page 1of 451

1

C H A P I T R E 1

NOTIONS
DE BASE
DÉFINITIONS GÉNÉRALES 2

1.1 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 mettre la clé dans le démarreur;


2 serrer le frein à main;
3 mettre le levier des vitesses au point mort;
4 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 compi-
lation 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 en-
vironnement de programmation particulier. Parfois ce terme représente l’environ-
nement lui-même.
LANGAGES DE PROGRAMMATION 4

1.2 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
Année Langage Remarques
d’application

1955 Fortran Calcul scientifi- Langage ancien, dont les versions plus récentes
que comportent encore des bizarreries héritées des
années 50; normalisé actuellement sous l’appellation
Fortran 98.

1960 Algol-60 Algorithmique Premier langage dit structuré grâce à la notion de


blocs.

1959 Lisp Intelligence artifi- Premier langage non impératif.


cielle

1961 Cobol Applications Langage verbeux, peu maniable, encore très utilisé
commerciales 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 mai- Basique, simple d’utilisation mais peu adapté à la
son» programmation structurée.

1970 Prolog Intelligence artifi- Langage non impératif.


cielle

1971 Pascal Enseignement Créé à l’Ecole polytechnique de Zurich, diffusé par-


tout mais souffrant des différences présentes dans les
nombreuses versions existantes.
LANGAGES DE PROGRAMMATION 5

Tableau 1.1 (suite) Principaux langages de programmation.

Domaine
Année Langage Remarques
d’application

1972 C Programmation Accès facile au matériel, largement diffusé; norma-


système lisé sous l’appellation ANSI-C.

1976 Smalltalk Langage général Programmation orientée objets, prototypage rapide.

1980 Modula-2 Programmation Descendant direct de Pascal, mêmes problèmes de


système versions différentes.

1983 Ada Langage général Riche, utilisé pour développer des systèmes comple-
xes 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 program-
mation d’applications classiques mais aussi
d’applets.

1.2.2 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 6

1.3 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 7

1.4 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’ori-
gine 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’habi-
tuer à 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 li-
gnes, ce qui représente encore un tout petit programme!
PROPRIÉTÉS D’UN PROGRAMME 8

1.5 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 forte-
ment 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 9

1.6 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ésolu-
tion, 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 10

1.7 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 ci-
dessus 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. Celle-
ci 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 11

1.8 APPLICATION À L’EXEMPLE INTRODUCTIF

1.8.1 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) X

(X,Y)
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écom-
position 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 : constant := 1; -- Pour se deplacer d'une unite
Gauche : constant := –1; -- dans les quatre directions
Haut : constant := –1;
Bas : constant := 1;
-- 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; -- Abscisse et ordonnee du point
Ordonnee_Carre : Integer; -- 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 15

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;
STRUCTURE D’UN PROGRAMME ADA 16

1.9 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 fonction-
nement 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 : constant := 1;
Gauche : constant := –1;
Haut : constant := –1;
Bas : constant := 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 verbe article nom


sujet verbe complément

identificateurs et symboles

Line ( A _ L a _ V e r t i c a l e, Cote_Carre * Bas ) ;

id. de procédure constante constante constante

appel de procédure paramètre paramètre

1.10.2 Jeu de caractères en Ada


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 ma-
juscules et minuscules, les lettres accentuées, les chiffres, les symboles de ponc-
tuation, 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 par-
lants 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 im-
primables 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 6 7 8 9

identificateur _
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 instruc-
tions 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 2

NOMBRES
ET
ENTRÉES
-SORTIES
26 NOMBRES ET ENTRÉES-SORTIES DE BASE
RAPPELS 27

2.1 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 ins-
tructions 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 28

2.2 TYPES ENTIERS

2.2.1 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

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

• 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; -- Une constante entiere


Nombre : Integer; -- Une variable entiere
begin -- Exemple_2_3
Nombre := 5; -- Affecte la valeur 5 a Nombre
Nombre := Nombre + 4; -- Affecte la valeur 9 a Nombre
Nombre := (36 / 10) * 2; -- Affecte la valeur 6 a Nombre
Nombre := Nombre / 2; -- Affecte la valeur 3 a Nombre
Nombre := Max; -- Affecte la valeur de Max a
... -- Nombre

2.2.4 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; -- Deux variables entieres sans
valeurs
Resultat : Integer; -- 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 12.0E–6

1_0.0 1_000.0 1_0.0E2 1.0E1_0

Les opérateurs arithmétiques possibles sur les valeurs réelles se répartissent en


deux catégories:
les opérateurs unaires + – abs

• 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 + – * / **

• 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.0estune 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 37

2.3.3 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 38

2.4 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é, ceux-
ci 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 40

2.5 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 )

• 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 ( 5 )5 est converti en 5.0;


Integer ( 2.03 )2.03 est converti en 2;
Float ( –800 ) / 2.5E2l’expression vaut –3.2;
Integer ( 3.5 ) 3.5 est converti en 4;
Integer ( –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 signi-
ficatifs ne signifient plus rien!
DIALOGUE PROGRAMME-UTILISATEUR 42

2.6 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 dan-
gereuse.
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ées-
sorties 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 com-
me 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 44

Ada.Float_Text_IO.Get ( Longueur_Brique );
Ada.Text_IO.Put ( "La largeur: " );
Ada.Float_Text_IO.Get ( Largeur_Brique );
Ada.Text_IO.Put ( "La hauteur: " );
Ada.Float_Text_IO.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.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 ce que l’utilisateur a donné

Ada.Float_Text_IO.Get(Longueur_Brique); 30.0 20.0 15.5


Ada.Float_Text_IO.Get(Largeur_Brique);
Ada.Float_Text_IO.Get(Hauteur_Brique);

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 ce que l’utilisateur a donné

Ada.Float_Text_IO.Get(Longueur_Brique); 30.0 20.0 15.5


Ada.Text_IO.Skip_Line; 10.0
Ada.Float_Text_IO.Get(Largeur_Brique); 5.0 1.0
Ada.Text_IO.Skip_Line;
Ada.Float_Text_IO.Get(Hauteur_Brique);

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 va-
riable 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 va-
riable 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 48

Le volume du mur de briques vaut : 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 EXERCICES

2.7.1 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 51

2.8 POINTS À RELEVER

2.8.1 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 3

SÉLECTION,
ITÉRATION,
AUTRES
TYPES DE
BASE
53 SÉLECTION, ITÉRATION, AUTRES TYPES DE BASE
INSTRUCTION IF SANS ALTERNATIVE 54

3.1 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;

• 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_234+5 = 56–17 est vraie;


expression_1 /= expression_234+5 /= 56–17 est fausse;
expression_1 <= expression_21 <= 5 est vraie, de même que 1 <= 1;
expression_1 < expression_25 < 3*3 est vraie;
expression_1 >= expression_25 >= 1 est vraie, de même que 1 >= 1;
expression_1 > expression_21 > 5 est fausse.

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 56

3.2 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 -- Nb_Courant est-il impair?
Put ( "est 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;

• 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 57

3.3 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 -- Nb_Courant est-il une unite?
Put ( "est une unite" );
elsif Nb_Courant < 100 then -- Nb_Courant est-il une dizaine?
Put ( "est une dizaine" );
elsif Nb_Courant < 1000 then -- Nb_Courant est-il une
centaine?
Put ( "est une centaine" );
elsif Nb_Courant < 10_000 then -- Nb_Courant est-il un millier?
Put ( "est 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


• 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 sont fausses, c’est
autre_suite_d_instructions qui l’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 59

3.4 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 = /= <= < >= >

• 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é.

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

3.4.3 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; -- Deux variables booleennes
Present : Boolean := False;
Nb_Courant : Integer := ...; -- 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); -- Lit une valeur booleenne
Put (Trouve); -- 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 63

3.5 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;

• 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’effec-
tuer. 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; -- Abscisse du point initial
Y_Init : constant := 30; -- 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); -- Dessin de la base
Line (–20, –20); -- Dessin du cote a droite
Line (–20, 20); -- 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 de expression_2 à
expression_1; reverse est un mot réservé de plus.
ITÉRATION: LA BOUCLE WHILE 67

3.6 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;

• 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 69

3.7 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 ( "Votre choix parmi les quatre options:" );
Put_Line ( "1. Calcul de la moyenne" );
Put_Line ( "2. Calcul de la variance" );
Put_Line ( "3. Calcul de l'ecart-type" );
Put_Line ( "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; -- Permet de terminer le
-- traitement des nombres
Nombre_Lu : Integer; -- 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 Précisions sur l’utilisation des boucles et de l’instruction


exit
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; -- Sortie de la boucle interne
-- si B est vraie
...
exit Externe when C; -- Sortie d’Externe si C est vraie
...
end loop;
...
end loop Externe;
L’instruction exit; (sans condition) existe et provoque la sortie incondi-
tionnelle 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 72

3.8 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 cons-
tante 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 := ' '; -- Le caractere appele
-- espace (space)
Signe : Character; -- Deux variables caracteres
Lettre_Courante : Character;
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); -- Lit un caractere
Put (Lettre); -- Ecrit le caractere lu
Put ('A'); -- 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éces-
saires que dans le texte source du programme pour les distinguer d’un identi-
ficateur, 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 75

3.9 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 expres-
sion 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’expres-
sion 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
n n!
coefficient i du binôme  a vaut --------------------------- , i et n commençant à 0.
 b i! × ( n – i )!
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 4

PROCÉDU
RES ET
83 PROCÉDURES ET FONCTIONS
MOTIVATION 84

4.1 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 ); -- Deplacement au point (10,10)
Dessiner_Carre; -- Dessin du carre depuis ce
point...
Move_To ( 110,10 ); -- ... et de meme pour les trois
autres
Dessiner_Carre;
Move_To ( 110,110 );
Dessiner_Carre;
Move_To ( 10,110 );
Dessiner_Carre;
end Dessin_Quatre_Carres;
MOTIVATION 86

On remarquera la lisibilité de ce programme, simple il est vrai, grâce à l’intro-


duction 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 program-
meur 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 87

4.2 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’en-
tê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 identificateur
( : de type ou )
de paramètre
de sous-type
out

, in out := expression

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)
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 procedure Dessin_Quatre_Carres is


Cote : constant := 50; begin -- Dessin_Quatre_Carres
begin -- Dessiner_Carre ap ...
pe
Line ( Cote, 0 ); l Move_To ( 10,10 );
Line ( 0, Cote ); Dessiner_Carre;
Line ( – Cote, 0 ); Move_To ( 110,10 );
Line ( 0, – Cote ); ur ...
to
end Dessiner_Carre; re
end Dessin Quatre Carres;
PARAMÈTRES DES PROCÉDURES 90

4.3 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 paren-
thè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 les
dimensions d'une brique ".

4.3.3 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;
Dessiner_Carre;le carré aura 50 unités de côté, valeur donnée par le calcul
de l’expression par défaut (constante 50).

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 inver-
sement) 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 97

4.4 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-
- 2
Lettre : Character; -- dans la region A-- 3
Nombre : Integer; -- 4
------------------------------------------------------------
-- 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; -- Trois identificateurs declares-
- 10
NOTIONS DE PORTÉE ET DE VISIBILITÉ DES IDENTIFICATEURS 98

Ok : Boolean; -- 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 Objet Déclaré


dans la A B C D à la
région 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_ C oui non oui oui procédure 7


2
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 101

4.5 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 102

4.6 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, option-
nellement, 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

identificateur
function identificateur liste de paramètres return
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;

• 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écu-
tion 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 104

4.7 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écifi-
cation 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 106

4.8 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éci-
fications 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 ambi-
guï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 108

4.9 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 := "+"( 3, 2.3 ); -- Premiere fonction "+" appelee
X := 3 + 2.3; -- Premiere fonction "+" appelee
X := 2.3 + 3; -- Seconde fonction "+" appelee
X := 2.0 + 3.0; -- Operateur "+" predefini pour les
reels
V := 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 sous-


programme 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 inver-
sement.

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 P;
procedure P ( E : in Integer);
procedure P ( A : Integer);
procedure P ( E : in out Integer);
procedure P ( E : in Integer := 0);
111

procedure P ( E : out Integer);


procedure P ( E : in Integer;
S : out Float);
procedure P ( E : in Float;
S : out Float);
procedure P ( E : in Integer;
S : 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 sous-
programme.
• 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 5

T YPES,
T YPES
É NUMÉRA
T IFS,
115 TYPES, TYPES ÉNUMÉRATIFS, INSTRUCTION CASE
NOTION DE TYPE 116

5.1 NOTION DE TYPE

5.1.1 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 affectation, addition, soustraction,


entre les bornes Integer'First comparaison, attributs, etc.
et Integer'Last

Charac- tous les caractères compris entre les affectation, comparaison, attributs,
ter deux bornes Character'First etc.
et Character'Last

Boolean les deux valeurs False et affectation, comparaison, attributs,


True 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 118

type Date is record -- § 7.2.1


Jour : Integer;
Mois : Mois_De_L_Annee;
Annee : Integer;
end record;
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 ty-
pes!) 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 119

5.2 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 ( Rouge, Orange, Jaune, Vert,


Bleu, Indigo, Violet );
type T_Mois_De_L_Annee is ( 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 la valeur Lundi a Demain
Jour := Jeudi; -- Affecte la valeur Jeudi a Jour
Demain := T_Jours_De_La_Semaine'Succ ( Jour );-- § 5.2.4
Hier := T_Jours_De_La_Semaine'Pred ( Jour );-- § 5.2.4
...

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éca-
nisme 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); -- Lit un jour de la semaine
Put (Jour); -- Affiche en majuscules la valeur
lue
Put (Mardi); -- Affiche MARDI
Put (T_Jours_De_La_Semaine'Val(6)); -- 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 Jour in Lundi..Dimanche loop ... end loop;
• for Jour in T_Jours_De_La_Semaine'First..Dimanche loop
• ...
• end loop;
• for 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 123

5.3 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; -- Nombre donne par l'utilisateur
...
Get ( Reponse ); -- Reponse donnee par l'utilisateur
case Reponse is
when 1 =>...; -- 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
6
when 7 .. 9 | 99 =>...; -- Traitement si Reponse est entre 7
-- et 9, ou 99
when others =>...; -- 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 sous-
programme 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 126

5.5 POINTS À RELEVER

5.5.1 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 6

SOUS-
TYPES,
TYPES
NUMÉRIQ
128 SOUS-TYPES, TYPES NUMÉRIQUES, EXCEPTIONS
SOUS-TYPES 129

6.1 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

• 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

• 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’in-


tervalle 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 134

6.2 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;

• 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 T_Octet_Signe is range –128 .. 127;


type T_Indice_Signe is range 0 .. 1000;
type T_Euros is range –1E12 .. 1E12;
type 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 );

• 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


• 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 );

• 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;

• 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’utili-
sation 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 );

• 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;

• 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 139

6.2.7 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écla-
ration 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; -- Un type entier signe
Nombre : T_Octet; -- Deux variables de ce type
Temp : T_Octet;
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 140

6.3 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 si Fortune de valeur 1;
Fortune := Fortune / 0;division par 0;
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; -- 0 n'appartient pas au sous-type
Positive
...
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 ); -- –3 n'appartient pas au sous-type
Natural
...
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 sous-
type 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’ex-
ploitation 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 traite-
exception 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 ); -- 1
Skip_Line;
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 instruc-
tions 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; -- Exposant jusqu'auquel seront
-- calculees les puissances
begin -- Exposant_Maximum
EXCEPTIONS 147

loop -- Boucler si Constraint_Error ou une autre


Put ( "Exposant maximum: " ); -- exception levee
begin -- Bloc sans partie declarative
Get ( Exposant ); -- Instructions du bloc
Skip_Line;
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’utilisa-


teur 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
when others => null; 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.
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
I + 10 / T_Sous_Entier(I)
M - 10 + T_Millier(I + 10) * (I + 1)
100 - Integer(M) * T_Sous_Entier'Last

6.4.2 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 150

6.5 POINTS À RELEVER

6.5.1 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 7

ARTICLES
SANS
153 ARTICLES SANS DISCRIMINANTS
MOTIVATION 154

7.1 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 155

7.2 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; -- § 5.2.2
Annee : Natural;
end record;
type T_Complexe is -- Pour traiter des nombres
complexes
record
Partie_Reelle : Float; -- Champ sans valeur initiale
Partie_Imaginaire : Float := 0.0; -- Champ avec valeur
end record; -- initiale
Noel : constant T_Date := ...; -- Pour le 25 decembre et pour
Paques : constant T_Date := ...; -- Paques; voir § 7.2.2 pour
la
-- valeur de ces constantes
Nombre : T_Complexe; -- Pour representer un nombre
complexe
-- 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;

• 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 éven-
tuellement 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
=> expression
de champ

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’en-
tré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 160

7.3 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); -- Une constante et
Nombre_1 : T_Complexe; -- deux variables de
Nombre_2 : T_Complexe; -- 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 divi-
sion de nombres complexes en utilisant le type T_Complexe (§ 7.2.1).
POINTS À RELEVER 162

7.5 POINTS À RELEVER

7.5.1 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 8

TABLE
164 TABLEAUX
MOTIVATION 165

8.1 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 inter-
valles (§ 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;

• 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); -- Un vecteur a deux composantes
Vecteur_3 : T_Vecteur (–1..1); -- 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;

• 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);

• 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 ta-


bleaux, 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

intervalle

others => expression

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 carree 2 x 2
( 1 => (0.0, 0.0), 2 => (1.0, 1.0) ) -- Matrice carree 2 x 2
( 1..3 => (0.0, 0.0) ) -- Matrice 3 x 2
( 1..3 => (1..2 => 0.0) ) -- Matrice 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) -- Vecteur a 4 composantes
T_Vecteur_2'(0.0, 0.0) -- Vecteur a 2 composantes
T_Vecteur_2'( others => 0.0) -- 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 sous-
programme) 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 lui-
mê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 sup-
plé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’ex-
ception Constraint_Error sera levée (note 8.2). Par contre, les bornes corres-
pondantes 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.

Vecteur_2 := (1.0, 2.0); 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 177

8.3 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)

• 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; -- Deux variables auxiliaires
Code : Natural;
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 179

8.4 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 de Vecteur, 0 et 100;
T_Vecteur ( Vecteur(20..30) ) bornes de la tranche, 20 et 30;
T_Vecteur_10 ( Vecteur(20..29) ) bornes de T_Vecteur_10, 1 et 10;
T_Autre_Vecteur ( Vecteur_Test ) 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 180

8.5 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, respec-
tivement 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; -- Une constante et une variable
Norme_Vecteur : Float; -- 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’ex-
pression 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 vraie;


String'('a','b') > ('a','b','c','d')expression fausse;
(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.

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 183

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).
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 186

8.7 POINTS À RELEVER

8.7.1 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’ex-
ception 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éra-
tions 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 9

CHAÎNES
DE
188 CHAÎNES DE CARACTÈRES
MOTIVATION 189

9.1 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 190

9.2 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 Put ( Item : in String );
procedure Put_Line ( Item : in String );
procedure Get ( Item : out String );
procedure Get_Line ( 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 193

Skip_Line; -- 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 194

9.3 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 sous-
chaî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) ); -- Affiche 2
Put ( Index (Texte, Tex) ); -- Affiche 24
Put ( Index (Texte, "Tex") ); -- 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 permet-
tent pas. Dans de tels cas des exceptions particulières seront levées [ARM A.4.1].
196

9.4 EXERCICES

9.4.1 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 197

9.5 POINTS À RELEVER

9.5.1 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 1 0

PAQUETA
GES
SIMPLES
ET
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 utilisa-
bles 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 paque-
tage 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 probable-
ment 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 con-
cepteur 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
with Ada.Text_IO;
package Ada.Text_IO is procedure Main is
... -- Ada.Text_IO, ou
end Ada.Text_IO; -- plus exactement
-- sa specification,
Corps de Ada.Text_IO -- est utilisable
package body Ada.Text_IO is -- dans Main
... ...
end Ada.Text_IO; 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 nou-


veauté 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 com-
plè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éno-
minateur 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 -- Diviseur positif
return ( X.Numerateur * Y.Denominateur,
X.Denominateur * Y.Numerateur );
elsif Y.Numerateur < 0 then -- Changer de signe
return ( – 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 (exem-


ples 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 tou-
jours 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’am-
biguï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’iden-
tificateurs. 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’uti-
lisation 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 sont-
ils 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 compi-
lateur des parties de code compilables pour elles-mêmes appelées unités de com-
pilation (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 parti-
culier 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) -- Pas de point-virgule


ici!
function Puissance ( 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 fonctions-
opé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 ration-
nels, 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 puis-
qu’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; -- Pour les multiples
Multiple_Y : Positive := Y;
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 -- Diviseur positif
return Irreductible(( 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 recom-
pilation 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 carac-
té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 fonc-
tions-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 parent Paquetage enfant


package Parent is package Parent.Enfant is
... ...
end Parent; end Parent.Enfant;

package body Parent is package body Parent.Enfant is


... ...
end Parent; 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’uti-
lisation 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:
• 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.

10.9.2 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 para-
graphe 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 1 1

ARTICLES
À
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 partiel-
lement 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 point-
virgule.
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 for-
me 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 im-
plé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 à discrimi-
nants (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);

• 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; -- Un polynome de degre 2
Polynome_Degre_5 : T_Polynome_5; -- Un polynome de degre 5

11.2.3 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 corres-
pondants, 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));
Afficher ( Deux_Plus_X ); -- Affichage de deux polynomes
Afficher ( Polynome_2 );
Polynome_2 := Polynome_1; -- Provoque Constraint_Error
... -- (discriminants differents)

11.2.5 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 241

P := ( Degre_Polynome,
P.Coefficients (T_Degre'First..Degre_Polynome) );
end if;
end Normaliser;
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 par-
ticuliers 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; -- Position de la fenetre
Ordonnee : Natural;
Longueur : Natural; -- 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 dis-
criminant 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 glo-
balement.

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 245

à la fin de la branche qui l’englobe.


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 1 2

FICHIER
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, exten-
sion... 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 pro-
gramme 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 carac-
tè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; -- Pour eviter de prefixer le type
File_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 : in out File_Type;
Mode : in File_Mode := Out_File;
Name : in String := "";
Form : in String := "" );
procedure Open ( File : in out File_Type;
Mode : in File_Mode;
Name : in String;
Form : in 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’utili-
sation 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ées-
sorties 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; -- Une variable caractere
Nb_Entier : Integer; -- Une variable entiere
Nb_Reel : Float; -- 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' ); -- Ecriture d'un caractere
Put ( Fichier_Traite, 123 ); -- Ecriture d'un entier
Put ( Fichier_Traite, 'N' ); -- Ecriture d'un caractere
FICHIERS TEXTE 255

New_Line ( Fichier_Traite ); -- Passage a la ligne


Put ( Fichier_Traite, 3.14 ); -- Ecriture d'un reel
New_Line ( Fichier_Traite ); -- 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 ); -- Lecture d'un caractere
-- (O)
Get ( Fichier_Traite, Nb_Entier ); -- Lecture d'un entier
-- (123)
Get ( Fichier_Traite, Lettre ); -- Lecture d'un caractere
-- (N)
Skip_Line ( Fichier_Traite ); -- Passage a la ligne
Get ( Fichier_Traite, Nb_Reel ); -- Lecture d'un reel (3.14)
Skip_Line ( Fichier_Traite ); -- 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; -- Une variable fichier texte
Lettre : Character; -- 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 ); -- Passage a la ligne
end loop;
FICHIERS TEXTE 257

Close ( Fichier_Traite ); -- 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 (<sp> représente un espace).

T i t r e O<sp><sp><sp><sp><sp><sp><sp><sp>1 2 3 N<sp>3 . 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 ( Item: out Character );
procedure Get_Immediate ( 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 dis-
ponible. 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 essen-
tiellement 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 per-
mettent 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 dispo-
ser 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; -- Pas de use sur un paquetage qui


with Ada.Direct_IO; -- est generique (sect. 17.2)
-- ...
procedure Exemple_12_4 is
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 ad-
jonction 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 : in out File_Type;
Mode : in File_Mode := ...;
Name : in String := "";
Form : in String := "" );
procedure Open ( File : in out File_Type;
Mode : in File_Mode;
Name : in String;
Form : in 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 := (1, Avril, 1958);
Write ( Fichier_Dates, Date ); -- Ecriture de quelques dates
Write ( Fichier_Dates, (26, Decembre, 1964) );
Write ( Fichier_Dates, (7, Septembre, 1991) );
Write ( 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 ( Fichier_Dates, Date ); -- Lecture des quatre dates
Read ( Fichier_Dates, Date ); -- dans l'ordre selon lequel
Read ( Fichier_Dates, Date ); -- elles viennent d'etre
Read ( Fichier_Dates, Date ); -- ecrites
Close ( Fichier_Dates ); -- 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; -- Deux variables fichiers binaires
Copie : File_Type; -- sequentiels
Date : T_Date; -- Une variable pour la copie d'un
element
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 ); -- Fermeture des fichiers
Close ( Copie );
...

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 sous-
type Positive_Count construit sur le type de base Count. Toutes ces déclara-
tions 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 265

12.4.6 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; -- Une variable pour la copie d'un
element
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 ); -- Lecture d'une date
Write ( Copie, Date ); -- Ecriture d'une date
end loop;
Close ( Original ); -- Fermeture des fichiers
Close ( Copie );
...
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 Compléments sur l’utilisation des fichiers binaires à accès


direct
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 vice-
versa;
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 1 3

CRÉATIO
N ET
TRAITEM
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’initia-
lisation de paquetage, d’une tâche, d’un corps d’entrée ou d’une instruc-
tion accept; ces trois derniers cas sont mentionnés à titre indicatif puis-
que 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’identifi-
cateur.
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;

• 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 -- Diviseur positif
return Irreductible( X.Numerateur * Y.Denominateur,
X.Denominateur * Y.Numerateur );
elsif Y.Numerateur < 0 then -- Changer de signe
return Irreductible( X.Numerateur * (–
Y.Denominateur),
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 expli-
quer 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. Faut-
il 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. Faut-
il 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 1 4

STRUCTU
RES
LINÉAIR
ES
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ésen-
ter 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 288

14.2.2 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 289

utilisée pour réaliser des solutions de gestion de queues et piles.


LISTES STATIQUES 290

14.3 LISTES STATIQUES

Une liste statique est donc une structure de données statique (puisque sa lon-
gueur 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 -- Pour une liste statique
record
Contenu : T_Contenu; -- Contenu de la liste
-- Longueur de la liste
Longueur : T_Longueur := T_Longueur'First;
Tete : T_Numerotation; -- Tete et Queue de la liste
Queue : T_Numerotation;
end record;
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 -- Pour une queue statique
record
-- 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 -- Pour une queue statique
record
-- 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; -- Levee si la queue est vide
Queue_Pleine : exception; -- 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 6 ... Longueur_Max

Info 1 Info 2 Info 3 Info 4 Info 5

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 ... Longueur_Max

Info 1 Info 2 Info 3 Info 4 Info 5 Info 6 ... ... ... ... ... Info 100

ine
e_ Ple Longueur = Longueur_Max
Q ueu
Tete 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éces-
site 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 ... Longueur_Max

Info 4 Info 5 Info 6 ... Info 100

Longueur = Longueur_Max - 3

Tete Queue

Queue statique avant l’insertion mais après déplacement

Contenu
1 2 3 4 5 6 ... Longueur_Max

Info 4 Info 5 Info 6 ... Info 100

Longueur = Longueur_Max - 3

Tete Queue

Queue statique après l’insertion

Contenu
1 2 3 4 5 6 ... Longueur_Max

Info 4 Info 5 Info 6 ... 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 ... Longueur_Max

Info 3 Info 4 Info 5

Longueur = 3

Tete Queue

Figure 14.6 Tentative de suppression alors que la queue est vide.

Queue statique

Contenu
1 2 3 4 5 6 ... Longueur_Max

de
e _ Vi
Q ueu 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 modi-
fication 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 6 ... Longueur_Max

Info 3 Info 4 Info 5

Longueur = 3
Information à modifier

Tete Queue

Figure 14.8 Tentative de modification alors que la queue est vide.

Queue statique

Contenu
1 2 3 4 5 6 ... Longueur_Max

ide
eu e_V Longueur = 0
Qu

Tete Queue
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 informa-
tions 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 essentiel-
lement 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; -- Pour simplifier
Contenance : Float; -- En litres
Numero : Integer; -- Numero du ticket correspondant
end record;

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éci-
fication 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; -- Levee si la pile est vide
Pile_Pleine : exception; -- 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 para-
graphes 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 6 ... Longueur_Max

Info 1 Info 2 Info 3 Info 4 Info 5

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 2 3 4 5 6 ... Longueur_Max

Info 1 Info 2 Info 3 Info 4 Info 5 Info 6 ... ... ... ... ... Info 100

ne
lei Longueur = Longueur_Max
_P
P ile

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 4 5 6 ... Longueur_Max

Info 1 Info 2 Info 3

Longueur = 3

Sommet
PILES STATIQUES 309

Figure 14.13 Tentative de suppression alors que la pile est vide.

Pile statique

Contenu
1 2 3 4 5 6 ... Longueur_Max

id e
e_V
Pil 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 4 5 6 ... Longueur_Max

Info 1 Info 2 Info 3

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 6 ... Longueur_Max

id e
e_V
Pil 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 312

14.5.8 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 } bloc de P1

emplacement de B

emplacement de A
} 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) vaut-
elle 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éra-
tions 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 1 5

TYPES
318 TYPES ACCÈS
MOTIVATION 319

15.1 MOTIVATION

Les pointeurs sont difficiles à appréhender, en particulier pour les program-


meurs 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 program-
me 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 com-
pte les possibilités de restitution, de libération de mémoire (memory deallocation)
lorsque certaines portions deviennent inutilisées. Cette restitution peut être auto-
matique, 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; -- Un type pointeur sur le
-- type pointe Integer
type T_Pt_Float is access Float; -- Un type pointeur sur le
-- type pointe Float
type T_Pt_Tableau is access T_Tableau;-- Un type pointeur sur le
-- type pointe T_Tableau
type T_Pt_Article is access T_Article;-- 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 poin-
teur 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 Integerune variable pointée du type Integer est créée;


new Floatune variable pointée du type Float est créée;
new T_Tableauune variable pointée du type T_Tableau est créée;
new T_Articleune variable pointée du type T_Article est créée;
new 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

? ? ? ? ? ? ? ? ?

Pt_Article

0 0 0 0 0 0 0 0 0

15.2.5 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 -- 9
Put ( Pt_Article.Tab (I) );
end loop;
...

Afin de bien comprendre ce mécanisme d’accès, l’effet des différentes ins-


tructions 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

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

Instruction 5:
Pt_Tableau

5 0 0 0 1 2 1 2 1

Instruction 6: affichage successif des valeurs 5 0 0 0 1 2 1 2 1

Instruction 7:
Pt_Article

10

0 0 0 0 0 0 0 0 0

Instruction 8:
Pt_Article

10

5 0 0 0 1 2 1 2 1

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; -- 7,erreur de type
Pt_Float.all := 0.0; -- 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

Autre_Pt_Integer

Pt_Float

Instruction 2:
Pt_Integer

Autre_Pt_Integer

Instruction 3:
Pt_Integer

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 Variables pointées


Ligne 1: les deux déclarations
Pt_Integer

Autre_Pt_Integer

Instruction 2:
Pt_Integer variable pointée devenue inaccessible

5
Autre_Pt_Integer
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’al-
location 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 pos-
sible 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 ma-
niè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’in-
formation 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 -- 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_Liste_Dynamique is -- Pour une liste dynamique
record
Tete : T_Lien; -- Tete et queue de la liste
Queue : T_Lien;
end record;

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 -- Pour une variable dynamique
record
Information : T_Info; -- L'information, sur un seul champ
LISTES DYNAMIQUES 335

Suivant : T_Lien; -- Pour reperer l'element qui suit


end record;
type T_Liste_Dynamique is -- Pour une liste dynamique
record
Tete : T_Lien; -- Tete et queue de la liste
Queue : T_Lien;
end record;

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 évidem-
ment 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; -- 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;

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; -- 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;
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 ( La_Queue : in out


T_Queue_Dynamique;
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 Information 1 Information 2 Information 3

Queue
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; -- 2
else -- 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 Information 1

Queue
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

Queue

Pt_Nouveau

Instruction 4:

Tete Information 1 Information 2

Queue

Pt_Nouveau

Ligne 5:

Tete Information 1 Information 2

Queue
QUEUES DYNAMIQUES 342

15.5.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. 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
_ Vi
Queue
u eue
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 inacces-
sibles, 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

Queue

Instruction 2:

Tete Information 1 Information 2

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 Information 1 Information 2

Queue
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
u eue
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; -- 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_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 -- Pour une variable dynamique
record
Information : T_Info; -- L'information sur un seul
-- champ
Suivant : T_Lien; -- Pour reperer l'element qui
-- suit
PILES DYNAMIQUES 351

end record;
type T_Pile_Dynamique is -- Pour une pile dynamique
record
Sommet : T_Lien; -- 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 infor-
mation 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

Sommet
de
_ Vi
P 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

Instruction 2:

Sommet Information 2 Information 1

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 357

15.6.5 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 mo-
dification 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’ex-
emple 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

Sommet
de
_ Vi
P 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 359

15.6.8 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 361

-- 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;
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; -- Pas de use


-- ...
procedure Exemple_15_31 is
type T_Element is ...; -- Un type quelconque
type T_Pt_Element is access T_Element; -- 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 ...; -- Un type quelconque
type T_Pt_Element is access T_Element; -- 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 366

Pt_Gen_String (1) := 'b'; -- 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 indi-
rectement 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 sous-
programmes é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 à dispo-
sition 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 1 6

TYPES
PRIVÉS,
TYPES
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 chan-
gement dans la structure même du type pourrait demander de nombreuses modifi-
cations 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 per-
mettre 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-pro-
gramme 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 376

Zero : constant T_Rationnel; -- 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 sous-
type 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 struc-
ture 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 utili-
satrices 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; -- § 16.2.1


use type Nombres_Rationnels.T_Rationnel; -- § 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; -- Longueur de la queue
Tete : Positive := 1; -- Tete de la queue
Queue : Positive := 1; -- Queue de la queue
end record;
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 382

16.5.2 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 package Parent.Enfant is
... ...
private private
... ...
end Parent; end Parent.Enfant;

package body Parent is package body Parent.Enfant is


... ...
end Parent; 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 body Parent is


... ...
package Parent.Enfant is ...
... ...
...
private package body Parent.Enfant is
... ...
private ...
... ...
end Parent.Enfant; end Parent.Enfant;
end Parent; 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 package Parent.Enfant is
... ...
private private
... ...
end Parent; end Parent.Enfant;

package body Parent is package body Parent.Enfant is


... ...
end Parent; 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 package body Parent is


... ...
private ...
... ...
private package package body Parent.Enfant is
Parent.Enfant is ...
... ...
private ...
... ...
end Parent.Enfant; end Parent.Enfant;
end Parent; 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 rap-
port à 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 Spécification de
Nombres_Rationnels Nombres_Rationnels.Utilitaires
type T_Rationnel
valeur absolue,
zero, puissance,
addition, multiplication par un
construction, nombre entier,
soustraction, division par un nombre
entier
multiplication,
division,
Spécification de
comparaisons,
Nombres_Rationnels.Operations_Internes
numerateur,
denominateur P_G_C_D,
P_P_M_C,
Irreductible
Corps de
Nombres_Rationnels Corps de
addition, Nombres_Rationnels.Utilitaires
construction, valeur absolue,
soustraction, puissance,
multiplication, multiplication par un
division, nombre entier,
comparaisons, division par un nombre
numerateur, entier
denominateur
Corps de
Nombres_Rationnels.Operations_Internes

P_G_C_D,
P_P_M_C,
visibilité directe Irreductible
visibilité grâce à une clause de contexte

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 389

enfant privé Nombres_Rationnels.Operations_Internes.


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; -- Pour les soustractions
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 – 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’uni-


té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 1 7

BASES
DES
UNITÉS
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épen-
dait de l’utilisation faite d’un paquetage (§ 15.5.2) ou d’une procédure de traite-
ment (§ 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’utilisa-
tion 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 com-
pilation ou à l’exécution dont les paramètres sont spécifiés par les unités utili-
satrices. 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éri-
ques 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 paqueta-
ges 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’in-
té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éces-
saires. 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 pa-
quetage 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 <>; -- Pour un type entier signe
type T_Modulaire is mod <>; -- Pour un type entier non signe
-- (modulaire)
type T_Flottant is digits <>; -- Pour un type reel point-flottant
type T_Fixe is delta <>; -- Pour un type 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 sous-
programmes. 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 T_Pile

Construire Créer
Additionner Effacer
Soustraire Empiler
Multiplier Désempiler
Diviser Sommet
Numérateur Vide
Denominateur

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

Créer
Effacer
Constructeurs Empiler
Désempiler
Copier

Sommet
Sélecteurs 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 poin-
teur 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 1 8

UNITÉS
GÉNÉRIQ
UES:
NOTIONS
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 (<>); -- Les indices sont d'un type
discret
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 sem-
blables. 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 quel-
conque 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 -- Pour une queue statique
record
-- Contenu de la queue
Contenu : T_Contenu;
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’indis-
pensable 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 men-
tionner 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 dis-
criminant 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 intro-
duisant 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éma-
nentes (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; -- Tete de la queue
Queue : Positive := 1; -- 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 sous-


types 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 sous-
programmes 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 sous-
programme 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ées-
sorties 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 ("Numerateur: ");
Get ( X.Numerateur );
Put ("Denominateur: ");
Get ( 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 -- Pour une pile dynamique
record
Sommet : T_Lien; -- Sommet de la pile
end record;
end A_D_T_Piles_Dynamiques_G;
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 1 9

SURNOMS,
TYPES
DÉRIVÉS,
ANNEXES
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 sous-
type 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 syn-
taxiques de la figure 19.1.

Figure 19.1 Diagrammes syntaxiques de surnommage de sous-programmes.

Surnommage de procédure

identificateur
procedure liste de paramètres renames id de procédure
de surnom

Surnommage de fonction ou de fonction-opérateur

surnom de fonction return id de type ou


function liste de paramètres
ou d’opérateur sous-type

nom de fonction
renames
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 sous-
programme 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 T_Euro is new Integer; -- Integer est le type parent


type T_Franc is new Integer range –1E12 .. 1E12;
type T_Positif is new Positive;
type 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 rai-
sons 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 sim-
plement 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’ac-
cé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éma-


tiques π 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’im-
plé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
• 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).

19.5.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.