IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)



Comment allouer de la mémoire ?
auteur : LFE
En C, l'allocation dynamique de mémoire se faisait avec la fonction malloc(). En C++, l'allocation de mémoire se fait avec l'opérateur new.

A noter que la fonction malloc() fonctionne toujours en C++ comme en C.

lien : Comment libérer de la mémoire ?

Que se passe-t-il si new ne parvient pas à allouer la mémoire demandée ?
auteur : LFE
malloc(), dans le cas d'impossiblité d'allouer de la mémoire renvoie NULL. En C++, un des avantages de l'utilisation de new et que plutôt que de renvoyer NULL, une exception est lancée.


Pourquoi utiliser new plutot que malloc ?
auteur : LFE
Sur les types des base (int, char, float, ...) l'intérêt n'est pas énorme. Par contre, sur les classes, plutôt que simplement allouer la place mémoire pour stocker l'objet, il y a un appel au constructeur qui se fait et qui permet donc d'initialiser correctement l'objet.

De même, delete appelle le destructeur de la classe, alors que free() ne le fait pas.

MaClasse *BonObjet, *MauvaisObjet; MauvaisObject = malloc(sizeof(MaClasse)); // alloue de la mémoire mais n'appelle pas le constructeur BonObject = new MaClasse; // alloue de la mémoire et appelle le constructeur.

Comment libérer de la mémoire ?
auteur : LFE
En C, la libération de mémoire se fait avec la fonction free(). En C++, la libération d'un pointeur se fait avec l'opérateur delete.

A noter que la fonction free() fonctionne toujours en C++ comme en C.

Le delete fonctionne de la façon suivante : il appelle (implicitement) le destructeur de la classe et puis libère le pointeur.

lien : Comment allouer de la mémoire ?

Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?
auteur : LFE
Il ne se passe rien du tout. On peut considérer qu'un delete sur un pointeur NULL est purement et simplement ignoré, ce qui permet d'éviter de devoir faire ce contrôle.


Puis-je utiliser free() pour libérer un pointeur alloué par new ?
auteur : LFE
La réponse est négative. Un pointeur alloué par new doit être libéré par un delete et un pointeur alloué par malloc() doit être libéré par free().

Il est tout à fait possible que ce type d'allocation/libération fonctionne parfaitement sur un compilateur mais donnera des résultats tout à fait imprévisibles sur un autre.


Comment allouer dynamiquement un tableau ?
auteur : LFE
Allouer un tableau dynamiquement en C++ se fait grâce à l'opérateur new []. Pour la même raison que pour allouer une classe, il faut utiliser new [] plutôt que malloc(N*sizeof(...)).

int *mesObjets = new int[10]; // alloue un tableau de 10 objets et appelle les constructeurs
lien : Comment libérer un tableau alloué dynamiquement ?

Comment libérer un tableau alloué dynamiquement ?
auteur : LFE
Libérer un tableau alloué dynamiquement en C++ se fait grâce à l'opérateur delete []. Pour la même raison que pour allouer une classe, il faut utiliser delete [] plutôt que free(...).

Une erreur commune est d'oublier les [] après le delete, ce qui n'est pas signalé ni à la compilation, ni à l'exécution, mais qui mène à des résultats erronés.

delete [] mesObjets ; // libère le tableau mesObjets
A noter qu'il n'y a pas besoin de spécifier le nombre d'élements à libérer. Cette information a été stockée au moment de l'allocation du tableau.

lien : Comment allouer dynamiquement un tableau ?

Comment allouer dynamiquement un tableau à plusieurs dimensions ?
auteur : Bob
int** Tab; int i; Tab=new int*[10]; for(i=0; i<10; i++) { Tab[i]=new int[10]; }

Comment libérer un tableau à plusieurs dimensions alloué dynamiquement ?
auteur : Bob
int** Tab; int i; for(i=0; i<10; i++) { delete [] Tab[i]; } delete []Tab;

Peut-on déréférencer un pointeur NULL ?
auteur : LFE
La réponse est NON.
NULL étant une adresse non valide, *NULL donne une référence impossible.


Est-il possible de forcer new à allouer la mémoire à une adresse précise ?
auteur : Marshall Cline
Oui. La bonne nouvelle est que ces "pools de mémoire" sont utiles dans un certain nombre de situations. La mauvaise nouvelle est qu'il va falloir descendre dans le "comment cela fonctionne" avant de voir comment on l'utilise. SI vous ne savez pas comment fonctionnent les "pools de mémoire", ce sera chose réglée bientôt.

Avant tout, il faut savoir qu'un allocateur de mémoire est supposé retourner une zone de mémoire non initialisée, il n'est pas supposé créer des objets. En particulier, l'allocateur de mémoire n'est pas supposé mettre à jour le pointeur virtuel ou n'importe quelle autre partie de l'objet, étant donné que c'est le traviale du constructeur qui est éxécuté juste après l'allocation de la mémoire. En démarrant avec une simple fonction d'allocation de mémoire, allocate(), nous utilisons placement new pour construire un objet dans cette mémoire. En d'autres mots, ce qui suit est moralement équivalent à new Foo() :

void* raw = allocate(sizeof(Foo)); // line 1 Foo* p = new(raw) Foo(); // line 2
En supposant que l'on ait utilisé placement new et que l'on ait survécu au code précédent, l'étape suivante est de transformer l'allocateur de mémoire en un objet. Ce type d'objet est appelé un pool mémoire. Cela permet aux utilisateurs d'avoir plusieurs pools à partir des quels la mémoire peut être allouée. Chacun de ces pools mémoire allouera une grande quantité de mémoire en utilisant un appel système spécifique (mémoire partagée, mémoire persistente, etc ....) et le distribuera en petites quantités à la demande. Notre pool mémoire ressemblera à quelque chose de ce type :

class Pool { public: void* alloc(size_t nbytes); void dealloc(void* p); private: ...data members used in your pool object... }; void* Pool::alloc(size_t nbytes) { ...your algorithm goes here... } void Pool::dealloc(void* p) { ...your algorithm goes here... }
Maintenant, l'utilisateur devrait pouvoir obtenir un Pool (appelé pool), à partir du quel il pourra allouer des objets de la façon suivante :

Pool pool; ... void* raw = pool.alloc(sizeof(Foo)); Foo* p = new(raw) Foo();
ou encore :

Foo* p = new(pool.alloc(sizeof(Foo))) Foo();
La raison pour laquelle il serait bon de transformer Pool en une classe est que cela permet à l'utilisateur de créer N pools mémoire différents, plutôt que d'avoir un gros pool partagé par tous les utilisateurs. Cela permet aux utilisateurs de faire un tas de choses plus ou moins drôles. Par exemple, si nous avions un appel système qui permet d'allouer une énorme quantité de mémoire et puis disparait, la totalité de la mémoire pourrait être allouée dans un pool, et ensuite ne faire aucun delete des allocations faites dans ce pool, pour finalement libérer la totalité du pool en une fois. Ou il serait possible de créer une zone de mémoire partagée (où le système d'exploitation procure de la mémoire partagée entre différents processus) et que ce pool alloue des morceaux de mémoire partagée plutôt que de la mémoire locale au processsus.
La plupart des systèmes supportent une fonction alloca() qui alloue un bloc de mémoire sur la pile, plutôt que dans le heap. Bien entendu, ce bloc de mémoire est libéré à la fin de la fonction, faisant disparître le besoin de faire des delete explicites. Quelqu'un pourrait utiliser alloca() pour attribuer au Pool sa mémoire, et que toutes les petites allocations dans ce pool agiraient comme si elles etaient faites sur la pile : elles disparaîtraient à la fin de la fonction. Bien sûr, les destructeurs ne seraient pas appelés dans n'importe lequel de ces cas, et si celui-ci devait faire des choses non triviales, il vous serait impossible d'utiliser ces techniques, mais dans le cas ou le distructeur ne fait que désaloouer la mémoire, ce genre de techniques peuvent être utiles.

Maintenant que l'on a inclus les quelques lignes de code nécessaires à l'allocation dans la classe Pool, l'étape suivante est de changer la syntaxe d'allocation des objets. Le but est transformer une allocation au format inhabituel (new(pool.alloc(sizeof(Foo)))) en quelque chose de tout à fait classique (new(pool)). Pour y arriver, il faut ajouter les les 2 lignes suivantes à la définition de la classe Pool

inline void* operator new(size_t nbytes, Pool& pool) { return pool.alloc(nbytes); }
Maintenant, lorsque le compilateur rencontrera une instruction

new(pool) Foo()
l'opérateur new que l'on vient de définir et passera sizeof(Foo) et pool en tant que paramètres, et la seule fonction qui manipulera le pool sera ce nouvel opérateur new.

Passons maintenant à la destruction de l'objet Foo. Il est à noter que l'apporche brutale qui est parfois utilisée avec placement new est d'appeler explicitement le destrucuteur et d'ensuite désallouer la mémoire :

void sample(Pool& pool) { Foo* p = new(pool) Foo(); ... p->~Foo(); // explicitly call dtor pool.dealloc(p); // explicitly release the memory }
Ce code présente plusieurs problèmes, mais qui peuvent tous être réglés.

Il y aura une perte de mémoire si le constructeur lance une exception
La syntaxe de destruction/désallocation n'est pas conforme à ce que les programmeurs ont l'habitude de voir, ce qui va sûrement les perturber fortement.

L'utilisateur doit se rappeler d'une façon ou d'une autre des associations pool/objet.
Etant donné que le code qui alloue est souvent situé dans une autre fonction que celle qui libère, le programmeur devra manipuler deux pointeurs (un pour la classe et un pour le pool), ce qui peut devenir rapidement indigeste (par exemple, un tableau d'objet Foo qui seraient alloués dans des pools différents)

Nous allons régler ces problèmes.

Problène n° 1 : la fuite mémoire
Quand on utilise l'opérateur new habituel, le compilateur génère un bout de code particulier pour gérer le cas ou le constructeur lance une exception. Ce code ressemble à ceci :

principe
Foo* p; // ne pas intercepter les exceptions lancées par l'allocateur void* raw = operator new(sizeof(Foo)); // intercepter toute excpetion lancée par le constructeur try { p = new(raw) Foo(); // appel du constructeur avec 'raw' comme pointeur 'this' } catch (...) { // le constructeur a lancé une exception operator delete(raw); throw; // relancer l'exception du constructeur }
Le point à remarquer est que le compilateur libère la mémoire si le constructeur lance une excpetion. Mais dans le cas du "nex avec paramètres" (appelé communément "new avec placement"), le compilateur ne sait pas quoi faire si une excpetion est lancée, il ne fiat donc rien.

principe
void* raw = operator new(sizeof(Foo), pool); // cette fonction renvoie simplement "pool.alloc(sizeof(Foo))" Foo* p = new(raw) Foo(); // si la ligne précédente provoque une exception, pool.dealloc(raw) n'est PAS appelé
Le but est donc de faire faire au compilateur quelque chose de semblable à ce qu'il fait avec l'opérateur new global. Hereusement, c'est simple : quand le compilateur rencontre

new(pool) Foo()
il cherche un opérateur delete correspondant. Si il en trouve un, il fait un wrapping équivalent à celui de l'appel du constructeur dans un bloc try/catch. Nous devons juste fournir un opérateur delete avec la signature suivante. Aattention de ne pas se tromper ici, car si le second paramètre a un type différent de celui de l'opérateur new, le compilateur n'émettra aucun message, il ignorera simplement le bloc try/catch qaund l'utilisateur effectuera l'allocation.

void operator delete(void* p, Pool& pool) { pool.dealloc(p); }
Maintenant, le compilateur intégrera automatiquement les appels au constructeur dans un bloc try/catch.

principe
Foo* p; // ne pas intercepter les exceptions lancées par l'allocateur void* raw = operator new(sizeof(Foo), pool); // le code précédent renvoie simplement "pool.alloc(sizeof(Foo))" // intercepter toute exception lancée par le constructeur try { p = new(raw) Foo(); // appel du constructeur avec raw en tant que this } catch (...) { // le constructeur lance une excepton operator delete(raw, pool); // la ligne "magique" throw; // relance l'exception }
En d'autres mots, l'ajout de l'opérateur delete avec la signature ad hoc règle automatiquement le problème de fuite de mémoire.

Problème 2 : se souvenir des associations objet/pool
Ce problème est réglé par l'ajout de quelques lignes de code à un endroit. En d'autres mors, nous allons ajouter ces lignes de code à un endroit (le fichier header du pool), ce qui va simplifier par la même occasion un certain nombres d'appels.

L'idée est d'associer de manière implicite un Pool* avec chaque allocation. Le Pool* associé à l'allocateur global pourrait être NULL, mais conceptuellement on peut dire que chaque allocation a un Pool* associé.

Ensuite, nous remplacons l'operateur delete global de façon à ce qu'il examine le Pool* associé, et si il est non NULL, il appelera la fonction de libération associée. Par exemple, si le désallocateur normal utilisait free(), le remplacement pour l'opérateur delete global ressemblerait à quelque chose comme ceci :

void operator delete(void* p) { if (p != NULL) { Pool* pool = /* somehow get the associated 'Pool*' */; if (pool == null) free(p); else pool->dealloc(p); } }
Si free() était le désallocateur normal, l'approche la plus sûre est de remplacer aussi l'opérateur new par quelque chose qui utilise malloc(). Le code remplacant l'operateur global new ressemblerait à quelque chose comme ce qui suit :

void* operator new(size_t nbytes) { if (nbytes == 0) nbytes = 1; // chaque alloc obtient donc une adresse différente void* raw = malloc(nbytes); ... associer le Pool* à Null a 'raw'... return raw; }
Le dernier problème est d'associer un Pool* à une allocation. Une approche, utilisée dans au moins un prodtui comemrcial, est d'utiliser un

<void*,Pool*>
En d'autres mots, il suffit de construire une table associative ou les clés sont les pointeurs alloués et les valeurs sont les Pool* associés. Pour différentes raisons, il est essentiel que les paires clé/valeur soient insérées à partir dans l'opérateur new. En particulier, il ne faut pas insérer une paire de clé/valeur à partir de l'opérateur new global. La raison est la suivante, faire cela créerait un problème circulaire : étant donné que std::map utilise plus que probablement l'opérateur new global, à chaque insertion d'un élément serait appelé, pour insérer une nouvelle entrée, ce qui mène directement à une récursion infinie.

Même si cette technique exige une recherche dans le std::map a chaque libération, elle semble avoir des performances suffisantes, du moins dans la plupart des cas.

Une autre approche , plus rapide, mais qui peut utiliser plus de mémoire et est un peu plus complexe est de specifier un Pool* juste avant toutes les allocations. Par exemple, si nbytes vaut 24, c'est-à-dire que l'appelant veut allouer 24 bytes, nous allouerions 28 bytes (ou 32, si la machine aligne les doubles ou les long long sur 8 bytes), specifierions le Pool* dans les 4 premiers bytes, et retournerions le pointeur avec un décalage de 4 bytes (ou 8) suivant l'architecture). pour la libération du pointeur, l'opérateur delete libérerait la mémoire en tenant compte du décalage de 4 (ou 8) bytes. Si Pool* est NULL, nous utilisons free(), sinon pool->dealloc(). Le paramètre passé à free() et à pool->dealloc() serait le pointeur décrémente de 4 ou 8 bytes du paramètre original. Si on travailler avec un alignement de 4 bytes, le code ressemblerait à ceci :

void* operator new(size_t nbytes) { if (nbytes == 0) nbytes = 1; // so all alloc's get a distinct address void* ans = malloc(nbytes + 4); // overallocate by 4 bytes *(Pool**)ans = NULL; // use NULL in the global new return (char*)ans + 4; // don't let users see the Pool* } void* operator new(size_t nbytes, Pool& pool) { if (nbytes == 0) nbytes = 1; // so all alloc's get a distinct address void* ans = pool.alloc(nbytes + 4); // overallocate by 4 bytes *(Pool**)ans = &pool; // put the Pool* here return (char*)ans + 4; // don't let users see the Pool* } void operator delete(void* p) { if (p != NULL) { p = (char*)p - 4; // back off to the Pool* Pool* pool = *(Pool**)p; if (pool == null) free(p); // note: 4 bytes left of the original p else pool->dealloc(p); // note: 4 bytes left of the original p } }
Naturellement, ces derniers paragraphes sont uniquement valables si on peut modifier l'opérateur new global, ainsi que delete.
S'il n'est pas possible de changer le comportement de ces opérateurs globaux, les 3/4 du texte qui précède restent valables.



Ce document issu de http://www.developpez.com est soumis à la licence GNU FDL traduit en français ici.
Permission vous est donnée de distribuer, modifier des copies de cette page tant que cette note apparaît clairement.
Certaines parties de ce document sont sous copyright Marshall Cline