D'accord, je pense que nous sommes tous d'accord pour dire que ce qui se passe avec le code suivant n'est pas défini, en fonction de ce qui est passé,
void deleteForMe(int* pointer)
{
delete[] pointer;
}
Le pointeur pourrait être de toutes sortes de choses différentes, et effectuer ainsi un delete[]
Inconditionnel sur celui-ci est indéfini. Cependant, supposons que nous passions effectivement un pointeur de tableau,
int main()
{
int* arr = new int[5];
deleteForMe(arr);
return 0;
}
Ma question est, dans ce cas où le pointeur est un tableau, qui est-ce qui le sait? Je veux dire, du point de vue du langage/du compilateur, il ne sait pas si arr
est un pointeur sur un tableau ou un pointeur sur un seul int. Heck, il ne sait même pas si arr
a été créé de manière dynamique. Pourtant, si je fais plutôt ce qui suit,
int main()
{
int* num = new int(1);
deleteForMe(num);
return 0;
}
Le système d'exploitation est suffisamment intelligent pour ne supprimer qu'un seul int et ne pas utiliser une sorte de "massacre" en supprimant le reste de la mémoire au-delà de ce point (par opposition à strlen
et à un non - \0
] - chaîne terminée - il continuera jusqu'à ce qu'il atteigne 0).
Alors, à qui appartient-il de se souvenir de ces choses? Le système d'exploitation conserve-t-il un type d'enregistrement en arrière-plan? (Je veux dire, je me rends compte que j'ai commencé ce post en disant que ce qui se passe n'est pas défini, mais le fait est que le scénario de "massacre" ne se produit pas, donc dans le monde pratique quelqu'un se souvient.)
Le compilateur ne sait pas que c'est un tableau, il fait confiance au programmeur. Supprimer un pointeur sur un seul int
avec delete []
Entraînerait un comportement indéfini. Votre deuxième exemple main()
est dangereux, même s'il ne se bloque pas immédiatement.
Le compilateur doit savoir combien d'objets doivent être supprimés d'une manière ou d'une autre. Pour ce faire, il peut en allouer suffisamment pour stocker la taille du tableau. Pour plus de détails, voir C++ Super FAQ .
Une question à laquelle les réponses données jusqu'à présent ne semblent pas répondre: si les bibliothèques d'exécution (pas le système d'exploitation, en réalité) peuvent garder trace du nombre d'éléments contenus dans le tableau, alors pourquoi avons-nous besoin du delete[]
la syntaxe du tout? Pourquoi un seul formulaire delete
ne peut-il pas être utilisé pour gérer toutes les suppressions?
La réponse à cela remonte aux racines du C++ en tant que langage compatible C (qu’il n’essaye plus vraiment de devenir.) La philosophie de Stroustrup était que le programmeur ne devrait pas avoir à payer pour des fonctionnalités qu’il n’utilisait pas. S'ils n'utilisent pas de tableaux, ils ne devraient pas avoir à supporter le coût des tableaux d'objets pour chaque bloc de mémoire alloué.
Autrement dit, si votre code ne fait que
Foo* foo = new Foo;
alors, l'espace mémoire alloué à foo
ne devrait inclure aucune surcharge supplémentaire qui serait nécessaire pour prendre en charge les tableaux de Foo
.
Etant donné que seules les allocations de tableau sont configurées pour transporter les informations de taille de tableau supplémentaire, vous devez ensuite indiquer aux bibliothèques d'exécution de rechercher ces informations lorsque vous supprimez les objets. C'est pourquoi nous devons utiliser
delete[] bar;
au lieu de juste
delete bar;
si bar est un pointeur sur un tableau.
Pour la plupart d'entre nous (y compris moi-même), cette inquiétude à propos de quelques octets de mémoire supplémentaires semble étrange ces jours-ci. Cependant, il existe encore des situations dans lesquelles la sauvegarde de quelques octets (d'un nombre potentiellement très élevé de blocs de mémoire) peut être importante.
Oui, le système d'exploitation garde certaines choses à l'arrière-plan. Par exemple, si vous exécutez
int* num = new int[5];
le système d'exploitation peut allouer 4 octets supplémentaires, stocker la taille de l'allocation dans les 4 premiers octets de la mémoire allouée et renvoyer un pointeur de décalage 1003 stockant la taille de l'allocation). Ensuite, lorsque delete est appelé, il peut examiner 4 octets avant le pointeur qui lui est transmis pour rechercher la taille de l'allocation.
Je suis sûr qu'il existe d'autres moyens de suivre la taille d'une allocation, mais c'est une option.
Ceci est très similaire à this question et contient de nombreux détails que vous recherchez.
Mais il suffit de dire que ce n’est pas à l’OS de s’occuper de tout cela. Ce sont en fait les bibliothèques d'exécution ou le gestionnaire de mémoire sous-jacent qui suivront la taille du tableau. Cela se fait généralement en allouant de la mémoire supplémentaire à l’avance et en stockant la taille du tableau à cet emplacement (la plupart utilisent un nœud principal).
Ceci est visible sur certaines implémentations en exécutant le code suivant
int* pArray = new int[5];
int size = *(pArray-1);
delete
ou delete[]
libérerait probablement à la fois la mémoire allouée (mémoire pointée), mais la grande différence est que delete
sur un tableau n'appellera pas le destructeur de chaque élément du tableau.
Quoi qu'il en soit, mélanger new/new[]
et delete/delete[]
est probablement UB.
Il ne sait pas que c'est un tableau, c'est pourquoi vous devez fournir delete[]
au lieu du vieux delete
habituel.
J'ai eu une question similaire à celle-ci. En C, vous allouez de la mémoire avec malloc () (ou une autre fonction similaire) et vous la supprimez avec free (). Il n'y a qu'un seul malloc (), qui alloue simplement un certain nombre d'octets. Il n'y a qu'un seul free (), qui prend simplement un pointeur comme paramètre.
Alors, comment se fait-il qu'en C, vous puissiez simplement passer le pointeur sur free, alors qu'en C++, vous devez lui indiquer s'il s'agit d'un tableau ou d'une variable unique?
La réponse, j'ai appris, a à voir avec les destructeurs de classe.
Si vous allouez une instance d'une classe MyClass ...
classes = new MyClass[3];
Et supprimez-le avec delete, vous ne pouvez obtenir que le destructeur de la première instance de MyClass appelée. Si vous utilisez delete [], vous pouvez être assuré que le destructeur sera appelé pour toutes les occurrences du tableau.
CECI est la différence importante. Si vous travaillez simplement avec des types standard (par exemple, int), vous ne verrez pas vraiment ce problème. De plus, vous devez vous rappeler que le comportement d'utilisation de delete sur new [] et de suppression [] new est indéfini - il peut ne pas fonctionner de la même manière sur tous les compilateurs/systèmes.
L’UNE des approches pour les compilateurs consiste à allouer un peu plus de mémoire et à stocker le nombre d’éléments dans l’élément head.
Exemple comment cela pourrait être fait: Ici
int* i = new int[4];
le compilateur allouera sizeof (int) * 5 octets.
int *temp = malloc(sizeof(int)*5)
Stockera 4
Dans le premier sizeof(int)
octets
*temp = 4;
et définissez i
i = temp + 1;
Donc, i
pointe sur un tableau de 4 éléments et non de 5.
Et
delete[] i;
sera traité comme suit
int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)
C'est au moment de l'exécution qui est responsable de l'allocation de mémoire, de la même manière que vous pouvez supprimer un tableau créé avec malloc en C standard à l'aide de free. Je pense que chaque compilateur l'implémente différemment. Une méthode courante consiste à allouer une cellule supplémentaire pour la taille du tableau.
Cependant, le moteur d’exécution n’est pas assez intelligent pour détecter s’il s’agit ou non d’un tableau ou d’un pointeur, vous devez l’informer et, si vous vous trompez, vous ne supprimez pas correctement (par exemple, ptr au lieu de tableau), ou vous finissez par prendre une valeur sans rapport pour la taille et causer des dommages importants.
Sémantiquement, les deux versions de l'opérateur de suppression en C++ peuvent "manger" n'importe quel pointeur; cependant, si un pointeur sur un objet unique est donné à delete[]
, UB résultera, ce qui signifie que tout peut arriver, y compris un plantage du système ou rien du tout.
C++ requiert que le programmeur choisisse la version appropriée de l'opérateur de suppression en fonction du sujet de la désallocation: tableau ou objet unique.
Si le compilateur pouvait déterminer automatiquement si un pointeur transmis à l'opérateur de suppression était un tableau de pointeurs, il n'y aurait alors qu'un seul opérateur de suppression en C++, ce qui suffirait dans les deux cas.
Convenez que le compilateur ne sait pas s'il s'agit d'un tableau ou non. C'est au programmeur.
Le compilateur garde parfois une trace du nombre d'objets à supprimer en sur-allouant suffisamment pour stocker la taille du tableau, mais pas toujours nécessaire.
Pour obtenir une spécification complète lorsqu’un stockage supplémentaire est alloué, veuillez vous reporter à C++ ABI (comment les compilateurs sont implémentés): Itanium C++ ABI: nouveaux témoins de l’opérateur de matrice
"comportement indéfini" signifie simplement que la spécification de langue ne donne aucune garantie quant à ce qui va se passer. Cela ne signifie pas nécessairement que quelque chose de grave va arriver.
Alors, à qui appartient-il de se souvenir de ces choses? Le système d'exploitation conserve-t-il un type d'enregistrement en arrière-plan? (Je veux dire, je me rends compte que j'ai commencé ce billet en disant que ce qui se passe n'est pas défini, mais le fait est que le scénario de "massacre" ne se produit pas, donc dans le monde pratique, quelqu'un se souvient.)
Il y a généralement deux couches ici. Le gestionnaire de mémoire sous-jacent et l'implémentation C++.
En général, le gestionnaire de mémoire se souviendra (entre autres choses) de la taille du bloc de mémoire alloué. Cela peut être plus grand que le bloc demandé par l’implémentation C++. En règle générale, le gestionnaire de mémoire stocke ses métadonnées avant le bloc de mémoire alloué.
En général, l’implémentation C++ ne se souvient de la taille du tableau que si elle le souhaite pour ses propres besoins, généralement parce que le type a un destructeur non-trival.
Donc, pour les types avec un destructeur trivial, l'implémentation de "delete" et "delete []" est généralement la même. L'implémentation C++ passe simplement le pointeur au gestionnaire de mémoire sous-jacent. Quelque chose comme
free(p)
D'autre part, pour les types avec un destructeur non trivial, "delete" et "delete []" sont susceptibles d'être différents. "delete" serait quelque chose comme (où T est le type vers lequel pointe le pointeur)
p->~T();
free(p);
Alors que "supprimer []" serait quelque chose comme.
size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);
Vous ne pouvez pas utiliser delete pour un tableau et vous ne pouvez pas utiliser delete [] pour un non-tableau.