[EN] [IT]
[INFO] [ACQUÉRIR] [PLAN] [RESSOURCES]
CHAPITRE
APPRENDRE À PROGRAMMER AVEC FUTUREBASIC
NOTES

Les branchements II

Déclaration de fonctions
Portée des variables

Leçon 11 : Les fonctions-utilisateur

Déclaration d'une fonction
Pour pallier les carences des routines appelées par GOSUB, FutureBASIC a introduit, dans son implémentation du langage BASIC, la notion de fonction-utilisateur, comme les langages Pascal ou C le proposaient déjà. Cette notion de fonction-utilisateur ou LOCAL FN est dʼune importance primordiale. Les avantages patents des fonctions-utilisateur que nous allons examiner ont conduit tout simplement à la disparition de la commande GOSUB dans la quasi totalité des bouts de code que vous serez amené à rencontrer ultérieurement. Il est bien sûr toujours possible de programmer en BASIC dit standard avec FutureBASIC, mais il est clair que la commande GOSUB a été supplantée par la commande LOCAL FN à juste titre.

Les fonctions locales doivent être déclarées avant de pouvoir être utilisées. La déclaration dʼune fonction est a priori très simple :

LOCAL FN maFonction
  PRINT "A lʼintérieur de maFonction"
END FN


Les commandes LOCAL FN/END FN délimitent le corps de la fonction qui contient les instructions à exécuter.

Vous devez donner un nom unique à votre fonction; tout comme avec les constantes, vous ne pouvez pas avoir deux fonctions ayant des noms identiques dans un même programme.

Tout comme les routines invoquées par GOSUB, les fonctions peuvent appeler dʼautres fonctions préalablement définies, toutefois, vous ne pouvez pas déclarer une fonction à lʼintérieur dʼune autre fonction, dit autrement, les instructions LOCAL FN ne peuvent pas être imbriquées. On peut brancher très facilement le flux du programme sur une fonction (on dit appeler une fonction). La ligne de code suivante appellera maFonction et provoquera lʼexécution des lignes de code quʼelle contient, en lʼoccurrence dans lʼexemple ci-dessus, la chaîne « A lʼintérieur de maFonction » sera affichée à lʼécran.

FN maFonction

De manière générale, les fonctions sont déclarées avant lʼexécution du programme principal. Un programme se présente le plus souvent comme une longue suite de déclarations de fonctions terminée par le programme principal. Le Compilateur créera le code exécutable pour chacune dʼelles sans exécuter les instructions, car les fonctions ne sont exécutées que si vous les appelez explicitement par des instructions de votre code source.

LOCAL FN maPremiereFonction
END FN

LOCAL FN
maDeuxiemeFonction
END FN

LOCAL FN
maTroisiemeFonction
END FN

REM
programme principal. Début de lʼexécution
FN maDeuxiemeFonction
FN maPremiereFonction

Notez que les fonctions sont déclarées au fur et à mesure que le Compilateur les rencontre lorsquʼil produit le code exécutable. Arrivé donc au programme principal toutes les fonctions auront été déclarées et pourront donc être utilisées. Mais que se passerait-il si, par exemple la fonction maDeuxiemeFonction devait faire elle-même appel à maTroisiemeFonction ?

LOCAL FN maPremiereFonction
END FN

LOCAL FN
maDeuxiemeFonction
  FN maTroisiemeFonction
END FN

LOCAL FN
maTroisiemeFonction
END FN


Lorsque le Compilateur arrivera à maDeuxiemeFonction il voudra générer le code exécutable pour le corps de la fonction, malheureusement maTroisiemeFonction nʼaura pas encore été déclarée, il ne pourra donc pas compiler le code adéquate et arrêtera son travail en vous signalant que maTroisiemeFonction nʼa pas encore été définie.

Il y a heureusement des solutions à ce problème. La plus évidente est de réorganiser le code source pour que maTroisiemeFonction soit déclarée avant maDeuxiemeFonction :

LOCAL FN maPremiereFonction
END FN

LOCAL FN
maTroisiemeFonction
END FN

LOCAL FN
maDeuxiemeFonction
  FN maTroisiemeFonction
END FN


Lʼordre de déclaration des fonctions a donc une importance. Il existe cependant un autre mécanisme qui permet de pré-déclarer des fonctions et qui peut vous éviter la réorganisation de votre code source. Les fonctions peuvent-être prototypées, cʼest-à-dire quʼen fait, vous indiquez au Compilateur que la définition de la fonction viendra par la suite dans votre code source. La commande DEF FN vous permet de prototyper des fonctions :

DEF FN maTroisiemeFonction

LOCAL FN
maPremiereFonction
END FN

LOCAL FN
maDeuxiemeFonction
  FN maTroisiemeFonction
END FN

LOCAL FN
maTroisiemeFonction
END FN


Certains programmeurs ont pris lʼhabitude de prototyper toutes les fonctions utilisées dans leur programme et de placer cette liste de prototypes au début de leur code source de manière à ne plus se poser la question de savoir quelle fonction doit être déclarée avant telle autre.

Les fonctions-utilisateur ont non seulement la qualité des routines appelées par GOSUB, mais elles en décuplent la puissance.

A lʼintérieur dʼune fonction locale vous pouvez déclarer des variables qui nʼauront dʼexistence que tant que la fonction sera en cours dʼexécution. Cette particularité introduit la notion de portée des variables. Ceci est très important : toutes les variables nʼont pas la même portée. Une fonction-utilisateur ne sait rien des autres variables déclarées en dehors de son champ dʼaction.

Chaque fonction dispose donc de son propre jeu de variables à la manière dʼun petit programme à lʼintérieur dʼun plus grand. Deux fonctions différentes peuvent déclarer des variables ayant le même nom sans que le Compilateur ne signale de conflit. Les deux variables homonymes sont deux entités bien distinctes qui peuvent par ailleurs être de types différents :

LOCAL FN maPremiereFonction
  DIM x AS SHORT
END FN

LOCAL FN
maDeuxiemeFonction
  DIM x AS RECT
END FN


Lorsquʼune fonction a fini dʼêtre exécutée, ses variables locales retournent au néant à lʼexception des variables qui référencent des blocs de mémoire relogeables ou non que vous avez réservés explicitement ou indirectement. En réalité, dans un tel cas, la variable disparaît, et vous ne pouvez plus y faire référence dans une instruction, mais le bloc de mémoire est considéré comme toujours alloué si vous ne prenez pas soin de le libérer avant de quitter la fonction. Notez quʼil est assez rare de créer soi-même des pointeurs qui allouent un espace en mémoire, car généralement on laisse FutureBASIC se charger du travail en déclarant des variables. FutureBASIC alloue lʼespace et le libère automatiquement pour chaque variable que vous déclarez avec lʼinstruction DIM. Le problème devient plus délicat du fait que lʼon peut faire appel aux fonctions et procédures de la Toolbox dans le corps de la fonction locale, car le programmeur a la possibilité de réserver lui-même un bloc en mémoire de la taille quʼil souhaite avec des instructions comme NewPtr ou NewHandle. Prenez lʼexemple suivant :

LOCAL FN danger
  DIM monPointeur AS PTR
  monPointeur = FN NewPtr(100)
END FN

Dans lʼexemple ci-dessus, la variable monPointeur est déclarée, rappelez-vous quʼune variable pointeur est censée contenir lʼadresse dʼun bloc mémoire non relogeable. La fonction de la Toolbox NewPtr réserve un bloc en mémoire de la taille que vous lui demandez (ici 100 octets) et retourne lʼadresse du bloc en mémoire si tout sʼest bien passé. Cette adresse sera alors rangée dans la variable monPointeur. Lorsque la fonction se termine, FutureBASIC détruit la variable monPointeur qui contient cette adresse, vous nʼavez alors plus aucun moyen de connaître lʼemplacement où le bloc a été alloué. Maintenant, imaginez que votre fonction soit appelée des centaines de fois au cours du programme. Que se passera-t-il ?

Eh bien, à chaque fois que la fonction sera exécutée une nouvelle variable monPointeur sera créée, et un nouveau bloc de 100 octets sera réservé. Les blocs successifs qui vont être alloués ne disparaîtront pas comme par enchantement, au contraire, ils vont sʼaccumuler jusquʼà saturer la mémoire et conduire au blocage de votre application. Ce phénomène est une erreur très fréquente en programmation, on appelle cela plus communément une fuite de mémoire (ou memory leak en anglais). Il nʼest pas toujours facile de déceler les fuites de mémoire dans un programme, dans la mesure où tout peut sembler fonctionner normalement pendant très longtemps si la fuite nʼest pas importante, jusquʼau moment où le programme se bloque faute de mémoire disponible sur une opération anodine nʼayant aucun rapport avec la cause du plantage.

Pour éviter de tomber dans ce piège, il est souhaitable de suivre au plus près les règles suivantes :

- Tout espace réservé durant lʼexécution dʼune fonction locale devrait autant que faire se peut être libéré avant que la fonction ne prenne fin :

LOCAL FN fonctionSecurisee
  DIM monPointeur AS PTR
  
  monPointeur = FN NewPtr(100)
  REM faire quelque chose avec le pointeur
  DisposePtr(monPointeur)
END FN


La procédure de la Toolbox DisposePtr libère la mémoire occupée par un bloc non relogeable.

- La deuxième règle est de ne pas allouer vous-même de pointeur sur un bloc de mémoire et de laisser FutureBASIC faire le ménage pour vous :

LOCAL FN fonctionSansDanger
  DIM maVariable.100
  REM
faire quelque chose avec maVariable
END FN


Ci-dessus, le Compilateur réservera un bloc de 100 octets pour la variable maVariable, bloc quʼil libèrera en même temps que la variable sera détruite.

Notez que les bouts de code ci-dessus sont présentés à titre dʼillustration, en réalité, sur les millions de lignes de code quʼil mʼa été donné de consulter, je peux compter sur les doigts dʼune seule main le nombre de fois où jʼai pu constater que le programmeur avait réservé de la mémoire au moyen de la fonction NewPtr. La dernière forme montrée est la forme en vigueur chez les programmeurs FutureBASIC.

Si la difficulté engendrée par les pointeurs explicitement déclarés peut être réduite à néant par lʼemploi de variables, comme dans lʼexemple montré ci-avant, il nʼen va pas de même avec les handles. Le Compilateur ne réservera, ni ne libérera automatiquement la mémoire dans le cas des handles. Le plus souvent, cʼest le programmeur qui alloue et libère lui-même les blocs relogeables au moyen des appels à la Toolbox NewHandle et DisposeHandle. Les choses se compliquent dʼautant que beaucoup dʼautres fonctions et procédures de la Toolbox peuvent se charger de réserver des blocs relogeables pour vous et de vous communiquer le moyen dʼaccéder à ces blocs. La libération des blocs ainsi réservés doit se traiter au cas par cas suivant les fonctions invoquées. Ici, il nʼy a pas de mystère, il faut consulter la documentation sur la Toolbox d'Apple pour savoir ce quʼon est autorisé à faire ou pas dans telle ou telle circonstance.

Grâce à l'autonomie des fonctions locales, la mémoire est utilisée plus efficacement, leurs variables nʼoccupant lʼespace mémoire que durant le bref instant de lʼexécution de la fonction. Lʼavantage immédiat des fonctions locales est de faciliter lʼécriture de programmes très sophistiqués, comportant des milliers de fonctions, sans jamais saturer la mémoire par une cohorte de variables. De surcroît, il devient facile de faire évoluer de tels programmes.

Portée des variables
Tout ceci est parfait, mais que se passe-t-il si une fonction locale a besoin de connaître, pour éventuellement modifier, la valeur dʼune variable qui a été déclarée et assignée en dehors de son champ dʼaction ?

LOCAL FN bienvenue
  PRINT "Bonjour "; prenom
END FN

// Programme principal
DIM prenom AS STR63

prenom = "Élodie"
FN bienvenue

Le programme ci-dessus ne donnera pas le résultat qu'on pourrait croire, car la variable prenom n'est pas connue de la fonction locale bienvenue. Créer une autre variable prenom à l'intérieur de la fonction ne résoudra pas le problème, on aura juste créé une variable différente qui n'existera que durant l'exécution de la fonction.

Il y a deux possibilités pour régler cette apparente difficulté.

La première qui vient à l'esprit est de se dire qu'il serait nécessaire d'avoir des variables particulières qui seraient "vues" par toutes les fonctions locales qui pourraient en avoir besoin. Il est, en effet, possible de créer ce genre de variable grâce à la structure BEGIN GLOBALS/END GLOBALS. Toutes les variables déclarées à l'intérieur d'une telle structure sont accessibles par l'ensemble des fonctions locales du programme.

Rappelez-vous que l'ordre dans lequel apparaissent vos déclarations a une importance au moment de la compilation, c'est pourquoi de manière générale, on commence par déclarer dans son code source les variables qui auront une portée globale dans l'ensemble du programme. Le programme ci-dessus, s'écrirait éventuellement comme ceci pour atteindre le résultat souhaité :

BEGIN GLOBALS
  DIM
gPrenom AS STR63
END GLOBALS

LOCAL FN
bienvenue
  PRINT "Bonjour "; gPrenom
END FN

// Programme principal
gPrenom = "Élodie"
FN bienvenue

La tendance naturelle d'un apprenti programmeur est de déclarer une variable en tant que globale à chaque fois qu'il a besoin de la manipuler dans différentes fonctions. Autant le dire tout de suite, ceci n'est pas une bonne pratique.

La première bonne habitude à prendre est de faire le contraire en utilisant le moins de variables globales possible; cet exercice vous oblige à réfléchir sur les réels besoins de votre programme, sur l'organisation des variables qu'il doit manipuler et sur les fonctions qu'il vous faudra écrire pour réaliser les tâches dévolues à votre application.

Le deuxième avantage, et sans doute le plus important, est de vous conduire à écrire des fonctions complètement indépendantes qui pourront, par conséquent, être réutilisées dans d'autres projets par simple copier/coller sans plus de travail à fournir de votre part. À la différence des routines appelées par GOSUB qui ont besoin d'avoir accès aux variables qu'elles doivent manipuler, les fonctions-utilisateur sont très facilement réutilisables. C'est de cette façon que les programmeurs se constituent au fil des années leurs bibliothèques de fonctions spécialisées prêtes à l'emploi. Fort heureusement, on ne réinvente pas la roue à chaque fois que l'on commence un nouveau projet, mais notez que ceci est vraiment beaucoup plus difficile à obtenir avec des routines appelées par GOSUB du fait de leur dépendance avec le programme principal dans lequel elles auront été programmées à l'origine.

Une autre bonne habitude à prendre également concernant les variables globales est de les nommer de manière à pouvoir les distinguer facilement partout où elles sont utilisées. La convention d'écriture traditionnellement adoptée est de leur donner un nom symbolique qui commence avec un g minuscule. Il est alors aisé pour vous, comme pour un éventuel lecteur, de déterminer à la première lecture, dans un bout de code isolé, les variables qui sont locales à une fonction et celles qui sont susceptibles d'être modifiées ailleurs par le programme.

La deuxième solution au problème posé consiste à envoyer à la fonction des informations au moment où elle est appelée. On peut passer des paramètres à une fonction locale, mais cette fonctionnalité nécessite à elle seule un long chapitre d'explications…



[Precédent] [Table des Matières] [Suivant]
{Note}
© 2000 Pix&Mix
Tous droits réservés

FutureBASIC est une marque déposée appartenant à Staz Software, Inc et utilisée avec permission.