Pascal Avancé
Pascal Avancé
LE DEVELOPPEMENT EN COOPERATION
OR8TOM
MAA- Milieux et activités agricoles
UR 3J Organisations régionales
Cours de programmation
avancée en Pascal
Gérard Cochonncau
INTRODUCTION
Ce manuel de cours est la version française d'un manuel en portugais, du même auteur,
utilisé lors de la réalisation d'un cours au Departamento de Informatica de l'EMBRAPA
(Empresa Brasileira de Pesquisa Agropecuaria), du 16 au 27 juillet 1990. Ce cours, basé sur la
version 5.5 do Turbo-Pascal, a été qualifié de "Pascal Avancé" car il s'agit d'un complément à
un cours de Pascal Basique donné précédemment au même endroit.
L'élaboration de ce cours a eu pour but d'améliorer les connaissances pratiques des
informaticiens de l'EMBRAPA impliqués dans le développement et la maintenance de
programmes écrits en Pascal. La majorité des exemples et des exercices sont issus de
situations réelles rencontrées dans le développement des modules de SISGEO. Le dernier
chapitre traite de la Programmation Orientée Objet; il s'agit seulement d'une initiation qui
n'approfondit pas la totalité des techniques de la POO. Enfin, deux annexes rassemblent les
en-têtes des routines disponibles dans les units standards du Turbo-Pascal ainsi que la
signification des différents messages d'erreurs à l'éxécution.
Unités et overlays
1.1 Introduction.
En général, une fonction quelconque d'un système en cours de développement peut être
logiquement divisée en plusieurs sous-fonctions relativement indépendantes qui ont
seulement besoin de partager un nombre minimum d'informations. Prenons pour exemple une
fonction d'émission des données d'un fichier: elle peut être divisée en trois sous-fonctions:
- obtenir la demande de l'utilisateur (critères de sélection des données, choix du
périphérique de sortie, généralement obtenus à l'aide d'un écran de saisie),
- émission des données sur l'écran, si l'utilisateur a choisi ce périphérique,
- émission des données sur l'imprimante.
Les données qui doivent communiquer entre les différentes fonctions étant:
- le type de périphérique qui permettra de choisir entre les deux types d'émission,
- les critères de sélection.
En adaptant ce raisonnement au niveau physique, nous obtenons un programme
principal:
- qui fait d'abord appel à un module pour saisir les critères de sélection et le type de
périphérique,
- qui ensuite, en fonction du type de périphérique, exécute un module d'émission sur
l'écran ou un module d'émission sur l'imprimante.
Une forme adéquate d'implémenter cela en Turbo-Pascal est justement l'utilisation de
trois unités correspondantes utilisées par un programme principal.
Un autre cas souvent rencontré concerne l'utilisation, dans plusieurs fonctions du
système, des mêmes sous-fonctions. Par exemple, le fichier dont les données sont à émettre
dans l'exemple antérieur, devra également être accédé (en lecture et en écriture) par le
programme de mise à jour du fichier.
L'utilisation d'une unité rassemblant les routines en relation avec ce fichier (ouverture,
lecture, écriture, fermeture) constitue la meilleure manière d'implémenter cela en Turbo-
Pascal; cette unité étant ensuite utilisée par plusieurs programmes.
Une unité est un ensemble d'objets du langage Pascal (constantes, variables, types,
procédures, fonctions), de la même manière qu'un programme normal. La structure d'une
unit <identification>
interface
uses <listes d'unités> lfacultatif}
ldéclarations publiques}
implementation
ldéclarations privées}
{implémentation des procédures et fonctions}
begin
{partie d'initialisation}
end.
L'en-tête de l'unité commence obligatoirement par le mot réservé unit suivi du nom de
l'unité, sous une forme semblable au nom d'un programme.
interface
uses crt. dos;
Déclarer ensuite tous les éléments qui doivent être visibles de l'extérieur de l'unité. La
syntaxe est identique à celle utilisée dans un programme normal, en ce qui concerne les
types, les constantes et les variables. Toute procédure ou fonction susceptible d'être appelée
de l'extérieur de l'unité doit également être déclarée: il suffit pour cela de répéter son en-
tête dans la partie d'interface en respectant les mêmes règles syntaxiques que pour l'en-tête
utilisé pour définir une fonction ou une procédure dans un programme normal. La partie
d'implémentation de la routine (variables locales, corps de la routine) doit seulement être
introduite dans la partie d'implémentation de l'unité (voir note b de l'exemple 1.6).
Il s'agit de la dernière partie de l'unité. Elle peut se réduire à la directive end. qui
indique la fin de l'unité (voir note a de l'exemple 1.6). Dans le cas contraire, elle commence
avec la directive begin et ressemble au corps d'un programme principal.
La partie d'initialisation contient des ordres exécutables quelconques et peut faire
référence à tous les objets visibles de l'intérieur de l'unité: objets de la partie d'interface
comme de la partie d'implémentation. Elle sera toujours exécutée, une seule fois, au début de
l'exécution des programmes qui font appel à l'unité.
L'unité fenetres, présentée dans ce chapitre, est utilisée par l'unité VisuTas qui sera
étudiée dans le chapitre II, Elle contient quelques routines utiles à la définitions de fenêtres
sur l'écran. Les chapitres précédent ont fait référence aux notes présentées en commentaires
dans le listing.
I-------·----·--··---·--·····cours de Pascal-Avancé--·----·--·-·--·--·---·-----}
1 }
j exemple d'unité }
j (disponible dans le fichier fenetres.pas) }
1-----------·--·-----------------·---------------------------------------------}
unit fenetres;
type
TypeFen record
Li gne,
Colonne.
Largeur,
Hauteur byte;
Ti tre string;
end;
implementation
begin
with Fen do
begin
window(Colonne, Ligne, Colonne + Largeur, Ligne + Hauteur):
k ;= (Largeur - 2 - length(Titre)) div 2;
Affi cherTexteO, 1. 'r');
AfficherTexte(O, 0, Chaine(k, '-')):
AfficherTexte(O, 0, Titre):
Toutes les unités utilisées par un programme doivent être citées dans une directive uses
située dans la partie de déclarations d'un programme, préalablement à une référence
quelconque à un des éléments de ces unités. Par exemple:
La directive uses doit être unique à l'intérieur d'un programme et doit suivre l'en-tête
facultatif du programme.
Une unité peut utiliser d'autres unités, de la même manière qu'un programme principal.
La directive uses doit être placée immédiatement après la directive interface et ne peut être
utilisée qu'une seule fois dans la partie d'interface d'une unité.
La déclaration, dans la partie d'interface d'une unité a, de l'utilisation d'une unité b qui
utilise l'unité a n'est pas permise. L'exemple suivant provoquerait une erreur 68 Circular unit
reference lors de la compilation:
unit a; unit b;
interface interface
uses b; uses a;
Cette limitation peut être contournée par l'utilisation d'une directive uses dans la partie
d'implémentation. L'unité présentée à suivre est utilisée par l'unité fenetres pour afficher un
message d'erreur. Or, pour afficher le message, la routine AfficherErreur a besoin de la
routine AfficherTexte, elle-même définie dans l'unité fenetres. Il s'agit donc d'un cas de
référence circulaire d'unité qui a été résolu par l'utilisation de la directive uses, tant dans la
partie d'implémentation de fenetres (voir note f de l'exemple 1.6), que dans celle
AflicherErreur (voir plus bas).
Ceci n'est acceptable que si la partie d'interface de l'unité n'a pas besoin d'un élément
de celle déclarée dans la partie d'implémentation, toujours située après la partie d'interface.
I----------------------------cours de Pascal-Avancé----------------------------f
1 f
{ exemple de référence circulaire d'unités f
{ (disponible dans le fichier afferr.pas) f
1----·--------_· __ ·_---------_··_----_· __ ··_------_···_ .. ------.---------- .. ---}
unit afferr;
interface
implementation
uses Fenetres;
end.
Pour chaque unité citée dans la clause uses, le compilateur consulte d'abord les unités
résidantes (celles de Turbo.TpI) ; si l'unité recherchée n'y est pas trouvée, le compilateur
recherche, sur le disque, un fichier d'extension .TPU portant le même nom que l'unité (dans le
répertoire courant ou dans les répertoires signalés par la directive de compilation lU). Si le
fichier n'est pas trouvé sur le disque et, en fonction de la méthode choisie pour la
compilation, le compilateur pourra rechercher le fichier source de l'unité et le compiler avant
de poursuivre la compilation du programme principal.
Au moment du traitement de la clause uses, toutes les informations de la partie
d'interface de chaque unité citée sont mémorisées dans la table des symboles.
Le Turbo-Pascal 5.5 possède huit unités standards, dont quelques unes sont rassemblées
dans la bibliothèque TURDO.TPL, chargée en mémoire au moment de la compilation:
- l'unité SYSTEM qui contient les routines de bas niveau utilisées durant
l'exécution; elle est toujours utilisée par toutes les unités et tous les programmes et ce, sans
qu'il soit nécessaire de la citer dans une clause uses;
- l'unité CRT qui contient toutes les routines pour l'utilisation du clavier et de l'écran
en mode texte:
- l'unité DOS qui contient les routines pour exécuter les fonctions du Dos, avoir accès
à la date et à l'heure du système, etc... ;
- l'unité PRINTER qui permet d'utiliser l'imprimante;
- l'unité OVERLAY qui permet la déclaration et l'utilisation d'overlays; elle sera
détaillée dans le chapitre 1.7 ;
- l'unité GRAPH qui implémente les routines de graphique: elle sera étudiée dans le
chapitre VII :
- l'unité TURD03 qui fournit des routines pour une meilleure compatibilité avec la
version 3 du Turbo-Pascal;
- l'unité GRAPII3 qui implémente les anciennes routines de graphique de la version 3
du Turbo-Pascal.
6.1 Définitions.
Les overlays sont des parties de programme qui, au moment de l'exécution, occuperont
à tour de rôle le même emplacement dans la mémoire. L'utilisation d'overlays conduit à une
réduction significative de la taille totale de la mémoire nécessaire au fonctionnement d'un
programme. Ainsi le programme est divisé en :
- une partie qui contient le programme principal et les routines d'intérêt général;
- une partie qui contient les modules qui sont indépendants les uns des autres et, par
conséquent, n'ont pas besoin d'être tous présents en mémoire au même moment.
Le cadre placé à gauche du schéma suivant représente l'espace total occupé par
l'exécution d'un programme qui réalise trois fonctions d'un système. Les trois fonctions sont
implémentées sous forme de trois modules (par exemple trois unités), appelés par un
gestionnaire qui constitue le programme principal.
1 1 1
1 1 1
1 1 1
1 1 1
1 fonction 1 1 1
tampon
1 1 1 d'overlays
1 1 1
1 fonction 2 1
1
1 1
1 1
1 fonction 3 1
1 1
1 1
1 1
copy lb prog.exe+prog.ovr
Il est intéressant de noter que le terme résidant utilisé dans ce chapitre n'a rien à voir
avec les programmes résidants (appelés aussi TSR-Terminate and Stay Resident) qui, à
l'exemple de SideKick, restent présents en mémoire pendant l'exécution d'autres programmes.
Dans notre cas, il s'agit simplement de la partie du programme qui n'a pas été implémentée
sous forme d'overlay.
Au moment de l'exécution, le mécanisme est le suivant:
- pendant le chargement du programme exécutable, le système tente de réserver
l'espace nécessaire au tampon d'overlay, entre la pile et la fin de la mémoire disponible; si la
tentative échoue, un message sera émis pour avertir l'utilisateur du manque de mémoire et le
programme sera interrompu;
- quand l'utilisation de la fonction 2 (par exemple) devient nécessaire, le programme la
recherche dans le fichier d'extension .OVR et la charge dans le tampon d'overlay, où elle sera
exécutée;
- quand il sera nécessaire de faire appel à une autre fonction (la fonction 1 par
exemple), le programme tentera de la charger dans le tampon, soit en remplacement de la
fonction précédemment chargée, soit sans éliminer celle-ci lorsque la taille du tampon est
suffisante pour contenir les deux (ce qui peut éviter un accès au disque lors d'une utilisation
future de la fonction 2) ;
- quand le programme aura besoin de la fonction 3 qui, étant la plus grande, occupera
tou t l'espace, celle-ci sera récupérée dans le fichier .OVR et chargée à la place des
précédentes.
Une possibilité intéressante est le chargement initial du fichier .OVR dans la mémoire
EMS, au delà des 640 Ko de mémoire accessibles par le DOS. Ceci ne peu t se faire que si
l'ordinateur possède une extension de mémoire EMS (Extended Memory Specification)
répondant aux spécifications LIMS (LotuslInteIlMicroSoft). Même en utilisant cette
possibilité, le tampon d'overlay reste nécessaire. L'utilisation de la mémoire EMS n'augmente
donc pas la taille de la mémoire disponible pour le programme ou les données; elle accélère
seulement l'échange entre les overlays qui sera effectué de mémoire à mémoire, au lieu de
disque à mémoire.
program OvrEmpl:
{$F+}
uses overl ay. ert. dos. fonetl. fonet2. fonet]:
1$0 fonctl!
{$O fonct2!
{$O fonct3!
L'unité OVERLAY, unité standard du Turbo-Pascal, doit être citée dans la directive uses
du programme principal, avant l'une quelconque des unités qui seront placées en overlay.
6.3 Limitations.
Les programmes qui utilisent des overlays doivent obligatoirement être compilés en
utilisant le disque comme destination; ils ne peuvent pas être compilés vers la mémoire.
Les unités SYSTEM, CRT, OVERLAY, GRAPII, TURB03, et GRAPII3 ne peuvent pas être
placées en overlay. De manière plus générale, il en est de même de toutes les unités qui
contiennent des procédures d'interruption (voir chapitre V).
Une unité ne peut pas déclarer d'autres unités comme overlay. Ce qui signifie qu'il ne
peut exister qu'un seul niveau d'overlay, à l'exclusion de niveaux emboîtés.
En plus des règles énumérées ci-dessus, il est possible d'utiliser les procédures et les
fonctions disponibles dans l'unité OVERLAY, décrite dans le chapitre suivant.
7 Unité OVERIAY.
La variable OvrResult, de type integer, peut être utilisée comme code de retour de
toutes les procédures et fonctions de l'unité OVERLAY. Elle peut prendre les valeurs
suivantes:
- OvrOk (valeur 0) indique que tout s'est bien passé;
- OvrError (valeur -1) indique une erreur du gestionnaire d'overlay ;
- OvrNotFound (valeur -2) si le fichier .aVR n'a pas été trouvé;
- OvrNoMemory (valeur -3) si la mémoire est insuffisante pour le tampon d'overlay ;
- OvrIOError (valeur -4) en cas d'erreur de lecture du fichier .aVR ;
- OvrNoEMSDriver (valeur -5) si l'extension de mémoire EMS n'est pas installée;
- OvrNoEMSMemory (valeur -6) si la mémoire EMS est insuffisante.
D'autres variables sont disponibles pour permettre au programmeur de gérer lui même
le tampon d'overlay. Il s'agit de OvrTrapCount, OvrLoadCount, OvrFilcModc, OvrRcadl3uf.
Dans la presque totalité des programmes qui utilisent des overlays, il suffit de laisser faire le
gestionnaire standard, sans intervenir. Nous citons ces variables seulement pour information.
Dans des cas très spécifiques, se reporter au manuel de Turbo-Pascal pour les utiliser.
où NomFichier est le nom du fichier, éventuellement complété par le nom de l'unité logique et
du chemin (nom complet). Quand le fichier .OVR a été placé à la fin du fichier .EXE, il suffit
de passer comme paramètre le nom du ficher .EXE qui peut être récupéré sur la ligne de
commande:
Ovrlnit(ParamStr(O));
Elle doit être appelée avant toutes les autres procédures et fonctions qui sont en
relation avec les overlays, et avant l'allocation des variables dynamiques (voir chapitre II).
La procédure OvrInitEMS permet de charger le fichier .OVR dans la mémoire EMS. Elle
n'u tilise pas de paramètres:
procedure OvrlnitEMS:
Si la mémoire EMS n'est pas installée ou si elle est de taille insuffisante pour recevoir le
fichier .OVR, le programme continuera à fonctionner normalement en utilisant le disque.
OvrInitEMS peut donc être appelée systématiquement.
D'autres routines sont disponibles et peuvent être appelées dans des cas spécifiques.
Elles interviennent dans la gestion du tampon d'overlays. Il s'agit de :
La structure du programme doit être étudiée avec soin pour choisir les unités à placer
en overlays. Une unité susceptible d'être appelée souvent par une autre, placée également en
overlay, ne doit pas être placée en overlay. Par exemple, quand l'unité d'administration d'un
fichier est placée en overlay, il ne peut pas en être de même avec l'unité qui contient les
routines d'accès au fichier: si c'était le cas, chaque fois qu'il serait nécessaire d'exécuter une
de ces routines, il faudrait procéder à un échange d'overlays, ce qui nuirait aux performances
du programme. A moins que, par hasard ou par une gestion adéquate du tampon d'overlay,
les deux unités puissent cohabiter en mémoire au même instant.
Il est également conseillé d'éviter de placer en overlay les unités qui comportent une
partie d'initialisation. Bien qu'elle soit exécutée une seule fois, cette partie d'initialisation
serait chargée à chaque échange d'overlay et occuperait inutilement de l'espace en mémoire.
Il vaut mieux regrouper toutes les parties d'initialisation d'unité dans une autre unité
également placée en overlay. Elle sera ainsi chargée et exécutée au début du programme puis
oubliée.
\$O+.r+}
unit fitoovr;
interface
uses overlay;
impl ementati on
begin
Ovrlnit( 'SISFITO.OVR');
case OvrResult of
OvrNotFound {erreur le fichier SISFITO.OVR n'a pas été trouvé};
OvrError : {erreur autre erreur dans la gestion des overlays};
end;
Ovrlni tEMS;
case OvrResult of
OvrError {erreur autre erreur dans la gestion des overlaysl:
OvrNoEMSDri ver {erreur pilote de la mémoire EMS non installé}:
OvrNoEMSMemory {erreu r taille de la mémoire EMS insuffisante}:
end;
end.
{$M 16384,O,655360}
program sisfito;
uses
dos,
c rt,
global, {routines d'intérêt général}
fitoovr, {initialisation du système d'overlays}
sistab. {gestion des tables}
fitoOOl, {gestion de l'herbier}
fito002, {édition de l'herbier}
fito003; {gestion de prêts}
begin
{initialisations}
{gestionnaire de menu}
Un type pointeur est identifié par le symbole" placé devant le type de la variable. Par
exemple, les déclarations qui suivent:
type
PointeurEntier = "integer;
va r
Entier!,
EntierZ : PointeurEntier;
signifient que Entier! et Entier2 sont des variables pointeurs qui pointent vers une adresse
de la mémoire dont le contenu sera interprété comme étant la valeur d'un entier.
Du point de vue syntaxique, Entierl représente l'adresse d'un entier, alors que Entier!"
représente le contenu (ou la valeur) de l'entier.
Physiquement, une variable de type pointeur, puisqu'elle contient une adresse longue,
occupe 4 octets: deux pour le segment et deux pour le déplacement à l'intérieur du segment.
On a coutume de représenter la valeur d'un pointeur par la valeur en hexadécimal du
segment, suivi du symbole : et suivi de la valeur en hexadécimal du déplacement. Par
exemple, un pointeur de valeur OAGB:OOIF pointe vers l'adresse absolue OAGCF, c'est à dire
OAGBxlO + IF (valeurs exprimées en hexadécimal).
Un pointeur peut désigner l'adresse d'une variable quelconque, y compris celle d'une
structure ou d'un autre pointeur. Il faut signaler également que:
- le type pointer est un type standard du Turbo-Pascal qui désigne un pointeur vers
une variable de type quelconque et constitué, comme tout pointeur, d'un segment et d'un
déplacement;
- la valeur nil est compatible avec tous les types de pointeu rs et peut être utilisée
pour représenter la valeur d'un pointeur dont la valeur n'a pas de signification, c'est à dire
un pointeur qui ne pointe pas vers un objet défini; n'il équivaut à la valeur 0000:0000 ;
- les opérateurs de comparaison peuvent être utilisés pour comparer des variables de
type pointeur;
- on appelle pointeur normalisé un pointeur dont le déplacement à l'intérieur du
segment est une valeur entre 00 et OF.
1
PSP préfixe de segment de programme 1
L....
1
---I' <- Prefi xSeg
Les variables suivantes sont déclarées dans l'unité SYSTEM (toujours utilisée par un
programme en Turbo-Pascal) et directement mises à jour par le gestionnaire de tas. Elles sont
donc directement utilisables par le programmeur.
FreePtr pointe vers la fin de la table des fragments qui croît vers le bas, à chaque fois
que de nouveaux trous deviennent disponibles sur le tas. Il faut noter que si le dernier espace
libéré est voisin d'un espace déjà disponible, les adresses seront réorganisées pour obtenir
une seul espace disponible; ce qui signifie que la table des fragments peut également
diminuer au moment de la libération de variables, jusqu'à disparaître quand plus aucune
variable n'est allouée (tas complètement disponible).
HeapOrg pointe vers le début Oimite inférieure) du tas; sa valeur est connue
immédiatement après le chargement du programme et doit rester la même jusqu'à la fin de
son exécution.
HeapPtr pointe vers la fin Oimite supérieure) du tas; après chaque opération
d'allocation ou de libération, sa valeur est remise à jour.
Deux procédures permettent d'allouer des variables dans la mémoire dynamique: il
s'agit de New et GetMem. Deux méthodes permettent de libérer l'espace utilisé par des
variables dynamiques précédemment allouées, la première utilise les procédures Dispose ou
Fl'eel\1cm, la seconde utilise Mark et Rclease.
qui alloue un espace de t octets dont l'adresse est retournée dans le pointeur p; la variable
ainsi définie peut ensuite être référencée par pA ;
qui réalise une opération similaire à celle réalisée par GetMem où t est remplacé par la taille
de la variable pointée par p; en d'autres termes New(p) est équivalent à GetMem(p,
sizeof(pA».
Dans les deux cas, si le plus grand trou disponible dans le tas est de taille inférieure à t ou
sizeof(pA), il se produira une erreur 203 lIeap overnow error, p deviendra égal à nit et
l'exécution du programme sera interrompue. Pour éviter ce problème, il est recommandé
d'utiliser la fonction MaxAvail pour contrôler la situation du tas avant de tenter d'allouer la
variable. Ce qui peut se faire ainsi:
Pour libérer l'espace occupé par une variable dynamique, nous disposons également de
deux procédures:
qui place dans lIeapPtr la valeur de p (attention: Release(p) n'est pas équivalent à
HeapPtr := p, car Release réorganise également la table des fragments).
Comme pour la méthode antérieure, une valeur allouée dans l'espace libéré ne peut plus
être utilisée (sans être à nouveau allouée). Les manuels de Turbo-Pascal conseillent
l'utilisation de Freemem et Dispose plutôt que Mark/Release, car cette dernière méthode
oblige le programmeur à connaître parfaitement quelles sont les variables qui ont été libérées.
type
FreeRec record
DrgPtr,
EndPtr : pointer:
end;
FreeList = array[D .. 8191] of FreeRec;
FreeListP = AFreeList;
Chaque bloc de mémoire libéré est défini par son adresse de début (OrgPtr) et son
adresse de fin (EndPtr) qui sont des pointeurs normalisés. A tout moment, le nombre
d'éléments de la table des fragments peut être calculé par la formule:
ce qui signifie que la table des fragments peut contenir jusqu'à 8191 éléments.
Quand une ou plusieurs variables dynamiques non contiguës sont libérées, la table des
fragments croît vers le bas pour mémoriser les adresses des espaces libérés. Ceci fonctionne
tant qu'il y a suffisamment d'espace entre HeapPtr et FreePtr et tant que le nombre de .
fragments est inférieur à 8191. Dans le cas contraire une erreur interrompt l'exécution.
Pour prévenir le premier cas de figure, il est possible d'utiliser la variable FreeMin en lui
donnant une valeur minimum de l'espace entre IIeapPtr et FreePlr. A chaque appel de
CetMem et New, le gestionnaire du tas vérifie que l'allocation de mémoire demandée ne va
pas réduire l'espace disponible à une taille inférieure à FreeMin. Si c'est le cas, la variable ne
sera pas allouée.
La prévention contre le second cas de figure fait l'objet du chapitre suivant.
Par défaut, la granularité du gestionnaire de tas est de un octet. Ce qui signifie que si
on alloue une variable de un octet, un seul octet sera effectivement occupé; ce qui signifie
également que, quand cette variable sera libérée, un octet sera libéré mais huit seront
occupés dans la table des fragments pour conserver l'adresse de l'espace libéré (si celui-ci
n'est pas voisin d'un espace déjà libéré). Cet exemple peut paraître peu réaliste, mais la
même chose peut se produire quand on alloue des espaces de taille variable comme, par
exemple, les lignes d'un traitement de texte. Considérons l'exemple de l'allocation puis de la
libération d'une ligne de 50 octets; si l'allocation suivante nécessite 49 octets, il restera un
espace d'un octet mémorisé comme tel dans la table des fragments. L'accumulation de ces
espaces de petite taille conduit à une fragmentation excessive de la mémoire disponible, ce
qui peut entraîner plus de 8191 fragments et produire une erreur d'exécution irrécupérable.
La solution pour éviter ce genre de problèmes est d'augmenter la granularité du
gestionnaire du tas. Pour cela, le programme est obligé de définir et d'utiliser ses propres
routines d'allocation et de libération de mémoire. On trouve ci-dessous les procédures
GetMem et FreeMem qui permettront de maintenir une granularité de 16 octets;
Dans la plupart des cas, cependant, la limite de 8191 sera suffisante sans autre
précaution.
7 Exemples.
L'unité VisuTas, disponible dans le fichier VISUTAS.PAS, peut être utilisée dans un
programme quelconque pour montrer, à certains moments, l'état de la mémoire
dynamique: espace disponible, taille du plus grand espace disponible, nombre et taille des
trous de mémoire disponibles, etc...
En appelant la procédure MontrerTas. ces informations sont montrées dans une fenêtre
large de 80 colonnes, ouverte sur la ligne choisie par le programmeur et comportant un titre
passé en paramètre. Il suffit alors d'appuyer sur une touche quelconque pour montrer plus
d'informations ou effacer la fenêtre et revenir au programme appelant.
Le programme AllocTas, imprimé ci-dessous, ne fait qu'allouer puis libérer des variables
dynamiques de divers types. Il fait également appel à l'unité VisuTas pour montrer l'état du
tas après chaque opération.
var
PtrMat "TypeMat;
ptrMatl "TypeMat;
ptrStr "TypeStr;
p pointer;
s "string;
word;
{$F+I
function ErreurTas(t word) integer;
{$F - }
begin
ErreurTas := 1;
end;
begin
HeapError ;= @ErreurTas;
MontrerTas 0, 'au début');
if MaxAvail >= sizeof(PtrMat)
then begin
new( PtrMat);
MontrerTas 0, 'après all ocati on d "un vecteur d" enti ers'):
end
else {Mémoire insuffisante};
getmem( s,50) ;
if s <> ni 1
th en MontrerTas(I, 'après allocation d"une cha1ne de 50 caractères')
else {Mémoire insuffisantel:
getmem(PtrMatl, 500*sizeof(integer»;
if PtrMatl <> nil
then MontrerTasO, 'après allocation d"un vecteur incomplet d"entiers')
else {Mémoire insuffisantel;
new ( Pt rSt r) ;
if PtrStr <> nil
th en MontrerTas(l, 'après allocation d"un vecteur de cha1nes de caractères')
else {Mémoire insuffisantel;
if PtrStr <> ni 1
then for i ;= 1 to 50 do
if i mod 2 = 0
th en begin
p ;= PtrStr;
p ;= ptr(seg(p"), ofs(p")+pred(i)*sizeof(string»;
freemem(PtrNormalise(p), sizeof(string»;
if PtrMat <> ni l
then dispose(PtrMat);
MontrerTas(l, 'après libération du vecteur d"entiers complet');
ifs<>nil
th en freemem(s, 50);
MontrerTas(1, 'après libération de la cha'ne de 50 caractères');
end.
Il faut signaler que la méthode utilisée dans ce programme permet de le protéger contre
le manque de mémoire: la procédure ErreurTas contrôle les erreurs susceptibles de se
produire pendant l'utilisation de GetMem et New. La routine standard, pointée par
HeapError, retourne toujours la valeur 0, ce qui provoque une erreur d'exécution quand la
mémoire disponible devient insuffisante pour contenir la nouvelle variable à allouer. Il vaut
mieux implémenter une routine du type de ErreurTas qui retourne une valeur différente de
zéro en cas d'erreur et tester la valeur du pointeur après la tentative d'allocation (en cas
d'erreur, GetMem et New retournent un pointeur avec la valeur nit) :
HeapError ;= @ErreurTas;
Enfin, dans le programme AliocTas, le lecteur pourra découvrir une autre méthode de
contrôle: il s'agit de vérifier la taille de l'espace disponible avant d'appeler la routine
d'allocation. Ceci a été fait dans le cas de l'allocation du tableau complet d'entiers.
Le prochain exemple présente ce qui pourrait être la définition d'une ligne de texte dans
un traitement de texte. Comme indiqué dans les routines d'allocation et de destruction de
I----------------------------cours de Pascal-Avancé----------------------------}
1 }
{ exemple incomplet d'allocation de lignes de texte }
{------------------------------------------------------------------------------}
consl
MaxLigne = 255;
type
LigText string[MaxLigne]; {type ligne de texte}
PtrText "LigText; Itype pointeur vers une ligne de texte}
PtrLign "LigDesc; {type pointeur vers une description de ligne}
LigDesc record {type description de ligne}
LigSuiv, {pointeur vers la ligne précédente}
LigPrec PtrLign; {pointeur vers la ligne suivante}
Texte PtrText; {pointeur vers le texte contenu dans la ligne}
Flags, Iflags pour caractériser la ligne}
TextLen integer; {taille du texte de la ligne}
end;
begin
if (Maxavail > D) and (Maxavail <= (Taille + sizeof(LigDesc)))
then begin {si taille insuffisante, sortir}
NouvelleLigne := nil;
exit
end;
Getmem (Ligne,sizeof(LigDesc)); {alloue l'espace nécessaire à la description de la ligne}
TailLigne := succ(Taille); {taille du texte = Taille + 1 }
Getmem(Ligne".Texte, TailLigne); {alloue Taille+1 espaces pour le texte}
Ligne".TextLen ;= predCTailLigne);{initialise la taille du texte}
Fillchar(Ligne",Texte",TailLigne, ' '); {remplit le texte de blancs}
Ligne".Flags := D; {initialise les flags }
NouvelleLigne .- Ligne;
end;
8 Exercices.
cl Implémenter une unité avec les procédures nécessaires à la définition d'une matrice
d'entiers, à deux dimensions, et qui utilise au maximum la mémoire disponible. La partie
d'initialisation doit effecteur la réservation de l'espace disponible. Trois routines doivent être
mises à disposition de l'utilisateur pour mémoriser une valeur X dans la position (l, J) de la
matrice, récupérer la valeur de la position CI, J) et finalement libérer l'espace occupé
initialemen t.
Turbo-Access : gestionnaire
de fichier séquentiel indexé.
1 Informations générales.
1.1 Définitions.
Le fichier logique à gérer est vu par le Turbo-Access comme deux fichiers physiques ou
plus:
- le fichier des données, décrit comme DataFile, qui contient les informations à
gérer; chaque élément du fichier de données est un enregistrement; un fichier de données
peut contenir plus de deux milliards d'enregistrements et la taille maximum d'un
enregistrement est de 65535 octets;
- un ou plusieurs index, décrits comme IndexFile, où sont stockées les clés qui
identifient un enregistrement de données.
La clé doit être constituée par un ou plusieurs champs de l'information à gérer (champs
du fichier de données), de telle manière que le contenu d'une clé permette d'identifier
complètement l'enregistrement correspondant dans le fichier de données. Toutefois, uh
paramètre du Turbo-Access autorise l'existence de clés dupliquées, ce qui nécessite quand
même un travail supplémentaire de la part du programmeur pour différencier les données qui
correspondent à deux clés identiques. A l'intérieur de chaque index, à chaque clé est associé
un numéro d'enregistrement de données. Ainsi, chaque élément d'un fichier d'index est
composé d'une clé et d'un numéro d'enregistrement, bien que, physiquement, ces éléments
soient regroupés en enregistrements de n éléments appelés pages, n étant la taille de la page.
Pour un fichier déterminé, au moins un index doit être défini, sinon ce ne serait plus un
fichier indexé. Par contre, l'existence du fichier de données n'est pas obligatoire: si tous les
champs de données à gérer font partie de la clé, il suffit de gérer l'index.
Turbo-Access met à la disposition du programmeur deux ensembles de routines pour
créer, administrer et supprimer des fichiers séquentiels indexés:
- un ensemble de routines de bas niveau qui permet d'implémenter toutes les
possibilités offertes;
- un ensemble de routines de plus haut niveau qui facilite le travail du programmeur
en accédant au fichier sous une forme globale, sans distinguer le fichier de données et
va r
Fichier: DataSet:
va r
FichierDonnees DataFile:
Fichierlndex IndexFile;
type
EnregFichier record
Status : longint:
lchamps d'informationl
end:
- le type de la clé doit être déclaré comme string et avec la taille exacte de la clé du
fichier; par exemple:
type
EnregCle = string[10]:
var
Enreg EnregFichier:
Cle EnregCle;
- dans le cas où l'on souhaite travailler avec les routines de bas niveau, il est
également nécessaire de déclarer une variable de type longint, qui correspond au numéro de
l'enregistrement de données; par exemple:
var
NumEnreg longint:
Les routines décrites ci-dessous font partie de tahigh, mais la variable Ok, utilisée
comme code de retour, est définie dans l'unité taccess. En conséquence, pour utiliser ces
rou tines, il est nécessaire de déclarer les deux unités (taccess et tahigh) dans la directive uses
du programme. Comme tahigh utilise aussi taccess, cette dernière doit être citée en premier.
Fichier est une variable déclarée dans le programme appelant, de type DataSet.
NomFichier et NomIndex sont les noms physiques du fichier de données et du fichier d'index
associés à la variable Fichier. TailleEnreg et TailleCle sont respectivement la taille de
l'enregistrement de données et la taille de la clé; on a coutume de les définir en utilisant
l'opérateur sizeof: sizeof(EnregFichier) pour TailleEnreg et pred(sizeof(EnregCle)) pour
TailleCle.
TACreate crée le fichier de données et l'index et les laisse ouverts et prêts à
l'utilisation. La variable Ok n'est pas affectée par l'exécution de TACreate. Seules des erreurs
du DOS (répertoire ou disque plein) peuvent se produire durant l'exécution.
Voir exemple au paragraphe 2.2.
Fichier doit être une variable de type DataSet, qui représente un fichier séquentiel
indexé ouvert par TAOpen ou TACreate. Enreg est une variable de type EnregFichier et CIe
est une variable de type EnregCle. 11 est évident que tous les champs de Enreg doivent déjà
avoir été remplis avec les valeurs correctes, y compris le champ Status qui doit avoir été mis
à zéro. De la même façon, CIe doit avoir été construite avec les valeurs correspondantes des
champs de l'enregistrement à écrire.
Si la clé existe déjà, l'enregistrement va remplacer celui qui existe déjà. Dans le cas
contraire, l'enregistrement et la clé seront inclus respectivement dans le fichier de données et
dans l'index. La variable Ok n'est pas affectée par l'exécution de TAWrite : la seule erreur
possible est le manque d'espace sur le disque, ce qui provoquera une erreur du DOS.
Dans l'exemple présenté ci-dessous, l'enregistrement Prix et la cle ClePrix sont remplis
avant l'appel de TAWrite pour écrire l'enregistrement sur disque:
with Prix do
begin
Nom : = 'B rés il' ;
Date := '900325';
Resul tel] := 01;
Resu1t[5] := 20;
Status := 0;
end;
ClePrix := Prix.Nom;
TAWrite(FicPrix, Prix, ClePrix);
La manipulation des enregistrements d'un fichier séquentiel indexé est basée sur les clés
des enregistrements. Pour rechercher un enregistrement, il est donc nécessaire de définir
d'abord la valeur de la clé, avant d'appeler la routine TARead :
Fichier doit être un fichier de type DataSet, déjà ouvert. CIe et Enreg sont
respectivement la clé à rechercher et la variable où sera retourné le contenu de
l'enregistrement lu. Rappelons que la valeur de la clé doit avoir été définie avant l'appel.
RechercheExacte est une variable booléenne pour préciser si on désire rechercher
l'enregistrement dont la clé correspond exactement à la clé passée comme paramètre (valeur
true), ou si on souhaite seulement récupérer le premier enregistrement dont la clé commence
par les caractères de celle passée comme paramètre (valeur false). Ok revient avec la valeur
true si un enregistrement a été trouvé, avec la valeur false dans le cas contraire.
Dans l'exemple ci-dessous, on cherche à lire l'enregistrement qui correspond au Grand
Prix du Brésil. Pour cela, la clé est initialisée à la valeur 'BRESIL' :
ClePrix := 'BRESIL';
TAReadlFicPrix, Prix, ClePrix, true);
if Ok
then {enregistrement trouvél
else {enregistrement non trouvé}
Dans l'exemple suivant, on cherche à lire le premier GP qui commence par la lettre B :
ClePrix := 'B';
TAReadlFicPrix, Prix, ClePrix, false);
if Ok
then {un enregistrement a été trouvé lce peut ~tre Belgique)}
else {aucun GP ne commence par la lettre BI
Utiliser la procédure TADelete pour effacer un enregistrement et, par conséquent, la clé
correspond an te.
Fichier est un fichier séquentiel indexé déjà ouvert et CIe, de type EnregCle, doit avoir
été initialisée avec la valeur de la clé qui correspond à l'enregistrement à supprimer. La
valeur truc sera retournée dans OK si la clé recherchée a été trouvée et exclue. La valeur
false sera retournée si la clé n'a pas été trouvée. En réalité, la clé est retirée de l'index, mais
l'enregistrement reste physiquement dans le fichier de données avec une valeur non nulle
dans le champ d'état (Status); ce qui permettra la réutilisation du même espace pour
introduire un nouvel enregistrement plus tard.
Dans l'exemple ci-dessous, on cherche à retirer le CP de Belgique du fichier:
ClePrix := 'BELGIQUE';
TADelete{FicPrix, ClePrix);
if not Ok
then {GP de Belgique non trouvé};
La recherche séquentielle peut être réalisée par ordre croissant ou décroissant de.s clés
de l'index.
TANext permet de lire un enregistrement dont la clé est la suivante par rapport à une
clé déterminée:
Fichier est un fichier séquentiel indexé déjà ouvert. Clé est une variable de type
EnregCIe qui doit être initialisée avant l'appel. Enreg est une variable de type EnregFichier
qui recevra le contenu de l'enregistrement trouvé.
Après l'exécution de TANext :
- si Ok a la valeur false, alors la clé passée comme paramètre est égale ou supérieure à
la dernière clé du fichier; par conséquent, aucune clé suivante n'a été trouvée et le contenu
de Clé et Enreg est indéfini; si on appelle à nouveau TANext, le fichier sera positionné à son
début et le premier enregistrement sera récupéré;
- si Ok a la valeur truc, alors la clé suivante a été trouvée et retournée dans la variable
CIe; les données de l'enregistrement associé ont été placées dans la variable Enreg.
TAPrev, qui fonctionne de la même manière et avec les mêmes paramètres, permet de
récupérer l'enregistrement avec la clé précédente.
TAReset permet de se placer au début du fichier:
TAReset{Prix) :
Ok := true;
whll e Ok do
begin
T/lNext{FicPrix, Prix, ClePrix);
if Ok
then {traiter l'enregistrement (impression par exemple)}
Il est indispensable de fermer un fichier séquentiel indexé qui a été mis a Jour, avant
que le programme ne se termine. La fermeture automatique, qui est réalisée lorsqu'un
programme rend le contrôle au système opérationnel, n'est pas suffisante pour maintenir la
structure correcte des fichiers séquentiels indexés. Tout cela parce que, en plus de transférer,
de la mémoire vers le disque, les tampons associés au fichier, il est nécessaire de mettre à
jour certaines informations enregistrées dans le premier enregistrement de l'index.
Pour fermer un fichier ouvert, utiliser la procédure TAClose :
Il est évident qu'après avoir été fermé, aucune opération ne peut plus être faite sur le
fichier ou son index, sans qu'il ait été réouvert par la procédure TAOpen.
Une autre procédure intéressante, comparable à l'instruction flush disponible dans le
Turbo-Pascal pour un fichier classique, est la procédure TAFlush :
TAErase ne fait que supprimer du disque les fichiers physiques (données et index)
associés à un fichier séquentiel indexé ouvert.
Pour utiliser les routines de bas niveau, il faut citer l'unité taccess dans la directive uses
du programme. Les tâches réalisées par ces routines sont semblables à celles réalisées par les
routines de haut niveau; généralement, il est nécessaire de faire appel à deux routines de bas
niveau (une pour la clé, une autre pour l'enregistrement) là où il suflisait d'une seule routine
de haut niveau. Le grand avantage des routines de bas niveau est d'autoriser un accès plus
Les procédures MakeFile et MakeIndex doivent être utilisées pour créer, respectivement,
le fichier de données et le (ou les) index.
FichierDonnee est une variable déclarée dans le programme appelant, de type DataFile.
NomFichier est le nom du fichier de données et TailleEnreg est la taille de l'enregistrement de
données.
Fichierlndex est une variable déclarée dans le programme appelant, de type IndexFile.
NomIndex est le nom physique du fichier d'index. TailleCle est la taille de la clé du fichier.
Enfin, Dupliquees est un byte pour indiquer si l'index peut contenir des clés dupliquées (clés
identiques qui pointent vers des enregistrements différents) : la valeur 0 indique qu'il ne peut
y avoir de clés dupliquées, la valeur 1 indique qu'il peut y en avoir.
On a coutume d'utiliser l'opérateur sizeof pour définir la taille de l'enregistrement et de
la clé : sizeof(EnregFichier) pour TailleEnreg et pred(sizeof(EnregCle» pour TailleCle.
Ces routines, en plus de créer les fichiers, les laissent ouverts et prêts à être utilisés. Si
le fichier (données ou index) n'a pu être créé, la variable Ok revient avec la valeur false. Si le
fichier est défini pour fonctionner avec plusieurs index, MakeIndex doit être appelé pour
chacun des index.
Voir exemple au chapitre 3.2.
Il s'agit en réalité de la recherche d'une clé dans le fichier d'indice, puisque, une fois la
clé trouvée, il suffira de lire l'enregistrement associé, identifié par son numéro, dans le fichier
de données.
Plusieurs procédures permettent de rechercher une clé ; elles utilisent tou tes les mêmes
paramètres :
- la variable FichierIndex, de type IndexFile, associée à l'index du fichier,
- le numéro d'enregistrement, NumEnreg,
- la variable CIe qui contient, avant l'appel, la valeur de la clé à rechercher et, après
l'appel, la valeur de la clé trouvée.
Toutes fonctionnent de manière identique: le fichier d'index doit être ouvert, la clé
dont la valeur est passée dans CIe est recherchée et, en sortie de la procédure, si Ok est true,
CIe contient la clé cherchée (qui dépend du type de recherche) et NumEnreg contient le
numéro d'enregistrement associé. Nous allons maintenant détailler le fonctionnement de
chacune de ces procédures.
La procédure FindKey recherche dans l'index, une clé dont la valeur soit égale à celle
passée comme paramètre:
La procedure ScarchKcy recherche dans l'index, la clé dont la valeur est égale ou
supérieure à celle passée comme paramètre:
Si Ok revient avec la valeur faLse, la clé passée comme paramètre était supérieure à la
dernière clé de l'index,
La procédure NextKey recherche la clé dont la valeur est immédiatement supérieure à
celle de la clé passée comme paramètre:
Si Ok revient avec la valeur false, la clé passée comme paramètre était égale ou
supérieure à la dernière clé de l'index.
La procédure PrevKey recherche la clé dont la valeur est immédiatement inférieure à
celle passée comme paramètre:
Si Ok revient avec la valeur false, la clé passée comme paramètre était inférieure à la
première clé de l'index.
Enfin, la procédure ClearKey, qui utilise un seul paramètre, permet de se placer au
début de l'index:
FicDonnees est un fichier de données de type DataFile, déjà ouvert par OpenFile ou
MakeFile. Enreg contient les informations à écrire et le champ Status qui doit être mis à zéro.
Après l'écriture, NumEnreg contient le numéro qui a été attribué à l'enregistrement.
La procédure AddKey permet d'insérer une nouvelle clé :
Avant l'appel, l'index doit avoir été ouvert, Cie doit contenir la clé à insérer ct
NumEnreg doit avoir la valeur du numéro de l'enregistrement associé à la clé. Généralement,
NumEnreg est calculé immédiatement avant l'appel, par AddRec.
Il faut également signaler que, en cas d'appel à AddRec sans appel à AddKey,
l'enregistrement sera écrit mais ne pourra jamais être récupéré, puisque la clé associée n'aura
pas été insérée.
La procédure PutRec permet de réécrire, au même emplacement, un enregistrement déjà
Les paramètres de PutRec sont identiques à ceux de AddRec, à la différence près que
NumEnreg doit avoir été renseigné avant l'appel. Généralement, NumEnreg peut avoir été
mémorisé au moment de la lecture de l'enregistrement ou doit être déterminé à nouveau par
la procédure FindKey (consulter l'exemple plus bas, dans ce même paragraphe).
Parmi les routines de bas niveau, il n'en existe aucune capable de décider
automatiquement si un enregistrement doit remplacer un enregistrement déjà existant, ou s'il
s'agit d'un nouvel enregistrement qui doit être inséré. C'est donc au programmeur de
déterminer quelle est la situation du prochain enregistrement à écrire. Pour ce faire,
l'exemple ci-dessous peut servir de modèle:
Result[5] .- 20;
Status := 0;
end;
ClePrix := 'BRESIL'; {définir la clé}
i f not Ok
then begin {la clé n'existe pas encore}
AddRec( Fi cPrix, {écrit l'enregistrement et retourne son numéro}
NumPrlx, P rl x) ;
AddKey( IndPrix, li nsère 1a cl é correspondante dans 1 '1 ndex}
NumPrix, ClePrix);
end
else begin {la clé existe déjè et NumPrix
contient maintenant le numéro de l'enregistrement associé}
Put Re c ( Fic Pr i x , {i 1 suffit alors de réécri re l'enregi strement au même empl acement}
NumPrix, Prix);
end;
Il est évident que, si le fichier a été défini avec plusieurs index, une clé devra être
construite pour chacun d'eux, avec les champs correspondants de l'enregistrement.
Pour lire un enregistrement de données, il est tout d'abord nécessaire de connaître son
numéro. Pour cela, utiliser une des routines de recherche de clé. Il suffit ensuite de faire
appel à la procédure GetRec :
ClePrix := 'BRESIL':
FindKey<IndPrix, NumPrix, ClePrix):
1f Ok
then GetRec(FicPrix, NumPrix, Prix);
else {enregistrement non trouvé}
ClearKey(IndPrix):
NextKey(IndPrix, NumPrix, ClePrix):
if Ok
th en GetRec(FicPrix, NumPrix, Prix)
else {fichier vide}
La procédure DeleteKey doit être utilisée pour détruire une clé de l'index:
La variable FicIndex, de type IndexFile, représente un index déjà ouvert. CIe doit avoir
la valeur de la clé à détruire. Si la clé est trouvée, elle sera retirée de l'index, Ok reviendra
avec la valeur true, et le numéro de l'enregistrement sera retourné dans la variable
NumEnreg.
La procédure DeleteRec doit être utilisée pour effacer un enregistrement du fichier des
données:
ClePrix := 'B';
SearchKey(IndPrix, {recherche la première clé qui commence par B}
NumPrix, ClePrix);
while Ok and (ClePrix[l] = 'B') do {tant que la clé trouvée commence par B}
begin
DeleteKey(IndPrix, {effacer la clé}
NumPrix, ClePrix):
S'ils ont été mis à jour, il est indispensable de fermer le fichier de données et l'index,
avant de terminer le programme. La fermeture automatique qui se produit lorsque le
programme rend le contrôle au système opérationnel n'est pas suffisante pour maintenir une
structure correcte du fichier séquentiel indexé. La raison en est que, en plus de copier, de la
mémoire vers le disque, les tampons associés au fichier, il est également nécessaire de mettre
à jour certaines informations stockées dans le premier enregistrement de l'index.
Pour fermer un fichier de données, utiliser la procédure CloseFile :
Il est évident qu'après la fermeture des fichiers, aucune opération ne peut plus être
faite sur ces derniers sans qu'ils aient été réouverts par OpenFile et OpenIndex.
Deux autres procédures, comparables à l'instruction Flush du Turbo-Pascal, sont
disponibles pour les fichiers séquentiels indexés; il s'agit de FlushFile et Flushlndex :
Elles permettent d'assurer l'intégrité des fichiers, même en cas d'arrêt intempestif du
programme ou de panne d'alimentation. Normalement, les dernières modifications faites sur le
fichier restent en mémoire: par exemple, une page de clés ne sera écrite sur le disque que
lorsqu'il sera nécessaire de récupérer l'espace qu'elle occupe en mémoire, pour charger une
autre page. Cela signifie que, en cas d'arrêt brutal du programme, sans fermeture des fichiers,
l'index et le fichier de données ne seront pas au même niveau de mise à jour, ce qui
entraînerait des erreurs lors d'une prochaine utilisation des fichiers. FlushIndex et FlushFile
écrivent sur le disque les dernières modifications faites, sans fermer les fichiers,
Ce peut être un bon choix d'utiliser FlushFile et Flushlndex après avoir écrit ou effacé
un enregistrement. En contrepartie, le traitement sera plus lent, à cause des accès
supplémentaires au disque; mais ceci peut n'avoir que peu d'importance en cas de traitement
interactif.
EraseIndcx permet d'effacer du disque l'index associé à un fichier séquentiel indexé déjà
ouvert:
Ces deux routines peuvent être utilisées pour déterminer le pourcentage de l'espace
occupé par les données utiles:
Une valeur trop petite indique qu'une réorganisation de la base de données serait la
bienvenue.
L'exemple présenté dans ce chapitre sera également utilisé comme base des exercices du
chapitre 7. Il s'agit de l'administration des résultats du championnat automobile de Formule
1. La raison de ce choix est bien entendu l'existence d'une rivalité Brésil/France, tout à fait
d'actualité dans ce sport au moment du cours, et objet de discussions avec nos collègues
Brésiliens. La version présentée est simplifiée au maximum, le but étant seulement de donner
un exemple d'emploi du Turbo-Access.
Le programme se divise en trois parties:
- définition des fichiers séquentiels indexés;
- utilisation du Turbo-Access (routines de haut niveau) pour administrer le fichier des
pilotes et le fichier des grands prix ;
- utilisation de menus et de grilles d'écran simplifiées pour choisir les options et
renseigner les champs de données.
Pour économiser l'espace, l'exemple n'a pas été imprimé; il est seulement disponible sur
la disquette. Dans la version utilisée comme exemple, le programme Formulel permet de gérer
MaxDataType = EnregPilote;
MaxKeyType = EnregClePilote;
type
MaxoataType array[l. .76] of bytes; {76 taille de EnregPilotel
MaxKeySize string[15]; {15 taille de EnregClePilote}
Une réorganisation des fichiers peut être conseillée quand le fichier de données contient
beaucoup d'enregistrements effacés et non réutilisés.
La logique d'un programme de réorganisation est simple. Il suffit de parcourir
séquentiellement l'index, récupérer dans le fichier de données l'enregistrement associé et le
réécrire dans un nouveau fichier.
Un des exercices du chapitre 7 traite de l'implémentation d'un programme de
réorganisation.
Elle peut devenir nécessaire pour récupérer les données, dans le cas où l'index ou le
fichier de données ont été corrompus. On rencontre également des problèmes lors de
l'utilisation d'une nouvelle version de TACCESS ou TAHIGH, soit parce-qu'il a été nécessaire
de modifier les valeurs de MaxDataType ou MaxKeyType, soit parce-que TACCESS a été
recompilé accidentellement avec des paramètres différents.
Dans ce dernier cas, un des messages 1003 Data file created with different record size
ou 1004 Index File created with different key or page size sera produit.
Le programme présenté ci-dessous propose une solution pour ce type de situation: lire
le fichier de données comme un fichier séquentiel et, pour chaque enregistrement utile,
construire la clé et écrire l'enregistrement et la clé avec la nouvelle version des routines. Les
enregistrements inutiles sont le premier, utilisé à des fins de contrôle, et tous ceux dont le
champ Status est non nul (enregistrements effacés). Une réorganisation de ce type n'est
possible que si les champs qui composent la clé sont répétés dans l'enregistrement de
données, ce qui est toujours conseillé.
{défi nit ions du fichier à traiter-doivent être adaptées pour chaque cas}
type
TypeSorti e record
Status LongInt; {status de l'enregistrement}
Nom,
Prenom stri ng[l5];
Numero string[2];
Paysori stri ng[l5];
Auto string[2o];
end:
TypeCleSortie = string[15];
const
NomFicSortie 'PILOTES. oAT' ;
NomIndSortie 'PILoTES.INO' ;
NomFicEntree ·PILoTES.XXX· ;
{declarations du programme}
const
SizeSortie = sizeof(TypeSortiel;
SizeCleSortie = pred(sizeof(TypeCleSortiell;
TotEnreg : longint = 0;
va r
FicEntree file of TypeSortie:
EnrSortie TypeSortie:
CleSortie TypeCleSortie;
FicSortie OataFi l e;
IndSortie 1ndexFll e;
NumEnreg longint;
procedure ouvrirFichierEntree;
{ouverture de l'ancien fichier de données en le considérant comme un
fichier séquentiel}
begin
assign(FicEntree, NomFicEntreel;
reset(FicEntreel;
end;
procedure FermerFichierSortle;
begin
closefile(FicSortie);
closelndex(lndSortie);
end;
begi n
OuvrirFlchierEntree;
CreerFichierSortie;
seek(FlcEntree, 1); {pour Ignorer le premier enregistrement}
clrscr;
whlle not eof(FicEntree) do
begin
read(FicEntree, EnrSortie);
if EnrSortie.Status = 0
then begin {enregistrement avec données}
addrec(FicSortie,NumEnreg,EnrSortie);
CleSortie := CleCalculee;
i nc(TotEnreg);
addkey(lndSortie, NumEnreg, CleSortie);
gotoxy(10,10);write('récupération du nO ',TotEnreg:l0,' ',CleSortie);
end;
end;
FermerFichierSortle;
clrscr;
wrlteln('Récupératlon terminée - ' TotEnreg, ' enregistrements récupérés');
end.
Ce programme peut facilement être adapté pour le cas d'un autre fichier: il suffit de
substituer la définition de l'enregistrement et de la clé, la routine de calcul de la clé et le nom
des fichiers. Si le nouveau fichier indexé est créé avec le même nom, l'ancien fichier de
données doit avoir été renommé avant d'exécuter le programme,
7 Exercices.
Chapitre IV
Tri interne
1 Description et utilisation.
Il s'agit d'une procédure sans paramètres, compilée avec la directive $F+ car elle fera
l'objet d'un appel long par la procédure TurboSort. Le nom de la procédure est sans
importance et l'obtention des données à trier peu t être réalisée de manière quelconque
(lecture d'un fichier, calcul en mémoire). Mais chaque élément à trier doit être communiqué
au Turbo-Sort par la procédure SortRclcase.
Dans l'exemple ci-dessous, la procédure d'entrée des données lit tous les
enregistrements du fichier des pilotes de l'exemple du chapitre III :
Elle réalise la comparaison de deux éléments en suivant les critères de tri imposés par le
programmeur. Elle sera appelée souvent par la routine TurboSort; par conséquent, elle doit
obligatoirement être compilée avec la directive $F+ et être la plus efficace possible. La
fonction doit utiliser deux paramètres, passés par adresse, du type des données à trier et son
résultat doit être de type boolean. Dans le corps de la fonction, la comparaison doit calculer
la valeur de la fonction qui doit être true quand le premier paramètre est inférieur au second.
L'exemple ci-dessous montre la routine de classement des pilotes par leur prénom, alors
qu'initialement ils sont classés par nom de famille:
l $F+}
function PlusPetitPilotelvar p. a EnrPilotel;
l $F-}
begin
PlusPet1tPilote := P.Prenom < a.Prenom;
end
Son rôle est de récupérer les informations après le tri. Cette fois, c'est la partie de
récupération qui doit obligatoirement utiliser une routine de Turbo-Sort: la procédure
SortReturn. Une fois récupérée, la donnée peut faire l'objet d'un traitement
quelconque: visualisation à l'écran ou sur l'imprimante, écriture dans un fichier, etc...
La fonction SortEOS sert à connaître le moment où toutes les données ont été
récupérées.
{$F +}
procedure RecupererDonnees;
{SF-}
va r
P: EnrPllote:
beg1n
while not SortEDS do
beg1n
SortReturnIP):
with P do
wr1telnlPrenom. Nom);
end;
end:
La routine TurboSort est une fonction de type integer dont la valeur contient un code
de retour après l'exécution du tri. La valeur 0 signifie que tout s'est bien passé, une valeur
différente de 0 correspond à une erreur d'exécution. Les paramètres à passer sont:
- la taille des éléments à trier (il vaut mieux la calculer avec l'opérateur sizeoj) ;
- l'adresse de la routine d'introduction des données;
- l'adresse de la routine de comparaison;
- l'adresse de la routine de récupération des données.
La fonction TurboSort peu t renvoyer les valeurs suivantes:
- 0 quand tout s'est bien passé;
- 3 en cas de mémoire insuffisante (taille de la mémoire disponible inférieure à trois
fois la taille de l'élément à trier) ;
- 8 si la taille de l'élément à classer est inférieure à 2 ou supérieure à 32767 ;
- 9 si le nombre d'éléments à classer est supérieur à 32767 lors de l'utilisation de
SORT.TPU;
- 10 en cas d'erreur d'écriture sur disque;
- Il en cas d'erreur de lecture sur disque;
- 12 en cas d'erreur de création des fichiers de travail (disque plein).
Pour conclure, dans le cas de notre exemple, la syntaxe serait:
Le fichier TRiPILOT.PAS contient le programme complet d'où ont été extraits les
exemples utilisés ci-dessus.
2 Un exemple complet.
permet d'obtenir la liste triée des fichiers d'extension .PAS et d'extension .TPU. Si aucun
paramètre n'est précisé, tous les noms de fichier seront affichés.
Signalons également, dans cet exemple, l'emploi des types et des routines pré-déclarées
de l'unité Dos du Turbo-Pascal.
{----------------------------cours de Pascal-Avancé----------------------------}
{ }
{ exemple de sort interne }
{ ldisponible dans le fichier repert.pas) }
{------------------------------------------------------------------------------}
{affichage du contenu du répertoire en ordre alphabétique}
uses crt, dos. sort;
var
Result : integer;
NomFich : string[12);
{$F+ }
procedure ObtenirFichiers;
var
Fichier SearchRec;
i ,
j : byte;
begin
{si aucun paramètre sur la ligne de commande. on
assume *.*, c'est à dire tous les fichiers
j := ParamCount;
if j .. 0
then begin
j := 1;
NomFich := '*.*';
end;
procedure AfficherFich1ers;
var
Fich1er : SearchRec;
Date: DateT1me:
1 byte;
l word;
s string;
c char;
beg1n
l := 0;
writeln;
while not SortEDS do
begin
SortReturnCFichier); {récupère chaque fichier classé!
UnPackTimeCFichier.Time. Date); {transforme le temps en date/heure!
with Fichier, Date do
beg1n {affiche li l'écran les caractéristiques du fichier!
i := posC'.·. Name);
s := Name;
if i <> 0
then s := copyCcopyCName. 1, predCi))+' 1, 9)+copYCName. succCi). 3);
while lengthCs) < 12 do s := S + . ';
writelnCs. Size:lD. '
Day:2. '/'. CompleterCMonth):2. '/'. Year:4 .•
CompleterCHour):2. ':'. CompleterCMin):2. '.' CompleterCSec):2);
i ncC 1);
1f l mod 23 = 0
th en beg1n {temporisation quand l 'écran est plein!
writeC· suite·);
c := readkey;
while keypressed do c := readkey;
writeln;
end;
end;
end; {répète jusqu'au dernier fichier classé!
end;
{$F -!
begin
Result := TurboSortCsizeofCSearchRec). @ObtenirFichiers. @PlusPetitNom.
@AfficherFichiers);
end.
a) Modifier le programme TRIPILOT pour obtenir la liste des pilotes en ordre décroissant
des prénoms.
b) Compléter le programme REPERT pour autoriser les options IT, ID et lE, sur la ligne
de commande, afin d'obtenir la liste en ordre croissant de taille de fichier, de date de mise à
jour ou d'extension de fichier, respectivement. Pour ne pas trop compliquer le traitement des
options, on admet que, en cas d'options multiples, seul la dernière sera prise en compte. Par
exemple, Repert • .pas • .tpu IT devra produire la liste des fichiers par ordre croissant de taille
de fichier.
Cette partie du cours traite plus particulièrement des interactions entre le langage
Turbo-Pascal et l'environnement du système opérationnel du PC. Par conséquent, il est bon
que le lecteur ait des connaissances minimales de langage assembleur et du fonctionnement
interne du DOS et du BIOS, pour profiter pleinement des informations données dans ce
chapitre.
Le type registers, déclaré dans l'unité DOS, permet de manipuler les registres du
processeur:
type
Registers record
case i nteger of
o : (ax. bx. ex. dx. bp. si. di. ds. es. flags word);
1 : (al. ah. bl. bh. cl. ch. dl. dh : byte);
end;
end;
Il s'agit d'une structure avec variante, qui autorise la manipulation des registres aussi
bien en format 16 bits (ux, bx, ex, etc...) qu'en format 8 bits (al, ah, bl, bh, cl, ch, dl et dh).
La procédure Intr, déclarée dans l'unité DOS permet d'exécuter une interruption. En
voici l'en-tête:
où NumInt est le numéro de l'interruption à exécuter et Regs est une variable utilisée pour
communiquer des valeurs aux registres ou en récupérer.
Le premier exemple est assez simple: il s'agit d'exécuter une copie de l'écran sur
l'imprimante, de l'intérieur d'un programme, sans utiliser la touche correspondante du clavier
(touche PrtScr ou équivalente). L'interruption 05h réalise cette tâche, sans aucun paramètre
supplémentaire. Par conséquent, il suffit de déclarer la variable Regs et d'exécuter l'appel de
la procédure:
uses dos;
var
Regs: registers;
intr(5. Regs);
Le second exemple est plus complet. Il s'agit d'obtenir la date et l'heure courantes,
connues par le système opérationnel, et les transformer en chaîne de caractères. La fonction
DataHeure, imprimée et commentée ci-dessous fait appel à l'interruption DOS (21h) pour
récupérer la date et l'heure courantes:
var
Aux3 string[3];
Aux4 string[4];
Aux18 stri ng[18];
Regs Registers; (*variable qui contient les registres*)
begin
(*obtention de la date*)
Regs.AH := $2A; (*AH (la partie haute du regIstre AX)
doit contenir le numéro de la fonction
que retourne la date courante (fontion 2Ah)*)
(*obtent1on de l'heure*)
Regs.AX := $ZCOO;(*AH (la part1e haute du reg1stre AX)
do1t contenIr le numéro de la fonctIon
qu1 retourne l'heure courante (fonction ZCh)*)
Rappelons que l'appel Intr($21, Regs) pourrait être remplacé par l'appel direct aux
fonctions du DOS, c'est à dire MsDos(Regs). Dans les versions les plus récentes de Turbo-
Pascal, l'unité DOS offre déjà des fonctions pour récupérer et initialiser la date et l'heure du
système (voir annexe 1). Par conséquent, les routines ci-dessus ont seulement valeur
d'exemple.
Le troisième exemple vaut uniquement pour la version 3.3 du DOS ou les versions plus
récentes. Elle sert à contourner une limite qui perturbe fréquemment le développement de
grands systèmes, à savoir le nombre maximum de fichiers qu'il est possible de garder ouverts
au même instant. Par défaut, la limite maximum de fichiers ouverts que peut supporter le
DOS est de 20, y compris les périphériques standards (clavier, console).
Cette limite peut facilement être réduite par la directive FILES=n du fichier de
configuration CONFIG.SYS (voir documentation du DOS). Mais, pour l'augmenter, il ne suffit
pas de spécifier une valeur plus grande dans le fichier CONFIG,SYS, il est également
nécessaire d'utiliser la fonction 67h du DOS, La fonction ci-dessous peut être utilisée pour
cela:
begin
Vers10n := OosVersion;
1f (lo(Vers1on) > 3) or
( (lo(Vers1on) = 3) and (hi(Vers1on) >= 30) )
then beg1n
Regs.bx := n;
Regs.ah := $67;
MsOos(Regs);
MaxF1ch1er := (Regs.Flags and FCarry) 0;
end
if MaxFichier(25l
then Ipoursuivre le traitement}
else begin
Imessage}
hall ;
end;
I · ···cours de Pascal·Avancé····························}
1 }
1 exemple d'instruction inline }
1 (disponible dans le fichier inlinl.pasl }
1·······················-·························-···.-- .. -- - -}
program inlin1;
uses crt;
var
Rien: array[1 .. 5000] of word;
p : pointer;
c : char;
FilWrd word;
Fil Byt : array[l. .2] of char absol ute Fi lWrd;
begin
Chaque élément de l'instruction est séparé du suivant par le symbole "/" et représente
une constante ou une variable sur un octet ou un mot (deux octets) de code exécutable.
La taille de l'élément peut être déterminée automatiquement. Dans le cas d'une
constante, c'est évident: par exemple $40 va occuper un octet et $123f occupera deux octets.
Dans le cas d'une variable, la valeur est calculée en ajoutant, à la valeur du déplacement de
la variable à l'intérieur de son segment, la valeur d'une constante éventuelle qui suivrait le
nom de la variable: par exemple Zone+ 1 serait remplacé par la valeur de l'adresse de Zone
augmentée de l.
La taille peut également être imposée, en utilisant les opérateurs < (pour forcer à un
octet) ou > (pour forcer à un mot). Par exemple, />$20/ va générer deux octets ($20 et $00).
Le segment à utiliser comme base varie également en fonction de la variable utilisée, ce
qui a une influence sur la codification de l'instruction. Il est indispensable de se souvenir
que:
- pour une variable globale, ou une variable initialisée (constante avec type), on utilise
le segment de données, c'est à dire DS ;
- pour une variable locale, déclarée dans une routine ou une variable passée comme
paramètre, on utilise le segment de pile, c'est à dire BP, qui contient toujours l'adresse du
sommet de la pile au moment d'entrer dans la routine.
D'autre part, tous les registres peuvent être modifiés à l'intérieur d'une instruction
inline, à l'exception de BP, SP, SS et DS.
Avec ces éléments, il devient plus facile d'interpréter une instruction inline,
principalement lorsque le code assembleur équivalent est disponible en commentaire sur la
même ligne. Toutefois, le problème auquel le programmeur doit normalement faire face n'est
pas d'interpréter une instruction inline déjà existante, mais bien de programmer sa propre
Première étape: définir, en langage assembleur, ce que doit faire l'instruction. Il n'est
pas nécessaire pour cela d'utiliser un compilateur: il suffit de mettre sur le papier, ce que
serait la séquence d'instructions en assembleur (souvenez-vous qu'il s'agit de peu
d'instructions). Pour l'exemple ci-dessus, ce serait:
les di. Zone[bp] ;pour charger dans ES:DI. l'adresse de la zone:l remplir
mov ex. Compteur[bp] ;pour placer dans CX la taille de la zone (en mots de 2 octets)
mov ax, Mot[bp] ;pour placer en AX le mot Qui va replir la zone
cld ;pour définir l'incrément de Dl comme étant positif
rep stosw ;pour répéter CX fois la copie de AX :l l'adresse
pointée par ES:DI et incrémenter Dl de deux.
Remarquons l'usage de [bpI comme segment de base des variables, puisqu'elles sont
toutes passées comme paramètres,
Deuxième étape: utiliser l'utilitaire Debug du DOS (consulter le manuel du DOS) pour
traduire cette séquence d'instructions en langage-machine. Pour cela, entrer dans Debug et
utiliser la commande -a qui permet de saisir du code assembleur. Il est évident que le nom
des variables ne peut pas être utilisé directement, on le remplacera donc par une valeur de
déplacement sans signification, par exemple $1234, comme dans l'exemple ci-dessous:
C: >debug
-a
3F67 :0100 l es di, 1234[bp]
3F67:0104 mov ex, 1234[bp]
3F67:D108 mov ax, 1234[bp]
3F67:010C cld
3F67:010D rep stosw
3F67:010F
Après cela, utiliser la commande -u pour montrer à l'écran (ou mieux, avec copie sur
l'imprimante) le code exécutable depuis l'adresse de début (toujours 100) jusqu'à l'adresse de
fin des instructions saisies (dans notre cas: IDE) :
·u 100 10E
3F67:D100 C4BE3412 LES DI,[BP+1234]
3F67:D1D4 8B8E3412 MOV CX,[BP+1234]
3F67:0108 8B863412 MOV AX,[BP+1234]
3F67:010C FC CLD
3F67:010D F3 REPZ
3F67:01DE AB STOSW
Bien qu'elles puissent être considérées comme des macros (équivalentes aux macros de
l'assembleur), les directives inline sont très semblables aux instructions inline. Elles
permettent d'écrire des séquences de code exécutable, présentées comme des procédures ou
des fonctions, avec toutefois une particularité: au moment d'appeler une telle routine, ce
n'est pas une instruction GALL qui est générée (comme pour une routine normale), mais
simplement le code exécutable de la routine qui est inséré à l'endroit de l'appel. Ce qui
revient à dire qu'au moment d'être appelée, une directive inline est transformée en
instruction inline.
La syntaxe d'une directive inline est identique à celle d'une instruction inline, et donc
aussi complexe. Par conséquent, l'usage de ces routines doit se limiter à de courtes séquences
de code. Le fait que la séquence soit répétée à chaque appel renforce encore cette
recommandation (économie d'espace).
Les exemples les plus simples de directives inline sont les deux procédures suivantes:
unit in11n2;
interface
uses crt;
var
PtrAfficherErreur : pointer;
procedu re Ni mporteOuo1 ;
implementation
procedure NimporteOuoi;
{ }
begin
{instructions de la procédure}
if true
th en AfficherErreur(3);
{autres instructions}
end;
begin
PtrAfficherErreur := @AfficherErreurStandard;
end.
program lnlln3;
{SF+}
procedure AfficherErreurPersonnelle(n byte);
{SF'}
var
c : char;
begln
gotoxy(1.25) ;
case n of
1 write('Oisque plein');
2 : write('Unité de disquette non prête');
3 : write('Mémoire insuffisante');
{ 1
end;
c := readkey;
end;
begln
PtrAfficherErreur := @AfficherErreurPersonnelle;
{ 1
Nl mporteOuoi ;
{ }
end.
Il est obligatoire de respecter certaines règles dans la programmation des routines qui
vont être appelées par l'intermédiaire de directives inline :
- elles doivent avoir le même nombre de paramètres que ceux déclarés dans l'en-tête
de la directive; le type des paramètres doit également correspondre;
- elles doivent être déclarées comme adresses longues, en utilisant la directive de
compilation $F+ ;
- elles ne peuvent faire partie d'un autre bloc de code (elles ne peuvent être internes à
une autre routine) ;
- le compilateur n'a pas possibilité de faire une quelconque vérification au sujet de ces
règles mais, en cas d'erreur, les résultats de l'exécution seront imprévisibles.
Il s'agit des paramètres qui sont précédés du mot réservé var dans l'en-tête de la
routine. Dans tous les cas, indépendamment du type, le programme stocke sur la pile un
pointeur qui pointe vers l'adresse réelle du paramètre dans la mémoire.
Dans ce cas, la règle est plus complexe car elle dépend du type de paramètre et
également de sa taille:
- pour un paramètre de type shortint, byte, char, integer, word, la valeur du paramètre
est transmise comme un mot de deux octets Oe premier étant le moins significatif) ; dans le
cas de shortint, byte et char qui occupent seulement un octet, la valeur est stockée dans
l'octet le moins significatif du mot, le plus significatif étant ignoré;
- un paramètre de type longint est transmis en deux mots, le premier étant le moins
significatif;
- un paramètre de type boolean est transmis comme un byte de valeur 0 (pour faLse) ou
1 (pour true) ;
- un paramètre de type énuméré est transmis comme un byte (si le type contient 256
valeurs ou moins) ou comme un word (si le type contient plus de 256 valeurs) ;
- la valeur d'un paramètre de type real occupe 6 octets sur la pile;
- les paramètres de type single, double, exterui.€d et comp, spécifiques du co-processeur
mathématique, sont transmis respectivement sur 4, 8, 10 et 10 octets; (attention à la
compatibilité avec la version 4 du Turbo-Pascal où ces paramètres étaient transmis par la pile
du 80x87);
- la valeur d'un paramètre de type pointeur occupe 4 octets sur la pile Oe premier mot
contient le déplacement, le second contient le segment) ;
- pour un paramètre de type string, un pointeur qui occupe 4 octets est stocké sur la
pile et pointe vers l'adresse réelle du paramètre;
- pour un paramètre de type set, un pointeur qui occupe 4 octets est stocké sur la pile
et pointe vers une zone de 32 octets occupée par le paramètre;
- pour un paramètre de type array ou record qui occupe moins de 5 octets, la valeur
du paramètre est copiée sur la pile; pour les paramètres de même type, mais de taille
supérieure à 4 bytes, un pointeur est stocké sur la pile comme dans le cas d'un passage par
adresse.
{-----------------------·----cours de Pascal-Avancé--------------··------------}
{ }
{ programme pour lllustrer le passage de paramètres 1
{ (disponible dans le fichier param.pasl 1
{--_ .. _----_._---------_._-----------------------------------------------------}
program param;
uses crt, dos, utlltas;
type
TypePetitRecord reco rd
w word;
c : char;
d : byte;
end;
TypeGra nd Record record
r : TypePetltRecord;
suivant: ATypePetltRecord;
end;
const
Entler : lnteger = 4096;
Str : strlng = 'Exemple de paramètre cha'ne de caractères';
Carac : char = 'A';
PetHRecord : TypePetitRecord = (w : SFFFF; c : 'X'; d : SFFl;
var
GrandRecord TypeGrandRecord;
begin
writeln;
writeln('Pile ' + copy(t, 1. 70)+' :'l;
writeln;
write(' ');
for l := 1 to 8 do write(1 :5):
l ;= 0:
while l < max do
begin
if (1 mod 8l 0
then begin
writeln;
write(PointeurHexa(pl,' ');
end;
write (MotHex (word (pA) ), ' ');
p := Ptr(seg(pA), ofs(p Al+2l;
inc(1l;
end;
writeln;
write(' ');
for l := 1 to 8 do write(l :5l;
end;
begin
clrscr;
{afficher les adresses et les valeurs des paramètres passés
à la procédure SansNom}
writeln('Variable Adresse Valeur');
writeln;
writeln( 'Entier ',PointeurHexa(@Entier),' Entier);
writeln('Str ',PointeurHexa(@Str), Slr);
writeln('Carac ',PointeurHexa(@Caracl, Carac);
with PetitRecord do
wri tel n( 'Peti tRecord ',Poi nteurHexa(@PetitRecord),' w, ' c, ' dl;
writeln('GrandRecord ',PointeurHexa(@GrandRecord));
1 2 3 4 5 6 7 8
2AFB: FE06 FFEE 05BF 0156 2ABF FFFF FF58 0041 0004
2AFB:FEE6 2ABF 1000 0002 2ABF 3209 4241 3A46 3130
1 2 3 4 5 6 7 8
PUSH BP
MDV BP,SP
- la récupération des paramètres éventuels, à partir de BP+4 pour une routine NEAR, à
partir de BP+6 pour une routine FAR, en respectant les règles de passage de paramètres;
- une sortie de routine de la forme:
MDV SP,BP
POP BP
RET taille totale des paramètres
De plus:
- toutes les procédures et fonctions déclarées dans la routine en assembleur doivent
appartenir au segment CODE et toutes les variables privées de cette routines doivent
Le but de l'unité présentée ci-dessous comme exemple est de fournir une fonction pour
transformer une chaîne de caractères en majuscules, en prenant en compte les minuscules
accentuées, ce que ne ferait pas la fonction UpCase disponible dans le Turbo-Pascal. Ceci peut
servir pour trier des variables de type string et éviter que, par exemple dans le cas d'une
table de pays, Pérou ne se retrouve placé après Philippines (ce qui serait le cas en respectant
l'ordre ASCII pour lequel "é" est placé derrière toutes les lettres non accentuées).
On peut consulter ci-dessous le listing de la routine en assembleur Majuscules et le
listing de l'unité Majuscul qui l'utilise.
;{------------------------·-·-cours de Pascal-Avancé-----·------------------···-}
;j }
;1 exemple de routine en assembleur }
;{ (disponible dans le fichier majuscul.asm) }
;{-----_._ .. _--_. __ ... _-------_ ... __ ._--_._---_ .... _--------_. __ ._--------------}
fi n:
pop ds
pop bp
ret 4
code ends
end
{----------------------------cours de Pascal-Avancé----------------------------I
{ 1
{ exemple d'unité utilisant une routine écrite en assembleur 1
{ (disponible dans le fichier majuscul.pas) 1
(------------------------------------------------------------------------------1
uni t majuscul;
interface
implementation
($L MAJUSCUL.OBJI
function Majuscules(s string) stri ng: external;
end.
Il est possible, en Turbo-Pascal, de définir et installer ses propres interruptions pour les
substituer à celles du BIaS ou du DOS.
Une procédure destinée à être installée comme interruption doit être déclarée comme
Le contenu de ln procédure peut être défini comme pour une procédure normale.
Toutefois, dans le cas d'une interruption "hard" (interruption exécutée automatiquement par
le système opérationnel), les routines d'entrée/sortie et d'allocation de mémoire du Turbo-
Pascal ne peuvent pas être utilisées, ni les fonctions du DOS, car elles ne sont pas
réentrantes. A leur place, on doit utiliser les interruptions du BIOS,
Pour installer une interruption, utiliser les procédures GetIntVec pour sauver l'adresse
originale de l'interruption et SetIntVec pour installer la nouvelle ou réinstaller l'ancienne. La
syntaxe est:
GetlntVec<Numlnt, pl;
SetlntVec<Numlnt, q)
où Numlnt est un octet qui contient le numéro de l'interruption à traiter, p est une variable
de type pointer, pour mémoriser l'ancienne adresse, et q est l'adresse de la procédure qui va
devenir la nouvelle interruption (voir exemple ci-dessous).
{----------·--------------.-.cours de Pascal-Avancé··--·····-----------------··}
{ }
{ exemple d'installation d'interruption }
{ <disponible dans le fichier temps.pas) }
{ _ _._-_._--_._---------_._-_ .. _._ _------------------}
unit Temps;
interface
uses crt.dos;
const
{position et attri buts pour affi cher l' heure}
LigHeur byte = 3;
Col Heur byte = 72;
AttHeur byte = 0;
TimeOn boolean = false;
var
{adresse où débute la mémoire vidéo et taille d'une ligne}
ScrAddr,
TailLig: word;
implementation
var
Savlntlc : pointer;
const
{$F+ }
procedure Intlc(flags. cs. ip. ax, bx. cx, dx. si. di, ds. es. bp word);interrupt;
begin
{récupère l'heure directement en mémoire}
Heure := MemW[$0040:$006e]:
Interv := round«MemW[$0040:$006c]) . . 1.0 1 18.Z);{Heure en secondes}
1f Anclnterv <> Interv
then begin {si l'heure a changé d'au moins une seconde, afficher a l'écran}
Anclnterv :- Interv;
{calcul de l'heure}
Minutes := Interv div 60:
Secondes := Interv mod 60:
str(Heure+100:3,AuxStr);
StrHeur := copy(AuxStr.Z,Z)+':':
str(Minutes+100:3,AuxStr);
StrHeur := StrHeur + copy(AuxStr.Z.Z)+·:·:
str(Secondes . . +100:3.AuxStr);
StrHeur := StrHeur + copy(AuxStr.Z.Z);
procedure Tempson:
{installe la nouvelle 1nterrupt1on 1cH}
beg1n
if not Timeon
then beg1n
GetlntVec($1c, Savlnt1c);
in11ne(Sfa):
SetlntVec($1c, @Int1c);
inline($fb);
Timeon .- true;
end:
end;
procedure Tempsoff:
{restaure le vecteur 1nitial}
begin
if Timeon
then begin
inline(Sfa):
SetlntVec($1c, Savlnt1c);
1nline(Sfbl:
Timeon .- false;
end;
end:
end.
{----------------------------cours de Pascal-Avancé--------··---------·--------}
{ }
{ exemple d'interruption }
{ (disponible dans le fichier afftemps.pasl }
{--_._----------_._---_ .. _-------_._-----------------------------------_._-----}
uses crt, Temps;
var
i. j : byte;
SaveExit : pointer;
begin
cl rscr;
i ; = 1;
j ;= 1;
TempsOn;
{corps du programme}
Randomize;
repeat
gotoxy(;. j); cl reol;
i ;= Random(42)+1;
j := Random(22)+4;
gotoxy (i, j l ;
write('Appuyez sur une touche quelconque pour arrêter l"horloge');delay(100l;
unti 1 keypressed;
TempsOff;
end.
Enfin, pour être complet, l'exemple ci-dessus aurait besoin d'être amélioré pour prévenir
une sortie anormale du programme (Break ou erreur fatale). Il faudrait pour cela que le
programme installe sa propre routine de sortie en cas d'erreur.
Les problèmes causés par le manque d'espace sur disque ou disquette ne seront pas
abordés dans ce chapitre, dans la mesure où nous les considérons évidents à résoudre.
L'utilisation et le développement de logiciels complexes n'a pas lieu d'être sans l'utilisation de
disques durs, aussi bien pour l'implantation des logiciels utilisés pour le développement que
pour l'implantation des fichiers-sources, des exécutables et des fichiers de tests du système
développé. Par conséquent, nous considérons comme une condition minimum l'utilisation d'un
disque dur de capacité suffisante.
La plupart des autres problèmes rencontrés sont liés à la disponibilité de mémoire
pendant l'exécution du logiciel développé:
- la taille maximum du segment de données est de 64 K octets, pour un programme
exécutable créé par le Turbo-Pascal;
- la taille maximum du segment de code d'un programme ou d'une unité ne peut
dépasser 64 Koctets; mais la taille totale du code exécutable (programme + unité) n'est pas
limitée, sinon par la taille de la mémoire disponible;
- la taille maximum du segment de la pile est également de 64 K octets pour un
programme exécutable;
- la taille maximum du tas, jointe à celle de la table des fragments, est limitée à la
mémoire disponible;
- la taille de la mémoire accessible au DOS est de 640 Koctets;
- la taille de la table de réadressage du fichier .EXE ne peut dépasser 64 K octets, c'est
à dire 16384 éléments.
Nous allons détailler chacune de ces limitations dans les paragraphes suivants, et définir
des recommandations pour les contourner.
Il s'agit du cas de l'erreur 49 Data segment 100 large. Pour résoudre ce problème, il
suffit de déclarer les variables structurées de plus grande taille comme étant des variables
dynamiques et les allouer sur le tas en utilisant les procédures New ou GetMem (voir chapitre
II de ce manuel). Ainsi, seul le pointeur (4 octets) sera stocké dans le segment de données; en
contrepartie, l'espace disponible sur le tas devra être suffisant pour allouer la variable.
Il provoque une erreur 96 Too many variables, quand l'espace total occupé par les
variables locales d'une routine dépasse 65520 octets. L'espace total doit être diminué, ou
alors la routine doit être divisée en plusieurs routines où les variables seront redistribuées.
L'utilisation de variables dynamiques est également une bonne solution: seul le pointeur (4
octets) est alors conservé sur la pile, la variable étant allouée sur le tas.
Si l'erreur se produit dans un module qui a été compilé avec la directive $S+, un
message 202 Stack overflow error est produit et l'exécution du programme est définitivement
stoppée. Si l'erreur se produit dans un module compilé avec la directive $S-, aucun message
ne sera émis et les conséquences sont imprévisibles et vont en général jusqu'au blocage du
clavier de l'ordinateur.
Le risque est plus grand quand une procédure utilise beaucoup de variables locales ou
en cas de récursivité.
Le programme ci-dessous (recurs1) qui calcule la fonction factorielle d'un entier (n) ne
fonctionne plus dès que n dépasse une certaine valeur. A cause du fait que la fonction
Factorielle définie est appelée récursivement n-1 fois et qu'à chaque fois le résultat est gardé
sur la pile. La taille de la pile étant limitée à 16384 octets, l'erreur 202 se produit pour n plus
grand que 792 (si on compile le programme avec $F+, l'erreur se produit pour n supérieur à
719, car l'adresse de retour occupe deux octets de plus à chaque appel).
I-------------------·-··-----cours de Pascal-Avancé---------··-----------------}
1 }
1 exemple de récursivité }
1 (disponible dans le fichier recurs1.pas) }
1-----------_·_····_---------------------_·_·_--------.---------------.--------}
I$N+.E+.S+}
I$M 16384. O. 655360}
program recurs;
va r
n : word;
begin
n := Maxlnt;
while n <> 0 do
begin
write( 'entrer la valeur positive de n (0 pour terminer) ');
readln(n);
if n <> 0
then writeln(' n! Factorielle(n));
end;
end.
j$N+,E+.S+}
j$M 1024, 0, 655360}
Prog ram recu rs2:
va r
n : word;
begin
n := Maxlnt:
while n <> 0 do
begin
wrlte('entrer la valeur positive de n (0 pour terminer) '):
readln(n):
if n <> 0
then writeln(' n! Factorielle(n));
end:
end.
program parval;
j$S+,M 1024, 0, 655360} {pile limitée a 1024 octets}
type
Ecran = array[1 .. 25J 25 lignes de
of array[1 .. S0J of word; SO colonnes
var
Zone1 : Ecran;
begin
UtiliseEcran(Zone1);
end.
Cette erreur se traduit par le message 1 Out of memory et peut être évitée de plusieurs
manières:
- vérifier tou t d'abord que la compilation génère le code exécutable dans la mémoire
(item Destination du sous-menu Compilation) ; si c'est le cas, faire la modification nécessaire
pour générer le code exécutable sur disque;
- vérifier que le tampon de l'édition de liens a été configuré pour être placé sur le
disque (Sous-item Link Bu/Ter de l'item Compiler du menu Options) ; si c'est le cas, utiliser la
directive /L pour placer le tampon de l'édition de liens sur le disque, ce qui libèrera la
mémoire qu'il occupait ;
- si l'ordinateur dispose d'une mémoire EMS, utiliser le programme TINST.EXE
d'installation pour vérifier qu'il autorise l'utilisation de la mémoire EMS pour stocker le
tampon de l'éditeur du programme source;
- retirer, de la mémoire, les programmes résidents (comme SideKick) ;
- diminuer éventuellement la valeur du paramètre BUFFERS du fichier CONFIG.SYS,
s'il est très grand (supérieur à 20) ; trop le diminuer pénaliserait la vitesse des opérations
d'entrée/sortie avec le disque;
- compiler avec les directives $S- et $R-, ce qui entraîne quelques inconvénients: il n'y
aura plus de contrôle de l'espace disponible sur la pile, ni des limites des indices de matrices,
Dans l'environnement du Turbo.exe, cette situation se traduit par une erreur 108 Not
enough memory to run the program. Le plus simple est alors de générer le code exécutable
sur le disque (fichier .EXE) et sortir de l'environnement intégré pour exécuter le fichier .EXE.
Toutefois, dans certains cas, il est indispensable d'exécuter le programme dans
l'environnement intégré (pour utiliser le Debugger, par exemple). On peut alors tenter de
résoudre le problème en appliquant les recettes du paragraphe 5.1 pour diminuer l'occupation
de mémoire.
Si l'erreur persiste ou si l'erreur Insufficient memory ou équivalente, apparaît (dans le
cas d'un fichier .EXE), la seule solution est l'utilisation d'overlays (voir chapitre 1). Si le
programme a été bien structuré dès le début, la transformation des unités principales en
overlays ne doit pas poser de problèmes.
Puisque la mémoire disponible pour être utilisée comme tas est la différence entre la
mémoire totale et la mémoire occupée par le programme chargé, il est évident que les causes
identifiées au paragraphe 5.2, et les solutions envisagées, restent valables pour le cas de
mémoire dynamique insuffisante.
Toutefois, d'autres précautions peuvent être prises. Bien qu'elles s'appliquent également
pour résoudre le manque de mémoire totale, elles sont exposées ici car elles sont liées à la
gestion du tas.
Le message Insufficient memory peut se produire, à cause du manque de place sur le
tas, lors du chargement du programme, si la valeur minimale du tas est supérieure à 0
(second paramètre de la directive $M). Vérifier que la valeur indiquée est correcte.
Nous avons déjà vu au chapitre II comment contrôler le manque de place sur le tas en
phase d'exécution. Il est très important de contrôler ces erreurs de manière à éviter
l'abandon du programme: dans certains cas, l'impossibilité d'ouvrir une fenêtre par manque
de mémoire peut être une péripétie sans importance; il suffit d'en avertir l'utilisateur et de
continuer le traitement. Dans tous les cas, on doit chercher à :
- optimiser la taille des variables dynamiques à allouer dans le programme et les
unités; un cas édifiant est la valeur du paramètre PageSize de Turbo-Access : la valeur par
défaut, calculée par TaBuild, correspond à une utilisation de 64 K octets de mémoire mais, en
diminuant la valeur de PageSize, on peut réduire de façon significative cet espace, ce qui peut
permettre de dépasser la limite de mémoire; il est évident, qu'en contrepartie, la fréquence
des accès au disque sera augmentée;
- libérer les variables allouées, dès qu'elles n'ont plus d'utilité; ce qui augmentera
l'espace disponible pour l'allocation d'autres variables.
L'erreur provoquée par cette limite se produit pendant l'édition de liens. Il s'agit de
l'erreur 107 Too many relocations items: trop d'éléments dans la table de réadressage. Pour
la comprendre. il est nécessaire d'examiner la structure d'un fichier d'extension .EXE qui est
divisé en deux parties distinctes:
- un en-tête qui contient des informations de contrôle qui permettront de charger en
mémoire le programme exécutable;
- le code exécutable lui-même et les variables initialisées du programme.
L'en-tête contient une partie de taille fixe (28 octets) qu'il est inutile de détailler ici et
une partie de taille variable, que nous appellerons table de réadressage, où sont stockées les
informations nécessaires à l'ajustement des adresses de certains éléments du programme
(variables ou instructions). Cet ajustement est nécessaire car l'édition de liens prépare le
programme comme si celui-ci était chargé au déplacement 0 du segment O. Quand on
l'exécute, le programme n'est jamais chargé dans le segment 0, mais dans la mémoire
disponible derrière le DOS et les éventuels programmes résidents, ce qui oblige à recalculer
les adresses dans la mémoire, en accord avec la table de réadressage.
Chaque élément de cette table est constitué de deux mots, le premier est l'adresse de
l'élément (variable ou instruction) à l'intérieur de son propre segment, le second est le
déplacement entre le segment de l'élément et le segment où commence le programme. Pour
information, nous allons expliquer le fonctionnement de la table de réadressage, en utilisant
l'exemple du programme PARAM.EXE du chapitre V.
Le listing en hexadécimal ci-dessous représente l'en-tête du programme PARAM.EXE, le
début de la table de réadressage et le début du code exécutable. Les informations qui nous
intéressent ont été soulignées:
- 58 00 représente le nombre d'éléments de la table de réadressage (dans ce cas
0058h = 88 éléments) ;
- 18 00 sert à déterminer l'adresse où commence le code exécutable, à l'intérieur du
fichier .EXE, car la valeur représente, en nombre de paragraphes de 16 octets, la taille de
l'en-tête et de la table de réadressage ; dans ce cas la valeur est 0018h, ce qui signifie que le
programme commence à l'adresse 180h du fichier .EXE. ;
- 1C 00 donne l'adresse de la table de réadressage à l'intérieur du fichier .EXE, dans ce
cas OOlCh ;
- 86 00 00 00, octets placés à partir de l'adresse 001Ch, constituent donc le premier
élément de la table de réadressage (0000:0086h) ;
- BD 00 représente OOBDh, valeur contenue à l'adresse qui correspond au premier
élément de la table (car 0000:0086 + 0180h = 0206h).
0000 40 5A 40 01 00 00 ~n 00 18 00 26 04 26 A4 A2 01
0010 00 40 00 00 9C 03 00 00 1C 00 00 00 86 00 00 00
0180 55 89 E5 C4 7E 06 26 C6 05 04 8A 46 05 30 E4 B9
0200 00 DE 57 9A 02 03 BD 00 80 BE 00 FE 16 57 C4 7E
-r
AX=OOOO BX=OOOO CX=1740 OX=OOOO SP=4000 BP=OOOO SI=OOOO 01=0000
OS=247C ES=247C SS=262E CS=248C IP=039C NV UP El PL NZ NA PO NC
-u 82
248C:0082 57 PUSH 01
248C:0083 9A02034925 CALL 2549:0302
Nous avons constaté que l'utilisation d'overLays permet de réduire de façon significative
le nombre d'éléments de la table de réadressage. Nous avons rencontré un exemple où la
transformation en overLays de 8 unités diminue la table de 10280 à 3461 éléments! Grâce à
l'utilisation d'overlays, une partie des réadressages sont transférés vers le fichier d'extension
.OVR.
Un autre facteur important est le nombre d'appel à des procédures ou à des fonctions
d'une unité vers une autre. Si plusieurs unités utilisent souvent une petite routine
implémentée dans une autre unité (ce qui va entraîner un réadressage à chaque appel), il
peut être intéressant de répéter cette petite routine dans la partie d'implémentation de
chaque unité. Il est évident que cette méthode ne peut (et ne doit) être utilisée que dans des
cas très spéciaux: il serait très désavantageux de la généraliser car elle est contraire à
l'esprit des unités.
Graphiques-unité Graph
1.1 Equipement.
Le code exécutable qui trace des graphiques en Turbo-Pascal pourra être exécuté sur
différents types de terminaux: écran CGA, EGA, VGA, Hercules, AT&T 400, PC3270 et
IBM8514/A. Ceci grâce à l'utilisation, en temps d'exécution, de pilotes appropriés, nommés
fichiers .BGI par Borland (Borland Graphies Interface).
La routine InitGraph permet de détecter automatiquement quel est le type de terminal
et quel est la meilleure résolution possible sur ce terminal. Ce qui permet d'identifier quel est
le fichier d'extension .BGI correspondant. Celui-ci sera alors chargé sur le tas. L'en-tête de la
routine est:
où, le plus fréquemment, Pilote doit être initialisé à la valeur Detect (constante de valeur
nulle) et Chemin doit contenir le nom du répertoire où se trouvent les fichiers .BGI. Après
l'exécution de la routine, Driver et Mode reviennent avec les valeurs adaptées au type de
matériel utilisé. Dans certains cas, l'utilisateur peut forcer le type de pilote et le mode
graphique, en initialisant les deux paramètres avant l'appel de la routine.
Dans tous les cas, le fichier correspondant d'extension .BGI doit être accessible dans le
répertoire Chemin ou dans le répertoire courant si Chemin est une chaine vide.
La procédure CloseGraph, sans paramètre, permet de sortir du mode graphique et
retourner au mode antérieur (généralement mode texte). Elle retire le pilote de la mémoire.
La procédure RestoreCrtMode permet de revenir au mode texte sans retirer le pilote de
la mémoire.
La procédure:
const
jpilotes}
CGA 1; IBM8514 = 6;
MCGA = 2; HercMono = 7;
1.2 Couleurs.
const
Black 0; {noi r} LightBlue 9; {bleu cl ai r}
Blue 1 : {bleu} Li ghtGreen ID; {vert cl ai r}
Green 2 ; {vert} LightCyan Il; { cyan cl air}
Cyan '" 3; {cyan} Li ghtRed 12 : {rouge clair}
Red = 4; {rouge} Li ghtMagenta 13: {magenta cl ai r}
Magenta 5 ; {magenta} Yellow 14; {jaune}
Brown '" 6; {marron} White 15; lblanc}
li ghtGray '" 7: {gris clair}
DarkGray '" 8: {gris foncé} MaxColors 15; {nombre maximum de coul eurs}
Pour n'importe lequel des terminaux énumérés plus haut, on attribue au coin supérieur
gauche les coordonnées (0,0). Les abscisses croissent de la gauche vers la droite, les
ordonnées croissent du haut vers le bas. Le nombre de lignes ou de colonnes, en mode
graphique, dépend du matériel et de la résolution utilisée. Dans le cas du mode CGA-haute
résolution par exemple, il existe 640 colonnes, numérotées de 0 à 639, et 200 lignes
numérotées de 0 à 199, ce que l'on a coutume de résumer comme étant une résolution
640x200. Le mode Hercules a une résolution de 720x348. L'intersection de chaque ligne avec
chaque colonne est appelée point ou pixel (abréviation de l'anglais picture element). En mode
CGA-haute résolution, il existe 128000 Pixels (640 fois 200). Le schéma ci-dessous montre les
coordonnées des quatres coins d'un écran CGA-haute résolution.
(0.0) r - - - - - - - - - - - - - - - - - - - - - - , (639.0)
On utilise également la notion de pointeur courant qui pointe vers la position d'un
curseur graphique, semblable à un curseur en mode texte, mais invisible. Le pointeur courant
peut se représenter par un couple (x, y) de coordonnées. Plusieurs procédures et fonctions
liées au mode graphique manipulent le pointeur courant: par exemple MoveTo(159, 99) place
le curseur au milieu d'un écran CGA en mode haute-résolution.
lA Textes.
Deux types de polices de caractères sont disponibles pour écrire des textes sur un écran
en mode graphique:
- une police de caractères dessinés avec une matrice de 8x8 pixels;
- plusieurs polices de caractères vectorielles.
Le premier est suffisant pour écrire des caractères de petite taille. Les autres devront
être préférés pour écrire des caractères de taille plus grande car, même une fois agrandi, le
caractère garde une bonne résolution, ce qui n'est pas le cas avec les matrices de pixels. On
pourra exécuter le programme ORSTOM.PAS, disponible sur la disquette, pour le vérifier.
Les fichiers d'extension .CIIR contiennent les polices vectorielles et doivent être
accessibles pendant l'exécution du programme.
Les routines SctTcxtJustify, SetUscrCharSizc, SetTextStyle, OutTcxt, OutTextXY et
GetTextScttings manipulent les textes et les caractéristiques des caractères.
const
{polices de caractéresl
DefaultFont 0; {police matricielle}
TriplexFont = 1; {police vectorielle (fichier TRIP.CHRI}
SmallFont = 2: {police vectorielle (fichier LITT.CHRI}
SansSerifFont = 3; {police vectorielle (fichier S/lNS.CIJRl)
GothicFont = 4: {police vectorielle (fichier GOTII.CIIRl}
l sens pou r l' affi chage des textes}
HorizDi r = 0; {sens horizontal}
VertOir = 1: {sens vertical}
{taille des caractères}
NormSize = 1; {taille normale}
{type de justification horizontale}
LeftText 0: {justification il gauche}
CenterText = 1; {texte centré}
RightText = 2; {justification il droite}
{type de justification verticale}
BottomText 0; {justification en dessous du point}
CenterText 1; {texte centré}
TopText 2: {justification au dessus du point}
type
{type de caractéristiques de texte}
TextSettingsType = record
Font. {police}
Direction word: {sens}
CharSize word; {tai 11 e}
Horiz. {justification horizontale}
Vert word; {justification verticale}
end;
De nombreuses routines peuvent être utilisées pour tracer des lignes, des cercles, des
ellipses, des diagrammes de barre, des "camemberts", des polygones et les remplir avec des
hachures ou d'autres motifs.
Les types de lignes ou de hachures peuvent être choisis ou même définis par le
programme. On utilise pour cela les routines SetLineStyle, SetFillStyle et SetFloodPattern
avant d'appeler les routines qui remplissent les zones (FillPoly et FloodFill).
Les objets suivants, liés aux types de tracés, sont pré-définis:
const
{types de lignes}
SolidLn 0; {trait continu}
DottedLn = 1; {trait pointillé}
CenterLn 2; {..... }
DashedLn = 3: {ti retés}
UserBi Un = 4; {défi ni pa r 1e programmeur}
{épaisseur du trait}
NormWidth = 1: {trait normal}
ThickWidth= 3; {trait large}
const
{motif de remplissagel
EmptyFill 0: {remplit avec la couleur du fond de l'écran)
SolidFill 1: {couleur unie)
LineFill 2 ; {ti retets horizontauxl
LtSl ashFi 11 3; {III 1 avec un trait fi n 1
SlashFill 4' {III 1 avec un trait épais}
BkSlashFill 5 : {\ \ \ \ avec un trait épaisl
LtBkSl ashFi 11 6; {\ \ \ \ avec un trait fi ni
HatchFill '" 7; {hachures fines)
XHatchFill 8; {hachures épaissesl
InterLeaveFill 9; {hachures croiséesl
WideDotFill ID; {pointillé espacé)
CloseDotFill '" 11; {pointillé peu espacé)
UserFi 11 12; {défi ni par 1e programmeurl
type
FillSettingsType record
Pattern word; {type de remplissage)
Color word; {coul eur)
end;
{type qui définit un type de remplissage)
FillPatternType = array[l, .8] of byte;
type
Vi ewPortType record
xl. yI. x2, y2 integer; {coins de la fenêtre}
Cl i P boolean; {contrôle des limites}
end;
Divers modes graphiques permettent de travailler avec plusieurs pages graphiques, une
de ces pages (la page active) étant visible à l'écran alors que les autres sont stockées en
mémoire. Des routines permettent de manipuler ces pages, pour réaliser une animation par
exemple.
const
NormalPut 0; {utilise l'instruction MDV de l'ùssembleur}
XORPut 1 ; {utilise l'instruction XOR de l' assembl eur}
ORPut 2 ; {utilise l'instruction OR de l' assembl eur}
AND Put 3; {utilise l'instruction AND de l'assembleur}
NOT Put = 4; {utilise l'i nstruct ion NOT de l' assembl eur}
{-------------·----------·---cours de Pasca1-Avanc6---------------------·---··-f
{ }
{ exemp1 e de di agramme de barres }
{ (disponible dans le fichier grafc1mO.pas) }
{----------_._---_._-----------------_._ .... _._ ... _----------------------------}
{SV -}
uses
crt,
graph,
grafc1m1:
const
NomMois array[l .. 12] of stri ng[3] =
('Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jun',
'Ju1', 'Aou', 'Sep', 'Oct', 'Nov'. 'Oec'):
var
TabMens TipoTabMens:
Station string[13]:
Annee ; word;
Mois, Jour; byte;
HautBarre. BarGauche : rea1:
xe, xd, PixAxeX, PixAxeY,
PixMaxiX, PixMaxiY word;
PixOe1taY, ValOe1ta, PixOeltaX,
MaxiMois, t, v, y, w ; rea 1;
Total : array[l .. 12] of rea1;
Pilote, Mode, i, NombGrad : integer;
s : string;
begin
if FichierOuvert(Station. Annee)
then begin
{affichage du litre}
str(Annee, s);
s := 'Pluie mensuelle - station' + Station + ' - Annee' + s;
SetTextStyle(SansSerifFont, HorizDir. 1);
SetUserCha rSl zeO, 2, 1. 2);
SetTextJustlfy(CenterText, CenterText);
OutTextXY(GetHaxX div 2, round(GetMaxY*O.05). s);
end;
while not keypressed do;
RestoreCrtMode;
end.
4 Exercices.
Les trois exercices ont pour but de compléter et généraliser l'exemple présenté au
chapitre 3, Le programme GRAFCLM.PAS disponible sur la disquette présente un menu avec
trois possibilités:
- graphique annuel d'une variable quelconque du fichier de climatologie;
- graphique de la température maximum et de la température minimum sur le même
diagramme;
- diagramme de barres de la pluie mensuelle.
Le travail à réaliser est le suivant:
Pour accéder aux données, utiliser l'unité GRAFCLMI dont la partie d'interface est
décrite ci-dessous:
type
TypeTabMens • arrily[l .. 31] of reill; {lilbleilu des villeurs mensuelles d'une vilriable
quelconque}
TypeTabAnual = array[l. .366] of real; {tableau des valeurs annuelles d'une variable
quelconque}
procedure FermerFichier;
{ferme le fichier de climatologie}
1 Compilation conditionnelle.
I$DEFINE Francais}
Les processeurs de la famille 80x86 manipulent aisément les nombres entiers, c'est à
dire les variables de type byte, slwrtint, integer, word et Zongint. Mais ils ne sont pas conçus
pour manipuler les nombres réels comme, par exemple, les variables de type reaZ en Turbo-
Pascal. C'est pourquoi il existe en parallèle la famille des processeurs 80x87 appelés co-
processeurs arithmétiques, capables d'exécuter très rapidement des opérations sur les
nombres réels. La grande majorité des PC ou compatibles peuvent être équipés d'un co-
processeur correspondant au processeur original (8087 pour un 8086, 80287 pour un 80286,
etc...).
Le Turbo-Pascal a toujours permis l'utilisation de réels, en utilisant le 80x86 par
l'intermédiaire des routines de sa bibliothèque. A partir de la version 4.0, il est devenu
possible d'utiliser directement le co-processeur, en compilant les programmes avec la directive
$N+, avec toutefois un inconvénient: un programme compilé pour utiliser le 80x87 ne pouvait
pas fonctionner sur un micro-ordinateur non équipé du co-processeur. Heureusement, depuis
la version 5.0, il est possible de compiler un programme de telle façon qu'il utilise le co-
processeur, s'il est installé, ou qu'il l'émule dans le cas contraire. Dans ce dernier cas, il est
évident que la rapidité du co-processeur ne pourra pas être exploitée, puisque les calculs
seront faits par des routines utilisant le BOx86.
Sans utilisation du co-processeur, le seule type de variable réelle disponible est le type
real. En utilisant le co-processeur, nous disposons des types single (équivalent du type real),
double, extended et comp. Il faut savoir que le type real occupe 6 octets alors que le type
single occupe 4 octets, ce qui implique qu'une variable réelle écrite dans un fichier créé par
un programme qui n'utilise pas le 80x87 ne pourra pas être relue par le même programme (ou
un autre) qui utilise le 80x87. Ce qui était une limitation de la version 4.0.
En pratique. depuis la version 5.0, si le système à développer comporte des variables
réelles, on a tout intérêt à placer au début de chaque module les directives {$N+,E+} et
utiliser seulement les types single, double, extended et camp. Cette méthode présente à la fois
des avantages:
- une seule version du programme-source et du programme-exécutable, avec ou sans
co-processeur;
- les fichiers de réels créés sans le co-processeur pourront être utilisés avec, et
inversement;
- toutes les possibilités offertes par le co-processeur seront utilisées, lorsqu'il est
installé;
et des inconvénients:
- à cause de l'émulation, le fichier .EXE sera légèrement plus important, puisqu'il
contiendra les routines d'émulation;
- en l'absence du co-processeur, les calculs qui utilisent des variables de type doubw,
extended ou romp pourront être plus lents que s'ils étaient réalisés avec des variables de type
real; en contrepartie, ils seront aussi plus précis.
Le tableau ci-dessous regroupe les temps d'exécution du programme P8087, sur
différentes configurations du même ordinateur:
Ceci peut servir pour vérifier qu'un programme développé sur un ordinateur équipé de
co-processeur fonctionne également sans co-processeur.
Enfin, la variable Tcst8087 déclarée dans l'unité SYSTEM peut être testée à l'intérieur
d'un programme pour connaître la configuration. Elle peut prendre les valeurs 0, 1, 2, 3 pour
indiquer qu'il n'y a pas de co-processeur ou qu'il y a un 8087, un 80287 ou un 80387,
respectivement.
Nous allons nous intéresser, dans ce chapitre, aux erreurs qui se produisent lors de
l'exécution d'un programme. Nous les classerons en erreurs de deux types: celles qui sont
accompagnées d'un message d'erreur (voir annexe) et celles qui provoquent un arrêt du
programme ou du système, sans message ou avec un message sans signification.
En consultant la liste des messages d'erreurs, il est facile d'interpréter le numéro nnn. Il
reste ensuite à déterminer la ligne du programme où l'erreur s'est produite.
Si le programme est relativement petit, le plus efficace est de le compiler et l'exécuter
dans l'environnement intégré, avec la directive $0+ ; ainsi la ligne où se produit l'erreur sera
localisée automatiquement. Une autre solution est de compiler, avec TPC.EXE, le programme
et toutes les unités qu'il utilise avec la directive IF pour indiquer l'adresse de l'erreur et la
directive $0 pour générer les informations nécessaires à la recherche de la position de
l'erreur:
L'adresse indiquée dans le message d'erreur doit obligatoirement être placée derrière IF,
sans introduire d'espace. lB oblige à compiler toutes les unités dont le source est accessible.
1$0+ oblige à créer les informations pour le débogueur, qui seront mémorisées dans les
fichiers. TPU et dans le fichier .EXE.
S'il est possible de trouver l'endroit où se produit l'erreur, la ligne correspondante sera
montrée à l'écran.
Si le programme est trop grand (beaucoup d'unités, message Out of memory pendant la
compilation avec $0+) ou si la méthode précédente s'est terminée par le message Target
adress not round, il vaut mieux tout d'abord détecter dans quel module l'erreur s'est
produite. Pour ce faire:
- compiler le programme avec l'option IGS pour créer un fichier .MAP avec tous les
segments utilisés dans le programme;
- consulter alors le fichier .MAP pour trouver l'unité qui correspond au segment
indiqué dans le message d'erreur;
{--·------·-----------··-----cours de Pascal·Avancé-·--·-------··--------------}
{ }
{ exemple de recherche d'erreur d'exécution 1
{ (disponible dans le fichier uniterr.pas) 1
{---- ... --------- ... -------.--------- .... -----.---.-.---··--------··--------··-1
unit uniterr:
interface
function Factorielle(n word) longint;
implementation
function Factorielle(n word) longint;
begin
if n = o
th en Factori ell e := 1
else Factorielle .- round(Factorielle(n-1) * n);
end:
end.
{---·-------·-----·--··------cours de Pascal·Avancé---------------·------------1
{ 1
{ exemple de recherche d'erreur d'exécution }
{ (disponible dans le fichier progerr.pas) 1
{.-------.------- .. ------- .... -------.------.-.- .. -.-----··--------------------1
uses uniterr;
begin
write(Factorielle(170»;
end.
Dans la première colonne, sont indiquées les adresses absolues du début de chaque
segment: ce qui permet de voir que, à l'adresse absolue 00040h (qui correspond à une valeur
de segment de 0004h), commence l'unité UNITERR.
pour générer un autre fichier .MAP où, en plus des informations déjà exammees plus haut,
nous trouvons également la liste des lignes de UNITERR et les adresses correspondantes:
Il devient alors facile d'identifier la ligne 9 de l'unité UnitErr comme étant celle où
l'erreur s'est produite, puisqu'elle commence en 0004:0020 et que la ligne 10 commence en
0004:0047.
Il s'agit des erreurs qui bloquent la clavier, obligeant à éteindre et rallumer l'ordinateur
ou tout au moins à réinitialiser le système opérationnel (Ctrl+Alt+Del).
Dans cette situation, vérifiez tout d'abord que la compilation des modules a été faite
avec les directives $S+, pour contrôler si la pile est suffisante, et $R+ pour contrôler les
limites des indices. Dans le doute, recompiler (le programme entier ou une unité en
particulier) avec les directives $S+,R+ et exécuter le programme à nouveau pour localiser
l'erreur éventuelle.
Si la méthode précédente ne donne pas de résultat, cela devient plus laborieux, la seule
alternative restant l'utilisation du débogueur (en plus, évidemment, d'une vérification de la
logique du programme par le programmeur).
Si le programme est suffisamment court, il suffit d'utiliser le débogueur de
l'environnement intégré. Dans le cas contraire, Turbo-Debugger, autre produit de Borland,
permet de déboguer un programme exécutable.
Il est difficile de décrire par écrit le débogage en utilisant ces produits, étant donné le
caractère interactif du procédé. C'est pourquoi nous nous contenterons d'énumérer ci-dessous
les principales touches de fonctions qui peuvent être utilisées; pour obtenir des informations
complémentaires et la signification d'autres touches, consulter le manuel du Turbo-Pascal et
du Turbo-Debugger.
- F4 exécute le programme jusqu'à la ligne où se trouve le curseur; utile pour arriver
rapidement à la partie du programme à déboguer.
- F7 exécute pas-à-pas chaque instruction; quand on rencontre un appel de routine,
elle permet de continuer à l'intérieur de la routine, toujours en exécutant pas-à-pas ; quand
on l'utilise pour commencer une exécution, le curseur est placé sur la directive begin de la
première partie d'initialisation d'unité ou du programme principal.
- F8 exécute également pas-à-pas mais, quand on rencontre un appel de routine, celle-
ci est exécutée en une seule fois; elle peut également être utilisée pour commencer
l'exécution.
- Ait F5 visualise l'écran correspondant au résultat de l'exécution du programme, sans
sortir du programme.
4 Son.
5 Exercice.
Transformez votre clavier en piano (jouet). De telle mamere qu'une ligne de touches
représente les touches blanches et la ligne de touches au-dessus représente les touches noires
(notes altérées). Voici le tableau des fréquences des notes à partir du troisième octave:
Dans sa version 5.5, Turbo-Pascal possède les caractéristiques d'un Langage Orienté
Objets (LOO). Ce qui n'empêche pas de l'utiliser pour ce qu'on pourrait appeler la
programmation traditionnelle, comme nous l'avons vu dans les chapitres précédents. Ce
dernier chapitre a seulement pour but d'initier le lecteur aux concepts de la Programmation
Orientée Objets (POO), appliquée au Turbo-Pascal 5.5.
1 Premières définitions.
La POO est une nouvelle méthode de programmation, déjà disponible dans plusieurs
langages de programmation (C++, SmallTalk, TP5.5, MicroSoft-Pascal 6.0). Par rapport à la
programmation structurée, elle introduit de nouveaux concepts et, par conséquent, une
nouvelle terminologie. En Turbo-Pascal, cela conduit à de nouveaux types de données et de
nouvelles rou tines.
Considérons une table de codification dans un programme de mise à jour de ces tables.
On peut la décomposer en plusieurs éléments:
- le code de la table,
- le nom (titre) de la table,
- le nom de la grille de saisie utilisée pour mettre à jour les éléments de la table.
Ce qui pourrait être implémenté de la manière suivante, en définissant un type record
(TypeTable) et une variable (Table) :
type
TypeTab1e = record
CodeTab string[3];
NomTab string[40]
Gri 11 e string[8];
end;
va r
Table TypeTable;
type
TypeTable = object
CodeTab str1ng[3];
NomTab string[40];
Grille string[8];
procedure AfficherGrille(Grille : string);
procedure EffacerTable;
procedure VisualiserElement(Element : TypeElement);
etc ...
end;
var
Table TypeTable:
Les routines qui manipulent les données des objets sont appelées méthodes. Il faut
remarquer qu'une variable de type object (appelée également instance) est définie de la même
façon qu'une variable quelconque. On peut également définir des pointeurs vers des objets et
les utiliser de la même manière que des pointeurs vers d'autres types.
1.3 Héritage.
Les propriétés des objets ne s'arrêtent pas là. Il est probable que, pour en revenir à
notre exemple, des grilles sont également utilisées pour manipuler d'autres données que les
éléments d'une table. Par conséquent, pourquoi ne pas considérer une grille comme un objet
en soi? La routine d'affichage ou la routine d'effacement de la grille seront les mêmes pour
toutes les grilles. L'exemple ci-dessus pourrait alors devenir:
type
TypeGrille = object
Grille: str1ng[8];
procedure Aff1cherGr1lle;
procedure EffacerGrille:
end;
TypeTable object(TypeGrille)
CodeTab : string[3];
NomTab : string[40];
procedure AfficherElement(Element TypeElement);
etc ...
end;
var
Table: TypeTable:
TypeTable est défini comme étant object(fypeCrille). Ce qui signilie que TypeTable est
un type descendant de TypeCrille ; par conséquent, il hérite de toutes les caractéristiques et
de toutes les méthodes de son ancêtre (ou type ascendant). Cette caractéristique, appelée
héritage, constitue une des différences essentielles entre l'utilisation du type record dans la
programmation traditionnelle et l'utilisation du type object dans la POO.
La décomposition de l'exemple ci-dessus pourrait continuer: une grille est une fenêtre
avec des champs, l'objet TypeCrille pourrait donc descendre d'un objet fenêtre, etc...
La propriété qu'ont les objets d'hériter se traduit par le fait que le champ Grille, bien
qu'il ne soit pas explicitement défini dans l'objet TypeTable, peut être utilisé comme s'il était
un champ de TypeTable: la notation Table.Grille est tout à fait valide. De même pour les
méthodes: l'instruction Table.AfficherGrille; exécute la méthode AfficherCrille de l'objet
Table, c'est à dire qu'elle affiche la grille associée à Table.
L'exemple ci-dessus montre que les champs et les méthodes d'un objet peuvent être
utilisés de la même manière que les champs d'une structure de type record. L'instruction with
peut également être utilisée avec des objets.
Autre point important: au contraire d'autres LOO, Turbo-Pascal permet d'avoir accès
directement aux champs d'un objet. Toutefois il est conseillé d'éviter d'utiliser cette facilité,
afin d'augmenter l'abstraction des objets. Tous les champs d'un objet devraient être
manipulés exclusivement par ses propres méthodes. Quand elle est respectée, cette règle rend
plus facile l'adaptation d'une application: pour changer l'implémentation d'un objet, il suffit
alors de se préoccuper des données et des méthodes de l'objet, sans se préoccuper du reste
du programme; la seule limitation est de conserver le même en-tête pour les méthodes.
Nous avons déjà vu que, par suite de l'héritage, un type objet descendant hérite de tous
les champs et toutes les méthodes de son ancêtre. Il hérite également de la compatibilité avec
tous ses ancêtres. Dans l'exemple des tables déjà vu plus haut, cela signifie que les
instructions suivantes sont correctes:
var
GrllAux : TypeGrille:
GrllPtr : ATypeGrllle:
TablePtr ; ATypeTable:
GrllAux ;= Table:
GrllPtr ;= TablePtr:
Comme on le voit, la compatibilité est également valable pour les pointeurs. Les types
descendants peuvent être utilisés en lieu et place d'un ancêtre quelconque. La réciproque
n'est pas vraie (fable := GrilAux provoquerait une erreur de compilation).
Dans le cas d'une grille à plusieurs champs, cette séquence de code se répète souvent et
il serait intéressant d'implémenter une manière de réduire le code source nécessaire à
l'exécution de cette commande.
En Turbo-Pascal traditionnel, la solution est de définir une routine de saisie, ayant
comme paramètres: le nom du champ sur l'écran, le type de variable à récupérer, et la
variable (sans type) où sera placée la valeur saisie. Cette manière de faire est disponible sur
la disquette d'exemples dans le fichier SAISIE.PAS.
Une autre solution, implémentée en POO, est disponible dans le fichier OSAISIE.PAS et
imprimée ci-dessous. Les deux programmes utilisent la grille OSAISIE.MAP et supposent que
AFFTURBO.COM (partie résidente de Turbo-Screen) a été chargée.
{----------···------------··.cours de Pascal-Avancé-------··--·----------·····-}
{ exemple de déclaratlon et utlllsatlon d'objets pour paramétrer }
{ la salsie de champs avec Turbo-Screen }
{ (dlsponlble dans le flchler osalsle.pas) }
{-_._ .. _-------_._._--------_. __ .. _-----_._---_ ... _-_.---_ .. _ ..... _-_ ... __ ..... }
uses tscreen;
type
ochamp = object
function Saisie(NomChamp string): string;
end;
oreal object(ochamp)
r : rea l ;
procedure Saisie(NomChamp string);
end;
obyte object(ochamp)
b : byte;
procedure Salsie(NomChamp strlng);
end;
ointeger = object(ochamp)
i : integer;
procedure Saisie(NomChamp string);
end;
TypeEn r reco rd
vrea l orea l ;
vinteger ointeger;
vbyte obyte;
vstrlng strl ng[25];
end;
var
Enr TypeEnr;
vchamp : ochamp:
function ochamp.Sais1e;
var
s : string;
begin
write(Oeb, 'SAISIE, '+NomChamp+' ,CR', Fin);
readl n(Cr);
if Cr = • RET'
then readln(s)
else s := " ;
Saisie := s;
end;
procedure oreal.Saisie(NOmChamp string);
var
s : string;
c : integer;
begin
s ;= ochamp.Saisie(NomChamp);
val(s. r, cl;
end;
procedure obyte.Saisie(NomChamp string);
va r
s : string;
r : integer;
begin
s := ochamp.Saisie(NomChamp);
val (s, b, r);
end;
procedure ointeger.Sa1sie(NomChamp string);
var
s : string;
r : integer;
begin
s := ochamp.Saisie(NomChamp);
val(s, i. r);
end;
begin
write(Oeb, 'UTILISE,OSAISIE.MAP· ,Fin);
wi th Enr do
begin
vbyte.Sai si e(' TELBYTE');
vinteger.Saisie( 'TELINTEGER');
vreal.Saisie( 'TELREAL');
vstr1ng := vchamp.Saisie('TELSTRING');
end;
end.
Un type object ochamp a été défini (avec la particularité de ne posséder aucun champ
mais seulement une méthode) ainsi que trois autres types object (oreal, obyte et ointeger)
descendants de ochamp, chacun redéfinissant la méthode de ochamp. Il n'a pas été possible
de définir un type ostring pour les champs de type string car ils sont de taille variable, Une
instance vchamp (de type ochamp) sera pourtant utilisée pour traiter les champs de type
string.
La variable Enr, de type TypeEnr, va recevoir les données saisies. Remarquons que
chaque champ de cette structure est de type object, à l'exception de vstring.
La méthode Saisie de chaque objet utilise la méthode Saisie du type ochamp pour
récupérer les caractères saisis au clavier et les transformer dans le type de la variable
correct.
{----·-----------------------cours de Pascal-Avancé----------------------------}
{ }
{ exemple de déclaration et utilisation d'objets }
{ (disponible dans le fichier ofenetr.pas) }
{------_ ... _._-----------_ .. _-------------_ .. _._----------_ .... _---------------}
uses crt;
type
opoint = object
Li gne.
Colonne : byte;
procedure lnit(x. y : byte);
function LignPos byte;
function ColoPos : byte;
end:
orectangle = object(opoint)
Largeur.
Hauteur : byte:
Sauve pointer;
procedure lnit(x, y, l, h byte);
procedure Afficher;
procedure Effacer;
procedure Deplacer(dx, dy integer);
procedure Liberer;
end;
ofenetre object(orectangle)
Titre: string[80J;
procedure IniUx, y, l, h byte; t : string):
procedure Afficher;
procedure Deplacer(dx. dy integer);
end;
var
Fenetre ofenetre;
procedure orectangle.Afficher;
var
i, j ; byte;
p : pointer;
begin
p := Sauve;
for i ;= Ligne to pred(Ligne+Hauteur) do
begin
Move(Mem[$bBOO:2*(pred(i)*BO+pred(Colonne))). pA. Largeur*2J;
p := ptr(seg(pA), ofs(pA)+Largeur*2);
end;
gotoxy(Colonne, Ligne);
write(' r');
for i := succ(Colonne) to Colonne+Largeur-2 do write('-');
write( " ');
for i := 2 to pred(Hauteur) do
begin
gotoxy (Colonne, p red ( Li gne+i ) ) ;
write('I');
for j := 2 to pred(Largeur) do write(' ');
gotoxy(pred(Colonne+Largeur), pred(Ligne+i));
write("');
end;
gotoxy(Colonne, pred(Ligne+Hauteur));
write(' L');
for i := succ(Colonne) to Colonne+Largeur-2 do write('-');
write( 'J ');
end;
procedure orectangle.Effacer;
var
i : byte;
p : pointer;
begin
p := Sauve;
for i := Ligne to pred(Ligne+Hauteur) do
begin
Move (pA, Mem[ $bSOO: 2* (p red (i )*SO+pred (Colonne) )] , La rgeu r*2) ;
p := ptr(seg(pA), ofs(pA)+Largeur*2);
end;
end;
procedure orectangle.Liberer;
begin
freemem(Sauve, Largeur*Hauteur*2);
end;
procedure ofenetre.Afficher;
var
l : byte;
begin
orectangle.Afficher;
l := (Largeur-length(Titre») div 2;
gotoxy(Colonne+pred(l), Ligne);
write(Titre);
end;
begin
Fenetre.lnit(5. 10,40, ID, 'Fenêtre d"exemple');
Fenetre.Afficher;
while not keypressed do
begin
Fenetre.Deplacer(integer(Random(4o)-2o), integer(Random(lo)-5»;
delay(2oo);
end;
Fenetre.Effacer;
Fenetre.Liberer;
end.
Les méthodes rencontrées jusqu'à maintenant sont des méthodes statiques, dans la
mesure où elles sont connues dès la compilation. Cela est souvent insuffisant pour gérer
correctement les méthodes. Comme nous l'avons vu plus haut, dans le second exemple, il
paraît superflu de redéfinir la méthode Deplacer pour l'objet ofenetre, puisque son code
source est identique à celui de la méthode correspondante de l'objet orectangle. Cet
inconvénient disparait avec l'usage de méthodes virtuelles.
Les variables peuvent être divisées en variables statiques, dont l'emplacement est connu
dès la compilation et en variables dynamiques allouées durant l'exécution. La distinction
entre méthodes statiques et méthodes virtuelles est comparable: les premières sont reliées
aux objets dès l'édition de liens, alors que les secondes sont reliées aux objets, le plus tard
possible, pendant l'exécution.
Pour déclarer une méthode virtuelle, il suffit d'ajouter le mot réservé virtual
immédiatement après la déclaration de la méthode. Par exemple:
Dans un type descendant, toute méthode qui redéfinit une méthode virtuelle doit être
virtuelle.
La procédure d'initialisation d'un objet qui comporte au moins une méthode virtuelle
n'est plus une procedure mais un constructor (nouveau mot réservé du Turbo-Pascal). Le
constructor d'un objet réalise automatiquement des opérations en rapport avec la table des
méthodes virtuelles (TMV). C'est pourquoi, il doit être exécuté avant tout appel à une
3.2 Polymorphisme.
Pour en revenir à notre exemple de fenêtres, nous avons déjà noté que la méthode
Deplacer, bien qu'ayant le même code source, doit être redéfinie pour les deux objets
(orectangle et ofenetre) car la méthode Afficher est propre à chacun des objets.
Ce problème peut être résolu avec l'utilisation de méthodes virtuelles. Après avoir défini
les méthodes utilisées par Deplacer comme virtuelles, il devient inutile de déclarer Deplacer
pour l'objet ofenetre. Puisque ofenetre descend de orectangle, il hérite de la méthode
Deplacer et comme Afficher est déclaré comme virtual, la liaison entre Deplacer et
Afficher sera réalisée au moment de l'exécution:
- s'il s'agit de déplacer une variable de type orectangle, le programme exécutera
Afficher et Effacer de orectangle ;
- s'il s'agit de déplacer une variable de type ofenetre, le programme exécutera
Afficher de ofenetre et Effacer de orectangle (puisque cette dernière méthode n'est pas
redéfinie pour l'objet ofenetre).
Ce concept est nommé polymorphisme, auquel sont associés les objets polymorphes.
Le polymorphisme est une des caractéristiques les plus puissantes de la POO. Si on suppose
que les objets ci-dessus ont été déclarés dans une unit distribuée sous forme de fichier
.TPU, le programmeur, connaissant la partie interface de l'unit, peut créer de nouveaux objets
descendants de orectangle et ofenetre et leur appliquer la méthode Deplacer de
orectangle, sans avoir à la redéfinir. On en conclut que la méthode Deplacer sera utilisée
pour des opérations sur des variables dont le type n'était pas connu au moment de la
compilation.
4 Objets dynamiques.
De même qu'une variable, une instance d'objet peut être allouée dans la mémoire
dynamique, grâce à la procédure New (voir chapitre II). Exemple:
va r
GrillePtr ; ATypeGrille;
New<GrillePtr);
Les objets qui comportent des méthodes virtuelles doivent être initialisés par leur
constructor en même temps qu'ils sont alloués. C'est pourquoi la procédure New accepte un
second paramètre et peut fonctionner comme une fonction qui renvoie un pointeur. Voici
Exemple
var
ptrFenetre : Aofenetre;
Un nouveau mot réservé permet de déclarer les routines de libération des objets
dynamiques. Il s'agit de destructor qui remplace le mot procedure dans l'en-tête.
Il est conseillé d'utiliser le nom Done (standard de Borland) pour un destructor. Un
objet peut posséder plusieurs destructors, de noms différents. Un destructor peut être vide:
destructor ofenetre.done;
begin
end;
4.3 Exemple.
{--------··-······--····----·cours de Pascal-Avancé--·--------------···--·-·---}
j }
{ exemple de déclaration et utilisation d'objets polymorphes et dynamiques }
{ (disponible dans le fichier ofenetrl.pas) }
j---------_._._ _.. _ _.. _-----_._----------------_ .. _ _---_._---_ .. _--}
uses crt;
type
opo1nt = object
Li gne,
Colonne : byte;
procedure lnit(x, y : byte):
function LignPos byte;
function ColoPos : byte;
end;
orectangle = object(opoint)
La rgeur,
Hauteur: byte;
Sauve : pointer;
constructor lnit(x, y, l, h: byte);
procedure Afficher; vi rtual:
Ptrofenetre = ~ofenetre;
va r
Fenetre: Ptrofenetre;
procedure orectangle.Afficher;
var
i, j : byte:
p : pointer;
begin
p := Sauve;
for i ;= Ligne to pred(Ligne+Hauteur) do
begin
Move(Mem[$bSOO:2*(pred(i)*SO+pred(Colonne))], p~, Largeur*2);
p := ptr(seg(p~). ofs(p~)+Largeur*2);
end;
gotoxy(Colonne, Ligne);
write(' r');
for i ;= succ(Colonne) to Colonne+Largeur-2 do write('·');
write( '1');
for i ;= 2 to pred(Hauteur) do
begin
gotoxy(Colonne. pred(Ligne+i));
write('I');
procedure orectangle.Effacer;
va r
i : byte;
p : pointer;
begin
p := Sauve:
for i := Ligne to pred(Ligne+Hauteur) do
begin
Move(p", Mem[$bBOO:2*(pred(i )*80+pred(Colonne))], Largeur*2);
p := ptr(seg(p"), ofs(p")+Largeur*2);
end;
end:
destructor orectangle.Done;
begin
freemem(Sauve, Largeur*Hauteur*2);
end;
procedure ofenetre.Afficher:
var
l : byte;
begin
orectangle.Afficher:
l := (Largeur-length(Titre)) div 2:
gotoxy(Col onne+pred( 1), Li gne);
write<Ti tre);
end:
bcgin
Fenetre := new(Ptrofenetre. IniUS. 10.40. 10. 'Fenêtre d"exemple'»;
FenetreA.Afficher;
while not kcypressed do
begin
Fenetre A.Deplacer(integer(Random(40)-ZO). integer(Random(10)-S»;
delay(ZOO);
end;
FenetreA.Effacer:
Dispose(Fenetre. Done);
end.
5 Compléments.
Les unités Graph et OverJay ayant déjà été étudiées, respectivement, aux chapitres 1 et
VII, il n'en sera pas question dans cette annexe,
Unité SYSTEM
const
OvrCodeL i st ward = 0; {liste des segments du tampon d'overlayl
OvrHeapSize ward = 0; {taille initiale du tampon d'overlayl
OvrOebugPtr pointer = nil;{pointeur pour le débogage d'overlaysl
OvrHeapOrg ward 0; {origine du tampon d'overlayl
Ov rHeapPt r wo rd 0; {pointeur du tampon d'overlayl
OvrHeapEnd ward 0; {fin du tampon d'overlayl
OvrLoadL i st ward 0; {liste des overlays chargés en mémoirel
OvrDosHandle ward 0; {gestionnaire DOS d'overlayl
OvrEMSHandle ward 0; {gestionnaire EMS d'overlayl
HeapO rg pointer nil;{début du tas (mémoire dynamiqueJ)
HeapPtr pointer = nil ;{pointeur vers la fin de la partie occupée du tasl
FreePtr poi nter = nil;{pointeur vers la table des fragmentsl
FreeMin ward = 0; {taille minimum de la table des fragmentsl
HeapError poi nter nil;{adresse de la fonction de gestion des erreurs du tas}
Exi tproc pointer nil;{adresse de la procédure de sortie du programmel
ExitCode integer 0; {code de retour du programmel
ErrorAddr pointer nil; ladresse de la procédure de traitement d'erreurl
PrefixSeg ward = 0; {préfixe de segment de programme (PSP)1
StackLimit ward = 0; {limite inférieure du pointeur de la pilel
InOutRes i nteger = 0; {code de retour des opérations d'entrée/sortiel
RandSeed longint = 0; {valeur aléatoi re initialel
FileMode byte 2; {mode d'ouverture des fichiersl
TestBOB7 byte = 0; {témoin d'installation ou non du co-processeur BOxB7}
var
Input text; {fichier d'entrée standard (clavier)l
Output text; {fichier de sortie standard (écran)l
SaveIntOO pointer; {sauvegarde du vecteur d'interruption DOl
SaveInt02, SaveInt1b, SaveInt23, SaveInt24, SaveInt34, SaveInt35, SaveInt36, Savelnt37, SaveInt3B,
SaveInt39, SaveInt3a, SaveInt3b, SaveInt3c, SaveInt3d, SaveInt3e, SaveInt3f, SaveInt75 : pointer;
(sauvegarde du vecteur d'interruption correspondantl
Unité CRT
con st
{modes d'affichage è l'écran, utilisés par TextModel
BW40 0: {4o colonnes x 25 lIgnes, noir et blancl
C040 1: {4o colonnes x 25 lignes, couleurl
BW80 2: {8o colonnes x 25 lignes, noir et blancl
C080 3: {8o colonnes x 25 lignes, couleur}
Mono 7: {8o colonnes x 25 lignes sur écran monochrome}
Font8x8 256; {mode EGA 43 lignes ou VGA 50 lignesl
{couleurs de textesl
Black 0; {noirl Blue 1 ; {bl eu 1
Green 2; {vert} Cyan 3; {cyan}
Red 4; {rougel Magenta 5; {magental
Brown 6; {ma rronl LightGray 7 : {gris clairl
oarkGray 8; {gris foncél LightBlue 9: {bl eu cl ai rI
Li ghtGreen 10; {vert cl ai rI LightCyan 11: {cyan cl air 1
LightRed 12: {rouge clairl LightMagenta 13: {magenta cl ai rI
Yellow 14: {jaune} White 15: {blancl
Blink 128: {clignotant}
var
CheckBreak boolean; {i ndi cateur de Ctrl Breakl
CheckEof boolean: {indicateur de Ctrl Z quand on lit un fichier a parti r du clavierl
CheckSnow boolean: {indicateur de contrôle d'effet de neige sur les écrans CGAI
DirectVideo boolean: {indicateur d'accès direct è la mémoire de l'écranl
LastMode word; jvaleur du mode d'affichage au début du programmel
TextAttr byte; {mémorise les attributs (couleurs) du mode textel
WindMin,
WindMax word: {coordonnées de la fenêtre courantel
Unité DOS
const
{valeurs des indicateurs de flag}
FCa r ry $0001: {retenue}
FParity $0004; {parité}
FAuxi l i ary $0010; {retenue intermédiaire}
FZero $0040; {égal è zéro}
FSign $0080; {signe}
FOverfl ow $0800; {overflow}
{modes des fichiers}
fmClosed $D7BO;
fmlnput $D7Bl:
fmOutput $D7B2;
fmlnOut $D7B3;
{attri buts de fichiers}
ReadOnly $01: {attribut pour lecture seule}
Hidden $02; {fichier caché}
SysFile $04; {fichier du système opérationnel}
Volumeld $08; {nom de volume}
Directory $10; {répertoire}
Archive $20; {fichier normal}
AnyFll e $3F; {tous les fichiers}
type
{type pour fichier "file" ou "file of"}
FileRec = record
Handle word;
Mode word;
RecSize word;
Private array[l. .26] of byte;
UserData array[l. .16] of byte;
Name a rray[O .. 79] of char;
end;
{types pour fichiers de type "text"}
TextBuf array[0 .. 127] of char;
TextRec = record
Handle word;
Mode word;
BufSize word;
Private word;
BufPos word;
BufEnd word:
BufPtr "TextBuf:
openFunc pointer;
InoutFunc pointer;
FlushFunc pointer;
CloseFunc pointer;
UserData array[l .. 16] of byte;
Name array[o .. 79] of char;
Buffer TextBuf;
end;
var
DosError: integer; {code d'erreur du DOS}
procedure FSplit(NomFichier : PalhStr; var Rep : DirStr; var Nome: NameStr; var Ext ExlStr);
Divise le nom d'un fichier en nom de répertoire, nom et extension du fichier.
procedure GetCBreak(var Break: boolean);
Renvoie l'état actuel (true ou false) du détecteur de Ctrl Break.
procedure GetDate(var Annee, Mois, Jour, JourSemaine : word);
Renvoie la date courante du syst~me opérationnel.
procedure GelDir(D : byte; var Rep : string):
Renvoie le nom du répertoire courant du disque D.
function GelEnv(Var[nv : string) : string;
Renvoie la valeur de la variable d'environnement du DOS passée comme paramètre.
procedure GetFAttr(var Fichier: fne, file of ou text; var Attrib : word);
Renvoie les attributs du fichier (voir constantes d'attributs de l'unité DOS).
procedure GetFTime(var : Fichier: file, file of ou text; Date: longint);
Renvoie la date et l'heure de dernière mise è jour du fichier. Fai re appel è UnPackTime pour
décodifier la variable Date.
procedure GetIntVec(Int : byte; var Vecteur: pointer);
Renvoie la valeur du vecteur de l'interruption Int.
procedure GetTime(var Heure, Minutes, Secondes, Centièmes: word);
Renvoie l'heure courante utilisée par le syst~me opérationnel.
procedure GetVerify(var Ver: boolean);
Renvoie la valeur (true ou false) de l'indicateur VERIFY du DOS.
procedure Intr(Int : byte; var Regs: registers):
Exécute l'interruption Int. Voir la définition du type "registers" dans l'unité DOS.
procedure Keep(Code : word);
Stoppe l'exécution du programme et retourne au système opérationnel, mais en laissant le
programme résident en mémoire.
procedure MsDos(var Regs: Registers);
Exécute une fonction du DOS. Voir la définition du type "registers" dans l'unité DOS.
procedure PackTime(var T : DateTime; var DateHeure : longint);
Convertit un temps de type DateTime en longint.
procedure SetCBreak(var Break: boolean):
Initialise è true ou false l'état du détecteur de CLrl-Break.
procedure SetDate(Annee, Mois, Jour: word);
Initialise la date utilisée par le système opérationnel.
procedure SetFAttr(var Fichier: file, file of ou text: Attribut: word):
Définit les attributs d'un fichier. Voir les constantes d'attributs définies dans l'unité DOS.
procedure SetFTime(var Fichier: fne, flle of ou text; DateHeure : longint);
Définit la date et l'heure de la dernière mise è jour du fichier.
procedure SetIntVec(Int : byte; Vecteur: pointer);
Définit la nouvelle valeur du vecteur de l'interruption Int.
procedure SetTime(Heure, Minutes, Secondes, Centiemes : word);
Initialise l'heure utilisée par le système opérationnel.
procedure SetVerify(Ver : boolean);
Définit la valeur (true ou false) de l'indicateur VERIFY du DOS.
procedure SwapVectors;
Echange le contenu des pointeurs SaveIntxx de l'unité System avec le contenu actuel des
vecteurs d'interruption.
procedure UnPackTime(DateHeure : longint; var T : DaleTime);
Convertit un temps de type longint en temps de type DateTime.
Unité PRINTER
var
lst text;
.
,
Cochonneau Gérard (1990)
Cours de programmation avançée en Pascal
Paris : ORSTOM, 129 p. multigr.