Annexe I : Remarques d'usage
Tables des matières
1. Style de programmation
Pour qu'un programme soit "compréhensible" par un ordinateur, il
faut qu'il respecte les règles syntaxiques du Pascal. Ces
règles, qui sont l'objet principal de ce manuel, peuvent être
décrites avec précision, sans ambiguďté. Elles doivent être
appliquées.
D'autre part, pour qu'un programme soit facilement compréhensible
par un lecteur humain, il est bon qu'il respecte quelques "règles
de style". Mais la notion même de "programme clair" pouvant
varier d'individu á individu, nul n'a pu jusqu'á présent établir
un ensemble de règles très précises et universelles, qui garantiraient
la clarté de n'importe quel programme dans l'esprit de
n'importe quel lecteur.
Ainsi, les conseils généraux donnés cidessous doivent être
suivis avec discernement. Le lecteur doit surtout s'efforcer
d'en saisir l'esprit, afin de les adapter au mieux aux programmes
qu'il écrira. Si la correction syntaxique d'un programme dépend
de règles strictes, sa clarté est avant tout une question de bon
sens.
Voici par exemple un programme PASCAL syntaxiquement correct :
program p (data,output);
var a,b : integer; data : text;
begin
reset(data);b := 0; {on met b a zero}
while not eof(data) do
begin readln(data,a);
if a > 42 then b := b+1 end;
writeln(a)
end.
Ce programme souffre de plusieurs défauts, qui le rendent peu
clair; ces défauts ont été éliminés dans le programme suivant,
qui est équivalent au premier.
program sup (data, output);
{ Donnees : un fichier "data" de type text, contenant un entier par ligne
Resultat : afficher a la sortie standard le nombre d'elements de "data" qui sont > BORNE }
const BORNE = 42 ;
type nombre = integer ; {type des elements de "data"}
var element : nombre ; {valeur de chaque element examine}
compteur : integer ; {nombre d'elements > BORNE}
data : text ;
begin
reset (data) ;
compteur := 0 ; {initialisation : on n'a pas encore trouve d'elements > BORNE}
while not eof (data) do
begin
readln(data,element) ;
if element > BORNE then compteur := compteur + 1
{compteur = nombre d'elements lus jusqu'a present et > BORNE}
end ;
{on a lu toutes les donnees dans data}
writeln (compteur,´ elements de data sont > ´,BORNE)
end.
Ce deuxième programme suit les conseils généraux suivants :
- Faciliter la compréhension de la fonctionnalité du programme.
En tête du programme, un commentaire indique ce que le programme
fait (mais pas comment il le fait) et comment l'utiliser (format
précis des données et des résultats). Le cas échéant, il est
utile de mentionner si des données doivent satisfaire certaines
contraintes, et si le programme vérifie si ces contraintes sont
effectivement satisfaites (par exemple : "tel fichier de données
ne peut pas être vide; s'il est vide, un message d'erreur est
affiché, et l'exécution du programme s'arrête").
La fonctionnalité de chaque procédure ou fonction sera également indiquée
par un commentaire placé en tête du sousprogramme concerné.
Le rôle de tels commentaires est d'éviter d'avoir á comprendre
l'algorithme sousjacent á un programme pour découvrir la manière
de l'utiliser.
D'autre part, on veillera á ce que l'utilisation du programme ait
un minimum de convivialité :
- lors de l'exécution d'un programme interactif, l'utilisateur
doit á tout moment savoir quelle donnée il doit introduire.
On peut pour cela faire précéder toute lecture á l'entrée
standard de l'affichage d'un message tel que "Taper un nombre
entier", "Taper le nombre d'éléments á trier", etc.
- les résultats doivent être interprétables de manière non
ambiguë. C'est ainsi que, dans le deuxième programme exemple,
l'affichage du résultat est constitué d'une phrase permettant
facilement de l'interpréter (au contraire du premier
exemple).
- si le programme est prévu pour détecter des situations
d'erreur, on veillera á lui faire afficher des messages
d'erreur explicites. A la vue d'un programme dont
l'exécution s'arrête sans message d'erreur, bien que les
résultats attendus ne soient pas fournis, l'utilisateur est
incapable de déterminer facilement si c'est une condition
d'erreur a été détectée, ou si c'est l'ordinateur qui est en
panne !
Toutefois, il ne faut pas tomber dans le danger consistant á
soigner particulièrement la saisie des données et la présentation
des résultats, au détriment de la qualité de l'algorithme
implémenté ! On aurait tort de ne juger un programme que sur la
beauté de son interface avec l'utilisateur...
- Faciliter la compréhension de l'algorithme sousjacent.
- Utiliser des noms significatifs pour les constantes, types,
sousprogrammes et variables.
Le lecteur peut évidemment se
laisser guider par son bon sens pour parfois enfreindre cette
règle : par exemple, la variable compteur d'une boucle for
est bien souvent appelée i ou j sans que cela ne nuise á la
clarté du programme.
- Indiquer en commentaire le but d'une suite d'instructions, ou
le rôle d'une variable.
Un commentaire tel que celui qui
figure dans le premier programme exemple est absolument
inutile : il n'est rien d'autre qu'une traduction en français
d'une instruction Pascal. Il n'apporte donc aucun renseignement
(á ceux qui connaissent le Pascal...). Il est
par contre plus utile, á la fin d'une itération, d'indiquer
en commentaire dans quelle situation l'on se trouve. Ceci
est important, tout particulièrement lorsqu'il existe
plusieurs conditions possibles différentes qui peuvent être
remplies en fin d'itération.
- Faire ressortir la structure du programme au moyen d'une
bonne indentation.
On décalera ainsi vers la droite le
corps d'une boucle ou d'un test.
- Rendre le programme modulaire.
Un programme principal assez
court construit á partir d'un ensemble de sousprogrammes eux
aussi assez courts est en général plus simple á comprendre
qu'un long programme principal.
- Concernant les sousprogrammes :
- Eviter qu'une fonction modifie ses arguments.
D'après la définition mathématique du mot, on s'attend á ce
qu'une fonction calcule une valeur dépendant de ses
arguments, et non qu'elle modifie ses arguments.
Beaucoup considèrent donc comme déroutant, et dès lors peu
clair, le fait qu'une fonction puisse modifier ses arguments.
Cependant, certains spécialistes considèrent
que de telles fonctions peuvent avoir leur place. Par
exemple, on peut transformer une procédure, qui modifie
certains de ses arguments, en une fonction qui fait
exactement le même travail que cette procédure, si ce
n'est qu'elle détecte des erreurs lors de l'exécution du
sousprogramme (lecture dans un fichier vide, division
par zéro, etc.). La fonction peut être construite pour
renvoyer une valeur booléenne indiquant si des erreurs
ont été rencontrées, ou pour renvoyer un entier indiquant
un code d'erreur.
- Réduire l'utilisation de variables globales.
Suivant le principe d'abstraction procédurale, un
sousprogramme doit pouvoir être considéré comme une
"boîte noire" : il
n'est pas nécessaire de connaître les détails de son
implémentation pour pouvoir l'appeler correctement dans
un autre programme (par exemple, il n'y a pas de conflit
entre une variable locale et une variable globale de
même nom). Il est donc ainsi possible de comprendre le
fonctionnement d'un programme, sans devoir comprendre
tous les détails des sousprogrammes qu'il appelle.
La communication entre un sousprogramme et le programme
appelant est établie au moyen des paramètres ou au moyen
de variables globales. Syntaxiquement, l'intérêt des
paramètres est qu'ils apparaissent explicitement dans
l'appel du sousprogramme, et dans l'entête de sa
définition. A la lecture du sousprogramme ou du programme
appelant, on voit donc facilement comment ces
deux entités interagissent. Par contre, il n'est pas
immédiat de repérer les variables globales qu'ils partagent.
Ceci peut toutefois s'arranger : il suffit de
faire figurer ces variables en commentaires près des
appels du sousprogramme, et près de l'entête de sa
définition. Mais d'autre part, les variables globales
restent gênantes si l'on souhaite extraire un sous
programme du programme pour lequel il était initialement
prévu afin de l'utiliser dans un autre programme.
- Faciliter l'adaptation du programme á des problèmes proches.
2. Quelques sources d'erreurs fréquentes
Après avoir lu de nombreux programmes PASCAL d'étudiants, les
auteurs ont pu isoler un certain nombre de notions qui, mal comprises
ou mal assimilées, sont la source de fréquentes erreurs
syntaxiques ou sémantiques.
Ces notions ne sont pas particulièrement mises en évidence dans
le corps du présent manuel. Mais le lecteur en trouvera la
liste dans les pages qui suivent, qui sont destinées á lui servir
de pensebête. Libre á lui d'utiliser les moyens de son choix,
surligneur ou autre, pour mettre en évidence le "danger" ou
l'importance de ces notions dans les pages précédentes.
2.1. Concernant = := ; , [] () {}
- Ne pas confondre ":" et "=" dans les déclarations : "=" est
associé á const et type, tandis que ":" est associé á var
(c'est normal : dans une déclaration de la forme var x :
integer, x appartient au type integer, mais x n'est pas égal
au type integer).
- "=" et ":=" ne doivent pas non plus être confondus : ":=" est
le symbole d'affectation, utilisé dans une instruction
d'affectation, pour donner une nouvelle valeur á une variable
(cfr Ch 8.2.1).
"=" est l'opérateur binaire
d'égalité, utilisé dans une expression booléenne pour comparer
deux valeurs
(cfr Ch 7.2).
- Dans un appel de sousprogramme, les arguments sont séparés
par des ","
(cfr Ch 11.1).
Mais dans l'entête
de sa définition apparaissent, á des endroits
différents, des "," et/ou des ";"
(cfr Ch 6.1).
- Les {} entourent les commentaires et n'ont pas d'autre usage.
En particulier, ce sont les () qui servent á définir les
types énumérés
(cfr Ch 2.1.5)
et les [] qui
servent á définir les littéraux des types set of ... (cfr
Ch 2.2.3).
- Ce sont les [], et pas les (), que l'on utilise dans une
expression qui désigne un élément de tableau
(cfr Ch 7.2).
2.2. Déclarations
2.3. Nombres
- Les types integer et real ne sont pas interchangeables. Des
expressions entières sont affectables á des variables
réelles, mais le contraire est illicite (par exemple, x :=
2.0 n'est pas correct si x est de type integer).
Cfr Ch 7.4;
Ch 8.2.2;
Ch 11.2.4.2.
- Il existe deux opérateurs de division
(cfr Ch 2.1.1)
: "div" (division entière, résultat entier) et "/"
(division "exacte", résultat réel).
- Le programmeur consciencieux doit se poser la question, pour
chaque "/" ou "div" qui figure dans son programme, de la possibilité
d'effectuer une division par zéro. Si cette possibilité
existe, il faut la détecter avant.
- Il n'existe pas d'opérateur d'exponentiation. On peut par
eyemple utiliser exp(y * ln(x)) pour calculer
x . Si y est entier, il vaut mieux multiplier x par lui-même
y fois.
2.4. Booléens
- La priorité des opérateurs logiques
(cfr Ch 11.3)
est telle qu'une expression comme
i < 10 and i > 0
est évaluée comme
(i < (10 and i)) > 0
ce qui n'a pas de sens et donnera un message d'erreur lors de
la compilation. Si l'on veut que cette expression signifie
réellement
(i < 10) and (i > 0)
on doit utiliser des parenthèses.
2.5. Tableaux et fichiers
- Il est très important de comprendre les différences entre les
divers types structurés et, en particulier, entre les
tableaux
(Ch 2.2.1)
et les fichiers
(Ch 2.2.4).
Les fichiers externes
(cfr Ch 5.3)
peuvent être "remplis" avant l'exécution d'un programme
(par exemple á l'aide d'un éditeur de texte ou d'un autre
programme), et lus pendant cette exécution. Mais un
tableau, lui, doit toujours être rempli pendant l'exécution
du programme, comme c'est le cas pour les variables simples.
- Les instructions reset et rewrite
(Ch 10.1.1
et Ch 10.2.2)
s'appliquent á des fichiers, pas á des tableaux.
2.6. Sousprogrammes
- Il est très important de comprendre la différence entre
fonction et procédure
(cfr Ch 12.1).
Un appel de
procédure est une instruction
(cfr Ch 8.1);
un appel de fonction est une expression
(Ch 7.2),
qui peut, par exemple, figurer á droite d'un symbole
d'affectation
(cfr Ch 8.2.1).
- Il est très important de comprendre la notion de paramètre
(Ch 11.1),
ainsi que la différence entre
paramètre et variable locale
(cfr Ch 13.1 et
Ch 13.2).
En particulier, les paramètres d'un sousprogramme
sont déclarés dans son entête
(cfr Ch 6.1).
Ils ne peuvent pas être redéclarés dans la partie déclaration
de variables du bloc composant ce sousprogramme (cfr Introduction, § 2).
- Il est très important de comprendre la différence entre les
deux formes de passage de paramètres : par valeur et par
addresse
(cfr Ch 11.2).
- Il est important de comprendre la différence entre identificateur
local et identificateur global
(cfr Ch 13).
- Un fichier doit toujours être passé par adresse; une composante
d'une variable de type packed array doit toujours
être passé par valeur
(cfr Ch 11.2.3).
- Si un paramètre est de type structuré (tableau, enregistrement,
fichier), il faut déclarer son type dans l'entête
du sousprogramme au moyen d'un identificateur de type
(cfr Ch 4.1
et Ch 4.3;
Ch 11.2.4.1
et Ch 11.2.4.2)
, sauf pour les tableaux homologues
(cfr Ch 11.2.4.3).
- Deux appels successifs d'une fonction provoquent deux
exécutions différentes de cette fonction, et peuvent
éventuellement fournir peutêtre deux résultats différents.
Par exemple, si une fonction "MinLigne" renvoie le minimum de
la ligne courante d'un fichier text contenant des entiers,
alors
if MinLigne (fichier) < min then min := MinLigne(fichier);
n'est pas équivalent á
m := MinLigne (fichier) ;
if m < min then min := m ;
- Une fonction renvoie une valeur
(cfr Ch 12.2).
Il faut s'assurer que, quelle que soit la valeur des arguments,
une instruction de renvoi de la forme
nom de la fonction := expression
est bien effectuée avant la fin de l'exécution de la fonction.
2.7. Instructions composées
- Le ";" sépare deux instructions á l'intérieur d'une instruction
composée
(cfr Ch 7.1).
Un ";" situé immédiatement avant un else est incorrect.
Mais un ";"
situé immédiatement avant un end est inutile mais correct
(Ch 8.1).
- and est un opérateur booléen. Il ne peut jamais remplacer
un ";". Par exemple, le morceau de programme suivant est
incorrect :
if a < min then min := a and nombre := nombre + 1
{incorrect !}
- L'indentation (cfr Annexe I, § 1) ne dispense pas de la
présence de begin et end. Par exemple,
while not eof(data) do
readln(data,x) ;
n := n + 1 ;
est, pour un compilateur, entièrement équivalent á
while not eof(data) do readln(data,x) ;
n := n + 1 ;
mais sűrement pas équivalent á
while not eof(data) do
begin
readln(data,x) ; n := n + 1 ;
end ;
Une telle erreur, que l'on peut mettre longtemps á détecter,
apparaît le plus souvent lors de la modification de programmes,
lorsque l'on ajoute une instruction au corps d'une
instruction de répétition qui n'en comportait qu'une.
2.8. Instructions conditionnelles
2.9. Instructions de répétition
2.10. Entrées/Sorties
- Un programmeur consciencieux vérifiera, pour chaque read ou
readln, que lors de toute exécution de cette instruction, il
reste des données á lire.
- Un programme peut relire plusieurs fois un même fichier
(depuis le début). Chaque relecture doit être précédée
d'une instruction reset, qui fera en sorte que la prochaine
instruction read ou readln lise le premier élément du
fichier. Ceci n'est cependant pas très efficace.
- L'entrée et la sortie standard ne doivent
pas être initialisées
(cfr Ch 10.3.1).
L'exécution de
reset(input) ou de rewrite(output) peut entraîner un
comportement indésiré du programme.
- Lecture d'un nombre dans un fichier de type text (cfr
Ch 10.2.3.1)
: lors de l'exécution d'un read,
les séparateurs se trouvant á gauche du nombre á lire sont
"sautés". Mais les séparateurs se trouvant á droite de ce
nombre ne seront "sautés" que par l'exécution suivante d'un
read. Par conséquent, si, par exemple, le dernier nombre á
lire est suivi d'un blanc ou d'un retour chariot,
l'expression eof(...) sera évaluée á faux après la lecture au
moyen d'un read de ce dernier nombre : après ce dernier nombre,
il reste des caractères á lire (même si ces caractères
sont des séparateurs). A ce moment, en Turbo-PASCAL,
toute tentative de lecture d'un nombre aura pour effet de mettre á zéro
la variable correspondante, sans message d'erreur ou
d'avertissement, tandis qu'en PASCAL standard, la lecture se soldera
par un message d'erreur suivi de
l'arrêt du programme. On aura ce problème en tête en
écrivant une instruction de la forme
while not eof(...) do ...
dont le corps n'effectue des lectures qu'au moyen de
l'instruction read. En particulier, on se souviendra alors
que l'instruction readln permet de "sauter" un caractère
retour chariot se trouvant après le dernier nombre, de sorte
que eof(...) soit évalué á vrai (sauf si le dernier nombre
est suivi de plusieurs caractères retour chariot !).
- Lecture á l'entrée standard (input) : pour que eof (ou eoln)
puisse être évaluée, il faut qu'un (ou plusieurs) caractère
ait été tapé et reçu par l'ordinateur, qui pourra alors
décider si ce caractère est ou non le caractère de fin de
fichier (ou de fin de ligne). Par exemple, lors de
l'exécution de l'instruction
while not eof do
begin
writeln (´Taper un nombre´) ;
readln (i) ;
...
end ;
le message Taper un nombre ne sera pas affiché avant que eof
ait été évaluée, c'estádire avant que l'on ait effectivement
tapé quelque chose. Si cet effet n'est pas souhaité,
il faut alors ajouter cette instruction writeln avant
l'instruction while :
writeln (´Taper un nombre´) ;
while not eof do
begin
readln (i) ;
writeln (´Taper un nombre´) ;
...
end ;
- Une instruction read ou readln ne sert qu'á lire, jamais á
écrire. Par exemple,
writeln ('Taper un nombre') ;
readln (x) ;
ne peut pas être remplacé par
readln ('Taper un nombre',x) ; {incorrect !}
- Fichiers de type text : dans une instruction de lecture, la
ou les variables dont on lit les valeurs ne peuvent pas être
de type structuré
(cfr Ch 10.2.3.1).
Par exemple, on ne peut donc pas lire la valeur de la totalité
des éléments d'un tableau au moyen d'une seule instruction
read ou readln; on doit effectuer une lecture par élément du
tableau. Même remarque pour l'écriture
(cfr Ch 10.2.4.1),
si ce n'est que l'on peut écrire en une seule
instruction write ou writeln la totalité d'une chaîne de
caractères
(cfr Ch 2.2.1.2).
- Les auteurs ont remarqué que de nombreuses erreurs, lors de
l'exécution d'un programme, proviennent de l'oubli du nom de
fichier concerné comme argument de eof, eoln, read, readln,
write ou writeln. Ceci n'est pas une erreur syntaxique,
mais, á l'exécution, ce sera l'entrée standard, ou la sortie
standard, qui sera prise en compte, au lieu du fichier
souhaité. L'effet attendu ne sera donc pas atteint.