Kann auf den Speicher einer lokalen Variablen außerhalb ihres Bereichs zugegriffen werden?

1029

Ich habe den folgenden Code.

#include <iostream>

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

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

Und der Code läuft nur ohne Laufzeitausnahmen!

Die Ausgabe war 58

Wie kann es sein? Ist der Speicher einer lokalen Variablen außerhalb ihrer Funktion nicht unzugänglich?

unbekannt
quelle
14
Dies wird nicht einmal so kompiliert, wie es ist. Wenn Sie das fehlerhafte Geschäft beheben, warnt gcc weiterhin address of local variable ‘a’ returned. Valgrind ShowsInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe
76
@Serge: In meiner Jugend habe ich einmal an einem kniffligen Nullring-Code gearbeitet, der auf dem Netware-Betriebssystem ausgeführt wurde und bei dem der Stapelzeiger auf eine Weise bewegt wurde, die vom Betriebssystem nicht genau genehmigt wurde. Ich würde wissen, wann ich einen Fehler gemacht habe, weil der Stapel oft den Bildschirmspeicher überlappte und ich einfach beobachten konnte, wie die Bytes direkt auf das Display geschrieben wurden. Mit so etwas kommt man heutzutage nicht mehr durch.
Eric Lippert
23
lol. Ich musste die Frage und einige Antworten lesen, bevor ich überhaupt verstand, wo das Problem liegt. Ist das eigentlich eine Frage zum Zugriffsbereich der Variablen? Sie verwenden nicht einmal 'a' außerhalb Ihrer Funktion. Und das ist alles was dazu gehört. Das Herumwerfen einiger Speicherreferenzen ist ein völlig anderes Thema als der variable Bereich.
Erikbwork
10
Betrogene Antwort bedeutet nicht betrogene Frage. Viele der betrogenen Fragen, die hier vorgeschlagen wurden, sind völlig unterschiedliche Fragen, die sich zufällig auf dasselbe zugrunde liegende Symptom beziehen ... aber der Fragesteller weiß, wie er das weiß, also sollten sie offen bleiben. Ich habe einen älteren Betrüger geschlossen und ihn in diese Frage eingefügt, die offen bleiben sollte, da sie eine sehr gute Antwort hat.
Joel Spolsky
16
@Joel: Wenn die Antwort hier gut ist, sollte sie in ältere Fragen eingefügt werden, von denen dies ein Betrug ist, nicht umgekehrt. Und diese Frage ist in der Tat ein Betrug der anderen hier und dann vorgeschlagenen Fragen (obwohl einige der vorgeschlagenen Fragen besser passen als andere). Beachten Sie, dass ich Erics Antwort gut finde. (Tatsächlich habe ich diese Frage markiert, um die Antworten in eine der älteren Fragen zusammenzuführen, um die älteren Fragen zu retten.)
sbi

Antworten:

4801

Wie kann es sein? Ist der Speicher einer lokalen Variablen außerhalb ihrer Funktion nicht unzugänglich?

Sie mieten ein Hotelzimmer. Sie legen ein Buch in die oberste Schublade des Nachttisches und schlafen ein. Sie checken am nächsten Morgen aus, aber "vergessen", Ihren Schlüssel zurückzugeben. Du stiehlst den Schlüssel!

Eine Woche später kehren Sie ins Hotel zurück, checken nicht ein, schleichen sich mit Ihrem gestohlenen Schlüssel in Ihr altes Zimmer und schauen in die Schublade. Dein Buch ist noch da. Erstaunlich!

Wie kann das sein? Ist der Inhalt einer Hotelzimmerschublade nicht unzugänglich, wenn Sie das Zimmer nicht gemietet haben?

Nun, offensichtlich kann dieses Szenario in der realen Welt kein Problem sein. Es gibt keine mysteriöse Kraft, die dazu führt, dass Ihr Buch verschwindet, wenn Sie nicht mehr berechtigt sind, im Raum zu sein. Es gibt auch keine mysteriöse Kraft, die Sie daran hindert, einen Raum mit einem gestohlenen Schlüssel zu betreten.

Die Hotelleitung ist nicht verpflichtet , Ihr Buch zu entfernen. Sie haben mit ihnen keinen Vertrag geschlossen, der besagt, dass sie Dinge für Sie zerreißen, wenn Sie sie zurücklassen. Wenn Sie illegal Ihr Zimmer mit einem gestohlenen Schlüssel erneut eingeben es zurück zu bekommen, ist das Hotel Sicherheitspersonal nicht erforderlich fangen Sie schleichen in. Sie haben keinen Vertrag mit ihnen machen, der „sagte , wenn ich versuche , meine zu schleichen zurück in Zimmer später müssen Sie mich aufhalten. " Sie haben vielmehr einen Vertrag mit ihnen unterzeichnet, der besagt: "Ich verspreche, mich später nicht mehr in mein Zimmer zu schleichen", einen Vertrag, den Sie gebrochen haben .

In dieser Situation kann alles passieren . Das Buch kann da sein - du hast Glück gehabt. Das Buch eines anderen kann dort sein und Ihr Buch könnte im Ofen des Hotels sein. Jemand könnte genau dort sein, wenn Sie hereinkommen und Ihr Buch in Stücke reißen. Das Hotel hätte den Tisch und das Buch komplett entfernen und durch einen Kleiderschrank ersetzen können. Das gesamte Hotel könnte gerade abgerissen und durch ein Fußballstadion ersetzt werden, und Sie werden bei einer Explosion sterben, während Sie sich herumschleichen.

Sie wissen nicht, was passieren wird; Als Sie aus dem Hotel auscheckten und einen Schlüssel stahlen, um ihn später illegal zu verwenden, gaben Sie das Recht auf, in einer vorhersehbaren, sicheren Welt zu leben, weil Sie sich entschieden hatten, gegen die Regeln des Systems zu verstoßen.

C ++ ist keine sichere Sprache . Es wird Ihnen fröhlich erlauben, die Regeln des Systems zu brechen. Wenn Sie versuchen, etwas Illegales und Dummes zu tun, wie in einen Raum zurückzukehren, in dem Sie nicht autorisiert sind, und durch einen Schreibtisch zu stöbern, der möglicherweise nicht mehr vorhanden ist, wird C ++ Sie nicht aufhalten. Sicherere Sprachen als C ++ lösen dieses Problem, indem Sie Ihre Leistung einschränken - beispielsweise durch eine viel strengere Kontrolle über Schlüssel.

AKTUALISIEREN

Heilige Güte, diese Antwort bekommt viel Aufmerksamkeit. (Ich bin mir nicht sicher warum - ich hielt es nur für eine "lustige" kleine Analogie, aber was auch immer.)

Ich dachte, es wäre vielleicht sinnvoll, dies mit ein paar weiteren technischen Gedanken ein wenig zu aktualisieren.

Compiler generieren Code, der die Speicherung der von diesem Programm manipulierten Daten verwaltet. Es gibt viele verschiedene Möglichkeiten, Code zum Verwalten des Speichers zu generieren, aber im Laufe der Zeit haben sich zwei grundlegende Techniken festgesetzt.

Die erste besteht darin, eine Art "langlebigen" Speicherbereich zu haben, in dem die "Lebensdauer" jedes Bytes im Speicher - dh der Zeitraum, in dem es einer Programmvariablen gültig zugeordnet ist - nicht einfach vorhergesagt werden kann von Zeit. Der Compiler generiert Aufrufe an einen "Heap-Manager", der weiß, wie Speicher dynamisch zugewiesen wird, wenn er benötigt wird, und ihn zurückfordert, wenn er nicht mehr benötigt wird.

Die zweite Methode besteht darin, einen "kurzlebigen" Speicherbereich zu haben, in dem die Lebensdauer jedes Bytes bekannt ist. Hier folgen die Lebensdauern einem „Verschachtelungsmuster“. Die langlebigsten dieser kurzlebigen Variablen werden vor allen anderen kurzlebigen Variablen zugewiesen und zuletzt freigegeben. Variablen mit kürzerer Lebensdauer werden nach den Variablen mit der längsten Lebensdauer zugewiesen und vor ihnen freigegeben. Die Lebensdauer dieser kurzlebigen Variablen ist innerhalb der Lebensdauer längerlebiger Variablen „verschachtelt“.

Lokale Variablen folgen dem letzteren Muster; Wenn eine Methode eingegeben wird, werden ihre lokalen Variablen lebendig. Wenn diese Methode eine andere Methode aufruft, werden die lokalen Variablen der neuen Methode lebendig. Sie sind tot, bevor die lokalen Variablen der ersten Methode tot sind. Die relative Reihenfolge der Anfänge und Enden der Lebensdauer von Speichern, die mit lokalen Variablen verknüpft sind, kann im Voraus ermittelt werden.

Aus diesem Grund werden lokale Variablen normalerweise als Speicher in einer "Stapel" -Datenstruktur generiert, da ein Stapel die Eigenschaft hat, dass das erste, was darauf gedrückt wird, das letzte ist, das herausspringt.

Es ist, als ob das Hotel beschließt, Zimmer nur nacheinander zu vermieten, und Sie können erst auschecken, wenn alle Zimmer eine höhere Zimmernummer haben als Sie.

Denken wir also über den Stapel nach. In vielen Betriebssystemen erhalten Sie einen Stapel pro Thread und der Stapel wird einer bestimmten festen Größe zugewiesen. Wenn Sie eine Methode aufrufen, werden Inhalte auf den Stapel verschoben. Wenn Sie dann einen Zeiger auf den Stapel aus Ihrer Methode zurückgeben, wie dies hier im Originalposter der Fall ist, ist dies nur ein Zeiger auf die Mitte eines vollständig gültigen Millionen-Byte-Speicherblocks. In unserer Analogie checken Sie aus dem Hotel aus; Wenn Sie dies tun, haben Sie gerade aus dem am höchsten belegten Raum ausgecheckt. Wenn niemand anderes nach Ihnen eincheckt und Sie illegal in Ihr Zimmer zurückkehren, sind alle Ihre Sachen garantiert immer noch in diesem bestimmten Hotel vorhanden .

Wir verwenden Stapel für temporäre Geschäfte, weil sie wirklich billig und einfach sind. Eine Implementierung von C ++ ist nicht erforderlich, um einen Stapel zum Speichern von Einheimischen zu verwenden. es könnte den Haufen gebrauchen. Dies ist nicht der Fall, da dies das Programm verlangsamen würde.

Eine Implementierung von C ++ ist nicht erforderlich, um den Müll, den Sie auf dem Stapel gelassen haben, unberührt zu lassen, damit Sie später illegal darauf zurückkommen können. Es ist völlig legal, dass der Compiler Code generiert, der alles in dem "Raum", den Sie gerade verlassen haben, auf Null zurücksetzt. Es ist nicht so, denn das wäre wieder teuer.

Eine Implementierung von C ++ ist nicht erforderlich, um sicherzustellen, dass beim logischen Verkleinern des Stapels die früher gültigen Adressen weiterhin im Speicher zugeordnet werden. Die Implementierung darf dem Betriebssystem mitteilen, dass "wir diese Stapelseite jetzt nicht mehr verwenden. Bis ich etwas anderes sage, geben Sie eine Ausnahme aus, die den Prozess zerstört, wenn jemand die zuvor gültige Stapelseite berührt." Wiederum tun Implementierungen dies nicht wirklich, weil es langsam und unnötig ist.

Stattdessen können Sie mit Implementierungen Fehler machen und damit durchkommen. Meistens. Bis eines Tages etwas wirklich Schreckliches schief geht und der Prozess explodiert.

Das ist problematisch. Es gibt viele Regeln und es ist sehr leicht, sie versehentlich zu brechen. Ich habe sicherlich viele Male. Und schlimmer noch, das Problem tritt oft nur dann auf, wenn festgestellt wird, dass der Speicher Milliarden von Nanosekunden nach der Korruption beschädigt ist, wenn es sehr schwer ist herauszufinden, wer ihn durcheinander gebracht hat.

Mehr speichersichere Sprachen lösen dieses Problem, indem sie Ihre Leistung einschränken. In "normalem" C # gibt es einfach keine Möglichkeit, die Adresse eines Einheimischen zu übernehmen und zurückzugeben oder für später zu speichern. Sie können die Adresse eines Einheimischen verwenden, aber die Sprache ist so konzipiert, dass es unmöglich ist, sie nach Ablauf der Lebensdauer des Orts zu verwenden. Um die Adresse eines lokalen Objekts zu übernehmen und zurückzugeben, müssen Sie den Compiler in einen speziellen "unsicheren" Modus versetzen und das Wort "unsicher" in Ihr Programm aufnehmen, um auf die Tatsache aufmerksam zu machen, dass Sie dies wahrscheinlich tun etwas Gefährliches, das gegen die Regeln verstoßen könnte.

Zur weiteren Lektüre:

Eric Lippert
quelle
56
@muntoo: Leider ist es nicht so, dass das Betriebssystem eine Warnsirene auslöst, bevor eine Seite des virtuellen Speichers freigegeben oder freigegeben wird. Wenn Sie mit diesem Speicher herumspielen, wenn Sie ihn nicht mehr besitzen, hat das Betriebssystem das Recht, den gesamten Prozess herunterzufahren, wenn Sie eine freigegebene Seite berühren. Boom!
Eric Lippert
83
@ Kyle: Das machen nur sichere Hotels. Die unsicheren Hotels erzielen messbare Gewinngewinne, wenn sie keine Zeit mit Programmierschlüsseln verschwenden müssen.
Alexander Torstling
498
@cyberguijarro: Dass C ++ nicht speichersicher ist, ist einfach eine Tatsache. Es "schlägt" nichts. Hätte ich zum Beispiel gesagt: "C ++ ist eine schreckliche Mischung aus unterbestimmten, übermäßig komplexen Funktionen, die auf einem spröden, gefährlichen Speichermodell liegen, und ich bin jeden Tag dankbar, dass ich nicht mehr daran arbeite, um meine eigene Gesundheit zu gewährleisten." das würde C ++ schlagen. Wenn Sie darauf hinweisen, dass es nicht speichersicher ist , wird erklärt, warum das Originalplakat dieses Problem aufweist. Es beantwortet die Frage und redigiert nicht.
Eric Lippert
50
Genau genommen sollte in der Analogie erwähnt werden, dass die Rezeptionistin im Hotel sich sehr darüber gefreut hat, dass Sie den Schlüssel mitgenommen haben. "Oh, macht es dir etwas aus, wenn ich diesen Schlüssel mitnehme?" "Mach weiter. Warum sollte es mich interessieren? Ich arbeite nur hier." Es wird nicht illegal, bis Sie versuchen, es zu verwenden.
Philsquared
140
Bitte, bitte denken Sie zumindest daran, eines Tages ein Buch zu schreiben. Ich würde es kaufen, selbst wenn es nur eine Sammlung überarbeiteter und erweiterter Blog-Beiträge wäre, und ich bin sicher, dass dies auch viele Leute tun würden. Aber ein Buch mit Ihren ursprünglichen Gedanken zu verschiedenen programmierbezogenen Themen wäre eine gute Lektüre. Ich weiß, dass es unglaublich schwer ist, die Zeit dafür zu finden, aber bitte überlegen Sie, eine zu schreiben.
Dyppl
276

Was Sie hier tun , ist einfach zu lesen und in den Speicher zu schreiben , dass verwendet die Adresse zu sein a. Jetzt, wo Sie sich außerhalb befinden foo, ist es nur ein Zeiger auf einen zufälligen Speicherbereich. Es kommt einfach so vor, dass in Ihrem Beispiel dieser Speicherbereich vorhanden ist und im Moment von nichts anderem verwendet wird. Sie brechen nichts, indem Sie es weiter verwenden, und nichts anderes hat es noch überschrieben. Daher ist das 5noch da. In einem echten Programm würde dieser Speicher fast sofort wiederverwendet und Sie würden dadurch etwas kaputt machen (obwohl die Symptome möglicherweise erst viel später auftreten!).

Wenn Sie von zurückkehren foo, teilen Sie dem Betriebssystem mit, dass Sie diesen Speicher nicht mehr verwenden und dass er einem anderen Speicher zugewiesen werden kann. Wenn Sie Glück haben und es nie neu zugewiesen wird und das Betriebssystem Sie nicht wieder beim Verwenden erwischt, werden Sie mit der Lüge davonkommen. Es besteht die Möglichkeit, dass Sie am Ende über alles schreiben, was mit dieser Adresse endet.

Wenn Sie sich jetzt fragen, warum sich der Compiler nicht beschwert, liegt dies wahrscheinlich daran, foodass er durch die Optimierung beseitigt wurde. Es wird Sie normalerweise vor solchen Dingen warnen. C setzt jedoch voraus, dass Sie wissen, was Sie tun, und technisch gesehen haben Sie hier nicht gegen den Gültigkeitsbereich verstoßen (es gibt keinen Verweis auf sich aselbst außerhalb von foo), sondern nur auf Speicherzugriffsregeln, die nur eine Warnung und keinen Fehler auslösen.

Kurz gesagt: Dies funktioniert normalerweise nicht, manchmal aber auch zufällig.

Rena
quelle
152

Weil der Speicherplatz noch nicht belegt war. Verlassen Sie sich nicht auf dieses Verhalten.

msw
quelle
1
Mann, das war das längste Warten auf einen Kommentar seit: "Was ist Wahrheit? Sagte Pilatus scherzend." Vielleicht war es eine Gideon-Bibel in dieser Hotelschublade. Und was ist überhaupt mit ihnen passiert? Beachten Sie, dass sie zumindest in London nicht mehr vorhanden sind. Ich denke, dass Sie nach der Gleichstellungsgesetzgebung eine Bibliothek religiöser Traktate benötigen würden.
Rob Kent
Ich hätte schwören können, dass ich das vor langer Zeit geschrieben habe, aber es ist kürzlich aufgetaucht und hat festgestellt, dass meine Antwort nicht da war. Jetzt muss ich Ihre Anspielungen oben herausfinden, da ich davon ausgehe, dass ich amüsiert sein werde, wenn ich>. <
msw
1
Haha. Francis Bacon, einer der größten Essayisten Großbritanniens, von dem einige vermuten, dass er Shakespeares Stücke geschrieben hat, weil sie nicht akzeptieren können, dass ein Gymnasiast aus dem Land, Sohn eines Handschuhmachers, ein Genie sein könnte. So ist das englische Klassensystem. Jesus sagte: "Ich bin die Wahrheit." oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent
84

Eine kleine Ergänzung zu allen Antworten:

wenn du so etwas machst:

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

Die Ausgabe wird wahrscheinlich sein: 7

Dies liegt daran, dass nach der Rückkehr von foo () der Stapel freigegeben und dann von boo () wiederverwendet wird. Wenn Sie die ausführbare Datei zerlegen, sehen Sie sie deutlich.

Michael
quelle
2
Einfaches, aber großartiges Beispiel, um die zugrunde liegende Stapeltheorie zu verstehen. Nur eine Testaddition mit der Angabe "int a = 5;" in foo () als "static int a = 5;" kann verwendet werden, um den Umfang und die Lebensdauer einer statischen Variablen zu verstehen.
Kontrolle
15
-1 "für wird wahrscheinlich 7 " sein. Der Compiler registriert möglicherweise ein in boo. Es könnte es entfernen, weil es unnötig ist. Es besteht eine gute Chance, dass * p nicht 5 sein wird , aber das bedeutet nicht, dass es einen besonders guten Grund gibt, warum es wahrscheinlich 7 sein wird .
Matt
2
Es heißt undefiniertes Verhalten!
Francis Cugler
Warum und wie boowird der fooStapel wiederverwendet ? sind keine Funktionsstapel voneinander getrennt, auch ich bekomme Müll, der diesen Code auf Visual Studio 2015
ausführt
1
@ampawd es ist fast ein Jahr alt, aber nein, "Funktionsstapel" sind nicht voneinander getrennt. Ein KONTEXT hat einen Stapel. Dieser Kontext verwendet seinen Stapel, um main einzugeben, dann abzusteigen foo(), zu existieren und dann abzusteigen boo(). Foo()und Boo()beide treten mit dem Stapelzeiger an derselben Stelle ein. Dies ist jedoch kein Verhalten, auf das man sich verlassen sollte. Andere 'Sachen' (wie Interrupts oder das Betriebssystem) können den Stapel zwischen dem Aufruf von boo()und verwenden foo()und dessen Inhalt ändern ...
Russ Schultz
72

In C ++, Sie können eine beliebige Adresse zugreifen, aber es bedeutet nicht , Sie sollten . Die Adresse, auf die Sie zugreifen, ist nicht mehr gültig. Es funktioniert, weil nichts anderes den Speicher nach der Rückkehr von foo verschlüsselt hat, aber es könnte unter vielen Umständen abstürzen. Versuchen Sie, Ihr Programm mit Valgrind zu analysieren oder es einfach nur optimiert zu kompilieren, und sehen Sie ...

Charles Brunet
quelle
5
Sie meinen wahrscheinlich, Sie können versuchen, auf eine beliebige Adresse zuzugreifen. Weil die meisten heutigen Betriebssysteme kein Programm auf eine Adresse zugreifen lassen; Es gibt unzählige Sicherheitsvorkehrungen zum Schutz des Adressraums. Aus diesem Grund wird es keine weitere LOADLIN.EXE geben.
v010dya
67

Sie lösen niemals eine C ++ - Ausnahme aus, indem Sie auf ungültigen Speicher zugreifen. Sie geben nur ein Beispiel für die allgemeine Idee, auf einen beliebigen Speicherort zu verweisen. Ich könnte das auch so machen:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Hier behandle ich 123456 einfach als Adresse eines Doppelpacks und schreibe darauf. Es können beliebig viele Dinge passieren:

  1. qkönnte tatsächlich eine gültige Adresse eines Doppels sein, z double p; q = &p;.
  2. q könnte irgendwo in den zugewiesenen Speicher zeigen und ich überschreibe dort nur 8 Bytes.
  3. q Punkte außerhalb des zugewiesenen Speichers und der Speichermanager des Betriebssystems sendet ein Segmentierungsfehlersignal an mein Programm, wodurch die Laufzeit es beendet.
  4. Sie gewinnen die Lotterie.

Die Art und Weise, wie Sie es einrichten, ist etwas vernünftiger, dass die zurückgegebene Adresse auf einen gültigen Speicherbereich verweist, da sie sich wahrscheinlich nur ein wenig weiter unten im Stapel befindet, aber immer noch ein ungültiger Speicherort ist, auf den Sie in einem nicht zugreifen können deterministische Mode.

Während der normalen Programmausführung überprüft niemand automatisch die semantische Gültigkeit solcher Speicheradressen für Sie. Ein Speicher-Debugger wie valgrinddieser erledigt dies jedoch gerne. Sie sollten Ihr Programm also ausführen und die Fehler beobachten.

Kerrek SB
quelle
9
Ich werde jetzt nur ein Programm schreiben, das dieses Programm weiter 4) I win the lottery
ausführt
29

Haben Sie Ihr Programm mit aktiviertem Optimierer kompiliert? Die foo()Funktion ist recht einfach und wurde möglicherweise im resultierenden Code eingefügt oder ersetzt.

Aber ich stimme Mark B zu, dass das resultierende Verhalten undefiniert ist.

gastush
quelle
Das ist meine Wette. Das Optimierungsprogramm hat den Funktionsaufruf ausgegeben.
Erik Aronesty
9
Das ist nicht nötig. Da nach foo () keine neue Funktion aufgerufen wird, wird der lokale Stack-Frame der Funktionen einfach noch nicht überschrieben. Fügen Sie nach foo () einen weiteren Funktionsaufruf hinzu, und der 5wird geändert ...
Tomas
Ich habe das Programm mit GCC 4.8 ausgeführt und cout durch printf (und einschließlich stdio) ersetzt. Warnt zu Recht "Warnung: Adresse der lokalen Variablen 'a' zurückgegeben [-Wreturn-local-addr]". Ausgänge 58 ohne Optimierung und 08 mit -O3. Seltsamerweise hat P eine Adresse, obwohl ihr Wert 0 ist. Ich habe NULL (0) als Adresse erwartet.
Kevinf
23

Ihr Problem hat nichts mit dem Umfang zu tun . In dem Code, den Sie anzeigen, mainsieht die Funktion die Namen in der Funktion nicht foo, sodass Sie ain foo nicht direkt mit diesem Namen außerhalb zugreifen können foo.

Das Problem, das Sie haben, ist, warum das Programm beim Verweisen auf unzulässigen Speicher keinen Fehler signalisiert. Dies liegt daran, dass C ++ - Standards keine sehr klare Grenze zwischen illegalem Speicher und legalem Speicher festlegen. Das Verweisen auf etwas im herausgesprungenen Stapel führt manchmal zu Fehlern und manchmal nicht. Es hängt davon ab, ob. Verlassen Sie sich nicht auf dieses Verhalten. Angenommen, es wird beim Programmieren immer zu Fehlern führen, aber beim Debuggen wird niemals ein Fehler angezeigt.

Chang Peng
quelle
Ich erinnere mich an eine alte Kopie von Turbo C Programming für IBM , mit der ich vor einiger Zeit herumgespielt habe, als beschrieben wurde, wie direkt der Grafikspeicher und das Layout des Videospeichers im IBM-Textmodus manipuliert wurden. Natürlich hat das System, auf dem der Code ausgeführt wurde, klar definiert, was das Schreiben an diese Adressen bedeutet. Solange Sie sich keine Gedanken über die Portabilität auf andere Systeme machten, war alles in Ordnung. IIRC, Hinweise auf Leere waren ein häufiges Thema in diesem Buch.
Ein Lebenslauf
@ Michael Kjörling: Sicher! Die Leute machen ab und zu gerne Drecksarbeit;)
Chang Peng
18

Sie geben nur eine Speicheradresse zurück, es ist erlaubt, aber wahrscheinlich ein Fehler.

Ja, wenn Sie versuchen, diese Speicheradresse zu dereferenzieren, haben Sie ein undefiniertes Verhalten.

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
quelle
Ich bin anderer Meinung: Es gibt ein Problem vor dem cout. *azeigt auf nicht zugewiesenen (freigegebenen) Speicher. Selbst wenn Sie es nicht derefenzieren, ist es immer noch gefährlich (und wahrscheinlich falsch).
Am
@ereOn: Ich habe mehr klargestellt, was ich unter Problem verstehe, aber nein, es ist nicht gefährlich in Bezug auf gültigen C ++ - Code. Aber es ist gefährlich in Bezug auf die Wahrscheinlichkeit, dass der Benutzer einen Fehler gemacht hat und etwas Schlechtes tun wird. Vielleicht versuchen Sie zum Beispiel zu sehen, wie der Stapel wächst, und Sie kümmern sich nur um den Adresswert und werden ihn niemals dereferenzieren.
Brian R. Bondy
18

Das ist klassisches undefiniertes Verhalten , das hier vor nicht einmal zwei Tagen diskutiert wurde - suchen Sie ein bisschen auf der Website. Kurz gesagt, Sie hatten Glück, aber alles hätte passieren können, und Ihr Code macht einen ungültigen Zugriff auf den Speicher.

Kerrek SB
quelle
18

Dieses Verhalten ist undefiniert, wie Alex betonte - tatsächlich werden die meisten Compiler davor warnen, da dies ein einfacher Weg ist, um Abstürze zu bekommen.

Probieren Sie dieses Beispiel aus, um ein Beispiel für das gruselige Verhalten zu erhalten, das Sie wahrscheinlich bekommen:

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

Dies gibt "y = 123" aus, aber Ihre Ergebnisse können variieren (wirklich!). Ihr Zeiger blockiert andere, nicht verwandte lokale Variablen.

AHelps
quelle
18

Beachten Sie alle Warnungen. Lösen Sie nicht nur Fehler.
GCC zeigt diese Warnung

Warnung: Adresse der lokalen Variablen 'a' zurückgegeben

Das ist die Kraft von C ++. Sie sollten sich um das Gedächtnis kümmern. Mit dem -WerrorFlag wurde diese Warnung zu einem Fehler und jetzt müssen Sie sie debuggen.

sam
quelle
17

Es funktioniert, weil der Stapel (noch) nicht geändert wurde, seit a dort abgelegt wurde. Rufen Sie einige andere Funktionen auf (die auch andere Funktionen aufrufen), bevor Sie aerneut darauf zugreifen , und Sie werden wahrscheinlich nicht mehr so ​​viel Glück haben ... ;-)

Adrian Grigore
quelle
16

Sie haben tatsächlich undefiniertes Verhalten aufgerufen.

Rückgabe der Adresse eines temporären Werks, aber wenn temporäre Werke am Ende einer Funktion zerstört werden, sind die Ergebnisse des Zugriffs auf sie undefiniert.

Sie haben also nicht geändert a, sondern den Speicherort, an dem sich aeinmal befand. Dieser Unterschied ist dem Unterschied zwischen Absturz und Nichtabsturz sehr ähnlich.

Alexander Gessler
quelle
14

In typischen Compiler-Implementierungen können Sie sich den Code als "Ausdruck des Werts des Speicherblocks mit der Adresse, die früher von a belegt war" vorstellen . Wenn Sie einer Funktion, die eine lokale Funktion festhält, einen neuen Funktionsaufruf hinzufügen, besteht eine intgute Chance, dass sich der Wert a(oder die Speicheradresse, auf die afrüher verwiesen wurde ) ändert. Dies liegt daran, dass der Stapel mit einem neuen Frame überschrieben wird, der andere Daten enthält.

Dies ist jedoch ein undefiniertes Verhalten und Sie sollten sich nicht darauf verlassen, dass es funktioniert!

Larsmoa
quelle
3
"Drucken Sie den Wert des Speicherblocks mit der Adresse aus, die früher von a belegt war" ist nicht ganz richtig. Dies klingt so, als hätte sein Code eine genau definierte Bedeutung, was nicht der Fall ist. Sie haben Recht, dass die meisten Compiler dies wahrscheinlich so implementieren würden.
Brennan Vincent
@BrennanVincent: Während der Speicher belegt war a, enthielt der Zeiger die Adresse von a. Obwohl der Standard nicht verlangt, dass Implementierungen das Verhalten von Adressen nach Ablauf der Lebensdauer ihres Ziels definieren, erkennt er auch an, dass UB auf einigen Plattformen auf eine dokumentierte Weise verarbeitet wird, die für die Umgebung charakteristisch ist. Während die Adresse einer lokalen Variablen nach Ablauf des Gültigkeitsbereichs im Allgemeinen nicht mehr von großem Nutzen ist, können einige andere Arten von Adressen nach der Lebensdauer ihrer jeweiligen Ziele noch von Bedeutung sein.
Supercat
@BrennanVincent: Während der Standard möglicherweise nicht verlangt, dass Implementierungen den reallocVergleich eines übergebenen Zeigers mit dem Rückgabewert zulassen oder Zeiger auf Adressen innerhalb des alten Blocks so anpassen, dass sie auf den neuen verweisen, tun dies einige Implementierungen und Code, der eine solche Funktion ausnutzt, kann effizienter sein als Code, der jegliche Aktion - sogar Vergleiche - vermeiden muss, die Zeiger auf die Zuweisung enthält, die gegeben wurde realloc.
Supercat
14

Dies ist möglich, da aeine Variable vorübergehend für die Lebensdauer ihres Bereichs ( fooFunktion) zugewiesen wird . Nach Ihrer Rückkehr aus foodem Speicher ist frei und kann überschrieben werden.

Was Sie tun, wird als undefiniertes Verhalten beschrieben . Das Ergebnis kann nicht vorhergesagt werden.

littleadv
quelle
12

Die Dinge mit korrekter (?) Konsolenausgabe können sich dramatisch ändern, wenn Sie :: printf verwenden, aber nicht cout. Sie können mit dem Debugger im folgenden Code herumspielen (getestet auf 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); 
}
Mykola
quelle
5

Nach der Rückkehr von einer Funktion werden alle Bezeichner zerstört, anstatt Werte an einem Speicherort zu speichern, und wir können die Werte nicht finden, ohne einen Bezeichner zu haben. Dieser Speicherort enthält jedoch weiterhin den von der vorherigen Funktion gespeicherten Wert.

Hier gibt die Funktion foo()also die Adresse von zurück aund awird nach der Rückgabe ihrer Adresse zerstört. Über diese zurückgegebene Adresse können Sie auf den geänderten Wert zugreifen.

Lassen Sie mich ein Beispiel aus der realen Welt nehmen:

Angenommen, ein Mann versteckt Geld an einem Ort und teilt Ihnen den Ort mit. Nach einiger Zeit stirbt der Mann, der Ihnen den Geldstandort mitgeteilt hat. Aber Sie haben immer noch Zugang zu diesem versteckten Geld.

Ghulam Moinul Quadir
quelle
4

Es ist eine "schmutzige" Art, Speicheradressen zu verwenden. Wenn Sie eine Adresse (einen Zeiger) zurückgeben, wissen Sie nicht, ob sie zum lokalen Bereich einer Funktion gehört. Es ist nur eine Adresse. Nachdem Sie die Funktion 'foo' aufgerufen haben, wurde diese Adresse (Speicherort) von 'a' bereits dort im (zumindest vorerst sicher) adressierbaren Speicher Ihrer Anwendung (Prozess) zugewiesen. Nachdem die 'foo'-Funktion zurückgegeben wurde, kann die Adresse von' a 'als' schmutzig 'betrachtet werden, sie ist jedoch vorhanden, wird weder bereinigt noch durch Ausdrücke in einem anderen Teil des Programms gestört / geändert (zumindest in diesem speziellen Fall). Der AC / C ++ - Compiler hindert Sie nicht an einem solchen "schmutzigen" Zugriff (könnte Sie jedoch warnen, wenn Sie sich darum kümmern).

Ayub
quelle
1

Ihr Code ist sehr riskant. Sie erstellen eine lokale Variable (die nach Funktionsende als zerstört gilt) und geben die Speicheradresse dieser Variablen zurück, nachdem sie zerstört wurde.

Dies bedeutet, dass die Speicheradresse gültig sein kann oder nicht, und Ihr Code ist anfällig für mögliche Speicheradressprobleme (z. B. Segmentierungsfehler).

Dies bedeutet, dass Sie eine sehr schlechte Sache tun, weil Sie eine Speicheradresse an einen Zeiger übergeben, der überhaupt nicht vertrauenswürdig ist.

Betrachten Sie stattdessen dieses Beispiel und testen Sie es:

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

Im Gegensatz zu Ihrem Beispiel sind Sie mit diesem Beispiel:

  • Zuweisen von Speicher für int in eine lokale Funktion
  • Diese Speicheradresse ist auch nach Ablauf der Funktion noch gültig (sie wird von niemandem gelöscht).
  • Die Speicheradresse ist vertrauenswürdig (dieser Speicherblock wird nicht als frei betrachtet, sodass er erst überschrieben wird, wenn er gelöscht wird.)
  • Die Speicheradresse sollte gelöscht werden, wenn sie nicht verwendet wird. (siehe Löschen am Ende des Programms)
Nobun
quelle
Haben Sie etwas hinzugefügt, das noch nicht in den vorhandenen Antworten enthalten ist? Und bitte verwenden Sie keine rohen Zeiger / new.
Leichtigkeitsrennen im Orbit
1
Der Fragesteller verwendete rohe Zeiger. Ich habe ein Beispiel gemacht, das genau das Beispiel widerspiegelt, das er gemacht hat, damit er den Unterschied zwischen einem nicht vertrauenswürdigen und einem vertrauenswürdigen Zeiger erkennen kann. Eigentlich gibt es eine andere Antwort, die meiner ähnlich ist, aber sie verwendet strcpy, was meiner Meinung nach für einen unerfahrenen Codierer weniger klar sein könnte als mein Beispiel, das new verwendet.
Nobun
Sie haben nicht benutzt new. Du bringst ihnen bei, es zu benutzen new. Aber du solltest nicht benutzen new.
Leichtigkeitsrennen im Orbit
Ihrer Meinung nach ist es also besser, eine Adresse an eine lokale Variable zu übergeben, die in einer Funktion zerstört wird, als tatsächlich Speicher zuzuweisen? Das macht keinen Sinn. Es ist wichtig, das Konzept der Zuweisung und Freigabe von Speicher zu verstehen, vor allem, wenn Sie nach Zeigern fragen (der Fragesteller hat keine neuen, sondern verwendete Zeiger verwendet).
Nobun
Wann habe ich das gesagt? Nein, es ist besser, intelligente Zeiger zu verwenden, um den Besitz der referenzierten Ressource korrekt anzuzeigen. Verwenden Sie es nicht newim Jahr 2019 (es sei denn, Sie schreiben Bibliothekscode) und bringen Sie Neulingen dies auch nicht bei! Prost.
Leichtigkeitsrennen im Orbit