È possibile accedere alla memoria di una variabile locale al di fuori del suo ambito?


1029

Ho il codice seguente.

#include <iostream>

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

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

E il codice è in esecuzione senza eccezioni di runtime!

L'output è stato 58

Come può essere? La memoria di una variabile locale non è inaccessibile al di fuori della sua funzione?


14
questo non si compila nemmeno così com'è; se si risolvono gli affari non conformi, gcc avvertirà comunque address of local variable ‘a’ returned; spettacoli di ValgrindInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe

76
@Serge: Quando ero giovane, una volta ho lavorato su un tipo di codice zero-ring complicato che correva sul sistema operativo Netware che comportava un movimento intelligente attorno al puntatore dello stack in un modo non esattamente sanzionato dal sistema operativo. Lo sapevo quando avevo fatto un errore perché spesso lo stack finiva per sovrapporsi alla memoria dello schermo e potevo solo guardare i byte che venivano scritti direttamente sul display. In questi giorni non puoi cavartela con questo genere di cose.
Eric Lippert,

23
lol. Avevo bisogno di leggere la domanda e alcune risposte prima ancora di capire dove fosse il problema. È davvero una domanda sull'ambito di accesso della variabile? Non usi nemmeno una "a" al di fuori della tua funzione. E questo è tutto ciò che c'è da fare. Lanciare attorno ad alcuni riferimenti di memoria è un argomento totalmente diverso dall'ambito variabile.
erikbwork,

10
La risposta doppia non significa una domanda doppia. Molte delle domande duplici che le persone qui proposte sono domande completamente diverse che fanno riferimento allo stesso sintomo sottostante ... ma l'interrogante ha modo di saperlo, quindi dovrebbe rimanere aperto. Ho chiuso un vecchio duplicato e l'ho unito a questa domanda che dovrebbe rimanere aperta perché ha un'ottima risposta.
Joel Spolsky

16
@Joel: se la risposta qui è buona, dovrebbe essere unita a domande più vecchie , di cui si tratta di un imbroglione, non viceversa. E questa domanda è davvero una copia delle altre domande proposte qui e poi alcune (anche se alcune delle proposte si adattano meglio di altre). Nota che penso che la risposta di Eric sia buona. (In effetti, ho contrassegnato questa domanda per fondere le risposte in una delle domande più vecchie al fine di salvare le domande più vecchie.)
sbi

Risposte:


4801

Come può essere? La memoria di una variabile locale non è inaccessibile al di fuori della sua funzione?

Si affitta una stanza d'albergo. Metti un libro nel primo cassetto del comodino e vai a dormire. Fai il check out la mattina dopo, ma "dimentica" di restituire la chiave. Hai rubato la chiave!

Una settimana dopo, ritorni in hotel, non fare il check-in, sgattaiolare nella tua vecchia stanza con la chiave rubata e guardare nel cassetto. Il tuo libro è ancora lì. Stupefacente!

Come può essere? Il contenuto del cassetto di una stanza d'albergo non è inaccessibile se non l'hai affittata?

Bene, ovviamente quello scenario può accadere nel mondo reale senza problemi. Non esiste una forza misteriosa che fa scomparire il tuo libro quando non sei più autorizzato a trovarti nella stanza. Né esiste una forza misteriosa che ti impedisce di entrare in una stanza con una chiave rubata.

La direzione dell'hotel non è tenuta a rimuovere il tuo libro. Non hai stipulato un contratto con loro che dice che se lasci qualcosa alle loro spalle, te lo distruggeranno. Se rientri illegalmente nella tua camera con una chiave rubata per riaverla, il personale di sicurezza dell'hotel non è tenuto a sorprenderti di nascosto. Non hai stipulato un contratto con loro che dice "se provo a intrufolarmi di nuovo nel mio stanza più tardi, devi fermarmi. " Piuttosto, hai firmato un contratto con loro che diceva "Prometto di non rientrare più tardi nella mia stanza", un contratto che hai rotto .

In questa situazione può succedere di tutto . Il libro può essere lì - sei stato fortunato. Il libro di qualcun altro può essere lì e il tuo potrebbe essere nella fornace dell'hotel. Qualcuno potrebbe essere lì quando entri, facendo a pezzi il tuo libro. L'hotel avrebbe potuto rimuovere completamente il tavolo e prenotare e sostituirlo con un armadio. L'intero hotel potrebbe essere quasi distrutto e sostituito con uno stadio di calcio, e morirai in un'esplosione mentre ti stai avvicinando di soppiatto.

Non sai cosa succederà; quando hai fatto il check out dall'hotel e hai rubato una chiave da utilizzare illegalmente in seguito, hai rinunciato al diritto di vivere in un mondo prevedibile e sicuro perché hai scelto di infrangere le regole del sistema.

C ++ non è un linguaggio sicuro . Ti permetterà allegramente di infrangere le regole del sistema. Se provi a fare qualcosa di illegale e sciocco come tornare in una stanza in cui non sei autorizzato a entrare e frugare in una scrivania che potrebbe anche non esserci più, C ++ non ti fermerà. Linguaggi più sicuri del C ++ risolvono questo problema limitando il tuo potere, ad esempio con un controllo molto più rigoroso sui tasti.

AGGIORNARE

Santo cielo, questa risposta sta suscitando molta attenzione. (Non sono sicuro del perché - l'ho considerato solo una piccola "analogia" divertente, ma qualunque cosa.)

Ho pensato che potrebbe essere germano aggiornarlo un po 'con qualche pensiero tecnico in più.

I compilatori si occupano della generazione di codice che gestisce la memorizzazione dei dati manipolati da quel programma. Esistono molti modi diversi di generare codice per gestire la memoria, ma nel tempo si sono radicate due tecniche di base.

Il primo è avere una sorta di area di memoria "longevo" in cui la "durata" di ciascun byte nella memoria - ovvero il periodo di tempo in cui è validamente associato ad una variabile di programma - non può essere facilmente prevista in anticipo di tempo. Il compilatore genera chiamate in un "gestore di heap" che sa come allocare dinamicamente lo spazio di archiviazione quando è necessario e recuperarlo quando non è più necessario.

Il secondo metodo consiste nell'avere un'area di archiviazione "di breve durata" in cui è nota la durata di ciascun byte. Qui, le vite seguono uno schema di "nidificazione". La più lunga durata di queste variabili di breve durata verrà allocata prima di qualsiasi altra variabile di breve durata e verrà liberata per ultima. Le variabili a vita più breve verranno allocate dopo quelle a vita più lunga e verranno liberate prima di esse. La durata di queste variabili di breve durata è "nidificata" all'interno di quelle di più lunga durata.

Le variabili locali seguono quest'ultimo schema; quando viene inserito un metodo, le sue variabili locali prendono vita. Quando quel metodo chiama un altro metodo, le variabili locali del nuovo metodo prendono vita. Saranno morti prima che le variabili locali del primo metodo siano morte. L'ordine relativo degli inizi e dei finali delle vite degli archivi associati alle variabili locali può essere elaborato in anticipo.

Per questo motivo, le variabili locali vengono generalmente generate come memoria su una struttura di dati "stack", poiché uno stack ha la proprietà che la prima cosa che viene spinta su di essa sarà l'ultima cosa spuntata.

È come se l'hotel decidesse di affittare solo le camere in sequenza, e non è possibile effettuare il check-out fino a quando tutti con un numero di camera superiore a quello che si è verificato.

Quindi pensiamo allo stack. In molti sistemi operativi si ottiene uno stack per thread e lo stack viene assegnato a una determinata dimensione fissa. Quando chiami un metodo, le cose vengono inserite nello stack. Se poi passi un puntatore allo stack fuori dal tuo metodo, come fa il poster originale qui, è solo un puntatore al centro di un blocco di memoria da un milione di byte completamente valido. Nella nostra analogia, fai il check out dall'hotel; quando lo fai, sei appena uscito dalla stanza occupata con il numero più alto. Se nessun altro effettua il check-in dopo di te e torni nella tua camera illegalmente, tutte le tue cose sono garantite per essere ancora lì in questo particolare hotel .

Usiamo stack per negozi temporanei perché sono davvero economici e facili. Non è richiesta un'implementazione di C ++ per utilizzare uno stack per l'archiviazione dei locali; potrebbe usare l'heap. Non lo fa, perché ciò renderebbe il programma più lento.

Non è necessaria un'implementazione di C ++ per lasciare intatta la spazzatura che hai lasciato nello stack in modo da poter tornare in seguito in modo illegale; è perfettamente legale per il compilatore generare codice che ritorni a zero tutto nella "stanza" che hai appena lasciato libero. Non perché, di nuovo, sarebbe costoso.

Non è necessaria un'implementazione di C ++ per garantire che quando lo stack si restringe logicamente, gli indirizzi che erano validi erano ancora mappati in memoria. L'implementazione può dire al sistema operativo "abbiamo finito di usare questa pagina di stack ora. Fino a quando non dico diversamente, emettere un'eccezione che distrugge il processo se qualcuno tocca la pagina di stack precedentemente valida". Ancora una volta, le implementazioni non lo fanno perché è lenta e non necessaria.

Invece, le implementazioni ti consentono di fare errori e cavartela. La maggior parte delle volte. Fino a quando un giorno qualcosa di veramente terribile va storto e il processo esplode.

Questo è problematico. Ci sono molte regole ed è molto facile infrangerle accidentalmente. Ho sicuramente molte volte. E peggio ancora, il problema si presenta spesso solo quando la memoria viene rilevata come miliardi di nanosecondi corrotti dopo che si è verificata la corruzione, quando è molto difficile capire chi l'ha incasinata.

Linguaggi più sicuri per la memoria risolvono questo problema limitando la tua potenza. In C # "normale" semplicemente non c'è modo di prendere l'indirizzo di un locale e restituirlo o conservarlo per dopo. Puoi prendere l'indirizzo di un locale, ma la lingua è progettata in modo intelligente in modo che sia impossibile usarlo dopo la fine del ciclo di vita locale. Per prendere l'indirizzo di un locale e passarlo indietro, devi mettere il compilatore in una modalità speciale "non sicura", e mettere la parola "non sicuro" nel tuo programma, per richiamare l'attenzione sul fatto che probabilmente stai facendo qualcosa di pericoloso che potrebbe infrangere le regole.

Per ulteriori letture:


56
@muntoo: Sfortunatamente non è come se il sistema operativo suonasse una sirena di avvertimento prima di disimpegnare o dislocare una pagina di memoria virtuale. Se stai scherzando con quella memoria quando non la possiedi più, il sistema operativo è perfettamente nei suoi diritti di annullare l'intero processo quando tocchi una pagina deallocata. Boom!
Eric Lippert,

83
@Kyle: solo gli hotel sicuri lo fanno. Gli hotel non sicuri ottengono guadagni misurabili dal non dover perdere tempo con le chiavi di programmazione.
Alexander Torstling,

498
@cyberguijarro: che C ++ non sia sicuro per la memoria è semplicemente un dato di fatto. Non "ruba" nulla. Se avessi detto, ad esempio, "Il C ++ è un orribile miscuglio di caratteristiche troppo specificate e troppo complesse ammucchiate sopra un fragile, pericoloso modello di memoria e sono grato ogni giorno di non lavorarci più per la mia sanità mentale", sarebbe colpire C ++. Sottolineando che non è sicuro per la memoria sta spiegando perché il poster originale sta riscontrando questo problema; sta rispondendo alla domanda, non pubblicizzando.
Eric Lippert,

50
A rigor di termini l'analogia dovrebbe menzionare che l'addetto alla reception dell'hotel era abbastanza felice che tu portassi la chiave con te. "Oh, ti dispiace se prendo questa chiave con me?" "Vai avanti. Perché dovrebbe importarmene? Lavoro solo qui". Non diventa illegale fino a quando non si tenta di utilizzarlo.
philsquared il

140
Per favore, almeno considera di scrivere un libro un giorno. Lo comprerei anche se fosse solo una raccolta di post di blog rivisti e ampliati, e sono sicuro che lo sarebbero anche molte persone. Ma un libro con i tuoi pensieri originali su varie questioni relative alla programmazione sarebbe un'ottima lettura. So che è incredibile trovare il tempo, ma per favore considera di scriverne uno.
Dyppl,

276

Quello che stai facendo qui è semplicemente leggere e scrivere in memoria che era l'indirizzo di a. Ora che sei fuori da foo, è solo un puntatore a qualche area di memoria casuale. Accade semplicemente che nel tuo esempio quell'area di memoria esista e nient'altro la sta usando al momento. Non rompi nulla continuando a usarlo e nient'altro lo ha ancora sovrascritto. Pertanto, il 5è ancora lì. In un vero programma, quella memoria verrebbe riutilizzata quasi immediatamente e si romperà qualcosa facendo questo (anche se i sintomi potrebbero non apparire fino a molto tempo dopo!)

Quando torni da foo, dici al sistema operativo che non stai più usando quella memoria e può essere riassegnato a qualcos'altro. Se sei fortunato e non viene mai riassegnato, e il sistema operativo non ti sorprende a usarlo di nuovo, allora ti allontanerai dalla bugia. È probabile che finirai per scrivere su qualsiasi altra cosa finisca con quell'indirizzo.

Ora, se ti stai chiedendo perché il compilatore non si lamenta, è probabilmente perché è foostato eliminato dall'ottimizzazione. Di solito ti avvertirà di questo genere di cose. C presume che tu sappia cosa stai facendo, e tecnicamente non hai violato l'ambito qui (non c'è alcun riferimento a ase stesso al di fuori di foo), solo regole di accesso alla memoria, che innesca solo un avviso piuttosto che un errore.

In breve: di solito non funzionerà, ma a volte per caso.


152

Perché lo spazio di archiviazione non è stato ancora calpestato. Non contare su quel comportamento.


1
Amico, quella è stata la più lunga attesa di un commento da "Qual è la verità?" Disse scherzando Pilato. Forse era una Bibbia di Gideon nel cassetto di quell'hotel. E comunque cosa è successo a loro? Si noti che non sono più presenti, almeno a Londra. Immagino che ai sensi della legislazione sull'uguaglianza, avresti bisogno di una biblioteca di volantini religiosi.
Rob Kent,

Avrei potuto giurare di averlo scritto tanto tempo fa, ma è spuntato di recente e ho scoperto che la mia risposta non c'era. Ora devo andare a capire le tue allusioni sopra perché mi aspetto che sarò divertito quando lo farò>. <
msw

1
Haha. Francis Bacon, uno dei più grandi saggisti della Gran Bretagna, che alcuni sospettano abbia scritto le opere di Shakespeare, perché non possono accettare che un ragazzo di scuola di grammatica del paese, figlio di un guerriero, possa essere un genio. Tale è il sistema di classe inglese. Gesù disse: "Io sono la verità". oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent,

84

Una piccola aggiunta a tutte le risposte:

se fai qualcosa del genere:

#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);
}

l'uscita probabilmente sarà: 7

Questo perché dopo il ritorno da foo () lo stack viene liberato e quindi riutilizzato da boo (). Se rimuovi il file eseguibile, lo vedrai chiaramente.


2
Semplice, ma ottimo esempio per comprendere la teoria dello stack sottostante. Solo un'aggiunta di test, che dichiara "int a = 5;" in foo () come "static int a = 5;" può essere utilizzato per comprendere l'ambito e la durata di una variabile statica.
controllo

15
-1 "per sarà probabilmente 7 ". Il compilatore potrebbe registrare a in boo. Potrebbe rimuoverlo perché non è necessario. C'è una buona probabilità che * p non sia 5 , ma ciò non significa che ci sia una ragione particolarmente buona per cui probabilmente sarà 7 .
Matt,

2
Si chiama comportamento indefinito!
Francis Cugler,

perché e come booriutilizza lo foostack? non sono stack di funzioni separati l'uno dall'altro, inoltre ottengo immondizia che esegue questo codice su Visual Studio 2015
ampawd

1
@ampawd ha quasi un anno, ma no, le "pile di funzioni" non sono separate l'una dall'altra. Un CONTESTO ha uno stack. Quel contesto usa il suo stack per inserire main, quindi scende in foo(), esiste, quindi scende in boo(). Foo()ed Boo()entrambi entrano con il puntatore dello stack nella stessa posizione. Questo non è tuttavia un comportamento su cui fare affidamento. Altre "cose" (come gli interrupt o il sistema operativo) possono usare lo stack tra la chiamata di boo()e foo(), modificandone il contenuto ...
Russ Schultz,

72

In C ++, puoi accedere a qualsiasi indirizzo, ma ciò non significa che dovresti . L'indirizzo a cui accedi non è più valido. Esso funziona perché nient'altro strapazzate la memoria dopo foo restituito, ma potrebbe bloccarsi in molte circostanze. Prova ad analizzare il tuo programma con Valgrind o anche solo a compilarlo ottimizzato e vedi ...


5
Probabilmente intendi che puoi tentare di accedere a qualsiasi indirizzo. Perché la maggior parte dei sistemi operativi oggi non consentirà a nessun programma di accedere a nessun indirizzo; ci sono tonnellate di garanzie per proteggere lo spazio degli indirizzi. Questo è il motivo per cui non ci sarà un altro LOADLIN.EXE là fuori.
v010dya,

67

Non si genera mai un'eccezione C ++ accedendo alla memoria non valida. Stai solo dando un esempio dell'idea generale di fare riferimento a una posizione di memoria arbitraria. Potrei fare lo stesso in questo modo:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Qui sto semplicemente trattando 123456 come l'indirizzo di un doppio e scrivo ad esso. Potrebbe succedere un numero qualsiasi di cose:

  1. qpotrebbe in effetti essere veramente un indirizzo valido di un doppio, ad es double p; q = &p;.
  2. q potrebbe puntare da qualche parte nella memoria allocata e ho appena sovrascritto 8 byte lì dentro.
  3. q punti all'esterno della memoria allocata e il gestore della memoria del sistema operativo invia un segnale di errore di segmentazione al mio programma, causando il termine del runtime.
  4. Vinci la lotteria.

Il modo in cui lo configuri è un po 'più ragionevole che l'indirizzo restituito punti in un'area di memoria valida, poiché probabilmente sarà solo un po' più in basso nello stack, ma è ancora una posizione non valida a cui non puoi accedere in un moda deterministica.

Nessuno verificherà automaticamente la validità semantica degli indirizzi di memoria come quella per te durante la normale esecuzione del programma. Tuttavia, un debugger di memoria come lo valgrindfarà felicemente, quindi dovresti eseguire il programma attraverso di esso e testimoniare gli errori.


9
Sto per scrivere un programma ora che continua a eseguire questo programma in modo che4) I win the lottery
Aidiakapi

29

Hai compilato il tuo programma con l'ottimizzatore abilitato? Ilfoo() funzione è abbastanza semplice e potrebbe essere stata incorporata o sostituita nel codice risultante.

Ma concordo con Mark B sul fatto che il comportamento risultante non è definito.


Questa è la mia scommessa. L'ottimizzatore ha scaricato la chiamata di funzione.
Erik Aronesty,

9
Non è necessario Poiché non viene chiamata alcuna nuova funzione dopo foo (), il frame dello stack locale delle funzioni semplicemente non è ancora sovrascritto. Aggiungi un'altra funzione invocazione dopo foo (), e 5verrà cambiata ...
Tomas,

Ho eseguito il programma con GCC 4.8, sostituendo cout con printf (e incluso stdio). Avvisa giustamente "avvertimento: indirizzo della variabile locale 'a' restituito [-Wreturn-local-addr]". Uscite 58 senza ottimizzazione e 08 con -O3. Stranamente P ha un indirizzo, anche se il suo valore è 0. Mi aspettavo NULL (0) come indirizzo.
kevinf,

23

Il tuo problema non ha nulla a che fare con l' ambito . Nel codice che mostri, la funzione mainnon vede i nomi nella funzione foo, quindi non puoi accedervi adirettamente con questo nome all'esterno foo.

Il problema riscontrato è il motivo per cui il programma non segnala un errore quando si fa riferimento alla memoria illegale. Questo perché gli standard C ++ non specificano un confine molto chiaro tra memoria illegale e memoria legale. Fare riferimento a qualcosa nello stack spuntato a volte causa errori e talvolta no. Dipende. Non contare su questo comportamento. Supponiamo che causerà sempre errori durante la programmazione, ma supponiamo che non segnalerà mai errori durante il debug.


Ricordo da una vecchia copia di Turbo C Programming per IBM , che ho usato per giocare con un po 'di tempo quando, come manipolare direttamente la memoria grafica e il layout della memoria video in modalità testo di IBM, è stata descritta in modo molto dettagliato. Naturalmente, quindi, il sistema su cui era in esecuzione il codice ha definito chiaramente cosa significava scrivere su quegli indirizzi, quindi finché non ti preoccupavi della portabilità su altri sistemi, tutto andava bene. IIRC, i puntatori al vuoto erano un tema comune in quel libro.
un CVn del

@Michael Kjörling: Sicuro!
Chang Peng,

18

Stai solo restituendo un indirizzo di memoria, è consentito ma probabilmente è un errore.

Sì, se provi a dereferenziare quell'indirizzo di memoria avrai un comportamento indefinito.

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
}

Non sono d'accordo: c'è un problema prima del cout. *apunta alla memoria non allocata (liberata). Anche se non lo fai, è comunque pericoloso (e probabilmente fasullo).
ereOn

@ereOn: ho chiarito di più cosa intendevo per problema, ma no non è pericoloso in termini di codice c ++ valido. Ma è pericoloso in termini di probabilità che l'utente abbia commesso un errore e farà qualcosa di brutto. Forse, ad esempio, stai cercando di vedere come cresce lo stack, e ti preoccupi solo del valore dell'indirizzo e non lo farai mai.
Brian R. Bondy,

18

È un classico comportamento indefinito che è stato discusso qui non due giorni fa: cerca un po 'nel sito. In poche parole, sei stato fortunato, ma sarebbe potuto succedere di tutto e il tuo codice sta rendendo non valido l'accesso alla memoria.


18

Questo comportamento è indefinito, come ha sottolineato Alex - in effetti, la maggior parte dei compilatori avvertirà di non farlo, perché è un modo semplice per ottenere crash.

Per un esempio del tipo di comportamento spettrale si rischia di ottenere, provare questo esempio:

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;
}

Questo stampa "y = 123", ma i risultati possono variare (davvero!). Il puntatore sta bloccando altre variabili locali non correlate.


18

Prestare attenzione a tutti gli avvisi. Non solo risolvere errori.
GCC mostra questo avviso

avviso: indirizzo della variabile locale 'a' restituito

Questa è la potenza di C ++. Dovresti preoccuparti della memoria. Con il -Werrorflag, questo avviso diventa un errore e ora devi eseguirne il debug.


17

Funziona perché lo stack non è stato modificato (ancora) da quando è stato inserito uno. Chiama alcune altre funzioni (che chiamano anche altre funzioni) prima di accedere di anuovo e probabilmente non sarai più così fortunato ... ;-)


16

In realtà hai invocato un comportamento indefinito.

Restituzione dell'indirizzo di un'opera temporanea, ma man mano che i temporali vengono distrutti al termine di una funzione, i risultati dell'accesso ad essi non saranno definiti.

Quindi non hai modificato, ama piuttosto la posizione della memoria in cui auna volta era. Questa differenza è molto simile alla differenza tra crash e non crash.


14

Nelle tipiche implementazioni del compilatore, puoi pensare al codice come "stampa il valore del blocco di memoria con l'indirizzo che era occupato da un". Inoltre, se si aggiunge una nuova chiamata di funzione a una funzione che contiene un locale, intè probabile che il valore di a(o l'indirizzo di memoria autilizzato per indicare) cambi. Ciò accade perché lo stack verrà sovrascritto con un nuovo frame contenente dati diversi.

Tuttavia, questo è un comportamento indefinito e non dovresti fare affidamento su di esso per funzionare!


3
"stampare il valore del blocco di memoria con l'indirizzo che era occupato da un" non è del tutto corretto. Questo fa sembrare che il suo codice abbia un significato ben definito, il che non è il caso. Hai ragione, questo è probabilmente il modo in cui la maggior parte dei compilatori lo implementerebbe.
Brennan Vincent,

@BrennanVincent: mentre la memoria era occupata da a, il puntatore conteneva l'indirizzo di a. Sebbene lo standard non richieda che le implementazioni definiscano il comportamento degli indirizzi al termine della durata del loro target, riconosce anche che su alcune piattaforme UB viene elaborato in maniera documentata caratteristica dell'ambiente. Sebbene l'indirizzo di una variabile locale non sarà generalmente molto utile dopo che è uscito dal campo di applicazione, alcuni altri tipi di indirizzi possono essere significativi dopo la durata dei rispettivi target.
supercat,

@BrennanVincent: Ad esempio, mentre lo Standard potrebbe non richiedere che le implementazioni consentano di confrontare un puntatore passato con reallocil valore restituito, né di regolare i puntatori agli indirizzi all'interno del vecchio blocco in modo che puntino a quello nuovo, alcune implementazioni lo fanno e il codice che sfrutta tale funzionalità può essere più efficiente del codice che deve evitare qualsiasi azione, anche i confronti, che implichi indicazioni sull'allocazione assegnata realloc.
supercat,

14

Può, perché aè una variabile allocata temporaneamente per la durata del suo ambito ( foofunzione). Dopo il ritorno dalla foomemoria è gratuito e può essere sovrascritto.

Quello che stai facendo è descritto come un comportamento indefinito . Il risultato non può essere previsto.


12

Le cose con l'output corretto (?) Della console possono cambiare radicalmente se usi :: printf ma non cout. Puoi giocare con il debugger nel seguente codice (testato su x86, 32-bit, 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); 
}

5

Dopo essere tornati da una funzione, tutti gli identificatori vengono distrutti anziché i valori conservati in una posizione di memoria e non possiamo individuare i valori senza un identificatore, ma tale posizione contiene ancora il valore memorizzato dalla funzione precedente.

Quindi, qui la funzione foo()restituisce l'indirizzo di ae aviene distrutta dopo aver restituito il suo indirizzo. E puoi accedere al valore modificato tramite quell'indirizzo restituito.

Vorrei fare un esempio del mondo reale:

Supponiamo che un uomo nasconda denaro in un luogo e ti dica il luogo. Dopo un po 'di tempo, l'uomo che ti aveva detto la posizione del denaro muore. Ma hai ancora accesso a quei soldi nascosti.


4

È un modo "sporco" di usare gli indirizzi di memoria. Quando si restituisce un indirizzo (puntatore) non si sa se appartiene all'ambito locale di una funzione. È solo un indirizzo. Ora che hai invocato la funzione 'pippo', quell'indirizzo (posizione di memoria) di 'a' era già stato allocato nella memoria indirizzabile (sicura, almeno per ora) della tua applicazione (processo). Dopo il ritorno della funzione "pippo", l'indirizzo di "a" può essere considerato "sporco" ma è lì, non ripulito, né disturbato / modificato dalle espressioni in altre parti del programma (almeno in questo caso specifico). Il compilatore AC / C ++ non ti impedisce tale accesso "sporco" (potresti avvertirti, se ti interessa).


1

Il tuo codice è molto rischioso. Stai creando una variabile locale (che viene considerata distrutta al termine della funzione) e restituisci l'indirizzo di memoria di quella variabile dopo che è stata distrutta.

Ciò significa che l'indirizzo di memoria potrebbe essere valido o meno e il codice sarà vulnerabile a possibili problemi di indirizzo di memoria (ad esempio errore di segmentazione).

Ciò significa che stai facendo una cosa molto brutta, perché stai passando un indirizzo di memoria a un puntatore che non è affatto affidabile.

Considera invece questo esempio e testalo:

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;
}

A differenza del tuo esempio, con questo esempio sei:

  • allocare memoria per int in una funzione locale
  • quell'indirizzo di memoria è ancora valido anche quando la funzione scade, (non viene cancellato da nessuno)
  • l'indirizzo di memoria è affidabile (quel blocco di memoria non è considerato libero, quindi non verrà ignorato fino a quando non verrà eliminato)
  • l'indirizzo di memoria deve essere eliminato quando non utilizzato. (vedi la cancellazione alla fine del programma)

Hai aggiunto qualcosa che non era già coperto dalle risposte esistenti? E per favore non usare i puntatori non elaborati / new.
Lightness Races in Orbit

1
Il richiedente ha usato i puntatori grezzi. Ho fatto un esempio che rifletteva esattamente l'esempio che aveva fatto per permettergli di vedere la differenza tra un puntatore non affidabile e uno fidato. In realtà c'è un'altra risposta simile alla mia, ma usa strcpy che, IMHO, potrebbe essere meno chiaro per un programmatore principiante rispetto al mio esempio che usa nuovo.
Nobun,

Non hanno usato new. Stai insegnando loro a usare new. Ma non dovresti usare new.
Lightness Races in Orbit

Quindi secondo te è meglio passare un indirizzo a una variabile locale che viene distrutta in una funzione piuttosto che allocare effettivamente la memoria? Questo non ha senso. Comprendere il concetto di allocazione della memoria di deallocazione è importante, imho, soprattutto se stai chiedendo dei puntatori (asker non ha usato nuovi, ma usati puntatori).
Nobun,

Quando l'ho detto? No, è meglio utilizzare i puntatori intelligenti per indicare correttamente la proprietà della risorsa di riferimento. Non usare newnel 2019 (a meno che tu non stia scrivendo il codice della biblioteca) e non insegnare nemmeno ai nuovi arrivati ​​a farlo! Saluti.
Lightness Races in Orbit
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.