Les bases de la programmation


précédentsommairesuivant

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.

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

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 :

Image non disponible

et voici un extrait du code de ce programme :

 
Sélectionnez
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 :

Image non disponible

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é !

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible

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 :

Image non disponible

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 :

Image non disponible

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 :

Image non disponible

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.

Image non disponible

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 :

Image non disponible

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 :

Image non disponible

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 :

Image non disponible

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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

Image non disponible

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 :

Image non disponible

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

Image non disponible

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 :

 
Sélectionnez
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 :

Image non disponible

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 :

 
Sélectionnez
  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 :

 
Sélectionnez
  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 :

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

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

À 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 :

 
Sélectionnez
  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 :

 
Sélectionnez
  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 m2.

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

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
   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 m2. En ramenant ce résultat dans l'expression de départ nous obtenons :

 
Sélectionnez
34,46 - 3,78;

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

Image non disponible

Principes généraux et compléments

Déclaration et appel de procédures

Déclaration d'une procédure

 
Sélectionnez
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 :

 
Sélectionnez
   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 :

 
Sélectionnez
   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.

 
Sélectionnez
   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 :

 
Sélectionnez
   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 :

 
Sélectionnez
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 :

 
Sélectionnez
         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 :

 
Sélectionnez
    FormerMail (Prenom1, Nom);
 
Sélectionnez
    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.

Image non disponible

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 :

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

Image non disponible

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

 
Sélectionnez
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 :
 
Sélectionnez
NomDeLaFonction := expression;

Appel de fonction

Un appel de fonction s'écrit :

 
Sélectionnez
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 :

 
Sélectionnez
    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 :

Image non disponible

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 :

Image non disponible

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 :

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 :

Image non disponible

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


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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