Travaux Pratiques n°4

calculette de bureau

Instructions

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.

Cahier des charges

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 :

  1. La calculette devra être présentée sous la forme d'une classe.
  2. Cette calculette utilisera la notation postfixée, aussi appelée Polonaise inverse.
  3. Une telle calculette fonctionne à l'aide d'une pile (au sens informatique du terme, bien entendu ...) qui stocke les parties d'expressions à évaluer et le résultat de ces évaluations (voir l'algorithme). Celle-ci aura un pile supplémentaire permettant d'avoir un historique de toutes les expressions entrées.
  4. Elle devra au minimum être capable de traiter les quatre opérations +, -, × et ÷.
  5. Elle devra pouvoir fonctionner selon deux modes :
    • interactif, où une fois le programme lancé, il attend que l'utilisateur entre des expressions à évaluer et éventuellement certaines commandes (voir ci-dessous) ;
    • non interactif, où elle reçoit une expression unique à évaluer, sur la ligne de commande et affiche le résultat en retour dans la fenêtre du terminal.
  6. En mode interactif, elle devra répondre aux commandes suivantes :
    • q, pour quitter ;
    • =, pour afficher le sommet de la pile ;
    • d, pour afficher le contenu de la pile ;
    • h, pour afficher le contenu de l'historique. À traiter vraiment quand on sera certain que tout le reste fonctionne)
    • p, pour afficher l'expression infixée équivalente (voir le tableau suivant) à ce qui est contenu dans la pile d'historique ;
    • w, pour effacer le contenu de la pile et de l'historique.

Algorithme de traitement des expressions

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

Exemples de notations postfixées et infixées
Notation postfixéeNotation infixée
5.2 2.8  ×5.2 × 2.8
5  3  9  -  ×5 × 3 - 9
25  4  0.25  +  ÷25 ÷ 4 + 0.25
25  4  -  0.25  3.2  ×  ÷25 - 4 ÷ 0.25 × 3.2

L'algorithme de traitement est de fait assez simple :

tantque expression ≠ vide faire
	x ← decoupe(expression)
	si x est une operande alors
		empiler(x)
	sinon {x est un operateur que l'on nomme OP}
		a ← depiler()
		b ← depiler()
		c ← b OP a
		empiler(c)
	finsi
fintantque
Algorithme de traitement d'une expression postfixée.

Réalisation

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.

Classe, méthodes et champs

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.

Exercice

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

Création d'une pile

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

Exercice

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.

Transformation d'une chaîne de caractères en nombre réel

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 :

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.

Exercice

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.

Découpage et gestion des expressions

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.

Exercice

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.

Mode non-interactif

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.

Mode interactif

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.

Exercice

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.

Affichage

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

Exercice

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.

Expressions infixées

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.

- / 3 5 + 4 3
Représentation sous forme d'arbre d'évaluation de l'expression 54+3-3.

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

Exercice

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.

Gestion d'un arbre binaire

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.

Exercice

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

#ifndef _NODE_H_
#define _NODE_H_

template <typename T>
class node {
public:
	// Deux constructeurs pour cette classe
	node (T value) ;
	node (T value, node *left, node *right) ;
	
	// L'objet stocké.
	// Il est public, donc il n'y aura pas besoin de
	// méthode particulière pour y accéder, en lecture
	// et/ou en écriture.
	T value ;

	// Méthode qui retourne une copie du noeud courrant
	// Elle est déclarée 'const' car elle ne modifie pas
	// l'objet qu'elle manipule
	node *copy() const ;

	// Une méthode permettant de détruire un noeud
	void release() ;

	// Méthodes de positionnement et d'accès aux fils.
	// Pour simplifier l'écriture, nous utilisons la
	// surcharge de méthodes : pour chaque fils, nous avons
	// deux méthodes avec le même nom mais qui
	// i ) n'admettent pas les mêmes arguments et
	// ii) ne retournent pas le même type.
	node *left() const ;
	void left(node<T> *l) ;
	node *right() const ;
	void right(node<T> *r) ;

protected:
	// Les pointeurs vers les fils d'un noeud ne
	// sont pas publics, mais protégés.
	// Si une classe hérite de celle-ci, elle pourra
	// manipuler directement ces champs sans passer
	// par les méthodes left(...) et right(...)
	// ci-dessus.
	node<T> *leftptr ;
	node<T> *rightptr ;
} ;

#endif
Déclaration de la classe node.

Éléments de stockage

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 :

Exercice

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

Aller plus loin

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


  1. [↑] vStack.h à télécharger.
  2. [↑] Vous pourrez très certainement trouver le code permettant de faire cela sur Internet, cependant, le but n'est pas de produire du code auquel on ne comprend rien, mais de comprendre le code que l'on produit.