Zwei Fragen zu jeder Variablen
Jede Variable in C hat zwei getrennte Eigenschaften, und sie zu verwechseln ist die Quelle der meisten Überraschungen auf dieser Seite:
- Gültigkeitsbereich - wo der Name benutzt werden kann. Eine Frage der Übersetzungszeit.
- Lebensdauer - wie lange der Speicher existiert. Eine Frage der Laufzeit.
Sie bewegen sich meist zusammen (eine gewöhnliche Lokale ist in ihrem Block sichtbar und lebt genau so lange), aber static entkoppelt sie, weshalb es hier das verständniswürdige Schlüsselwort ist.
Blockgültigkeit
Eine innerhalb geschweifter Klammern deklarierte Variable ist von ihrer Deklaration bis zur schließenden Klammer sichtbar und sonst nirgends.
Der innere Block kann outer sehen, weil er darin verschachtelt ist. Der äußere Block kann inner nicht sehen. Das gilt für jedes Paar geschweifter Klammern - Funktionsrümpfe, if-Rümpfe, Schleifenrümpfe oder einen nackten Block wie den obigen.
Schleifen machen es greifbar. Ein im for-Kopf deklarierter Zähler gehört der Schleife:
for (int i = 0; i < 5; i++) {
/* i lebt hier */
}
/* i existiert hier nicht */
Und genau das willst du: Zwei Schleifen in derselben Funktion dürfen beide i benutzen, ohne sich zu stören, und der Zähler kann nach der Schleife nicht versehentlich gelesen werden. Wenn du den Wert danach brauchst - den Index, an dem eine Suche stoppte -, deklariere ihn vor der Schleife.
Funktionsparameter haben die Gültigkeit des Funktionsrumpfes, verhalten sich also wie Lokale, die ganz oben deklariert wurden.
C99 erlaubt eine Deklaration überall in einem Block, nicht nur oben. Deklariere Variablen dort, wo du sie zuerst brauchst; eine Variable mit kurzer Gültigkeit ist eine Variable mit weniger Möglichkeiten, falsch zu sein.
Lebensdauer: automatischer Speicher
Eine gewöhnliche Lokale hat automatischen Speicher: Sie entsteht, wenn die Steuerung ihren Block betritt, und wird zerstört, wenn die Steuerung ihn verlässt. Ihr Speicher liegt auf dem Stack.
Es gibt dreimal 1 aus. Jeder Aufruf bekommt ein brandneues count, mit 0 initialisiert, und beim Zurückkehren verworfen. Deshalb hat auch jeder Rahmen einer rekursiven Funktion seine eigene Kopie jeder Lokalen.
Zwei Folgen, klar gesagt. Eine uninitialisierte Lokale enthält Müll, nicht null - die Bytes, die auf dem Stack lagen. Und einen Zeiger auf eine Lokale zurückzugeben ist undefiniertes Verhalten, denn der Speicher ist in dem Moment fort, in dem die Funktion zurückkehrt:
/* BUG: buf existiert nach dem return nicht mehr */
char *broken(void) {
char buf[64] = "hallo";
return buf;
}
static-Lokale: Gedächtnis zwischen Aufrufen
Setze static an eine Lokale, und ihre Lebensdauer wird das gesamte Programm, während ihr Gültigkeitsbereich genau derselbe bleibt.
Jetzt gibt der Zähler 1, 2, 3 aus. Das = 0 läuft einmal, bevor main startet - nicht bei jedem Aufruf.
Drei Regeln für static-Lokale:
- Der Initialisierer muss ein konstanter Ausdruck sein, denn er wird beim Programmstart angewandt, nicht zur Laufzeit.
- Ohne Initialisierer wird ein
staticmit null initialisiert (anders als eine automatische Lokale, die Müll hält). - Die Variable ist weiterhin der Funktion privat. Kein anderer Code kann sie benennen.
Der letzte Punkt ist der ganze Reiz: Eine Funktion kann sich etwas zwischen Aufrufen merken, ohne eine globale Variable zu entblößen, die alles ändern könnte. Nimm es für Aufrufzähler, bei der ersten Nutzung aufgebaute Nachschlagetabellen und Flags für einmalige Initialisierung.
Der Preis ist, dass die Funktion keine reine Abbildung von Eingabe auf Ausgabe mehr ist - dieselben Argumente können verschiedene Antworten liefern -, was sie schwerer testbar und ohne Absicherung unsicher aus mehreren Threads aufrufbar macht. Nimm static-Lokale bewusst, nicht aus Bequemlichkeit.
Dateiebene: globale Variablen
Eine außerhalb jeder Funktion deklarierte Variable hat Dateigültigkeit. Sie ist von ihrer Deklaration bis zum Ende der Datei sichtbar, in jeder Funktion darunter, und ihre Lebensdauer ist das gesamte Programm.
Globale Variablen werden standardmäßig mit null initialisiert, totalOperations startet also auch ohne das = 0 bei 0.
Standardmäßig hat eine globale Variable auch externe Bindung: Andere .c-Dateien desselben Programms erreichen sie, indem sie sie als extern deklarieren:
/* in stats.c */
int totalOperations = 0; /* die Definition - genau eine im Programm */
/* in main.c */
extern int totalOperations; /* eine Deklaration: "es existiert irgendwo" */
Setze die extern-Deklaration in eine Header-Datei, damit jeder Benutzer dieselbe sieht. Beachte die Asymmetrie: Die extern-Deklaration sagt, dass die Variable existiert, und genau eine .c-Datei muss sie tatsächlich definieren.
Warum globale Variablen beißen
Globale Variablen sind der einfachste Weg, Daten zu teilen, und der einfachste Weg, Bugs zu erzeugen, die du nicht eingrenzen kannst. Vier konkrete Probleme:
Alles kann sie ändern. Wenn totalOperations einen falschen Wert hält, ist der Schuldige jede Zeile in jeder Datei. Bei einem Parameter sind die Verdächtigen die Aufrufstellen, die du sehen kannst.
Funktionen werden untestbar. Eine Funktion, die eine globale Variable liest, lässt sich nicht isoliert aufrufen; du musst zuerst globalen Zustand herrichten und daran denken, ihn danach zurückzusetzen.
Der Name gilt programmweit. Eine globale Variable namens count oder buffer kollidiert irgendwann mit der von jemand anderem.
Ihre Initialisierer müssen Konstanten zur Übersetzungszeit sein. In C kann eine globale Variable nur mit einem konstanten Ausdruck initialisiert werden - int limit = readConfig(); lässt sich nicht übersetzen, und int b = a * 2; auf Dateiebene ebenso wenig. Alles Berechnete muss zur Laufzeit von einer Einrichtungsfunktion zugewiesen werden, und jede Datei, die die globale Variable vor diesem Aufruf anfasst, sieht eine stille 0.
Die Alternativen sind fast immer verfügbar: Werte als Parameter übergeben, Ergebnisse zurückgeben und zusammengehörigen Zustand in eine Struktur bündeln, die ausdrücklich herumgereicht wird. Echte Ausnahmen gibt es - ein programmweites Konfigurationsobjekt, ein Protokollierungs-Handle -, und selbst die sind auf Dateiebene als static mit Zugriffsfunktionen besser, was der nächste Abschnitt ist.
static auf Dateiebene: Privatheit
An einer globalen Variablen oder einer Funktion bedeutet static etwas völlig anderes als an einer Lokalen: interne Bindung. Der Name wird der eigenen .c-Datei privat und ist aus keiner anderen erreichbar.
/* counter.c */
#include "counter.h"
static int count = 0; /* privat für diese Datei - keine andere kann sie anfassen */
static void validate(void) { /* ein privater Helfer, nicht Teil der Schnittstelle */
if (count < 0) count = 0;
}
void increment(void) { /* öffentlich: in counter.h deklariert */
count++;
validate();
}
int getCount(void) { /* öffentlich */
return count;
}
Eine andere Datei kann increment und getCount aufrufen, aber sie kann count nicht sehen und validate nicht aufrufen - der Linker löst diese Namen nicht auf. Das ist Cs Modulsystem, so wie es ist: ein Header, der die öffentlichen Funktionen deklariert, und static an allem anderen.
Zwei praktische Vorteile über die Ordnung hinaus. Zwei Dateien dürfen je ein static void validate(void) ohne Kollision haben, was sie nicht könnten, wären die Funktionen öffentlich. Und der Compiler weiß, dass eine static-Funktion außerhalb der Datei keine Aufrufer hat, er kann sie also aggressiver einbinden oder warnen, dass sie unbenutzt ist.
Das Schlüsselwort hat also zwei verschiedene Bedeutungen, entschieden dadurch, wo es steht:
static an einer LOKALEN Variablen -> Lebensdauer wird dauerhaft (Gültigkeit unverändert)
static auf DATEIEBENE -> Bindung wird intern (Lebensdauer unverändert)
Verdecken
Einen Namen in einem inneren Bereich zu deklarieren, den es in einem äußeren schon gibt, verdeckt den äußeren Namen: Innerhalb des inneren Bereichs bezieht sich der Name auf die neue Variable, und die äußere ist unerreichbar.
Das ist erlaubt und gelegentlich beabsichtigt. Häufiger ist es ein Versehen, und ein teures: Eine Funktion, die eine globale Variable ändern sollte, ändert stattdessen eine gleichnamige Lokale, und die globale ändert sich nie. Dasselbe passiert, wenn eine Lokale einen Parameter verdeckt, sodass die Zuweisung, die für die Eingabe gedacht war, ins Leere geht.
gcc -Wshadow meldet jeden Fall. Es steckt nicht in -Wall, schalte es also ausdrücklich ein:
gcc -Wall -Wextra -Wshadow program.c -o program
Die Gewohnheit, die das Problem ganz vermeidet, ist die Benennung nach der Rolle statt nach dem Typ: totalScore und itemScore können einander nicht verdecken, während zwei Variablen namens n es irgendwann tun werden.
Eine kurze Übersicht
in einem Block deklariert Blockgültigkeit, automatische Lebensdauer, Müll wenn uninitialisiert
Funktionsparameter Blockgültigkeit des Rumpfes, eine Kopie des Arguments
static in einer Funktion Blockgültigkeit, PROGRAMM-Lebensdauer, null-initialisiert, zwischen Aufrufen erhalten
außerhalb aller Funktionen Dateigültigkeit, Programm-Lebensdauer, null-initialisiert, für andere Dateien sichtbar
static außerhalb aller Funktionen Dateigültigkeit, Programm-Lebensdauer, PRIVAT für diese .c-Datei
extern-Deklaration benennt eine in einer anderen Datei definierte Variable
Häufig gestellte Fragen
Was ist der Gültigkeitsbereich in C?
Der Bereich des Codes, in dem ein Name sichtbar ist. Eine innerhalb eines Blocks (eines Paars geschweifter Klammern) deklarierte Variable ist nur in diesem Block sichtbar - das ist Blockgültigkeit. Eine außerhalb jeder Funktion deklarierte hat Dateigültigkeit und ist von ihrer Deklaration bis zum Ende der Datei sichtbar.
Was macht static mit einer lokalen Variablen in C?
Es ändert die Lebensdauer der Variablen, ohne ihren Gültigkeitsbereich zu ändern. Eine static-Lokale wird einmal erzeugt, einmal initialisiert und behält ihren Wert zwischen Aufrufen, ist aber weiterhin nur innerhalb ihrer Funktion sichtbar. So gibt man einer Funktion ein Gedächtnis, ohne eine globale Variable zu benutzen.
Was ist der Unterschied zwischen static und global in C?
Eine schlichte globale Variable ist für jede Datei des Programms sichtbar, die andere Dateien mit extern erreichen können. Eine static-Variable oder -Funktion auf Dateiebene ist dieser einen .c-Datei privat - andere Dateien können sie nicht linken. static auf Dateiebene geht ums Verstecken; static an einer Lokalen geht um die Lebensdauer.
Warum sind globale Variablen in C schlecht?
Jede Funktion kann sie ändern, ein Bug kann also von überall im Programm stammen; Funktionen, die sie lesen, lassen sich nicht isoliert testen; und der Name belegt das gesamte Programm und lädt zu Kollisionen ein. Übergib Werte stattdessen als Parameter und gib Ergebnisse zurück, und wo gemeinsamer Zustand wirklich nötig ist, mach ihn auf Dateiebene static, damit nur eine Datei ihn anfassen kann.