Menu

Pointeurs intelligents C++ : unique_ptr et shared_ptr expliqués

Les pointeurs intelligents possèdent la mémoire du tas et la libèrent automatiquement. Apprends unique_ptr, shared_ptr, make_unique et make_shared, et pourquoi tu ne devrais presque plus jamais écrire new/delete.

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

Le problème que résolvent les pointeurs intelligents

À la page précédente, tu as alloué de la mémoire avec new et tu l'as libérée avec delete. Ça fonctionne, mais cela fait peser la charge sur toi : chaque new a besoin d'un delete correspondant, sur chaque chemin de code, y compris ceux où une exception est levée à mi-parcours. Si tu en oublies un, tu provoques une fuite mémoire ; si tu exécutes delete deux fois, tu corromps le tas.

Les pointeurs intelligents règlent cela en liant la durée de vie de la mémoire du tas à un objet ordinaire sur la pile. Quand cet objet sort de sa portée, son destructeur exécute delete à ta place, c'est garanti, même si une exception déroule la pile. Cette idée s'appelle le RAII (Resource Acquisition Is Initialization), et les pointeurs intelligents se trouvent dans l'en-tête <memory>.

Tu utilises *p et p->member exactement comme avec un pointeur brut. La différence, c'est que tu n'appelles jamais delete : le pointeur intelligent s'en charge.

unique_ptr : un seul propriétaire, aucun partage

unique_ptr est le pointeur intelligent vers lequel tu devrais te tourner par défaut. Il représente une propriété exclusive : un seul unique_ptr possède l'objet à un instant donné, et quand ce pointeur meurt, l'objet meurt avec lui. Il a un surcoût d'exécution nul par rapport à un pointeur brut.

Crée-en un avec make_unique (C++14). Il prend les arguments du constructeur et te rend un pointeur prêt à l'emploi :

Comme il ne peut y avoir qu'un seul propriétaire, un unique_ptr ne peut pas être copié. Tenter de le copier est une erreur de compilation, et cette erreur, c'est le langage qui te protège contre deux propriétaires essayant tous deux de faire delete sur le même objet :

auto a = make_unique<int>(10);
auto b = a;   // error: call to deleted copy constructor of unique_ptr

Pour transmettre la propriété à quelqu'un d'autre, tu la déplaces avec std::move. Après le déplacement, le pointeur d'origine est vide (il contient nullptr) :

C'est le modèle que tu voudras la plupart du temps : il y a toujours exactement un propriétaire clair, et le compilateur le fait respecter.

shared_ptr : propriété partagée par comptage de références

Parfois, plusieurs parties de ton programme ont réellement besoin de partager le même objet, et aucune ne sait laquelle finira en dernier. C'est à cela que sert shared_ptr. Il tient un compteur de références : chaque copie incrémente le compteur, chaque destruction le décrémente, et l'objet n'est libéré que lorsque le compteur atteint zéro.

Crée-les avec make_shared :

Contrairement à unique_ptr, copier un shared_ptr ne pose pas de problème : c'est tout l'intérêt. La contrepartie, c'est le coût : le compteur de références est stocké sur le tas et mis à jour de façon atomique (thread-safe), donc shared_ptr est plus lourd que unique_ptr. N'y recours que lorsque la propriété est réellement partagée, pas seulement pour éviter de réfléchir à qui possède quoi.

make_shared est aussi plus efficace que shared_ptr<T>(new T(...)) : il alloue l'objet et le bloc de contrôle en une seule allocation au lieu de deux.

weak_ptr et la rupture des cycles de références

shared_ptr a un piège classique : si deux objets détiennent des shared_ptr l'un vers l'autre, leurs compteurs de références n'atteignent jamais zéro, donc aucun n'est jamais libéré — une fuite mémoire alors même que tu as utilisé des pointeurs intelligents.

struct Node {
    shared_ptr<Node> next;   // si deux nœuds pointent l'un vers l'autre,
};                           // ils se maintiennent mutuellement en vie pour toujours

La solution, c'est weak_ptr : un observateur non propriétaire d'un shared_ptr. Il n'augmente pas le compteur de références, donc il ne maintient jamais un objet en vie. Pour utiliser l'objet, tu appelles .lock(), qui te donne un shared_ptr si l'objet existe encore, ou un pointeur vide s'il a déjà disparu.

Utilise weak_ptr pour les « pointeurs de retour » et les caches — partout où tu veux référencer un objet sans en revendiquer la propriété.

Erreurs fréquentes et pièges

Les pointeurs intelligents éliminent la plupart des bugs de mémoire, mais quelques pièges subsistent :

Ne mélange pas propriété intelligente et propriété brute de la même mémoire. Ne construis jamais deux pointeurs intelligents à partir du même pointeur brut : chacun essaiera d'appeler delete dessus :

int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw);   // catastrophe : les deux supprimeront le même int (double libération)

C'est exactement pour ça que tu préfères make_unique/make_shared — il n'y a aucun pointeur brut traînant qu'on pourrait mal utiliser.

Un unique_ptr ne peut être que déplacé, donc passe-le par valeur pour transférer la propriété. Si une fonction doit utiliser mais pas posséder l'objet, prends plutôt une référence simple ou un T* brut — un pointeur brut qui se contente d'observer est parfaitement acceptable :

void consume(unique_ptr<int> p);   // prend la propriété (déplacement vers l'intérieur)
void observe(int* p);              // ne fait que regarder, ne possède rien

Ne te tourne pas vers shared_ptr par défaut. C'est tentant parce qu'il se copie librement, mais le comptage atomique de références coûte des performances bien réelles, et la propriété partagée rend les durées de vie plus difficiles à raisonner. Mets unique_ptr par défaut ; passe à shared_ptr uniquement quand tu as réellement besoin de plusieurs propriétaires.

unique_ptr pour les tableaux a besoin de la forme tableau. make_unique<int[]>(n) te donne un unique_ptr<int[]> qui appelle correctement delete[]. En pratique, préfère std::vector pour les tableaux dynamiques — il gère la mémoire à ta place et te donne en plus le suivi de la taille.

Suivant : les chaînes de caractères

Tu maîtrises désormais la gestion de la mémoire : les pointeurs intelligents te donnent l'allocation sur le tas sans les fuites. L'une des choses les plus courantes que tu vas allouer et faire circuler, c'est du texte — et le C++ te donne un outil bien plus sûr que les tampons bruts char*. La page suivante couvre std::string : comment elle grandit toute seule, les opérations que tu utiliseras tous les jours, et pourquoi elle te libère entièrement du travail manuel sur la mémoire.

Questions fréquentes

Que sont les pointeurs intelligents en C++ ?

Les pointeurs intelligents sont des objets de l'en-tête <memory> (unique_ptr, shared_ptr, weak_ptr) qui enveloppent un pointeur brut et appellent automatiquement delete sur la mémoire lorsqu'ils sortent de leur portée. Ils t'offrent l'allocation sur le tas sans le delete manuel ni les fuites que provoque son oubli.

Quelle est la différence entre unique_ptr et shared_ptr ?

unique_ptr est le propriétaire unique de son objet : il ne peut pas être copié, seulement déplacé, et il libère la mémoire au moment où il meurt. shared_ptr permet une propriété partagée grâce à un comptage de références : plusieurs shared_ptr peuvent pointer vers le même objet, et celui-ci n'est libéré que lorsque le dernier est détruit. Préfère unique_ptr sauf si tu as vraiment besoin d'une propriété partagée.

Dois-je utiliser make_unique ou new en C++ moderne ?

Utilise make_unique et make_shared. Ils allouent et enveloppent l'objet en une seule étape, il n'y a donc pas de new brut dont le résultat pourrait fuiter avant d'atteindre un pointeur intelligent. En règle générale, une base de code C++ moderne ne devrait presque jamais contenir de new ou de delete nus.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER