Maîtriser les classes et les objets en C++ : mémoire, copie, déplacement et fonctions membres

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 et les objets en C++ : mémoire, copie, déplacement et fonctions membres

Post by Hydraxx »

Maîtriser les classes et les objets en C++ : mémoire, copie, déplacement et fonctions membres

1. Ressources dynamiques et durée de vie

Une classe C++ peut posséder une ressource : mémoire dynamique, buffer, handle Windows, socket, descripteur POSIX, etc. Le point essentiel est de savoir qui possède cette ressource et qui doit la libérer.

Code: Select all

class Buffer
{
private:
    int* data;
    size_t size;

public:
    Buffer(size_t s) : size(s)
    {
        data = new int[size];
    }

    ~Buffer()
    {
        delete[] data;
    }
};
Le constructeur acquiert la ressource et le destructeur la libère. C'est le principe fondamental du RAII : la durée de vie de la ressource est liée à celle de l'objet.

2. Copie superficielle et copie profonde

Une copie automatique membre par membre d'un objet contenant un pointeur peut copier seulement l'adresse.

Code: Select all

Buffer a(100);
Buffer b = a;
Si les deux objets contiennent la même adresse, ils peuvent tous les deux tenter de libérer la même allocation. Cela provoque potentiellement double free, use-after-free, corruption mémoire ou comportement indéfini.

Une copie profonde crée au contraire une nouvelle ressource et copie son contenu.

3. Constructeur de copie

Le constructeur de copie crée un nouvel objet à partir d'un autre objet existant.

Code: Select all

ClassName(const ClassName& other);
Exemple :

Code: Select all

Buffer::Buffer(const Buffer& other) : size(other.size)
{
    data = new int[size];

    for (size_t i = 0; i < size; ++i)
        data[i] = other.data[i];
}
Chaque objet possède alors sa propre allocation.

4. Opérateur d'affectation par copie

Il faut distinguer construction et affectation.

Code: Select all

Buffer b = a;   // construction par copie

Buffer c(100);
c = a;          // affectation par copie
L'opérateur correspondant est :

Code: Select all

Buffer& operator=(const Buffer& other);
L'objet destination existe déjà et peut déjà posséder une ressource. L'affectation doit donc gérer correctement l'ancienne ressource, la nouvelle et l'auto-affectation telle que :

Code: Select all

a = a;
5. Move semantics

Copier une grosse ressource peut coûter cher. Lorsqu'un objet source est temporaire ou que son ancien contenu n'est plus nécessaire, C++ permet de transférer sa ressource au lieu de la recopier.

Avant :

Code: Select all

A ----> grosse allocation
Après déplacement :

Code: Select all

A ----> vide
B ----> grosse allocation
La ressource elle-même n'est pas nécessairement copiée : sa possession est transférée.

6. Lvalues, rvalues et références rvalue

Une variable nommée telle que :

Code: Select all

Buffer buffer(100);
est une lvalue. Un temporaire tel que :

Code: Select all

Buffer(100)
peut être une rvalue.

Les références rvalue utilisent && :

Code: Select all

Buffer&&
Elles sont au cœur des move semantics.

7. Constructeur de déplacement

Sa forme classique est :

Code: Select all

Buffer(Buffer&& other) noexcept;
Exemple :

Code: Select all

Buffer::Buffer(Buffer&& other) noexcept
    : data(other.data), size(other.size)
{
    other.data = nullptr;
    other.size = 0;
}
Le nouvel objet récupère la ressource. L'ancien objet reste valide et destructible, mais son ancien contenu ne doit plus être supposé présent.

8. Affectation par déplacement

L'affectation par déplacement transfère une ressource vers un objet qui existe déjà.

Code: Select all

Buffer& Buffer::operator=(Buffer&& other) noexcept
{
    if (this != &other)
    {
        delete[] data;
        data = other.data;
        size = other.size;

        other.data = nullptr;
        other.size = 0;
    }

    return *this;
}
9. std::move

std::move ne déplace pas lui-même la mémoire. Il permet de traiter une expression comme une rvalue afin qu'une opération de déplacement puisse être sélectionnée.

Code: Select all

Buffer a(1000);
Buffer b(std::move(a));
Après le déplacement, a reste valide mais son état est généralement non spécifié. On peut le détruire ou lui affecter une nouvelle valeur.

10. Passage optimal des paramètres

Passage par valeur :

Code: Select all

void process(Buffer buffer);
Une copie ou un déplacement peut intervenir.

Passage par référence :

Code: Select all

void process(Buffer& buffer);
La fonction travaille sur l'objet original et peut le modifier.

Passage par référence constante :

Code: Select all

void process(const Buffer& buffer);
Aucune copie n'est nécessaire et la fonction ne modifie normalement pas l'objet. C'est très courant pour lire efficacement un objet volumineux.

Référence rvalue :

Code: Select all

void process(Buffer&& buffer);
Elle est utile lorsqu'une fonction traite spécifiquement une valeur temporaire ou déplaçable.

11. Rule of Five

Une classe qui possède directement une ressource peut devoir contrôler cinq fonctions spéciales :
  • le destructeur ;
  • le constructeur de copie ;
  • l'opérateur d'affectation par copie ;
  • le constructeur de déplacement ;
  • l'opérateur d'affectation par déplacement.

Code: Select all

~T();
T(const T&);
T& operator=(const T&);
T(T&&);
T& operator=(T&&);
Ces opérations déterminent ce qui arrive à la ressource lorsqu'un objet est détruit, copié ou déplacé.

12. Rule of Zero

Si les membres d'une classe gèrent déjà correctement leurs ressources, il vaut souvent mieux laisser le compilateur générer les opérations spéciales.

Par exemple :

Code: Select all

class Buffer
{
private:
    std::vector<int> data;
};
std::vector sait déjà gérer destruction, copie, déplacement et mémoire. La classe Buffer peut donc souvent ne définir aucune des cinq opérations.

Pour du bas niveau, la règle importante reste : le propriétaire d'une ressource doit avoir une politique de durée de vie claire.

13. Fonctions membres static

Une fonction membre normale agit sur une instance et dispose implicitement de this. Une fonction static appartient à la classe et ne possède pas de pointeur this.

Code: Select all

class Counter
{
public:
    static int getGlobalCount();
};
Utilisation :

Code: Select all

Counter::getGlobalCount();
Elle ne peut pas accéder directement aux membres non statiques d'une instance particulière.

14. Fonctions membres const

Le const placé après une fonction membre signifie que la fonction promet de ne pas modifier normalement l'état de l'objet.

Code: Select all

size_t getSize() const;
Une telle fonction peut être appelée sur un objet const.

Code: Select all

const Buffer buffer(100);
buffer.getSize();
Les fonctions qui ne modifient pas l'objet devraient généralement être const.

15. Surcharge basée sur const

Une classe peut proposer une version modifiable et une version const d'une fonction.

Code: Select all

T& get();
const T& get() const;
Un objet modifiable peut obtenir une référence modifiable, alors qu'un objet const ne reçoit qu'une référence const.

16. mutable

Un membre mutable peut être modifié depuis une fonction const.

Code: Select all

class Data
{
private:
    mutable int accessCount = 0;

public:
    int getValue() const
    {
        ++accessCount;
        return 42;
    }
};
Cela peut servir pour un cache, des statistiques ou certains mécanismes internes qui ne modifient pas la valeur logique de l'objet. mutable ne doit pas servir simplement à contourner const.

17. Surcharge des fonctions membres

Plusieurs fonctions peuvent avoir le même nom si leurs paramètres permettent au compilateur de les distinguer.

Code: Select all

void setValue(int value);
void setValue(double value);
Le compilateur choisit la meilleure surcharge selon les arguments.

18. Fonctions membres qualifiées par référence

Une fonction membre peut être réservée aux lvalues ou aux rvalues.

Code: Select all

void function() &;
void function() &&;
Exemple conceptuel :

Code: Select all

Object obj;
obj.function();        // version &
Object().function();   // version &&
C'est surtout utile dans du C++ avancé et dans les bibliothèques génériques.

19. Fonctions inline

Une fonction définie directement dans une classe est implicitement inline.

Code: Select all

class Counter
{
public:
    int get() const
    {
        return value;
    }

private:
    int value;
};
inline ne garantit pas que le compilateur remplacera physiquement l'appel par le corps de la fonction. Le compilateur décide des optimisations. Le mot-clé possède également un rôle dans les règles de définition entre unités de traduction.

20. constexpr

constexpr indique qu'une valeur ou une fonction peut participer à une évaluation à la compilation lorsque les conditions le permettent.

Code: Select all

constexpr int square(int x)
{
    return x * x;
}

constexpr int result = square(5);
Une fonction constexpr peut aussi être exécutée au runtime si ses arguments ne sont pas connus à la compilation.

Code: Select all

int x;
std::cin >> x;
int result = square(x);
constexpr est utile pour les constantes, tailles, calculs connus à la compilation et certaines optimisations ou abstractions sans coût runtime.

21. consteval

consteval est plus strict que constexpr. Une fonction immédiate consteval doit être évaluée à la compilation lorsqu'elle est appelée.

Code: Select all

consteval int square(int x)
{
    return x * x;
}
À retenir :

Code: Select all

constexpr -> peut être évalué à la compilation
consteval -> doit être évalué à la compilation
22. Constructeurs et objets constexpr

Un type peut être conçu pour créer des objets utilisables dans des expressions constantes.

Code: Select all

class Point
{
public:
    constexpr Point(int x, int y) : x(x), y(y) {}

    constexpr int getX() const
    {
        return x;
    }

private:
    int x;
    int y;
};

constexpr Point p(10, 20);
23. Données membres static

Une donnée static appartient à la classe plutôt qu'à chaque objet.

Code: Select all

class Process
{
private:
    static int count;
};
Plusieurs objets Process partagent donc le même Process::count.

24. Variables inline static

Le C++ moderne permet d'initialiser certaines données statiques directement dans la classe.

Code: Select all

class Counter
{
public:
    inline static int count = 0;
};
Cela évite dans de nombreux cas une définition séparée dans un fichier source.

25. Membres static constexpr

Une constante liée logiquement à une classe peut être déclarée ainsi :

Code: Select all

class Buffer
{
public:
    static constexpr size_t MaxSize = 4096;
};
Utilisation :

Code: Select all

Buffer::MaxSize
C'est pratique pour des limites, tailles maximales ou constantes propres à un type.

26. Membres références

Une classe peut stocker une référence vers un autre objet.

Code: Select all

class Manager
{
private:
    Application& app;

public:
    Manager(Application& a) : app(a) {}
};
Une référence doit être initialisée à la construction et ne peut ensuite pas être redirigée comme un pointeur.

La classe ne possède pas nécessairement l'objet référencé. Il faut donc garantir que l'objet référencé reste vivant suffisamment longtemps. Sinon, la classe contiendra une référence pendante.

27. Classes imbriquées

Une classe peut être déclarée à l'intérieur d'une autre.

Code: Select all

class Process
{
public:
    class Thread
    {
    public:
        int id;
    };
};
Le type peut être désigné par :

Code: Select all

Process::Thread
Cela permet de regrouper des types étroitement liés.

28. Énumérations dans les classes

Une énumération peut être placée dans une classe.

Code: Select all

class Device
{
public:
    enum class State
    {
        Stopped,
        Running,
        Error
    };
};
Utilisation :

Code: Select all

Device::State::Running
Cela exprime clairement que State appartient au domaine logique de Device.

29. Surcharge des opérateurs

C++ permet de donner un comportement aux opérateurs pour des types utilisateur.

Une classe numérique peut par exemple permettre :

Code: Select all

a + b
au lieu de :

Code: Select all

a.add(b)
Une surcharge d'opérateur ne crée pas un nouvel opérateur : elle définit le comportement d'un opérateur existant pour un type.

30. Opérateur membre ou fonction non membre

Un opérateur peut être une fonction membre :

Code: Select all

Value Value::operator+(const Value& rhs) const;
Ou une fonction non membre :

Code: Select all

Value operator+(const Value& lhs, const Value& rhs);
Les fonctions non membres peuvent être intéressantes lorsque les deux opérandes doivent être traités symétriquement, notamment vis-à-vis des conversions implicites.

31. Opérateurs arithmétiques

On peut surcharger +, -, * et /.

Code: Select all

class Number
{
public:
    Number operator+(const Number& other) const
    {
        return Number(value + other.value);
    }

private:
    int value;
};
Une bonne surcharge doit avoir une signification intuitive. Il faut éviter d'utiliser + pour une opération sans rapport avec une addition logique.

32. Opérateurs arithmétiques composés

Les opérateurs +=, -=, *= et /= peuvent également être surchargés.

Code: Select all

Number& operator+=(const Number& other)
{
    value += other.value;
    return *this;
}
On peut ensuite réutiliser += pour implémenter + et réduire la duplication de code.

Code: Select all

Number operator+(Number lhs, const Number& rhs)
{
    lhs += rhs;
    return lhs;
}
33. Opérateurs de comparaison

Les classes peuvent supporter :

Code: Select all

==
!=
<
<=
>
>=
Exemple :

Code: Select all

bool operator==(const Value& other) const
{
    return value == other.value;
}
Avant C++20, supporter toutes les comparaisons demandait souvent plusieurs fonctions répétitives.

34. Comparaisons depuis C++20 et operator<=>

C++20 introduit l'opérateur <=>, surnommé spaceship operator.

Dans les cas simples :

Code: Select all

auto operator<=>(const MyClass&) const = default;
Le compilateur peut utiliser les membres de la classe pour générer une grande partie des opérations de comparaison.

Cela réduit fortement le code répétitif.

35. = delete

Une opération peut être explicitement interdite.

Code: Select all

class Resource
{
public:
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
};
Cette classe n'est plus copiable.

C'est très utile pour une ressource dont la duplication n'a pas de sens. On peut par exemple concevoir un objet déplaçable mais non copiable.

36. = default

On peut demander explicitement au compilateur de générer son comportement standard.

Code: Select all

MyClass() = default;
Ou, par exemple :

Code: Select all

bool operator==(const MyClass&) const = default;
Cela indique clairement que l'implémentation automatique convient.

37. Interfaces stables

Une classe publique devrait exposer ce dont ses utilisateurs ont besoin sans révéler inutilement son fonctionnement interne.

Conceptuellement :

Code: Select all

utilisateur
    |
    v
interface publique
    |
    v
implémentation privée
Cette séparation permet notamment de réduire les dépendances et de faciliter l'évolution de l'implémentation.

38. PImpl : Pointer to Implementation

Une technique classique consiste à déplacer les détails internes dans une classe d'implémentation séparée.

Conceptuellement :

Code: Select all

class PublicClass
{
private:
    Impl* impl;
};
La définition complète de Impl peut rester dans le fichier d'implémentation.

Code: Select all

PublicClass
    |
    +----> Impl
             |
             +---- données privées
             +---- dépendances
             +---- logique interne
Cette technique peut améliorer la stabilité d'une interface et réduire certaines dépendances de compilation. Elle ajoute toutefois une indirection et peut ajouter une allocation ou de la complexité. Il ne faut donc pas l'utiliser automatiquement.

39. Importance pour le développement système

Pour du C++ système, les notions les plus importantes de ce chapitre sont :
  • durée de vie des objets ;
  • possession des ressources ;
  • copie profonde et copie superficielle ;
  • constructeur et affectation par copie ;
  • constructeur et affectation par déplacement ;
  • std::move ;
  • Rule of Five et Rule of Zero ;
  • const ;
  • références et durée de vie.
Elles sont directement liées à des bugs tels que :

Code: Select all

double free
use-after-free
dangling pointer
dangling reference
resource leak
copie involontaire
destruction prématurée
static, constexpr, = delete, = default et la séparation interface/implémentation sont également importants.

Les ref-qualified member functions, consteval, classes imbriquées, PImpl et les surcharges avancées d'opérateurs sont utiles à connaître mais moins prioritaires pour commencer du Win32/POSIX/kernel.

40. Raisonner en propriétaire de ressource

Pour une ressource système, la question fondamentale est :

Code: Select all

Qui possède cette ressource ?
Une copie naïve peut donner :

Code: Select all

A ----+
      +----> RESOURCE
B ----+
Si A et B pensent tous les deux être propriétaires, le design est dangereux.

Trois stratégies sont possibles selon le type.

Copie réelle :

Code: Select all

A ----> RESOURCE A
B ----> RESOURCE B
Copie interdite :

Code: Select all

A ----> RESOURCE
copie de A -> erreur de compilation
Déplacement :

Code: Select all

avant :
A ----> RESOURCE

after move :
A ----> vide
B ----> RESOURCE
C'est cette notion de propriété qui permet de comprendre réellement la copie et le déplacement en C++.

41. Résumé

Une classe C++ peut représenter directement la durée de vie d'une ressource.

Code: Select all

constructeur
    |
    v
acquisition
    |
    v
utilisation
    |
    v
destructeur
    |
    v
libération
Lors d'une copie, on veut généralement obtenir deux objets indépendants lorsque chacun est propriétaire de sa ressource.

Lors d'un déplacement, la propriété est transférée sans recopier inutilement la ressource.

Les cinq fonctions spéciales essentielles sont :

Code: Select all

~T()
T(const T&)
T& operator=(const T&)
T(T&&)
T& operator=(T&&)
Lorsque les membres savent déjà gérer correctement leurs ressources, la Rule of Zero permet souvent de laisser le compilateur gérer ces opérations.

À retenir également :
  • const permet de garantir qu'une fonction membre ne modifie pas normalement l'objet ;
  • static associe une donnée ou une fonction à la classe plutôt qu'à une instance ;
  • constexpr permet l'évaluation à la compilation lorsque c'est possible ;
  • consteval impose l'évaluation à la compilation ;
  • = delete interdit explicitement une opération ;
  • = default demande le comportement généré par le compilateur ;
  • la surcharge d'opérateurs permet une syntaxe naturelle lorsqu'elle respecte la signification de l'opérateur ;
  • PImpl permet de cacher fortement l'implémentation derrière une interface publique.
Pour le développement bas niveau, retiens surtout ceci : à chaque fois qu'une classe contient un pointeur, un handle, un socket ou une autre ressource, demande-toi qui la possède, si elle peut être copiée, si elle peut être déplacée et qui la libère.

Return to “Bases du C++”

Who is online

Users browsing this forum: No registered users and 1 guest