J'ai le code suivant.
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
Et le code s'exécute sans aucune exception d'exécution!
La sortie était 58
Comment est-ce possible? La mémoire d'une variable locale n'est-elle pas inaccessible en dehors de sa fonction?
address of local variable ‘a’ returned
; valgrind spectaclesInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
Réponses:
Vous louez une chambre d'hôtel. Vous mettez un livre dans le tiroir supérieur de la table de chevet et vous vous endormez. Vous vérifiez le lendemain matin, mais "oubliez" de rendre votre clé. Vous volez la clé!
Une semaine plus tard, vous rentrez à l'hôtel, ne vous enregistrez pas, vous faufilez dans votre ancienne chambre avec votre clé volée et regardez dans le tiroir. Votre livre est toujours là. Étonnant!
Comment est-ce possible? Le contenu d'un tiroir de chambre d'hôtel n'est-il pas inaccessible si vous n'avez pas loué la chambre?
Eh bien, évidemment, ce scénario peut se produire dans le monde réel sans problème. Aucune force mystérieuse ne fait disparaître votre livre lorsque vous n'êtes plus autorisé à être dans la pièce. Il n'y a pas non plus de force mystérieuse qui vous empêche d'entrer dans une pièce avec une clé volée.
La direction de l'hôtel n'est pas obligée de retirer votre livre. Vous n'avez pas passé de contrat avec eux qui stipule que si vous laissez des choses derrière vous, ils les déchireront pour vous. Si vous rentrez illégalement dans votre chambre avec une clé volée pour la récupérer, le personnel de sécurité de l'hôtel n'est pas obligé de vous attraper en vous faufilant. plus tard, vous devez m'arrêter. " Vous avez plutôt signé avec eux un contrat qui disait "Je promets de ne pas rentrer plus tard dans ma chambre", un contrat que vous avez rompu .
Dans cette situation tout peut arriver . Le livre peut être là - vous avez eu de la chance. Le livre de quelqu'un d'autre peut être là et le vôtre pourrait être dans le four de l'hôtel. Quelqu'un pourrait être là à votre arrivée, déchirant votre livre en morceaux. L'hôtel aurait pu retirer complètement la table et le livre et les remplacer par une armoire. Tout l'hôtel pourrait être sur le point d'être démoli et remplacé par un stade de football, et vous allez mourir dans une explosion pendant que vous vous faufilez.
Vous ne savez pas ce qui va se passer; lorsque vous avez quitté l'hôtel et volé une clé pour l'utiliser illégalement plus tard, vous avez renoncé au droit de vivre dans un monde prévisible et sûr parce que vous avez choisi d'enfreindre les règles du système.
C ++ n'est pas un langage sûr . Il vous permettra joyeusement d'enfreindre les règles du système. Si vous essayez de faire quelque chose d'illégal et de stupide, comme rentrer dans une pièce dans laquelle vous n'êtes pas autorisé à fouiller et fouiller dans un bureau qui pourrait même ne plus être là, C ++ ne vous arrêtera pas. Des langages plus sûrs que C ++ résolvent ce problème en limitant votre pouvoir - en ayant un contrôle beaucoup plus strict sur les clés, par exemple.
MISE À JOUR
Sainte bonté, cette réponse retient beaucoup l'attention. (Je ne sais pas pourquoi - j'ai considéré que c'était juste une petite analogie "amusante", mais peu importe.)
J'ai pensé qu'il pourrait être pertinent de mettre à jour cela un peu avec quelques réflexions plus techniques.
Les compilateurs sont chargés de générer du code qui gère le stockage des données manipulées par ce programme. Il existe de nombreuses façons différentes de générer du code pour gérer la mémoire, mais au fil du temps, deux techniques de base se sont enracinées.
La première consiste à avoir une sorte de zone de stockage "à longue durée de vie" où la "durée de vie" de chaque octet dans le stockage - c'est-à-dire la période de temps où il est valablement associé à une variable de programme - ne peut pas être facilement prédite à l'avance de temps. Le compilateur génère des appels dans un «gestionnaire de tas» qui sait allouer dynamiquement du stockage quand il est nécessaire et le récupérer lorsqu'il n'est plus nécessaire.
La deuxième méthode consiste à avoir une zone de stockage «à durée de vie courte» où la durée de vie de chaque octet est bien connue. Ici, les durées de vie suivent un schéma de «nidification». La plus longue durée de vie de ces variables de courte durée sera allouée avant toute autre variable de courte durée et sera libérée en dernier. Les variables à durée de vie plus courte seront allouées après les plus longues et seront libérées avant elles. La durée de vie de ces variables à durée de vie plus courte est «imbriquée» dans la durée de vie de celles à durée de vie plus longue.
Les variables locales suivent ce dernier modèle; lorsqu'une méthode est entrée, ses variables locales prennent vie. Lorsque cette méthode appelle une autre méthode, les variables locales de la nouvelle méthode prennent vie. Ils seront morts avant la mort des variables locales de la première méthode. L'ordre relatif des débuts et des fins de durée de vie des stockages associés aux variables locales peut être déterminé à l'avance.
Pour cette raison, les variables locales sont généralement générées en tant que stockage sur une structure de données «pile», car une pile a la propriété que la première chose poussée dessus sera la dernière chose sautée.
C'est comme si l'hôtel décide de ne louer les chambres que séquentiellement, et vous ne pouvez pas partir avant que tout le monde avec un numéro de chambre plus élevé que vous ne l'ait fait.
Pensons donc à la pile. Dans de nombreux systèmes d'exploitation, vous obtenez une pile par thread et la pile est allouée pour avoir une certaine taille fixe. Lorsque vous appelez une méthode, le contenu est poussé dans la pile. Si vous passez ensuite un pointeur vers la pile hors de votre méthode, comme le fait l'affiche originale ici, ce n'est qu'un pointeur au milieu d'un bloc de mémoire d'un million d'octets entièrement valide. Dans notre analogie, vous quittez l'hôtel; lorsque vous le faites, vous venez de vérifier la chambre occupée au numéro le plus élevé. Si personne d'autre ne s'enregistre après vous et que vous rentrez illégalement dans votre chambre, toutes vos affaires sont garanties d'être toujours là dans cet hôtel particulier .
Nous utilisons des piles pour les magasins temporaires car elles sont vraiment bon marché et faciles. Une implémentation de C ++ n'est pas requise pour utiliser une pile pour le stockage des sections locales; il pourrait utiliser le tas. Ce n'est pas le cas, car cela ralentirait le programme.
Une implémentation de C ++ n'est pas requise pour laisser intactes les déchets que vous avez laissés sur la pile afin que vous puissiez y revenir plus tard illégalement; il est parfaitement légal pour le compilateur de générer du code qui remet à zéro tout dans la "salle" que vous venez de quitter. Ce n'est pas parce que, encore une fois, ce serait cher.
Une implémentation de C ++ n'est pas requise pour garantir que lorsque la pile diminue logiquement, les adresses qui étaient valides sont toujours mappées en mémoire. L'implémentation est autorisée à dire au système d'exploitation "nous avons fini d'utiliser cette page de pile maintenant. Jusqu'à ce que je dise le contraire, émettez une exception qui détruit le processus si quelqu'un touche la page de pile précédemment valide". Encore une fois, les implémentations ne le font pas réellement car elles sont lentes et inutiles.
Au lieu de cela, les implémentations vous permettent de faire des erreurs et de vous en tirer. La plupart du temps. Jusqu'au jour où quelque chose de vraiment horrible se passe mal et le processus explose.
C'est problématique. Il y a beaucoup de règles et il est très facile de les briser accidentellement. J'ai certainement plusieurs fois. Et pire, le problème n'apparaît souvent que lorsque la mémoire est détectée comme étant corrompue des milliards de nanosecondes après que la corruption s'est produite, lorsqu'il est très difficile de déterminer qui l'a gâchée.
Des langues plus sûres en mémoire résolvent ce problème en limitant votre puissance. En C # "normal", il n'y a tout simplement aucun moyen de prendre l'adresse d'un local et de la retourner ou de la stocker pour plus tard. Vous pouvez prendre l'adresse d'un local, mais la langue est intelligemment conçue de sorte qu'il est impossible de l'utiliser après la durée de vie des extrémités locales. Afin de prendre l'adresse d'un local et de la transmettre, vous devez mettre le compilateur dans un mode spécial "dangereux", et mettre le mot "dangereux" dans votre programme, pour attirer l'attention sur le fait que vous faites probablement quelque chose de dangereux qui pourrait enfreindre les règles.
Pour en savoir plus:
Et si C # permettait de renvoyer des références? Par coïncidence, c'est le sujet du blog d'aujourd'hui:
https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/
Pourquoi utilisons-nous des piles pour gérer la mémoire? Les types de valeurs en C # sont-ils toujours stockés sur la pile? Comment fonctionne la mémoire virtuelle? Et bien d'autres sujets sur le fonctionnement du gestionnaire de mémoire C #. Beaucoup de ces articles sont également utiles aux programmeurs C ++:
https://ericlippert.com/tag/memory-management/
la source
Ce que vous faites ici est en train de lire et d' écrire simplement à mémoire utilisée pour être l'adresse
a
. Maintenant que vous êtes en dehorsfoo
, c'est juste un pointeur vers une zone de mémoire aléatoire. Il se trouve que dans votre exemple, cette zone de mémoire existe et que rien d'autre ne l'utilise pour le moment. Vous ne cassez rien en continuant à l'utiliser, et rien d'autre ne l'a encore écrasé. Par conséquent, le5
est toujours là. Dans un vrai programme, cette mémoire serait réutilisée presque immédiatement et vous casseriez quelque chose en faisant cela (bien que les symptômes n'apparaissent que bien plus tard!)À votre retour
foo
, vous dites au système d'exploitation que vous n'utilisez plus cette mémoire et qu'elle peut être réaffectée à autre chose. Si vous êtes chanceux et qu'il n'est jamais réaffecté, et que le système d'exploitation ne vous surprend pas à l'utiliser à nouveau, vous vous en sortirez avec le mensonge. Il y a de fortes chances que vous finissiez par écrire sur tout ce qui se retrouverait avec cette adresse.Maintenant, si vous vous demandez pourquoi le compilateur ne se plaint pas, c'est probablement parce qu'il a
foo
été éliminé par optimisation. Il vous avertira généralement de ce genre de chose. C suppose que vous savez ce que vous faites, et techniquement, vous n'avez pas violé la portée ici (il n'y a aucune référence àa
lui-même en dehors defoo
), seulement des règles d'accès à la mémoire, qui déclenchent uniquement un avertissement plutôt qu'une erreur.En bref: cela ne fonctionnera généralement pas, mais parfois par hasard.
la source
Parce que l'espace de stockage n'a pas encore été piétiné. Ne comptez pas sur ce comportement.
la source
Un petit ajout à toutes les réponses:
si vous faites quelque chose comme ça:
la sortie sera probablement: 7
En effet, après son retour de foo (), la pile est libérée puis réutilisée par boo (). Si vous démontez l'exécutable, vous le verrez clairement.
la source
boo
réutilise lafoo
pile? ne sont pas des piles de fonctions séparées les unes des autres, je reçois également des ordures en exécutant ce code sur Visual Studio 2015foo()
, existe, puis descend dansboo()
.Foo()
et lesBoo()
deux entrent avec le pointeur de pile au même emplacement. Ce n'est cependant pas un comportement sur lequel il faut se fier. D'autres 'trucs' (comme les interruptions ou le système d'exploitation) peuvent utiliser la pile entre l'appel deboo()
etfoo()
, en modifiant son contenu ...En C ++, vous pouvez accéder à n'importe quelle adresse, mais cela ne signifie pas que vous le devriez . L'adresse à laquelle vous accédez n'est plus valide. Ça marche parce que rien d'autre n'a brouillé la mémoire après le retour de foo, mais il peut se bloquer dans de nombreuses circonstances. Essayez d'analyser votre programme avec Valgrind , ou même simplement de le compiler optimisé, et voyez ...
la source
Vous ne lancez jamais d'exception C ++ en accédant à une mémoire non valide. Vous donnez simplement un exemple de l'idée générale de référencer un emplacement de mémoire arbitraire. Je pourrais faire la même chose comme ceci:
Ici, je traite simplement 123456 comme l'adresse d'un double et je lui écris. Un certain nombre de choses peuvent se produire:
q
pourrait en fait vraiment être une adresse valide d'un double, par exempledouble p; q = &p;
.q
pourrait pointer quelque part dans la mémoire allouée et je viens d'écraser 8 octets là-dedans.q
pointe en dehors de la mémoire allouée et le gestionnaire de mémoire du système d'exploitation envoie un signal d'erreur de segmentation à mon programme, provoquant l'arrêt du runtime.La façon dont vous le configurez est un peu plus raisonnable que l'adresse renvoyée pointe vers une zone de mémoire valide, car elle sera probablement un peu plus loin dans la pile, mais c'est toujours un emplacement non valide auquel vous ne pouvez pas accéder dans un mode déterministe.
Personne ne vérifiera automatiquement la validité sémantique d'adresses de mémoire comme celle-là pour vous pendant l'exécution normale du programme. Cependant, un débogueur de mémoire tel que le
valgrind
fera volontiers, vous devez donc exécuter votre programme à travers lui et être témoin des erreurs.la source
4) I win the lottery
Avez-vous compilé votre programme avec l'optimiseur activé? La
foo()
fonction est assez simple et aurait pu être intégrée ou remplacée dans le code résultant.Mais je suis d'accord avec Mark B que le comportement résultant n'est pas défini.
la source
5
sera changé ...Votre problème n'a rien à voir avec la portée . Dans le code que vous montrez, la fonction
main
ne voit pas les noms dans la fonctionfoo
, vous ne pouvez donc pas accédera
à foo directement avec ce nom à l'extérieurfoo
.Le problème que vous rencontrez est la raison pour laquelle le programme ne signale pas d'erreur lors du référencement de la mémoire illégale. En effet, les normes C ++ ne spécifient pas de frontière très claire entre la mémoire illégale et la mémoire légale. Référencer quelque chose dans la pile surgie provoque parfois des erreurs et parfois non. Ça dépend. Ne comptez pas sur ce comportement. Supposez que cela entraînera toujours une erreur lorsque vous programmez, mais supposez qu'il ne signalera jamais d'erreur lorsque vous déboguez.
la source
Vous venez de renvoyer une adresse mémoire, c'est autorisé mais probablement une erreur.
Oui, si vous essayez de déréférencer cette adresse mémoire, vous aurez un comportement indéfini.
la source
cout
.*a
pointe vers la mémoire non allouée (libérée). Même si vous ne le déréfendez pas, il est toujours dangereux (et probablement faux).C'est un comportement indéfini classique qui a été discuté ici il n'y a pas deux jours - recherchez un peu sur le site. En un mot, vous avez eu de la chance, mais tout aurait pu arriver et votre code rend l'accès à la mémoire invalide.
la source
Ce comportement n'est pas défini, comme l'a souligné Alex - en fait, la plupart des compilateurs mettent en garde contre cela, car c'est un moyen facile d'obtenir des plantages.
Pour un exemple du type de comportement effrayant que vous êtes susceptible d'obtenir, essayez cet exemple:
Cela affiche "y = 123", mais vos résultats peuvent varier (vraiment!). Votre pointeur encombre d'autres variables locales non liées.
la source
Faites attention à tous les avertissements. Ne résolvez pas seulement les erreurs.
GCC affiche cet avertissement
C'est la puissance de C ++. Vous devez vous soucier de la mémoire. Avec le
-Werror
drapeau, cet avertissement devient une erreur et vous devez maintenant le déboguer.la source
Cela fonctionne parce que la pile n'a pas (encore) été modifiée depuis que y a été placée. Appelez quelques autres fonctions (qui appellent également d'autres fonctions) avant d'accéder à
a
nouveau et vous n'aurez probablement plus autant de chance ... ;-)la source
Vous avez en fait invoqué un comportement non défini.
Renvoyer l'adresse d'une œuvre temporaire, mais comme les temporaires sont détruits à la fin d'une fonction, les résultats de leur accès ne seront pas définis.
Vous n'avez donc pas modifié
a
mais plutôt l'emplacement de mémoire oùa
se trouvait autrefois. Cette différence est très similaire à la différence entre planter et ne pas planter.la source
Dans les implémentations de compilateur typiques, vous pouvez considérer le code comme "imprimer la valeur du bloc de mémoire avec l'adresse qui était auparavant occupée par un". De plus, si vous ajoutez une nouvelle invocation de fonction à une fonction qui contient un local,
int
il y a de fortes chances que la valeur dea
(ou l'adresse mémoire qui pointaita
) change. Cela se produit car la pile sera remplacée par une nouvelle trame contenant des données différentes.Cependant, il s'agit d' un comportement non défini et vous ne devez pas vous y fier pour fonctionner!
la source
a
, le pointeur contenait l'adresse dea
. Bien que la norme n'exige pas que les implémentations définissent le comportement des adresses après la fin de la durée de vie de leur cible, elle reconnaît également que sur certaines plateformes UB est traitée de manière documentée, caractéristique de l'environnement. Bien que l'adresse d'une variable locale ne soit généralement pas très utile une fois qu'elle est hors de portée, certains autres types d'adresses peuvent toujours avoir un sens après la durée de vie de leurs cibles respectives.realloc
de comparer un pointeur passé à la valeur de retour, ni que les pointeurs vers les adresses de l'ancien bloc soient ajustés pour pointer vers le nouveau, certaines implémentations le font , et le code qui exploite une telle fonctionnalité peut être plus efficace que le code qui doit éviter toute action - même des comparaisons - impliquant des pointeurs vers l'allocation qui a été donnéerealloc
.Il peut, car
a
est une variable allouée temporairement pour la durée de vie de sa portée (foo
fonction). Après votre retour defoo
la mémoire est libre et peut être écrasée.Ce que vous faites est décrit comme un comportement non défini . Le résultat n'est pas prévisible.
la source
Les choses avec une sortie console correcte (?) Peuvent changer radicalement si vous utilisez :: printf mais pas cout. Vous pouvez jouer avec le débogueur dans le code ci-dessous (testé sur x86, 32 bits, MSVisual Studio):
la source
Après le retour d'une fonction, tous les identifiants sont détruits au lieu des valeurs conservées dans un emplacement mémoire et nous ne pouvons pas localiser les valeurs sans avoir un identifiant, mais cet emplacement contient toujours la valeur stockée par la fonction précédente.
Donc, ici, la fonction
foo()
renvoie l'adresse dea
eta
est détruite après avoir renvoyé son adresse. Et vous pouvez accéder à la valeur modifiée via cette adresse renvoyée.Permettez-moi de prendre un exemple concret:
Supposons qu'un homme cache de l'argent à un emplacement et vous indique l'emplacement. Après un certain temps, l'homme qui vous avait dit où se trouvait l'argent meurt. Mais vous avez toujours accès à cet argent caché.
la source
C'est une façon «sale» d'utiliser les adresses mémoire. Lorsque vous renvoyez une adresse (pointeur), vous ne savez pas si elle appartient à la portée locale d'une fonction. Ce n'est qu'une adresse. Maintenant que vous avez invoqué la fonction 'foo', cette adresse (emplacement mémoire) de 'a' y était déjà allouée dans la mémoire adressable (en toute sécurité, pour l'instant au moins) de votre application (processus). Après le retour de la fonction 'foo', l'adresse de 'a' peut être considérée comme 'sale' mais elle est là, pas nettoyée, ni perturbée / modifiée par des expressions dans une autre partie du programme (dans ce cas spécifique au moins). Le compilateur AC / C ++ ne vous empêche pas d'avoir un tel accès "sale" (peut cependant vous avertir, si vous vous en souciez).
la source
Votre code est très risqué. Vous créez une variable locale (qui est considérée comme détruite après la fin de la fonction) et vous renvoyez l'adresse de la mémoire de cette variable après sa suppression.
Cela signifie que l'adresse mémoire peut être valide ou non, et votre code sera vulnérable à d'éventuels problèmes d'adresse mémoire (par exemple, une erreur de segmentation).
Cela signifie que vous faites une très mauvaise chose, car vous passez une adresse mémoire à un pointeur qui n'est pas du tout fiable.
Considérez plutôt cet exemple et testez-le:
Contrairement à votre exemple, avec cet exemple, vous êtes:
la source
new
.new
. Vous leur apprenez à utilisernew
. Mais vous ne devriez pas utilisernew
.new
en 2019 (sauf si vous écrivez du code de bibliothèque) et n'enseignez pas non plus aux nouveaux arrivants! À votre santé.