Les bases de la programmation

Eric Thirion

Ce cours peut être utile à toute personne désirant s'initier à la programmation et aux bases de données. Il s'adresse donc à des débutants et ne demande aucune connaissance préalable dans ces deux domaines.

Depuis 2008, je le remets continuellement à jour en corrigeant les inévitables erreurs et en le rendant plus complet et surtout, plus pédagogique.

Je l'utilise pour enseigner la partie développement d'applications du programme de BTS informatique (formation Bac+2). Il s'agit donc d'un cours « vécu sur le terrain », qui tient compte des difficultés rencontrées par les étudiants.

Commentez

- Titre : Les bases de la programmation

- Auteur : Eric Thirion

- Parution : 16 décembre 2013

- Mise à jour : 5 mars 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Eric Thirion. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- À propos de l'auteur

- Note de la rédaction de Developpez.com

- Premières notions de programmation

- But de ce chapitre

- Un peu d'histoire

- L'ordinateur

- Codage binaire

- Le bit

- Le transistor

- L'octet et autres unités

- Le codage des nombres entiers en base 2

- Le codage du texte en ASCII

- Mémoire vive et adresse

- Langage machine et programme exécutable

- Processus

- Les entrées-sorties

- Les interruptions

- Les Interfaces graphiques

- Historique

- Interface du système

- Le curseur

- Gestion du clic

- Fenêtre d'une application

- Les évènements

- Les zones de texte

- La programmation

- Notion de programme

- Notion de langage

- Fichiers sources

- Compilation

- Notion de compilateur

- Exemple de compilation

- Interprétation

- Les EDI

- La programmation évènementielle

- Lazarus

- Notion de formulaire

- Génération automatique de code

- Programmation évènementielle

- Ordre d'exécution des instructions

- Notice d'utilisation

- Les variables

- La notion de variable

- Les noms de variables en Pascal

- La notion de type

- Déclaration d'une variable

- Déclaration d'une variable en Pascal

- Syntaxe générale

- Déclaration d'un entier

- Le type double

- Déclaration d'une chaîne de caractères

- Regroupement des déclarations

- Où faut-il déclarer les variables ?

- Lecture et affichage

- Données et résultats d'un traitement

- L'image du sourd-muet qui ne sait pas additionner

- Pour un ordinateur

- Exemple du programme d'addition

- Historique

- La procédure writeln

- La procédure readln

- ETBib

- Exemple d'utilisation des procédures de ETBib

- Lecture des variables

- Affichage des variables

- Effacement des zones de texte

- Argumentation pédagogique

- Les expressions

- Les littéraux

- Les littéraux numériques

- Les littéraux chaînes de caractères

- Les constantes

- Déclaration d'une constante en Pascal

- Opérateur

- La concaténation

- Validité d'une expression

- Expressions valides de type chaîne de caractères

- Expressions valides de type numérique

- Affichage d'expression

- Expression de type chaîne de caractères

- Expression numérique

- Traitement sans variables-résultats

- L'affectation

- Écriture d'une instruction d'affectation en Pascal

- Exemple de code Pascal avec des affectations

- Affectation récursive

- Traitement itératif

- Exemple et conseils

- Exemple de programme et conseil

- Cahier des charges

- Déclaration des constantes

- Conception de l'interface graphique

- Lecture et déclaration des données

- Traitement, résultats intermédiaires et finals

- Le code de la procédure en entier

- Synthèse

- Exercices

- Sous-programmes

- But de ce chapitre

- Introduction

- Découpage d'un programme en sous-programmes

- Sous-programmes non évènementiels

- Les unités

- Procédures et fonctions

- Portée des variables

- Exemple 1 (fichier : Global/ProjetGlobal.lpi)

- Exemple 2 (fichier : Local0/ProjetLocal0.lpi)

- Exemple 3 (fichier : Local1/ProjetLocal1.lpi)

- Exemple 4 (fichier : Local2/ProjetLocal2.lpi)

- Synthèse

- Définition

- Portée

- Durée de vie

- Déclarations locales multiples

- Déclaration simultanée en local et en global

- Exemple d'utilisation de sous-programmes

- Utilisation des procédures

- Exemple 1

- Programmation descendante

- Retour d'un sous-programme

- Exemple 2

- Détail technique

- Exemple d'utilisation de fonctions

- Ancienne version du projet

- Nouvelle version : appel des fonctions

- Nouvelle version : déclaration des fonctions

- Retour du résultat

- Principes généraux et compléments

- Déclaration et appel de procédures

- Déclaration d'une procédure

- Entête

- Variables locales

- Écriture d'un appel de procédure

- Exécution d'un appel de procédure

- Principe du retour à la procédure appelante

- Passage des paramètres et exécution de l'appel

- Déclaration et appel de fonctions

- Déclaration d'une fonction

- Appel de fonction

- Différences avec un appel de procédure

- Les fonctions prédéfinies

- Expressions et appels de fonctions

- Exercices

- Les tableaux

- But de ce chapitre

- Les tableaux à une dimension

- Exemple introductif

- Le cas général

- Utilisation de constantes pour définir la dimension d'un tableau

- Tableaux remplis partiellement

- Représentation

- Adjonction d'une valeur

- Adjonction à la fin

- Insertion

- Suppression d'un élément

- Suppression du dernier élément

- Suppression d'un élément quelconque (différent du dernier)

- Notion de pile

- Les tableaux à deux dimensions

- Déclaration en Pascal

- Traitement de tous les éléments

- Traitement d'une ligne

- Traitement d'une colonne

- Exercices

- Structures de contrôle

- But de ce chapitre

- Les expressions logiques

- Variable booléenne

- Opérateurs logiques

- La négation

- La conjonction

- La disjonction

- Expressions logiques

- Les variables booléennes

- Utilisation des opérateurs logiques

- Utilisation des opérateurs de comparaison

- Les six opérateurs

- Comparaison d'expressions numériques

- Comparaison de chaînes de caractères

- Exercices

- La conditionnelle

- Le If

- Le If … Then

- Le If … Then … Else

- If imbriqués

- L'instruction Exit

- Case

- Exercices

- Les boucles

- Les boucles While

- Vocabulaire associé aux boucles

- Déroulement de la boucle

- Les boucles Repeat

- Les boucles For

- Exercices

- Corrigés des exercices

- Les types structurés

- Introduction

- Les types structurés

- Généralités indépendantes du langage de programmation

- Déclaration

- Déclaration d'un type structuré

- Déclaration de variables de type structuré

- Manipulation des champs d'une structure

- Définition de types complexes par combinaison

- Tableaux de structures : les tables

- Structure avec champs de type structuré

- Les pointeurs

- Déclaration d'un pointeur

- Allocation dynamique de mémoire

- Listes simplement chaînées

- Représentation

- Parcours

- Adjonction d'un élément

- En début de liste

- Insertion après un élément

- Suppression d'un élément

- Suppression du premier élément

- Suppression du suivant

- Listes doublement chaînées

- Représentation

- Adjonction d'un élément

- En début de liste

- Insertion après un élément

- Exercices

- Les fichiers

- But de ce chapitre

- Notion de fichier

- Qu'est-ce qu'un fichier ?

- Nom de fichier

- Chemin d'accès

- Identification d'un fichier par son nom complet

- Format

- Les fichiers texte

- Les fichiers structurés

- Opérations sur les fichiers

- Les erreurs d'accès

- Ouverture et fermeture d'un fichier

- À quoi sert l'ouverture d'un fichier ?

- À quoi sert la fermeture d'un fichier ?

- Accès direct et accès séquentiel

- Accès direct

- Accès séquentiel

- Manipulation des fichiers texte

- Ouverture d'un fichier texte

- Opérations sur les lignes

- Manipulation des fichiers structurés

- Ouverture d'un fichier structuré

- Accès séquentiel

- Accès direct

- Manipulation de fichiers texte en Pascal

- Déclaration

- Assignation

- Création d'un fichier texte

- Ouverture en écriture

- Écriture d'une ligne

- Lecture d'un fichier texte

- Ouverture d'un fichier texte en lecture

- Lecture d'une ligne et test de fin de fichier

- Les fichier structurés en Pascal

- Déclaration

- Assignation

- Ouverture et fermeture

- Écriture séquentielle

- Lecture séquentielle

- Lecture jusqu'en fin de fichier

- Exercices

À propos de l'auteur

Eric Thirion, titulaire d'un doctorat en informatique et d'un Capes de mathématiques, a passé plusieurs années à l'étranger (de 1990 à 1994 à l'Université Heriot-Watt d'Édimbourg, puis de 1994 à 1996 à l'université de Leuven en Belgique) dans le cadre de contrats de recherche en vision par ordinateur.

Auteur d'une vingtaine de publications, il a enseigné la programmation à l'école des Mines de Nancy, à l'IUT de Nancy et à l'université Louis Pasteur de Strasbourg.

Note de la rédaction de Developpez.com

La relecture des différentes parties du cours a été réalisée par Cédric DUPREZ, Claude Leloup, f-leb et Philippe DUVAL.

Premières notions de programmation

But de ce chapitre

Dans ce cours, j'ai tout d'abord rassemblé les notions qui m'ont semblé utiles à introduire avant de commencer proprement dit le cours de programmation.

En particulier, comme la programmation est conditionnée par la manière dont fonctionne un ordinateur, il m'a semblé logique de commencer par une présentation très sommaire de cette machine : sa structure, son fonctionnement, et du seul langage qu'elle est capable de comprendre : le langage machine.

J'aborde ensuite les principes d'interaction homme-machine via une souris, un clavier et un écran graphique. Cela me permet d'introduire la programmation évènementielle, avant de parler de l'environnement de développement intégré Lazarus, spécialement conçu pour ce type de programmation.

Le cours de programmation proprement dit porte sur les instructions permettant de rentrer des données dans la mémoire d'un ordinateur (la lecture, l'affectation) ou d'afficher des informations à l'écran, ce qui demande au préalable d'introduire les notions de variables, de type et d'expressions.

Un peu d'histoire

De la machine de Turing à UML, retrouvez les moments-clés de l'histoire de l'informatique dans cette série de slides : téléchargez le document Projo-Histoire.pdf.

L'ordinateur

Un ordinateur est une machine utilisant et produisant de l'information. Il reçoit de l'information (de l'utilisateur, par internet…), donne de l'information (informations affichées à l'écran, imprimées…), mémorise de l'information (en mémoire vive, sur disque dur, clé USB, CD, DVD…) et traite de l'information (calcul mathématique, « raisonnement » logique…).

Nous allons donc commencer par parler de l'information et plus exactement de la manière dont elle est représentée dans un ordinateur.

Codage binaire

Le bit

Toute information traitée par un ordinateur est codée en binaire, c'est-à-dire par une suite de bits valant zéro ou un.

La représentation physique du zéro et du un dépend du type de support utilisé : mémoire vive, disque dur, clé USB, CD, etc. Mais, quel que soit le type de support utilisé, il s'agit toujours d'un point de vue logique, d'une suite de zéros et de uns.

Le transistor

Pour prendre un exemple, la représentation physique actuelle d'un bit en mémoire vive est un transistor composé de silicium. La représentation du zéro et du un est basée sur la répartition des charges négative et positive dans le transistor.

Voici par exemple le zéro :

Et voici le un :

De 1970 à 2008, le nombre de transistors sur une puce électronique a augmenté de manière vertigineuse en suivant la loi de Moore : le nombre de transistors par puce double tous les deux ans pour le même prix de fabrication. Les transistors deviennent donc de plus en plus petits.

Comme un signal électrique met moins de temps à parcourir une distance plus courte, cette progression a des répercussions directes sur la vitesse des processeurs (elle double tous les 18 mois). Elle entraîne également une augmentation constante de la capacité des mémoires vives (la RAM des PC double tous les deux ans environ).

En 2008, la taille minimale d'un transistor était de l'ordre de 45 milliardièmes de mètre.

L'octet et autres unités

Une suite de huit bits est appelée un octet (ou byte en anglais). C'est l'unité de base permettant de définir la dimension d'un espace de stockage. Il en dérive des unités plus grandes comme :

- le kilooctet (ko = 210 octets = 1 024 octets) ;

- le mégaoctet (Mo = 1 024 Ko, soit environ un million d'octets) ;

- le gigaoctet (1 024 Mo, soit environ un milliard d'octets) ;

- le téraoctet (1 024 Go, soit environ mille milliards d'octets).

La mémoire vive des ordinateurs actuels (2013) est de l'ordre de plusieurs gigaoctets alors que la capacité des disques durs se compte en téraoctets.

La manière de coder une information en binaire dépend du type d'information. Les informations numériques, par exemple, ne sont pas codées de la même manière que le texte ou que les images ou le son.

Pour fixer les idées, voyons deux exemples de codage.

Le codage des nombres entiers en base 2

Le codage binaire des nombres entiers est basé sur l'écriture des nombres en base 2 (en utilisant uniquement les chiffres 0 et 1).

Le principe est le même que celui de l'écriture d'un nombre entier en base 10. c'est-à-dire l'écriture habituelle d'un nombre utilisant les 10 chiffres de 0 à 9. Rappelons que dans ce système, chaque chiffre est associé à une puissance de 10. Il y a un chiffre pour les unités (10^0), un chiffre pour les dizaines (10^1), un pour les centaines (10^2), un pour les milliers (10^3), etc. Par exemple, 2013 signifie : 2 × 10^3 + 0 × 10^2 + 1 × 10^1 + 3 × 10^0.

L'écriture d'un nombre entier en base 2 utilise le même principe, sauf que les puissances de 10 sont remplacées par des puissances de 2. Le nombre 40, s'écrira par exemple : 101000. Car 40 = 32 + 8 ou autrement dit 2^5 + 2^3 et donc 40 = 1 × 2^5 + 0 × 2^4 + 1 × 2^3 + 0 × 2^2 + 0 × 2^1 + 0 × 2^0.

Comme 40 n'a que six chiffres en base 2, on peut représenter ce nombre sur un octet comme suit :

Ce codage permet de coder de grands nombres en utilisant très peu de bits :

- jusqu'à 64 000 environ avec 2 octets pour les entiers non signés (32 000 avec signe) ;

- jusqu'à 4 milliards environ avec 4 octets pour les entiers non signés (2 milliards avec signe).

Le codage du texte en ASCII

Les informations textuelles (suite de caractères) sont souvent codées en ASCII. Dans ce codage, chaque caractère est représenté par un octet de valeur prédéfinie dans une table de correspondance.

La « valeur » est en fait l'interprétation du codage binaire de l'octet en nombre entier, comme nous l'avons vu au paragraphe précédent.

Prenons par exemple, le caractère « ( ». Dans la table de correspondance ASCII, on voit que sa valeur est 40. Autrement dit, le codage binaire du caractère « ( » est :

c'est-à-dire le même codage que celui du nombre 40 sur un octet !

Mais alors comment un ordinateur peut-il distinguer un nombre d'un caractère ?

Il ne le peut pas !

En effet, la simple connaissance d'une suite de 0 et de 1 ne permet pas de savoir ce que représente cette suite. Tout dépend de la manière dont elle est interprétée.

Si on donne l'octet précédent à un ordinateur, il y verra le nombre 40, si à ce moment-là il s'attend à un nombre et il y verra le caractère « ( », si à ce moment-là, il s'attend à un caractère.

Mémoire vive et adresse

Barrette de RAM DDR3 de 4 Go

La mémoire vive (ou RAM pour Random Access Memory) d'un ordinateur peut être vue comme une suite continue d'octets.

Chaque octet possède un numéro. Le numéro du premier est 0, du deuxième 1, etc. Ce numéro s'appelle l'adresse de l'octet.

Une plage mémoire est une suite d'octets consécutifs. Elle peut donc être définie par une plage d'adresses (adresses minimale et maximale) ou bien par l'adresse de début (ou autrement dit, l'adresse minimale) de la zone et du nombre d'octets qu'elle contient.

Voici, par exemple, une plage mémoire de trois octets, démarrant à l'adresse 2 :

Langage machine et programme exécutable

Processeur Intel 80486DX2 (taille: 12 × 6,75 mm)

Le processeur d'un ordinateur exécute en permanence des instructions que l'on appelle instructions machine. Il contient (en outre) des mémoires de petite taille (quelques octets), appelées registres, qui servent à transférer des données de la mémoire vers le processeur (ou inversement) en vue d'effectuer des opérations sur ces données.

Une instruction machine est un ordre donné au processeur.

Par exemple :

- copier une plage mémoire d'adresse donnée dans un registre ;

- copier le contenu d'un registre dans une plage mémoire d'adresse donnée ;

- additionner, soustraire, diviser ou multiplier les nombres contenus dans deux registres et enregistrer le résultat dans un troisième registre.

Nous avons vu plus haut que toute information traitée par un ordinateur est codée en binaire. Il en de même des instructions machine. Une instruction machine est codée en binaire sur quelques octets (4 ou 8 dans les machines actuelles). Une partie du codage définit le type d'opération à effectuer et une autre, les objets (les registres, par exemple) sur lesquels cette opération doit agir.

La manière de coder les instructions machine dépend du type de processeur.

On appelle langage machine l'ensemble de conventions définissant le codage binaire des instructions machine d'un type particulier de processeur.

Une suite d'instructions machine constitue un programme ou plus précisément un programme exécutable.

Il est important de noter que le langage machine est le seul langage directement exécutable par un ordinateur. Les langages de programmation (comme C, Java, PHP…) ne le sont pas !

Les programmes exécutables peuvent se trouver en mémoire (on dit qu'ils sont résidents) ou sur le disque, sous la forme de fichiers (reconnaissables sous Windows par leur extension .exe). Mais pour pouvoir être exécutés, ils doivent forcément être chargés en mémoire :

Nous avons dit plus haut qu'un programme exécutable est une suite d'instructions.

En fait, pour être plus exact, il faudrait définir un programme exécutable comme un mélange de zones de code contenant des instructions machine et de zones de données servant à stocker provisoirement les informations traitées (données ou résultats).

Processus

La mémoire d'un ordinateur contient plusieurs programmes que le processeur exécute à tour de rôle à une telle vitesse que l'on a l'impression qu'ils sont exécutés en même temps.

Chacun de ces programmes résidents possède donc ses propres zones d'instructions et de données.

L'exécution d'un programme particulier par un ordinateur s'appelle un processus.

Notez qu'un même programme peut donner lieu à plusieurs processus.

Parmi les processus résidents figurent nécessairement les processus système, qui servent (entre autres) à gérer les périphériques (clavier, souris, écran, disque, lecteur de CD, imprimantes, etc.) et les fichiers.

Les entrées-sorties

Jusqu'à présent, nous avons essentiellement décrit l'ordinateur du point de vue du processeur et de la mémoire vive. Le processeur exécute en permanence des programmes chargés en mémoire. Ces programmes sont des suites d'instructions machine et de plages mémoire réservées aux données.

Mais si les instructions exécutées par le processeur ne font que transférer des informations entre la mémoire vive et les registres, comment peut-il communiquer avec le clavier ? Avec la souris ? Avec l'écran ?

Comment les plages mémoire réservées aux données peuvent-elles recevoir les données de l'utilisateur ?

Comment peuvent-elles s'afficher à l'écran ?

Pour répondre à ces questions, il nous faut tout d'abord compléter notre vision d'un ordinateur et considérer les éléments externes à l'unité centrale que l'on appelle les périphériques (écran, clavier, souris, disque, lecteur de CD, clé USB, imprimante, etc.).

Les périphériques ont leurs propres circuits spécialisés appelés interfaces d'entrées-sorties. Pour l'écran, par exemple, il s'agit de la carte graphique. Ces circuits peuvent avoir leurs propres registres, leur propre mémoire et même leurs propres processeurs.

Ils communiquent avec le processeur et la mémoire par un ensemble de fils que l'on appelle le bus de données.

En réalité, les instructions exécutées par le processeur ne se limitent pas à la mémoire centrale. Certaines instructions spéciales, appelées instructions d'entrée-sortie, permettent de transférer des données entre les interfaces d'entrée-sortie et les registres du processeur ou directement avec la mémoire vive.

Ceci explique très sommairement comment se fait la communication entre le processeur et les périphériques et indirectement, comment un programme peut envoyer des informations vers un périphérique ou recevoir des informations provenant d'un périphérique.

Mais cela n'explique pas l'interaction avec l'utilisateur. En effet, vous remarquerez que votre ordinateur réagit instantanément (ou quasiment) lorsque vous éjectez un CD, introduisez une clé USB, bougez la souris, appuyez sur une touche du clavier, etc.

Ces actions physiques de l'utilisateur sur l'ordinateur peuvent se produire à n'importe quel moment, alors que le processeur est occupé à exécuter les instructions machine d'un programme quelconque. Or c'est nécessairement le processeur qui doit réagir à ces actions.

Il faut donc qu'il soit capable d'interrompre l'exécution du programme en cours pour réagir immédiatement à n'importe quelle action de l'utilisateur.

Comment cela est-il possible ?

Les interruptions

La réponse à cette question se trouve dans le mécanisme d'interruption.

Une interruption (physique) est un signal envoyé au processeur par un dispositif externe à l'unité centrale.

Le clavier va par exemple envoyer un signal d'interruption au processeur chaque fois qu'une touche est appuyée. La souris envoie une interruption chaque fois qu'elle est déplacée ou que l'utilisateur effectue un clic.

Le processeur possède des entrées spécialement prévues pour recevoir ces signaux.

Lorsqu'il reçoit une interruption, il interrompt provisoirement le programme qu'il est en train d'exécuter pour exécuter des instructions du système d'exploitation prévues, pour traiter ce type d'interruption. Il existe par exemple des instructions prévues pour traiter les interruptions clavier, d'autres pour traiter des interruptions souris, etc.

Le système ne gère pas entièrement les interruptions ; elles sont sous-traitées par les pilotes (drivers en anglais) des périphériques. Un pilote est un programme spécialisé dans la gestion d'un périphérique spécifique. Il ne fait pas partie du système d'exploitation, car il est généralement produit par le fabricant du périphérique.

Une fois l'interruption traitée, le processeur reprendra ce qu'il était en train de faire à l'endroit exact où il s'était arrêté.

Pour donner une image, on pourrait comparer ce mécanisme au réflexe humain. Prenons par exemple une personne lisant un livre. Le cerveau de cette personne joue le rôle du processeur. Le programme exécuté est la lecture du livre. Soudain, une abeille pique la personne. La douleur perçue est le signal d'interruption envoyé au cerveau (le processeur). Par réflexe, la personne lâche le livre. C'est une réaction préprogrammée dans l'inconscient (le système d'exploitation). Après avoir soigné la piqûre (traitement de l'interruption), la personne reprend la lecture du livre à l'endroit où elle s'était arrêtée.

Les Interfaces graphiques

Historique

Les premières interfaces graphiques sont apparues avec l'invention de la souris (1967) et des écrans graphiques, c'est-à-dire capables d'afficher des images.

Avant cela, les écrans d'ordinateur n'affichaient que du texte et ressemblaient plutôt à ceci :

En 1973 apparaît le premier ordinateur avec interface graphique : la station graphique Xerox Alto fabriquée en Californie, à Palo Alto, par la société Xerox (par ailleurs à l'origine de la machine à photocopier).

La station Xerox Alto

En 1983, cet ordinateur a inspiré le premier PC de Apple, le LISA, ancêtre des Macintosh puis en 1985, le premier système Windows (Windows 1.0) de Microsoft.

Le Lisa

C'est ainsi que sont apparus les premiers systèmes d'exploitation munis d'interfaces graphiques, dans lesquelles les applications apparaissent sous forme de fenêtres ou d'icônes sur lesquelles on peut agir grâce à la souris.

Interface du système

De manière générale, l'interface graphique d'un programme est la manière dont il se présente visuellement à l'écran lorsqu'il s'exécute.

Dans cette partie, nous décrivons plus particulièrement l'interface graphique du système qui apparaît au démarrage d'un ordinateur. Dans Windows, par exemple, elle est constituée du bureau et d'une barre de tâches.

Le bureau contient des icônes de différents programmes. La barre des tâches donne accès (entre autres) au menu Démarrer, par l'intermédiaire duquel on peut accéder à différents composants du système, configurer l'ordinateur, arrêter l'ordinateur, etc.

Tout ceci constitue l'interface graphique du système d'exploitation.

Le curseur

On voit également apparaître le curseur, qui se présente généralement sous la forme d'une petite flèche. Le curseur permet d'indiquer sur quel élément de l'interface graphique on veut agir.

Lorsque l'utilisateur déplace la souris, il provoque une interruption gérée par le système d'exploitation. Le traitement par défaut de cette interruption consiste à déplacer le curseur.

Gestion du clic

Comme nous l'avons vu, un clic provoque une interruption, qui est tout d'abord interceptée par le système d'exploitation. Le traitement de cette interruption dépendra de la position du curseur. Nous reviendrons là-dessus plus en détail un peu plus loin.

Lorsque l'on clique sur l'icône d'un programme, par exemple, le système le charge en mémoire et lance son exécution. L'interface graphique de ce programme s'affiche alors à l'écran.

Fenêtre d'une application

L'interface graphique d'un programme se présente généralement sous la forme d'un rectangle, appelé fenêtre, dans lequel apparaissent des composants ou contrôles. Les composants sont par exemple des menus, des barres d'outils, des zones de texte…

Voici par exemple une partie de l'interface graphique d'Excel 2003 :

Les zones de texte sont des zones rectangulaires de l'écran dans lesquelles il est possible de taper du texte sur une ligne. Nous reviendrons plus en détail sur ce composant graphique.

À un moment donné, l'écran contient une (ou plusieurs) fenêtre(s) pour chaque application ouverte. Par exemple, une fenêtre pour Internet Explorer, une fenêtre pour Word, une autre pour Photoshop, etc. Mais parmi toutes ces fenêtres, il y a une seule fenêtre active : c'est la fenêtre qui apparaît au premier plan et qui contient le curseur.

Les évènements

Lorsqu'il se produit une interruption souris (déplacement de la souris ou clic) ou clavier (touche appuyée ou relâchée), il y a deux possibilités :

1. Il n'y a aucune fenêtre active. Dans ce cas, l'interruption est directement gérée par le système. Sous Windows, c'est par exemple le cas lorsque vous cliquez sur le bureau ;

2. Il y a une fenêtre active. Dans ce cas, le système intercepte tout d'abord l'interruption, mais il ne la gère pas forcément entièrement. Une partie de la gestion de l'interruption est sous-traitée à l'application associée à cette fenêtre. Pour cela, il communique toutes les informations utiles à l'application sous la forme d'un évènement. Les parties d'une l'application qui interceptent les évènements et les gèrent sont appelés des gestionnaires d'évènements.

Un évènement est lié à un composant particulier de la fenêtre, que l'on pourrait appeler le composant actif : c'est le composant contenant le curseur au moment de l'évènement.

Une application pourra ainsi savoir sur quel composant l'utilisateur a cliqué ou quel était le composant actif lorsqu'il a appuyé sur telle touche du clavier.

Les zones de texte

Les interfaces graphiques sont généralement conçues pour éviter au maximum de taper du texte. Mais il arrive que ce soit inévitable. Par exemple, pour donner le nom d'un fichier, une adresse web, une recherche par mots-clés, etc.

Dans une interface graphique, la saisie de texte se fait par l'intermédiaire des zones de texte, zones rectangulaires dans lesquelles il est possible de « rentrer » du texte.

Pour cela, l'utilisateur doit tout d'abord activer la zone de texte en cliquant dedans. Le curseur change alors de forme. En général, il se transforme en une barre verticale clignotante, ce qui signifie que la zone de texte est active et prête à recevoir du texte.

Lorsqu'une zone de texte est active, les caractères frappés au clavier viennent s'afficher à l'écran à l'intérieur de celle-ci. On a ainsi l'impression que les caractères sont directement transférés du clavier vers l'écran. En réalité, ils sont tout d'abord transférés en mémoire par le système d'exploitation.

En effet, à chaque zone de texte est associée une plage mémoire, appelée buffer, destinée à mémoriser les caractères frappés.

Supposons qu'une zone de texte soit active et que l'utilisateur appuie sur la touche « s ». Le système va alors transférer le caractère « s » dans le buffer associé à cette zone de texte, puis dans un deuxième temps, afficher le contenu du buffer à l'écran à l'intérieur de la zone de texte.

S'il appuie ensuite sur la touche « t », le caractère « t » va être rajouté dans le buffer après le « s », puis le buffer va être réaffiché.

Ce mécanisme est très important, car c'est essentiellement de cette manière que des données sont communiquées à un programme. Nous reviendrons là-dessus lorsque nous parlerons de la lecture des données.

La programmation

Les langages de programmation ont été introduits pour éviter l'écriture directe de programmes en langage machine (tâche extrêmement fastidieuse, pour ne pas dire impossible !).

Mais tout d'abord, qu'est-ce qu'un programme ?

Notion de programme

Un programme écrit dans un langage de programmation se présente sous la forme d'un texte dans lequel figurent les instructions à exécuter. Dans ce texte, les plages mémoire contenant des données portent un nom. Ce sont les variables du programme.

Voici par exemple une instruction écrite en Pascal :

somme := x + y;

Cette instruction signifie : additionner les nombres contenus dans les variables x et y et stocker le résultat dans la variable somme.

Dans les cours de programmation, un programme est souvent défini comme une suite d'instructions. Cette définition n'est pas tout à fait exacte, car les programmes contiennent également des déclarations. Une déclaration permet (entre autres) de définir la nature des variables sur lesquelles le programme doit travailler.

Voici par exemple une déclaration de variable écrite en Pascal :

var x, y , somme : integer;

Ici le programmeur a déclaré que les variables x, y et somme doivent contenir des nombres entiers.

Notion de langage

Un langage de programmation est un ensemble de conventions définissant la manière d'écrire un programme sous la forme d'un texte.

Ces conventions comprennent des conventions syntaxiques (comparables à la grammaire d'une langue naturelle) et lexicales (comparables à l'orthographe d'une langue naturelle).

Elles sont évidemment différentes d'un langage à l'autre. Voici par exemple l'instruction d'addition vue plus haut traduite dans différents langages de programmation :

On constate que dans certains langages (comme PHP et LISP), les déclarations de variables ne sont pas obligatoires. Ces langages sont dits non typés.

Tous les langages de programmation possèdent des mots spéciaux, que l'on appelle des mots-clés (dans l'exemple ci-dessus, ces mots figurent en gras). Les mots-clés ont une signification particulière prédéfinie dans le langage de programmation. Ils ne peuvent donc pas être utilisés par le programmeur pour signifier autre chose.

En Pascal, par exemple, le mot-clé Integer signifie nombre entier.

Fichiers sources

Un programme écrit dans un langage de programmation se présente sous la forme d'un ou plusieurs fichiers appelés fichiers sources (par opposition aux fichiers exécutables).

Ces fichiers contiennent les programmes tels qu'ils ont été initialement écrits par les développeurs. Ce sont des textes écrits en un langage de programmation particulier, que l'on peut visualiser à l'aide d'un éditeur de texte (WordPad, par exemple).

Mais, comme nous l'avons déjà dit plus haut, le seul langage de programmation directement exécutable par un ordinateur est le langage machine adapté à son processeur.

Les fichiers sources doivent donc être traduits en langage machine avant de pouvoir être exécutés. On appelle cela la compilation. Mais nous verrons plus loin que la compilation n'est pas la seule approche possible : il en existe une autre appelée interprétation.

Compilation

Notion de compilateur

Certains langages, le C par exemple, ne peuvent être que compilés. Les langages de ce type sont appelés langages compilés. Pour pouvoir les utiliser, vous devez avoir installé sur votre ordinateur un compilateur spécifique à ce langage.

Le compilateur est un programme exécutable qui permet de traduire un programme écrit dans un langage de programmation en langage machine.

Par exemple, pour pouvoir programmer en C, vous devez nécessairement avoir installé un compilateur C sur votre machine.

Après avoir été compilé, le programme peut être exécuté en chargeant sa version exécutable en mémoire  :

Exemple de compilation

Pour fixer un peu les idées, reprenons le programme d'addition en Pascal et voyons comment il pourrait être traduit en langage machine.

Les variables déclarées sont tout d'abord associées à des plages mémoires :

Les instructions sont ensuite traduites en instructions machine portant sur les registres et les plages mémoire associées aux variables :

Dans cet exemple, nous n'avons pas donné le code binaire des instructions machine, car il serait sans intérêt. On voit qu'une simple instruction écrite en langage de programmation peut correspondre à de nombreuses instructions machine.

Interprétation

Les programmes écrits en langages interprétés (Java, Visual Basic, PHP…) peuvent être exécutés sans avoir été auparavant entièrement traduits en langage machine.

Ils font appel à un interpréteur.

Tout comme un compilateur, un interpréteur est spécifique à un langage donné.

Il existe, par exemple, un interpréteur Java, un interpréteur Visual Basic, un interpréteur PHP…

Contrairement aux programmes compilés, les programmes interprétés ne sont pas directement exécutés par l'ordinateur, mais par l'interpréteur (qui, lui, est exécuté par la machine !).

Pour exécuter un programme interprété, il faut tout d'abord lancer l'interpréteur, puis charger le code source du programme en mémoire. Ce code source peut ensuite être exécuté par l'interpréteur :

L'interpréteur s'exécute donc toujours « en même temps » que le programme interprété.

Notez que certains interpréteurs (comme Visual Basic) peuvent également fonctionner en tant que compilateur. Cela permet d'obtenir une version plus rapide et commercialisable du programme, lorsque la mise au point est terminée. En effet :

- l'exécution d'un programme compilé est beaucoup plus rapide que son interprétation ;

- sous sa forme exécutable, un programme peut être vendu sans divulguer les fichiers sources, ainsi qu'à des utilisateurs ne possédant pas l'interpréteur sur leur machine.

Les EDI

Le développement d'application (ou si vous préférez la programmation) se fait aujourd'hui grâce à des logiciels appelés EDI (pour Environnement de Développement Intégré) ou IDE en anglais. Un EDI peut être adapté à un langage de programmation particulier (Delphi, Lazarus pour le langage Pascal, Visual C++ pour le langage C++) ou à plusieurs langages (Eclipse pour Java, PHP, JavaScript).

Un EDI comprend en général un éditeur de texte spécialement adapté au langage, un compilateur et/ou un interpréteur, un débogueur (outil de mise au point) et, surtout, un outil de développement d'interface graphique.

Avant l'existence des EDI, la conception de l'interface graphique d'un programme était extrêmement fastidieuse, car le développeur devait lui-même préciser dans le programme les positions et dimensions de tous les composants de l'interface.

Avec un EDI cela se fait simplement en « dessinant » en quelque sorte l'interface graphique à l'aide de la souris. Évidemment, les instructions permettant d'afficher l'interface existent toujours, mais au lieu d'être données par le programmeur, elles sont générées automatiquement par l'EDI.

La programmation évènementielle

Avec l'apparition des interfaces graphiques dans les années 80, la programmation est devenue plus complexe. Les programmes devaient d'une part produire tout le design de l'interface et d'autre part pouvoir réagir aux évènements, c'est-à-dire aux actions de l'utilisateur sur les différents composants de cette interface.

Il est alors apparu une nouvelle manière de programmer, appelée programmation évènementielle, qui consiste à associer des gestionnaires d'évènements aux composants de l'interface.

Un des premiers langages permettant la programmation évènementielle a été le langage Visual Basic de Alan Cooper (première version en 1991) et son environnement de développement intégré (un des premiers également). Le concept a ensuite été repris par Borland avec Delphi.

Alan Cooper - inventeur du Visual Basic

Un des points les plus intéressants des EDI est qu'ils facilitent la programmation évènementielle. Rappelons qu'avec un EDI, l'interface graphique peut être « dessinée » par le programmeur. Il peut ensuite associer un gestionnaire d'évènements (initialement vide) à un composant de l'interface en cliquant sur celui-ci.

Lazarus

Lazarus est un environnement de développement intégré associé au langage Pascal Objet. C'est la version open source du logiciel Delphi de Borland. Ce logiciel facilite en particulier la création d'interfaces graphiques et la programmation évènementielle par un mécanisme de génération automatique de code.

Notion de formulaire

Sous Lazarus, une fenêtre s'appelle un formulaire.

Voici, par exemple, un formulaire réalisé avec Lazarus :

Ce formulaire comporte trois étiquettes, trois zones de texte et un bouton.

Nous avons vu que les zones de texte permettent à l'utilisateur de saisir des données. En fait, elles servent également à afficher des résultats.

Dans notre exemple, les deux zones de texte étiquetées X et Y servent à saisir les deux nombres à additionner. Ce sont les données du programme. La zone de texte étiquetée X+Y sert à afficher la somme de ces deux nombres. C'est le résultat du programme produit par l'ordinateur.

Les étiquettes servent donc en principe à indiquer à l'utilisateur quelle donnée il doit introduire dans la zone de texte située juste à côté ou bien quel résultat y sera affiché.

Lorsque l'utilisateur clique sur le bouton Additionner, après avoir saisi les deux nombres dans les zones de texte prévues à cet effet, le programme affiche le résultat dans la zone de texte étiquetée X+Y.

Un bouton sert donc à déclencher un traitement d'information particulier (dans notre cas, l'addition de deux nombres).

Génération automatique de code

La création d'une nouvelle application engendre automatiquement la création d'un formulaire (initialement vide) associé à un fichier source Pascal appelé unité (Unit en anglais).

Le formulaire de départ

Par défaut, ce formulaire s'appelle Form1 et l'unité Pascal associée, Unit1. Si le développeur ne la renomme pas autrement, elle sera représentée sur disque par le fichier Unit1.pas.

Dès la création du formulaire, l'unité qui lui est associée contient du code Pascal généré automatiquement.

Le voici :

Au fur et à mesure que le programmeur ajoute des composants sur le formulaire, leurs noms apparaissent automatiquement dans le code source. Avec l'exemple précédent du programme d'addition, ce serait Label1, Label2, Label3 pour les étiquettes ; Edit1, Edit2, Edit3 pour les zones de texte ; Button1 pour le bouton.

Voici le code source de l'unité associée au programme d'addition, après que le programmeur a déposé les différents composants sur le formulaire :

Ces noms permettront d'accéder aux composants par programme. Il ne faut pas les confondre avec leurs libellés. Les libellés apparaissent sur l'interface graphique. Les noms quant à eux sont invisibles.

Par exemple, le nom du bouton est Button1, mais son libellé est Additionner.

Pour pouvoir plus facilement identifier les composants dans le programme, le développeur peut leur donner des noms plus significatifs que ceux générés automatiquement par Lazarus. Par exemple, BoutonAdditionner au lieu de Button1 pour le bouton, ZoneTexteX et ZoneTexteY pour les zones de texte contenant les deux nombres à additionner, ZoneTexteSomme pour la zone de texte contenant la somme des deux nombres.

La modification des noms des composants se fait via l'interface graphique de Lazarus. Ils sont alors automatiquement modifiés dans le code.

Voici le code modifié automatiquement après modification des noms :

Programmation évènementielle

L'interface graphique de Lazarus est conçue pour faciliter la programmation évènementielle.

Rappelons que la programmation évènementielle consiste à associer des gestionnaires d'évènements à des évènements de l'interface graphique. Un évènement est défini par un composant de l'interface et un type d'interruption. Par exemple, un bouton (composant) et un clic sur ce bouton (interruption). Lorsque cet évènement a effectivement lieu, les instructions contenues dans le gestionnaire d'évènements sont automatiquement exécutées.

Sous Lazarus, un gestionnaire d'évènements s'appelle une procédure évènementielle.

Pour associer une procédure évènementielle à un composant, le développeur n'a besoin de taper aucune ligne de code. Il lui suffit en gros de cliquer sur le composant et éventuellement de sélectionner le type d'interruption. Un double-clic sur un composant sélectionne l'interruption par défaut. La déclaration de la procédure évènementielle est alors automatiquement générée dans le code avec un nom formé à partir du nom du composant et du nom de l'interruption.

Reprenons l'exemple de notre programme d'addition.

Supposons que le développeur souhaite associer une procédure évènementielle au bouton Additionner, qui serait activée lorsque l'utilisateur clique sur ce bouton.

Pour cela, il lui suffit de faire un double-clic sur ce bouton et la déclaration de cette procédure évènementielle sera automatiquement générée dans le code source, avec aucune instruction à l'intérieur.

Le nom de cette procédure est BoutonAdditionnerClick. Il est formé à partir du nom du composant (ici BoutonAdditionner) et du nom de l'interruption (Click = clic de souris).

Le développeur complètera ensuite ce gestionnaire d'évènements en plaçant des instructions à l'intérieur (entre begin et end), qui seront automatiquement exécutées lorsque l'utilisateur cliquera sur ce bouton.

Ordre d'exécution des instructions

En général, un programme ne s'exécute pas dans l'ordre des lignes du code source, c'est-à-dire de la première à la dernière ligne du fichier.

Considérons un programme dont l'interface graphique possède trois boutons A, B et C :

Dans le code source, on a d'abord la procédure évènementielle du bouton B, puis celle du bouton A, puis celle du bouton C. Mais l'ordre d'exécution de ces procédures est totalement indépendant de l'ordre dans lequel elles figurent dans le fichier source. Cela dépend évidemment de l'ordre dans lequel l'utilisateur cliquera sur les boutons A, B et C. Une même procédure peut même être exécutée plusieurs fois.

Lorsqu'une procédure évènementielle a fini de s'exécuter, le programme retourne dans un état d'attente d'évènement.

Notice d'utilisation

Téléchargez ici la notice d'utilisation de Lazarus.

Les variables

La notion de variable

La notion de variable existe dans tous les langages de programmation.

Les variables servent à conserver temporairement des données en mémoire.

Une variable peut être vue comme une plage mémoire dans une zone de données d'un programme. Elle porte un nom et possède à un instant donné une valeur.

On peut donc parler de l'adresse d'une variable : c'est l'adresse du début la plage mémoire qui lui est réservée.

Voici par exemple quatre variables nommées Age, Nom, NombreEnfant et Prenom.

La valeur d'une variable varie lors de l'exécution d'un programme (d'où le nom de variable justement !). La variable NombreEnfant pourra par exemple valoir successivement 2, 0, 3, 1, 0, 4…

Par contre, le nom d'une variable ne change pas : il est le même du début à la fin de l'exécution d'un programme.

Les noms de variables en Pascal

En Pascal, les noms de variables doivent respecter les règles suivantes :

Règle1 : le premier caractère est obligatoirement une lettre ;

Règle2 : les seuls caractères autorisés sont les lettres (sans accents), les chiffres et « _ » ;

Règle3 : un nom de variable ne peut pas être un mot-clé du langage.

Voici quelques exemples et contre-exemples de nom de variables :

Nom

Validité

Règles non respectées

Prenom

Oui

Prénom

Non

2

Nom101

Oui

Nom_Eleve

Oui

Nom Eleve

Non

2

_Age

Non

1

Age-Eleve

Non

2

Integer

Non

3

Attention : la casse n'a pas d'importance, c'est-à-dire que le Pascal ne distingue pas les minuscules des majuscules. Nom_Eleve, nom_Eleve, nom_eleve désignent donc la même variable.

La notion de type

Il existe différents types de variables.

Le type d'une variable définit la manière dont elle est représentée en mémoire et les opérations que l'on peut faire dessus.

Il existe, par exemple, des variables de type chaîne de caractères, qui vont servir à stocker du texte en mémoire. La variable Prenom pourrait, par exemple, être de ce type. À un instant donné, sa valeur pourrait être 'Astérix', à un autre moment 'Obélix' ou encore à un autre moment 'Abraracourcix'.

Il existe aussi des variables de type entier. Ce pourrait, par exemple, être les variables Age, ou NombreEnfant.

La représentation en mémoire d'une variable dépend de son de type. En particulier, deux variables de types distincts n'occupent pas en général la même place mémoire :

D'autre part, les opérations que l'on peut faire sur deux variables de types différents ne sont pas les mêmes. Par exemple, on ne pourra pas multiplier deux chaînes de caractères, alors que l'on peut très bien multiplier deux entiers.

Déclaration d'une variable

La déclaration d'une variable sert à définir son type et, par conséquent, la manière dont elle sera représentée en mémoire. Elle a pour effet de réserver une plage mémoire pour stocker la valeur de cette variable. Le type de la variable définit la taille de cette plage et la manière de coder ses valeurs.

Toute variable doit (en principe) être déclarée, avant de pouvoir être utilisée. En Pascal, la déclaration est obligatoire. D'autres langages (comme Visual Basic) autorisent une déclaration implicite avec un type par défaut. Malheureusement, cela autorise également des erreurs de programmation…

La manière de déclarer une variable dépend évidemment du langage de programmation, mais on y trouve toujours les mêmes ingrédients : le nom de la variable et son type.

Déclaration d'une variable en Pascal

Syntaxe générale

Une déclaration de variable en Pascal utilise le mot-clé Var suivi du nom de la variable, du caractère « : », puis du nom du type et d'un point-virgule.

Var Nom de la variable : nom du type ;

Déclaration d'un entier

Prenons par exemple la variable Age.

En Pascal, le type entier s'appelle Integer. La déclaration de la variable Age se ferait donc comme ceci :

Var Age : Integer ;

Le type double

Les nombres entiers ne sont pas les seuls nombres avec lesquels un ordinateur peut calculer. Il y a également les nombres décimaux (comme 0.5 ou 3.14 par exemple). Ces nombres peuvent par exemple (il y a d'autres possibilités) être représentés par le type Double en Pascal.

Déclaration d'une chaîne de caractères

Prenons par exemple la variable Prenom, de type chaîne de caractères.

En Pascal, le type chaîne de caractères s'appelle String. La déclaration de la variable Prenom s'écrira donc :

Var Prenom : String ;

Regroupement des déclarations

Remarquez qu'il est inutile de répéter le mot-clé var. De plus, les variables de même type peuvent être regroupées : on sépare les noms des variables par des virgules puis on précise leur type par : nom du type ;

Exemple :

Var Nom, Prenom : String ; Age, NombreEnfant : Integer;

Où faut-il déclarer les variables ?

Pour qu'une variable soit utilisable à n'importe quel endroit d'une unité Pascal, elle doit être déclarée entre var et implementation, comme dans l'exemple suivant :

var
Form1: TForm1;
// Déclaration des variables globales
x, y , somme : integer;

implementation

Nous avons déclaré ici trois variables de type entier : x, y et somme.

Les variables déclarées de cette manière sont appelées variables globales. Cela signifie qu'elles peuvent être utilisées dans tout le code du fichier où elles sont déclarées.

Lecture et affichage

Données et résultats d'un traitement

De nombreux traitements informatiques peuvent se schématiser suivant les trois étapes suivantes :

L'image du sourd-muet qui ne sait pas additionner

Pour bien comprendre le sens des termes lecture, traitement et affichage, mettez-vous à la place de l'ordinateur. Votre rôle est d'additionner mentalement deux nombres pour un sourd-muet qui ne sait pas calculer (l'utilisateur).

Comme il ne peut pas parler, il vous écrit les deux nombres à additionner sur un bout de papier, qui joue le rôle de l'écran.

Vous ne pouvez pas additionner les deux nombres si vous ne les connaissez pas. Il faut donc commencer par les lire. C'est la première étape de lecture des données.

À présent, les deux nombres à additionner sont dans votre mémoire. Vous pouvez donc effectuer le traitement, qui consiste à additionner mentalement ces deux nombres. C'est l'étape de traitement des données.

Vous avez le résultat. Mais si vous ne le communiquez pas à l'utilisateur, c'est comme si vous n'aviez rien fait. Comme il est sourd, vous allez écrire le résultat sur le bout de papier. C'est la troisième étape : l'affichage des résultats du traitement.

Pour un ordinateur

Pour un ordinateur c'est un peu la même chose :

1. Lire les données signifie transférer des informations affichées à l'écran vers la mémoire, ou plus précisément vers une variable-donnée, c'est-à-dire une variable prévue pour stocker une donnée du traitement.

2. Traiter les données signifie grosso modo faire des calculs qui dépendent des données pour produire des résultats. En principe, mais ce n'est pas obligatoire, chaque résultat est stocké dans une variable-résultat. La programmation de cette étape sera vue plus loin dans le cours lorsque nous parlerons d'expression et d'affectation.

3. Si les résultats ont été sauvegardés dans des variables-résultats, Afficher les résultats signifie afficher la valeur de chacune de ces variables à l'écran. Il est également possible d'afficher les résultats d'un traitement sans les avoir au préalable sauvegardés dans des variables. Mais il serait difficile pour l'instant d'expliquer comment. Nous verrons cela un peu plus loin.

Exemple du programme d'addition

Reprenons l'exemple de l'addition de deux nombres.

Lire les données signifierait dans ce cas lire les valeurs des deux nombres à additionner depuis (par exemple) deux zones de texte pour les stocker dans deux variables données X et Y.

Traiter les données signifierait additionner les valeurs des variables X et Y, puis mémoriser le résultat dans une variable Somme.

Enfin, afficher les résultats signifierait afficher la valeur de la variable somme dans la zone de texte prévue à cet effet.

Historique

Avant l'apparition des interfaces graphiques, tous les langages de programmation avaient des procédures (cette notion sera abordée dans le chapitre sous-programmes) pour lire et afficher.

En Pascal, par exemple, elles s'appellent readln (pour la lecture) et writeln (pour l'écriture).

La procédure writeln

La procédure writeln vous permettait d'afficher un texte à l'écran. Par exemple l'instruction :

writeln ('Hello world !');

vous affichait le texte « Hello Word » à l'écran, comme ceci (en supposant que l'écran était vide avant l'exécution de l'instruction) :

Avec la procédure writeln, vous pouviez également afficher la valeur d'une variable à l'écran. Par exemple, en supposant que la variable Somme contienne la valeur 2013, l'instruction :

writeln (Somme);

vous affichait « 2013 » à l'écran, comme ceci :

Chaque writeln affichait donc une nouvelle ligne à la suite des lignes déjà affichées à l'écran.

La procédure readln

La procédure readln permettait de lire la valeur d'une variable tapée au clavier (et donc affichée à l'écran).

Supposons par exemple qu'un programme exécute l'instruction suivante :

readln (X);

où X représente un nombre entier.

Dès que l'instruction est exécutée, le programme est momentanément interrompu. Le système d'exploitation prend alors la main et range chaque caractère saisi par l'utilisateur dans un buffer associé au clavier. Dès que l'utilisateur appuie sur la touche Entrée, le contenu du buffer est renvoyé au programme qui stocke alors la valeur correspondante dans la variable X et continue son exécution.

ETBib

ETBib est une unité Pascal que j'ai écrite afin d'introduire des procédures équivalentes à readln et writeln fonctionnant avec une interface graphique.

Ces procédures sont les suivantes :

- Lire, LireEntier et LireNombre : pour lire une variable depuis une zone de texte ;

- Afficher, AfficherEntier, et AfficherNombre : pour afficher une variable dans une zone de texte ou une zone de liste ;

- Effacer : pour effacer le contenu d'une zone texte ou d'une zone de liste.

Ces procédures ont été introduites pour des raisons pédagogiques (explications pour le lecteur averti). Donc gardez bien à l'esprit qu'elles ne font pas partie du langage Pascal.

Exemple d'utilisation des procédures de ETBib

Pour expliquer le fonctionnement des procédures de ETBib, nous allons prendre un exemple de programme qui permet de lire et d'afficher trois variables :

- Ville, de type String, est destinée à contenir le nom d'une ville. Sa valeur sera lue et affichée dans la zone de texte ZT_Ville ;

- CP, de type integer, pour le code postal de la ville, lue et affichée dans la zone de texte ZT_CodePostal ;

- NH, de type double, pour le nombre d'habitants de la ville exprimé en milliers. Remarquez que ce nombre n'est pas forcément entier : 264,2 représente par exemple 264 200 habitants. La zone de texte correspondante se nomme ZT_Hab.

Voici l'interface graphique du programme :

Lecture des variables

La lecture des trois variables est déclenchée par le bouton Lire. Voici la procédure évènementielle associée :

procedure TForm1.BT_LireClick(Sender: TObject);
begin
Lire (Ville,ZT_Ville);
LireEntier (CP, ZT_CodePostal);
LireNombre (NH,ZT_Hab);
end;

La procédure de lecture à utiliser dépend du type de la variable : Lire pour lire une variable de type String, LireEntier pour lire une variable de type Integer et LireNombre pour lire une variable de type Double.

Entre parenthèses figurent :

- en première position : le nom de la variable à lire ;

- en deuxième position : le nom de la zone de texte.

Rappelez-vous que chaque zone de texte est associée à un buffer, qui se remplit avec les caractères frappés au clavier.

Lorsque la variable est de type chaîne de caractères, la procédure de lecture ne fait que recopier ces caractères dans la variable.

S'il s'agit d'une variable de type Integer ou Double, il ne suffit pas de recopier le buffer dans la variable. Le contenu du buffer est tout d'abord codé en base 2, puis c'est le résultat de ce codage qui est stocké dans la variable.

Affichage des variables

L'affichage des trois variables se fait par le bouton Afficher, dont voici la procédure évènementielle :

procedure TForm1.BT_AfficherClick(Sender: TObject);
begin
Afficher (Ville, ZT_Ville);
AfficherEntier (CP, ZT_CodePostal);
AfficherNombre (NH, ZT_Hab);
end;

Comme pour la lecture, il faut choisir la procédure d'affichage en fonction du type de la variable : Afficher pour afficher une variable de type String, AfficherEntier pour une variable de type integer et AfficherNombre pour une variable de type Double.

Dans les parenthèses, on précise le nom de la variable à afficher, puis le nom de la zone de texte dans laquelle on souhaite l'afficher.

La procédure Afficher recopie simplement le contenu de la variable dans le buffer, qui est ensuite affiché dans la zone de texte par le système d'exploitation.

Dans le cas des procédures AfficherNombre et AfficherEntier, un décodage préalable est nécessaire avant de recopier la chaîne de caractères représentant le nombre dans le buffer.

Effacement des zones de texte

L'effacement des zones de texte s'effectue avec le bouton Effacer, dont voici la procédure évènementielle :

procedure TForm1.BT_EffacerClick(Sender: TObject);
begin
Effacer (ZT_Ville); Effacer (ZT_Hab);
Effacer (ZT_CodePostal);
end;

Il suffit ici de préciser entre parenthèses le nom de la zone de texte à effacer.

Argumentation pédagogique

Cette section s'adresse au lecteur averti (professeur ou élève avancé) désirant comprendre la raison pour laquelle j'ai introduit les procédures de lecture et d'affichage.

Reprenons notre exemple. Voici comment lire les trois variables sans utiliser les procédures Lire, LireEntier et LireNombre :

procedure TForm1.BT_LireClick(Sender: TObject);
begin
Ville := ZT_Ville.Text;
CP := StrToInt(ZT_CodePostal.Text);
NH := StrToFloat (ZT_Hab.Text);
end;

Ce qui me gêne ici est l'utilisation obligatoire de la programmation objet. On accède à l'attribut Text de la classe TEdit. C'est encore pire lorsqu'il s'agit d'afficher du texte dans une zone de liste, puisqu'il faut alors appliquer la méthode Add à l'attribut items (objet de la classe TStringList) d'un objet de la classe TListBox.

Or, je pense que l'on peut difficilement commencer un cours de programmation avec des notions de programmation objet. À mon sens, la programmation objet ne peut être abordée qu'après avoir bien assimilé les types structurés (puisque les objets sont en quelque sorte des types structurés étendus) et les sous-programmes (puisque les méthodes sont des sous-programmes associés à des classes).

Mis à part la programmation objet, d'autres arguments peuvent être ajoutés en faveur de l'utilisation des procédures de lecture et d'affichage :

- cela rend le code plus lisible ;

- cela oblige les élèves à programmer plus proprement : les données doivent obligatoirement être transférées dans des variables avant d'être utilisées. On ne travaille pas directement sur les zones de texte pour faire des calculs ;

- on voit mieux la différence entre une lecture (ou un affichage) et une simple affectation.

Les expressions

Vous connaissez déjà tous les expressions mathématiques dans lesquelles on retrouve des nombres, des opérateurs de calculs (+, -, etc.) et des variables. Par exemple :

(a + b) &#215; (a - 17)

Dans les langages de programmation, on retrouve également cette notion d'expression sous une forme un peu différente et plus générale.

Une expression est une écriture possédant une valeur et un type, dans laquelle figurent (entre autres) des littéraux, des constantes, des opérateurs et des noms de variables.

Vous savez déjà ce que sont les variables. Par contre, nous n'avons pas encore défini les littéraux, les constantes et les opérateurs.

La valeur d'une expression est le résultat du calcul de cette expression, en tenant compte des valeurs des variables qu'elle contient. Son type est le type du résultat.

L'écriture Age+1, est, par exemple, une expression de type entier, car :

- la variable Age est de type entier ;

- 1 est un entier ;

- la somme de deux entiers est un entier.

Sa valeur est celle de la variable Age augmentée de 1.

Une expression peut faire intervenir plusieurs opérations. Par exemple :

(Age + 1) * 2

représente le double de la valeur de la variable Age augmentée de 1.

Voilà différentes valeurs des expressions Age+1 et (Age+1)*2 pour différentes valeurs de la variable Age :

Age

Age+1

(Age + 1) * 2

10

11

22

0

1

2

43

44

88

Les littéraux

Un littéral est l'écriture d'une valeur d'un certain type de variable.

L'écriture des littéraux obéit à un certain nombre de conventions qui dépendent du type et du langage de programmation.

Les littéraux numériques

Un littéral numérique représente la valeur d'un nombre.

Il existe de nombreuses manières d'écrire un nombre. On peut par exemple avoir :

- une simple suite de chiffres. Cette écriture ne convient que pour les nombres entiers positifs. Exemple : 2012 ;

- pour pouvoir représenter des entiers relatifs, il faut autoriser la présence d'un signe + ou - devant cette suite de chiffres. Exemple : -15, +320 ;

- pour pouvoir représenter des nombres non entiers, on utilise le point. La partie entière du nombre est suivie d'un point puis d'une autre suite de chiffres. Exemples : 3.141559, -5.33333 ;

- pour pouvoir représenter des nombres très grands ou très petits, on autorise également la notation scientifique. Exemple : 1.7E+308.

Les littéraux chaînes de caractères

Un littéral de type chaîne de caractères s'écrit entre quotes.

Par exemple : 'Il fait beau', '4012', '(3 x + 1)', 'VisualC++', '@%^šÂ€' sont des littéraux de type chaîne de caractères.

On remarquera que tous les caractères sont autorisés entre les quotes sauf les quotes eux-mêmes ! Pour mettre une quote dans une chaîne de caractères, il faut la faire précéder par une autre quote. Par exemple :

L''horaire d''été'

Parmi les chaînes de caractères, il y en a une qui joue un rôle particulier : la chaîne vide.

La chaîne vide est une chaîne qui ne contient aucun caractère. On l'écrit '' (deux quotes).

Les constantes

Une constante est un littéral portant un nom. Avant la compilation (ou l'interprétation) du code, ce nom sera remplacé par le littéral dans tout le code.

Une constante est donc en quelque sorte le synonyme d'une valeur. On pourra par exemple introduire une constante PI représentant le nombre 3.14159. Avant la compilation du code (cette étape s'appelle en fait la précompilation), toutes les occurrences du mot PI seront remplacées par 3.14159.

Comme son nom l'indique, la valeur d'une constante ne peut pas varier durant l'exécution d'un programme.

Déclaration d'une constante en Pascal

En Pascal, une constante peut se déclarer de la manière suivante :

Const nom de constante = littéral ;

Le nom d'une constante obéit aux mêmes règles que le nom d'une variable.

Exemple :

Const PI = 3.14159 ;
CLEF_USB = 'F:' ;  DISQUE_DUR = 'C:';

Opérateur

En informatique, un opérateur est généralement représenté par un ou deux caractères représentant une certaine opération.

Voici, par exemple, quelques opérateurs fréquemment rencontrés dans les langages de programmation : +   -  /  *  ^  =  ++  !   %

Dans ce cours, nous nous concentrerons pour l'instant sur les opérateurs suivants :

- l'opérateur d'addition, représentée par le caractère + ;

- l'opérateur de soustraction,représenté par le caractère - ;

- l'opérateur de multiplication, représenté par le caractère * (et non pas × comme en mathématique !) ;

- L'opérateur de division, représenté par le caractère / ;

- l'opérateur de concaténation, représenté par le caractère +.

Vous connaissez déjà les quatre premiers opérateurs, puisqu'il s'agit des opérateurs mathématiques classiques. Mais si vous n'avez jamais fait de programmation, vous ignorez probablement le sens de l'opérateur de concaténation.

La concaténation

Concaténer deux chaînes de caractères signifie les mettre bout à bout.

Par exemple, si je concatène la chaîne 'para' avec la chaîne 'pluie', j'obtiens la chaîne 'parapluie'.

La manière de noter l'opérateur de concaténation dépend des langages.

En Pascal, il se note comme l'opérateur d'addition, c'est-à-dire avec un signe +.

L'opérateur de concaténation permet de construire des expressions de type chaîne de caractères.

Par exemple, 'Je suis '+Prenom est une expression de type chaîne de caractères constituée d'un littéral ('Je suis '), de l'opérateur de concaténation + et d'un nom de variable (Prenom).

On peut concaténer autant de chaînes de caractères qu'on le souhaite. Par exemple, dans l'expression 'Je suis '+Prenom+' '+Nom il y a quatre chaînes de caractères.

Voilà différentes valeurs de l'expression

'Je suis ' + Prenom

pour différentes valeurs de la variable Prenom :

Prenom

'Je suis '+Prenom

'Jimi'

'Je suis Jimi'

'Albert'

'Je suis Albert'

Voilà différentes valeurs de l'expression

'Je suis ' + Prenom + ' ' + Nom

pour différentes valeurs des variables Prenom et Nom :

Prenom

Nom

'Je suis '+Prenom+' '+Nom

'Jimi'

'Hendrix'

'Je suis Jimi Hendrix'

'Albert'

'Einstein'

'Je suis Albert Einstein'

'Albert'

''

'Je suis Albert'

Validité d'une expression

L'écriture d'une expression dans un certain langage de programmation doit nécessairement obéir aux règles propres à ce langage.

Une expression de type T est valide si elle peut être « comprise » par l'interpréteur (ou le compilateur) comme un calcul d'une valeur de type T ou plus généralement comme la construction d'un objet de type T.

Nous donnons ici, un certain nombre de règles non exhaustives permettant d'écrire des expressions valides en Pascal.

Expressions valides de type chaîne de caractères

Les règles suivantes permettent de construire des expressions de type chaîne de caractères valides :

Règle1 : un nom de variable est une expression valide si et seulement cette variable est de type chaîne de caractères ;

Règle2 : un littéral (ou une constante représentant ce littéral) est une expression valide si et seulement si il est du type chaîne de caractères ;

Règle3 : A+B est valide si et seulement si A et B sont deux expressions valides de type chaîne de caractères ;

Règle4 : un littéral, une constante ou un nom de variable ne pas être suivi immédiatement d'un littéral, d'une constante ou d'un nom de variable.

Exemples d'application de ces règles (Nom et Prenom sont des chaînes de caractères et Age est un entier) :

Expression

Validité

Règle non respectée

Prenom

Oui

Age

Non

1

'Thirion'

Oui

48

Non

2

'Eric'+'Thirion'

Oui

Prenom+Nom

Oui

Prenom+'Thirion'

Oui

'Cours de'+Prenom+'Thirion'

Oui

'Eric'+'Thirion a' +Age+' ans'

Non

3

'Eric'+'Thirion a' + 48 +' ans'

Non

3

Prenom Nom

Non

4

'Eric' 'Thirion'

Non

4

Prenom 'Thirion'

Non

4

Expressions valides de type numérique

Les règles suivantes (non exhaustives) permettent de construire des expressions numériques valides :

Règle1 : une variable est une expression numérique valide si et seulement si elle est de type numérique (integer ou double par exemple) ;

Règle2 : un littéral (ou une constante représentant ce littéral) est une expression numérique valide si et seulement si il est de type numérique ;

Règle3 : les écritures A+B, A-B, A*B, A/B sont des expressions numériques valides si et seulement si A et B sont des expressions numériques valides ;

Règle4 : un littéral, une constante ou un nom de variable ne peut pas être suivi immédiatement d'un littéral, d'une constante ou d'un nom de variable ;

Règle5 : (A) est une expression numérique valide si et seulement si A est une expression numérique valide ;

Règle6 : chaque parenthèse ouvrante correspond à une et une seule parenthèse fermante.

Exemples d'application de ces règles (avec Nom de type String, Age de type Integer et Poids de type Double) :

Expression

Validité

Règle non respectée

Nom

Non

1

Age

Oui

201

Oui

20.14

Oui

'20.14'

Non

2

Age * Poids

Oui

Age + Nom

Non

3

Poids/(Age*2.2)

Oui

Age Poids

Non

4

Age 49

Non

4

(Age * Poids)

Oui

(Age * Nom)

Non

5

(Poids/((Age*(2.2)))

Oui

Poids/((Age + 1 )

Non

6

Affichage d'expression

Les procédures d'affichage de l'unité ETBib permettent d'afficher la valeur d'une expression. Selon le type de l'expression, on utilisera Afficher, AfficherEntier ou AfficherNombre.

Expression de type chaîne de caractères

Pour afficher une expression E de type chaîne de caractères dans une zone de texte ou zone de liste Z, on utilisera l'instruction :

Afficher ( E, Z);

Exemple : la variable Nom contient le nom d'une personne et la variable Adresse son adresse. L'instruction suivante permet d'afficher le nom et l'adresse de la personne sur une seule ligne dans la zone de liste ZL_Client :

Afficher (Nom + '-' + Adresse, ZL_Client);

Le nom et l'adresse sont séparés par un tiret.

Expression numérique

Pour une expression numérique dont le résultat est toujours un nombre entier, on peut utiliser AfficherEntier ou AfficherNombre. Par contre, si le résultat n'est pas forcément entier, il faudra utiliser AfficherNombre.

Exemple 1 : un hôtel propose des chambres à 47 euros. La variable NJ contient le nombre de jours. L'instruction suivante permet d'afficher le prix du séjour dans la zone de texte ZT_PrixSejour :

AfficherEntier ( NJ * 47 , ZT_PrixSejour);

Dans cet exemple, on peut utiliser la procédure AfficherEntier, car le prix à payer est forcément un nombre entier.

Exemple 2 : supposons à présent que le prix d'une chambre soit variable et pas forcément entier. Ce prix est stocké dans une variable P de type double. Dans ce cas, on ne sait pas si le prix du séjour est un nombre entier. Il faut donc utiliser l'instruction :

AfficherNombre ( NJ * P , ZT_PrixSejour);

Traitement sans variables-résultats

Revenons sur le schéma lecture-traitement-affichage. À présent, nous pouvons expliquer comment afficher les résultats d'un traitement sans les sauvegarder dans des variables : il suffit d'écrire l'expression du traitement à l'intérieur de l'instruction d'affichage !

Voilà par exemple comment écrire le programme d'addition sans sauvegarder le résultat dans une variable :

var
Form1: TForm1;
// Déclaration des variables globales
x, y  : integer;

implementation

{ TForm1 }

procedure TForm1.BoutonAdditionnerClick(Sender: TObject);
begin
// Lecture des données
LireEntier (x,zoneTexteX);
LireEntier (y,ZoneTexteY);
// Traitement et Affichage du résultat
AfficherEntier (x + y, ZoneTexteSomme);

end;

Le calcul de l'addition se fait à l'intérieur de l'instruction d'affichage.

Dans des cas simples, cette manière de programmer peut être utilisée. Mais de manière générale, essayez autant que possible de séparer le traitement des données et l'affichage. Vous obtiendrez ainsi des programmes plus lisibles et plus faciles à modifier.

L'affectation

L'affectation est une instruction qui permet de modifier la valeur d'une variable. Plus précisément :

Affecter une valeur à une variable signifie enregistrer cette valeur dans la plage mémoire allouée à cette variable.

Affecter une valeur à une variable modifie donc l'état de la zone mémoire qui lui est allouée, ou autrement dit son « contenu ».

Reprenons notre exemple avec les quatre variables Age, NombreEnfant, Nom et Prenom. Supposons qu'à un instant donné l'état de la mémoire soit le suivant :

Si on affecte ensuite la valeur 33 à la variable Age, on obtient :

Et la zone mémoire allouée à la variable Age restera inchangée jusqu'à ce qu'une valeur différente lui soit affectée.

Écriture d'une instruction d'affectation en Pascal

En Pascal, on représente l'affectation par l'opérateur :=. Le nom de la variable figure à gauche de := et la valeur qu'on veut lui affecter, à droite.

Par exemple, affecter la valeur 33 à la variable Age s'écrira :

Age := 33 ;

De manière générale, ce qui figure à droite de := est une expression. Une affectation s'écrit donc sous la forme :

Nom de variable := expression ;

Une affectation est toujours exécutée de droite à gauche. c'est-à-dire que l'expression figurant à droite du signe := est d'abord évaluée et que sa valeur est ensuite enregistrée dans la variable dont le nom figure à gauche du :=.

Exemple de code Pascal avec des affectations

Voilà un premier exemple de code Pascal, principalement composé d'affectations :

Var Age, MonAge  : Integer;
begin
Age := 20 ;
MonAge := 47 ;
Age := Age+1 ;
Age := Age*2 ;
MonAge := Age ;
end;

Ce code commence par la déclaration de deux variables de type entier (Age et MonAge). Après la partie déclaration, on a cinq instructions d'affectation.

Voilà comment se déroule l'exécution de ce code :

Après l'exécution de l'instruction

Age vaut

MonAge vaut

Age := 20 ;

20

?

MonAge := 47 ;

20

47

Age := Age + 1 ;

21

47

Age := 2 * Age ;

42

47

MonAge := Age ;

42

42

Remarque : la première valeur de la variable MonAge est indéterminée. Tant que l'on n'a pas affecté de valeur à une variable, on ne sait pas ce qu'elle contient.

Affectation récursive

Dans une affectation, l'expression affectée à la variable peut elle-même contenir la variable. On dit alors que l'affectation est récursive.

Exemple 1

Ajouter le prix d'un article au prix total, se traduit par l'affectation récursive suivante :

PrixTotal := PrixTotal + Prix ;

Exemple 2

Augmenter une variable de 1, se dit incrémenter. C'est un cas particulier d'affectation récursive très fréquemment utilisé en programmation.

Par exemple, pour incrémenter le nombre d'articles :

NombreArticle := NombreArticle + 1 ;

Traitement itératif

Les affectations récursives sont en général utilisées dans des traitements itératifs, c'est-à-dire des traitements que l'on répète plusieurs fois. Chaque répétition d'un traitement itératif s'appelle une itération.

Illustrons ceci par un exemple.

Considérons un programme permettant de calculer la durée totale des morceaux d'un album. Voici son interface graphique :

Le bouton Ajouter ajoute le nouveau titre saisi dans la zone de liste et augmente la durée totale de l'album par la durée de ce morceau.

Nous avons ici un exemple de traitement itératif, puisque les mêmes opérations vont être effectuées chaque fois que l'utilisateur ajoute un album. Ou autrement dit, chaque fois qu'il clique sur le bouton Ajouter.

Voici, le code de la procédure évènementielle de ce bouton :

procedure TForm1.BT_AjouterClick(Sender: TObject);
begin
//--- Lecture des données
Lire (Titre, ZT_Titre);
LireEntier (Duree, ZT_Duree);

//--- Traitement des données
DureeT := DureeT + Duree;
Numero := Numero + 1;

//--- Affichage
AfficherEntier (Numero, ZL_Num);
Afficher (Titre, ZL_Titre);
AfficherEntier (Duree, ZL_Duree);
AfficherEntier (DureeT, ZT_DureeT);

end;

Le traitement consiste à :

- lire le titre du morceau et sa durée ;

- augmenter la durée totale (DureeT) par la durée du morceau ;

- incrémenter le numéro du morceau ;

- afficher le numéro, le titre et la durée du morceau dans les zones de liste prévues à cet effet ;

- afficher la durée totale.

Mais que vaut la durée totale au départ ? Quel est le numéro du premier morceau ?

Cela dépend de la valeur de ces variables avant la première itération, c'est-à-dire au démarrage du programme.

Il faudrait donc pouvoir affecter des valeurs de départ (on dit initialiser) aux variables DureeT et Numero. Sinon, on ne pourra pas connaître leur valeur initiale.

Mais avec ce que nous avons vu jusqu'à présent, nous n'avons aucun moyen de faire exécuter des instructions à l'ordinateur avant le premier clic sur un bouton.

Heureusement, il existe une procédure évènementielle qui est appelée dès le démarrage du programme. Elle s'appelle Form_Create. Pour l'ajouter au code, il suffit de double-cliquer sur le fond du formulaire.

C'est donc dans la procédure Form_Create, qu'il faut initialiser les variables.

Voici la procédure Form_Create de notre exemple :

procedure TForm1.FormCreate(Sender: TObject);
begin
//--- Initialisation
DureeT := 0;
Numero := 0;
end;

Comme cette procédure est déclenchée dès le démarrage du programme, nous saurons que la durée totale et le numéro seront nuls avant la première itération.

Exemple et conseils

L'objectif de la programmation n'est pas seulement de faire des programmes qui marchent.

En réalité, un programme n'est jamais terminé (du moins un programme volumineux tel que vous en rencontrerez en entreprise). Il faut donc pouvoir le modifier facilement, sachant que le développeur qui sera chargé de la mise à jour de votre programme ne sera pas forcément vous-même. Cela a deux conséquences :

1. Votre code doit être lisible, c'est-à-dire qu'en le lisant on devine facilement ce qu'il fait. ;

2. D'autre part, il doit être facilement modifiable.

Dans cette partie du cours, je vous donne à travers un exemple quelques conseils permettant de rendre un programme plus lisible et plus facile à modifier.

Mises à part la lisibilité et la facilité de modification, il existe d'autres critères importants en programmation que nous n'aborderons pas ici :

- la rapidité d'exécution du code. Cet aspect est le sujet essentiel de l'algorithmique, qui peut être vue comme la science de résoudre un problème par un programme de manière à ce que ce programme s'exécute le plus rapidement possible. Sachez simplement que la manière de programmer peut jouer énormément sur la vitesse d'exécution ;

- la concision du code, c'est-à-dire la résolution d'un problème avec un programme le plus court possible. Certains programmeurs sont capables d'écrire en deux lignes ce que d'autres écrivent en vingt lignes. Cet aspect est à mon avis moins important que les autres dans la mesure où un code concis est souvent peu lisible.

Exemple de programme et conseil

Cahier des charges

La réalisation d'un projet informatique démarre en général avec un cahier des charges. C'est en gros une description de ce que devrait faire le logiciel.

Voici le cahier des charges de notre exemple :

On souhaiterait écrire un programme qui calcule la surface à peindre dans une pièce rectangulaire possédant des portes et des fenêtres.

Pour simplifier, on supposera que les murs ont 2m50 de hauteur et que toutes les fenêtres ont la même dimension (1.4 × 1.2) et de même pour les portes (0.9 × 2.1).

Déclaration des constantes

Introduisons une constante pour chaque donnée fixe du problème :

const
LargeurFenetre = 1.4;
HauteurFenetre = 1.2;
LargeurPorte = 0.9;
HauteurPorte = 2.1;
HauteurMur = 2.5;

On pourrait se passer de ces déclarations et mettre directement les valeurs 1.4, 1.2, etc. dans les calculs, mais on obtiendrait ainsi un code moins lisible et plus difficile à mettre à jour.

S'il faut corriger la hauteur des portes par exemple, il suffit de changer la valeur de la constante et cette modification sera automatiquement répercutée dans tout le code (dans tous les calculs où la hauteur des portes intervient).

Essayez de procéder de cette manière dès que vous avez des valeurs fixées. Déclarez des constantes au lieu d'écrire les valeurs dans les instructions.

Conception de l'interface graphique

La surface à peindre dépendra donc de la taille de la pièce ainsi que du nombre de portes et de fenêtres. Ce seront les données du programme. On en déduit l'interface graphique suivante :

Les zones de texte sont nommées ZT_Largeur (largeur de la pièce), ZT_Longueur (longueur de la pièce), ZT_NF (nombre de fenêtres), ZT_NP (nombre de portes) et ZT_SP (surface à peindre).

Les noms des zones de texte sont tous préfixés par ZT_. Cette manière de procéder permet d'éviter des conflits avec des noms de variables. En programmation, un même nom ne peut désigner qu'une seule chose. Vous ne pouvez pas, par exemple, nommer une zone de texte Largeur et utiliser par ailleurs une variable de même nom. Je vous propose de préfixer les noms des zones de texte par ZT_, ceux des zones de liste par ZL_ et ceux des boutons par BT_. Vous pouvez bien sûr avoir vos propres conventions. L'essentiel est d'en avoir une.

Lecture et déclaration des données

Associons une variable à chaque donnée : LargeurPiece, LongueurPiece, NFenetre et NPorte. D'où les déclarations :

Var LargeurPiece, LongueurPiece : Double;
NFenetre, NPorte : Integer;

Je n'ai pas besoin de vous expliquer ici la signification de chaque variable, car les noms choisis permettent de deviner la fonction de chacune d'entre elles. Pour obtenir un code lisible, il est recommandé de procéder de cette manière : utilisez des noms de variables parlants. Cette remarque est évidemment également valable pour les noms de constantes et pour, de manière générale, n'importe quel nom utilisé dans un programme.

Nous pouvons à présent écrire les instructions de lecture de données dans la procédure évènementielle du bouton Calculer :

procedure TForm1.BT_CalculClick(Sender: TObject);
begin
// Lecture des données
LireNombre (LargeurPiece, ZT_Largeur);
LireNombre (LongueurPiece, ZT_Longueur);
LireEntier (NFenetre, ZT_NF);
LireEntier (NPorte, ZT_NP);
end;

Traitement, résultats intermédiaires et finals

Il nous faut maintenant réfléchir à la manière de calculer la surface à peindre. Pour simplifier le problème, nous allons le découper le traitement en plusieurs étapes :

1. Calcul de la surface des murs ;

2. Calcul de la surface des fenêtres ;

3. Calcul de la surface des portes ;

4. Calcul de la surface à peindre.

Le résultat de chaque étape sera sauvegardé dans une variable : SurfaceDesMurs, SurfaceDesFenetres, SurfaceDesPortes, SurfacePeinture.

Essayez également de procéder de cette manière lorsque vous avez un calcul compliqué. Découpez le calcul en plusieurs parties et sauvegardez le résultat de chacun de ces calculs dans une variable. À la fin, écrivez le calcul du résultat final en utilisant les résultats des calculs intermédiaires. Cette manière de programmer simplifie la résolution du problème et rend le programme plus lisible.

Il nous faut d'abord déclarer les nouvelles variables :

Var LargeurPiece, LongueurPiece : Double;
NFenetre, NPorte : Integer;
SurfaceDesMurs, SurfaceDesPortes, SurfaceDesFenetres,
SurfacePeinture : double;

Vous pouvez également procéder de cette manière. Il est en général difficile de prévoir dès le début toutes les variables que l'on aura besoin d'utiliser dans un programme. Ajoutez vos déclarations de variable au fur et à mesure des besoins.

Écrivons à présent les quatre étapes de calcul.

Calcul de la surface des quatre murs :

SurfaceDesMurs :=
2 * (LargeurPiece+LongueurPiece) * HauteurMur;

Calcul de la surface des fenêtres :

SurfaceDesFenetres :=
NFenetre * LargeurFenetre * HauteurFenetre;

Calcul de la surface des portes :

SurfaceDesPortes :=
NPorte * LargeurPorte * HauteurPorte;

Calcul de la surface à peindre :

SurfacePeinture :=
SurfaceDesMurs  - SurfaceDesFenetres - SurfaceDesPortes;

Le code de la procédure en entier

Pour conclure, voici le code de la procédure évènementielle du bouton Calculer :

procedure TForm1.BT_CalculClick(Sender: TObject);
begin
//--- Lecture des données

LireNombre (LargeurPiece, ZT_Largeur);
LireNombre (LongueurPiece, ZT_Longueur);
LireEntier (NFenetre, ZT_NF);
LireEntier (NPorte, ZT_NP);

//--- Traitement des données

SurfaceDesMurs :=
2 * (LargeurPiece+LongueurPiece) * HauteurMur;

SurfaceDesFenetres :=
NFenetre * LargeurFenetre * HauteurFenetre;

SurfaceDesPortes :=
NPorte * LargeurPorte * HauteurPorte;

SurfacePeinture :=
SurfaceDesMurs  - SurfaceDesFenetres - SurfaceDesPortes;

// Affichage des résultats

AfficherNombre (SurfacePeinture,ZT_SP);
end;

Le code de cette procédure est bien séparé en trois parties : lecture des données, traitement des données, puis affichage des résultats. Essayez également de procéder de cette manière. Écrivez votre code en séparant bien la lecture, le traitement et l'affichage.

Synthèse

Voici un résumé de tous les conseils vus précédemment.

- Lorsque vous avez des valeurs fixées, utilisez des constantes.

- Préfixez les noms des composants de l'interface graphique (suggestion : ZT_ = zone de texte, ZL_ = zone de liste, BT_ = bouton).

- Utilisez des noms parlants.

- Lorsque vous avez un calcul (ou plus généralement un traitement) compliqué, découpez-le en plusieurs parties et sauvegardez les résultats de chaque partie dans des variables.

- Séparez bien la lecture des données, le traitement des données et l'affichage des résultats.

Exercices

Retrouvez différents exercices sur le site de l'auteur :

- recherches complémentaires sur internet ;

- prise en main de Lazarus ;

- programme sans interface graphique ;

- exercices de programmation ;

- exercices complémentaires ;

- exercices théoriques.

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Sous-programmes

But de ce chapitre

Les sous-programmes existent dans tous les langages de programmation, depuis les origines. En effet, les développeurs se sont vite rendu compte que dans la programmation on retrouve très souvent des traitements similaires. Avec les sous-programmes, il devient possible de regrouper ces traitements en un seul bout de code réutilisable, ce qui permet une économie de temps considérable. D'autre part, l'utilisation de sous-programmes rend le code plus lisible et plus facile à mettre à jour. C'est donc une bonne manière de programmer.

Dans ce chapitre, nous verrons comment écrire et utiliser des sous-programmes. Nous verrons également qu'il est possible de déclarer des variables à l'intérieur d'un sous-programme et que ces variables, appelées variables locales, se différencient subtilement des variables globales que nous avons déjà rencontrées dans le premier cours.

Introduction

Le but de la programmation n'est pas seulement de faire des programmes qui marchent. Ils doivent également être lisibles, faciles à mettre à jour et plus concis.

C'est essentiellement à cela que servent les sous-programmes.

Découpage d'un programme en sous-programmes

Écrire du code à l'aide de sous-programmes est avant tout une manière de programmer : au lieu de concevoir un programme en un seul bloc, on le découpe en plusieurs parties aussi indépendantes que possible. Chacune de ces parties porte un nom : ce sont les sous-programmes.

Bien entendu, cette manière de programmer n'est pas obligatoire. On peut très bien programmer sans jamais écrire de sous-programmes, mais cela donne des programmes très longs, peu lisibles et difficiles à mettre à jour.

Par analogie, on pourrait comparer ceci au découpage d'une entreprise en plusieurs services. Chaque service ayant un rôle particulier à jouer dans l'entreprise. De même, on peut concevoir un programme en plusieurs sous-programmes réalisant chacun un traitement particulier.

Le PC est une autre analogie intéressante. Dans un PC, chaque partie est facilement remplaçable par une autre : vous pouvez remplacer l'écran par un autre écran, la carte son par une autre carte son, le clavier par un autre clavier, etc. Cette possibilité de remplacer chaque partie par une autre partie compatible est possible grâce à la conception modulaire du PC. Un programme sans sous-programme est comme un PC en un seul bloc : impossible d'interchanger les parties.

Sous-programmes non évènementiels

Dans le premier cours, nous avons déjà vu quelques sous-programmes.

Reprenons l'exemple du projet Addition.

Lorsque l'utilisateur clique sur le bouton, la procédure évènementielle BoutonAdditionnerClick est automatiquement exécutée.

Cette procédure évènementielle est un exemple de sous-programme.

Une autre catégorie de sous-programmes, que j'appellerais sous-programmes non évènementiels, ne peuvent s'exécuter que s'il existe quelque part une instruction spéciale : l'appel de sous-programme.

De telles instructions figurent à l'intérieur de la procédure BoutonAdditionnerClick :

procedure TForm1.BoutonAdditionnerClick(Sender: TObject);
begin
LireEntier (x,zoneTexteX);              { <-- }
LireEntier (y,ZoneTexteY);              { <-- }
somme := x + y;
AfficherEntier (somme, ZoneTexteSomme); { <-- }
end;

LireEntier et AfficherEntier sont en fait des sous-programmes (non évènementiels). Dans cet exemple, les trois instructions marquées d'une flèche sont des appels de sous-programme. LireEntier est donc appelé deux fois et AfficherEntier, une fois.

Par contre, le code du projet Addition ne contient aucun appel du sous-programme BoutonAdditionnerClick. Comme il s'agit d'un sous-programme évènementiel, son exécution sera déclenchée par un évènement ; en l'occurrence, un clic sur le bouton Additionner.

Évidemment, l'exécution des sous-programmes LireEntier et AfficherEntier est également déclenchée, mais de manière indirecte. Si BoutonAdditionClick ne contenait pas des instructions d'appel de ces procédures, elles ne seraient jamais exécutées !

Les unités

Un projet Lazarus est généralement composé de plusieurs fichiers source Pascal appelés unités.

Dans le projet Addition, par exemple, nous avons deux modules :

- l'unité UnitAddition, qui contient la procédure évènementielle du bouton Additionner ;

- l'unité entrees_sorties, qui contient le code des procédures Lire, Afficher, etc.

De manière générale, chaque formulaire est associé à une unité contenant les procédures évènementielles. C'est l'unité qui gère l'interface graphique du programme. Dans notre exemple, c'est l'unité UnitAddition. Mais il peut y avoir plusieurs autres unités contenant le code de sous-programmes non évènementiels. Dans notre exemple, il n'y a qu'une seule unité de ce type. C'est l'unité entrees_sorties.

Procédures et fonctions

Jetons un Âœil dans le fichier entrees_sorties. On y trouve, par exemple, le code du sous-programme AfficherEntier que voici :

procedure AfficherEntier(x: integer;c:TObject);
begin
if c is TEdit then
(c as TEdit).Text:=IntToStr(x)
else
if c is TListBox then
(c as TListBox).Items.Add(IntToStr(x))
else
begin
ShowMessage('Erreur dans la fonction Afficher :
type du 2e paramètre non valide');
end;
end;

Pour l'instant, vous ne comprenez pas grand-chose à ce code, car il fait appel à plein de notions que vous n'avez pas encore vues (structures de contrôles, programmation objet). Notez simplement que la première ligne commence par le mot-clé procedure, suivi du nom du sous-programme. Cela signifie que le sous-programme appartient à la catégorie des procédures.

À droite du nom de la procédure, on trouve les paramètres. Dans cet exemple, il y a deux paramètres : x de type integer et c de type TObject.

Toute la première ligne du sous-programme constitue l'entête : elle contient donc le nom du sous-programme et ses éventuels paramètres. Tout ce qui suit est le corps du sous-programme. À l'intérieur du corps du sous-programme figurent donc les instructions que le sous-programme doit exécuter.

Les paramètres sont les données d'un sous-programme. Dans le cas de la procédure AfficherEntier, x est le nombre à afficher et c le composant de l'interface graphique (zone de texte ou zone de liste) dans lequel on souhaite afficher x.

Nous reviendrons plus en détail sur cette notion de paramètres. Pour l'instant, sachez que tout sous-programme peut avoir des paramètres ou ne pas en avoir.

D'autres sous-programmes du fichier commencent par le mot-clé function. Ce sont les fonctions. C'est par exemple le cas du sous-programme ZoneDeTexteVide :

function ZoneDeTexteVide (zt: TEdit) : boolean;
begin
ZoneDeTexteVide := zt.Text = '';
end;

Nous avons donc deux catégories de sous-programmes : les procédures et les fonctions. Les différences entre ces deux catégories seront vues dans la suite du cours.

Portée des variables

Une variable peut être de portée locale ou globale. Elle est globale si elle est déclarée en dehors de tout sous-programme. Au contraire, une variable locale est déclarée à l'intérieur d'un sous-programme.

Mais pour l'instant, nous n'avons pas défini ce qu'est un sous-programme de manière générale. Par contre, vous savez ce qu'est une procédure évènementielle. Or, les procédures évènementielles représentent une catégorie particulière de sous-programme.

Nous allons donc, pour l'instant, baser nos explications concernant la portée des variables en utilisant uniquement les procédures évènementielles. Gardez toutefois à l'esprit que ces explications seront valables pour n'importe quel type de sous-programme.

Les variables locales et globales ne se comportent pas de la même manière, notamment en ce qui concerne leur durée de vie.

Pour illustrer la différence entre une variable locale et une variable globale, nous commencerons par quelques exemples de programmes, que vous pouvez télécharger sur le site de l'auteur :

- exemple 1 ;

- exemple 2 ;

- exemple 3 ;

- exemple 4.

Vous pouvez donc les ouvrir et les exécuter pour vous aider à mieux comprendre ou pour vous convaincre.

Exemple 1 (fichier : Global/ProjetGlobal.lpi)

Voici le formulaire du programme :

et voici un extrait du code de ce programme :

var
Form1: TForm1;
x : integer;

implementation

procedure TForm1.BoutonAffecterClick(Sender: TObject);
begin
x := 1961;
end;

procedure TForm1.BoutonAfficherClick(Sender: TObject);
begin
AfficherEntier (x, ZoneTexteX);
end;

Dans ce premier exemple, la variable x est déclarée à l'extérieur de tout sous-programme. Il s'agit donc d'une variable globale.

Cela a les conséquences suivantes :

- x peut être utilisée dans tout le fichier de code où elle est déclarée et, par conséquent, à l'intérieur de toutes les procédures définies dans ce fichier. Dans notre exemple, la variable x est utilisée dans les deux procédures évènementielles BoutonAffecterClick et BoutonAfficherClick. La procédure BoutonAffecterClick affecte la valeur 1961 à x et la procédure BoutonAfficherClick affiche la valeur de x ;

- x a une durée de vie égale à celle de l'exécution du programme : dès le démarrage du programme, de l'espace mémoire est alloué à la variable x. Lorsque le programme s'arrête, cet espace mémoire n'est plus réservé à la variable et on peut donc considérer qu'elle cesse d'exister. Pour vous en convaincre, lancez le programme, affectez la valeur 1961 à x. Vérifiez que x a bien la valeur 1961. Arrêtez le programme et relancez-le. Vous constaterez que x n'a plus la valeur 1961.

Exemple 2 (fichier : Local0/ProjetLocal0.lpi)

Déclarons cette fois-ci la variable x à l'intérieur de la procédure BoutonAffecterClick. On obtient le résultat suivant :

Lazarus affiche un message d'erreur Identifier not found « x ». Cette erreur est localisée dans l'instruction AfficherEntier(x, ZoneTexteX) de la procédure BoutonAfficherClick.

Lazarus prétend ne pas connaître l'identificateur x dont on parle dans cette instruction.

Or nous avons bien déclaré x !

Comment expliquer cette erreur ?

Le problème vient du fait qu'une variable locale n'est accessible qu'à l'intérieur du sous-programme où elle est déclarée. Autrement dit, la portée d'une variable locale est limitée à ce sous-programme.

Dans notre exemple, x n'est donc définie qu'à l'intérieur de la procédure BoutonAffecterClick et nulle part ailleurs.

Exemple 3 (fichier : Local1/ProjetLocal1.lpi)

Voici à présent une version légèrement modifiée du programme précédent, dans laquelle la variable x est déclarée au début du programme ainsi qu'à l'intérieur de la procédure BoutonAffecterClick :

var
Form1: TForm1;
x : integer;

implementation

procedure TForm1.BoutonAffecterClick(Sender: TObject);
var x : integer;
begin
x := 1961;
end;

procedure TForm1.BoutonAfficherClick(Sender: TObject);
begin
AfficherEntier (x, ZoneTexteX);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
x := 1935;
AfficherEntier (x, ZoneTexteX);
end;

Cet exemple ne provoque aucun message d'erreur.

Vous vous demandez peut-être comment une telle chose est possible, car a priori une variable ne peut pas être déclarée deux fois. On devrait avoir un message d'erreur. Or il n'en est rien !

En réalité, il y a deux variables distinctes : la variable x déclarée en global et celle déclarée en local dans la procédure BoutonAffecterClick.

Bien qu'elles portent le même nom, ces deux variables sont totalement indépendantes.

Mais si Lazarus rencontre une instruction utilisant x, comment peut-il savoir de quelle variable il s'agit ?

Il s'en sort de la manière suivante :

- l'affectation x := 1961, dans BoutonAffecterClick doit agir sur la variable locale x car x est défini dans cette procédure ;

- par contre, l'instruction AfficherEntier(x, ZoneTexteX) dans BoutonAfficherClick agit sur la variable globale x car cette procédure ne contient aucune déclaration de la variable x ;

- de même, les instructions x := 1935 et AfficherEntier (x, ZoneTexteX) dans FormCreate agissent sur la variable globale.

De manière générale :

La déclaration locale est prioritaire sur la déclaration globale.

Autrement dit, si une variable globale est redéclarée à l'intérieur de certains sous-programmes alors :

- à l'intérieur de ces sous-programmes, c'est la déclaration locale qui l'emporte ;

- à l'intérieur d'un sous-programme dans laquelle cette variable n'est pas redéclarée, c'est la déclaration globale qui est prise en compte.

À présent, faites l'expérience suivante :

1. lancez le programme. Il affiche la valeur 1935 pour x ;

2. cliquez sur le bouton Affecter la valeur 1961 à X, puis sur Afficher X. Résultat : la variable x n'a pas été modifiée !

Comment expliquer ceci ?

Lorsque vous lancez le programme, Lazarus exécute tout d'abord la procédure FormCreate. Celle-ci affecte la valeur 1935 à la variable x déclarée en globale, puis affiche la valeur de cette même variable.

Lorsque vous cliquez sur le bouton Affecter la valeur 1961 à X, il exécute la procédure BoutonAffecterClick, dans laquelle x est déclarée en local. Cette même procédure affecte la valeur 1961 à x. Mais comme x est déclarée dans cette procédure, cette affectation agit sur la variable x déclarée dans cette procédure et non pas sur la variable x déclarée en global.

Enfin, lorsque vous cliquez sur le bouton Afficher X, il exécute la procédure BoutonAfficherClick, dans laquelle ne figure aucune déclaration locale de la variable x. Cette procédure affiche donc la valeur de la variable globale x qui est toujours égale à 1935.

Exemple 4 (fichier : Local2/ProjetLocal2.lpi)

Cette fois-ci, nous déclarons x en local dans toutes les procédures, mais pas en global :

var
Form1: TForm1;

implementation

procedure TForm1.BoutonAffecterClick(Sender: TObject);
var x : integer;
begin
x := 1961;
end;

procedure TForm1.BoutonAfficherClick(Sender: TObject);
var x : integer;
begin
AfficherEntier (x, ZoneTexteX);
end;

procedure TForm1.FormCreate(Sender: TObject);
var x : integer;
begin
x := 1935;
AfficherEntier (x, ZoneTexteX);
end;

Nous avons en réalité ici trois variables locales x indépendantes : celle de la procédure BoutonAffecterClick, celle de la procédure BoutonAfficherClick et celle de la procédure FormCreate.

Même expérience qu'avant :

1. lancez le programme. Il affiche la valeur 1935 pour x ;

2. cliquez sur le bouton Affecter la valeur 1961 à X, puis sur Afficher X. La valeur affichée est (très très probablement) différente de 1961.

Explications :

Lorsque vous lancez le programme, Lazarus exécute FormCreate. Ce sous-programme affecte la valeur 1935 à la variable locale x, puis affiche la valeur de cette même variable.

Lorsque vous cliquez sur le bouton Affecter la valeur 1961 à X, il exécute le sous-programme BoutonAffecterClick, qui affecte la valeur 1961 à la variable locale x de cette procédure.

Enfin, lorsque vous cliquez sur le bouton Afficher X, il exécute le sous-programme BoutonAfficherClick, qui affiche la valeur de la variable locale x de cette procédure. Comme aucune valeur n'est affectée à x dans cette procédure, cette variable a donc une valeur indéfinie.

Synthèse

Voilà en résumé ce qu'il faut retenir sur les variables locales et globales :

Définition

Par définition, une variable est locale si elle est déclarée à l'intérieur d'un sous-programme. Elle est globale dans le cas contraire.

Portée

Une variable globale est utilisable par tous les sous-programmes contenus dans le fichier où elle est déclarée.

Une variable locale, au contraire, n'est utilisable qu'à l'intérieur du sous-programme dans lequel elle est déclarée.

Durée de vie

La durée de vie d'une variable globale est égale à celle du programme. La zone mémoire allouée pour cette variable lui reste allouée tant que le programme s'exécute.

La durée de vie d'une variable locale est celle d'une exécution du sous-programme dans lequel elle est déclarée. Elle peut donc avoir plusieurs vies : elle renaît chaque fois que le sous-programme s'exécute et meurt chaque fois qu'il se termine.

Déclarations locales multiples

Si une « même » variable est déclarée dans plusieurs sous-programmes, tout se passe comme si on avait donné des noms de variables distincts pour ces différentes déclarations. Autrement dit, chaque déclaration engendre une variable totalement indépendante des autres.

Déclaration simultanée en local et en global

Si une « même » variable est déclarée en local et en global :

- dans les sous-programmes où cette variable n'est pas déclarée, c'est la déclaration globale qui est prise en compte ;

- à l'intérieur des sous-programmes où cette variable est déclarée, c'est la déclaration locale qui est prise en compte.

Exemple d'utilisation de sous-programmes

Utilisation des procédures

Exemple 1

Concevoir un programme à l'aide de sous-programmes signifie découper un traitement en sous-traitements spécifiques. Pour illustrer ceci, reprenons le projet Peinture du cours Premières notions.

Rappelons que ce programme permet de calculer la surface à peindre d'une pièce rectangulaire, connaissant les dimensions de la pièce (longueur, largeur), le nombre de fenêtres et le nombres de portes.

Cet exemple nous avait justement servi à illustrer le principe du découpage d'un traitement en sous-traitements. Lorsqu'un calcul est trop complexe, on a intérêt à le découper en plusieurs calculs intermédiaires et sauvegarder les résultats intermédiaires dans des variables.

Cela nous avait donné le code suivant pour le bouton Calculer :

Nous avons ici un exemple de code qui peut très facilement se réécrire à l'aide de sous-programmes. Associons un appel de procédure à chaque traitement, cela nous donne :

Comparez ce code à celui de la version précédente : il est devenu plus court et plus lisible.

Écrivons à présent le code des procédures de calcul. Ce code doit être écrit avant la procédure évènementielle BT_CalculClick. Il suffit d'écrire une entête pour chaque procédure, puis de copier en-dessous l'affectation correspondante enrobée de begin end. On obtient :

Programmation descendante

Remarquez que l'on aurait pu procéder ainsi dès le départ : notre problème est de calculer la surface à peindre. Ce problème peut être décomposé en quatre sous-problèmes : calculer la surface des murs, la surface des fenêtres, la surface des portes et finalement la surface à peindre.

On associe à chaque problème une procédure, ce qui nous permet d'écrire le code du programme sans nous occuper des détails, c'est-à-dire avant même d'avoir résolu les sous-problèmes.

Cette manière de procéder est appelée la programmation descendante.

Retour d'un sous-programme

Lorsqu'une procédure évènementielle a fini de s'exécuter, le programme retourne automatiquement à un état d'attente d'évènement.

Dans le cas d'un sous-programme non évènementiel, c'est un peu différent : l'ordinateur revient à l'instruction qui suit l'appel. Dans le cas du projet Peinture, on obtient ceci :

Exemple 2

Dans le premier exemple d'utilisation des procédures, nous avons réécrit le projet Peinture à l'aide de procédures sans paramètres.

L'exemple présenté ici introduit les procédures avec paramètres. Il s'agit d'un programme (sans intérêt autre que pédagogique !) qui permet d'afficher le mail d'une personne de nom et prénom donnés :

Le bouton SFR affiche le mail chez SFR, alors que le bouton FREE affiche le mail chez Free. Voici le code de la procédure évènementielle associée au bouton FREE :

On constate que le code des deux procédures évènementielles associées aux boutons SFR et FREE se ressemble énormément. La seule chose qui change est le nom de l'opérateur.

Pour transformer ces deux traitements quasiment identiques en un seul traitement, on peut utiliser un sous-programme paramétré par le nom de l'opérateur. Voici le code de ce sous-programme :

procedure AfficherLeMailChez (operateur : string);
var prenom, nom : string;
begin
Lire (prenom, Form1.ZT_Prenom);
Lire (nom, Form1.ZT_Nom);
Afficher (prenom+'.'+nom+'@'+operateur+'.fr', Form1.ZT_Mail);
end;

Le code des deux procédures évènementielles se résume alors à l'appel de cette procédure avec deux valeurs de paramètres différents :

procedure TForm1.BT_SFRClick(Sender: TObject);
begin
AfficherLeMailChez ('sfr');
end;

procedure TForm1.BT_FREEClick(Sender: TObject);
begin
AfficherLeMailChez ('free');
end;

Les valeurs des paramètres à l'appel ('sfr' et 'free' dans notre exemple) sont les paramètres effectifs à ne pas confondre avec les paramètres formels : ce sont les paramètres déclarés dans l'entête de la procédure. Dans notre exemple, il n'y a qu'un seul paramètre formel (operateur de type string).

Détail technique

Un petit détail technique pour terminer la présentation de cet exemple. Vous remarquerez que dans le code de la procédure AfficherLeMailChez, les zones de texte sont toutes préfixées par Form1 :

procedure AfficherLeMailChez (operateur : string);
var prenom, nom : string;
begin
Lire (prenom, Form1.ZT_Prenom);
Lire (nom, Form1.ZT_Nom);
Afficher (prenom+'.'+nom+'@'+operateur+'.fr', Form1.ZT_Mail);
end;

De manière générale, pour accéder aux composants d'une interface graphique depuis un sous-programme non évènementiel, il est nécessaire de préfixer les noms de ces composants par le nom du formulaire.

À ce stade, nous ne pouvons pas expliquer pourquoi cela est nécessaire car l'explication fait appel à des notions de programmation objet.

Exemple d'utilisation de fonctions

L'exemple présenté ici reprend le projet Peinture en l'écrivant avec des fonctions. Ce nouvel exemple se trouve dans le répertoire Exemple-Sous-Programme/PeintureAvecFonctions, que vous pouvez télécharger sur le site de l'auteur.

Rappelons que le projet Peinture permet de calculer la surface à peindre d'une pièce rectangulaire, connaissant les dimensions de la pièce (longueur, largeur), le nombre de fenêtres et le nombre de portes.

Les dimensions des fenêtres, des portes et la hauteur des murs sont des constantes (LargeurFenetre=1,4 ; HauteurFenetre=1,2 ; LargeurPorte=0,9 ; HauteurPorte=2,1 ; HauteurMur=2,5).

Ancienne version du projet

La dernière version (répertoire Exemple-Sous-Programme/Peinture) nous avait servi à introduire les procédures. Nous n'avions alors aucune notion de variables locales, ni de paramètres. Cette version du projet avait donc été écrite en utilisant uniquement des variables globales et des procédures sans paramètre.

Pour calculer la surface à peindre à l'aide de procédures, nous avions procédé par programmation descendante en découpant le problème en quatre sous-problèmes :

1. calcul de la surface des murs ;

2. calcul de la surface des fenêtres ;

3. calcul de la surface des portes ;

4. calcul de la surface à peindre.

Nous avions associé une procédure à chacun de ces traitements. Voici le code de ces procédures :

et voici l'appel de ces procédures dans la procédure évènementielle associée au bouton Calculer :

Nouvelle version : appel des fonctions

La nouvelle version que nous allons présenter ici n'utilise au contraire aucune variable globale et effectue des calculs par l'intermédiaire de fonctions.

Voici le code de cette même procédure évènementielle dans la nouvelle version :

procedure TForm1.BT_CalculClick(Sender: TObject);
var LargeurPiece, LongueurPiece, Surface_A_Peindre : double;
NFenetre, NPorte : Integer;
begin
LireNombre (LargeurPiece, ZT_Largeur);
LireNombre (LongueurPiece, ZT_Longueur);
LireEntier (NFenetre, ZT_NF);
LireEntier (NPorte, ZT_NP);

Surface_A_Peindre :=
SurfaceDesMurs(LargeurPiece,LongueurPiece)
- SurfaceDesFenetres(NFenetre)
- SurfaceDesPortes(Nporte);

AfficherNombre (Surface_A_Peindre,ZT_SP);
end;

Le calcul de la surface à peindre se fait à présent en une seule affectation. Plus précisément, il est représenté par l'expression qui appelle trois fonctions :

1. SurfaceDesMurs : cette fonction calcule la surface des murs à partir des dimensions de la pièce ;

2. SurfaceDesFenetres : elle calcule la surface des fenêtres à partir du nombre de fenêtres ;

3. SurfaceDesPortes : idem pour les portes à partir du nombre de portes.

Nouvelle version : déclaration des fonctions

Voici le code de ces trois fonctions :

L'entête d'une fonction commence nécessairement par le mot clé function. Ce dernier est suivi du nom de la fonction puis, optionnellement, de la liste des paramètres. Enfin, l'entête se termine par le type du résultat produit par la fonction. Dans notre cas, il s'agit du type double, car le résultat produit est un nombre a priori non entier.

Une fonction se distingue d'une procédure par le fait qu'elle retourne un résultat. Pour comprendre ce que signifie « retourner un résultat », il faut voir comment l'ordinateur exécute une expression contenant des appels de fonctions. Nous reviendrons là-dessus un peu plus loin.

Dans notre exemple, chaque fonction ne contient qu'une seule instruction. Ce n'est pas vrai en général. Le corps d'une fonction peut contenir plusieurs instructions. Comment savoir alors quel est le résultat de la fonction ? En Pascal, le résultat d'une fonction est déterminée par la dernière exécution d'une affectation de la forme :

Nom de la fonction := expression ;

Il faut donc nécessairement qu'une affectation de ce type soit présente dans le corps de la fonction afin que le résultat retourné par celle-ci soit défini.

Vous constaterez que, dans notre exemple, chaque fonction contient bien une instruction de ce type.

Retour du résultat

Voyons maintenant ce que signifie « retourner un résultat ». Pour cela, nous allons voir comment est évaluée l'expression contenant les appels de fonctions :

SurfaceDesMurs(LargeurPiece,LongueurPiece)
- SurfaceDesFenetres(NFenetre)
- SurfaceDesPortes(Nporte);

L'expression est évaluée de gauche à droite. L'ordinateur va donc commencer par évaluer l'appel de la fonction SurfaceDesMurs. Supposons que LargeurPiece = 3,6 et LongueurPiece = 4,3.

Comme dans les appels de procédures, les paramètres formels sont remplacés par les paramètres effectifs. Donc ici, LaP va prendre la valeur 3,6 et LoP, la valeur 4,3. Puis le corps de la fonction est exécuté avec ces valeurs. La fonction effectue donc le calcul suivant :

SurfaceDesMurs :=  2 * (3,6 + 4,3) * HauteurMur;

Avec une hauteur de mur de 2,50 m, cela fait 39,5 m^2.

À la fin de l'exécution de la fonction, ce résultat va « retourner » dans l'expression qui l'a appelée. Tout se passe comme si la valeur 39,5 remplaçait à présent l'appel de fonction dans cette expression. c'est-à-dire qu'il faut à présent évaluer l'expression :

39,5  - SurfaceDesFenetres(NFenetre)
- SurfaceDesPortes(Nporte);

Mais l'évaluation de l'expression n'est pas terminée. L'ordinateur va à présent exécuter l'appel de la fonction SurfaceDesFenetres. Supposons que le nombre de fenêtres soit égal à trois ; nf, le paramètre formel de cette fonction, va donc prendre la valeur 3 et la fonction va faire le calcul suivant :

SurfaceDesFenetres := 3 * LargeurFenetre * HauteurFenetre;

Avec une largeur de fenêtre de 1,4 m et une hauteur de fenêtre de 1,2 m, cela donne 5,04 m^2.

Celle valeur retourne dans l'expression d'appel et nous obtenons :

39,5 - 5,04 - SurfaceDesPortes(Nporte);

Ici, l'ordinateur va d'abord effectuer la différence 39,5 - 5,04. Cela fait 34,46. Nous nous retrouvons donc avec l'expression :

34,46 - SurfaceDesPortes(Nporte);

Il reste donc à évaluer l'appel de la fonction SurfaceDesPortes. Supposons deux portes ; le paramètre formel np prend la valeur 2 et la fonction effectue le calcul suivant :

SurfaceDesPortes := 2 * LargeurPorte * HauteurPorte;

Avec une largeur de porte de 0,9 m et une hauteur de porte de 2,1 m, cela fait 3,78 m^2. En ramenant ce résultat dans l'expression de départ nous obtenons :

34,46 - 3,78;

Ce qui fait 30,68 m^2. Vous pouvez vérifier que c'est bien le résultat affiché par le programme :

Principes généraux et compléments

Déclaration et appel de procédures

Déclaration d'une procédure

procedure NomDeLaProcédure (liste des paramètres);
Déclaration des variables locales
begin
Instructions
end;

La première ligne est l'entête de la procédure, formée du mot-clé procedure suivi du nom de la procédure et d'une liste optionnelle de paramètres entre parenthèses.

La partie entre begin et end est le corps de la procédure.

Entête

Sous sa forme la moins compacte, la liste des paramètres s'écrit :

paramètre1 : type1,...., paramètreN : typeN

On peut obtenir une écriture plus concise en regroupant les paramètres consécutifs de même type, mais dans ce cas chaque groupe de paramètres doit être séparé du suivant par un point virgule, sachant qu'un groupe de paramètres s'écrit :

paramètre1,...., paramètreN : type

Variables locales

Toute procédure n'utilise pas nécessairement des variables locales, mais si c'est le cas, elles doivent être déclarées entre l'entête et le premier begin.

Écriture d'un appel de procédure

L'appel d'une procédure est formé du nom de cette procédure suivi d'une liste de paramètres effectifs entre parenthèses.

NomDeLaProcédure (liste des paramètres effectifs);

Dans l'exemple que nous avons présenté, les paramètres effectifs étaient des chaînes de caractères. De manière générale, la liste des paramètres effectifs est constituée d'expressions quelconques séparées par des virgules :

expression1,...., expressionN

Pour que l'appel de procédure soit compilable (et exécutable !), le type de ces expressions doit être compatible avec celui des paramètres formels :

- il doit y avoir autant de paramètres effectifs que de paramètres formels ;

- le type d'un paramètre effectif quelconque doit être compatible avec celui du paramètre formel de même position.

Voici par exemple une procédure qui possède trois paramètres formels (p, n ,o) de type chaîne de caractères :

procedure FormerMail (p, n , o : string);
begin
mail := p+'.'+n+'@'+o+'.fr';
end;

En supposant que Prenom1, Prenom2 et Nom soient des variables de type chaîne de caractères, elle pourrait être appelée de la manière suivante :

FormerMail (Prenom1 + '-' + Prenom2, Nom, 'free');

Le premier paramètre effectif est une expression de type chaîne de caractères, le deuxième une variable de type chaîne de caractères et le troisième, un littéral de type chaîne de caractères. Ces trois paramètres effectifs sont donc compatibles avec les paramètres formels.

Par contre, elle ne pourra être appelée d'aucune des manières suivantes :

FormerMail (Prenom1, Nom);

FormerMail (Prenom1, Nom, 9);

En effet, dans le premier appel, le nombre de paramètres effectifs n'est pas égal au nombre de paramètres formels et dans le deuxième, le type du troisième paramètre effectif (numérique) n'est pas compatible avec le type du troisième paramètre formel.

Exécution d'un appel de procédure

Principe du retour à la procédure appelante

Un appel de procédure est toujours contenu dans une autre procédure, que nous appellerons la procédure appelante.

Lorsqu'un appel de procédure est exécuté, le processeur interrompt momentanément l'exécution de la procédure appelante pour aller exécuter la procédure appelée. Après avoir exécuté les instructions de cette procédure, il reprend l'exécution de la procédure appelante à partir de l'instruction qui suivait l'appel.

Passage des paramètres et exécution de l'appel

Les paramètres formels d'un sous-programme peuvent être considérés comme des variables locales.

L'exécution d'un appel de procédure consiste à :

- évaluer chacun des paramètres effectifs ;

- allouer de la mémoire pour les paramètres formels ;

- affecter à chaque paramètre formel la valeur du paramètre effectif correspondant ;

- exécuter le code de la procédure avec ces valeurs de paramètres ;

- libérer l'espace mémoire alloué aux paramètres formels.

Reprenons l'exemple de la procédure FormerMail appelée de la manière suivante :

FormerMail (Prenom1+'-'+Prenom2, Nom, 'free');

En supposant que les variables Prenom1, Prenom2 et Nom aient respectivement pour valeur 'Jean', 'Sebastien' et 'Bach', cet appel donnerait lieu aux opérations suivantes :

- évaluation des paramètres effectifs :

- valeur de l'expression Prenom1 + '-' + Prenom2 : 'Jean-Sebastien' ;

- valeur de l'expression Nom : 'Bach' ;

- valeur de l'expression 'Free' : 'Free'.

- allocation mémoire : de la mémoire est allouée aux trois paramètres formels p, n et o ;

- affectation des valeurs aux paramètres formels :

- la valeur 'Jean-Sebastien' est affectée au paramètre p ;

- la valeur 'Bach' est affectée au paramètre n ;

- la valeur 'Free' est affectée au paramètre o.

- exécution du code de la procédure : l'instruction
mail := p + '.' + n + '@' + o + '.fr';
est exécutée avec ces valeurs de paramètres ;

- libération de place mémoire : la place mémoire allouée aux paramètres p, n et o est libérée.

Déclaration et appel de fonctions

Déclaration d'une fonction

function NomDeLaFonction (liste des paramètres) : type;
Déclaration des variables locales
begin
Instructions (dont une instruction de retour)
end;

Différences avec la déclaration d'une procédure :

- l'entête commence par le mot-clé function :

- l'entête se termine par le type du résultat retourné par la fonction ;

- parmi les instructions contenues dans le corps de la fonction doit figurer une instruction permettant de retourner le résultat. En Pascal, cette instruction s'écrit :

NomDeLaFonction := expression;

Appel de fonction

Un appel de fonction s'écrit :

NomDeLaFonction (liste des paramètres effectifs)

Comme pour les procédures, le nombre de paramètres effectifs doit être égal au nombre de paramètres formels et le type d'un paramètre effectif quelconque doit être compatible avec celui du paramètre formel de même position.

Différences avec un appel de procédure

Un appel de fonction est une expression dont la valeur est celle retournée par la fonction. Un appel de procédure, au contraire, n'est pas une expression mais une instruction. L'exécution d'une procédure ne retourne aucune valeur.

Une appel de fonction figure en général à l'intérieur d'une instruction ou d'une expression. Par exemple, si Moyenne est une fonction calculant la moyenne de deux nombres, on peut écrire :

m:= Moyenne (A,B);

AfficherNombre (Moyenne (A,B), ZT_Moy)

(Moyenne(A,B) + Moyenne(C,D)) / 2

Un appel de procédure, au contraire, ne peut pas figurer à l'intérieur d'une instruction, ni d'une expression.

Les fonctions prédéfinies

Comme tous les langages, le Pascal offre une multitude de fonctions prédéfinies. Ces fonctions réalisent en général des opérations très fréquemment utilisées afin de faciliter la tâche du programmeur. On y trouve en particulier les fonctions mathématiques (exemples) et les fonctions de manipulation de chaîne de caractères (exemples).

En Pascal, elles sont réparties dans différentes unités de code. Pour pouvoir les utiliser dans une unité d'un projet, le nom de ces unités doit figurer dans la liste des unités importées, c'est-à-dire après uses.

Par défaut, Lazarus importe huit unités dans une unité associée à un formulaire :

Comme toutes les fonctions prédéfinies ne figurent pas dans ces huit unités, il faut dans certains cas étendre la liste des unités importées.

Prenons par exemple la fonction floor. Cette fonction retourne la partie entière d'un nombre. Elle figure dans l'unité Math.pas (fonctions mathématiques). Pour pouvoir utiliser floor dans une unité, il faut donc ajouter Math dans la liste des unités importées par celle-ci :

Expressions et appels de fonctions

Comme nous l'avons déjà dit, un appel de fonction est une expression dont la valeur est celle retournée par la fonction.

Cela nous permet d'élargir la notion d'expression valide abordée dans le premier cours.

Pour cela, il nous suffit de rajouter la règle suivante :

Un appel de fonction est une expression valide de type T, si les paramètres effectifs sont compatibles avec les paramètres formels de la fonction et que cette fonction retourne un résultat de type T.

Cette nouvelle règle, en conjonction avec les règles déjà vues, nous permet de construire des expressions faisant intervenir des fonctions. Voici quelques exemples utilisant les fonctions prédéfinies :

Expression

Type

Valeur

10+Random(2)

Integer

10, 11 ou 12

'Age:' + IntToStr(51)

String

'Age : 51'

LeftStr('Thirion',Length('Thirion')-1)

String

'Thirio'

Exercices

Retrouvez différents exercices sur le site de l'auteur :

- exercices sous Lazarus ;

- exercices sur papier.

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Les tableaux

But de ce chapitre

Jusque là, nous n'avons que très peu de moyens de représenter les données d'un programme. Une donnée en mémoire est soit une variable de type Integer, soit une variable de type Double, String ou Boolean. Pour chaque donnée du programme, nous avons donc ainsi une unique variable.

En réalité, comme nous allons le voir dans ce cours et le suivant (cours concernant les types structurés), une même variable peut contenir un ensemble de données. En particulier, lorsque toutes ces données sont de même type, il est possible de les stocker dans une variable de type tableau.

L'objectif de ce cours est de vous apprendre à définir et à utiliser cette nouvelle manière de représenter les données.

Les tableaux à une dimension

Exemple introductif

Supposons qu'on ait le problème suivant : on voudrait écrire une procédure permettant de multiplier dix nombres entiers par 3.

Les dix entiers sont des variables globales. Il faut donc tout d'abord déclarer dix variables globales :

Var N1, N2, ..., N10 : Integer;

Et la procédure de multiplication devrait comporter dix instructions :

Procedure MultiplierPar3 ();
Begin
N1 := N1 * 3;
N2 := N2 * 3;
.
.
N10 := N10 * 3;
End;

Pour dix entiers, cette manière de procéder est envisageable. Mais imaginez qu'on ait le même problème avec un million d'entiers !

Ne croyez pas, d'ailleurs, que le traitement d'un million d'entiers soit un problème totalement irréaliste, car les ordinateurs ont souvent à traiter des millions de nombres en même temps. En informatique, ce type de problème est tout à fait courant.

Malheureusement, on ne peut pas utiliser de boucle pour faire ceci, car on n'a aucun moyen de faire varier le nom d'une variable !

On aurait envie d'écrire quelque chose du genre :

Var i : Integer;
For i:=1 To 10 Do Var Ni : Integer;

Procedure MultiplierPar3 ();
Var i : Integer;
Begin
For i := 1 To 10 Do Ni := Ni * 3;
End;

en pensant que Ni serait successivement remplacé par N1, N2, … , N10.

Malheureusement, ce mécanisme de génération automatique de nom de variable n'est pas possible !

Alors, comment faire ?

La solution est d'utiliser un tableau.

Pour utiliser un tableau, il faut tout d'abord déclarer une variable de type tableau.

Avec l'exemple précédent, ce serait :

Var N : array [1 .. 10] Of Integer;

Cette écriture revient à déclarer dix entiers N1, N2, … , N10, mais elle est beaucoup plus concise. Lorsqu'un interpréteur ou un compilateur rencontre une déclaration de tableau dans un programme, il alloue de la mémoire pour l'ensemble du tableau. Dans notre exemple, il y aurait donc une allocation mémoire de 20 octets en supposant des entiers 16 bits :

D'autre part, il devient à présent possible de multiplier les dix entiers du tableau par 3 avec une boucle For, comme ceci :

Procedure MultiplierPar3 ();
Var i : Integer;
Begin
For i := 1 To 10 Do N[i] := N[i] * 3;
End;

L'écriture N[i] désigne le ième élément du tableau, autrement dit l'élément d'indice i du tableau N.

Vous constaterez qu'avec un million d'entiers, le code du programme n'est pas plus long :

Var N : array [1 .. 1000000] of Integer;

Procedure MultiplierPar3 ();
Var i : Integer;
Begin
For i := 1 To 1000000 Do  N[i] := N[i] * 3;
End;

Le cas général

En Pascal, la déclaration d'un tableau s'écrit :

Var nom du tableau
: array [indice min .. indice max] Of type des éléments;

où indice min et indice max sont deux entiers qui représentent respectivement le plus petit indice et le plus grand indice des éléments du tableau.

Le type des éléments peut varier d'un tableau à l'autre, mais tous les éléments d'un même tableau sont forcément du même type.

Voici, par exemple, la déclaration d'un tableau de chaînes de caractères :

Var Prenom : array [1..1000 ] Of String;

On pourra ensuite affecter une valeur à un élément du tableau, tout comme on affecte une valeur à une variable. Par exemple l'instruction :

Prenom[3] := 'Jeanne';

affecte la chaîne de caractères 'Jeanne' à l'élément d'indice 3 du tableau Prenom.

Utilisation de constantes pour définir la dimension d'un tableau

Supposons que vous ayez à modifier un programme très long (plusieurs milliers de lignes, par exemple), qui utilise un tableau de T de dix entiers déclaré par :

Var T : array [1 .. 10] Of Integer;

On vous demande de changer la dimension du tableau T de 10 à 20.

Supposons que dans ce programme figurent des centaines d'instructions faisant référence à la dimension du tableau T, c'est-à-dire au nombre 10.

Dans toutes ces instructions, il va donc falloir remplacer le nombre 10 par le nombre 20.

Les constantes servent en particulier à éviter ce genre de manipulation longue et fastidieuse.

Supposons à présent que le programme initial ait été écrit en utilisant une constante DIMENSION, qui représente le nombre d'éléments du tableau T, déclarée comme suit :

Const DIMENSION = 10;

et que le tableau soit déclaré comme suit :

Var T : array [1..DIMENSION] of Integer;

Si le développeur qui a écrit le programme a bien fait les choses, il aura fait référence à la constante DIMENSION (et non pas 10 !) dans toutes les instructions utilisant la dimension du tableau T.

Un programme écrit de cette manière est beaucoup plus facile à mettre à jour. En effet, pour changer la dimension du tableau de 10 à 20, il vous suffit à présent de modifier une seule ligne de programme : la déclaration de la constante DIMENSION. c'est-à-dire que vous allez la remplacer par :

Const DIMENSION = 20;

et tout le reste fonctionnera tout seul !

Vous vous demandez peut-être pourquoi on n'aurait pas pu utiliser une variable à la place de la constante. c'est-à-dire déclarer :

Var DIMENSION : Integer;
T : array [1..DIMENSION] of Integer;

puis faire DIMENSION := 10; au début du programme.

Ceci ne fonctionne pas !

Pourquoi ?

Parce que la dimension d'un tableau ne peut pas être modifiée pendant l'exécution d'un programme.

En effet, un tableau est un objet statique, c'est-à-dire que l'espace mémoire qui lui est alloué ne peut être modifié en cours d'exécution.

Cela présente un certain nombre d'inconvénients. En particulier, si vous ne connaissez pas a priori la taille de tableau qu'il vous faudra pour résoudre un problème particulier, la seule solution que vous avez est de prévoir large, de manière à ce que vous ne risquiez pas d'avoir un dépassement de capacité (taille de tableau insuffisante pour enregistrer toutes les données).

Nous verrons ultérieurement qu'il existe d'autres manières de représenter les informations en mémoire (on parle de structures de données), qui permettent d'éviter ce problème.

Tableaux remplis partiellement

Représentation

Nous avons vu qu'un tableau est un objet statique, c'est-à-dire que sa dimension ne peut pas être modifiée pendant l'exécution d'un programme.

Il faut donc toujours prévoir large, c'est-à-dire déclarer un tableau de taille suffisante pour tous les cas de figure.

Cela signifie que l'on va, en général, remplir les tableaux partiellement jusqu'à un certain indice définissant la fin des données.

Pour représenter un tableau rempli partiellement, il faut donc nécessairement une variable entière qui contiendra à tout moment l'indice de fin des données.

Pour fixer les idées, prenons une variable T pour le tableau et une variable N pour l'indice de fin. On aura donc les déclarations suivantes :

Const M = Nombre maximum d'éléments;
Var N : Integer;
T : array [1..M] of Type;

Les données utiles sont stockées dans les N premiers éléments du tableau :

Le tableau est « vide » lorsque N vaut 0 et plein lorsque N est égal à M :

Adjonction d'une valeur

Adjonction à la fin

Pour ajouter une valeur V à un tableau rempli partiellement, le plus simple est de l'ajouter à la fin. On appelle cette opération empiler. L'élément qui suit le dernier prend la valeur V et l'indice de fin est incrémenté :

T [N+1] := V;
N :=  N+1;

Insertion

Insérer une valeur dans un tableau est une opération plus complexe. Supposons que l'on souhaite insérer une valeur V à l'indice P (P ≤ N) du tableau. Il faut d'abord « faire de la place » pour cette nouvelle valeur, c'est-à-dire décaler toutes les valeurs entre les indices P et N, d'un cran vers la droite. La valeur V peut ensuite être affectée à l'élément d'indice P. Puis, comme on a ajouté une valeur, l'indice de fin augmente de 1 :

For i := N DownTo P Do T[i+1] := T[i];
T[P] := V;
N := N+1;

Suppression d'un élément

Suppression du dernier élément

Le dernier élément d'un tableau est le plus simple à supprimer ; pour cela, il suffit de dépiler, c'est-à-dire de décrémenter l'indice de fin de 1 :

N := N - 1;

Suppression d'un élément quelconque (différent du dernier)

Supprimer l'élément d'indice P (P < N ) signifie décaler toutes les valeurs qui suivent P d'un cran vers la gauche, puis décrémenter l'indice de fin :

For i := P To N-1  Do T[i] := T[i+1];
N := N-1;

Notion de pile

Un tableau rempli partiellement sur lequel on ne fait que des opérations empiler et dépiler est une structure de données fondamentale de l'informatique que l'on appelle une pile (stack en anglais).

En particulier, la gestion mémoire des variables locales et des paramètres des sous-programmes utilise une pile : les valeurs des variables sont empilées lorsque l'on rentre dans le sous-programme et dépilées à la sortie.

Les tableaux à deux dimensions

Déclaration en Pascal

var T : array [ 1..3,1..4 ] of integer;

T [ i , j ] est l'élément de T se trouvant à la ligne i et à la colonne j.

Exemple : T [2, 3] := 10;

Traitement de tous les éléments

Principe :

for i := 1 to 3 do
begin
for j := 1 to 4 do
begin
Traitement de l'élément T[ i, j ]
end
end

Exemple :

for i := 1 to 3 do
begin
for j := 1 to 4 do
begin
T [ i , j ] := j + 3 * (i - 1) ;
end
end

Traitement d'une ligne

Principe : traitement de la ligne i :

for j := 1 to 4 do
begin
Traitement de l'élément T[ i, j ]
end

Exemple : traitement de la ligne 2 :

for j:= 1 to 4 do
begin
T [2, j ] := 2 * j ;
end

Traitement d'une colonne

Principe : traitement de la colonne j :

for i := 1 to 3 do
begin
Traitement de l'élément T[ i, j ]
end

Exemple : traitement de la colonne 3 :

for i := 1 to 3 do
begin
T [ i, 3 ] := 4 - i ;
end

Exercices

Retrouvez les énoncés d'exercices sur le site de l'auteur :

- les tableaux à une dimension :

- exercices sous Lazarus,

- exercices sur papier.

- tableaux remplis partiellement ;

- les tableaux à deux dimensions.

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Structures de contrôle

But de ce chapitre

Avec ce que nous avons vu dans les deux premiers cours (Premières notions et Sous-programmes), un programme se présente comme un ensemble de sous-programmes qui ne contiennent que deux types d'instructions : des affectations ou des appels de procédures.

Les programmes de ce type ont deux limitations importantes, qui disparaîtront avec les nouvelles notions introduites dans ce cours.

Premièrement, ils ne peuvent pas s'adapter aux données qu'ils traitent. Ils ne pourront donc jamais traiter différents cas de figure ni se protéger contre l'introduction de données erronées. Cette limitation disparaîtra avec l'introduction des conditionnelles.

La deuxième limitation concerne les traitements itératifs dont nous avons déjà eu un aperçu dans le premier cours. Mais il s'agissait alors de traitements itératifs réalisés manuellement par l'utilisateur. Pour répéter un traitement, il reclique sur le bouton concerné. En fait, de nombreux problèmes informatiques nécessitent de pouvoir répéter automatiquement des instructions. Or nous n'avons pour l'instant qu'un seul moyen de le faire : dupliquer ces instructions dans le code autant de fois que nécessaire. Cette solution n'est évidemment pas très pratique. Et comment faire alors si le nombre de répétitions dépend des données ?

Cette deuxième limitation disparaîtra avec l'introduction des boucles.

Les conditionnelles et les boucles utilisent toutes les deux un type d'expression que nous n'avons pas encore étudié : les expressions logiques.

Nous allons donc commencer par là.

Les expressions logiques

Variable booléenne

Le type booléen est un type de variable, tout comme les types entier ou chaîne de caractères.

Une variable de ce type sert à représenter une information qui peut être vraie ou fausse.

Par exemple, dans un programme de gestion de marchandises, on pourrait avoir une variable RuptureDeStock qui nous permettrait de savoir si le stock de marchandises est épuisé.

De manière générale :

Une variable booléenne est une variable qui peut prendre deux valeurs : true (vrai) et false (faux).

Il n'y a donc que deux littéraux de type booléen.

En Pascal, le type booléen se note Boolean et une variable booléenne se déclare de la manière suivante :

Var nom de la variable : Boolean;

On pourrait par exemple écrire :

Var RuptureDeStock : Boolean;
RuptureDeStock := True;

La première instruction déclare une variable booléenne nommée RuptureDeStock. La deuxième lui affecte la valeur True.

Opérateurs logiques

Nous avons déjà vu quelques opérateurs : les opérateurs arithmétiques, qui agissent sur des nombres, ainsi que l'opérateur de concaténation, qui agit sur des chaînes de caractères.

De la même manière, il existe des opérateurs logiques. Ceux-ci permettent de calculer une valeur logique (donc vrai ou faux) à partir d'autres valeurs logiques.

Il existe principalement trois opérateurs logiques : la négation , la conjonction et la disjonction.

La négation

En logique, la négation permet de représenter le contraire d'une proposition.

Si quelque chose est vrai, alors son contraire est faux et réciproquement.

En Pascal, l'opérateur de négation se note Not.

La table suivante (que l'on appelle table de vérité) résume l'effet de l'opérateur de négation :

X

Not X

True

False

False

True

Par exemple, avec deux variables booléennes ArticleDisponible et RuptureDeStock, on pourrait écrire :

ArticleDisponible := Not RuptureDeStock;

Si RuptureDeStock vaut true avant cette affectation, alors ArticleDisponible vaudra false après cette affectation.

Réciproquement, si RuptureDeStock vaut False avant cette affectation, alors ArticleDisponible vaudra true après cette affectation.

La conjonction

La conjonction, représentée en Pascal par l'opérateur And, permet d'exprimer le fait que deux choses sont vraies simultanément.

Voilà la table de vérité de l'opérateur And :

X

Y

X And Y

False

False

False

False

True

False

True

False

False

True

True

True

Prenons un exemple.

La variable booléenne AmpouleFonctionne représente le fait qu'une ampoule électrique fonctionne.

Une autre variable booléenne InterrupteurOn est vraie si et seulement si l'interrupteur est en position On.

La variable booléenne LumiereAllumee représente le fait que la lumière est allumée.

Après l'affectation suivante :

LumiereAllumee := AmpouleFonctionne And InterrupteurOn;

la variable LumiereAllumee ne vaudra True que si les variables AmpouleFonctionne et InterrupteurOn valaient également True juste avant l'affectation.

La disjonction

La disjonction de deux propositions est vraie si et seulement si au moins une de ces deux propositions est vraie.

Autrement dit, la disjonction de deux propositions n'est fausse que lorsque ces deux propositions sont fausses.

Par exemple, la proposition « Il est bête ou il ne comprend pas » ne sera fausse que si l'individu en question n'est pas bête et a très bien compris.

Attention ! Le « ou logique » n'est pas un « ou exclusif ». La disjonction de deux propositions est vraie lorsque les deux propositions sont vraies. Par exemple, l'affirmation « Il pleut ou il y a du soleil » est vraie lorsqu'il pleut et qu'il y a du soleil en même temps.

L'opérateur de disjonction est représenté en Pascal par le mot-clé Or. Voici sa table de vérité :

X

Y

X Or Y

False

False

False

False

True

True

True

False

True

True

True

True

Prenons un exemple.

La variable booléenne AmpouleFoutue représente le fait qu'une ampoule électrique ne fonctionne pas.

Une autre variable booléenne InterrupteurOff est vraie si et seulement si l'interrupteur est en position Off.

La variable booléenne LumiereEteinte représente le fait que la lumière est éteinte.

Après l'affectation suivante :

LumiereEteinte := AmpouleFoutue Or InterrupteurOff;

la variable LumiereEteinte ne vaudra True que si la variable AmpouleFoutue et/ou la variable InterrupteurOff avaient la valeur True juste avant l'affectation.

Expressions logiques

Une expression logique est une expression de type booléen, c'est-à-dire une expression pouvant prendre la valeur vrai ou faux.

Les variables booléennes

Vous connaissez déjà un cas particulièrement simple d'expression logique : ce sont les noms de variables booléennes.

Par exemple, si la variable LumiereEteinte est déclarée comme une variable booléenne, le simple nom de variable LumiereEteinte peut être considéré comme une expression logique.

Utilisation des opérateurs logiques

On peut également construire des expressions logiques en combinant d'autres expressions logiques avec les opérateurs Not, And, Or.

Supposons, par exemple, que x, y et z soient des variables booléennes. Alors Not x, x And y, x Or y sont des expressions logiques. Tout comme z Or (x And y), (Not z And x) et (x Or y)…

De manière générale :

- Si E est une expression logique, alors Not E est une expression logique.

- Si E et F sont des expressions logiques, alors E And F est une expression logique.

- Si E et F sont des expressions logiques, alors E Or F est une expression logique.

- Si E est une expression logique, alors (E) est une expression logique.

Utilisation des opérateurs de comparaison

Il existe encore un autre moyen de construire une expression logique : en utilisant des opérateurs de comparaison.

Un opérateur de comparaison est un opérateur qui permet de comparer deux expressions de même type.

Les six opérateurs

Il existe six opérateurs de comparaison :

Symboles mathématiques

Signification

=

Égal à

≠≠

Différent de

<

Strictement inférieur à

>

Strictement supérieur à

≤

Inférieur ou égal à

≥

Supérieur ou égal à

La notation des opérateurs de comparaison varie selon les langages :

Symboles mathématiques

Pascal

Langage C

=

=

==

≠

<>

!=

<

<

<

>

>

>

≤

<=

<=

≥

>=

>=

Comparaison d'expressions numériques

L'utilisation la plus fréquente des opérateurs de comparaison est la comparaison d'expressions numériques.

La plus simple est la comparaison directe de deux variables numériques. Exemple :

x

y

x = y

x <> y

x < y

x >= y

x > y

x <= y

1

1

True

False

False

True

False

True

1

2

False

True

True

False

False

True

2

1

False

True

False

True

True

False

Mais on peut bien sûr comparer des expressions numériques quelconques. Par exemple, pour x = 1 et y = 2, on aurait :

Expression logique

Valeur

(x+1) = y

True

(x+1)*(y+1) = 6

True

x+1 <= 2

True

x-1 < -y

False

Comparaison de chaînes de caractères

En Pascal, les six opérateurs de comparaison peuvent être utilisés avec des chaînes de caractères, en se basant sur l'ordre alphabétique. Exemple :

x

y

x = y

x <> y

x < y

x >= y

x > y

x <= y

'Trac'

'Trac'

True

False

False

True

False

True

'Trac'

'Truc'

False

True

True

False

False

True

'Truc'

'Trac'

False

True

False

True

True

False

Exercices

- Énoncés ;

- Pour obtenir les corrigés, rendez-vous au bas de la page.

La conditionnelle

La programmation n'aurait que très peu d'intérêt si elle ne permettait pas aux ordinateurs de réagir différemment selon différentes conditions.

Une machine programmable doit pouvoir effectuer des traitements différents selon les données qu'elle doit traiter.

Cela est possible grâce à l'existence des structures de contrôle dans les langages de programmation : la conditionnelle et les boucles.

L'objet de ce chapitre est d'expliquer le fonctionnement d'une conditionnelle.

Le If

La conditionnelle If existe en deux variantes : le If ... Then et le If … Then … Else.

Le If … Then

La structure de contrôle If … Then s'écrit de la manière suivante :

If condition Then
Begin
Instructions à exécuter
si la condition est vraie
End;

La condition n'est rien d'autre qu'une expression logique.

Les instructions entre begin et end ne seront exécutées que si la condition est vraie.

S'il n'y a qu'une seule instruction à exécuter, on peut omettre les mots-clés begin et end :

If condition Then Instruction à exécuter;

Prenons un exemple :

If n > 0 Then n := n - 1;

Si, par exemple, n vaut 5 juste avant l'exécution du If … Then, la condition n > 0 est vérifiée. L'ordinateur va donc exécuter l'affectation n := n - 1. n vaudra donc 4 à la sortie de la conditionnelle.

Si n vaut -1, par contre, la condition n > 0 n'est pas vérifiée. L'affectation ne sera pas exécutée et n vaudra toujours -1 à la sortie de la conditionnelle.

Le If … Then … Else

Le If … Then … Else est une autre forme de conditionnelle.

Elle permet d'exécuter certaines instructions si une condition est vraie, et d'autres si elle est fausse.

Voilà sa syntaxe :

If condition Then
begin
Instructions à exécuter
si la condition est vraie
End
Else
begin
Instructions à exécuter
si la condition est fausse
End;

De même, les begin et end ne sont pas nécessaires s'il n'y a qu'une seule instruction.

Prenons un exemple :

If n > 0 Then
n := n - 1
Else
n := n + 1;

Remarquez que nous n'avons pas mis de point-virgule après la première affectation. Si nous l'avions fait, le compilateur aurait généré une erreur de syntaxe, car il aurait interprété ceci comme un if … then simple suivi du mot-clé else, qui n'aurait rien à voir ici dans ce cas.

Par contre, le point-virgule est nécessaire après la deuxième affectation pour marquer la fin de la conditionnelle.

Que se passe-t-il lors de l'exécution de cette conditionnelle ?

Supposons, par exemple, que n vaut 5 juste avant l'exécution du If … Then … Else. La condition n > 0 est vérifiée. L'ordinateur va donc exécuter l'affectation n := n - 1. Par contre, l'affectation n := n + 1 ne sera pas exécutée. n vaudra donc 4 à la sortie de la conditionnelle.

Si n vaut -1, par contre, la condition n > 0 n'est pas vérifiée. L'affectation n := n - 1 n'est donc pas exécutée. Mais comme la condition est fausse, l'affectation n := n + 1 sera exécutée. n vaudra donc 0 à la sortie de la conditionnelle.

If imbriqués

Les instructions à l'intérieur d'un If ne sont pas forcément des affectations comme dans les exemples précédents.

On peut également y mettre d'autres structures de contrôle et, en particulier, d'autres If.

Exemple :

If n > 0 Then
n := n - 1
Else
If n >= -3 Then
n := n + 1
Else
n := 2 * n;

Pour n = 5, l'instruction exécutée est n := n - 1. n vaudra donc 4 à la sortie de la conditionnelle.

Pour n = -1, l'instruction exécutée est n := n + 1. n vaudra donc 0 à la sortie de la conditionnelle.

Pour n = -4, l'instruction exécutée est n := 2 * n. n vaudra donc -8 à la sortie de la conditionnelle.

Dans l'exemple précédent, on a deux niveaux d'imbrication. Mais on peut avoir autant de niveaux d'imbrication qu'on le souhaite.

L'instruction Exit

L'imbrication de If peut conduire à des programmes plus rapides (voir l'exercice Min-Max-Med), mais elle peut également compliquer l'écriture du code, notamment dans la protection d'un programme contre les erreurs.

Prenons par exemple un programme utilisant une date définie par un jour (nombre entier entre 1 et 31), un mois (nombre entier entre 1 et 12) et une année de ce siècle (nombre entier supérieur à 2000). Ces trois données sont lues depuis trois zones de texte (respectivement ZT_Jour, ZT_Mois et ZT_Annee) dans des variables entières, respectivement Jour, Mois, Annee. Ce traitement est effectué par la procédure évènementielle associée au bouton BT_GererDate.

Pour tester la validité de ces données avec des If imbriqués, on écrirait le code suivant :

procedure TForm1.BT_GererDateClick(Sender: TObject)
var Jour,Mois,Annee: integer;
begin
LireEntier (Jour, ZT_Jour);
If ( Jour < 1) or (Jour > 31) Then
ShowMessage ('Numéro du jour non valide!')
Else
begin
LireEntier (Mois, ZT_Mois);
If (Mois < 1) or (Mois > 12) Then
ShowMessage('Numéro du mois non valide!')
Else
Begin
LireEntier(Annee,ZT_Annee);
If Annee < 2000 Then
ShowMessage ('Année non valide!')
Else
Begin
Traitement de la date
End
End
End
End;

Pour raccourcir ce genre d'écriture, on peut utiliser l'instruction Exit, qui permet de forcer la fin de l'exécution du sous-programme qui la contient. Avec cette instruction, l'exemple précédent peut être écrit sans aucune imbrication de If :

procedure TForm1.BT_GererDateClick(Sender: TObject)
var Jour,Mois,Annee: integer;
begin
LireEntier (Jour, ZT_Jour);
If ( Jour < 1) or (Jour > 31) Then
Begin ShowMessage ('Numéro du jour non valide!'); Exit End;

LireEntier (Mois, ZT_Mois);
If (Mois < 1) or (Mois > 12) Then
Begin ShowMessage('Numéro du mois non valide!'); Exit End;

LireEntier(Annee,ZT_Annee);
If Annee < 2000 Then
Begin ShowMessage ('Année non valide!'); Exit End;

Traitement de la date

End;

Case

La conditionnelle Case est utilisée lorsque l'on veut effectuer différents traitements selon la valeur d'une expression quelconque.

Voici sa syntaxe :

Case Expression Of
V1 :
Instructions à exécuter lorsque
l'expression vaut V1 ;
V2 :
Instructions à exécuter lorsque
l'expression vaut V2 ;
.
.
VN :
Instructions à exécuter lorsque
l'expression vaut VN ;
Else
Instructions à exécuter dans les autres cas ;
End;

La partie Else est optionnelle.

Voici, par exemple, comment déterminer le nom d'un mois en fonction de son numéro avec une instruction Case :

Case NumeroMois  Of
1 : NomMois :='janvier';
2 : NomMois :='février';
.
.
12 : NomMois :='décembre';
Else
ShowMessage ('Numéro de mois inexistant!')
End;

Exercices

- Exercices avec ordinateur ;

- Exercices sans ordinateur ;

- Pour obtenir les corrigés, rendez-vous au bas de la page.

Les boucles

Tout comme les conditionnelles, les boucles sont des structures de contrôle fondamentales de la programmation.

Par l'intermédiaire d'une boucle, on peut demander à un ordinateur de répéter une partie du code.

Nous allons présenter trois types de boucles : les boucles While, les boucles Repeat et les boucles For.

En fait, la boucle While suffirait à écrire n'importe quel programme. Mais dans certains cas, l'écriture d'une boucle avec Repeat ou avec For est plus pratique.

Les boucles While

Une boucle While permet de répéter du code tant qu'une certaine condition est vraie.

Elle s'écrit de la manière suivante :

While condition do
begin
Instructions à répéter
tant que la condition est vraie
End;

La condition est une expression logique quelconque.

Voilà un exemple précis de boucle While :

S := 0;
i := 1;
While i <= n Do
begin
S := S + i;
i := i + 1;
End;

Cette boucle permet de calculer la somme S des n premiers entiers : 1 + 2 + 3 + … + n. Pour n = 4, on aura par exemple S = 1 + 2 + 3 + 4 = 10 à la fin de l'exécution de ce code.

Vocabulaire associé aux boucles

Un peu de vocabulaire :

Chaque exécution du code à l'intérieur d'une boucle s'appelle une itération.

Dans notre exemple, une itération est une exécution des instructions S := S + i; et i := i + 1;.

Généralement, une boucle est précédée d'un certain nombre d'affectations que l'on appelle des initialisations. Elles servent à donner des valeurs initiales aux variables de la boucle (c'est-à-dire toutes celles qui vont varier dans la boucle).

Dans notre exemple, ce sont les instructions S := 0 et i := 1.

Dans une boucle, on trouve fréquemment une variable qui joue le rôle de compteur (car elle compte en quelque sorte le nombre d'itérations). La valeur de cette variable est augmentée de 1 à chaque itération par une instruction de la forme variable := variable + 1, que l'on appelle une incrémentation.

Dans notre exemple, le compteur est représenté par la variable i. À chaque itération, la valeur de i est incrémentée par l'affectation i := i + 1.

Déroulement de la boucle

Nous allons à présent dérouler la boucle donnée en exemple, c'est-à-dire détailler toutes les instructions qui seront exécutées jusqu'à ce que la boucle s'arrête.

Supposons que n vaut 3 avant d'entrer dans la boucle. Voilà exactement ce qui se passe lorsque cette boucle est exécutée :

Instruction exécutée

Valeur de i

Valeur de S

S := 0

0

0

Initialisation

i := 1

1

0

S := S + i

1

1

1re itération

i := i + 1

2

1

S := S + i

2

3

2e itération

i := i + 1

3

3

S := S + i

3

6

3e itération

i := i + 1

4

6

Dans une boucle While, la condition d'arrêt est testée avant l'exécution d'une itération.

Donc avant d'entrer la première fois dans la boucle, l'ordinateur va tester la condition i ≤ n. Comme i vaut 1 et n vaut 3, il va rentrer dans la boucle et faire une première itération.

À la fin de la première itération, i vaut 2. La condition i ≤ n est toujours vérifiée. L'ordinateur va donc faire une deuxième itération.

À la fin de la deuxième itération, i vaut 3. La condition i ≤ n est toujours vérifiée. L'ordinateur va donc faire une troisième itération.

À la fin de la troisième itération, i vaut 4. Ce sera donc la dernière itération, car à présent la condition i ≤ n est fausse.

Les boucles Repeat

La boucle Repeat s'écrit de la manière suivante :

Repeat
Instructions à répéter
jusqu'à ce que la condition soit vraie
Until condition

Remarques :

- avec une boucle Repeat, le code à répéter est toujours exécuté au moins une fois ;

- il n'y a pas de Begin … End englobant les instructions à répéter, car Repeat et Until jouent déjà ce rôle.

Voici par exemple comment calculer la somme des n premiers nombres entiers avec une boucle Repeat :

S := 0;
i := 0;
Repeat
i := i + 1;
S := S+ i
Until i = n;

Comme précédemment, supposons que n vaut 3 et voyons ce qui se passe exactement lorsque la boucle Repeat est exécutée :

Instruction exécutée

Valeur de i

Valeur de S

S := 0

0

0

Avant d'entrer
dans la boucle

i := 0

0

0

i := i + 1

1

0

1re itération

S := S + i

1

1

i := i + 1

2

1

2e itération

S := S + i

2

3

i := i + 1

3

3

3e itération

S := S + i

3

6

Avant d'entrer dans la boucle, S et i sont initialisés à 0.

À la fin de chaque itération, l'ordinateur va tester si i est égal à n (donc à 3 dans notre exemple).

Cette condition n'est réalisée qu'à la fin de la troisième itération.

Les boucles For

Fondamentalement, une boucle For n'est rien d'autre qu'un raccourci d'écriture pour un certain type de boucle While que l'on rencontre très fréquemment :

compteur := valeur initiale;
While compteur <= valeur finale Do
Begin
Instructions à répéter
compteur := compteur + 1;
End;

Avant la boucle, on affecte une valeur initiale au compteur.

À chaque itération, le compteur est incrémenté.

La boucle s'arrête dès que le compteur dépasse la valeur finale.

La boucle For permet d'écrire ce type de boucle de manière plus concise, comme suit :

For compteur :=  valeur initiale To valeur finale Do
Begin
Instructions à répéter
End;

On économise ainsi l'écriture de l'incrémentation du compteur à l'intérieur de la boucle.

Attention : l'incrémentation du compteur n'est pas écrite, mais elle est bien exécutée.

Voici, par exemple, la somme des n premiers nombres entiers avec une boucle For et avec une boucle While :

En boucle For

En boucle While

S := 0;
For i := 1 To n Do
Begin
S := S+i
End

S := 0;
i := 1;
While i <= n Do
Begin
S := S+i;
i := i+1
End

Exercices

- Énoncés ;

- Pour obtenir les corrigés, voyez ci-dessous.

Corrigés des exercices

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Les types structurés

Introduction

Dans le cours sur les tableaux, nous avons vu un premier moyen de mémoriser un ensemble de données dans une variable. Cette manière de procéder est toutefois limitée à des données de même type. Les types structurés que nous allons présenter dans ce cours permettent de lever cette limitation.

Une autre limitation importante des tableaux est qu'ils sont de dimension fixe. On est donc souvent contraint de les surdimensionner, afin de s'assurer qu'ils pourront stocker toutes les données à traiter. Dans ce cours, nous allons voir comment stocker des ensembles de données de taille variable sous formes de listes. Avec cette nouvelle représentation, il sera possible d'utiliser une place mémoire proportionnelle au nombre de données à mémoriser.

Les types structurés

Généralités indépendantes du langage de programmation

Les concepts manipulés par l'être humain peuvent souvent être décrits par des attributs : un rectangle est caractérisé par sa largeur et sa longueur ; un livre peut être décrit par son titre, son auteur, son année de publication ; une voiture par sa marque, sa couleur, son kilométrage, sa puissance, etc.

Pour pouvoir représenter informatiquement de tels concepts, les langages de programmation permettent de définir des types structurés. Contrairement à un type prédéfini du langage (entier, réel, booléen, etc.), le nom d'un type structuré est défini par le programmeur. Il pourra par exemple définir un type « Livre », puis déclarer ensuite des variables de ce type.

Déclaration

Déclaration d'un type structuré

Prenons l'exemple du concept « Livre ». Pour représenter informatiquement un livre dans un programme Pascal, on pourra définir un nouveau type de la manière suivante :

Type Livre =
record
Titre : String;
Auteur : String;
AnneePublication : Integer;
end ;

On a ainsi défini un nouveau type de variable nommé « Livre ». « Titre », « Auteur » et « AnneePublication » sont les champs de ce type.

Les champs représentent les attributs du concept que l'on veut définir. Chaque champ est défini par son type. Dans notre exemple, Titre et Auteur sont des chaînes de caractères et AnneePublication est un entier.

Déclaration de variables de type structuré

À partir du moment où un nouveau type a été défini, il est possible de déclarer des variables de ce type. Une variable de type structuré est appelée une structure ou un enregistrement. Cette déclaration se fait comme pour un type prédéfini.

Par exemple, pour déclarer deux variables l1, l2 de type Livre, on écrira :

Var l1, l2 : Livre;

Manipulation des champs d'une structure

Pour accéder aux champs d'une structure, on utilise l'opérateur « . ».

Par exemple, pour affecter la valeur « Visual Basic 6 − Le Guide du programmeur » au titre du livre l1, on écrira :

l1.Titre := 'Visual Basic 6 - Le Guide du programmeur';

Un champ se manipule exactement comme une variable du même type. Par exemple, l1.AnneePublication se manipule exactement comme un entier. Pour obtenir l'âge du livre l1, on peut très bien écrire :

Var Age : Integer;
Age := AnneeActuelle - l1.AnneePublication;

Définition de types complexes par combinaison

Il est possible de définir des types complexes en combinant tableaux et structures, ou bien en combinant les types structurés entre eux. Donnons quelques exemples.

Tableaux de structures : les tables

Le type des éléments d'un tableau peut être quelconque. On peut donc définir un tableau dont les éléments sont des types structurés, ou autrement dit un tableau de structures.

Les tableaux de structures sont également appelés tables. Ils jouent un rôle important en informatique, puisqu'on les retrouve dans les bases de données. Nous verrons ultérieurement qu'une base de données n'est rien d'autre qu'un ensemble de tables stockées dans des fichiers.

Pour représenter l'ensemble des livres présents dans une bibliothèque, on peut par exemple utiliser un tableau dont les éléments sont de type Livre :

Var Bibliotheque : array [1..M] of Livre;

La variable Bibliotheque est donc un tableau de structures. La notation

Bibliotheque[6].Auteur

représente alors l'auteur du 6e livre.

Structure avec champs de type structuré

Enrichissons un peu notre représentation d'un livre. Pour la gestion d'une bibliothèque, il serait intéressant d'associer une date d'emprunt à chaque livre :

Type Livre =
record
Titre : String;
Auteur : String;
AnneePublication : Integer;
DateEmprunt : TDate;
end;

Le type TDate étant lui-même un type structuré défini par :

Type TDate =
record
Jour : Integer;
Mois : Integer;
Annee : Integer;
end;

Supposons que L soit une variable de type Livre. Pour définir la date d'emprunt de L, comme le 27 janvier 2009, on écrira :

L.DateEmprunt.Jour := 27;
L.DateEmprunt.Mois := 1;
L.DateEmprunt.Annee := 2009;

Les pointeurs

Déclaration d'un pointeur

Un pointeur est une variable contenant une adresse.

Rappelons (cf. le chapitre sur les premières notions) qu'une adresse est un entier qui représente la position d'un octet en mémoire. Cet entier permet donc en particulier de repérer la position, ou autrement dit l'adresse, d'une plage mémoire :

En Pascal, un pointeur se déclare de la manière suivante :

Var NomDuPointeur : ^Type;

où Type représente le type de donnée dont le pointeur contiendra l'adresse.

Exemple :

Type Ville =
record
Nom : String;
CodePostal : Integer;
End;
Var PVille : ^Ville;

Dans cet exemple, la variable PVille est un pointeur sur une donnée de type Ville.

Une autre manière de déclarer un pointeur est de définir tout d'abord un type pointeur de la manière suivante :

Type PointeurSurType = ^Type;

et de déclarer ensuite le pointeur comme une variable de ce type :

Var NomDuPointeur : PointeurSurType;

Avec l'exemple précédent cela donnerait :

Type Ville =
record
Nom : String;
CodePostal : Integer;
End;

PointeurSurVille = ^Ville;

Var PVille : PointeurSurVille;

Allocation dynamique de mémoire

Un pointeur permet de créer une variable sans nom, repérée uniquement par l'adresse qu'il contient.

Cette variable est créée par un mécanisme d'allocation dynamique de mémoire, que nous allons décrire à présent.

Les variables générées dynamiquement sont stockées dans une zone spéciale de la mémoire appelée le tas (heap en anglais).

En Pascal, l'allocation dynamique de mémoire se fait grâce à l'opérateur new.

Si p est un pointeur sur un type T, new(p) va allouer de la place mémoire dans le tas pour une variable de ce type et affecter à p l'adresse de cette variable.

Avec notre exemple précédent, new(p) réserverait de la place pour une structure de type Ville et affecterait l'adresse de cette structure à p. Par exemple, si la plage mémoire réservée pour la structure commence à l'adresse 1961, p contiendra cette adresse :

Une fois que la variable est créée, il est possible d'y accéder par l'opérateur ^. Si p est un pointeur, p^ représente la variable pointée par p.

Avec notre exemple, pour affecter « Strasbourg » au nom de la ville pointé par p, on écrirait :

p^.Nom := 'Strasbourg';

Tout se passe donc comme si p^ était une variable de type Ville.

Listes simplement chaînées

Pour l'instant, le seul moyen dont vous disposez pour représenter un ensemble de données est le tableau. Dans un tableau, chaque donnée (de même type) possède un indice définissant sa position dans le tableau.

Les listes sont une autre manière de représenter un ensemble de données n'utilisant pas d'indice et basée sur les pointeurs.

Représentation

Une liste simplement chaînée se représente par un ensemble de structures de même type possédant un champ contenant l'adresse de l'élément suivant.

Il serait assez difficile et peu pédagogique de présenter les listes chaînées dans un cas général abstrait. Nous allons donc baser toutes nos explications sur un exemple qui sera facile à généraliser : une liste de structures de type Ville dont voici la déclaration :

Type
PVille = ^Ville;

Ville = record
Nom : String;
CodePostal : String;
Suivant : PVille;
end;

Le champ Suivant sert à mémoriser l'adresse de l'élément suivant de la liste. Dans le cas général, il peut porter n'importe quel nom, mais il doit exister un champ jouant ce rôle.

La liste, quant à elle, est également un pointeur qui contient l'adresse de son premier élément.

Dans notre exemple, on pourrait donc représenter une liste de villes par une variable L de type PVille.

Voici par exemple une liste simplement chaînée contenant quatre villes :

L contient l'adresse de la première ville (Ribeauvillé). Le champ Suivant de la première ville contient l'adresse de la seconde (Kingersheim), etc.

Le dernier élément de la liste (Nancy) n'a pas d'élément suivant. Son champ Suivant contient la valeur NIL, qui représente en Pascal une adresse inexistante.

Une liste vide se représente simplement par un pointeur de liste égal à NIL (L = NIL dans notre exemple).

Parcours

Pour effectuer un traitement sur chaque élément d'une liste, il est nécessaire de savoir parcourir ses éléments. On utilisera pour cela un pointeur, qui prendra successivement la valeur des adresses de chaque élément de la liste. Voici par exemple comment parcourir une liste de villes dont l'adresse est contenue dans le pointeur L :

Var p : Pville;

p := L;
While p <> NIL Do
begin

{ Traitement de l'élément pointé par p }

p := p^.Suivant;
end;

Adjonction d'un élément

Considérons le problème suivant : on dispose d'une liste de villes L et d'une structure de type Ville pointée par un pointeur p. Comment procéder pour ajouter cette structure dans la liste ?

En début de liste

Pour ajouter l'élément p^ au début de la liste L, il suffit de faire :

p^.Suivant := L;
L := p;

La première affectation « accroche » la structure à la liste et la deuxième met à jour l'adresse de la liste, qui devient celle de p.

Cette méthode fonctionne également avec une liste vide. En effet, dans ce cas L vaut NIL avant l'adjonction. Après l'adjonction, on aura bien une liste d'un élément (p^) avec L = p et p^.Suivant = NIL.

La figure suivante illustre l'adjonction en début de liste de la ville Ribeauvillé :

Avant l'adjonction :

- la liste L contient les trois villes Kingersheim, Strasbourg et Nancy ;

- L contient l'adresse de la structure représentant Kingersheim.

Après l'adjonction :

- le champ Suivant de la structure représentant Ribeauvillé contient l'adresse de la structure représentant Kingersheim ;

- L contient l'adresse de la structure représentant Ribeauvillé ;

- L contient donc à présent les quatre villes Ribeauvillé, Kingersheim, Strasbourg et Nancy.

Insertion après un élément

Supposons à présent que la liste n'est pas vide et que l'on souhaite insérer un élément après un certain élément de la liste.

Soit p l'adresse du nouvel élément que l'on souhaite insérer, et Curseur l'adresse de l'élément après lequel on souhaite insérer p^. Les instructions suivantes permettent d'obtenir ce que l'on souhaite :

p^.Suivant := Curseur^.Suivant;
Curseur^.Suivant := p;

La figure suivante illustre l'insertion d'une structure représentant Strasbourg, après la structure représentant Kingersheim :

Suppression d'un élément

Suppression du premier élément

Pour supprimer le premier élément d'une liste simplement chaînée, il suffit de remplacer l'adresse de la liste par l'adresse de l'élément qui suit le premier.

Avec notre exemple, cela se traduit par l'instruction suivante :

L := L^.Suivant;

Si la liste ne contient qu'un seul élément, ce dernier n'a pas d'élément suivant, mais l'instruction précédente convient également, car, dans ce cas, L^.Suivant contient NIL et la liste devient par conséquent vide.

La figure suivante illustre la suppression du premier élément dans une liste de villes :

Avant la suppression, L pointe sur Ribeauvillé et la liste contient quatre villes.

Après la suppression, L pointe sur la ville qui suivait Ribeauvillé, c'est-à-dire Kingersheim. De ce fait, elle ne contient plus que les trois villes Kingersheim, Strasbourg et Nancy.

Suppression du suivant

Supposons à présent que l'on souhaite supprimer l'élément qui suit un certain élément de la liste d'adresses donnée.

Appelons Curseur l'adresse de cet élément. L'instruction suivante permet de supprimer l'élément qui suit Curseur^ :

Curseur^.Suivant := Curseur^.Suivant^.Suivant;

La figure suivante illustre ce mécanisme. On supprime ici la ville Strasbourg, en faisant pointer la ville précédente (Kingersheim) sur la ville qui suit Strasbourg (Nancy). De ce fait, Strasbourg n'existe plus dans la liste.

Listes doublement chaînées

Dans une liste doublement chaînée, chaque élément est lié non seulement au suivant, mais également au précédent. Cela facilite la suppression d'un élément, puisqu'il faut pour cela avoir l'adresse du précédent. Il devient également plus simple d'insérer un élément avant un élément donné.

Représentation

Reprenons notre exemple de liste de villes. Voici comment la représenter avec un chaînage double :

Type Ville =
record
Nom : String;
CodePostal : String;
Suivant : PVille;
Precedent : Pville;
end;

PVille = ^Ville;

Voici un exemple de liste doublement chaînée :

Adjonction d'un élément

En début de liste

L'adjonction d'un élément en début de liste nécessite deux instructions supplémentaires, puisqu'il faut mémoriser le fait que cet élément n'a pas d'élément précédent et qu'il devient à présent le précédent du premier élément de la liste (avant l'adjonction).

p^.Suivant := L ;
p^.Precedent := NIL;
L^.Precedent := p;
L := p;

Insertion après un élément

If Curseur^.Suivant <> NIL then
Curseur^.Suivant^.Precedent := p;
p^.Precedent := Curseur;
p^.Suivant := Curseur^.Suivant;
Curseur^.Suivant := p;

Exercices

Téléchargez :

- les exercices sur les types structurés ;

- les exercices sur les pointeurs et les listes.

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.

Les fichiers

But de ce chapitre

En utilisant ce que nous avons vu dans les cours précédents, vous ne pouvez pas réaliser un programme qui serait capable de conserver ses résultats une fois l'ordinateur éteint. D'autre part, les données doivent être nécessairement fournies par l'utilisateur à chaque exécution du programme. Ces limitations proviennent du type de mémoire utilisé : pour l'instant, vous ne savez traiter que des données stockées en mémoire vive. Or, la mémoire vive est volatile : lorsque l'ordinateur est mis hors tension, tout disparaît !

Ce problème pourrait donc être donc résolu si vous connaissiez les instructions permettant de lire ou d'enregistrer des données dans des mémoires non volatiles, stockées par exemple sur le disque dur ou sur une clé USB, sous la forme de fichiers.

L'objectif de ce cours est de vous décrire précisément les procédures Pascal réalisant ceci.

Notion de fichier

Qu'est-ce qu'un fichier ?

Un fichier est un ensemble de données informatiques stockées en mémoire auxiliaire (disque dur, CD, DVD, clef USB…) possédant un nom, un chemin d'accès et généralement implicitement associé à un format (format image, format de documents, formats de base de données…).

Le terme de mémoire auxiliaire désigne tout support de stockage de l'information d'un ordinateur autre que la mémoire vive.

Le fait qu'un fichier soit toujours stocké sur mémoire auxiliaire a deux conséquences.

Premièrement, les données contenues dans un fichier ne sont pas perdues lorsque l'ordinateur est mis hors tension (contrairement aux données contenues dans les variables d'un programme !).

Deuxièmement, l'accès à des données stockées dans un fichier est considérablement plus lent qu'un accès à des données contenues en mémoire vive.

Nom de fichier

La manière de nommer les fichiers dépend fortement du système d'exploitation. Par exemple, sous Windows, tout fichier possède généralement une extension (.doc, .jpg…) qui permet d'identifier son format.

Chemin d'accès

Le chemin d'accès d'un fichier définit sa localisation sur un périphérique donné. C'est une chaîne de caractères qui définit la suite des répertoires à parcourir pour atteindre le fichier depuis la racine du périphérique.

La manière de décrire un chemin dépend du système d'exploitation. Sous Windows, un chemin est une chaîne de caractères de la forme :

X:\Répertoire\Répertoire\ ... \Répertoire

où X est une lettre définissant le périphérique utilisé.

Par exemple :

C:\Utilisateurs\Thirion\Documents

Identification d'un fichier par son nom complet

Le nom d'un fichier ne l'identifie pas de manière unique. Par exemple, il peut exister un fichier nommé CV.doc sous

C:\Utilisateurs\Thirion\Documents

et un autre sous

C:\Utilisateurs\Einstein\Documents

Pour identifier un fichier de manière unique, il faut utiliser son chemin concaténé avec le nom (simple) du fichier. Par exemple :

C:\Utilisateurs\Thirion\Documents\CV.doc

désigne le CV de Thirion, alors que :

C:\Utilisateurs\Einstein\Documents\CV.doc

désigne le CV de Einstein.

J'utiliserai par la suite le terme de nom complet, pour désigner le chemin concaténé avec le nom du fichier.

Format

Le format d'un fichier est un ensemble de conventions définissant la représentation des données qu'il contient : la localisation et le codage des différents types d'informations contenues dans le fichier.

Le fichier en lui-même ne permet pas à coup sûr de retrouver son format de manière immédiate : comme toute chose en informatique, ce n'est qu'une suite de zéros et de uns. Une manière de vérifier si un fichier est bien dans un format donné est de l'ouvrir avec un logiciel adapté à ce format. Si le logiciel gère correctement le fichier, il est certainement dans le bon format (mais ce n'est pas une certitude !). Une autre manière est de scruter l'en-tête du fichier à la recherche d'une signature numérique propre à son format.

Exemple : si je peux ouvrir un fichier avec Word, il s'agit très certainement d'un fichier respectant un des formats acceptés par Word (.doc, .txt…). Au contraire, si j'essaie d'ouvrir avec Word un fichier qui n'est pas dans un format accepté par Word, il se produira probablement n'importe quoi.

Sous Windows, le format d'un fichier est repérable par l'extension (.doc pour un fichier Word, .exe pour un fichier exécutable…). Mais l'extension d'un fichier peut être changée manuellement… L'extension d'un fichier ne donne donc aucune certitude sur son format !

Les fichiers texte

Les fichiers texte sont des fichiers que l'on peut ouvrir avec un éditeur de texte (comme WordPad ou le Bloc-notes) afin de modifier leur contenu.

Ce sont par exemple les fichiers d'extension .txt sous Windows, les fichiers sources d'un programme, les fichiers HTML, etc.).

Un fichier texte est un fichier qui peut être interprété comme une suite de caractères (le plus souvent en code ASCII) affichables à l'écran.

Deux caractères spéciaux servent à coder le passage à la ligne et la fin du fichier.

Les fichiers structurés

Les fichiers structurés sont des fichiers interprétables comme une suite d'enregistrements d'un certain type. Tous les enregistrements sont forcément de même taille.

Les fichiers structurés peuvent être vus comme la représentation d'un tableau en mémoire auxiliaire. Rappelez-vous que tous les éléments d'un tableau doivent être de même type. Les éléments du tableau correspondent ici aux enregistrements.

Certains auteurs utilisent le nom de fichiers séquentiels pour désigner des fichiers structurés. Je trouve ce nom très mal adapté car il porte à confusion. Nous verrons pourquoi un peu plus loin.

Opérations sur les fichiers

Les langages de programmation permettent de :

- créer des fichiers ;

- enregistrer des informations dans un fichier : c'est l'accès en écriture ;

- lire des informations dans un fichier : c'est l'accès en lecture.

Les erreurs d'accès

Le système d'exploitation est responsable de la gestion des fichiers d'un ordinateur. Toute opération sur un fichier se fait donc nécessairement par l'intermédiaire du système d'exploitation. Lorsqu'un programme effectue une opération sur un fichier, il faut que le système d'exploitation autorise cette opération, sans quoi il se produira une erreur d'accès :

Voilà quelques-unes des erreurs les plus fréquentes :

- vous faites un accès en lecture sur un fichier qui n'existe pas ;

- vous faites un accès en lecture sur un fichier, alors que vous n'avez pas de droit de lecture sur ce fichier : ce fichier appartient à un autre utilisateur, c'est un fichier protégé, etc. ;

- vous faites un accès en écriture sur un fichier, alors que vous n'avez pas de droit d'écriture sur ce fichier ;

- vous faites un accès en écriture sur un fichier alors qu'un autre programme est en train d'y accéder en lecture ou en écriture ;

- vous faites un accès en écriture sur un fichier alors que le périphérique qui le contient est plein ;

- vous faites un accès en lecture ou en écriture « en dehors » du fichier.

Ouverture et fermeture d'un fichier

L'accès à un fichier par programmation fonctionne toujours selon le même principe :

- avant de lire ou d'écrire des informations dans un fichier, il faut l'ouvrir ;

- lorsque le traitement du fichier est terminé il faut le fermer.

À quoi sert l'ouverture d'un fichier ?

Ouvrir un fichier sert (entre autres) à informer le système d'exploitation que l'on a l'intention d'y accéder. C'est à ce moment-là que le système peut refuser l'accès au fichier, ce qui provoquera une erreur d'exécution du programme.

À quoi sert la fermeture d'un fichier ?

La fermeture d'un fichier sert (entre autres) à signaler au système d'exploitation que le fichier n'est plus utilisé par le programme, ce qui permettra à un autre programme (éventuellement d'un autre utilisateur) d'y accéder.

Accès direct et accès séquentiel

Il y a deux manières d'accéder à un fichier par programmation : l'accès direct et l'accès séquentiel.

Accès direct

L'accès direct n'est possible que pour des fichiers définis comme une suite de blocs de taille fixe que l'on appelle enregistrements. Il n'est donc possible que pour les fichiers structurés.

L'accès direct signifie qu'il est possible d'accéder directement à un enregistrement à partir de sa position dans le fichier : lire le énième enregistrement du fichier ou le modifier. Ce type d'accès est par exemple adapté à la gestion des tables d'une base de données.

Accès séquentiel

Dans l'accès séquentiel, une opération de lecture ou d'écriture se fait toujours juste après la dernière partie du fichier lue ou écrite.

Les fichiers texte n'autorisent que ce type d'accès.

Les fichiers structurés quant à eux autorisent les deux types d'accès (séquentiel ou direct).

Vous comprenez à présent pourquoi le fait d'appeler « fichiers séquentiels » les fichiers structurés est une source de confusion !

Manipulation des fichiers texte

Ouverture d'un fichier texte

L'ouverture d'un fichier texte se fait en lecture, en écriture ou en ajout. Nous ne traiterons pas ici de l'ouverture en ajout.

Si le fichier est ouvert en lecture, il ne peut y avoir que des lectures jusqu'à la fermeture du fichier.

De même, si le fichier est ouvert en écriture, il ne peut y avoir que des écritures jusqu'à la fermeture du fichier.

Opérations sur les lignes

Un fichier texte est normalement organisé en lignes. Les langages de programmation contiennent en général une instruction (ou une procédure) permettant de :

- lire une ligne du fichier en la recopiant dans une variable de type chaîne de caractères. Cette opération n'est possible que si le fichier est ouvert en lecture. De plus, comme il s'agit d'un accès séquentiel, elle se fait forcément juste après la dernière ligne lue. Sauf si le fichier vient d'être ouvert. Dans ce cas, la ligne lue est nécessairement la première ligne du fichier ;

- écrire une chaîne de caractères dans une ligne du fichier. De même, cette opération n'est possible que si le fichier est ouvert en écriture et se fait forcément juste après la dernière ligne écrite. Exception pour l'écriture d'une ligne qui suit immédiatement l'ouverture du fichier : ce sera forcément la première ligne du fichier.

Manipulation des fichiers structurés

Ouverture d'un fichier structuré

Pour ouvrir un fichier structuré, il n'est pas nécessaire de préciser si l'on veut ouvrir le fichier en lecture ou en écriture. En effet, avec un fichier structuré, ces deux types d'opérations (lecture et écriture) peuvent être mélangées dans n'importe quel ordre.

Si le fichier n'existe pas avant l'ouverture, il sera créé.

Accès séquentiel

L'accès séquentiel dans un fichier structuré consiste à lire ou à écrire un enregistrement sans préciser sa position. La lecture ou l'écriture se fait nécessairement juste après le dernier enregistrement lu ou écrit. Sauf si le fichier vient juste d'être ouvert. Dans ce cas, l'enregistrement lu ou écrit sera forcément le premier.

Accès direct

Dans l'accès direct, on accède à un enregistrement par sa position dans le fichier. Il est par exemple possible de lire le 25e enregistrement, puis de modifier le 122e. On peut parcourir le fichier dans n'importe quel ordre.

Manipulation de fichiers texte en Pascal

Déclaration

Un fichier texte est représenté en Pascal par le type TextFile. Pour pouvoir manipuler un fichier texte, il faut d'abord déclarer une variable de ce type. Par exemple :

var f : TextFile;

Dans toutes les explications qui suivent, f désignera le fichier déclaré de cette manière.

Assignation

Avant de pouvoir ouvrir un nouveau fichier texte, il faut d'abord associer la variable fichier au nom du fichier sur le disque. Cela se fait par la procédure AssignFile. Voici, par exemple, comment associer f à un fichier nommé exemple.txt :

AssignFile(f,'exemple.txt');

Création d'un fichier texte

Ouverture en écriture

Après avoir déclaré une variable de type fichier et assigné cette variable à un nom de fichier, on peut l'ouvrir en écriture. Cela a pour effet de créer un nouveau fichier vide. Attention : s'il existe déjà un fichier de même nom sur le disque, le contenu de ce dernier sera perdu !

L'ouverture en écriture, se fait par la procédure rewrite, comme suit :

rewrite(f);

Écriture d'une ligne

Dès que le fichier est ouvert en écriture, on peut y écrire du texte en utilisant la procédure writeln. Voici, par exemple, comment créer un fichier texte exemple.txt d'une ligne :

var f: TextFile;
begin
assign (f,'exemple.txt');
rewrite(f);
writeln (f,'Ceci est un fichier d'une ligne.');
CloseFile(f);
end;

La procédure CloseFile appelée à la fin sert à fermer le fichier.

Comme on fait de l'accès séquentiel, une écriture se fait forcément après la ligne écrite juste avant. Donc, après l'exécution du code suivant :

var f: TextFile;
begin
assign(f,'exemple.txt');
rewrite(f);
writeln(f,'Première ligne du fichier exemple.txt');
writeln(f,'Deuxième ligne du fichier exemple.txt');
CloseFile(f);
end;

le fichier exemple.txt contiendra les deux lignes :

Première ligne du fichier exemple.txt
Deuxième ligne du fichier exemple.txt

Lecture d'un fichier texte

Ouverture d'un fichier texte en lecture

Pour ouvrir un fichier en lecture, on utilise la procédure reset de la manière suivante :

reset(f);

Comme pour l'ouverture en écriture, il faut que le fichier ait auparavant été assigné à un nom de fichier. Par contre, si ce fichier n'existe pas, cela provoquera une erreur.

Lecture d'une ligne et test de fin de fichier

Pour lire une ligne dans le fichier, on utilise la procédure readln, qui affecte la ligne courante à une variable L de type chaîne de caractères :

readln(f,L);

Il s'agit ici d'un accès séquentiel, donc une lecture de ligne se fait nécessairement juste après la ligne lue précédemment.

Juste après l'ouverture du fichier, readln transfère la première ligne du fichier dans variable L.

À la deuxième exécution de cette procédure, L contiendra la 2e ligne du fichier et de manière générale, après la énième exécution, L contiendra la énième ligne du fichier.

Bien entendu, on provoquera une erreur d'exécution si l'on essaie de lire au-delà de la fin du fichier. Par exemple, si le fichier exemple.txt ne contient que 10 lignes, la 11e exécution de readln(f,L); provoquera une erreur d'exécution.

Pour éviter cette erreur, il faut utiliser la fonction eof, qui permet de tester si l'on se trouve en fin de fichier. Par exemple, le code suivant permet de lire toutes les lignes du fichier exemple.txt afin d'effectuer un certain traitement sur chaque ligne :

Var f : TextFile; L : String;
begin
assign(f,'exemple.txt');
reset(f);

while not eof(f) do
begin
readln(f,L);
// Traitement de la ligne L
end;
CloseFile(f);

end;

Les fichier structurés en Pascal

Déclaration

Un fichier structuré est un fichier constitué d'enregistrements de même type.

Le type d'un enregistrement est a priori n'importe quel type Pascal de taille fixe. En effet, comme chaque enregistrement doit occuper la même taille en mémoire auxiliaire, on ne peut pas déclarer un fichier constitué d'enregistrements de longueur variable. En particulier, le type String sera interdit et devra être remplacé par le type String[n], qui permet de préciser la taille de la chaîne de caractères.

Pour la suite des explications, nous allons utiliser le type structuré Livre déclaré comme suit :

Livre =
record
Titre : String[100];
Auteur : String[20];
AnnéePub : integer;
end;

Nous pouvons ensuite déclarer un fichier structuré composé d'enregistrements de type Livre comme suit :

var f : file of Livre;

Remarquez que le type des enregistrements d'un fichier structuré n'est pas forcément un type structuré. On peut par exemple déclarer un fichier d'entiers (file of integer) ou un fichier de caractères (file of char).

Assignation

Tout comme pour les fichiers texte, avant d'ouvrir un fichier structuré il faut associer la variable représentant le fichier à un nom de fichier avec la procédure AssignFile.

Exemple :

AssignFile(f,'Bibliotheque.dat');

Cette instruction assigne f au fichier de nom Bibliotheque.dat.

Ouverture et fermeture

Pour ouvrir des fichiers structurés, on utilise encore les procédures reset et rewrite. Mais elles n'ont pas la même signification que pour les fichiers texte.

En effet, il sera toujours possible de lire et d'écrire des enregistrements, que le fichier soit ouvert avec reset (ouverture en lecture pour un fichier texte) ou avec rewrite (ouverture en écriture pour un fichier texte).

Pour les fichiers structurés, rewrite n'est pas une ouverture en écriture. Cela signifie seulement que l'on demande au système d'exploitation de créer un nouveau fichier. Avec reset, on demande simplement l'ouverture, sans création d'un nouveau fichier.

Par contre, un fichier ne peut pas être ouvert avec reset s'il n'existe pas. Pour ouvrir un fichier structuré sans savoir auparavant s'il existe ou non, on utilisera la fonction FileExists (fonction permettant de tester l'existence d'un fichier de nom donné) de la manière suivante :

if FileExists('Bibliotheque.dat') then
reset(f)
else
rewrite(f);

Écriture séquentielle

L'écriture d'un enregistrement se fait avec la procédure write. Plus précisément, write (f,v) écrit la valeur de la variable v dans le fichier f à la suite du dernier enregistrement lu ou écrit. Bien entendu, la variable doit être de même type que les enregistrements du fichier.

Si le fichier vient d'être ouvert, l'écriture se fait dans le premier enregistrement.

Déclarons par exemple deux structures de type Livre :

var VB, Gaston : Livre;

Donnons des valeurs à leurs champs :

VB.Titre := 'Visual Basic 6, Le guide du programmeur';
VB.Auteur := 'Frantz';
VB.AnneePub := 2000;
Gaston.Titre := 'Gaston Lagaffe - Des Gaffes et des Dégâts';
Gaston.Auteur := 'Franquin';
Gaston.AnneePub := 1975;

Après les instructions suivantes :

AssignFile(f,'Bibliotheque.dat');
rewrite(f);
write(f,Gaston);
write(f,VB);

le fichier Bibliotheque.dat contiendra deux enregistrements de type Livre. Plus précisément, le premier enregistrement contiendra le livre Gaston et le deuxième, le livre VB.

Si, après cela, on fait encore une fois

write(f,Gaston);

le fichier contiendra trois enregistrements (Gaston, VB, Gaston).

Si on écrit un enregistrement juste après l'ouverture d'un fichier existant (donc avec un reset), cette écriture se fera dans le premier enregistrement du fichier. Par exemple, après l'exécution du code suivant :

AssignFile(f,'Bibliotheque.dat');
reset(f);
write(f, VB);

le fichier contiendra (VB, VB, Gaston).

Lecture séquentielle

La lecture d'un enregistrement à la suite du dernier enregistrement lu ou écrit se fait par la procédure read. Plus précisément, pour affecter la valeur d'un enregistrement à une variable v, on écrira read(f,v). Cela suppose bien entendu que la variable est du même type que les enregistrements du fichier.

Si le fichier vient d'être ouvert, l'enregistrement lu sera le premier.

Reprenons la suite de l'exemple précédent. Notre fichier Bibliotheque.dat contient à présent trois enregistrements de type Livre (VB, VB, Gaston).

Pour les récupérer, déclarons trois variables de type Livre :

var Livre1, Livre2, Livre3 : Livre;

Après les instructions suivantes :

AssignFile(f,'Bibliotheque.dat');
reset(f);
read(f,Livre1);
read(f,Livre2);
read(f,Livre3);

les structures Livre1 et Livre2 contiendront le livre VB et Livre3 contiendra le livre Gaston.

Lecture jusqu'en fin de fichier

De manière générale, on ne connaît pas le nombre d'enregistrements contenus dans un fichier structuré. On utilise alors la fonction eof pour savoir si on est arrivé à la fin du fichier. Voici comment lire séquentiellement un fichier structuré afin d'effectuer un traitement sur chacun de ses enregistrements :

var f : file of type;
x : type;
AssignFile(f,'Bibliotheque.dat');
reset(f);
while not eof(f) do
begin
read(f,x);
// Traitement de l'enregistrement x
end;

Exercices

Téléchargez les exercices sur le site de l'auteur :

- Exo-Fichiers-Pascal.pdf

Pour obtenir les corrigés, le téléchargement n'est possible que via un login et un mot de passe, que vous pouvez obtenir en envoyant un mail à l'adresse suivante :

en précisant un peu qui vous êtes et les raisons pour lesquelles ce cours vous intéresse.