Maîtriser les classes, les objets et leur cycle de vie en C++

Salut ce forum apprends les bases du c++

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Maîtriser les classes, les objets et leur cycle de vie en C++

Post by Hydraxx »

Maîtriser les classes, les objets et leur cycle de vie en C++

Introduction

Une classe permet de regrouper des données et les fonctions qui travaillent sur ces données.

Pour du développement système, il n'est pas nécessaire de transformer tout le programme en architecture orientée objet. En revanche, il est important de comprendre les classes, car une grande partie du C++ moderne et de nombreuses bibliothèques les utilisent.

Le point le plus important de ce chapitre est le cycle de vie des objets :
  • quand un objet est créé ;
  • comment ses membres sont initialisés ;
  • quand il est copié ;
  • quand il est affecté ;
  • quand il est détruit ;
  • ce que le compilateur génère automatiquement.
Ces mécanismes deviennent particulièrement importants lorsqu'un objet possède une ressource : mémoire dynamique, fichier, socket, handle Windows, verrou, etc.

1. Définir une classe

Une classe définit un nouveau type.

Code: Select all

class Buffer
{
public:
    void setSize(size_t size);
    size_t getSize() const;

private:
    size_t m_size;
};
Buffer est un type défini par le programmeur. Un objet est une instance de cette classe :

Code: Select all

Buffer buffer;
Une classe peut contenir des données membres, fonctions membres, constructeurs, destructeur et opérateurs.

2. public et private

public rend un membre accessible depuis l'extérieur. private réserve l'accès direct à l'implémentation de la classe.

Code: Select all

class Counter
{
public:
    void increment();

private:
    int m_value;
};
Dans du code système, une classe peut rester très simple : regrouper un état et quelques opérations sans construire une grosse architecture orientée objet.

3. Données et fonctions membres

Chaque objet possède ses propres données membres.

Code: Select all

class Counter
{
public:
    void increment()
    {
        m_value++;
    }

    int getValue() const
    {
        return m_value;
    }

private:
    int m_value = 0;
};
Deux objets Counter possèdent donc deux m_value distincts.

4. Fonctions membres const

Une fonction membre qui ne doit pas modifier l'état de l'objet peut être déclarée const.

Code: Select all

int getValue() const
{
    return m_value;
}
Cela permet notamment de l'appeler à travers une référence constante :

Code: Select all

void printCounter(const Counter& c)
{
    std::cout << c.getValue();
}
À retenir : le const placé après les parenthèses concerne l'objet sur lequel la fonction est appelée.

5. Initialisation des membres

Les données membres doivent être initialisées. Une valeur par défaut peut être donnée dans la classe :

Code: Select all

class Counter
{
private:
    int m_value = 0;
};
Un constructeur peut aussi choisir la valeur grâce à sa liste d'initialisation.

6. Le pointeur this

Dans une fonction membre non statique, this est un pointeur vers l'objet courant.

Code: Select all

void setValue(int value)
{
    this->m_value = value;
}
Si on appelle :

Code: Select all

Counter c;
c.setValue(10);
this désigne c pendant l'appel.

On n'a généralement pas besoin d'écrire this->. Il devient utile pour désambiguïser des noms, retourner l'objet courant ou transmettre son adresse.

7. Objets sur la stack et durée de vie automatique

Code: Select all

void function()
{
    Counter c;
}
c est un objet local. Sa destruction est automatique lorsqu'il sort de sa portée.

C'est une propriété fondamentale du C++ : la durée de vie des objets peut directement contrôler la durée de vie des ressources.

8. Allocation dynamique

Un objet peut être créé dynamiquement :

Code: Select all

Counter* c = new Counter;
c->increment();
delete c;
-> accède à un membre via un pointeur.

Code: Select all

c->increment();
correspond conceptuellement à :

Code: Select all

(*c).increment();
Avec new/delete manuel, oublier delete provoque une fuite. En C++ moderne, on préfère souvent la durée de vie automatique ou un propriétaire explicite plutôt que des new/delete dispersés.

9. Cycle de vie d'un objet
  • obtention du stockage ;
  • construction ;
  • utilisation ;
  • éventuelles copies ou affectations ;
  • destruction ;
  • libération du stockage si nécessaire.
La différence entre construction, copie, affectation et destruction est essentielle.

10. Constructeurs

Un constructeur est exécuté lors de la création d'un objet. Il porte le nom de la classe et n'a aucun type de retour.

Code: Select all

class Counter
{
public:
    Counter(int value)
        : m_value(value)
    {
    }

private:
    int m_value;
};
Utilisation :

Code: Select all

Counter c{10};
Le rôle principal du constructeur est de placer immédiatement l'objet dans un état valide.

11. Listes d'initialisation

Code: Select all

Counter(int value)
    : m_value(value)
{
}
m_value est ici directement initialisé.

À distinguer de :

Code: Select all

Counter(int value)
{
    m_value = value;
}
Dans ce second cas, le corps effectue une affectation après l'initialisation du membre.

Pour certains types simples la différence est minime, mais elle devient importante pour les objets complexes, références et membres const.

12. Ordre d'initialisation

Les membres sont initialisés selon leur ordre de déclaration dans la classe et non selon l'ordre écrit dans la liste d'initialisation.

Code: Select all

class Example
{
private:
    int a;
    int b;

public:
    Example()
        : b(20), a(10)
    {
    }
};
a est malgré tout initialisé avant b.

Il est donc préférable d'écrire la liste d'initialisation dans le même ordre que les déclarations.

13. Constructeur par défaut

Un constructeur par défaut est utilisable sans argument.

Code: Select all

class Counter
{
public:
    Counter()
        : m_value(0)
    {
    }

private:
    int m_value;
};
Puis :

Code: Select all

Counter c;
La présence de constructeurs personnalisés influence les fonctions que le compilateur génère automatiquement. Il ne faut donc pas supposer qu'un constructeur sans argument existe toujours.

14. = default

= default demande explicitement au compilateur son comportement par défaut.

Code: Select all

Counter() = default;
Counter(const Counter&) = default;
Counter& operator=(const Counter&) = default;
~Counter() = default;
On l'utilise lorsque le comportement automatique convient exactement.

15. = delete

= delete interdit une opération.

Code: Select all

class Resource
{
public:
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
};
La copie devient impossible.

C'est très utile pour un objet propriétaire exclusif d'une ressource : une copie naïve pourrait créer deux propriétaires de la même ressource et provoquer une double libération.

16. Plusieurs constructeurs

Une classe peut proposer plusieurs constructeurs.

Code: Select all

class Counter
{
public:
    Counter()
        : m_value(0)
    {
    }

    Counter(int value)
        : m_value(value)
    {
    }

private:
    int m_value;
};

Code: Select all

Counter a;
Counter b{100};
Le compilateur sélectionne le constructeur correspondant aux arguments.

17. Constructeurs délégués

Un constructeur peut appeler un autre constructeur de la même classe.

Code: Select all

class Counter
{
public:
    Counter()
        : Counter(0)
    {
    }

    Counter(int value)
        : m_value(value)
    {
    }

private:
    int m_value;
};
Cela évite de dupliquer la logique d'initialisation.

18. Constructeurs de conversion et explicit

Un constructeur à un argument peut permettre certaines conversions implicites.

Code: Select all

class Number
{
public:
    Number(int value)
        : m_value(value)
    {
    }

private:
    int m_value;
};
Pour empêcher les conversions involontaires, on peut écrire :

Code: Select all

explicit Number(int value)
    : m_value(value)
{
}
Pour du code système utilisant des types représentant des concepts précis, explicit évite certaines conversions accidentelles difficiles à repérer.

19. Constructeur de copie

Un constructeur de copie construit un NOUVEL objet à partir d'un objet existant.

Forme classique :

Code: Select all

Buffer(const Buffer& other);
Exemple :

Code: Select all

Buffer a;
Buffer b = a;
b n'existait pas auparavant : il est construit à partir de a.

Le paramètre est généralement const Buffer& afin d'éviter de devoir copier l'objet simplement pour le transmettre au constructeur de copie.

20. Copie membre par membre

Pour une classe composée de membres dont la copie normale est correcte, la copie générée par le compilateur peut suffire.

Code: Select all

class Position
{
public:
    int x;
    int y;
};
Copier Position copie x et y.

Le problème apparaît surtout lorsqu'une classe possède directement une ressource.

21. Copie superficielle et ressources

Code: Select all

class Buffer
{
private:
    char* m_data;
};
Copier automatiquement m_data copie l'adresse, pas le bloc mémoire pointé.

Deux objets peuvent donc finir par pointer vers le même bloc. Si chacun pense en être propriétaire et le libère, on obtient une double libération.

Une classe propriétaire d'une ressource doit définir clairement sa politique :
  • copie profonde ;
  • copie interdite ;
  • partage explicite ;
  • transfert de propriété.
22. Passage par valeur

Code: Select all

void process(Buffer buffer);
Le paramètre est un objet distinct construit à partir de l'argument, sous réserve des règles d'élision et d'optimisation du langage.

Pour un gros objet, la copie peut être coûteuse.

23. Passage par référence constante

Lorsqu'une fonction doit seulement consulter un objet :

Code: Select all

void process(const Buffer& buffer);
  • on évite normalement une copie de Buffer ;
  • on travaille sur l'objet existant ;
  • const empêche sa modification via cette référence.
Pour un petit type comme int, le passage par valeur reste généralement préférable et simple.

24. Affectation d'un objet

Code: Select all

Buffer a;
Buffer b;

b = a;
b existe déjà : ce n'est pas une construction. Son état est affecté depuis a.

Compare avec :

Code: Select all

Buffer b = a;
Ici b est créé à cet instant : c'est une construction par copie.

25. Opérateur d'affectation par copie

Forme classique :

Code: Select all

Buffer& operator=(const Buffer& other);
Exemple :

Code: Select all

Buffer& Buffer::operator=(const Buffer& other)
{
    if (this == &other)
        return *this;

    // Copier correctement l'état.

    return *this;
}
this représente la destination et other la source.

return *this renvoie l'objet courant par référence.

26. Auto-affectation

Code: Select all

a = a;
Une implémentation qui détruit une ancienne ressource avant de recopier la source peut rencontrer un problème si source et destination sont le même objet.

Un test classique est :

Code: Select all

if (this == &other)
    return *this;
Toutes les implémentations n'en ont pas forcément besoin, mais il faut comprendre le cas.

27. Destructeur

Le destructeur est exécuté lorsque la durée de vie de l'objet se termine.

Code: Select all

class Buffer
{
public:
    ~Buffer();
};

Code: Select all

Buffer::~Buffer()
{
    // libération des ressources possédées
}
Pour un objet local, l'appel est automatique à la sortie de portée.

Pour :

Code: Select all

Buffer* b = new Buffer;
delete b;
delete provoque notamment l'appel du destructeur avant la libération du stockage de l'objet.

28. Ordre de destruction

Les objets locaux d'une même portée sont détruits dans l'ordre inverse de leur construction.

Code: Select all

Counter a;
Counter b;
Counter c;
Construction :

Code: Select all

a
b
c
Destruction :

Code: Select all

c
b
a
Cette règle compte lorsqu'une ressource dépend d'une autre.

29. Construction, copie et affectation

Construction normale

Code: Select all

Buffer a;
Construction avec paramètre

Code: Select all

Buffer a{1024};
Construction par copie

Code: Select all

Buffer b = a;
b n'existait pas.

Affectation

Code: Select all

Buffer b;
b = a;
b existait déjà.

Destruction

Lorsque la durée de vie de b se termine, son destructeur est exécuté.

C'est LA distinction essentielle du chapitre.

30. Fonctions spéciales générées par le compilateur

Selon la définition de la classe, le compilateur peut générer plusieurs fonctions spéciales :
  • constructeur par défaut ;
  • constructeur de copie ;
  • opérateur d'affectation par copie ;
  • constructeur de déplacement ;
  • opérateur d'affectation par déplacement ;
  • destructeur.
Les règles exactes dépendent des fonctions que la classe déclare elle-même.

Une copie automatique n'est pas forcément correcte simplement parce que le programme compile.

31. Copie autorisée ou interdite

Si la copie automatique est correcte :

Code: Select all

Buffer(const Buffer&) = default;
Buffer& operator=(const Buffer&) = default;
Si la classe ne doit jamais être copiée :

Code: Select all

Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
Pour un propriétaire exclusif de ressource, interdire la copie est souvent une décision logique.

32. Exemple système : propriétaire d'une ressource

Code: Select all

class Resource
{
public:
    Resource();
    ~Resource();

    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;

private:
    void* m_resource;
};
Conceptuellement :
  • le constructeur obtient la ressource ;
  • le destructeur la libère ;
  • la copie est interdite ;
  • un seul objet en est propriétaire.
Cela évite que deux objets pensent posséder la même ressource.

33. Rapport avec le RAII

Le RAII associe la durée de vie d'une ressource à celle d'un objet.

Code: Select all

construction -> acquisition
destruction  -> libération
Cela peut concerner mémoire, handles, fichiers, sockets, mappings, mutex et verrous.

Même si tu préfères parfois appeler directement les fonctions de fermeture des API système, il faut comprendre ce principe car il est omniprésent en C++ professionnel.

34. Classes et développement bas niveau

Une classe système peut simplement représenter :
  • un buffer ;
  • une ressource ;
  • un mapping mémoire ;
  • un socket ;
  • un fichier ;
  • un contexte de thread ;
  • un paquet réseau ;
  • un parseur binaire.
Il n'est pas nécessaire d'utiliser héritage et architectures OOP complexes pour profiter du modèle objet C++.

35. Ce qu'il faut absolument retenir
  • Une classe définit un type ; un objet est une instance.
  • public expose l'interface et private protège l'état interne.
  • Une fonction membre const ne modifie normalement pas l'objet.
  • this pointe vers l'objet courant.
  • Un constructeur initialise un nouvel objet.
  • La liste d'initialisation initialise directement les membres.
  • Les membres sont initialisés dans leur ordre de déclaration.
  • Un destructeur termine la vie de l'objet et peut libérer ses ressources.
  • Les objets locaux sont détruits automatiquement en sortie de portée.
  • Le constructeur de copie crée un nouvel objet depuis un autre.
  • L'opérateur d'affectation travaille sur un objet déjà existant.
  • Buffer b = a; et b = a; ne représentent donc pas la même opération.
  • const& permet souvent de transmettre un gros objet sans le copier.
  • Copier un pointeur copie son adresse, pas les données pointées.
  • Une classe propriétaire d'une ressource doit avoir une politique de copie claire.
  • = default demande le comportement automatique.
  • = delete interdit explicitement une opération.
  • explicit évite certaines conversions implicites.
  • Pour le développement système, comprendre le cycle de vie est plus important que construire des architectures OOP complexes.
36. Mini mémo

Code: Select all

// Construction
Type a;

// Construction avec argument
Type a{10};

// Construction par copie
Type b = a;

// Affectation
b = a;

// Passage sans copie
void function(const Type& object);

// Constructeur
Type(int value);

// Constructeur de copie
Type(const Type& other);

// Affectation par copie
Type& operator=(const Type& other);

// Destructeur
~Type();

// Comportement automatique demandé
Type() = default;

// Copie interdite
Type(const Type&) = delete;

// Conversion implicite limitée
explicit Type(int value);
Conclusion

Pour du C++ bas niveau, l'objectif principal de ce chapitre n'est pas de devenir spécialiste de l'OOP. Il faut surtout comprendre exactement la durée de vie des objets.

Quand tu vois :

Code: Select all

Type object;
prends l'habitude de te demander :
  • quel constructeur est appelé ?
  • quels membres sont initialisés ?
  • dans quel ordre ?
  • l'objet possède-t-il une ressource ?
  • peut-il être copié ?
  • que signifie son affectation ?
  • quand sera-t-il détruit ?
  • que doit faire son destructeur ?
Cette compréhension devient essentielle pour écrire du C++ système robuste et éviter les fuites, doubles libérations et erreurs de durée de vie.

Return to “Bases du C++”

Who is online

Users browsing this forum: No registered users and 1 guest