Peut-on accéder à la mémoire d'une variable locale en dehors de sa portée?

1031

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?

inconnus
la source
14
cela ne compilera même pas tel quel; si vous corrigez l'affaire non conforme, gcc avertira toujours address of local variable ‘a’ returned; valgrind spectaclesInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe
76
@Serge: Dans ma jeunesse, j'ai déjà travaillé sur un code un peu délicat qui fonctionnait sur le système d'exploitation Netware, ce qui impliquait de se déplacer intelligemment autour du pointeur de la pile d'une manière qui n'était pas exactement approuvée par le système d'exploitation. Je savais quand j'avais fait une erreur car souvent la pile finissait par chevaucher la mémoire de l'écran et je pouvais simplement regarder les octets être écrits directement sur l'écran. Vous ne pouvez pas vous en sortir avec ce genre de choses de nos jours.
Eric Lippert
23
lol. J'avais besoin de lire la question et quelques réponses avant même de comprendre où est le problème. Est-ce en fait une question sur la portée d'accès des variables? Vous n'utilisez même pas «a» en dehors de votre fonction. Et c'est tout ce qu'il y a à faire. Le fait de contourner certaines références de mémoire est un sujet totalement différent de la portée variable.
erikbwork
10
La réponse Dupe ne signifie pas une question dupe. Beaucoup de questions dupes que les gens ont proposées ici sont des questions complètement différentes qui se rapportent au même symptôme sous-jacent ... mais le questionneur a une manière de le savoir et doit donc rester ouvert. J'ai fermé un vieux dupe et l'ai fusionné dans cette question qui devrait rester ouverte car elle a une très bonne réponse.
Joel Spolsky
16
@Joel: Si la réponse ici est bonne, elle devrait être fusionnée dans des questions plus anciennes , dont c'est une dupe, et non l'inverse. Et ça question est en effet une dupe des autres questions proposées ici et puis certaines (même si certaines des propositions sont mieux adaptées que d'autres). Notez que je pense que la réponse d'Eric est bonne. (En fait, j'ai signalé cette question pour avoir fusionné les réponses dans l'une des questions les plus anciennes afin de récupérer les questions les plus anciennes.)
sbi

Réponses:

4802

Comment est-ce possible? La mémoire d'une variable locale n'est-elle pas inaccessible en dehors de sa fonction?

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:

Eric Lippert
la source
56
@muntoo: Malheureusement, ce n'est pas comme si le système d'exploitation émet une sirène d'avertissement avant de désactiver ou de désallouer une page de mémoire virtuelle. Si vous nettoyez cette mémoire alors que vous ne la possédez plus, le système d'exploitation a parfaitement le droit de supprimer tout le processus lorsque vous touchez une page désallouée. Boom!
Eric Lippert
83
@Kyle: Seuls les hôtels sûrs le font. Les hôtels dangereux obtiennent des gains de bénéfices mesurables en ne perdant pas de temps sur les clés de programmation.
Alexander Torstling
498
@cyberguijarro: Le fait que C ++ ne soit pas sûr pour la mémoire est tout simplement un fait. Ce n'est rien "de dénigrer". Si j'avais dit, par exemple, "C ++ est un méli-mélo horrible de fonctionnalités sous-spécifiées et trop complexes empilées sur un modèle de mémoire fragile et dangereux et je suis reconnaissant chaque jour de ne plus y travailler pour ma propre raison", ce serait dénigrer C ++. Faire remarquer que ce n'est pas sûr pour la mémoire explique pourquoi l'affiche originale voit ce problème; c'est répondre à la question, pas éditorialiser.
Eric Lippert
50
Strictement parlant, l'analogie devrait mentionner que la réceptionniste de l'hôtel était très heureuse que vous preniez la clé avec vous. "Oh, ça te dérange si je prends cette clé avec moi?" "Allez-y. Pourquoi m'en soucierais-je? Je ne travaille que ici". Il ne devient illégal que lorsque vous essayez de l'utiliser.
philsquared
140
S'il vous plaît, veuillez au moins envisager d'écrire un livre un jour. Je l'achèterais même s'il ne s'agissait que d'une collection d'articles de blog révisés et étendus, et je suis sûr que beaucoup de gens le feraient aussi. Mais un livre avec vos réflexions originales sur diverses questions liées à la programmation serait une excellente lecture. Je sais qu'il est incroyablement difficile de trouver le temps, mais pensez à en écrire un.
Dyppl
276

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 dehors foo, 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, le 5est 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 à alui-même en dehors de foo), 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.

Rena
la source
152

Parce que l'espace de stockage n'a pas encore été piétiné. Ne comptez pas sur ce comportement.

msw
la source
1
Mec, c'était la plus longue attente pour un commentaire depuis: "Qu'est-ce que la vérité? Dit Pilate en plaisantant." C'était peut-être une Bible de Gédéon dans ce tiroir d'hôtel. Et que leur est-il arrivé, de toute façon? Remarquez qu'ils ne sont plus présents, du moins à Londres. Je suppose qu'en vertu de la législation sur l'égalité, vous auriez besoin d'une bibliothèque de tracts religieux.
Rob Kent
J'aurais juré avoir écrit cela il y a longtemps, mais il est apparu récemment et a trouvé que ma réponse n'était pas là. Maintenant, je dois aller comprendre vos allusions ci-dessus car je pense que je serai amusé quand je le ferai. <
msw
1
Haha. Francis Bacon, l'un des plus grands essayistes britanniques, que certains soupçonnent d'avoir écrit les pièces de Shakespeare, parce qu'ils ne peuvent pas accepter qu'un gamin du pays, fils de gant, puisse être un génie. Tel est le système de cours d'anglais. Jésus a dit: «Je suis la vérité». oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent
84

Un petit ajout à toutes les réponses:

si vous faites quelque chose comme ça:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

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.

Michael
la source
2
Exemple simple mais excellent pour comprendre la théorie sous-jacente de la pile. Un seul ajout de test, déclarant "int a = 5;" dans foo () comme "static int a = 5;" peut être utilisé pour comprendre la portée et la durée de vie d'une variable statique.
contrôle
15
-1 "car sera probablement 7 ". Le compilateur peut enregistrer un in boo. Il pourrait le supprimer car il n'est pas nécessaire. Il y a de fortes chances que * p ne soit pas 5 , mais cela ne signifie pas qu'il existe une raison particulièrement bonne pour laquelle il sera probablement 7 .
Matt
2
Cela s'appelle un comportement indéfini!
Francis Cugler
pourquoi et comment booréutilise la foopile? 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 2015
ampawd
1
@ampawd a presque un an, mais non, les "piles de fonctions" ne sont pas séparées les unes des autres. Un CONTEXTE a une pile. Ce contexte utilise sa pile pour entrer main, puis descend dans foo(), existe, puis descend dans boo(). Foo()et les Boo()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 de boo()et foo(), en modifiant son contenu ...
Russ Schultz
72

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 ...

Charles Brunet
la source
5
Vous voulez probablement dire que vous pouvez tenter d'accéder à n'importe quelle adresse. Parce que la plupart des systèmes d'exploitation ne permettent à aucun programme d'accéder à une adresse; il existe des tonnes de garanties pour protéger l'espace d'adressage. C'est pourquoi il n'y aura pas d'autre LOADLIN.EXE sur le marché.
v010dya
67

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:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Ici, je traite simplement 123456 comme l'adresse d'un double et je lui écris. Un certain nombre de choses peuvent se produire:

  1. qpourrait en fait vraiment être une adresse valide d'un double, par exemple double p; q = &p;.
  2. q pourrait pointer quelque part dans la mémoire allouée et je viens d'écraser 8 octets là-dedans.
  3. 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.
  4. Vous gagnez à la loterie.

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 valgrindfera volontiers, vous devez donc exécuter votre programme à travers lui et être témoin des erreurs.

Kerrek SB
la source
9
Je vais juste écrire un programme maintenant qui continue d'exécuter ce programme pour que4) I win the lottery
Aidiakapi
29

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.

gastush
la source
Voilà mon pari. L'optimiseur a vidé l'appel de fonction.
Erik Aronesty
9
Ce n'est pas nécessaire. Puisqu'aucune nouvelle fonction n'est appelée après foo (), le cadre de pile local des fonctions n'est tout simplement pas encore écrasé. Ajoutez une autre invocation de fonction après foo (), et le 5sera changé ...
Tomas
J'ai exécuté le programme avec GCC 4.8, en remplaçant cout par printf (et y compris stdio). Avertit à juste titre "avertissement: l'adresse de la variable locale 'a' a renvoyé [-Wreturn-local-addr]". Sorties 58 sans optimisation et 08 avec -O3. Étrangement, P a une adresse, même si sa valeur est 0. Je m'attendais à NULL (0) comme adresse.
kevinf
23

Votre problème n'a rien à voir avec la portée . Dans le code que vous montrez, la fonction mainne voit pas les noms dans la fonction foo, vous ne pouvez donc pas accéder aà foo directement avec ce nom à l'extérieur foo.

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.

Chang Peng
la source
Je me souviens d'une ancienne copie de la programmation Turbo C pour IBM , que j'avais l'habitude de jouer avec quelque temps en arrière quand, comment manipuler directement la mémoire graphique et la disposition de la mémoire vidéo en mode texte d'IBM, était décrite en détail. Bien sûr, le système sur lequel le code s'exécutait définissait clairement ce que signifiait l'écriture dans ces adresses, donc tant que vous ne vous inquiétiez pas de la portabilité vers d'autres systèmes, tout allait bien. IIRC, pointers to void étaient un thème commun dans ce livre.
un CVn
@Michael Kjörling: Bien sûr! Les gens aiment faire du sale boulot de temps en temps;)
Chang Peng
18

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.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
Brian R. Bondy
la source
Je ne suis pas d'accord: il y a un problème avant le cout. *apointe 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).
avant
@ereOn: J'ai clarifié davantage ce que je voulais dire par problème, mais non ce n'est pas dangereux en termes de code c ++ valide. Mais il est dangereux en termes de probabilité que l'utilisateur ait fait une erreur et fasse quelque chose de mal. Par exemple, vous essayez de voir comment la pile se développe et vous ne vous souciez que de la valeur de l'adresse et vous ne la déférerez jamais.
Brian R. Bondy,
18

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.

Kerrek SB
la source
18

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:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Cela affiche "y = 123", mais vos résultats peuvent varier (vraiment!). Votre pointeur encombre d'autres variables locales non liées.

AHelps
la source
18

Faites attention à tous les avertissements. Ne résolvez pas seulement les erreurs.
GCC affiche cet avertissement

avertissement: adresse de la variable locale 'a' retournée

C'est la puissance de C ++. Vous devez vous soucier de la mémoire. Avec le -Werrordrapeau, cet avertissement devient une erreur et vous devez maintenant le déboguer.

sam
la source
17

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 à anouveau et vous n'aurez probablement plus autant de chance ... ;-)

Adrian Grigore
la source
16

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é amais plutôt l'emplacement de mémoire où ase trouvait autrefois. Cette différence est très similaire à la différence entre planter et ne pas planter.

Alexander Gessler
la source
14

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, intil y a de fortes chances que la valeur de a(ou l'adresse mémoire qui pointait a) 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!

larsmoa
la source
3
"afficher la valeur du bloc de mémoire avec l'adresse qui était occupée par un" n'est pas tout à fait correct. Cela donne l'impression que son code a une signification bien définie, ce qui n'est pas le cas. Vous avez raison de dire que c'est probablement ainsi que la plupart des compilateurs l'implémenteraient.
Brennan Vincent
@BrennanVincent: Pendant que le stockage était occupé par a, le pointeur contenait l'adresse de a. 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.
supercat
@BrennanVincent: Par exemple, même si la norme ne requiert pas que les implémentations permettent reallocde 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ée realloc.
supercat
14

Il peut, car aest une variable allouée temporairement pour la durée de vie de sa portée ( foofonction). 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.

littleadv
la source
12

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):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
Mykola
la source
5

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 de aet aest 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é.

Ghulam Moinul Quadir
la source
4

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).

Ayub
la source
1

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:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

Contrairement à votre exemple, avec cet exemple, vous êtes:

  • allouer de la mémoire pour int dans une fonction locale
  • cette adresse mémoire est toujours valide également à l'expiration de la fonction (elle n'est supprimée par personne)
  • l'adresse mémoire est fiable (ce bloc de mémoire n'est pas considéré comme libre, il ne sera donc pas remplacé jusqu'à ce qu'il soit supprimé)
  • l'adresse mémoire doit être supprimée lorsqu'elle n'est pas utilisée. (voir la suppression à la fin du programme)
Nobun
la source
Avez-vous ajouté quelque chose qui n'était pas déjà couvert par les réponses existantes? Et veuillez ne pas utiliser de pointeurs bruts / new.
Courses de légèreté en orbite
1
Le demandeur a utilisé des pointeurs bruts. J'ai fait un exemple qui reflétait exactement l'exemple qu'il a fait afin de lui permettre de voir la différence entre un pointeur non fiable et un pointeur fidèle. En fait, il y a une autre réponse similaire à la mienne, mais elle utilise strcpy qui, à mon humble avis, pourrait être moins clair pour un codeur novice que mon exemple qui utilise new.
Nobun
Ils n'ont pas utilisé new. Vous leur apprenez à utiliser new. Mais vous ne devriez pas utiliser new.
Courses de légèreté en orbite
Donc, à votre avis, il vaut mieux passer une adresse à une variable locale qui est détruite dans une fonction que d'allouer réellement de la mémoire? Cela n'a aucun sens. Comprendre le concept d'allocation de la mémoire de désallocation est important, à mon humble avis, principalement si vous posez des questions sur les pointeurs (le demandeur n'a pas utilisé de nouveaux pointeurs, mais des pointeurs utilisés).
Nobun
Quand ai-je dit ça? Non, il est préférable d'utiliser des pointeurs intelligents pour indiquer correctement la propriété de la ressource référencée. Ne l'utilisez pas newen 2019 (sauf si vous écrivez du code de bibliothèque) et n'enseignez pas non plus aux nouveaux arrivants! À votre santé.
Courses de légèreté en orbite