Comment delete [] sait-il que c'est un tableau?

136

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 peut être toutes sortes de choses différentes, et donc exécuter une inconditionnelle delete[]dessus n'est pas défini. Cependant, supposons que nous passons 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 le sait? Je veux dire, du point de vue du langage / compilateur, il n'a aucune idée si oui ou non arrest un pointeur de tableau par rapport à un pointeur vers un seul int. Heck, il ne sait même pas s'il a arrété créé dynamiquement. Pourtant, si je fais ce qui suit à la place,

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 se lancer dans un certain type de `` folie meurtrière '' en supprimant le reste de la mémoire au-delà de ce point (comparez cela avec strlenet une \0chaîne non terminée - il continuera jusqu'à ce qu'il atteint 0).

Alors, à qui revient-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 la `` folie meurtrière '' ne se produit pas, donc dans le monde pratique quelqu'un se souvient.)

GRB
la source
6
il sait des crochets après la suppression
JoelFan
"le pointeur est un tableau". Non, les pointeurs ne sont jamais des tableaux. Ils pointent souvent vers le premier élément du tableau, mais c'est une chose différente.
Aaron McDaid

Réponses:

99

Le compilateur ne sait pas que c'est un tableau, il fait confiance au programmeur. La suppression d'un pointeur vers un seul intavec delete []entraînerait un comportement indéfini. Votre deuxième main()exemple est dangereux, même s'il ne plante pas immédiatement.

Le compilateur doit garder une trace du nombre d'objets à supprimer d'une manière ou d'une autre. Il peut le faire en surallouant suffisamment pour stocker la taille du tableau. Pour plus de détails, consultez la Super FAQ C ++ .

Fred Larson
la source
14
En fait, utiliser delete [] pour supprimer quelque chose de créé avec new est exploitable. taossa.com/index.php/2007/01/03/…
Rodrigo
23
@Rodrigo Le lien dans votre commentaire est cassé, mais heureusement, la machine de retour en a une copie sur replay.web.archive.org/20080703153358/http://taossa.com/…
David Gardner
103

Une question que les réponses données jusqu'à présent ne semblent pas aborder: si les bibliothèques d'exécution (pas le système d'exploitation, vraiment) peuvent garder une trace du nombre de choses dans le tableau, alors pourquoi avons-nous besoin de la delete[]syntaxe? Pourquoi un seul deleteformulaire ne peut-il pas être utilisé pour gérer toutes les suppressions?

La réponse à cela remonte aux racines de C ++ en tant que langage compatible C (ce qu'il ne cherche plus vraiment à être.) La philosophie de Stroustrup était que le programmeur ne devrait pas avoir à payer pour des fonctionnalités qu'il n'utilise 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 fait simplement

Foo* foo = new Foo;

alors l'espace mémoire alloué foone devrait pas inclure de surcharge supplémentaire qui serait nécessaire pour prendre en charge les tableaux de Foo.

Étant donné que seules les allocations de tableau sont configurées pour transporter les informations de taille de tableau supplémentaires, vous devez alors 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 vers un tableau.

Pour la plupart d'entre nous (moi y compris), cette inquiétude à propos de quelques octets supplémentaires de mémoire semble étrange ces jours-ci. Mais il existe encore des situations où la sauvegarde de quelques octets (à partir de ce qui pourrait être un nombre très élevé de blocs de mémoire) peut être importante.

Dan Breslau
la source
20
"l'agitation au sujet de quelques octets supplémentaires de mémoire semble étrange ces jours-ci". Heureusement, pour ces personnes, les tableaux nus commencent également à paraître pittoresques, ils peuvent donc simplement utiliser un vecteur ou boost :: array, et oublier delete [] pour toujours :-)
Steve Jessop
28

Oui, le système d'exploitation garde certaines choses en «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 (c'est-à-dire qu'il alloue des espaces mémoire de 1000 à 1024 mais le pointeur renvoyé pointe vers 1004, avec des emplacements 1000- 1003 stockant la taille de l'allocation). Ensuite, lorsque delete est appelé, il peut regarder 4 octets avant que le pointeur ne lui soit passé pour trouver 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.

bsdfish
la source
26
+1 - point valide en général sauf que généralement le moteur d'exécution du langage est responsable du stockage de ces métadonnées, pas du système d'exploitation.
Dentranchante
Qu'arrive-t-il à la taille du tableau ou à la taille d'un objet dont le tableau est défini? Affiche-t-il les 4 octets supplémentaires lorsque vous effectuez un sizeof sur cet objet?
Shree
3
Non, sizeof montre uniquement la taille du tableau. Si le runtime choisit de l'implémenter avec la méthode que j'ai décrite, c'est strictement un détail d'implémentation et du point de vue de l'utilisateur, cela devrait être masqué. La mémoire avant le pointeur n'appartient pas à l'utilisateur et n'est pas comptée.
bsdfish
2
Plus important encore, sizeof ne retournera en aucun cas la taille réelle d'un tableau alloué dynamiquement. Il ne peut renvoyer que des tailles connues au moment de la compilation.
bdonlan
Est-il possible d'utiliser ces métadonnées dans une boucle for pour effectuer une boucle précise sur le tableau? par exemple for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam
13

Ceci est très similaire à cette question et contient de nombreux détails que vous recherchez.

Mais il suffit de dire que ce n'est pas le travail du système d'exploitation de suivre quoi que ce soit. 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);
JaredPar
la source
Est-ce que ça va marcher? Sous Windows et Linux, cela ne fonctionne pas.
buddy
1
essayez à la size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)place
9

deleteou delete[]libéreraient probablement tous les deux la mémoire allouée (mémoire pointée), mais la grande différence est quedelete sur un tableau n'appelle pas le destructeur de chaque élément du tableau.

Quoi qu'il en soit, le mixage new/new[]et delete/delete[]c'est probablement UB.

Benoît
la source
1
Réponse claire, courte et la plus utile!
GntS
6

Il ne sait pas que c'est un tableau, c'est pourquoi vous devez fournir delete[]au lieu d'un ancien delete.

Eduffy
la source
5

J'avais une question similaire à cela. 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 pourquoi en C vous pouvez simplement remettre le pointeur sur free, mais en C ++, vous devez lui dire s'il s'agit d'un tableau ou d'une seule variable?

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 le destructeur que pour la première instance de MyClass appelée. Si vous utilisez delete [], vous pouvez être assuré que le destructeur sera appelé pour toutes les instances 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 delete [] sur new n'est pas défini - il peut ne pas fonctionner de la même manière sur tous les compilateurs / systèmes.

ProdigySim
la source
3

C'est au runtime 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 standard C en utilisant 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 runtime 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, soit vous ne supprimez pas correctement (par exemple, ptr au lieu de tableau), soit vous finissez par prendre une valeur sans rapport avec la taille et causer des dommages importants.

Uri
la source
3

L'UNE DES approches pour les compilateurs est d'allouer un peu plus de mémoire et de 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 4dans les premiers sizeof(int)octets

*temp = 4;

Et mettre i

i = temp + 1;

Alors i tableau de 4 éléments, pas 5.

Et

delete[] i;

sera traité de la manière suivante

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)
Avt
la source
1

Sémantiquement, les deux versions de l'opérateur de suppression en C ++ peuvent "manger" n'importe quel pointeur; cependant, si un pointeur vers un seul objet est donné àdelete[] , alors UB en résultera, ce qui signifie que tout peut arriver, y compris une panne du système ou rien du tout.

C ++ demande au programmeur de choisir la bonne version 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 passé à l'opérateur de suppression était un tableau de pointeurs, alors il n'y aurait qu'un seul opérateur de suppression en C ++, ce qui suffirait dans les deux cas.

mloskot
la source
1

Convenez que le compilateur ne sait pas s'il s'agit d'un tableau ou non. Cela dépend du programmeur.

Le compilateur garde parfois une trace du nombre d'objets à supprimer en surallouant suffisamment pour stocker la taille du tableau, mais pas toujours nécessaire.

Pour une spécification complète lorsqu'un stockage supplémentaire est alloué, veuillez vous référer à C ++ ABI (comment les compilateurs sont implémentés): Itanium C ++ ABI: Array Operator new Cookies

shibo
la source
Je souhaite seulement que chaque compilateur observe une ABI documentée pour C ++. +1 pour le lien, que j'ai déjà visité. Merci.
Don Wakefield
0

Vous ne pouvez pas utiliser delete pour un tableau et vous ne pouvez pas utiliser delete [] pour un non-tableau.

Don Wakefield
la source
8
Je pense que vous ne devriez pas dire, car votre compilateur moyen ne détectera pas l'abus.
Don Wakefield
0

«comportement indéfini» signifie simplement que la spécification du langage ne donne aucune garantie quant à ce qui va se passer. Cela ne signifie pas nécessairement que quelque chose de mauvais va se passer.

Alors, à qui revient-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é cet article en disant que ce qui se passe n'est pas défini, mais le fait est que le scénario de la `` folie meurtrière '' 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) de la taille du bloc de mémoire qui a été 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 stockera ses métadonnées avant le bloc de mémoire alloué.

L'implémentation C ++ ne se souviendra généralement de la taille du tableau que si elle a besoin de le faire 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 ++ transmet simplement le pointeur au gestionnaire de mémoire sous-jacent. Quelque chose comme

free(p)

D'un autre côté, pour les types avec un destructeur non trivial, "delete" et "delete []" sont susceptibles d'être différents. "supprimer" serait quelque chose comme (où T est le type vers lequel le pointeur pointe)

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);
plugwash
la source
-1

Parcourez un tableau d'objets et appelez destructor pour chacun d'entre eux. J'ai créé ce code simple qui surcharge les expressions new [] et delete [] et fournit une fonction de modèle pour désallouer la mémoire et appeler le destructeur pour chaque objet si nécessaire:

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////
Rafal Rebisz
la source
-2

définissez simplement un destructeur dans une classe et exécutez votre code avec les deux syntaxes

delete pointer

delete [] pointer

selon la sortie, vous pouvez trouver les solutions

bubu
la source
utilisez delete [] lorsque vous créez un type de tableau. par exemple int * a = new int; int * b = nouveau int [5]; supprimer un; supprimer [] b;
Lineesh K Mohan
-3

La réponse:

int * pArray = new int [5];

taille int = * (pArray-1);

La publication ci-dessus n'est pas correcte et produit une valeur non valide. Le "-1" compte les éléments Sur le système d'exploitation Windows 64 bits, la taille de tampon correcte réside dans l'adresse Ptr - 4 octets

Evgeni Raikhel
la source