Les constructeurs sont des fonctions spéciales qui permettent d'initialiser les objets d'une classe. Ils peuvent accepter des paramètres, offrant ainsi la flexibilité de définir des états d'initialisation différents pour chaque objet.
Surcharge de constructeurs
Une classe peut définir plusieurs constructeurs, chacun avec une liste de paramètres différente. C'est ce qu'on appelle la surcharge de constructeurs. Ces constructeurs surchargés doivent respecter les mêmes règles que pour la surcharge de fonctions classiques en C++.
Exemple de déclaration de constructeur avec paramètre :
class MonTest {
public:
MonTest(int valeur) {
// Utiliser 'valeur' pour initialiser un membre de la classe
}
};
Définition vs Déclaration d'objet
Il est important de distinguer la définition d'un objet de sa déclaration :
- Définition d'objet : Alloue la mémoire nécessaire pour l'objet et appelle automatiquement le constructeur approprié.
- Déclaration d'objet : Informe le compilateur de l'existence d'un objet, sans pour autant allouer de mémoire ou appeler de constructeur à ce stade.
MonTest obj; // Définit l'objet 'obj' et appelle le constructeur
int main() {
extern MonTest obj; // Déclare que 'obj' existe quelque part
return 0;
}
Appel automatique des constructeurs
Les constructeurs sont généralement appelés automatiquemant lors de la définition d'un objet. Le compilateur choisit le constructeur approprié en fonction des arguments fournis.
class Testeurs {
public:
Testeurs() { } // Constructeur par défaut
Testeurs(int v) { } // Constructeur avec un entier
};
int main() {
Testeurs t1; // Appelle Testeurs()
Testeurs t2(10); // Appelle Testeurs(int v)
Testeurs t3 = 20; // Appelle également Testeurs(int v) en utilisant l'initialisation par copie
return 0;
}
Dans l'exemple Testeurs t2(10);, le compilateur recherche un constructeur acceptant un type int. Si plusieurs constructeurs correspondent, les règles de surcharge sont appliquées pour choisir le plus approprié.
Exemple de code complet :
#include <cstdio>
class MonObjet {
public:
MonObjet() {
printf("MonObjet()\n");
}
MonObjet(int val) {
printf("MonObjet(int val), val = %d\n", val);
}
};
int main() {
MonObjet o1; // Appel de MonObjet()
MonObjet o2(100); // Appel de MonObjet(int val)
MonObjet o3 = 200; // Appel de MonObjet(int val) via l'initialisation par copie
int nombre(300); // Initialisation de 'nombre' avec 300, équivalent à int nombre = 300;
printf("nombre = %d\n", nombre);
return 0;
}
Différence entre initialisation et affectation
Il est crucial de comprendre la différence entre l'initialisation et l'affectation en C++ :
- Initialisation : Se produit lors de la définition d'un objet et déclenche l'appel d'un constructeur. Par exemple,
MonObjet o3 = 200;est une initialisation. - Affectation : Se produit après qu'un objet a déjà été créé et initialisé. Elle ne fait pas appel aux constructeurs. Par exemple, si
int x = 1;puisx = 2;, la seconde ligne est une affectation.
En C++, l'initialisation a des implications plus profondes car elle peut impliquer l'exécution de constructeurs, alors que l'affectation est une opération distincte.
Appel manuel de constructeurs
Bien que les constructeurs soient généralement appelés automatiquement lors de la définition d'un objet, il existe des scénarios où un appel manuel est nécessaire ou souhaitable, notamment lors de la création de tableaux d'objets.
Création d'un tableau d'objets avec appel manuel :
#include <cstdio>
class Calculateur {
private:
int valeur_memoire;
public:
Calculateur() {
printf("Calculateur()\n");
valeur_memoire = 0;
}
Calculateur(int v) {
printf("Calculateur(int v), v = %d\n", v);
valeur_memoire = v;
}
int obtenirValeur() const {
return valeur_memoire;
}
};
int main() {
// Création d'un tableau en appelant explicitement des constructeurs pour chaque élément
Calculateur tab_calculateurs[3] = {Calculateur(), Calculateur(1), Calculateur(2)};
for(int i = 0; i < 3; ++i) {
printf("tab_calculateurs[%d].obtenirValeur() = %d\n", i, tab_calculateurs[i].obtenirValeur());
}
// Création d'un objet unique en appelant manuellement le constructeur
Calculateur obj_unique = Calculateur(100);
printf("obj_unique.obtenirValeur() = %d\n", obj_unique.obtenirValeur());
return 0;
}
Implémentation d'un tableau sécurisé : la classe TableauEntiers
Pour pallier les limitations des tableaux natifs C++, on peut développer une classe wrapper comme TableauEntiers qui offre des fonctionnalités de sécurité et de gestion.
Interface (TableauEntiers.h)
#ifndef _TABLEAUENTIERS_H_
#define _TABLEAUENTIERS_H_
class TableauEntiers {
private:
int taille_max;
int* ptr_donnees;
public:
// Constructeur qui alloue la mémoire et initialise les éléments à zéro
explicit TableauEntiers(int taille);
// Retourne la taille du tableau
int obtenirTaille() const;
// Tente de récupérer une valeur à un index donné. Retourne true si l'opération réussit.
bool lire(int index, int& valeur) const;
// Tente de définir une valeur à un index donné. Retourne true si l'opération réussit.
bool ecrire(int index, int valeur);
// Libère la mémoire allouée
void liberer();
};
#endif
Implémentation (TableauEntiers.cpp)
#include "TableauEntiers.h"
#include <cstdio> // Pour les messages de débogage potentiels
TableauEntiers::TableauEntiers(int taille) : taille_max(taille), ptr_donnees(nullptr) {
if (taille > 0) {
ptr_donnees = new int[taille];
for (int i = 0; i < taille; ++i) {
ptr_donnees[i] = 0; // Initialisation à zéro
}
} else {
taille_max = 0; // Gérer le cas d'une taille non positive
}
}
int TableauEntiers::obtenirTaille() const {
return taille_max;
}
bool TableauEntiers::lire(int index, int& valeur) const {
bool succes = (index >= 0) && (index < taille_max);
if (succes && ptr_donnees) {
valeur = ptr_donnees[index];
}
return succes;
}
bool TableauEntiers::ecrire(int index, int valeur) {
bool succes = (index >= 0) && (index < taille_max);
if (succes && ptr_donnees) {
ptr_donnees[index] = valeur;
}
return succes;
}
void TableauEntiers::liberer() {
delete[] ptr_donnees;
ptr_donnees = nullptr; // Bonne pratique pour éviter les pointeurs errants
taille_max = 0;
}
Exemple d'utilisation (main.cpp)
#include <cstdio>
#include "TableauEntiers.h"
int main() {
TableauEntiers mon_tableau(5);
// Remplissage du tableau
for (int i = 0; i < mon_tableau.obtenirTaille(); ++i) {
mon_tableau.ecrire(i, (i + 1) * 10);
}
// Lecture des éléments du tableau
for (int i = 0; i < mon_tableau.obtenirTaille(); ++i) {
int valeur_lue = 0;
if (mon_tableau.lire(i, valeur_lue)) {
printf("mon_tableau[%d] = %d\n", i, valeur_lue);
} else {
printf("Erreur de lecture à l'index %d\n", i);
}
}
mon_tableau.liberer(); // Libération manuelle de la mémoire
return 0;
}
Résumé
- Les constructeurs peuvent être définis avec des paramètres pour une initialisation personnalisée.
- Plusieurs constructeurs peuvent coexister dans une classe via la surcharge.
- Les constructeurs sont appelés automatiquement lors de la définition d'un objet.
- Des appels manuels de constructeurs sont possbiles dans certains contextes.