Cours C 11 Portabilite maintenabilite et reutilisabilite
…
Sébastien Paumier
– du compilateur
– du système
– de la machine
– force à coder proprement en prenant du recul
– facilite la diffusion des applications
Un code qui compile partout
– éviter les trucs exotiques comme #pragma
– respecter les normes (-ansi -Wall)
– cerner le code qui dépend du compilateur, du système ou de la machine
– éviter les dépendances vers des bibliothèques non portables
– faire des Makefile portables
Cerner le code variable
/**
* System-dependent function that compares
* file names.
*/
int fcompare(const char* a,const char* b) {
#ifdef WINDOWS_LIKE
/* case doesn't matter under windows */
return strcmp_ignore_case(a,b);
#else
return strcmp(a,b);
#endif
}
Cerner le code variable
types, les constantes, etc.
#ifdef WINDOWS_LIKE #include "mygetopt.h" #else
#include #endif
Noms de fichiers
motor.c motor.h
compile sous Windows, pas sous Linux
Dépendances au système
– valeurs/types (séparateur de fichier / ou \)
– comportements (casse des noms de fichiers)
Dépendances au système
– mauvaise solution: constante arbitraire
– bonne solution: utiliser la constante
FILENAME_MAX (stdio.h) adaptée au système courant
Tailles des types
sizeof(void*) vaut 8
Tailles des types
stdint.h
sur 2 octets
#include typedef uint16_t unichar;
Les sauts de lignes
– comment lire/écrire un fichier texte multi
plateforme ?
Endianness
– comment lire/écrire un fichier binaire contenant des int d'une façon multi plateforme ?
– exemple de solution: encodage utf8
* (toute ressemblance avec un transparent existant serait forfuite et involontaire)
– compilateur à utiliser
– options de compilation
– répertoires (include, lib, etc)
– commandes d'installation et de nettoyage
– noms des sorties (exemple: .exe ou pas?)
– etc.
Définitions dépendant du système
Maintenabilité & réutilisabilité
Ou introduction au génie logiciel
Référence: L'art de la programmation UNIX, Eric S.
Raymond, Vuibert
"Pour être un bon développeur en biniou,
il suffit d'être bon en algo et de maîtriser la syntaxe de biniou "
Proverbe étudiant
"Quand on est con, on est con"
Georges Brassens
Qu'est-ce que le GL ?
– mieux=moins de bugs, code réutilisable, évolutif, etc.
– plus vite=accélérer le débogage, éviter de tout casser tout le temps, etc.
*(comment devenir des feignants efficaces) écrire des éléments simples et les relier par des interfaces propres
– risque plus élevé de bugs
– moins facile à entretenir
concevoir des programmes à connecter à d'autres programmes
– utiliser les choses faites par ceux qui savent
(vous gérez les .pnm ? utilisez anytopnm pour les autres formats)
– utiliser les dépendances
séparer méthode et mécanismes;
séparer moteur et interfaces
– tester, tester, tester
– fichiers de configuration
Règle de robustesse
cas particulier 4 cas particulier 7 cas particulier 8 cas général cas particulier 3
State* s;
int n_states;
Transition* t;
} Automaton;
void minimize(Automaton* a); éviter les surprises dans la conception des interfaces
– des programmeurs (pourquoi changer si for(i=0;i<n;i++) convient ?)
– des utilisateurs (ne pas appeler .txt un fichier binaire) n'afficher des informations que si nécessaires
– dans l'immense majorité des cas, strlen Suffit éviter le travail manuel; écrire plutôt des programmes de génération (de programmes, de données, etc.)
– utiliser des outils de profiling se méfier de la bonne solution unique
void foo(Automaton* a) {
if (is_final(s)) {
/* ... */
}
/* ... */
}
concevoir des systèmes simples; n'introduire de la complexité que si nécessaire
– pas de hashtable générique si l'on ne manipule que des entiers
– pas de param. inutiles, au cas où, plus tard...
Règle d'extensibilité
concevoir en pensant à l'avenir, qui sera là plus tôt qu'on ne l'imagine
Règle d'extensibilité
encodings.h :
/* This library is designed to * manage various implementations * of fputc for various encodings. * Defaults = "ASCII" "UTF8" */
#ifndef encodings_H
#define encodings_H
#include
typedef int (*encoder) (int,FILE*); interface très simple
void add_encoder(char* name,encoder f); encoder get_encoder(char* name); #endif
Règle d'extensibilité
début de encodings.c : initialisation automatique au chargement du code, pour mettre des encodages par défaut implémentation cachée
Règle d'extensibilité
fin de encodings.c : est-ce la peine de gérer les doublons ? pas la peine d'optimiser en utilisant des constantes au lieu de chaînes
Et tout le reste...
… … … …
...
A.2.3.b. (2) portabilité et bibliothèques de fonctions • Les limites de la portabilité
A.2.3.c. (3) discipline de programmation
Nous voici arrivés à un point crucial: C est un langage près de la machine, donc dangereux et bien que C soit un langage de programmation structuré, il ne nous force pas à adopter un certain style de programmation (comme p.ex. Pascal). Dans un certain sens, tout est permis et la tentation de programmer du 'code spaghetti' est grande. (Même la commande 'goto', si redoutée par les puristes ne manque pas en C). Le programmeur a donc beaucoup de libertés, mais aussi des responsabilités: il doit veiller lui-même à adopter un style de programmation propre, solide et compréhensible.
Au fil de l'introduction du langage C, ce manuel contiendra quelques recommandations au sujet de l'utilisation des différents moyens de programmation. Il est impossible de donner des règles universelles à ce sujet.
A.3. Domaines d’utilisation :.
-les variables ;
La programmation en langage C implique la maîtrise des notions suivantes :
-les opérateurs ;
-les structures de contrôle; -les fonctions ; -les pointeurs.
B.1. La filière C
Schéma: Bibliothèques de fonctions et compilation
B.2.2. Exemple : Hello C !
Suivons la tradition et commençons la découverte de C par l'inévitable programme 'hello world'. Ce programme ne fait rien d'autre qu'imprimer les mots suivants sur l'écran:
Comparons d'abord la définition du programme en C avec celle en langage algorithmique. HELLO_WORLD en langage algorithmique programme HELLO_ WORLD
| (* Notre premier programme en C *)
| écrire "hello, world"
fprogramme
HELLO_WORLD en C
#include
main()
/* Notre premier programme en C */ {
printf("hello, world\n");
return 0;
}
// Dans la suite du chapitre, nous allons discuter les détails de cette implémentation.
...
B.3.1. Caractéristiques du C
Langage structuré, conçu pour traiter les tâches d'un programme en les mettant dans des blocs.
Il produit des programmes efficaces : il possède les mêmes possibilités de contrôle de la machine que l'assembleur et il génère un code compact et rapide.
Déclaratif : normalement, tout objet C doit être déclaré avant d'être utilisé. S'il ne l'est pas, il est considéré comme étant du type entier.
Format libre : la mise en page des divers composants d'un programme est totalement libre.
Cette possibilité doit être exploitée pour rendre les programmes lisibles.
Modulaire : une application pourra être découpée en modules qui pourront être compilés séparément.
Un ensemble de programmes déjà opérationnels pourra être réuni dans une librairie. Cette aptitude permet au langage C de se développer de lui même.
Souple et permissivité : peu de vérifications et d'interdits, hormis la syntaxe. Il est important de remarquer que la tentation est grande d'utiliser cette caractéristique pour écrire le plus souvent des atrocités.
Transportable : les entrées/sorties sont réunies dans une librairie externe au langage.
B.3.2. Structure d'un programme C Un programme C est composé de :
o inclusion de fichiers,
o substitutions,
o macros,
o compilation conditionnelle.
Une directive du préprocesseur est une ligne de programme source commençant par le caractère dièse (#). Le préprocesseur (/lib/cpp) est appelé automatiquement, en tout premier, par la commande /bin/cc.
o Déclaration : la déclaration d'un objet C donne simplement ses caractéristiques au compilateur et ne génère aucun code.
o Définition : la définition d'un objet C déclare cet objet et crée effectivement cet objet.
Pour ignorer une partie de programme il est préférable d'utiliser une directive du préprocesseur (#if 0 ... #endif)
B.3.3. Connaissances de base
regardons ce petit programme :
#include
#define TVA 18.6
void main(void)
{
float HT,TTC;
puts ("veuillez entrer le prix H.T.");
scanf("%f",&HT);
TTC=HT*(1+(TVA/100));
printf("prix T.T.C. %f\n",TTC);
}
On trouve dans ce programme :
* des directives du pré processeur (commençant par #) ( pas de ; )
#include : inclure le fichier définissant (on préfère dire déclarant) les fonctions standard d'entrées/sorties (en anglais STanDard In/Out), qui feront le lien entre le programme et la console (clavier/écran). Dans cet exemple il s'agit de puts, scanf et printf.
#define : définit une constante. A chaque fois que le compilateur rencontrera, dans sa traduction de la suite du fichier en langage machine, le mot TVA, ces trois lettres seront remplacées par 18.6. Ces transformation sont faites dans une première passe (appelée pré compilation), où l'on ne fait que du "traitement de texte", c'est à dire des remplacements d'un texte par un autre sans chercher à en comprendre la signification.
* une entête de fonction. Dans ce cas on ne possède qu'une seule fonction, la fonction principale (main function). Cette ligne est obligatoire en C, elle définit le "point d'entrée" du programme, c'est à dire l'endroit où débutera l'exécution du programme. Les fonctions sont écrites sous la forme : entête { corps } L'entête est de la forme : type_résultat nom (arguments) . Le type_résultat n'était obligatoire (avant la
norme ANSI) que s'il était différent de int (entier). Il doit désormais être void (rien) si la fonction ne renvoie rien (dans un autre langage on l'aurait alors appelé sous-programme, procédure, ou sous-routine). Les arguments, s'ils existent, sont passés par valeur. Si la fonction ne nécessite aucun argument, il faut
Le corps est composé de déclarations de variables locales, et d'instructions, toutes terminées par un ;
* un "bloc d'instructions", délimité par des accolades { }, et comportant :
* des déclarations de variables, sous la forme : type listevariables;
Une variable est un case mémoire de l'ordinateur, que l'on se réserve pour notre programme. On définit le nom que l'on choisit pour chaque variable, ainsi que son type, ici float, c'est à dire réel (type dit à virgule flottante, d'où ce nom). Les trois types scalaires de base du C sont l'entier (int), le réel (float) et le caractère (char). On ne peut jamais utiliser de variable sans l'avoir déclarée auparavant. Une faute de frappe devrait donc être facilement détectée, à condition d'avoir choisi des noms de variables suffisamment différents (et de plus d'une lettre).
* des instructions, toutes terminées par un ; (point virgule). Une instruction est un ordre élémentaire que l'on donne à la machine, qui manipulera les données (variables) du programme, ici soit par appel de fonctions (puts, scanf, printf) soit par affectation (=).
L'instruction nulle est composée d'un ; seul.
Il est recommandé, afin de faciliter la lecture et le "deboguage" de ne mettre qu'une seule instruction par ligne dans le source du programme.
Un bloc est une suite d'instructions élémentaires délimitées par des accolades { et }
Un bloc peut contenir un ou plusieurs blocs inclus
Un bloc peut commencer par des déclarations/définitions d'objets qui seront locaux à ce bloc. Ces objets ne pourront être utilisés que dans ce bloc et les éventuels blocs inclus à ce bloc.
Détaillons les 4 instructions de notre programme :
puts affiche à l'écran le texte qu'on lui donne (entre parenthèses, comme tout ce que l'on donne à une fonction, et entre guillemets, comme toute constante texte en C).
printf affichera enfin le résultat stocké dans TTC.
B.3.4. Fonctions d'entrées/sorties les plus utilisées
Le langage C se veut totalement indépendant du matériel sur lequel il est implanté. Les entrées sorties, bien que nécessaires à tout programme (il faut lui donner les données de départ et connaître les résultats), ne font donc pas partie intégrante du langage. On a simplement prévu des bibliothèques de fonctions de base, qui sont néanmoins standardisées, c'est à dire que sur chaque compilateur on dispose de ces bibliothèques, contenant les même fonctions (du moins du même nom et faisant apparemment la même chose, mais programmées différemment en fonction du matériel). Ces bibliothèques ont été définies par la norme ANSI, on peut donc les utiliser tout en restant portable.
Nous détaillerons ici deux bibliothèques : CONIO.H et STDIO.H. B.3.4.a. Dans CONIO.H
on trouve les fonctions de base de gestion de la console :
putch(char) : affiche sur l'écran (ou du moins stdout) le caractère fourni en argument (entre parenthèses). stdout est l'écran, ou un fichier si on a redirigé l'écran (en rajoutant >nomfichier derrière l'appel du programme, sous DOS ou UNIX). Si besoin est, cette fonction rend le caractère affiché ou EOF en cas d'erreur.
getch(void) : attend le prochain appui sur le clavier, et rend le caractère qui a été saisi. L'appui sur une touche se fait sans écho, c'est à dire que rien n'est affiché à l'écran. En cas de redirection du clavier, on prend le prochain caractère dans le fichier d'entrée.
getche(void) : idem getch mais avec écho
B.3.4.b. Dans STDIO.H,
On possède encore d'autres fonctions dans STDIO, en particulier pour gérer les fichiers.
B.3.5. les fonctions B.3.5.a. Généralités
Comme en mathématique, elles peuvent accepter un ou plusieurs paramètres d'entrée et renvoyer un résultat ou aucun.
Elles peuvent renvoyer plusieurs résultats mais de façon plus compliquée qui ne sera pas abordée ici.
-les fonctions standards : fournies avec le compilateur C, correspondant aux fonctions les plus couramment utilisées par le programmeur : saisie, affichage, ...
-les fonctions utilisateurs : écrites par l'utilisateur pour répondre à un besoin spécifique.
Remarque : il est souvent judicieux de regrouper les fonctions utilisateurs dans une bibliothèque utilisateur ; ce qui permettra une réutilisation ultérieure des fonctions développées pour une application spécifique.
B.3.5.b. Définition d'une fonction utilisateur en C
La mise en œuvre des fonctions utilisateurs nécessite 3 étapes : - la déclaration ;
- l'appel ;
- la définition.
#include #include
/* Déclaration des fonctions du programme */ void efface();
void affiche(int x);
int calcule(int x,inty);
void main(void) {
int a,b,s;
efface(); /*Appel à la fonction */