Il y a une différence importante entre les deux.
Tout ce qui n'est pas alloué avec new
se comporte comme les types de valeur en C # (et les gens disent souvent que ces objets sont alloués sur la pile, ce qui est probablement le cas le plus courant / évident, mais pas toujours vrai. Plus précisément, les objets alloués sans utiliser new
ont un stockage automatique duration
Tout alloué avec new
est alloué sur le tas, et un pointeur est retourné, exactement comme les types de référence en C #.
Tout ce qui est alloué sur la pile doit avoir une taille constante, déterminée au moment de la compilation (le compilateur doit définir correctement le pointeur de pile, ou si l'objet est membre d'une autre classe, il doit ajuster la taille de cette autre classe) . C'est pourquoi les tableaux en C # sont des types de référence. Ils doivent l'être, car avec les types de référence, nous pouvons décider au moment de l'exécution de la quantité de mémoire à demander. Et la même chose s'applique ici. Seuls les tableaux de taille constante (une taille qui peut être déterminée au moment de la compilation) peuvent être alloués avec une durée de stockage automatique (sur la pile). Les tableaux de taille dynamique doivent être alloués sur le tas, en appelant new
.
(Et c'est là que toute similitude avec C # s'arrête)
Maintenant, tout ce qui est alloué sur la pile a une durée de stockage "automatique" (vous pouvez en fait déclarer une variable comme auto
, mais c'est la valeur par défaut si aucun autre type de stockage n'est spécifié, donc le mot-clé n'est pas vraiment utilisé dans la pratique, mais c'est là qu'il vient de)
La durée de stockage automatique signifie exactement à quoi cela ressemble, la durée de la variable est gérée automatiquement. En revanche, tout ce qui est alloué sur le tas doit être supprimé manuellement par vous. Voici un exemple:
void foo() {
bar b;
bar* b2 = new bar();
}
Cette fonction crée trois valeurs à considérer:
Sur la ligne 1, il déclare une variable b
de type bar
sur la pile (durée automatique).
Sur la ligne 2, il déclare un bar
pointeur b2
sur la pile (durée automatique), et appelle new, allouant un bar
objet sur le tas. (durée dynamique)
Lorsque la fonction revient, les événements suivants se produisent: Premièrement, b2
sort du domaine d'application (l'ordre de destruction est toujours opposé à l'ordre de construction). Mais ce b2
n'est qu'un pointeur, donc rien ne se passe, la mémoire qu'il occupe est simplement libérée. Et surtout, la mémoire vers laquelle il pointe (l' bar
instance sur le tas) n'est PAS touchée. Seul le pointeur est libéré, car seul le pointeur a une durée automatique. Deuxièmement, b
sort du domaine, donc comme il a une durée automatique, son destructeur est appelé et la mémoire est libérée.
Et l' bar
instance sur le tas? C'est probablement toujours là. Personne n'a pris la peine de le supprimer, nous avons donc perdu de la mémoire.
De cet exemple, nous pouvons voir que tout ce qui a une durée automatique est garanti d'avoir son destructeur appelé quand il sort du domaine. C'est utile. Mais tout ce qui est alloué sur le tas dure aussi longtemps que nous en avons besoin et peut être dimensionné dynamiquement, comme dans le cas des tableaux. C'est également utile. Nous pouvons l'utiliser pour gérer nos allocations de mémoire. Et si la classe Foo allouait de la mémoire sur le tas dans son constructeur et supprimait cette mémoire dans son destructeur. Ensuite, nous pourrions obtenir le meilleur des deux mondes, des allocations de mémoire sûres qui sont garanties d'être à nouveau libérées, mais sans les limitations de forcer tout à être sur la pile.
Et c'est à peu près exactement comment fonctionne la plupart du code C ++. Regardez std::vector
par exemple la bibliothèque standard . Cela est généralement alloué sur la pile, mais peut être dimensionné et redimensionné dynamiquement. Et il le fait en allouant en interne de la mémoire sur le tas si nécessaire. L'utilisateur de la classe ne voit jamais cela, il n'y a donc aucun risque de fuite de mémoire ou d'oubli de nettoyer ce que vous avez alloué.
Ce principe est appelé RAII (Resource Acquisition is Initialization), et il peut être étendu à toute ressource qui doit être acquise et libérée. (sockets réseau, fichiers, connexions à la base de données, verrous de synchronisation). Tous peuvent être acquis dans le constructeur et libérés dans le destructeur, vous avez donc la garantie que toutes les ressources que vous acquérez seront à nouveau libérées.
En règle générale, n'utilisez jamais new / delete directement à partir de votre code de haut niveau. Enveloppez-le toujours dans une classe qui peut gérer la mémoire pour vous et qui garantira qu'elle sera à nouveau libérée. (Oui, il peut y avoir des exceptions à cette règle. En particulier, les pointeurs intelligents vous obligent à appeler new
directement et à passer le pointeur à son constructeur, qui prend alors le relais et s'assure qu'il delete
est appelé correctement. Mais c'est toujours une règle d'or très importante )