Il convient de noter que, dans le cas de C ++, il est souvent mal compris que "vous devez gérer manuellement la mémoire". En fait, votre code ne gère généralement pas la mémoire.
Objets de taille fixe (avec durée de vie)
Dans la grande majorité des cas, lorsque vous avez besoin d'un objet, celui-ci aura une durée de vie définie dans votre programme et sera créé sur la pile. Cela fonctionne pour tous les types de données primitifs intégrés, mais aussi pour les instances de classes et de structures:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
Les objets empilés sont automatiquement supprimés à la fin de la fonction. En Java, les objets sont toujours créés sur le tas et doivent donc être supprimés par un mécanisme tel que le garbage collection. Ceci n'est pas un problème pour les objets de pile.
Objets gérant des données dynamiques (avec durée de vie)
L'utilisation de l'espace sur la pile fonctionne pour les objets de taille fixe. Lorsque vous avez besoin d'une quantité variable d'espace, telle qu'un tableau, une autre approche est utilisée: la liste est encapsulée dans un objet de taille fixe qui gère la mémoire dynamique à votre place. Cela fonctionne car les objets peuvent avoir une fonction de nettoyage spéciale, le destructeur. Il est garanti d'être appelé lorsque l'objet sort du domaine et fait l'inverse du constructeur:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
Il n'y a pas du tout de gestion de mémoire dans le code où la mémoire est utilisée. La seule chose dont nous devons nous assurer est que l'objet que nous avons écrit possède un destructeur approprié. Peu importe la façon dont nous laissons les choses en place listTest
, que ce soit via une exception ou simplement en y retournant, le destructeur ~MyList()
sera appelé et nous n’aurons pas besoin de gérer de mémoire.
(Je pense que c'est une décision de conception amusante d'utiliser l' opérateur binaire NOT~
, pour indiquer le destructeur. Lorsqu'il est utilisé sur des nombres, il inverse les bits; par analogie, cela indique que ce que le constructeur a fait est inversé.)
Fondamentalement, tous les objets C ++ qui ont besoin de mémoire dynamique utilisent cette encapsulation. Cela a été appelé RAII ("l’acquisition des ressources est une initialisation"), ce qui est assez étrange pour exprimer la simple idée que les objets se soucient de leurs propres contenus; ce qu'ils acquièrent, c'est à eux de les nettoyer.
Objets polymorphes et durée de vie au-delà de la portée
Ces deux cas concernaient une mémoire dont la durée de vie est clairement définie: la durée de vie est identique à la portée. Si nous ne voulons pas qu'un objet expire lorsque nous quittons la portée, il existe un troisième mécanisme qui peut gérer la mémoire pour nous: un pointeur intelligent. Les pointeurs intelligents sont également utilisés lorsque vous avez des instances d'objets dont le type varie au moment de l'exécution, mais qui ont une interface ou une classe de base commune:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
Il existe un autre type de pointeur intelligent std::shared_ptr
permettant de partager des objets entre plusieurs clients. Ils ne suppriment leur objet contenu que lorsque le dernier client est hors de portée. Ils peuvent donc être utilisés dans des situations dans lesquelles on ignore totalement le nombre de clients et la durée d'utilisation de l'objet.
En résumé, nous voyons que vous ne faites pas vraiment de gestion de mémoire manuelle. Tout est encapsulé et est ensuite pris en charge au moyen d'une gestion de la mémoire entièrement automatique et basée sur la portée. Dans les cas où cela ne suffit pas, des pointeurs intelligents sont utilisés pour encapsuler la mémoire brute.
Il est considéré comme une très mauvaise pratique d’utiliser des pointeurs bruts en tant que propriétaires de ressources n’importe où dans le code C ++, des allocations brutes en dehors des constructeurs et des delete
appels bruts en dehors des destructeurs, car ils sont presque impossibles à gérer en cas d’exception et généralement difficiles à utiliser en toute sécurité.
Le meilleur: cela fonctionne pour tous les types de ressources
L’un des principaux avantages de RAII est qu’il ne se limite pas à la mémoire. Il fournit en fait un moyen très naturel de gérer des ressources telles que des fichiers et des sockets (ouverture / fermeture) et des mécanismes de synchronisation tels que des mutex (verrouillage / déverrouillage). Fondamentalement, toutes les ressources pouvant être acquises et devant être libérées sont gérées exactement de la même manière en C ++, et aucune partie de cette gestion n'est laissée à l'utilisateur. Tout cela est encapsulé dans des classes qui acquièrent dans le constructeur et libèrent dans le destructeur.
Par exemple, une fonction verrouillant un mutex est généralement écrite comme ceci en C ++:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
D'autres langues compliquent beaucoup les choses, en vous demandant de le faire manuellement (dans une finally
clause, par exemple ) ou en générant des mécanismes spécialisés qui résolvent ce problème, mais pas de manière particulièrement élégante (généralement plus tard dans la vie, lorsque suffisamment de souffert de la lacune). Ces mécanismes sont try-with-resources en Java et l' instruction using en C #, qui sont tous deux des approximations de la norme RAII de C ++.
Donc, pour résumer, tout ceci était un compte-rendu très superficiel de RAII en C ++, mais j'espère que cela aidera les lecteurs à comprendre que la gestion de la mémoire et même des ressources en C ++ n'est généralement pas "manuelle", mais en réalité essentiellement automatique.