2015-04-26 14 views
5

Ich lese C-Programmierung - eine moderne Methode von K.N.King, um die C-Programmiersprache zu lernen, und es wurde festgestellt, dass goto Anweisungen Array-Deklarationen variabler Länge nicht überspringen können.Variable Deklaration mit goto überspringen?

Aber jetzt ist die Frage: Warum dürfen goto Sprünge Array-Deklarationen fester Länge und gewöhnliche Deklarationen überspringen? Und genauer, wie verhalten sich solche Beispiele nach dem C99-Standard? Als ich diese Fälle testete, schien es, als wären die Erklärungen nicht übersprungen worden, aber ist das richtig? Sind die Variablen, deren Deklarationen möglicherweise übersprungen wurden, sicher zu verwenden?

1.

goto later; 
int a = 4; 
later: 
printf("%d", a); 

2.

goto later; 
int a; 
later: 
a = 4; 
printf("%d", a); 

3.

goto later; 
int a[4]; 
a[0] = 1; 
later: 
a[1] = 2; 
for(int i = 0; i < sizeof(a)/sizeof(a[0]); i++) 
    printf("%d\n", a[i]); 
+0

@ Mints97 Ah, also, wenn Statements ihre eigenen Blöcke auch ohne zusammengesetzte Anweisungen haben? Ich nehme an das ist die antwort dann :) schade ich kann keine kommentare – MinecraftShamrock

+0

was meinst du damit? was haben die if-aussagen damit zu tun? Und Blöcke und zusammengesetzte Anweisungen sind mehr oder weniger gleich, IIRC – Mints97

+0

@ Mints97 Ich meine, bedingt deklarierte Variablen werden nicht an den Anfang der gesamten Funktion verschoben, sondern nur bis zum Anfang des bedingten "Blocks", in dem sie existieren? Eine if-Anweisung ohne zusammengesetzte Aussage würde also auch einen solchen Block darstellen. Ist mein Verständnis richtig? – MinecraftShamrock

Antwort

9

Ich bin in der Stimmung zu erklären dies ohne blutige Gedächtnis- la Ihre Details (glauben Sie mir, sie erhalten sehr gory, wenn VLAs verwendet werden; Einzelheiten finden Sie in der Antwort von @ Ulfalizer.

So ursprünglich in C89, war es zwingend notwendig, alle Variablen zu Beginn eines Blockes, wie dies zu erklären:

{ 
    int a = 1; 
    a++; 
    /* ... */ 
} 

bedeutet dies direkt eine sehr wichtige Sache: einen Block == ein unveränderlicher Satz von Variablendeklarationen.

C99 hat dies geändert. Darin können Sie Variablen in jedem Teil des Blocks deklarieren, aber Deklarationsanweisungen unterscheiden sich immer noch von regulären Anweisungen.

Um dies zu verstehen, können Sie sich vorstellen, dass alle Variablendeklarationen implizit an den Anfang des Blocks verschoben werden, wo sie deklariert und für alle Anweisungen, die ihnen vorausgehen, nicht verfügbar sind.

Das ist einfach weil der eine Block == eine Reihe von Deklarationen Regel noch gilt.

Deshalb können Sie nicht "über eine Deklaration springen". Die deklarierte Variable würde immer noch existieren.

Das Problem ist die Initialisierung. Es wird nirgends "bewegt". Also, technisch gesehen, für Ihren Fall, die folgenden Programme als gleichwertig angesehen werden könnten:

goto later; 
int a = 100; 
later: 
printf("%d", a); 

und

int a; 
goto later; 
a = 100; 
later: 
printf("%d", a); 

Wie Sie sehen können, die Erklärung ist immer noch da, was ausgelassen wird, ist die Initialisierung.

Der Grund, warum dies nicht mit VLAs funktioniert, ist, dass sie anders sind.Kurz gesagt, ist es, weil dies gilt:

int size = 7; 
int test[size]; 

Die Erklärungen von VLAs werden, im Gegensatz zu allen anderen Erklärungen, verhalten sich anders in verschiedenen Teilen der Blöcke, in dem sie deklariert ist. Tatsächlich kann ein VLA völlig unterschiedliche Speicherlayouts haben, abhängig davon, wo es deklariert ist. Du kannst es einfach nicht außerhalb des Ortes bewegen, über den du gerade gesprungen bist.

Sie können fragen, "in Ordnung, dann warum es nicht so machen, dass die Deklaration von der goto unberührt bleiben würde"? Nun, Sie würden immer noch Fälle wie diese:

goto later; 
int size = 7; 
int test[size]; 
later: 

Was erwarten Sie eigentlich, dies zu tun ..

So, das Verbot des Sprungs über VLA Erklärungen gibt es für einen Grund - es ist am meisten? logische Entscheidung, um Fälle wie die obigen zu behandeln, indem man sie einfach ganz verbietet.

+3

Selbst in C89/C90 war es legal, in einen Block zu springen: 'GOTO LABEL ; {int n = 42; LABEL: printf ("% d \ n", n); } ' –

+0

@KeithThompson: woah, danke! Ich habe das komplett vergessen =) Ich werde die Antwort sofort bearbeiten! – Mints97

+1

Ein erstaunliches/erschreckendes Beispiel dafür, was @KeithThompson erwähnt, ist bekannt als [Duffs Gerät] (https://en.wikipedia.org/wiki/Duff%27s_device). Es verwendet Switch-Labels, um in die Mitte einer While-Schleife zu springen. –

5

Der Grund, warum Sie die Deklaration eines Array variabler Länge (VLA) nicht überspringen dürfen, ist, dass es mit der Art, wie VLAs allgemein implementiert werden, unordentlich wird und die Semantik der Sprache komplizieren würde. Die Art, wie VLAs in der Praxis wahrscheinlich implementiert werden, besteht darin, den Stapelzeiger durch einen dynamischen (zur Laufzeit berechneten) Betrag zu dekrementieren (oder auf Architekturen zu erhöhen, wo der Stapel nach oben wächst), um Platz für das VLA auf dem Stapel zu schaffen. Dies geschieht an dem Punkt, an dem der VLA deklariert wird (konzeptionell zumindest, Optimierungen ignorierend). Dies wird benötigt, damit spätere Stapeloperationen (z. B. Schieben von Argumenten an den Stapel für einen Funktionsaufruf) nicht auf den Speicher des VLA treten.

Für VLAs, die in Blöcken verschachtelt sind, wird der Stapelzeiger üblicherweise am Ende des Blocks mit dem VLA wiederhergestellt. Wenn ein goto in einen solchen Block springen und die Deklaration eines VLA überspringen konnte, würde der Code zum Wiederherstellen des Stapelzeigers ohne den entsprechenden Initialisierungscode laufen, was wahrscheinlich Probleme verursachen würde. Zum Beispiel könnte der Stapelzeiger um die Größe des VLA inkrementiert werden, obwohl er niemals dekrementiert wurde, was unter anderem dazu führen würde, dass die Rücksprungadresse, die beim Aufruf der Funktion, die den VLA enthielt, aufgerufen wurde, an der falschen relativen Stelle erscheint zum Stapelzeiger.

Es ist auch chaotisch von einer reinen Sprache Semantik Perspektive. Wenn Sie die Deklaration überspringen können, was wäre dann die Größe des Arrays? Was sollte sizeof zurückgeben? Was bedeutet es, darauf zuzugreifen?

Für die Nicht-VLA-Fälle würden Sie einfach die Initialisierung des Werts überspringen (falls vorhanden), was an sich nicht notwendigerweise Probleme verursacht. Wenn Sie über eine Nicht-VLA-Definition wie int x; springen, ist der Speicher weiterhin für die Variable x reserviert. VLAs unterscheiden sich darin, dass ihre Größe zur Laufzeit berechnet wird, was die Dinge komplizierter macht. Als Randnotiz ist eine der Beweggründe dafür, Variablen in C99 an einer beliebigen Stelle innerhalb eines Blocks zu deklarieren (C89 verlangt, dass Deklarationen am Anfang des Blocks stehen, obwohl zumindest GCC sie innerhalb des Blocks als Erweiterung erlaubt)) sollte VLAs unterstützen. Die Möglichkeit, früher im Block Berechnungen auszuführen, bevor die Größe des VLA deklariert wird, ist praktisch.

Aus etwas verwandten Gründen erlaubt C++ goto s nicht, Objektdeklarationen (oder Initialisierungen für einfache alte Datentypen, z. B. int) zu überspringen. Dies liegt daran, dass es nicht sicher wäre, über den Code zu springen, der den Konstruktor aufruft, aber den Destruktor immer noch am Ende des Blocks ausführt.

3

Mit einem goto über die Deklaration einer Variablen zu springen ist fast sicher eine sehr schlechte Idee, aber es ist völlig legal.

C macht einen Unterschied zwischen der Lebenszeit einer Variablen und ihrem Umfang .

Für eine Variable, die ohne das Schlüsselwort static innerhalb einer Funktion deklariert wurde, erstreckt sich ihr Gültigkeitsbereich (der Bereich des Programmtexts, in dem der Name sichtbar ist) von der Definition bis zum Ende des nächsten umschließenden Blocks. Ihre Lebensdauer (Speicherdauer) beginnt beim Eintritt in den Block und endet beim Verlassen des Blocks. Wenn es einen Initialisierer hat, wird es ausgeführt, wenn (und wenn) die Definition erreicht ist.

Zum Beispiel:

{ /* the lifetime of x and y starts here; memory is allocated for both */ 
    int x = 10; /* the name x is visible from here to the "}" */ 
    int y = 20; /* the name y is visible from here to the "}" */ 
    int vla[y]; /* vla is visible, and its lifetime begins here */ 
    /* ... */ 
} 

Für Arrays mit variabler Länge (VLAs), die Sichtbarkeit der Kennung das gleiche ist, aber die Lebensdauer des Objekts beginnt bei der Definition. Warum? Weil die Länge des Arrays vor diesem Punkt nicht unbedingt bekannt ist. Im Beispiel ist es nicht möglich, zu Beginn des Bausteins Speicher für vla zu reservieren, da wir den Wert y noch nicht kennen.

Ein goto, der eine Objektdefinition überspringt, überbrückt jeden Initialisierer für dieses Objekt, aber es wird immer noch Speicher zugewiesen. Wenn der goto in einen Block springt, wird Speicher zugewiesen, wenn der Block eingegeben wird. Wenn dies nicht der Fall ist (wenn sowohl goto als auch das Ziellabel im selben Block auf derselben Ebene sind), wurde das Objekt bereits zugewiesen.

... 
goto LABEL; 
{ 
    int x = 10; 
    LABEL: printf("x = %d\n", x); 
} 

Wenn die printf Anweisung ausgeführt wird, x existiert und sein Name ist sichtbar, aber die Initialisierung umgangen wurde, so hat es einen unbestimmten Wert. Die Sprache verbietet goto, die die Definition eines Array variabler Länge überspringt. Wenn es erlaubt wäre, würde die Speicherzuweisung für das Objekt übersprungen werden, und jeder Versuch, darauf Bezug zu nehmen, würde zu undefiniertem Verhalten führen.

goto Aussagen do have their uses. Sie zu verwenden, um Deklarationen zu überspringen, obwohl dies von der Sprache erlaubt ist, gehört nicht dazu.