Gestion de la mémoire dynamique en C++ : new, delete, et Smart Pointers

Gestion de la mémoire dynamique en C++ : new, delete, et Smart Pointers

Amine Abidi - Lead Software Engineer C++/Qt - Co-fondateur PointerLab

Publié par Amine Abidi - Lead Software Engineer C++/Qt - Co-fondateur PointerLab

Introduction

La gestion de la mémoire est un aspect essentiel de la programmation, en particulier en C++, où le développeur est souvent responsable de l'allocation et de la libération de la mémoire. Contrairement à d'autres langages comme Java ou Python, C++ ne dispose pas de garbage collector automatique, ce qui signifie qu'une mauvaise gestion peut entraîner des fuites de mémoire ou des comportements indéfinis.

Dans cet article, nous allons explorer :

  • Comment gérer la mémoire dynamique avec new et delete.
  • Les problèmes potentiels liés à cette approche.
  • Les bonnes pratiques modernes avec les smart pointers de la bibliothèque standard.

1. Comprendre la mémoire dynamique en C++

En C++, la mémoire est divisée en trois parties principales :

  1. Mémoire statique : Utilisée pour les variables globales et statiques, allouée au démarrage du programme.
  2. Mémoire automatique (pile) : Utilisée pour les variables locales, gérée automatiquement par le compilateur.
  3. Mémoire dynamique (tas) : Allouée et libérée manuellement par le programmeur.

La mémoire dynamique est gérée à l'aide des opérateurs new et delete.

#include <iostream>

int main() {
int* ptr = new int(42); // Allocation dynamique
std::cout << "Valeur : " << *ptr << std::endl;

    delete ptr;  // Libération de la mémoire
    return 0;

}

2. Risques associés à la mémoire dynamique

Bien que new et delete soient puissants, ils présentent des risques :

1. Fuites de mémoire

Si vous oubliez de libérer la mémoire allouée, celle-ci reste occupée inutilement, ce qui peut épuiser les ressources du système. Cela peut être particulièrement problématique si une exception interrompt l'exécution avant que la mémoire ne soit libérée correctement.
Pour en savoir plus sur la gestion des erreurs et les exceptions, consultez notre article : Exceptions et gestion des erreurs en C++.

#include <iostream>

void fuiteMemoire() {
    int* ptr = new int(10);  // Mémoire allouée
    // Pas de delete ici : fuite de mémoire !
}

int main() {
    fuiteMemoire();
    return 0;
}

2. Dangling pointers

Un pointeur peut devenir invalide si la mémoire qu'il pointe est libérée.

#include <iostream>

int main() {
    int* ptr = new int(10);
    delete ptr;  // Mémoire libérée

    // Accès à un pointeur non valide (dangling pointer)
    std::cout << "Valeur : " << *ptr << std::endl;  // Comportement indéfini
    return 0;
}

3. Double libération

Libérer deux fois la même mémoire peut provoquer des erreurs graves.

#include <iostream>

int main() {
    int* ptr = new int(10);
    delete ptr;  // Libération de la mémoire
    delete ptr;  // Erreur ! Double libération

    return 0;
}

3. Améliorations avec les Smart Pointers

Avec les évolutions modernes du langage, il est préférable d'utiliser les smart pointers de la STL, qui gèrent automatiquement la durée de vie des objets. Pour en savoir plus sur la Standard Template Library (STL) et ses fonctionnalités, consultez notre article : Introduction à la STL en C++.

Les types principaux de Smart Pointers :

  1. std::unique_ptr

    • Gestion exclusive de la mémoire.
    • Non copiable, mais transférable avec std::move.
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);  // Allocation sans fuite
    std::cout << "Valeur : " << *ptr << std::endl;

    // La mémoire est automatiquement libérée à la fin de la portée
    return 0;
}
  1. std::shared_ptr
  • Permet un partage d'ownership entre plusieurs pointeurs.
  • Utilise un compteur de référence pour gérer la mémoire.
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::shared_ptr<int> ptr2 = ptr1;  // Partage du même pointeur

    std::cout << "Valeur via ptr1 : " << *ptr1 << std::endl;
    std::cout << "Valeur via ptr2 : " << *ptr2 << std::endl;

    // La mémoire est libérée lorsque le compteur atteint zéro
    return 0;
}
  1. std::weak_ptr
  • Référence non-propriétaire utilisée pour éviter les cycles avec std::shared_ptr.
#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;  // Référence faible pour éviter les cycles
};

int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();

    node1->next = node2;
    node2->prev = node1;  // Pas de cycle grâce à weak_ptr

    return 0;
}

4. Comparaison entre new/delete et Smart Pointers

Aspect new / delete Smart Pointers
Gestion automatique ❌ Non ✅ Oui
Risque de fuite de mémoire ✅ Elevé ❌ Réduit
Complexité ✅ Manuelle ✅ Simplifiée

5. Conseils pratiques pour la gestion de la mémoire

  • Toujours préférer les smart pointers à l'utilisation directe de new et delete.
  • Utiliser les conteneurs de la STL (std::vector, std::map, etc.) pour éviter l'allocation manuelle. Pour découvrir les principaux conteneurs et leur utilisation, consultez notre article : Introduction à la STL en C++.
  • Utiliser des outils comme Valgrind ou AddressSanitizer pour détecter les fuites de mémoire.

Découvrez l'ensemble de notre blog sur le C++

Rejoignez la communauté C++ 🇫🇷 sur Discord !

Un espace convivial pour échanger et apprendre ensemble.