mardi 23 octobre 2012

La notion de Classe (C++)


Nous allons enfin parler, dans ce chapitre, de Programmation Orientée Objet. Nous allons commencer par comprendre le mécanisme des classes.

Ecriture d’une première classe

Une classe est en quelque sorte une structure complexe qui permet l’encapsulation de données.
Une classe est composée de données et de méthodes. Lorsque l’encapsulation des données est parfaite, seules certaines méthodes sont accessibles, devenant ainsi l’interface. Ceci évite en principe à l’utilisateur de la classe, de se soucier de son fonctionnement et de faire des erreurs en changeant directement la valeur de certaines données.
Prenons un exemple concret et simple : l’écriture d’une classe Point. Cet exemple va nous suivre tout au long de ce chapitre. En C, nous aurions fait une structure comme suit :
struct Point
{
     int x;     // Abscisse du point 
     int y;     // Ordonnée 
};



La déclaration précédente fonctionne parfaitement en C++. Mais nous aimerions rajouter des fonctions qui sont fortement liées à ces données, comme l’affichage d’un point, son déplacement, etc. Voici une solution en C++ :
class Point
{
public :
     int x;
     int y;
     void Init(intint); // Initialisation d’un point 
     void Deplace(intint); // Déplacement du point 
     void Affiche();          // Affichage du point 
};
Vous remarquerez tout de suite plusieurs éléments :
  • class : le "struct" a été remplacé, même si dans cet exemple précis, il aurait pu être conservé. Mais nous ne rentrerons pas dans les détails.
  • public : le terme public signifie que tous les membres qui suivent (données comme méthodes) sont accessibles depuis l’extérieur de la classe. Nous verrons les différentes possibilités plus tard.
  • L’ajout des fonctions (ou plutôt méthodes puisqu’elles font partie de la classe) "Init", "Deplace" et "Affiche". Elles permettent respectivement d’initialiser un point, de le déplacer (addition de coordonnées) et de l’afficher (contenu des variables x et y).

Utilisation de la classe

Voici maintenant un programme complet pour mettre en application tout ceci :
#include <iostream.h>
class Point
{
public :
     int x;
     int y;
     void Init(int a, int b){ x = a; y = b; }
     void Deplace(int a, int b){ x += a; y += b; }
     void Affiche(){ cout << x << ", " << y << endl; }
};
void main()
{
     Point p;
     p.Init(3,4);
     p.Affiche();
     p.Deplace(4,6);
     p.Affiche();
}
Les méthodes de la classe Point sont implémentées dans la classe même. Ceci fonctionne très bien, mais devient bien entendu assez lourd lorsque le code est plus long. C’est pourquoi il vaut mieux placer la déclaration seulement, au sein de la classe. Notre code devient alors :
#include <iostream.h>
class Point
{
public :
     int x;
     int y;
     void Init(int a, int b);
     void Deplace(int a, int b);
     void Affiche();
};
void Point::Init(int a, int b)
{
     x = a;
     y = b;
}
void Point::Deplace(int a, int b)
{
     x += a;
     y += b;
}
void Point::Affiche()
{
     cout << x << ", " << y << endl;
}
void main()
{
     Point p;
     p.Init(3,4);
     p.Affiche();
     p.Deplace(4,6);
     p.Affiche();
}
Vous avez remarqué la présence du "Point::" qui signifie que la fonction est en fait une méthode de la classe Point. Le reste est complètement identique. La seule différence entre ces deux programmes vient du fait qu’on dit que les méthodes du premier programme (dont l’implémentation est faite dans la classe), sont implicitement "inline". Ceci signifie que chaque appel à la méthode sera remplacé dans l’exécutable, par le code de la méthode en elle-même (un peu comme une macro en C). D’où un gain de temps certain, mais une augmentation de la taille du fichier en sortie[3].
Mais la différence entre une structure et une classe n’a pas encore été vraiment détaillée. En effet, vous pourriez très bien compiler le même source en retirant le terme "public" et en remplaçant "class" par "struct".
En fait, l’intérêt réel du C++ tourne autour de cette notion importante d’encapsulation de données. Dans l’exemple de la classe Point, nous n’avons pour l’instant spécifié aucune protection de données ; vous pouvez rajouter ces quelques lignes, sans erreur de compilation :
     ...
     p.x = 25; // Accès aux variables de la classe point 
     p.y = p.x + 10;
     cout << "le point est en " << p.x << ", " << p.y << endl;
     ...
L’encapsulation a pour objet d’empêcher cela, afin de notamment limiter la nécessité de compréhension d’un objet par l’utilisateur. La classe devient alors une espèce de "boîte noire" avec des interfaces d’entrée et de sortie. D’où la déclaration suivante :
class Point
{
private :
     int x;
     int y;
public :
     void Init(int a, int b);
     void Deplace(int a, int b);
     void Affiche();
};
Il est alors interdit d’accéder aux variables x et y qui sont des membres "privés", en dehors de la classe Point (elles restent accessibles dans les méthodes de Point !).
Il est également possible d’effectuer une affection de classe, comme pour une structure C. Ceci a le même effet puisque l’affectation a lieu sur les données membre :
#include <iostream.h>
... // déclaration de la classe point 
void main()
{
     Point p;
     p.Init(3,4);
     p.Affiche();
     Point p2;
     p2 = p;
     p2.Affiche();
}

Constructeur et Destructeur

Au cours de l’élaboration de cet exemple aussi simple que concret, vous vous êtes peut-être dit qu’il serait intéressant d’initialiser l’objet au moment de sa déclaration. En effet, il faut de toute façon généralement utiliser tout de suite après la méthode "Init", alors pourquoi ne pas faire d’une pierre deux coups ! Ceci est bien entendu possible en C++ : il s’agit des constructeurs.
Voici comment nous pouvons mettre en oeuvre un constructeur :
class Point
{
     // Ici le "private" est optionnel dans la mesure où tout 
     // ce qui suit la première accolade est privé par défaut 
     int x;
     int y;
public :
     Point(intint); // Constructeur de la classe point 
     void Init(int a, int b);
     void Deplace(int a, int b);
     void Affiche();
};
Vous vous demandez sûrement comment déclarer désormais, une variable de type Point ! Vous pensez peut-être pouvoir faire ceci :
Point p; 
En fait, non. A partir du moment où un constructeur est défini, il doit pouvoir être appelé par défaut pour la création de n’importe quel objet. Dans notre cas il faut par conséquent préciser les paramètres, par exemple :
Point p(4,5); 
Pour laisser plus de liberté, et permettre une déclaration sans initialisation, il faut prévoir un constructeur par défaut :
class Point
{
     int x;
     int y;
public :
     Point();  // Constructeur par défaut 
     Point(intint);
     void Init(int a, int b);
     void Deplace(int a, int b);
     void Affiche();
};
Tout comme il existe un constructeur, on peut spécifier un destructeur. Ce dernier est appelé lors de la destruction de l’objet, explicite ou non.
class Point
{
     int x;
     int y;
public :
     Point();
     Point(intint);
     ~Point(); // Destructeur de la classe Point 
     void Init(int a, int b);
     void Deplace(int a, int b);
     void Affiche();
};
Dans notre cas de classe Point, le destructeur a peu d’utilité. On pourrait à la rigueur placer une instruction permettant de tracer la destruction. En revanche, lorsqu’une classe possède par exemple des pointeurs comme données membre, il est possible de désallouer la mémoire à cet endroit.
Un exemple :
#include <iostream.h>
class Test
{
     int nSize;
public :
     // pas d’encapsulation pour ce membre. 
     // Ca n’est pas très bon, mais c’est juste pour l’exemple. 
     int *pArray;
     Test(int n);
     ~Test();
     int GetSize(){ return nSize; }
};
Test::Test(int n)
{
     cout << "-- Constructeur --" << endl;
     nSize = n;
     pArray = new int[nSize];
}
Test::~Test()
{
     cout << "-- Destructeur --" << endl;
     if( pArray )
       delete []pArray;
}
void main()
{
     Test t(5);
     forint i=0; i<t.GetSize(); i++ )
          t.pArray[i]=i;
     Test *t2; // Pointeur d’objet 
     t2 = new Test(10);  // Allocation dynamique 
     for( i=0; i<t2->GetSize(); i++ )
          t2->pArray[i]=i;
     delete t2; // Destruction explicite 
}

Les Fonctions membre

Surdéfinition

Nous avons vu dans le chapitre précédent qu’il était possible de définir plusieurs constructeurs différents. Nous pouvons étendre cette possibilité de surdéfinition à d’autres méthodes que le constructeur (sauf le destructeur !) :
class Point
{
     int x;
     int y;
public :
     Point();
     Point(intint);
     ~Point();
     void Init(int a, int b);
     void Init(int a); // Initialisation avec une même valeur 
     void Deplace(int a, int b);
     void Deplace(int a);
     void Affiche();
     void Affiche(char* strMesg); // Affichage avec un message 
};

Arguments par défaut

Tout comme une fonction C++ classique, il est possible de définir des arguments par défaut. Ceux-ci permettent à l’utilisateur de ne pas renseigner certains paramètres. Par exemple, imaginons que l’initialisation par défaut d’un point soit (0,0). Nous pouvons donc changer la méthode Init, de sorte qu’elle devienne :
void Init(int a=0); 
Désormais, quand l’utilisateur appelle cette méthode, il a la possibilité de ne pas donner de paramètre, signifiant qu’il veut initialiser son point à 0. De même :
void Affiche(char* strMesg=""); 
Permet de remplacer l’implémentation de deux méthodes par une seule, mais qui prend en compte le non renseignement du paramètre. Notre programme devient donc :
#include <iostream.h>
class Point
{
     int x;
     int y;
public :
     Point();
     Point(intint);
     ~Point();
     void Init(int a, int b);
     void Init(int a=0);
     void Deplace(int a, int b);
     void Deplace(int a=0);
     void Affiche(char* strMesg="");
};
Point::Point()
{
     cout << "--Constructeur par defaut--" << endl;
}
Point::Point(int a, int b)
{
     cout << "--Constructeur (a,b)--" << endl;
     Init(a,b);
}
Point::~Point()
{
     cout << "--Destructeur--" << endl;
}
void Point::Init(int a, int b)
{
     x = a;
     y = b;
}
void Point::Init(int a)
{
     Init(a,a);
}
void Point::Deplace(int a, int b)
{
     x += a;
     y += b;
}
void Point::Deplace(int a)
{
     Deplace(a,a);
}
void Point::Affiche(char *strMesg)
{     // On ne rajoute pas le paramètre par  
     // défaut dans l’implémentation ! 
     cout << strMesg << x << ", " << y << endl;
}
void main()
{
     Point p(1,2);
     p.Deplace(4);
     p.Affiche("Le point vaut ");
     p.Init(10);
     p.Affiche("Le point vaut desormais : ");
     Point pp;
     pp = p;
     p.Deplace(12,13);
     pp.Deplace(5);
     p.Affiche("Le point p vaut ");
     pp.Affiche("Le point pp vaut ");
}
Vous commencez à avoir un programme un peu plus long...

Objets transmis en argument d’une fonction membre

Nous pouvons maintenant imaginer vouloir comparer deux points, afin de savoir s’ils sont égaux. Pour cela, nous allons mettre en oeuvre une méthode "Coincide" qui renvoie "1" lorsque les coordonnées des deux points sont égales :
class Point
{
     int x;
     int y;
public :
     Point(int a, int b){ x=a; y=b; }
     int Coincide(Point p);
};
int Point::Coincide(Point p)
{
     if( (p.x==x) && (p.y==y) )
       return 1;
     else
       return 0;
}
Cette partie de programme fonctionne parfaitement, mais elle possède un inconvénient majeur : le passage de paramètre par valeur, ce qui implique une "duplication" de l’objet d’origine. Cela n’est bien sûr pas très efficace.
La solution qui vous vient à l’esprit dans un premier temps est probablement de passer par un pointeur. Cette solution est possible, mais n’est pas la meilleure, dans la mesure où nous savons fort bien que ces pointeurs sont toujours sources d’erreurs (lorsqu’ils sont non initialisés, par exemple).
La vraie solution offerte par le C++ est de passer par des références. Avec ce type de passage de paramètre, aucune erreur est possible puisque l’objet à passer doit déjà exister (être instancié). En plus, les références offrent une simplification d’écriture, par rapport aux pointeurs :
#include <iostream.h>
class Point
{
     int x;
     int y;
public :
     Point(int a=0, int b=0){ x=a; y=b; }
     int Coincide(Point &);
};
int Point::Coincide(Point & p)
{
     if( (p.x==x) && (p.y==y) )
       return 1;
     else
       return 0;
}
void main()
{
     Point p(2,0);
     Point pp(2);
     if( p.Coincide(pp) )
          cout << "p et pp coincident !" << endl;
     if( pp.Coincide(p) )
          cout << "pp et p coincident !" << endl;
}

Aucun commentaire:

Enregistrer un commentaire