Gestion de la mémoire dynamique en C et C++

Distribution de la mémoire en C et C++

La mémoire dans un programme C ou C++ est répartie en plusieurs segments distincts. Le segment de la pile (stack) stocke les variables locales non statiques, les paramètres de fonction et les valeurs de retour ; il croît vers le bas. Le tas (heap) est utilisé pour l'allocation dynamique de mémoire pendant l'exécution du programme, avec une croissance ascendante. Le segment de données (data segment) contient les données globales et statiques. Le segment de code (code segment) renferme le code exécutable et les constantes en lecture seule.

Considérons l'exemple de code suivant pour illustrer ces concepts :


int variableGlobale = 1;
static int variableStatiqueGlobale = 1;
void Exemple()
{
    static int variableStatiqueLocale = 1;
    int variableLocale = 1;
    int tableauEntiers[10] = { 1, 2, 3, 4 };
    char chaineCaract[] = "abcd";
    const char* pointeurConstante = "abcd";
    int* ptr1 = (int*)malloc(sizeof(int) * 4);
    int* ptr2 = (int*)calloc(4, sizeof(int));
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    free(ptr1);
    free(ptr3);
}

Questions sur la mémoire

Pour les variables et pointeurs ci-dessus, identifiez leur emplacement mémoire : pile (A), tas (B), segment de données (C), ou segment de code (D).

  • variableGlobale se trouve dans C (segment de données)
  • variableStatiqueGlobale se trouve dans C (segment de données)
  • variableStatiqueLocale se trouve dans C (segment de données)
  • variableLocale se trouve dans A (pile)
  • tableauEntiers se trouve dans A (pile)
  • chaineCaract se trouve dans A (pile), car il s'agit d'une copie du tableau
  • *chaineCaract (premier élément) se trouve dans A (pile)
  • pointeurConstante se trouve dans A (pile), bien que le pointeur soit constant
  • *pointeurConstante se trouve dans D (segment de code), car la chaîne pointée est constante
  • ptr1 se trouve dans A (pile), mais pointe vers B (tas) via malloc
  • *ptr1 se trouve dans B (tas)

Questions de taille mémoire :

  • sizeof(tableauEntiers) = 40
  • sizeof(chaineCaract) = 5
  • strlen(chaineCaract) = 4
  • sizeof(pointeurConstante) = 4 ou 8 (selon l'architecture)
  • strlen(pointeurConstante) = 4
  • sizeof(ptr1) = 4 ou 8 (selon l'architecture)

Différence entre sizeof et strlen : sizeof retourne la taille en octets d'une varible ou d'un type, tandis que strlen calcule la longueur d'une chaîne de caractères terminée par un caractère nul.

Gestion dynamique de la mémoire en C

En C, la mémoire dynamique est gérée via les fonctions malloc, calloc, realloc et free.


void Demonstration()
{
    int* ptrA = (int*)malloc(sizeof(int));
    free(ptrA);

    int* ptrB = (int*)calloc(4, sizeof(int));
    int* ptrC = (int*)realloc(ptrB, sizeof(int) * 10);
    free(ptrC);
}

Différences entre malloc, calloc et realloc :

  • malloc alloue un bloc de mémoire de taille spécifiée sans initialisation, retournant un pointeur vers ce bloc.
  • calloc alloue un bloc de mémoire pour plusieurs éléments consécutifs, initialisant tous les octets à zéro.
  • realloc modifie la taille d'un bloc de mémoire précédemment alloué, préservant les données existantes si possible.

Le mécanisme interne de malloc dans les bibliothèques comme glibc gère les allocations avec des stratégies d'optimisation.

Gestion de la mémoire en C++

C++ introduit les opérateurs new et delete pour une gestion plus simple et sûre de la mémoire dynamique, en complément des fonctions C.

Pour les types intégrés :


void ExempleCPP()
{
    int* p1 = new int;          // Allocation simple
    int* p2 = new int(42);      // Allocation avec initialisation
    int* p3 = new int[5]{1, 2, 3}; // Allocation d'un tableau avec initialisation partielle
    delete p1;
    delete p2;
    delete[] p3;
}

Utiliser new/delete pour des éléments individuels et new[]/delete[] pour des tableaux.

Pour les types définis par l'utilisateur, new et delete appellent automatiquement les constructeurs et destructeurs :


class ClasseExemple {
public:
    ClasseExemple(int val = 0) : valeur(val) {
        std::cout << "Constructeur appelé à " << this << std::endl;
    }
    ~ClasseExemple() {
        std::cout << "Destructeur appelé à " << this << std::endl;
    }
private:
    int valeur;
};

int main()
{
    ClasseExemple* obj1 = (ClasseExemple*)malloc(sizeof(ClasseExemple)); // Pas d'appel au constructeur
    ClasseExemple* obj2 = new ClasseExemple(10); // Constructeur appelé
    free(obj1);
    delete obj2;

    ClasseExemple* tableauObj = new ClasseExemple[3]; // Constructeurs appelés pour chaque élément
    delete[] tableauObj;

    int* p4 = new int;
    delete p4;
    return 0;
}

Fonctions operaotr new et operator delete

new et delete sont des opérateurs, mais ils s'appuient sur les fonctions globales operator new et operator delete pour les allocations sous-jacentes.


// Implémentation simplifiée de operator new
void* operator new(size_t taille) {
    void* ptr;
    while ((ptr = malloc(taille)) == nullptr) {
        if (/* gestion d'échec */) {
            throw std::bad_alloc();
        }
    }
    return ptr;
}

// Implémentation simplifiée de operator delete
void operator delete(void* ptr) noexcept {
    free(ptr);
}

Ainsi, operator new utilise malloc en interne et lance une exception en cas d'échec, tandis que operator delete libère la mémoire via free.

Principe d'implémentation de new et delete

Pour les types intégrés, new et delete fonctionnent de manière similaire à malloc et free, avec des différences dans la gestion des erreurs (exception pour new).

Pour les types définis par l'utilisateur :

  • new : appelle operator new pour allouer de l'espace, puis le constructeur pour initialiser l'objet.
  • delete : appelle le destructeur pour nettoyer les ressources, puis operator delete pour libérer la mémoire.
  • new T[N] : appelle operator new[] pour allouer l'espace pour N objets, puis exécute N constructeurs.
  • delete[] : exécute N destructeurs, puis appelle operator delete[] pour libérer la mémoire.

Expression new de placement (placement-new)

Le placement-new permet d'appeler un constructeur sur de la mémoire déjà allouée, utile avec des pools de mémoire.


void UtilisationPlacementNew() {
    void* memoire = operator new(sizeof(ClasseExemple)); // Allocation sans construction
    ClasseExemple* obj = new(memoire) ClasseExemple(20); // Appel du constructeur
    obj->~ClasseExemple(); // Appel manuel du destructeur
    operator delete(memoire); // Libération de la mémoire
}

Format : new (adresse) type ou new (adresse) type(initialisation).

Questions fréquentes en entretien

Différences entre malloc/free et new/delete :

  • malloc et free sont des fonctions, tandis que new et delete sont des opérateurs.
  • malloc ne initialise pas la mémoire ; new peut initialiser avec des valeurs ou des constructeurs.
  • malloc nécessite un calcul manuel de la taille, tandis que new déduit la taille du type.
  • malloc retourne void* et nécessite un cast explicite ; new retourne un pointeur du type approprié.
  • En cas d'échec, malloc retourne NULL (nécessite une vérification), tandis que new lance une expection.
  • Pour les types définis par l'utilisateur, malloc/free n'appellent pas les constructeurs/desctructeurs, contrairement à new/delete.

Fuites de mémoire

Une fuite de mémoire survient lorsqu'un programme alloue de la mémoire mais ne la libère pas, entraînant une perte de contrôle sur cette mémoire et un gaspillage des ressources. Cela peut affecter les performances des applications à long terme, provoquant des ralentissements ou des blocages.


void RisqueDeFuite() {
    int* p1 = new int[10]; // Allocation dynamique
    // Si une exception survient ici, p1 ne sera pas libéré
    throw std::runtime_error("Erreur");
    delete[] p1; // Non atteint en cas d'exception
}

Types de fuites :

  • Fuite de tas (Heap leak) : mémoire allouée via malloc/calloc/realloc/new non libérée.
  • Fuite de ressources système : des descripteurs de fichiers, sockets ou autres ressources non libérés.

Détection et prévention :

  • Sous Windows, utiliser _CrtDumpMemoryLeaks() pour une détection basique.
  • Recourir à des outils tiers comme VLD pour une analyse approfondie.
  • Suivre des bonnes pratiques : libérer systématiquement la mémoire allouée, utiliser des pointeurs intelligents (RAII), et implémenter des bibliothèques de gestion mémoire internes.

Étiquettes: C C++ malloc new delete

Publié le 20 juillet à 17h23