Constructeur
par défaut/initialisant
Constructeurs
simples
Nous avons aborder précédemment
la possibilité d'initialiser un objet lors de sa construction, ou
encore de proposer un constructeur par défaut. Nous allons dans
ce chapitre généraliser le principe tout en l'approfondissant.
Tout d'abord, il faut bien
comprendre l'intérêt de la chose : un constructeur initialisant
permet comme son nom l'indique d'initialiser les données membre,
que ces valeurs soient entrées par l'utilisateur, ou bien qu'elles
soient par défaut. Les destructeur quant à lui est appelé
à la fin de la vie de l'objet, de façon automatique ou non
(si l'objet est un pointeur).
Ceci prend toute son importance
dans le cas d'un objet qui contient lui-même des données de
type pointeur. Prenons par exemple une classe Vecteur qui gère
un vecteur mémoire d'entiers. Nous allons essayer d'intégrer
dans un premier temps :
-
un constructeur initialisant,
connaissant la taille du vecteur,
-
un destructeur,
-
une méthode de lecture
d'une valeur dans le vecteur,
-
---------------- d'écriture
---------------------------------,
-
une méthode qui renvoie
la taille du vecteur.
Dans la mesure où vous
devriez être capable de réaliser cette classe, essayez de
la concevoir tout seul dans un premier temps...
Solution
| #include
class Vecteur
{
int *pVecteur;
int nTaille;
public :
Vecteur(int);
//
Construction connaissant la taille du vecteur
~Vecteur(); //
Destruction
int GetAt(int
ind) // Lecture
de valeur en "inline"
{
if(
ind>=0 && ind
return pVecteur[ind];
else
//
comportement indéfini, pour l'instant retour 0;
return 0;
}
void SetAt(int
ind, int val) //
Ecriture de valeur
{
if(
ind>=0 && ind
pVecteur[ind]=val;
}
int Size(){
return
nTaille; }
};
Vecteur::Vecteur(int Taille)
{
// Aucun contrôle
de taille n'est fait pour simplifier
nTaille = Taille;
pVecteur = new
int[nTaille];
}
Vecteur::~Vecteur()
{
delete pVecteur;
}
void main()
{
Vecteur *v;
v = new Vecteur(10);
for( int
i=0; iSize(); i++ )
v->SetAt(i,
i*i-i);
for( i=0;
iSize(); i++ )
cout <<
v->GetAt(i) << " ";
cout << endl;
delete v;
} |
Le pourquoi du comment
Il faut bien comprendre
le rôle du destructeur. Un destructeur de classe est appelé
à la destruction de l'objet, c'est-à-dire à la fin
du bloc dans lequel il a été instancié, ou bien à
un endroit spécifié par l'utilisateur, si l'objet a été
créé dynamiquement (appel à un delete
à ce moment là).
Mais le destructeur détruit
*seulement* l'objet, soit la mémoire allouée pour les membres.
En outre, il ne peut pas savoir que la classe est composée de pointeurs
et qu'une allocation mémoire a été effectuée
vers tel segment mémoire. C'est au créateur de la classe
qu'il advient de spécifier la désallocation des pointeurs
(tout comme l'allcoation d'ailleurs).
Quant au main, il
est vraiment très simple. Il se propose juste de créer un
objet de type Vecteur, dynamiquement, de le remplir avec différentes
valeurs, puis de les afficher.
Pour comprendre un peu le
fonctionnement de la classe, essayez d'autres opérations telles
que différentes créations d'objets statiquement et dynamiquement.
Constructeur
par recopie
Reprenons notre
classe Point. Nous avons donc déjà deux constructeurs,
à savoir un par défaut et un autre par initialisation des
coordonnées.
Il apparaît qu'il
pourrait être intéressant de réaliser un constructeur
à partir d'un point !
C++ permet cette fonctionnalité
par défaut, en voici l'exemple :
// Définition de la classe Point
...
Point pt(1,2);
Point pt2(pt); //
Construction par recopie de Point
... |
Il est possible de redéfinir
ce constructeur. Bien entendu, il faut qu'il est un intérêt
quelconque.
class Point
{
int
x;
int
y;
public :
Point(){x=-1;y=-1;}
Point(int
a, int b){ x=a;
y=b; }
Point(const
Point & pt) // Constructeur par recopie
de Point
{
x = pt.x;
y = pt.y;
}
... //
D'éventuelles autres méthodes
}; |
Ajouter un constructeur par
recopie est donc assez simple dans l'esprit. En ce qui concerne la syntaxe,
vous remarquerez deux choses :
-
le passage par référence
:
C++ oblige un passage par référence dans le cas d'une construction
par recopie. Ceci vient du fait qu'il faut réellement utiliser l'objet
lui-même, et non une copie.
-
la présence d'un "const"
: en fait, rien n'oblige de le placer ici, c'est simplement une protection
de l'objet à recopier. En effet, lors de cette recopie, nous faisons
appel à l'objet lui-même, et il n'y aurait aucun sens à
vouloir le modifier !
Cet
exemple est utile pour introduire la nécessité d'un constructeur
par recopie. En effet, dans le cas d'un objet qui comporte un pointeur
(notre classe Vecteur, par exemple), lorsqu'on veut recopier un
objet, on ne recopie pas ce qui est pointé, mais l'adresse du pointeur
seulement. Du coup, on se retrouve avec deux objets différents,
mais qui possèdent une donnée qui pointe vers la même
chose ! Ceci est bien entendu fort dangeureux, et par là même,
est à éviter.
Un petit schéma pour
mieux comprendre :
Figure
10 : recopie simple d'un objet Vecteur
On voit clairement qu'après
recopie, les membres pVecteur
de v et
de v2
pointent au même endroit.
La parade à celà
vient tout naturellement du constructeur par recopie. Au lieu de copier
"bêtement" le pointeur, on peut allouer la mémoire et puis
recopier les valeurs.
Mise
en oeuvre
Application à
la classe Vecteur :
| #include
class Vecteur
{
int *pVecteur;
int nTaille;
public :
Vecteur(int);
//
Construction connaissant la taille du vecteur
Vecteur(const
Vecteur &); // Construction par recopie
~Vecteur(); //
Destruction
int GetAt(int
ind) // Lecture
de valeur en "inline"
{
if(
ind>=0 && ind
return pVecteur[ind];
else
//
comportement indéfini, pour l'instant retour 0;
return 0;
}
void SetAt(int
ind, int val) //
Ecriture de valeur
{
if(
ind>=0 && ind
pVecteur[ind]=val;
}
int Size(){
return
nTaille; }
};
Vecteur::Vecteur(int Taille)
{
// Aucun contrôle
de taille n'est fait pour simplifier
nTaille = Taille;
pVecteur = new int[nTaille];
}
Vecteur::Vecteur(const Vecteur
& v)
{
nTaille = v.nTaille;
pVecteur = new
int[nTaille]; // allocation de la mémoire
for( int
i=0; i// recopie des valeurs
pVecteur[i]=v.pVecteur[i];
}
Vecteur::~Vecteur()
{
delete pVecteur;
}
void main()
{
Vecteur *v;
v = new Vecteur(10);
for( int
i=0; iSize(); i++ )
v->SetAt(i,
i*i-i);
Vecteur vv(*v); //
construction par recopie
vv.SetAt(0, 13); //
Changement de quelques valeurs
vv.SetAt(1, 13);
vv.SetAt(2, 13);
for( i=0;
iSize(); i++ )
cout <<
v->GetAt(i) << " ";
cout << endl;
for( i=0;
i
cout <<
vv.GetAt(i) << " ";
cout << endl;
delete v;
} |
Avec l'exemple de dessus, vous
devez obtenir en sortie :
0 0 2 6 12 20 30 42 56 72
13 13 13 6 12 20 30 42 56 72
Ce qui montre bel et bien l'intérêt
de la recopie.
|