La programmation orientée objet
Classes et instances
L’objectif poursuivi par Bjarne Stroustrup était, rappelons-le, d’implémenter sur un compilateur C les classes décrites par le langage Simula. Ces deux derniers langages étant radicalement opposés dans leur approche, il fallait identifier une double continuité, notamment du côté du C.
Il fut aisé de remarquer que la programmation serait grandement simplifiée si certaines fonctions pouvaient migrer à l’intérieur des structures du C. De ce fait, il n’y aurait plus de structure à passer à ces fonctions puisqu’elles s’appliqueraient évidemment aux champs de la structure.
Toutefois, il fallait conserver un moyen de distinguer deux instances et c’est pour cette raison que l’on a modifié la syntaxe de l’opérateur point :
Programmation fonctionnelle |
Programmation orientée objet |
|
|
|
|
Cette différence d’approche a plusieurs conséquences positives pour la programmation. Pour commencer, le programmeur n’a plus à effectuer un choix parmi les différents modes de passage de la structure à la fonction afficher(). Ensuite, nous allons pouvoir opérer une distinction entre les éléments (champs, fonctions) de premier plan et de second plan. Ceux de premier plan seront visibles, accessibles à l’extérieur de la structure. Les autres seront cachés, inaccessibles.
Ce procédé garantit une grande indépendance dans l’implémentation d’un concept, ce qui induit également une bonne stabilité des développements.
1. Définition de classe
Une classe est donc une structure possédant à la fois des champs et des fonctions. Lorsque les fonctions sont considérées à l’intérieur d’une classe, elles reçoivent le nom de méthodes.
L’ensemble...
Héritage
1. Dérivation de classe (héritage)
Maintenant que nous connaissons bien la structure et le fonctionnement d’une classe, nous allons rendre nos programmes plus génériques. Il est fréquent de décrire un problème général avec des algorithmes appropriés, puis de procéder à de petites modifications lorsqu’un cas similaire vient à être traité.
La philosophie orientée objet consiste à limiter au maximum les macros, les inclusions, les modules. Cette façon d’aborder les choses présente de nombreux risques lorsque la complexité des problèmes vient à croître. La programmation orientée objet s’exprime plutôt sur un axe générique/spécifique bien plus adapté aux petites variations des données d’un problème. Des méthodes de modélisation s’appuyant sur UML peuvent vous guider pour construire des réseaux de classes adaptés aux circonstances d’un projet, d’autant plus que C++ fait partie des langages supportant cette approche.
2. Exemple de dérivation de classe
Imaginons une classe Compte, composée des éléments suivants :
class Compte
{
protected:
int numero; // numéro du compte
double solde; // solde du compte
static int num; // variable utilisée pour calculer le prochain
numéro
static int prochain_numero();
public:
char*titulaire; // titulaire du compte
Compte(char*titulaire);
~Compte(void);
void crediter(double montant);
bool debiter(double montant);
void relever();
};
Nous pouvons maintenant imaginer une classe CompteRemunere, spécialisant le fonctionnement de la classe Compte. Il est aisé de concevoir qu’un compte rémunéré admet globalement les mêmes opérations qu’un compte classique, son comportement étant légèrement modifié pour...
Autres aspects de la POO
1. Conversion dynamique
a. Conversions depuis un autre type
Les constructeurs permettent de convertir des objets à partir d’instances (de valeurs) exprimées dans un autre type.
Prenons le cas de notre classe Chaine. Il serait intéressant de « convertir » un char* ou un char en chaîne :
#include <string.h>
class Chaine
{
private:
char*buffer;
int t_buf;
int longueur;
public:
// un constructeur par défaut
Chaine()
{
t_buf=100;
buffer=new char[t_buf];
longueur=0;
}
Chaine (int t_buf)
{
this->t_buf=t_buf;
buffer=new char[t_buf];
longueur=0;
}
Chaine(char c)
{
t_buf=1;
longueur=1;
buffer=new char[t_buf];
buffer[0]=c;
}
Chaine(char*s)
{
t_buf=strlen(s)+1;
buffer=new char[t_buf];
longueur=strlen(s);
strcpy(buffer,s);
}
void afficher()
{
for(int i=0; i<longueur; i++)
printf("%c",buffer[i]);
printf("\n");
}
} ;
int main(int argc, char* argv[])
{
// Ecriture 1
// Conversion par emploi explicite de constructeur
Chaine x;
x=Chaine("bonjour"); // conversion ...
Les exceptions plus sûres que les erreurs
L’énorme avantage des exceptions vient du fait qu’elles sont intrinsèques au langage, et même à l’environnement d’exécution (runtime voire framework). Elles sont donc beaucoup plus sûres pour contrôler l’exécution d’un programme multithread (ce qui est le cas de nombreuses librairies). De plus, les exceptions guident le programmeur et lui facilitent la tâche de structuration du code. En d’autres termes, il est difficile de s’en passer lorsque l’on maîtrise leur utilisation.
Certains langages vont même plus loin et exigent leur prise en compte à la rédaction du programme. C’est notamment le cas de Java.
Le langage C++ propose des exceptions structurées, bien entendu à base de classes et d’une gestion avancée de la pile. L’idée est de mettre une séquence d’instructions - pouvant contenir des appels à des fonctions - sous surveillance. Lorsqu’un problème survient, le programmeur peut intercepter l’exception décrivant le problème au fil de sa propagation dans la pile.
Le principe des exceptions C++ est de séparer la détection d’un problème de son traitement. En effet, une fonction de calcul n’a sans doute pas les moyens de décider de la stratégie à adopter en cas de défaillance. Les différentes alternatives sont de continuer avec un résultat faux, d’intégrer de nouvelles valeurs saisies par l’utilisateur, de suspendre le calcul...
Lorsqu’une fonction déclenche une exception, celle-ci est propagée à travers la pile des appels jusqu’à ce qu’elle soit interceptée.
Cette organisation réduit à néant les contraintes imposées par la bibliothèque du langage C ; il n’y a plus d’accès concurrentiel à une variable globale chargée de décrire l’état d’erreur, puisque le contexte de l’erreur est défini par une instance à part entière. Le déclenchement d’une exception provoque un retour immédiat de la procédure ou de la fonction et débute la recherche d’un bloc de traitement....
Programmation générique
Le langage C est trop proche de la machine pour laisser entrevoir une réelle généricité. Au mieux, l’emploi de pointeurs void* (par ailleurs introduits par C++) permet le travail sur différents types. Mais cette imprécision se paye cher, tant du point de vue de la lisibilité des programmes que de la robustesse ou des performances.
Les macros ne sont pas non plus une solution viable pour l’implémentation d’un algorithme indépendamment du typage. Les macros développent une philosophie inverse aux techniques de la compilation. Elles peuvent convenir dans certains cas simples mais leur utilisation relève le plus souvent du bricolage.
Reste que tous les algorithmes ne sont pas destinés à être implémentés pour l’universalité des types de données C++. Pour un certain nombre de types, on se met alors à penser en termes de fonctions polymorphes.
Finalement, les modèles C++ constituent une solution bien plus élégante, très simple et en même temps très sûre d’emploi. Le type de donnée est choisi par le programmeur qui va instancier son modèle à la demande. Le langage C++ propose des modèles de fonctions et des modèles de classes et nous allons traiter tour à tour ces deux aspects.
1. Modèles de fonctions
Tous les algorithmes ne se prêtent pas à la construction d’un modèle. Identifier un algorithme est une précaution simple à prendre car un modèle bâti à mauvais escient peut diminuer les qualités d’une implémentation.
Pour l’algorithmie standard, la bibliothèque STL a choisi d’être implémentée sous forme de modèles. Les conteneurs sont des structures dont le fonctionnement est bien entendu indépendant du type d’objet à mémoriser.
Pour les chaînes, le choix est plus discutable. La librairie STL propose donc un modèle de classes pour des chaînes indépendantes du codage de caractère, ce qui autorise une ouverture vers des formats très variés. Pour le codage le plus courant, l’ASCII, la classe string est une implémentation spécifique.
Le domaine numérique...
Travaux pratiques
1. Utilisation de l’héritage de classes dans l’interprète tiny-lisp
L’outil tiny-lisp est un interprète LISP écrit en C++ dont le code source est disponible avec cet ouvrage.
Dans l’interprète tiny-lisp, des classes dérivant de ScriptBox sont chargées de faire fonctionner l’analyseur dans autant d’environnements :
-
La boîte à sable ou Sandbox est un environnement isolé où les entrées-sorties sont neutralisées (elles ne sont pas activées).
-
L’environnement ConsoleBox assure des entrées-sorties sur la console système.
La classe de base est appelée ScriptBox et elle définit une méthode virtuelle init_events() :
/*
Environnement d'exécution générique
*/
class ScriptBox
{
public:
LexScan*lexScan;
Evaluator*parser;
ScriptBox();
~ScriptBox();
virtual void init_events();
void new_text();
void set_text(string text);
void parse();
bool has_errors();
string get_errors();
string get_text();
bool set_debug();
};
Dans la version neutre, Sandbox, cette méthode n’est pas surchargée.
/*
Environnement d'exécution neutre
*/
class Sandbox :
public ScriptBox
{
public:
Sandbox();
~Sandbox();
};
Dans le cas de l’environnement Consolebox, la méthode est, au contraire, surchargée pour déclarer des événements de type entrées-sorties sur la console (écran, clavier).
/*
Environnement...