MekkiSoft Formation Gratuite C++

Construction, destruction et initialisation d'objets

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 :

Recopie simple d'un objet Vecteur
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.

Cours précédent Cours précédent
Cours suivant Cours suivant