Ce cours est visible gratuitement en ligne.

Got it!
C++ : Gérer correctement ses allocations dynamiques

C++ : Gérer correctement ses allocations dynamiques

Last updated on Tuesday, November 19, 2013
  • Moyen

Introduction du cours

En C++, la mémoire allouée dynamiquement n'est pas libérée automatiquement.
Vouloir gérer ceci manuellement tout en produisant du code correct est une tâche très difficile et risquée. On peut donc s'intéresser à la question suivante : comment automatiser tout cela ?

Rappels et prise de conscience

Rappels

Qu'est-ce qu'une allocation dynamique ? C'est simplement l'allocation d'un bout de mémoire sur le tas. Par opposition, l'allocation statique est faite sur la pile. Les objets alloués sur le tas ont une durée de vie que le programmeur peut contrôler autrement que par les blocs : leur destruction n'est pas automatique. En revanche, pour les objets alloués sur la pile, la destruction est faite à la fin du bloc où se trouve la déclaration. Si leur destruction n'est pas automatique, c'est que c'est justement au programmeur de la faire. Ainsi, le C++ offre quatre opérateurs pour gérer l'allocation dynamique : new, new[], delete et delete[]. On les utilise très simplement :

int* array = new int[42];
delete[] array;
A* obj = new A("blabla", true, 'a');
delete obj;

new alloue un objet unique sur le tas tandis que new[] alloue une série d'objets, un tableau. Respectivement, il faut utiliser delete et delete[] pour détruire les entités allouées. Il est très important de ne pas mélanger ces opérateurs, on ne fait pas n'importe quoi avec l'allocation dynamique.

Pourquoi ne pas utiliser std::malloc() qui est une fonction héritée du C ? Tout simplement parce que std::malloc() n'est pas du tout adaptée au C++ : en C, les types sont tous ce qu'en C++ on appelle PODT (plain old data type). Un certain nombre de règles doivent être satisfaites pour qu'un type soit un PODT (vous trouverez plus d'info sur wikipédia à ce sujet) et notamment l'absence de constructeur et de destructeur dans une structure. C'est là le problème : std::malloc() n'appelle pas le constructeur et la fonction de libération de la mémoire qui va avec, std::free(), n'appelle pas le destructeur. S'il n'y a pas appel au constructeur et au destructeur, automatiquement, un objet non-POD ne pourra pas correctement être construit ni correctement être détruit. Pourtant, c'est un comportement qu'on attend d'un système d'allocation dynamique en C++, et comme les opérateurs new, etc. ne sont pas aussi restrictifs et fonctionnent aussi sur les PODT, on préfèrera toujours utiliser new, delete, etc. plutôt que std::malloc() et std::free().

Quand bien même on serait amené à utiliser std::malloc(), il faut impérativement libérer la mémoire avec std::free() et avec rien d'autre. Plus généralement, il y a un principe très simple que l'on dicte très souvent aux débutants et qui permet de faire du code correct au niveau de la gestion de la mémoire : un new = un delete ; un new[] = un delete[] ; un std::malloc() = un std::free().

Le danger dans tout ça

C'est bien beau, mais si l'on suit toujours cette règle à la lettre, notre code restera toujours garanti sans fuite de mémoire et autre tracas ? Normalement oui, mais en pratique ... non. Un système d'allocations dynamiques géré entièrement ainsi ("à la main" vais-je dire) ne sera jamais ou presque jamais à 100% sécurisé. Du moins, pour un projet de taille conséquente, il est très difficile de parvenir à ce résultat et quand on y arrive, on aura tellement amoché le code qu'il ne ressemblera à plus rien. "À la main", on arrivera pas à trouver un bon compromis entre "code lisible" et "code correct". Le principal concept du C++ qui présente un réel danger pour ces systèmes gérés "à la main" sont les exceptions. Plus rarement, il peut tout simplement y avoir un oubli quelque part.

Les exceptions sont des objets qui peuvent être lancés à partir d'un point du programme (souvent quand un problème s'est présenté) et qui, une fois lancés, remontent la pile d'appels jusqu'à ce qu'ils rencontrent un système de réception (un bloc try) suivi d'un système de traitement (un ou plusieurs bloc catch). Un ensemble try - catch suit toujours cette logique : "on exécute le bout de code que je contiens et s'il lance une exception, je la rattrape et je la traite selon son type". C'est un mécanisme d'interruption de code et de gestion d'erreurs très pratique dans beaucoup de cas. Si une exception n'est jamais rattrapée, c'est-à-dire si elle n'est même pas rattrapée dans le main, on obtient une erreur fatale.

Pourquoi ces exceptions présentent-ils un réel problème pour l'allocation dynamique "nue" ? Imaginez simplement un bout de code présentant à la chaîne une allocation dynamique, un appel à une fonction (appelons-la f()) et la libération de la mémoire allouée juste avant. Si f() lance une exception, la mémoire allouée dynamiquement ne sera jamais libérée puisque l'instruction de libération ne sera jamais exécutée. On pourrait évidemment contourner ce problème avec quelques blocs en plus, mais le code deviendrait carrément ingérable et totalement incompréhensible.

Autre exemple : imaginez une classe qui fait deux allocations dynamiques pour deux pointeurs membres dans le constructeur et que leur libération est faite dans le destructeur. À priori, pas de problème. Mais si la deuxième allocation échoue (ce qui peut arriver) mais que la première a réussi, une exception est lancée et l'objet n'est ainsi pas considéré comme existant, comme construit. En d'autres termes le destructeur n'est jamais appelé et la première zone pourtant allouée avec succès ne sera jamais libérée.

D'autres problèmes qui ne sont pas forcément liés aux exceptions peuvent aussi survenir comme la tentative de libération d'une zone non-allouée (par exemple quand on essaye de delete un pointeur non-initialisé ou qu'on delete deux fois un pointeur sur une zone allouée dynamiquement), ou encore la tentative d'accès à la valeur pointée par un pointeur invalide : imaginez deux pointeurs sur le même objet alloué sur le tas, on peut très bien par accident "deleter" un pointeur et continuer à bosser avec l'autre comme si de rien n'était.

Le C++ possède pourtant la capacité de contrecarrer ces problèmes, et je vais présenter une technique très répandue de gestion sécurisée des allocations dynamiques.

Solution : les pointeurs intelligents

Une idée savante qu'on eu très tôt les programmeurs, c'est de ne plus manipuler des pointeurs "nus" (comme int*, float*, A**, etc.), mais des pointeurs encapsulés dans une classe à qui l'on confierait le travail d'assurer sa bonne gestion. Une instance d'une telle classe, allouée statiquement, serait alors en mesure de libérer à sa destruction automatiquement la mémoire allouée, ou encore d'empêcher les tentatives d'accès aux données de la zone pointée si celle-ci n'est pas allouée, etc. On parle de pointeur intelligent.

Une méthode plus générale : la RAII

Ce que j'ai décrit plus haut n'est qu'une application pratique parmi d'autres d'un principe très répandu en programmation orientée objet : RAII, pour "Ressource Acquisition Is Initialisation". Très simplement, ce principe nous dit que pour chaque acquisition de ressource que l'on fait, on crée aussi un objet qui va garantir sa bonne gestion. Cet objet encapsulera la ressource et nous donnera une autre interface, plus adaptée, une interface de plus haut niveau. C'est un idiome de programmation qui permet de faire du code plus fiable, plus maintenable, plus compréhensible, plus sécurisée et plus simple.

L'utilité de la RAII trouve sa source dans la destruction automatique des objets alloués sur la pile : chaque objet alloué statiquement sera libéré, son destructeur sera appelé et ceci même en cas d'exception. On écrira l'instruction de libération de la ressource dans le destructeur et on est certain que la ressource sera libérée, sans besoin d'un appel à delete ou close() etc. Si l'acquisition échoue dans le constructeur, l'objet RAII en tiendra compte pour la suite. Cela implique donc aussi qu'on n'aura pas besoin de contrôler immédiatement de manière barbante si l'allocation d'une ressource s'est faite correctement.

Vous connaissez tous au moins une classe de la STL qui utilise ce principe. Il s'agit de std::fstream. On vous présente toujours cette classe comme offrant un système simple et sécurisé pour manipuler les fichiers en C++. Ce qu'on oublie de dire en général, c'est que c'est totalement basé sur de la RAII : la fichier est ouvert à la construction d'un objet fstream (qui sera son représentant), alloué statiquement, et il est automatiquement fermé à la destruction de cet objet. L'approche RAII se retrouve encore ailleurs : std::vector, std::string, etc. Et elle constitue la base de la technique du pointeur intelligent.

Zoom

Pourquoi parle-t-on de pointeur intelligent ? En quoi sont-ils intelligents ?

Premièrement, ils libèrent la mémoire sans qu'on le demande explicitement et ce quand ils partent du principe qu'on en a plus besoin. Dans la même logique que std::fstream, on crée un objet statiquement représentant une zone mémoire allouée dynamiquement. En disséquant un peu, cela se présente comme un objet encapsulant un pointeur sur cette zone. On se sert alors de la libération statique de cet objet pour mettre en œuvre une libération dynamique.

Deuxièmement, ils permettent d'adopter un autre point de vue sur les entités allouées. Quand l'on travaille avec un pointeur nu, on voit un pointeur et on se dit que ce n'est qu'une variable stockant une valeur qui est une adresse d'une autre variable. Mais quand on voit dans un code une déclaration et une utilisation d'un pointeur intelligent, on voit un objet en lui-même, l'entité allouée dynamiquement prend ainsi forme et se confond dans l'entité du pointeur ; un peu comme pour un objet std::fstream duquel on dirait "voilà notre fichier".

Troisièmement, le pointeur contrôle ce que l'on veut faire et assure avant tout la sécurité du code. Le problème de l'allocation dynamique gérée "à la main", c'est souvent la copie de pointeurs ou l'accès à des données non-allouées. On peut alors très imaginer un pointeur intelligent qui mettra en place un système sécurisé de copie ou qui empêchera l'utilisateur d'accéder à une zone non-allouée.

L'objet de ce concept RAII (c'est-à-dire le pointeur intelligent) a ainsi un double rôle : il doit ajouter ou au moins préserver la sémantique du code. Autrement dit, à la vue du code, on doit être en mesure de comprendre simplement ce que l'on fait ou veut faire, et au moins tout aussi simplement qu'avec les pointeurs nus. Deuxièmement, cet objet doit s'occuper de tous les tracas de l'allocation dynamique à notre place. Les deux sont très liés : en étant "intelligents", ces pointeurs nous évitent d'écrire du code qui aurait résolu (peut-être mal) autrement les problèmes, code souvent indigeste qu'on peut donc s'épargner. On ajoute de la lisibilité, de la sémantique.

La STL a évidemment (comme toujours) pensé à nous et nous propose un type de pointeur intelligent que je vais présenter : std::auto_ptr. Malheureusement, auto_ptr souffre d'un problème (que nous allons voir tout de suite), ce qui fait qu'on le déconseille très souvent. Boost également propose toute une panoplie de pointeurs intelligents, chacun pour un usage différent. Je vais vous présenter le plus connu d'entre eux, boost::shared_ptr, et ses avantages par rapport à std::auto_ptr. Cela dit en passant, si vous n'avez pas encore installé Boost, c'est le moment de le faire, on ne fait plus grand-chose sans ce framework.

En C++, ça donne quoi ?

En C++, un pointeur intelligent est donc un objet. Le type de cet objet est quasiment toujours une instance d'une classe template, c'est-à-dire ici une classe qui prend le type de l'objet (au sens large) alloué dynamiquement en paramètre template. D'un point de vue de méta-programmation template, en créant un pointeur intelligent, on écrit d'abord une classe similaire à la classe template en remplaçant son paramètre par le type de la donnée que l'on va vouloir pointer. std::auto_ptr fonctionne de cette manière, boost::shared_ptr également (et d'autres).

Un détail surprend souvent les débutants : le paramètre template n'est pas le type du pointeur "nu" que l'on aurait eu à la place. En se disant qu'ils veulent un pointeur intelligent sur un int par exemple, ils (ou peut-être vous si vous êtes débutant) écrivent intuitivement "pointeur_intelligent < int* >", pourtant ceci n'aura pas l'effet attendu. En effet, il ne faut pas considérer un pointeur intelligent comme un objet encapsulant un pointeur, mais plutot comme un vrai pointeur dont on précise le type de la donnée pointée. Dans ce cas, "pointeur_intelligent < int >" est donc correct.

Un autre détail d'ordre "C++" : l'allocation dynamique est tout de même faite en-dehors du pointeur intelligent ; ce n'est pas lui qui s'en charge, il se contente de récupérer un pointeur sur une zone déjà allouée ou NULL.

Première exemple : std::auto_ptr

La STL fourni donc un pointeur intelligent standard : std::auto_ptr, défini dans le fichier <memory>. Il s'agit donc d'une classe template qui prend en paramètre le type traité. Le constructeur de std::auto_ptr prend un pointeur "nu" en paramètre qui pointera sur une zone que l'on aura pris soin d'allouer. Généralement, on retrouve donc l'allocation dynamique directement en tant qu'expression dans les paramètres du constructeur lors de la déclaration du pointeur intelligent.

std::auto_ptr considère qu'il manipule une zone allouée avec l'opérateur new : ne vous permettez donc pas de lui envoyer une zone allouée avec new[] car il tentera tôt ou tard d'y appliquer l'opérateur delete. Il y a d'autres pointeurs intelligents qui sont faits pour cela, en particulier un que je mentionnerai plus loin. Parce que l'allocation à proprement parler n'est pas gérée par la classe, cette dernière n'est pas susceptible de lancer une exception (une exception std::bad_alloc ou encore une exception lancée depuis le constructeur de l'objet alloué dynamiquement aurait été possible sinon).

std::auto_ptr surcharge les opérateurs classiques que l'on peut appliquer aux pointeurs nus : operator*, operator->, operator=, etc. mais aussi la conversion vers d'autres types de pointeurs. La sémantique d'un vrai pointeur est donc gardée sauf qu'on a ajouté de l' "intelligence". Exemple sans intérêt :

#include <memory>

// du code...

std::auto_ptr < std::string > ptr(new std::string("hello"));
std::cout << *ptr << std::endl; // affiche hello
std::cout << ptr -> at(1) << std::endl; // affiche e
*ptr += " world !";

// du code...

Et on n'écrira aucune libération de mémoire, c'est entièrement l'objet ptr qui s'en occupera à sa destruction. Tout est bien beau, mais quel est alors le problème de std::auto_ptr que j'ai mentionné plus haut ? Pour le comprendre, il faut comprendre la stratégie de std::auto_ptr pour gérer les copies. En effet, la plupart des problèmes que l'on rencontre avec les pointeurs nus existent parce qu'il y a eu copie de pointeur. C'est donc principalement au niveau des copies de pointeurs intelligents qu'il faut ajouter justement de l' "intelligence". Par exemple, si l'on part du principe que la libération de mémoire est faite à coup sûr dans le destructeur, est-il seulement raisonnable de laisser la possibilité de copier un pointeur intelligent ? Si l'on est aussi restrictif, on garde beaucoup de problèmes qu'on souhaiterait pourtant régler.

Plusieurs approches sont possibles pour résoudre le problème, mais l'idée de base c'est d'adopter un certain point de vue sur les copies. std::auto_ptr considère que chaque pointeur intelligent est un propriétaire unique d'une zone allouée dynamiquement. Ce type considère les copies simplement comme un changement de propriétaire. On ne libère la mémoire dans le destructeur que si l'objet en question est propriétaire de quelque chose, ce qu'il n'est donc plus forcément (ou n'a jamais été si le pointeur n'a jamais été initialisé). Le pointeur copié est donc en quelque sort "destitué" de son titre de propriétaire et ne libèrera rien, ça sera à la copie de s'en charger.

Le problème de std::auto_ptr est donc là : on est certain que la mémoire allouée sera libérée tôt ou tard, mais peut-être ... trop tôt. Prenez par exemple le code suivant :

std::auto_ptr < int > ptr(new int(42));
f(ptr);
*ptr = 36;

f() est une fonction qui prend en paramètre un pointeur intelligent std::auto_ptr. En supposant qu'il n'y a pas passage par référence, il y aura une copie lors de l'envoi de ptr à cette fonction. Autrement dit, c'est le pointeur intelligent que l'on retrouvera dans f() qui sera le propriétaire de l'entier alloué à la ligne 1. Conséquence : ptr n'en est plus le propriétaire et n'y a donc plus droit d'accès. La ligne 3 n'a donc aucun sens, d'autant plus qu'au moment où l'on exécute cette instruction, le pointeur intelligent propriétaire interne à f() aura déjà libéré l'entier. Si l'on est pas à l'aise avec l'approche de std::auto_ptr, il vaut donc mieux éviter de l'utiliser pour ne pas tomber dans de pièges subtils de ce genre.

Deuxième exemple : boost::shared_ptr

Comment résoudre ce problème de copie et de propriétaire unique ? Une astuce possible serait d'autoriser les propriétaires multiples et à la fois de garantir qu'aucun de ces propriétaires ne sera invalidés mais que la zone allouée soit tout de même libérée à coup sûr.

L'idée est de trouver une autre approche sur la copie de pointeur intelligent : déjà on ne parlera plus de propriétaire mais de référence sur une zone allouée, mais en plus une copie ne sera plus un changement de propriétaire mais la création d'une nouvelle référence sur la zone allouée. Par opposition, la destruction d'un pointeur intelligent est la suppression d'une référence sur cette zone. Cette approche est très simple et pourtant très puissante : en gardant trace quelque part du nombre de références sur chaque zone allouée dynamiquement, on peut aisément déterminer quand libérer la mémoire sans qu'il y ait d'éventuelles retombées plus tard.

Concrètement, on teste dans le destructeur d'un pointeur intelligent s'il est le dernier objet à pointer sur la zone qu'il réfère. Si oui, il peut tranquillement la libérer (et même doit, car normalement personne ne pourra le faire après lui), sinon, il se contente de décrémenter le nombre de références sur cette zone, puisque lui sera alors en moins.

Ingénieux ? Oui, très. Avec cette approche, on résout non seulement tous les problèmes des pointeurs "nus" mais également le principal problème que l'on reproche à std::auto_ptr. C'est en fait un principe répandu et très connu appelé de manière transparente : "comptage de référence". Ce principe possède néanmoins le défaut de devoir stocker en plus de l'objet RAII le nombre de références sur chaque zone allouée dynamiquement et contrôlée par un tel système.

Cette fois-ci, c'est Boost qui nous propose une classe pour gérer un tel pointeur intelligent : boost::shared_ptr ("pointeur partagé"). En réalité, Boost propose plusieurs pointeurs intelligents comme dit, chacun servant à un usage spécifique (boost::weak_ptr, boost::scoped_ptr, boost::unique_ptr, etc.). Cependant, boost::shared_ptr est celui que l'on peut utiliser le plus généralement. Pour ce faire, il faut déjà commencer par inclure le fichier boost/shared_ptr.hpp ou boost/smart_ptr/shared_ptr.hpp. Pour ce qui est du reste, un pointeur intelligent de type boost::shared_ptr s'utilise avec la même sémantique qu'un objet std::auto_ptr.

boost::shared_ptr < int > ptr(new int(42));
f(ptr);
*ptr = 36; // affectation cohérente et autorisée

Boost pense à tout. Non seulement c'est simple, mais en plus maintenant on peut dormir sur nos deux oreilles. Si l'on veut appliquer la même stratégie pour gérer de manière sécurisée des pointeurs sur des zones allouées avec new[], on peut par exemple envisager d'utiliser boost::shared_array. Cependant, ne vous précipitez pas : pour les tableaux dynamiques, on préfèrera si possible toujours std::vector ou plus spécifiquement std::string.

Discussion

Les pools de mémoire

Connaissez-vous les pools de mémoire ? Il s'agit d'un système pour gérer les allocations et les libérations de mémoire. En gros, plutôt que de confier ce travail à l'OS, on peut le laisser à une entité qui pourra peut-être mieux s'en charger selon ce que l'on veut faire. Un pool de mémoire se présente comme un objet auquel on peut demander de la mémoire (en précisant un nombre de bytes ou un type) et plus tard la libération de manière correcte de cette mémoire. On peut lui demander de la mémoire pour de nombreux objets.

Il y a deux types de pools de mémoire : les pools objet et les pools singleton. Les pools objet libèrent à leur propre destruction toutes les allocations de mémoire qui ont été faites avec. Les pools singleton sont presque similaires, sauf qu'ils libèrent la mémoire juste avant la fermeture du programme et pas avant. std::allocator par exemple, que vous avez peut-être déjà vu, est un type de pool.

Différentes raisons peuvent nous pousser à utiliser des pools. Premièrement, on peut très bien imaginer un pool distribuant exclusivement de la mémoire partagée. On peut ainsi construire des objets sur cette mémoire en confiant simplement le travail d'allocation à ce pool. Un autre exemple : on peut également souhaiter n'avoir que des objets contigus en mémoire, ce que permettrait de faire un pool et non une série de new traditionnels. On peut aussi supposer qu'on travaille sur un microcontroleur qui a un mécanisme de gestion de la mémoire très exotique qui ne nous permet pas d'utiliser new.

En C++, un pool se présente généralement comme un objet, instance d'une classe template - s'il faut préciser un type - ou non - si la taille en bytes est passée au constructeur -, qui possède entre-autres une méthode spéciale pour réserver de la place en mémoire (comment et selon quelle logique, c'est géré en interne). On récupère un pointeur sur cette zone mémoire allouée et on s'en sert ordinairement pour faire un placement new (new auquel on précise où construire l'objet). Le placement new n'autorise donc plus new à choisir l'emplacement mémoire, il sert juste à construire véritablement les objets.

Si vous voulez vous procurer une bibliothèque proposant des pools, c'est encore Boost qui vient à notre secours et qui propose Boost.Pool. Celle-ci met à disposition deux types de pool objet : boost::pool et boost::object_pool ; et deux types de pool singleton : boost::singleton_pool et boost::pool_allocator.

Pool ou pointeur intelligent ?

Pourquoi est-ce que j'en parle ici ? Tout simplement parce qu'un pool libère quasiment toujours automatiquement toute la mémoire qu'on lui a demandée (c'est le cas de boost::object_pool, std::allocator, etc.). Autrement dit, les pools de mémoire pourraient être, parmi tant d'autres applications, une alternative intéressante aux pointeurs intelligents.

Il y a plusieurs avantages à utiliser les pools : déjà, on peut manipuler des pointeurs nus sans problème (vu qu'ils pointent sur une zone allouée avec un pool). Visuellement, c'est un peu plus agréable qu'une flopée de boost::shared_ptr un peu partout. On peut même envisager d'utiliser des références, mais c'est du détail. De plus, on peut appliquer toutes les copies que l'on veut et avoir autant de références sur un même objet qu'on le souhaite sans se poser plus de questions : toute la mémoire est libérée à la destruction du pool ou au moins à la fermeture du programme. Si par exemple on a un pool dont la durée de vie des objets alloués est égale au programme, on peut très paisiblement manipuler ces objets via des pointeurs nus comme on le souhaite et où l'on veut en étant toujours sûr que ce système n'invalidera à un aucun moment ces pointeurs mais que toute la mémoire sera libérée à coup sûr.

Ce dernier avantage est également le principal défaut des pools par rapport aux pointeurs intelligents : ils gardent toute la mémoire allouée et ne la libèrent que d'un coup (on peut par contre demander explicitement la destruction d'une zone bien spécifique, mais ça ne rentre pas dans nos propos ici vu qu'on aimerait éviter d'avoir à faire cela). Souvent, on ne trouve donc plus aucune utilité à garder un objet en mémoire, mais il y restera quand même encore un certain temps.

Les pointeurs intelligents n'ont pas ce problème, vu qu'ils n'adoptent pas une vision globale mais plutôt focalisée astucieusement sur chaque zone. Si notre but est de gérer de manière sécurisée la mémoire et non d'utiliser quelque chose de plus spécifique aux pools, il peut donc être intéressant de peser le pour et le contre et de choisir intelligemment entre pool et pointeur RAII.

Ce cours n'est qu'une introduction aux concepts présentés : intéressez-vous plus en détail aux pools et aux différents pointeurs intelligents que proposent par exemple Boost. Et surtout, n'en restez pas à la théorie, écrivez vous-même du code, testez et constatez les choses. Je vous conseille même d'essayer de réécrire certains classes comme boost::shared_ptr, c'est un très bon exercice et c'est toujours une expérience stimulante.

Les notions que j'ai présentées ne sont pas aussi rigides qu'il n'y parait. On peut par exemple très bien utiliser std::malloc() en C++ et ainsi réserver de la mémoire sur laquelle on va pouvoir faire en toute légalité ... un placement new. Ou encore, le problème de std::auto_ptr peut se régler autrement que par le comptage de référence : on peut s'apercevoir qu'une copie d'un tel pointeur est en fait un déplacement déguisé. boost::unique_ptr par exemple résout le problème de auto_ptr en retirant ce "déguisement" et en interdisant les copies. C'est une approche différente de celle de boost::shared_ptr conçue pour une utilisation plus spécifique mais qui résout tout de même le problème d'auto_ptr.

Un grand merci à Thunderseb pour sa relecture.
shareman

PS : Renseignez-vous d'avantage ! Quelques liens :
Présentation de std::auto_ptr et boost::unique_ptr ;
Fac C++ de développez.com : Gestion dynamique de la mémoire ;
Boost et les pointeurs intelligents ;
Wikipédia (en) : pool de mémoire ;
Wikipédia (en) : pointeur intelligent ;
Cplusplus.com : std::auto_ptr.

How courses work

  • 1

    You have now access to the course contents and exercises.

  • 2

    You will advance in the course week by week. Each week, you will work on one part of the course.

  • !

    Exercises must be completed within one week. The completion deadline will be announced at the start of each new part in the course. You must complete the exercises to get your certificate of achievement.

  • 3

    At the end of the course, you will get an email with your results. You will also get a certificate of achievement if you are a

Example of certificate of achievement
Example of certificate of achievement