Professional Documents
Culture Documents
C H A P I T R E 1
NOTIONS
DE BASE
DÉFINITIONS GÉNÉRALES 2
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.
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.
1961 Cobol Applications Langage verbeux, peu maniable, encore très utilisé
commerciales en informatique de gestion.
1965 Basic «Travail à la mai- Basique, simple d’utilisation mais peu adapté à la
son» programmation structurée.
Domaine
Année Langage Remarques
d’application
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.
1996 Java Internet Semblable à C++, mais plus sûr; permet la program-
mation d’applications classiques mais aussi
d’applets.
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.
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
(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.
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.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.
identificateurs et symboles
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.).
chiffre
0 1 2 3 4 5 6 7 8 9
identificateur _
lettre
chiffre
lettre
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.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
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
Numéral
chiffre
chiffre
Exposant positif
E
Numéral
e
+
Numéral
Exposant positif
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).
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).
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.
-- ...
procedure Exemple_2_3 is
TYPES ENTIERS 32
-- ...
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).
Le programmeur qui utilise la valeur d’une variable, de quelque type que ce soit, doit être certain que
TYPES ENTIERS 33
Exemple 2.6 Valeurs possibles pour les nombres les plus petits et les plus grands des types entiers.
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.
Numéral . Numéral
Exposant
Exposant
-
E
Numéral
e
+
TYPES RÉELS 36
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;
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.
La conversion d’une valeur réelle en une valeur entière est faite par arrondi vers
l’entier le plus proche.
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.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.
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;
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);
L’affichage d’un message sur l’écran, pour l’utilisateur, est maintenant facile:
Ada.Text_IO.Put ( "Donnez les dimensions d’une brique " );
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.7 EXERCICES
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.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.
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
où
• elsif est un nouveau mot réservé;
• la suite_d_instructions_i est exécutée pour la première
condition_i vraie dans l’ordre d’écriture et le reste de l’instruction if
est sauté; si toutes ces
conditions 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.1 Généralités
Boolean est un identificateur prédéfini. Il signifie «un objet (constante,
variable, expression...) de type Boolean aura la valeur vrai ou faux et rien
d’autre». Le type Boolean fait partie des types discrets (discrete types, [ARM
3.2]).
Les constantes booléennes sont False et True, qui sont aussi des
identificateurs prédéfinis et qui représentent respectivement les valeurs faux et
vrai.
Les opérations possibles sur les valeurs de type Boolean sont:
and or xor not = /= <= < >= >
où
• and représente la fonction logique et;
• or représente la fonction logique ou (non exclusif);
• xor représente la fonction logique ou exclusif;
• not représente la fonction logique non;
• comme déjà mentionné (§ 3.1.2) = /= <= < >= et > représentent les
opérateurs de comparaison.
Ces quatre nouveaux opérateurs sont appelés opérateurs logiques (ou
opérateurs booléens). Les opérateurs de comparaison possèdent un sens bien défini
car, par définition, le type Boolean est ordonné, avec False < True.
Present : Boolean;
Nombre : Integer;déclaration d’une variable entière;
Trouve and Presentexpression vraie si Trouve et Present sont vrais;
not Trouveexpression vraie si Trouve est faux;
Trouve or Presentexpression vraie si Trouve ou Present est vrai;
not (Trouve and Present)expression vraie si Trouve ou Present est
faux;
not Trouve or not Presentexpression équivalente à la précédente;
Nombre = 0 or Nombre = 1expression vraie si Nombre vaut 0 ou 1;
Trouve and Nombre = 0expression vraie si Trouve est vraie et si Nombre
vaut 0.
A titre de synthèse, tous les opérateurs existant en Ada sont donnés dans le
tableau 3.1, par ordre décroissant de priorité.
Tableau 3.1 Opérateurs dans l’ordre décroissant de priorité.
3.4.4 Affectation
L’affectation s’effectue comme dans l’exemple 3.4.
-- ...
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
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 );
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.
3.5.2 Généralités
La réalisation d’itérations se fait en utilisant des instructions appelées
communément boucles (il y a trois boucles différentes en Ada). Le fait que le
nombre d’itérations à effectuer soit calculable avant l’arrivée dans la boucle
conduit à choisir l’instruction for. Cette instruction s’écrit:
for I in 1..10 loop
-- dessiner le triangle
end loop;
où
• I est une variable entière déclarée automatiquement.
Cette instruction for va s’exécuter de la manière suivante: la variable de boucle
I va prendre successivement les valeurs 1, 2, 3, ..., 9 et 10 et, pour chacune de ces
valeurs, l’action dessiner le triangle qui suit le mot réservé loop va s’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.
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;
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.
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.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.
La suite d’instructions comprise entre loop et end loop est répétée tant que la
condition Nombre_Lu /= Nombre_Final est vraie, i.e. tant que Nombre_Lu est
différent de 0. Le style de codage de cet exemple peut cependant être amélioré
comme dans l’exemple 3.10.
La forme générale de l’instruction while est:
while expression_de_type_Boolean loop
suite_d_instructions;
ITÉRATION: LA BOUCLE WHILE 68
end loop;
où
• la suite_d_instructions contenue dans la boucle est exécutée tant que
l’expression_de_type_Boolean est vraie.
3.6.2 Précisions sur l’utilisation de la boucle while
Toutes les variables appartenant à l’expression booléenne doivent avoir une
valeur bien définie (note 2.3).
Il faut s’assurer que l’expression booléenne devient fausse après un nombre fini
d’itérations, faute de quoi le programme exécuterait l’instruction while
indéfiniment.
Si l’expression booléenne est initialement fausse, l’instruction while n’est pas
effectuée.
ITÉRATION: LA BOUCLE GÉNÉRALE LOOP 69
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;
...
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.
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.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.
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.
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
Exemple 3.13 Utilisation des attributs First, Last, Succ, Pred, Pos et Val.
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.
-- 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
Expression qualifiée
3.11 EXERCICES
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
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)?
(10,10) (110,10)
(10,110) (110,110)
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.
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.
En-tête de procédure
Liste de paramètres
in
identificateur identificateur
( : de type ou )
de paramètre
de sous-type
out
, in out := expression
access
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;
Dessiner_Carre ( 50 );
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
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.
Majusculiser ( Lettre);
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
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.
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).
-- 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
interrogation.
Tableau 1.2 Portée et visibilité des identificateurs.
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
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).
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
En-tête de fonction
identificateur
function identificateur liste de paramètres return
de type
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;
-- 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
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.
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
4.11 EXERCICES
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
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.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.
Charac- tous les caractères compris entre les affectation, comparaison, attributs,
ter deux bornes Character'First etc.
et Character'Last
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).
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.
-- 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
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.
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
5.2.3 Affectation
L’affectation se fait de manière habituelle, comme dans l’exemple 5.4.
-- ...
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
...
Exemple 5.5 Utilisation des attributs First, Last, Succ, Pred, Pos et Val.
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 );
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.
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.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.4 EXERCICES
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.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
où
• borne_inf et borne_sup sont des expressions d’un type de base discret;
• identificateur_de_type_ou_sous_type d’un type de base égale-
ment discret ;
• attribut_range sera introduit plus loin (§ 8.2.7).
Contrainte
d’intervalle
Exemple 6.4 Attribution de valeurs à des objets dont la déclaration comporte une contrainte.
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.
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.
Ces types sont dits signés car la suite de bits implémentant toute valeur d’un tel
type verra l’un de ses bits (souvent celui de poids le plus fort) interprété comme bit
de signe.
TYPES NUMÉRIQUES 135
Les opérations possibles applicables à de tels types sont les mêmes que pour le
type prédéfini Integer (§ 2.2.2) avec la particularité que le second opérande de
l’opérateur arithmétique ** doit toujours être du sous-type Natural.
L’affectation, le passage en paramètre et l’utilisation d’attributs s’effectuent
également comme pour Integer. Pour réaliser des entrées-sorties, il faut avoir
déclaré
package ES_Entiers_Signes is new
Ada.Text_IO.Integer_IO ( Id_Type_Entier_Signe );
où
• ES_Entiers_Signes est le nom du paquetage d’entrées-sorties créé,
nom choisi par le programmeur;
• Id_Type_Entier_Signe est l’identificateur du type entier signé.
Une fois cette déclaration effectuée et de manière analogue aux types énumé-
ratifs (§ 5.2.5), il est alors possible d’utiliser Get et Put pour lire et écrire des
valeurs du type Id_Type_Entier_Signe. Le préfixe ES_Entiers_Signes doit
être mentionné, sauf si une clause use ES_Entiers_Signes suit cette ligne de
déclaration.
Le lecteur malin aura réalisé que le nom développé Ada.Integer_Text_IO
(sect. 2.6) vient d’une déclaration prédéfinie qui s’écrit
package Ada.Integer_Text_IO is new
Ada.Text_IO.Integer_IO ( Integer );
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).
où
• le_modulo est une expression entière positive statique (sect. 3.10), souvent
une puissance de 2; par définition, l’intervalle des valeurs du type est
toujours 0 .. le_modulo – 1 (exemple 6.8).
Ces types sont dits non signés car la suite de bits implémentant toute valeur
d’un tel type n’aura aucun bit interprété comme bit de signe.
Les opérations applicables à de tels types sont les mêmes que pour le type
prédéfini Integer (§ 2.2.2) avec la particularité que le second opérande de
l’opérateur arithmétique ** doit toujours être du sous-type Natural.
La principale caractéristique de ces types est qu’il ne peut jamais se produire de
débordement de capacité car toute l’arithmétique est effectuée modulo
Le_Modulo. De plus, comme c’est le cas pour l’arithmétique d’un processeur, les
opérateurs and, or, xor et not sont applicables à des opérandes d’un type modulo,
considérés alors comme des suites de bits.
L’affectation, le passage en paramètre et l’utilisation d’attributs s’effectuent
également comme pour Integer. Pour réaliser des entrées-sorties, il faut avoir
déclaré
package ES_Entiers_Non_Signes is new
Ada.Text_IO.Modular_IO ( Id_Type_Entier_Non_Signe );
où
• ES_Entiers_Non_Signes est le nom du paquetage d’entrées-sorties
créé, nom choisi par le programmeur;
• Id_Type_Entier_Non_Signe est l’identificateur du type entier non
signé.
De manière analogue aux types énumératifs (§ 5.2.5), il est alors possible
d’utiliser Get et Put pour lire et pour écrire des valeurs d’un type comme
Id_Type_Entier_Non_Signe. Le préfixe ES_Entiers_Non_Signes doit être
mentionné, sauf si cette ligne de déclaration est suivie d’une clause use
ES_Entiers_Non_Signes.
Mais comme mentionné auparavant (§ 2.3.5) d’autres types tels que Short_Float
ou Long_Float peuvent également être disponibles. Encore une fois tous sont
dépendants de la machine utilisée. Pour renforcer la portabilité d’une application
mais aussi pour assurer la précision souhaitée dans les applications numériques
(calcul scientifique), Ada permet la déclaration de types réels point-flottant
(exemple 6.9) de la manière suivante:
type identificateur is digits nb_chiffres;
où
• nb_chiffres (statique) représente la précision désirée.
Notons tout de suite qu’il est encore possible de contraindre un tel type en
définissant un intervalle statique (§ 6.1.1).
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 );
où
• ES_Reels_Point_Flottant est le nom du paquetage d’entrées-sorties
créé, nom choisi par le programmeur;
• Id_Type_Reel_Point_Flottant est l’identificateur du type réel point-
flottant.
De manière analogue aux types énumératifs (§ 5.2.5), il est alors possible
d’utiliser Get et Put pour lire et pour écrire des valeurs d’un type comme
Id_Type_Reel_Point_Flottant. Le préfixe ES_Reels_Point_Flottant
doit être mentionné, sauf si cette ligne de déclaration est suivie d’une clause use
ES_Reels_Point_Flottant.
La réalisation d’applications numériques demanderait un approfondissement de
l’utilisation des types réels point-flottant, ce qui dépasse l’objectif de ce texte.
Un type réel point-fixe est utile s’il est nécessaire de travailler avec des
nombres réels pour lesquels la différence entre deux nombres consécutifs est
constante (er-reur absolue) comme lors du calcul de durées temporelles ou dans les
applications comptables. Un tel type se déclare de la manière suivante (exemple
6.10):
type identificateur is delta erreur range borne_inf .. borne_sup;
où
• erreur (statique) représente l’erreur tolérée;
• borne_inf et borne_sup sont des expressions réelles statiques.
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.
-- ...
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
...
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).
-- 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
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).
-- ...
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;
-- ...
function Exposant_Maximum return Natural is
Exposant : Natural; -- Exposant jusqu'auquel seront
-- calculees les puissances
begin -- Exposant_Maximum
EXCEPTIONS 147
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
6.4 EXERCICES
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
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
Les types articles font partie des types composés (composite types, [ARM 3.2]).
Seuls les types articles sans discriminants (contraints) seront exposés dans ce
chapitre. Les types articles avec discriminants (non contraints) feront l’objet
ultérieurement d’une présentation particulière (chap. 11).
La forme générale d’un type article sans discriminants est la suivante:
type identificateur is
GÉNÉRALITÉS 156
record
suite_de_champs_1 : identificateur_de_type_ou_sous_type
:= expression_1;
suite_de_champs_2 : identificateur_de_type_ou_sous_type
:= expression_2;
suite_de_champs_3 : identificateur_de_type_ou_sous_type
:= expression_3;
...
suite_de_champs_N : identificateur_de_type_ou_sous_type
:= expression_N;
end record;
où
• record est un nouveau mot réservé;
• identificateur est le nom du type;
• suite_de_champs_i est une liste d’identificateurs séparés par des virgules et
désignant les champs du type article;
• identificateur_de_type_ou_sous_type donne le type du champ et éven-
tuellement une contrainte;
• expression_i est optionnelle et représente la valeur initiale du champ.
Agrégat article
,
( expression )
identificateur
=> expression
de champ
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.
-- ...
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
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.4 EXERCICES
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
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.
-- 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
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;
Agrégat tableau
,
( expression )
intervalle
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.
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
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).
Lors de l’affectation de tableaux, leurs longueurs doivent être identiques faute de quoi l’exception
Constraint_Error sera levée (§ 6.3.2).
tableau Vecteur_2
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
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
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.
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.
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.
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).
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.6 EXERCICES
é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.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
-- ...
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
...
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).
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 );
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’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.4 EXERCICES
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
10.2 GÉNÉRALITÉS
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
nom
Nom
identificateur du 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).
Corps de paquetage
end ;
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).
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;
...
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;
...
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
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).
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.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
Clause separate
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.
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
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.
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
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.
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!
------------------------------------------------------------
end Nombres_Rationnels.Utilitaires;
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.
-- 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
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.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
-- 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
-- ...
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
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.
-- ...
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));
...
P := ( Degre_Polynome,
P.Coefficients (T_Degre'First..Degre_Polynome) );
end if;
end Normaliser;
ARTICLES À PARTIES VARIANTES 242
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
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.5 EXERCICES
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.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.
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
...
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.
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.
Titre
O123N
3.14000E+00
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.
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.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).
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).
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
...
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.
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
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.
Index de dates.dat
Index de copie de dates.dat
Index de dates.dat
Après la première itération:
1 avril 195826 decembre 19647 septembre 1991
28 juillet 1998
28 juillet 1998
Index de dates.dat
Index de copie de dates.dat
12.7 EXERCICES
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
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
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.
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
...
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 ...;
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
13.6 EXERCICES
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.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
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.
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.
------------------------------------------------------------
-- 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
Info 1
Longueur = 1
Tete Queue
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).
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.
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = Longueur_Max - 3
Tete Queue
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = Longueur_Max - 3
Tete Queue
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = Longueur_Max - 2
Tete Queue
Exemple 14.4 Procédure d’insertion d’une information dans une queue statique.
-- 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
Queue statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 3
Tete Queue
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.
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.
Queue statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 3
Information à modifier
Tete Queue
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.
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.
Exemple 14.8 Fonction retournant l’état (vide/non vide) d’une queue statique.
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.
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.
Comme pour la recherche et pour les mêmes raisons, le cas d’une queue vide
ne provoque aucune exception.
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).
type T_Info is
record
Boisson : Integer; -- Pour simplifier
Contenance : Float; -- En litres
Numero : Integer; -- Numero du ticket correspondant
end record;
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.
Pile statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 0
Sommet
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
Pile statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 5
Sommet
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.
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.
Pile statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 3
Sommet
PILES STATIQUES 309
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.
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
Pile statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
Longueur = 3
Information à modifier
Sommet
Lorsque la pile est vide, toute tentative de modification doit lever l’exception
Pile_Vide pour en avertir l’auteur (fig. 14.15).
Pile statique
Contenu
1 2 3 4 5 6 ... Longueur_Max
id e
e_V
Pil Longueur = 0
Sommet
Exemple 14.17 Procédure de modification d’une information dans une pile statique.
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.
Exemple 14.19 Fonction retournant l’état (vide/non vide) d’une pile statique.
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.
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.
Comme pour la recherche et pour les mêmes raisons, le cas d’une pile vide ne
provoque aucune exception.
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
Appel de P2:
place libre dans la pile
sommet de la pile
emplacement de B
emplacement de A
} bloc de P2
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
14.6 EXERCICES
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
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.
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.2 Utilisation de l’allocateur new avec les types de l’exemple 15.1.
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.
Pt_Integer
Pt_Float
10.0
Pt_Tableau
? ? ? ? ? ? ? ? ?
Pt_Article
0 0 0 0 0 0 0 0 0
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).
Instruction 1:
Pt_Integer
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 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
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.
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
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.
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.
Exemple 15.7 Type accès et types tableau non contraint ou article à discriminant.
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).
Tete
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.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.9 Définitions complètes et correctes pour la réalisation d’une liste dynamique.
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.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).
Queue dynamique
Queue
QUEUES DYNAMIQUES 339
Exemple 15.12 Procédure d’insertion d’une information dans une queue dynamique.
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.
Tete
Queue
Pt_Nouveau
Information 1
Instruction 2:
Tete Information 1
Queue
Pt_Nouveau
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).
Tete Information 1
Queue
Pt_Nouveau
Information 2
Instruction 3:
Queue
Pt_Nouveau
Instruction 4:
Queue
Pt_Nouveau
Ligne 5:
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).
Queue dynamique
Tete Information N
Queue
Figure 15.11 Tentative de suppression alors que la queue dynamique est vide.
Queue dynamique
Tete
de
_ Vi
Queue
u eue
Q
Exemple 15.13 Procédure de suppression d’une information dans une queue dynamique.
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;
Figure 15.12 Suppression d’un élement à partir d’une queue dynamique de longueur 2.
Ligne 1:
Queue
Instruction 2:
Queue
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).
Queue dynamique
Information à modifier
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
Exemple 15.14 Procédure de modification d’une information dans une queue dynamique.
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.
Exemple 15.16 Fonction retournant l’état (vide/non vide) d’une queue dynamique.
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.
-- 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;
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.
end Parcourir;
Comme pour la recherche et pour les mêmes raisons, le cas d’une queue vide
ne provoque aucune exception.
Pile dynamique
Sommet
Une pile dynamique peut maintenant être créée par une simple déclaration de
variable de type T_Pile_Dynamique.
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;
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.
Pile dynamique
Exemple 15.22 Procédure d’insertion d’une information dans une pile dynamique.
Figure 15.18 Insertion d’un élément à partir d’une pile dynamique vide.
Sommet
Pt_Nouveau
Information 1
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).
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:
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.
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).
Pile dynamique
Pile dynamique
Sommet
de
_ Vi
P ile
PILES DYNAMIQUES 356
Exemple 15.24 Procédure de suppression d’une information dans une pile dynamique.
Figure 15.22 Suppression d’un élément à partir d’une pile dynamique de longueur 2.
Ligne 1:
Instruction 2:
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.
Pile dynamique
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.
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.
Exemple 15.27 Fonction retournant l’état (vide/non vide) d’une pile dynamique.
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.
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
Comme pour la recherche et pour les mêmes raisons, le cas d’une pile vide ne
provoque aucune exception.
Exemple 15.30 Inversion des éléments d’une suite grâce à une pile dynamique.
...
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
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.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
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.
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.
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.
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.
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.
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é.
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
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.
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é.
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.
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
-- ...
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.10 EXERCICES
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
generic
Paramètres génériques
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
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,
... );
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é.
-- ...
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
------------------------------------------------------------
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
T_Nombre_Rationnel T_Pile
Construire Créer
Additionner Effacer
Soustraire Empiler
Multiplier Désempiler
Diviser Sommet
Numérateur Vide
Denominateur
Créer
Effacer
Constructeurs Empiler
Désempiler
Copier
Sommet
Sélecteurs Vide
Cardinalité
Itérateur Parcourir
Exemple 17.9 Spécification de la réalisation d’un type de données abstrait de pile dynamique.
17.8 EXERCICES
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.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
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.
-- ...
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.
-- 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);
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
Exemple 18.4 Types privés comme paramètre générique et comme type exporté.
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.7 Traitement des informations contenues dans une queue statique générique.
Exemple 18.8 Traitement des informations contenues dans une queue statique générique.
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).
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
Exemple 18.13 Spécification de la réalisation d’un type de données abstrait de pile dynamique.
18.6 EXERCICES
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
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
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.
Surnommage de procédure
identificateur
procedure liste de paramètres renames id de procédure
de surnom
nom de fonction
renames
ou d’opérateur
SURNOMMAGE 441
-- § 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 "/";
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.
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
Exemple 19.8 Paquetage dont le type privé est réalisé par un type dérivé.
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
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.