Funktionieren Klammern in C als Stapelrahmen?


153

Wenn ich eine Variable innerhalb eines neuen Satzes von geschweiften Klammern erstelle, ist diese Variable dann auf der schließenden Klammer vom Stapel gefallen oder hängt sie bis zum Ende der Funktion? Beispielsweise:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Wird dwährend des code that takes a whileAbschnitts Speicherplatz beanspruchen?


8
Meinen Sie (1) gemäß dem Standard, (2) universelle Praxis bei Implementierungen oder (3) allgemeine Praxis bei Implementierungen?
David Thornley

Antworten:


83

Nein, geschweifte Klammern fungieren nicht als Stapelrahmen. In C bezeichnen geschweifte Klammern nur einen Namensbereich, aber nichts wird zerstört und nichts wird vom Stapel gesprungen, wenn die Kontrolle darüber ausfällt.

Als Programmierer, der Code schreibt, kann man sich das oft so vorstellen, als wäre es ein Stapelrahmen. Die in den geschweiften Klammern deklarierten Bezeichner sind nur innerhalb der geschweiften Klammern zugänglich. Aus Sicht eines Programmierers ist es also so, als würden sie beim Deklarieren auf den Stapel geschoben und dann beim Verlassen des Bereichs geöffnet. Compiler müssen jedoch keinen Code generieren, der beim Ein- / Ausstieg etwas pusht / platzt (und im Allgemeinen auch nicht).

Beachten Sie auch, dass lokale Variablen möglicherweise überhaupt keinen Stapelspeicher belegen: Sie können in CPU-Registern oder an einem anderen zusätzlichen Speicherort gespeichert oder vollständig optimiert werden.

dTheoretisch könnte das Array also Speicher für die gesamte Funktion verbrauchen. Der Compiler kann es jedoch optimieren oder seinen Speicher mit anderen lokalen Variablen teilen, deren Nutzungsdauer sich nicht überschneidet.


9
Ist das nicht implementierungsspezifisch?
Avakar

54
In C ++ wird der Destruktor eines Objekts am Ende seines Gültigkeitsbereichs aufgerufen. Ob der Speicher zurückgefordert wird, ist ein implementierungsspezifisches Problem.
Kristopher Johnson

8
@ pm100: Die Destruktoren werden aufgerufen. Das sagt nichts über die Erinnerung aus, die diese Objekte besetzten.
Donal Fellows

9
Der C-Standard gibt an, dass die Lebensdauer der im Block deklarierten automatischen Variablen nur bis zum Ende der Ausführung des Blocks verlängert wird. So im Wesentlichen solche automatischen Variablen haben am Ende des Blocks „zerstört“ erhalten.
Café

3
@KristopherJohnson: Wenn eine Methode zwei separate Blöcke hätte, von denen jeder ein 1-KByte-Array deklarierte, und einen dritten Block, der eine verschachtelte Methode aufrief, könnte ein Compiler denselben Speicher für beide Arrays verwenden und / oder das Array platzieren Bewegen Sie den Stapelzeiger im flachsten Teil des Stapels darüber und rufen Sie die verschachtelte Methode auf. Ein solches Verhalten könnte die für den Funktionsaufruf erforderliche Stapeltiefe um 2 KB verringern.
Supercat

39

Die Zeit, in der die Variable tatsächlich Speicher belegt, ist offensichtlich vom Compiler abhängig (und viele Compiler passen den Stapelzeiger nicht an, wenn innere Blöcke innerhalb von Funktionen eingegeben und verlassen werden).

Eine eng verwandte, aber möglicherweise interessantere Frage ist jedoch, ob das Programm auf dieses innere Objekt außerhalb des inneren Bereichs (aber innerhalb der enthaltenden Funktion) zugreifen darf, dh:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(Mit anderen Worten: Darf der Compiler die Zuordnung aufheben d, auch wenn dies in der Praxis die meisten nicht tun?)

Die Antwort ist , dass der Compiler ist ausplanen erlaubt d, und den Zugriff auf, p[0]wo der Kommentar angibt , nicht definiertes Verhalten (das Programm ist nicht das innere Objekt außerhalb des inneren Umfangs für den Zugriff erlaubt). Der relevante Teil des C-Standards ist 6.2.4p5:

Für ein solches Objekt [eines Objekts mit automatischer Speicherdauer] ohne Array-Typ variabler Länge erstreckt sich seine Lebensdauer vom Eintritt in den Block, dem es zugeordnet ist, bis die Ausführung dieses Blocks in irgendeiner Weise endet . (Durch das Eingeben eines eingeschlossenen Blocks oder das Aufrufen einer Funktion wird die Ausführung des aktuellen Blocks angehalten, aber nicht beendet.) Wenn der Block rekursiv eingegeben wird, wird jedes Mal eine neue Instanz des Objekts erstellt. Der Anfangswert des Objekts ist unbestimmt. Wenn für das Objekt eine Initialisierung angegeben ist, wird diese jedes Mal ausgeführt, wenn die Deklaration bei der Ausführung des Blocks erreicht wird. Andernfalls wird der Wert bei jedem Erreichen der Deklaration unbestimmt.


Als jemand, der nach Jahren der Verwendung höherer Sprachen lernt, wie Umfang und Speicher in C und C ++ funktionieren, finde ich diese Antwort präziser und nützlicher als die akzeptierte.
Chris

20

Ihre Frage ist nicht klar genug, um eindeutig beantwortet zu werden.

Einerseits führen Compiler normalerweise keine lokale Speicherzuweisung-Freigabe für verschachtelte Blockbereiche durch. Der lokale Speicher wird normalerweise nur einmal beim Funktionseingang zugewiesen und beim Funktionsende freigegeben.

Wenn andererseits die Lebensdauer eines lokalen Objekts endet, kann der von diesem Objekt belegte Speicher später für ein anderes lokales Objekt wiederverwendet werden. Zum Beispiel in diesem Code

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

Beide Arrays belegen normalerweise denselben Speicherbereich, was bedeutet, dass die Gesamtmenge des von der Funktion benötigten lokalen Speichers foofür das größte von zwei Arrays erforderlich ist , nicht für beide gleichzeitig.

Ob letzterer dim Kontext Ihrer Frage weiterhin bis zum Ende der Funktion das Gedächtnis belegt, müssen Sie selbst entscheiden.


6

Es ist implementierungsabhängig. Ich habe ein kurzes Programm geschrieben, um zu testen, was gcc 4.3.4 macht, und es ordnet zu Beginn der Funktion den gesamten Stapelspeicher auf einmal zu. Sie können die von gcc erzeugte Assembly mit dem Flag -S untersuchen.


3

Nein, d [] befindet sich für den Rest der Routine nicht auf dem Stapel. Aber alloca () ist anders.

Bearbeiten: Kristopher Johnson (und Simon und Daniel) haben Recht , und meine erste Antwort war falsch . Mit gcc 4.3.4.on CYGWIN lautet der Code:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

gibt:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Lebe und lerne! Und ein schneller Test scheint zu zeigen, dass AndreyT auch bei mehreren Zuordnungen korrekt ist.

Viel später hinzugefügt : Der obige Test zeigt, dass die gcc-Dokumentation nicht ganz richtig ist. Seit Jahren heißt es (Hervorhebung hinzugefügt):

"Der Speicherplatz für ein Array mit variabler Länge wird freigegeben , sobald der Gültigkeitsbereich des Arraynamens endet ."


Das Kompilieren mit deaktivierter Optimierung zeigt Ihnen nicht unbedingt, was Sie in optimiertem Code erhalten. In diesem Fall ist das Verhalten dasselbe (zu Beginn der Funktion zuweisen und nur beim Verlassen der Funktion frei): godbolt.org/g/M112AQ . Nicht-Cygwin-GCC ruft jedoch keine allocaFunktion auf. Ich bin wirklich überrascht, dass Cygwin GCC das tun würde. Es ist nicht einmal ein Array mit variabler Länge, also IDK, warum Sie das ansprechen.
Peter Cordes

2

Sie könnten. Sie könnten nicht. Die Antwort, die Sie meiner Meinung nach wirklich brauchen, lautet: Nehmen Sie niemals etwas an. Moderne Compiler machen alle Arten von Architektur und implementierungsspezifischer Magie. Schreiben Sie Ihren Code einfach und leserlich an Menschen und lassen Sie den Compiler die guten Dinge tun. Wenn Sie versuchen, um den Compiler herum zu programmieren, fragen Sie nach Problemen - und die Probleme, die Sie normalerweise in diesen Situationen bekommen, sind normalerweise schrecklich subtil und schwer zu diagnostizieren.


1

Ihre Variable dwird normalerweise nicht vom Stapel genommen. Geschweifte Klammern bezeichnen keinen Stapelrahmen. Andernfalls könnten Sie so etwas nicht tun:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

Wenn geschweifte Klammern einen echten Stack-Push / Pop verursachen würden (wie es ein Funktionsaufruf tun würde), würde der obige Code nicht kompiliert werden, da der Code in den Klammern nicht auf die Variable zugreifen könnte var, die außerhalb der Klammern lebt (genau wie ein Unter-) Funktion kann nicht direkt auf Variablen in der aufrufenden Funktion zugreifen). Wir wissen, dass dies nicht der Fall ist.

Geschweifte Klammern werden einfach zum Scoping verwendet. Der Compiler behandelt jeden Zugriff auf die "innere" Variable von außerhalb der umschließenden Klammern als ungültig und verwendet diesen Speicher möglicherweise für etwas anderes (dies ist implementierungsabhängig). Es kann jedoch nicht vom Stapel entfernt werden, bis die umschließende Funktion zurückkehrt.

Update: Hier ist, was die C-Spezifikation zu sagen hat. In Bezug auf Objekte mit automatischer Speicherdauer (Abschnitt 6.4.2):

Bei einem Objekt ohne Array-Typ variabler Länge erstreckt sich seine Lebensdauer vom Eintrag in den Block, dem es zugeordnet ist, bis die Ausführung dieses Blocks ohnehin endet.

Der gleiche Abschnitt definiert den Begriff "Lebensdauer" als (Schwerpunkt Mine):

Die Lebensdauer eines Objekts ist der Teil der Programmausführung, während dessen der Speicher garantiert dafür reserviert ist. Ein Objekt existiert, hat eine konstante Adresse und behält seinen zuletzt gespeicherten Wert während seiner gesamten Lebensdauer bei. Wenn auf ein Objekt außerhalb seiner Lebensdauer verwiesen wird, ist das Verhalten undefiniert.

Das Schlüsselwort hier ist natürlich "garantiert". Sobald Sie den Bereich des inneren Satzes von geschweiften Klammern verlassen, ist die Lebensdauer des Arrays abgelaufen. Möglicherweise wird noch Speicher dafür zugewiesen (Ihr Compiler verwendet den Speicherplatz möglicherweise für etwas anderes), aber alle Versuche, auf das Array zuzugreifen, rufen undefiniertes Verhalten hervor und führen zu unvorhersehbaren Ergebnissen.

Die C-Spezifikation kennt keine Stapelrahmen. Es spricht nur das Verhalten des resultierenden Programms an und überlässt die Implementierungsdetails dem Compiler (schließlich würde die Implementierung auf einer stapellosen CPU ganz anders aussehen als auf einer CPU mit einem Hardware-Stapel). In der C-Spezifikation gibt es nichts, was vorschreibt, wo ein Stapelrahmen enden wird oder nicht. Die einzige wirkliche Möglichkeit zu wissen , ist , den Code auf Ihre speziellen Compiler / Plattform zu kompilieren und die resultierende Anordnung zu untersuchen. Die aktuellen Optimierungsoptionen Ihres Compilers werden wahrscheinlich auch hier eine Rolle spielen.

Wenn Sie sicherstellen möchten , dass die Anordnung dnicht mehr Speicher zu essen, während der Code ausgeführt wird , können Sie entweder den Code in geschweiften Klammern in eine separate Funktion umwandeln oder explizit mallocund freedem Speicher statt mit dynamischem Speicher.


1
"Wenn geschweifte Klammern einen Stapel-Push / Pop verursachen würden, würde der obige Code nicht kompiliert, da der Code in den Klammern nicht auf die Variable var zugreifen könnte, die außerhalb der Klammern lebt" - dies ist einfach nicht wahr. Der Compiler kann sich immer den Abstand vom Stapel- / Rahmenzeiger merken und ihn zum Verweisen auf äußere Variablen verwenden. In Josephs Antwort finden Sie auch ein Beispiel für geschweifte Klammern, die einen Stapel-Push / Pop verursachen.
George

@ george- Das von Ihnen beschriebene Verhalten sowie Josephs Beispiel hängen von dem Compiler und der Plattform ab, die Sie verwenden. Wenn Sie beispielsweise denselben Code für ein MIPS-Ziel kompilieren, erhalten Sie völlig unterschiedliche Ergebnisse. Ich habe nur aus der Sicht der C-Spezifikation gesprochen (da das OP keinen Compiler oder kein Ziel angegeben hat). Ich werde die Antwort bearbeiten und weitere Einzelheiten hinzufügen.
Bta

0

Ich glaube, dass es außerhalb des Gültigkeitsbereichs liegt, aber erst dann vom Stapel genommen wird, wenn die Funktion zurückkehrt. Es nimmt also weiterhin Speicher auf dem Stapel ein, bis die Funktion abgeschlossen ist, ist jedoch stromabwärts der ersten schließenden geschweiften Klammer nicht zugänglich.


3
Keine Garantien. Sobald der Bereich geschlossen ist, verfolgt der Compiler diesen Speicher nicht mehr (oder muss ihn zumindest nicht mehr ...) und kann ihn möglicherweise wiederverwenden. Aus diesem Grund ist das Berühren des Speichers, der früher von einer Variablen außerhalb des Gültigkeitsbereichs belegt war, ein undefiniertes Verhalten. Hüten Sie sich vor Nasendämonen und ähnlichen Warnungen.
dmckee --- Ex-Moderator Kätzchen

0

Es wurden bereits viele Informationen zu dem Standard gegeben, die darauf hinweisen, dass er tatsächlich implementierungsspezifisch ist.

Ein Experiment könnte also von Interesse sein. Wenn wir den folgenden Code versuchen:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Mit gcc erhalten wir hier zweimal die gleiche Adresse: Coliro

Aber wenn wir den folgenden Code versuchen:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Mit gcc erhalten wir hier zwei verschiedene Adressen: Coliro

Sie können sich also nicht sicher sein, was los ist.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.