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 les (certaines) méthodes sont accessibles.
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(int,
int);
//
Initialisation d'un point
void
Deplace(int,
int);
//
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 de 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
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
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 faite une méthode de la classe
Point.
Le reste est complètement identique. La seule différence
entre ces 2 programmes vient du fait qu'on dit que les méthodes
du premier programme (dont l'implémentation est faite dans la classe),
sont "inline". Ceci signifie que chaque appel à la méthode
sera remplacé dans l'exécutable, par 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.
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écifier 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 pour 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
... // 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(int, int);
//
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(int, int);
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(int, int);
~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
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);
for(
int
i=0; i
{
t.pArray[i]=i;
}
Test *t2; //
Pointeur d'objet
t2 = new
Test(10); // Allocation dynamique
for(
i=0; iGetSize(); 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(int, int);
~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
class Point
{
int
x;
int
y;
public :
Point();
Point(int,
int);
~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 : la 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
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;
} |
|