Constructeurs avec paramètres et surcharge en C++

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; puis x = 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.

Étiquettes: C++ constructeur Surcharge initialisation allocation mémoire

Publié le 29 juillet à 13h28