Menu

Constructeurs C++ : initialiser ses objets correctement

Un constructeur est la fonction membre spéciale qui s'exécute lorsqu'un objet est créé. Découvrez les constructeurs par défaut, paramétrés et de copie, les listes d'initialisation de membres, et comment éviter de laisser des objets à moitié initialisés.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Ce qu'est un constructeur

À la page précédente, vous avez défini des classes et leur avez donné des variables membres. Mais les membres d'un objet fraîchement créé contiennent les déchets qui se trouvaient dans cette mémoire, sauf si vous les définissez. Un constructeur corrige cela : c'est une fonction membre spéciale qui s'exécute automatiquement au moment où un objet est créé, et son unique rôle est de laisser l'objet dans un état valide et entièrement initialisé.

Un constructeur porte le même nom que la classe et n'a aucun type de retour, pas même void. Vous ne l'appelez jamais directement ; le compilateur l'appelle pour vous chaque fois qu'un objet vient à exister.

Le Counter() sans paramètre est appelé constructeur par défaut : c'est celui utilisé lorsque vous créez un objet sans passer aucun argument.

Constructeurs paramétrés

Un constructeur qui ne prend aucun argument convient, mais on veut généralement créer un objet avec des valeurs précises. Un constructeur paramétré accepte des arguments et les utilise pour initialiser les membres.

Une classe peut avoir plusieurs constructeurs, du moment que leurs listes de paramètres diffèrent : c'est la surcharge de fonctions ordinaire appliquée aux constructeurs. Ici, Point peut être créé avec ou sans coordonnées :

Un piège courant : Point p(); ne crée pas d'objet ; le compilateur le lit comme la déclaration d'une fonction nommée p qui renvoie un Point. Pour appeler le constructeur par défaut, écrivez Point p; (sans parenthèses) ou Point p{}; avec des accolades.

Listes d'initialisation de membres

Jusqu'ici, les exemples affectaient les membres à l'intérieur du corps du constructeur. Cela fonctionne pour les types simples, mais c'est le mauvais outil. Au moment où le corps s'exécute, chaque membre a déjà été construit par défaut ; le corps jette alors ce travail et écrase par une affectation. Une liste d'initialisation de membres initialise chaque membre directement, avant le corps, en une seule étape.

La syntaxe est un deux-points après la liste des paramètres, suivi de paires member(value) :

Pour un membre string, cela évite aussi de construire une chaîne vide puis de l'affecter : la liste d'initialisation la construit correctement du premier coup.

La liste d'initialisation n'est pas qu'une optimisation ; elle est obligatoire dans trois cas, car le corps ne peut qu'affecter, pas initialiser :

  • Les membres const : on ne peut pas affecter à un const une fois qu'il existe.
  • Les membres référence : une référence doit être liée au moment de sa naissance.
  • Les membres dont le type n'a pas de constructeur par défaut.
class Sensor {
    const int id;        // membre const
    int& slot;           // membre référence

public:
    Sensor(int sensorId, int& s) : id(sensorId), slot(s) {}
    // Essayer d'affecter id ou slot dans le corps ne compilerait pas.
};

Une subtilité à connaître : les membres sont initialisés dans l'ordre où ils sont déclarés dans la classe, et non dans l'ordre où vous les listez dans la liste d'initialisation. Si l'initialiseur d'un membre lit un autre membre, c'est l'ordre de déclaration qui compte ; confondre les deux est une source classique d'utilisation d'une valeur pas encore initialisée.

Arguments par défaut et constructeurs délégants

Vous n'avez pas toujours besoin de surcharges séparées. Les arguments par défaut permettent à un seul constructeur de couvrir plusieurs cas : omettez un argument et la valeur par défaut le remplit :

Soyez prudent en combinant un constructeur paramétré avec valeurs par défaut et un constructeur par défaut Point() séparé : le compilateur ne peut pas déterminer lequel appeler pour Point p; et signalera une ambiguïté. Choisissez une seule approche.

Lorsque vous avez plusieurs constructeurs qui partagent une même initialisation, un constructeur délégant (C++11) permet à un constructeur d'en appeler un autre au lieu de répéter la logique. Vous « déléguez » en plaçant l'autre constructeur dans la liste d'initialisation :

Le constructeur de copie

Lorsque vous créez un objet comme copie d'un autre — en le passant par valeur, en le renvoyant ou en écrivant Foo b = a; — le constructeur de copie s'exécute. Sa signature prend une référence const au même type :

ClassName(const ClassName& other);

Si vous n'en écrivez pas, le compilateur génère un constructeur de copie par défaut qui copie chaque membre. Pour les classes ne contenant que des valeurs (int, string, vector), c'est exactement ce qu'il faut et vous ne devriez pas écrire le vôtre.

Le gros piège vit dans le prochain chapitre sur la mémoire : si votre classe possède un pointeur brut vers de la mémoire du tas, le constructeur de copie par défaut copie le pointeur, pas les données — deux objets finissent donc par pointer vers la même mémoire, et tous deux tenteront de la libérer. C'est le bug de la double libération (double-free). La règle empirique est la règle de trois/cinq : si vous écrivez un destructeur personnalisé, vous avez presque certainement besoin d'un constructeur de copie personnalisé (et de l'affectation par copie) également. En C++ moderne, la solution la plus propre est de stocker un std::vector ou un pointeur intelligent, pour que la copie générée par le compilateur fonctionne simplement.

Notez aussi que prendre le paramètre par référence est obligatoire, pas optionnel : un constructeur de copie qui prendrait son argument par valeur devrait copier l'argument pour s'appeler lui-même — une récursion infinie qui ne compilerait même pas.

Ensuite : les destructeurs

Un constructeur met un objet en place ; un destructeur le démonte. Quand un objet sort de sa portée ou est supprimé, son destructeur s'exécute automatiquement — l'endroit parfait pour libérer des fichiers, des connexions réseau ou la mémoire du tas que l'objet détenait. La page suivante explique comment fonctionnent les destructeurs, quand exactement ils se déclenchent, et comment ils s'associent aux constructeurs pour donner à C++ son puissant modèle RAII.

Questions fréquentes

Qu'est-ce qu'un constructeur en C++ ?

Un constructeur est une fonction membre spéciale qui porte le même nom que la classe et n'a aucun type de retour. Il s'exécute automatiquement lorsqu'un objet est créé, et son rôle est d'amener l'objet dans un état valide et entièrement initialisé avant qu'un autre code ne l'utilise.

Quelle est la différence entre un constructeur par défaut et un constructeur paramétré ?

Un constructeur par défaut ne prend aucun argument et est utilisé quand vous créez un objet sans fournir de valeurs (Point p;). Un constructeur paramétré prend des arguments pour que l'appelant puisse initialiser l'objet avec des valeurs précises (Point p(3, 4);). Une classe peut avoir les deux, car les constructeurs sont surchargés selon leurs listes de paramètres.

Pourquoi devrais-je utiliser une liste d'initialisation de membres en C++ ?

Une liste d'initialisation de membres (: name(n), age(a)) initialise les membres directement, avant l'exécution du corps du constructeur. Elle est obligatoire pour les membres const, les références et les membres sans constructeur par défaut, et elle évite le coûteux construire-par-défaut-puis-affecter qui se produit quand on affecte dans le corps.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER