web-dev-qa-db-fra.com

Pourquoi mon programme plante-il lorsque j'incrémente un pointeur puis le supprime?

Je résolvais un exercice de programmation lorsque j'ai réalisé que j'avais un gros malentendu au sujet des pointeurs. Veuillez quelqu'un pourrait expliquer la raison pour laquelle ce code provoque un crash en C++.

#include <iostream>

int main()
{
    int* someInts = new int[5];

    someInts[0] = 1; 
    someInts[1] = 1;

    std::cout << *someInts;
    someInts++; //This line causes program to crash 

    delete[] someInts;
    return 0;
}

P.S Je suis conscient qu'il n'y a aucune raison d'utiliser "nouveau" ici, je fais juste l'exemple le plus petit possible.

30
Beetroot

C'est en fait la déclaration après celle que vous marquez comme provoquant le plantage du programme qui provoque le plantage du programme!

Vous devez passer le même pointeur à delete[] à votre retour de new[].

Sinon, le comportement du programme est non défini.

80
Bathsheba

Le problème est qu'avec le someInts++; vous passez l'adresse du deuxième élément d'un tableau à votre delete[] déclaration. Vous devez transmettre l'adresse du premier élément (original):

int* someInts = new int[5];
int* originalInts = someInts; // points to the first element
someInts[0] = 1;
someInts[1] = 1;

std::cout << *someInts;
someInts++; // points at the second element now

delete[] originalInts;
33
user1593881

Sans entrer dans les détails d'une implémentation spécifique ici, la raison intuitive du crash peut être expliquée simplement en considérant ce que delete[] est censé faire:

Détruit un tableau créé par un new[]-expression

Nous voulons delete[] un pointeur vers un tableau. Entre autres choses, il doit libérer la mémoire qu'il a allouée pour contenir le contenu de ce tableau.

Comment l'allocateur sait-il quoi libérer? Il utilise le pointeur que vous lui avez donné comme clé pour rechercher la structure de données qui contient les informations de comptabilité pour le bloc alloué. Quelque part, il existe une structure qui stocke le mappage entre les pointeurs sur les blocs précédemment alloués et l'opération de comptabilité associée.

Vous pouvez souhaiter que cette recherche entraîne une sorte de message d'erreur convivial si le pointeur que vous passez à delete [] n'a pas été renvoyé par un correspondant new[], mais rien dans la norme ne le garantit.

Donc, c'est possible, étant donné un pointeur qui n'avait pas été précédemment alloué par new[], delete[] finit par regarder quelque chose qui n'est vraiment pas une structure comptable cohérente. Les fils se croisent. Un crash s'ensuit.

Ou, vous pourriez souhaiter que delete[] dirait "hé, il semble que ce pointeur pointe vers quelque part à l'intérieur d'une région que j'ai allouée auparavant. Permettez-moi de revenir en arrière et de trouver le pointeur que j'ai renvoyé lorsque j'ai alloué cette région et de l'utiliser pour rechercher les informations de comptabilité" mais, encore une fois , il n'y a pas une telle exigence dans la norme:

Pour la deuxième forme (tableau), l'expression doit être une valeur de pointeur nulle ou une valeur de pointeur précédemment obtenue par une forme de tableau de new-expression. Si l'expression est autre chose, y compris si c'est un pointeur obtenu par la forme non tableau de new-expression, le comportement n'est pas défini. [c'est moi qui souligne]

Dans ce cas, vous avez de la chance car vous avez découvert que vous avez fait quelque chose de mal instantanément.

PS: Ceci est une explication ondulée à la main

19
Sinan Ünür

Vous pouvez incrémenter un pointeur dans le bloc et utiliser ce pointeur incrémenté pour accéder à différentes parties du bloc, c'est très bien.

Cependant, vous devez passer Supprimer le pointeur que vous avez obtenu de Nouveau. Pas une version incrémentée de celui-ci, pas un pointeur qui a été alloué par d'autres moyens.

Pourquoi? Eh bien, la réponse est parce que c'est ce que dit la norme.

La réponse pratique est que, pour libérer un bloc de mémoire, le gestionnaire de mémoire a besoin d'informations sur le bloc. Par exemple, où il commence et se termine, et si les morceaux adjacents sont libres (normalement un gestionnaire de mémoire combinera des morceaux libres adjacents) et à quelle arène il appartient (important pour verrouiller les gestionnaires de mémoire multithread).

Ces informations sont généralement stockées juste avant la mémoire allouée. Le gestionnaire de mémoire soustraira une valeur fixe de votre pointeur et recherchera une structure de métadonnées d'allocation à cet emplacement.

Si vous passez un pointeur qui ne pointe pas vers le début d'un bloc de mémoire allouée, le gestionnaire de mémoire tente d'effectuer la soustraction et de lire son bloc de contrôle, mais ce qu'il finit par lire n'est pas un bloc de contrôle valide.

Si vous avez de la chance, le code se bloque rapidement, si vous n'avez pas de chance, vous pouvez vous retrouver avec une corruption de mémoire subtile.

8
plugwash