calculette de bureau
Ces travaux pratiques sont à effectuer seul.
Les réponses aux exercices sont à envoyer à mon adresse mail avant la séance suivante.
Remarque : Cet exercice est plus difficile que les précédents. Attendez-vous à y passer plus de temps.
Nous nous proposons de réaliser une calculette de bureau sous la forme d'un programme informatique écrit en langage C++ qui répond aux caractéristiques suivantes :
Une expression valide est définie par une suite d'opérandes (des nombres réels) et d'opérateurs (un des quatre suivants : , , , ).
Le tableau suivant donne des exemples de notations postfixées avec leur équivalent infixé.
Notation postfixée | Notation infixée |
L'algorithme de traitement est de fait assez simple :
Nous allons procéder à une réalisation pas à pas en essayant de faire un programme fonctionnel à chaque étape.
Il sera enrichi de fonctionnalités et sa robustesse sera améliorée au fur à et à mesure.
Il vous appartient de gérer la répartition du code en plusieurs fichiers.
Vous devez avoir à présent une idée assez précise de la façon dont va fonctionner cette calculette.
Nous allons la créer sous la forme d'une classe donc le nom sera pcalc
.
À partir de cela réfléchissez sans coder quoi que ce soit à tous les champs et à toutes les méthodes nécessaires à son fonctionnement, ainsi qu'à leur visibilité (public
, protected
, private
).
Pensez en terme de fonctionnalités et imaginez plusieurs méthodes qui font peu de choses mais qui le font bien.
Nous avons vu que nous allons avoir besoin d'une pile pour stocker les opérandes et les résultats des opérations.
Nous allons de plus avoir besoin d'une autre pile pour l'historique.
Le C++ actuel, propose dans sa STL (Standard Template Library) une classe stack
. Cependant, celle-ci ne se comporte pas tout à fait comme une pile classique, à savoir que l'opération pop()
dépile bien le résultat mais ne le retourne pas. Il faut pour cela d'abord utiliser top()
qui elle retourne le résultat mais ... ne le dépile pas.
Je vous propose donc une classe patron vStack
↓, extrêmement basique, basée sur des vecteurs, qui se comporte comme espéré.
Téléchargez et ajoutez des commentaires à ce code.
Il est important que vous compreniez bien ce code qui vous présente une classe complète et qui introduit en plus la notion de template
, c'est à dire de classe générique.
N'hésitez pas à poser des questions, à échanger vos impressions, ...
Note : ce code est difficile à comprendre et cette partie va vous demander beaucoup de temps. Ne faites pas l'impasse dessus, cela conditionne en grande partie la compréhension du reste de ce module.
Les expressions que va entrer l'utilisateur vont toujours être "vues" par notre programme comme des chaînes de caractères. Cependant ce sont des nombres que nous désirons manipuler. Il faut donc faire la conversion d'une représentation à l'autre.
Pour faire cela il existe plusieurs moyens :
strtod
;iostream
du C++.C'est cette dernière solution que nous allons utiliser.
Pour cela vous déclérerez une variable de type istringstream
dont l'instance sera initialisée à la construction avec la chaîne de caractères à convertir.
Cette chaîne de caractères sera du type string
qui est une des classe de base de la STL et qui, comme son nom l'indique, permet de manipuler des chaînes de caractères : de plus elle a l'avantage de proposer une gestion telle que le programmeur n'a pas à se soucier de la gestion de l'espace nécessaire à leur stockage.
Cette classe propose une interface qui permet de manipuler les objets de type string
comme des flux d'entrée.
Cela signifie que l'on va pouvoir extraire notre chaîne de caractères dans un objet d'un autre type avec l'opérateur >>
et que la conversion va se faire automatiquement.
Cependant, il se peut que cette conversion échoue (par exemple, on ne parviendra pas à convertir la chaîne "cent vingt cinq virgule 12" en un double). Dans ce cas, l'opérateur >>
va positionner des bits d'erreur dans l'objet sur lequel il est utilisé, ici notre istringstream
. La lexture de ces bits se fait à l'aide de la méthode rdstate
de l'objet en question dont on combine la valeur retournée aux valeurs ios::failbit
, ios::badbit
, ... comme dans le code ci-dessous :
double toDouble(const string &s, bool &err) { istringstream S(s) ; double d ; S >> d ; if ( S.rdstate() & ios::failbit ) { // ça n'a pas fonctionné } else { // ça a fonctionné } // ... return d ; }
Enfin, pour utiliser istringstream
vous devrez inclure les fichiers d'en-têtes iostream
et sstream
.
Dans la classe pcalc
ajoutez (déclaration et définition) une méthode privée dont la signature est :
double toDouble(const string &s, bool &err) ;
le booléen err
sert à informer la méthode appelante du fait que la conversion s'est bien passée ou pas (true
quand il y a eu une erreur, false
sinon).
L'objet istringtream
sera initialisé (au sens de son constructeur) avec la chaîne s
.
Les expressions sont ce qui va nourrir notre calculette. Nous avons vu de quoi elles doivent être composées mais rien ne permet de savoir ce que l'utilisateur va entrer et dans quel ordre. Pour cela il faut que notre programme tourne autour d'une boucle de prise en charge d'une expression.
Dans un premier temps, nous considérerons que l'expression est une unique chaîne de caractères comme dans l'exemple ci-dessous :
#> ./pcalc Bienvenue dans Pcalc v0.1beta Bons calculs... pcalc> 4 3 + 7 pcalc>
Cette expression sera stockée dans une variable de type string
pour ensuite être découpée et analysée conformément à l'algorithme.
Comme vous disposez déjà d'une classe de pile, vous pouvez traiter directement les opérandes. Nous verrons les opérateurs plus tard.
Mettez en place le corps de la boucle de traitement dans une méthode de la classe pcalc
.
Cette méthode sera réutilisée dans le cadre du mode interactif et de mode non-interactif, assurez-vous qu'elle soit robuste et que son interface soit pratique à utiliser.
Dans ce mode, l'espression à évaluer est passée directement sur la ligne de commande. Plusieurs cas peuvent alors se présenter et nous en montrons certains sur l'exemple ci-dessous :
#> ./pcalc 4 3 + 7 #> ./pcalc "4 3 +" ← Notez les guillemets 7 #> ./pcalc "4 3" + ← Notez le mélange guillemets/non-guillemets 7 #>
Vous remarquerez donc qu'il peut y avoir un ou plusieurs arguments sur la ligne de commande. Mais au final, tout se passe comme si vous n'aviez qu'une série d'expressions à évaluer les unes à la suite des autres. La gestion de votre pile ne change pas et vous aurez un résultat à la fin.
Que ce soit en mode interactif ou pas, notre programme va traiter les expressions qui lui sont passées.
En mode interactif, il va devoir en plus traiter les commandes passées par l'utilisateur.
Le mode interactif est utilisé automatiquement lorsque le programme est lancé sans argument.
Créez une méthode qui sera appelée uniquement en mode interactif et qui comporte une boucle de gestion des commandes utilisateur.
Cette boucle devra orienter le programme vers la boucle de traitement des expressions si ce qui est passé par l'utilisateur n'est pas une commande.
Les trois commandes =, d et h ont pour résultat des affichages plus ou moins complexes.
Pour l'affichage du résultat, il n'y a pas de méthode vraiment particulière à utiliser.
Par contre, pour l'affichage des piles, il sera intéressant de se pencher sur le code de la classe vStack
et en particulier sur la surcharge de l'opérateur >>
.
Codez deux méthodes publiques de la classe pcalc
, une pour l'affichage du résultat et l'autre pour l'affichage de la pile.
Intégrez leur appel dans la boucle de gestion des commandes.
L'affichage de l'historique sera traité plus tard.
Une des fonctionnalité de notre calculette est de pouvoir donner la version infixée (voir le tableau) de l'expression évaluée.
Il existe plusieurs façons de gérer cela et nous allons étudier celle utilisant un arbre binaire d'évaluation.
Un tel arbre répond à une structuration bien précise :
La figure suivante représente un tel arbre.
La façon dont on parcours l'arbre permet d'évaluer l'expression d'une façon ou d'une autre.
Par exemple, un parcours récursif dit GDR (pour fils Gauche, fils Droit, Racine) permet d'obtenir la représentation postfixée que nous manipulons depuis le début de ces travaux pratiques.
Cependant un problème se pose : alors qu'en notation postfixée, il n'est pas utile d'utiliser de parenthèses, elles sont obligatoires en notation infixée pour respecter l'ordre correct d'évaluation des opérateurs en fonction de leur priorité.
Dans le cadre de ces travaux pratiques, nous vous demandons une gestion minimale des parenthèses : toute expression sera automatiquement parenthésée. Par exemple, l'expression de la figure ci-dessus donnera la sortie infixée suivante : ((5/(4+3))-3).
Proposez une méthode de parcours d'un arbre d'évaluation qui permette d'obtenir la notation infixée.
Si vous avez le temps, vous pourrez proposer un algorithme permettant d'avoir la notation avec le minimum de parenthèses.
Pour que notre programme puisse faire la conversion d'une notation à l'autre, nous devons transformer notre historique en un arbre binaire d'évaluation.
Pour cela, nous avons besoin d'une classe représentant un arbre binaire. Comme il n'en existe pas dans la STL et que nous sommes ici dans un but d'apprentissage↓, nous allons la créer.
Cette classe devra être générique : ce sera une classe patron (template) qui pourra contenir n'importe quel type d'objet (un arbre de nombres réels, un arbre de personnes, ...). Ceci permet donc que la réutiliser dans d'autres situations, dans d'autres programmes et c'est là tout l'avantage de la programmation objet.
Pour vous aider dans l'écriture de cette classe, vous trouverez dans la figure ci-dessous la déclaration de la classe, de ses champs et de ses méthodes.
Écrivez la définition des méthodes de la classe node
.
→ Si vous n'avez pas le temps, vous pouvez trouver une version de cette classe ici.
Mais que va donc contenir notre arbre ? D'un côté nous avons les opérateurs et de l'autre les opérandes. Bien que notre classe d'arbre binaire soit générique, elle n'est capable de stocker qu'un type d'objet à la fois.
Pour palier ce problème une solution est de créer une classe container qui puisse recevoir nos deux objets. Encore une fois, il y a plusieurs façons de faire. À vous de choisir parmi l'une de celles-là, ou même une autre :
Écrivez une classe elem
qui réponde à cette problématique.
Une fois ceci fait, vous pourrez créer votre pile d'historique qui, tout naturellement, contiendra des objets de type elem
.
De fait, vous pourrez écrire la méthode d'affichage de la pile d'historique et intégrer son appel à la boucle de gestion des commandes du mode interactif.
Si vous êtes arrivés jusqu'ici et que votre calculette est fonctionnelle et qu'elle répond au cahier des charges, vous pourrez peut être avoir envie de la développer plus avant.
Une piste serait d'ajouter la possibilité de prendre en compte les opérateurs complexes comme par exemple les opérateurs trigonométriques, la racine carrée,
Une autre amélioration serait la façon dont la méthode toDouble
nous averti qu'une erreur s'est produite. Une manière élégante de faire serait d'employer des exceptions.
Enfin, nous pourrions nous intéresser aux entrées/sorties de notre programme en lui ajoutant la possibilité de lire des expressions depuis un fichier. Chaque ligne du fichier représenterai une expression à évaluer et/ou à transformer en expression infixée en fonction d'une commande (un caractère) qui se trouverait en début de ligne (c
pour compute et t
pour transform par exemple).