Date de première publication web : 2014/11/27
L'objectif de ce TP est de faire une petite calculatrice en mode texte. On met en œuvre les pointeurs de fonctions et un affichage graphique peut être proposé.
Le programme utilise les fonctions mathématiques, il faut donc faire l'édition des liens avec la bibliothèque mathématique.
Un squelette d'application C est disponible ici :
git clone https://gitlab.com/kiux/skel.git
Mise en bouche
Pincettes...
- Écrire une fonction
identification()
qui reçoit en entrée une chaîne de caractères correspondant à une expression mathématique simple et teste si elle appartient au tableau de chaînes de caractèresOPER_NAMES
suivant :
const char * OPER_NAMES[] = { "x", "sin(x)", "cos(x)", "log(x)", "exp(x)", NULL };
La présence de la valeur de fin NULL
permet de ne pas avoir à connaître le nombre d'éléments du tableau.
Si la chaîne lue est trouvée, on retourne :
- son indice dans le tableau et –1 sinon,
- ou mieux encore, la valeur issue de l'énumération suivante :
typedef enum ope {
NONE = -1, ID , SIN, COS, LOG, EXP
} OP;
Vous aurez traduit que ID
est pour la fonction identite et que NONE
est une valeur qui ne correspond à aucune des fonctions précédentes.
TEST(identification_avec_indice) {
CHECK( 0 == identification("x"));
CHECK( -1 == identification("x\n"));
CHECK( 1 == identification("sin(x)"));
CHECK( 2 == identification("cos(x)"));
CHECK( 3 == identification("log(x)"));
CHECK( 4 == identification("exp(x)"));
CHECK( -1 == identification("t"));
}
TEST(identification_avec_enum) {
CHECK( ID == identification("x"));
CHECK( NONE == identification("x\n"));
CHECK( SIN == identification("sin(x)"));
CHECK( COS == identification("cos(x)"));
CHECK( LOG == identification("log(x)"));
CHECK( EXP == identification("exp(x)"));
CHECK( NONE == identification("t"));
}
- Écrire la fonction
evalf()
qui accepte en entrée une valeur réelle double précision et un indice ou une énumération correspondant à une expression mathématique identifiée par la fonction précédente. Ainsievalf(0.0, SIN) "==" 0.0
. Si vous le voulez, vous pouvez renvoyer le nombreNAN
- Not A Number - lorsque l'identification a mené à quelque chose d'inconnu.
TEST(control_eval_f) {
CHECK( EQ( 0.0, evalf( 0.0, ID)));
CHECK( EQ( 0.0, evalf( 0.0, SIN)));
CHECK( EQ( 1.0, evalf( 0.0, COS)));
CHECK( EQ( 1.0, evalf( 0.0, EXP)));
CHECK( EQ( M_PI , evalf( M_PI, ID)));
CHECK( EQ( 0.0 , evalf( M_PI, SIN)));
CHECK( EQ( -1.0 , evalf( M_PI, COS)));
CHECK( EQ( exp(M_PI) , evalf( M_PI, EXP)));
CHECK( EQ( 0.0 , evalf( M_PI/2.0, COS)));
}
- Écrire une fonction
calcul()
qui reçoit en entrée un intervalle[a, b]
, un pas de progressiondelta
dans cet intervalle, un identificateur d'expression (indice ou énumération) et un fichier dans lequel écrire. Cette fonction évalue l'expression "identifiée" pour toutes les valeurs entre a et b par pas de delta et écrit dans le fichier précisé (par exemple, la sortie standard) chaque résultat d'évaluation (séparer les résultats par des espaces).
- Écrire un programme principal qui demande à l'utilisateur la fonction à afficher, l'intervalle d'affichage et la progression et qui affiche tout cela.
Nous rappelons que la fonction privilégiée pour faire une saisie texte est fgets()
et que pour convertir des chaînes de caractères en nombre, vous pouvez utiliser sscanf()
ou les fonctions suivantes :
double atof(char *chaine);
qui convertit chaine en undouble
int atoi(char *chaine);
qui convertit chaine en unint
long atol(char *chaine);
qui convertit chaine en unlong int
Pincettes...
On va essayer de simplifier l'écriture de la fonction d'évaluation evalf()
en utilisant un tableau de pointeurs de fonction comme celui-ci :
double (*OPER_FN [])(double) = { identite, sin, cos, log, exp, erreur };
identite()
et erreur()
sont deux fonctions à écrire avec le même prototype que les autres fonctions mathématiques. identite(t)
renvoie la valeur t
. erreur(t)
affiche un message d'erreur sur la sortie d'erreur par exemple.
Écrire la fonction evalp()
avec le même prototype que evalf()
. Si le paramètre correspondant à la fonction est mauvais, la fonction erreur
est appelée !
On peut aussi d'affranchir de l'énumération OP
si la fonction d'identification renvoie directement le pointeur de l'opération à effectuer !
Deuxième partie
La chaîne lue comporte maintenant deux expressions du type défini dans la première partie
séparées par un des opérateurs suivants :
+, -, *, /
. On va par exemple lire la chaine suivante : "x + sin(x)"
Version simple ?
Vous utiliserez la fonction fgets()
pour lire la chaîne et la fonction sscanf()
de la manière suivante pour séparer les expressions et l'opérateur :
sscanf (chaine,"%s %c %s", exp1, &op, exp2);
Réaliser un programme qui évalue l'expression lue dans l'intervalle [a,b]
avec un pas de delta
.
Identifiez de vous même les fonctions élémentaires dont vous avez besoin pour limiter le travail,
et celles qu'il faudra modifier pour réaliser ce programme.
Limitez au maximum des erreurs (segmentation fault, erreurs de lecture, …).
Passage aux pointeurs de fonctions
C'est un peu plus sioux que pour la première partie. Je vous propose de regarder les regarder les codes ASCII des opérateurs et de les ordonner. Le tableau de pointeurs de fonction est défini avec les fonctions que l'on prendra le soin de d'écrire et éventuellement des pointeurs NULL.
double (*tab2[]) (double, double) = { mul, add, NULL, sou, NULL, dyv };
div()
est une fonction standard. L'ordre des opérateurs n'est pas contractuel
Il suffit alors de faire un tab2[c-'*'](x,y)
pour avoir le résultat.
Plat de résistance
Si vous vous êtes pris au jeu, on peut aller plus loin et faire les choses suivantes :
- Lire une phrase avec la notation polonaise inverse et l'évaluer
- Faire un affichage graphique dans la bibliothèque de votre choix
La notation polonaise inverse est une manière d'évaluer une expression mathématique sans utiliser de parenthèse. L'intérêt est que l'évaluation est très simple et se fait de gauche à droite en empilant et dépilant les éléments
Il est également aisé de convertir une expression parenthésée en notation polonaise inverse.
Calculer sin(x) + 3
revient à évaluer x sin 3 +
Structure de stockage pour l'évaluation
Je vais limiter arficiellement le nombre d'opérations (symboles) à effectuer à une taille donnée (par exemple 256). Un symbole peut être :
- une valeur réelle
- une variable réelle
- une fonction à un argument :
cos
,sin
,exp
,log
. On pourra ajouter aux fonctions usuelles les fonctionsneg
(-val) oudup
pour dupliquer un élément de la pile - une fonction à deux arguments : +, -, /, * auxquelles on pourra ajouter une fonction
swap
d'échange d'arguments. - un éventuel symbole de fin (vous pouvez aussi utiliser une variable)
Je vous propose cet éventail de possibilités avec une structure contenant une énumération et une union :-)
Voici la liste des symboles possibles :
enum e{
VAL, VAR, F1, F2, FIN
};
qui correspondent à l'union suivante :
union u {
double val;
double * var;
double (*f1)(double);
double (*f2)(double, double);
};
Il faudra donc traduire la phrase de l'utilisateur par une suite de symboles
struct s {
enum e type;
union u data;
};
struct s donnees[256];
Structure de pile
Pour la structure de pile, il suffit de prendre un tableau de dimension maximale fixée et de connaître le nombre d'éléments de la pile.
struct pile {
struct s tab[CAPACITE];
int taille;
};
Code
Vous avez pu noter que les variables sont connues par leur adresse (soient éléments d'un tableau de variables, soit nommées directement :
double x;
struct s symb;
symb.type = VAR;
symb.data.var = &x;
Pour la reconnaissance des variables, on peut se limiter à une liste finie (x
et t
dans un premier temps par exemple).
Voici ce que je vous conseille de faire :
- Initialiser un tableau de symboles qui représente une fonction, par exemple :
x 3 + FIN
- Ecrire une fonction d'évaluation
evaluer()
de l'expression dans le tableau et qui renvoie une valeur réelle. Si vous changez la valeur d'une variable et que réévaluez, la valeur sera différente :-). Cette fonction utilisera une pile pour l'évaluation.
Vous pouvez supposer que la chaine de symboles est correcte (c'est-à-dire que les erreurs éventuelles seraient détectées en amont, à la création par exemple
- Il faut ensuite écrire le passage de la chaine de caractères saisie par l'utilisateur en un tableau de symboles.
Pour convertir une chaine en nombre réél, vous pouvez également utiliser la fonction strtod()
. On peut savoir si la conversion a échoué.
Si tout cela ne vous donne pas envie de faire une calculatrice graphique !
Have Fun !