Platzierung der variablen Deklaration in C.

129

Ich habe lange gedacht, dass in C alle Variablen am Anfang der Funktion deklariert werden müssen. Ich weiß, dass in C99 die Regeln dieselben sind wie in C ++, aber wie lauten die Regeln für die Platzierung von Variablendeklarationen für C89 / ANSI C?

Der folgende Code wird erfolgreich mit gcc -std=c89und kompiliert gcc -ansi:

#include <stdio.h>
int main() {
    int i;
    for (i = 0; i < 10; i++) {
        char c = (i % 95) + 32;
        printf("%i: %c\n", i, c);
        char *s;
        s = "some string";
        puts(s);
    }
    return 0;
}

Sollten die Deklarationen von cund nicht seinen Fehler im C89 / ANSI-Modus verursachen?

mcjabberz
quelle
54
Nur eine Anmerkung: Variablen in ansi C müssen nicht am Anfang einer Funktion deklariert werden, sondern am Anfang eines Blocks. Also, char c = ... oben in Ihrer for-Schleife ist in ansi C völlig legal. Die char * s wären es jedoch nicht.
Jason Coco

Antworten:

149

Es wird erfolgreich kompiliert, da GCC die Deklaration sals GNU-Erweiterung zulässt , obwohl es nicht Teil des C89- oder ANSI-Standards ist. Wenn Sie diese Standards strikt einhalten möchten, müssen Sie die -pedanticFlagge übergeben.

Die Deklaration cam Anfang eines { }Blocks ist Teil des C89-Standards; Der Block muss keine Funktion sein.

Mipadi
quelle
40
Es ist wahrscheinlich erwähnenswert, dass nur die Erklärung von seine Erweiterung ist (aus Sicht von C89). Die Erklärung von cist in C89 vollkommen legal, es sind keine Erweiterungen erforderlich.
Am
7
@AndreyT: Ja, in C sollten Variablendeklarationen @ der Anfang eines Blocks sein und keine Funktion an sich; Aber die Leute verwechseln Block mit Funktion, da dies das primäre Beispiel für einen Block ist.
Legends2k
1
Ich habe den Kommentar mit +39 Stimmen in die Antwort verschoben.
MarcH
78

Für C89 müssen Sie alle Ihre Variablen am Anfang eines Bereichsblocks deklarieren .

Ihre char cDeklaration ist also gültig, da sie sich oben im for-Schleifenbereichsblock befindet. Die char *sErklärung sollte jedoch ein Fehler sein.

Kiley Hykawy
quelle
2
Ganz richtig. Sie können Variablen am Anfang eines beliebigen {...} deklarieren.
Artelius
5
@Artelius Nicht ganz richtig. Nur wenn die Locken Teil eines Blocks sind (nicht wenn sie Teil einer Struktur- oder Vereinigungsdeklaration oder eines geschweiften Initialisierers sind)
Jens
Um pedantisch zu sein, sollte die fehlerhafte Erklärung zumindest gemäß dem C-Standard gemeldet werden. Es sollte also ein Fehler oder eine Warnung sein gcc. Vertrauen Sie also nicht darauf, dass ein Programm so kompiliert werden kann, dass es kompatibel ist.
Jinawee
35

Das Gruppieren von Variablendeklarationen am oberen Rand des Blocks ist ein Erbe, das wahrscheinlich auf Einschränkungen alter, primitiver C-Compiler zurückzuführen ist. Alle modernen Sprachen empfehlen und erzwingen manchmal sogar die Deklaration lokaler Variablen spätestens dort, wo sie zuerst initialisiert werden. Weil dadurch das Risiko beseitigt wird, versehentlich einen zufälligen Wert zu verwenden. Das Trennen von Deklaration und Initialisierung verhindert auch, dass Sie "const" (oder "final") verwenden, wenn Sie könnten.

C ++ akzeptiert leider weiterhin die alte Top-Deklarationsmethode für die Abwärtskompatibilität mit C (eine C-Kompatibilität zieht sich aus vielen anderen heraus ...). Aber C ++ versucht, sich davon zu entfernen:

  • Das Design von C ++ - Referenzen erlaubt nicht einmal eine solche Gruppengruppierung.
  • Wenn Sie die Deklaration und Initialisierung eines lokalen C ++ - Objekts trennen , zahlen Sie die Kosten für einen zusätzlichen Konstruktor für nichts. Wenn der Konstruktor no-arg nicht existiert, dürfen Sie nicht einmal beide trennen!

C99 beginnt, C in dieselbe Richtung zu bewegen.

Wenn Sie befürchten, nicht zu finden, wo lokale Variablen deklariert sind, haben Sie ein viel größeres Problem: Der umschließende Block ist zu lang und sollte aufgeteilt werden.

https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions

März
quelle
Siehe auch, wie das Erzwingen von Variablendeklarationen am oberen Rand
MarcH
"C ++ akzeptiert leider weiterhin die alte Top-Deklarationsmethode für die Abwärtskompatibilität mit C": IMHO, es ist nur die saubere Methode, dies zu tun. Andere Sprachen "lösen" dieses Problem, indem sie immer mit 0 initialisieren. Bzzt, das logische Fehler nur maskiert, wenn Sie mich fragen. Und es gibt einige Fälle, in denen Sie eine Deklaration ohne Initialisierung benötigen, da es mehrere mögliche Orte für die Initialisierung gibt. Und deshalb ist RA ++ in C ++ wirklich ein großes Problem. Jetzt müssen Sie in jedes Objekt einen "gültigen" nicht initialisierten Status einfügen, um diese Fälle zu berücksichtigen.
Jo So
1
@JoSo: Ich bin verwirrt, warum Sie denken, dass das Lesen nicht initialisierter Variablen zu willkürlichen Effekten führt, wodurch Programmierfehler leichter zu erkennen sind, als wenn sie entweder einen konsistenten Wert oder einen deterministischen Fehler ergeben? Beachten Sie, dass es keine Garantie dafür gibt, dass sich das Lesen von nicht initialisiertem Speicher in einer Weise verhält, die mit jedem Bitmuster übereinstimmt, das die Variable hätte halten können, und dass sich ein solches Programm auch nicht in einer Weise verhält, die den üblichen Gesetzen der Zeit und der Kausalität entspricht. Gegeben etwas wie int y; ... if (x) { printf("X was true"); y=23;} return y;...
Supercat
1
@JoSo: Für Zeiger, insbesondere bei Implementierungen, bei denen Operationen abgefangen werden null, ist All-Bit-Null häufig ein nützlicher Trap-Wert. In Sprachen, in denen explizit angegeben wird, dass Variablen standardmäßig alle Bits Null sind, ist die Abhängigkeit von diesem Wert kein Fehler . Compiler neigen noch nicht dazu, mit ihren "Optimierungen" übermäßig verrückt zu werden, aber Compiler-Autoren versuchen immer wieder, immer klüger zu werden. Eine Compileroption zum Initialisieren von Variablen mit absichtlichen Pseudozufallsvariablen kann nützlich sein, um Fehler zu identifizieren. Wenn Sie jedoch den Speicher nur auf seinem letzten Wert belassen, können Fehler manchmal maskiert werden.
Supercat
22

Unter dem Gesichtspunkt der Wartbarkeit und nicht der Syntaktik gibt es mindestens drei Gedankengänge:

  1. Deklarieren Sie alle Variablen am Anfang der Funktion, damit sie sich an einer Stelle befinden und Sie die umfassende Liste auf einen Blick sehen können.

  2. Deklarieren Sie alle Variablen so nah wie möglich an dem Ort, an dem sie zuerst verwendet werden, damit Sie wissen, warum sie benötigt werden.

  3. Deklarieren Sie alle Variablen am Anfang des innersten Bereichsblocks, damit sie so schnell wie möglich den Bereich verlassen und der Compiler den Speicher optimieren und Ihnen mitteilen kann, wenn Sie sie versehentlich dort verwenden, wo Sie es nicht beabsichtigt hatten.

Im Allgemeinen bevorzuge ich die erste Option, da ich finde, dass die anderen mich oft zwingen, den Code für die Deklarationen zu durchsuchen. Das Definieren aller Variablen im Voraus erleichtert auch das Initialisieren und Überwachen über einen Debugger.

Ich deklariere manchmal Variablen innerhalb eines kleineren Bereichsblocks, aber nur aus einem guten Grund, von dem ich nur sehr wenige habe. Ein Beispiel könnte nach a sein fork(), um Variablen zu deklarieren, die nur vom untergeordneten Prozess benötigt werden. Für mich ist diese visuelle Anzeige eine hilfreiche Erinnerung an ihren Zweck.

Adam Liss
quelle
27
Ich verwende Option 2 oder 3, damit die Variablen leichter zu finden sind - da die Funktionen nicht so groß sein sollten, dass Sie die Variablendeklarationen nicht sehen können.
Jonathan Leffler
8
Option 3 ist kein Problem, es sei denn, Sie verwenden einen Compiler aus den 70er Jahren.
edgar.holleis
15
Wenn Sie eine anständige IDE verwenden würden, müssten Sie nicht nach Code suchen, da es einen IDE-Befehl geben sollte, um die Deklaration für Sie zu finden. (F3 in Eclipse)
edgar.holleis
4
Ich verstehe nicht, wie Sie die Initialisierung in Option 1 sicherstellen können. Möglicherweise können Sie den Anfangswert erst später im Block erhalten, indem Sie eine andere Funktion aufrufen oder eine Berechnung durchführen.
Plumenator
4
@Plumenator: Option 1 gewährleistet keine Initialisierung. Ich habe mich entschieden, sie bei der Deklaration entweder auf ihre "korrekten" Werte oder auf etwas zu initialisieren, das garantiert, dass der nachfolgende Code beschädigt wird, wenn sie nicht richtig eingestellt werden. Ich sage "gewählt", weil sich meine Präferenz seit dem Schreiben auf # 2 geändert hat, vielleicht weil ich jetzt mehr Java als C verwende und weil ich bessere Entwicklertools habe.
Adam Liss
6

Wie von anderen angemerkt, ist GCC in dieser Hinsicht (und möglicherweise auch andere Compiler, abhängig von den Argumenten, mit denen sie aufgerufen werden) zulässig, selbst wenn sie sich im Modus "C89" befinden, es sei denn, Sie verwenden die Prüfung "pedantisch". Um ehrlich zu sein, gibt es nicht viele gute Gründe, nicht pedantisch zu sein. Hochwertiger moderner Code sollte immer ohne Warnungen kompiliert werden (oder nur sehr wenige, bei denen Sie wissen, dass Sie etwas Bestimmtes tun, das für den Compiler als möglicher Fehler verdächtig ist). Wenn Sie Ihren Code also nicht mit einem pedantischen Setup kompilieren können, muss er wahrscheinlich etwas beachtet werden.

C89 erfordert, dass Variablen vor allen anderen Anweisungen in jedem Bereich deklariert werden. Spätere Standards ermöglichen eine näher zu verwendende Deklaration (die sowohl intuitiver als auch effizienter sein kann), insbesondere die gleichzeitige Deklaration und Initialisierung einer Regelungsvariablen in 'for'-Schleifen.

Gaidheal
quelle
0

Wie bereits erwähnt, gibt es hierzu zwei Denkschulen.

1) Deklarieren Sie alles an der Spitze der Funktionen, da das Jahr 1987 ist.

2) Erklären Sie am nächsten zum ersten Gebrauch und im kleinstmöglichen Umfang.

Meine Antwort darauf lautet BEIDES! Lassen Sie mich erklären:

Für lange Funktionen macht 1) das Refactoring sehr schwierig. Wenn Sie in einer Codebasis arbeiten, in der die Entwickler gegen die Idee von Subroutinen sind, haben Sie zu Beginn der Funktion 50 Variablendeklarationen, von denen einige möglicherweise nur ein "i" für eine for-Schleife sind, die genau am Ende steht unten in der Funktion.

Daraus entwickelte ich eine Erklärung zur obersten PTBS und versuchte, Option 2) religiös zu machen.

Ich bin wegen einer Sache zu Option eins zurückgekehrt: kurzen Funktionen. Wenn Ihre Funktionen kurz genug sind, haben Sie nur wenige lokale Variablen. Da die Funktion kurz ist und Sie sie oben in die Funktion einfügen, sind sie immer noch nahe an der ersten Verwendung.

Das Anti-Muster "deklarieren und auf NULL setzen", wenn Sie oben deklarieren möchten, aber einige für die Initialisierung erforderliche Berechnungen nicht durchgeführt haben, wird ebenfalls behoben, da die zu initialisierenden Elemente wahrscheinlich als Argumente empfangen werden.

Jetzt denke ich also, dass Sie an der Spitze der Funktionen deklarieren und so nah wie möglich an der ersten Verwendung sein sollten. Also BEIDE! Und der Weg, dies zu tun, führt über gut unterteilte Unterprogramme.

Wenn Sie jedoch an einer langen Funktion arbeiten, sollten Sie die Dinge am nächsten verwenden, da dies das Extrahieren von Methoden erleichtert.

Mein Rezept ist das. Nehmen Sie für alle lokalen Variablen die Variable und verschieben Sie ihre Deklaration nach unten, kompilieren Sie sie und verschieben Sie die Deklaration kurz vor dem Kompilierungsfehler. Das ist die erste Verwendung. Tun Sie dies für alle lokalen Variablen.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Definieren Sie nun einen Bereichsblock, der vor der Deklaration beginnt, und verschieben Sie das Ende, bis das Programm kompiliert wird

{
    int foo = 0;
    <code that uses foo>
}

int bar = 1;
<code that uses bar>

>>> First compilation error here
<code that uses foo>

Dies wird nicht kompiliert, da es mehr Code gibt, der foo verwendet. Wir können feststellen, dass der Compiler den Code durchgehen konnte, der bar verwendet, weil er nicht foo verwendet. Zu diesem Zeitpunkt gibt es zwei Möglichkeiten. Die mechanische besteht darin, das "}" einfach nach unten zu bewegen, bis es kompiliert ist, und die andere Möglichkeit besteht darin, den Code zu überprüfen und festzustellen, ob die Reihenfolge geändert werden kann in:

{
    int foo = 0;
    <code that uses foo>
}

<code that uses foo>

int bar = 1;
<code that uses bar>

Wenn die Reihenfolge geändert werden kann, ist dies wahrscheinlich das, was Sie möchten, da dies die Lebensdauer temporärer Werte verkürzt.

Eine andere Sache, die zu beachten ist, ist, dass der Wert von foo zwischen den Codeblöcken, die es verwenden, beibehalten werden muss, oder könnte es in beiden nur ein anderes foo sein. Beispielsweise

int i;

for(i = 0; i < 8; ++i){
    ...
}

<some stuff>

for(i = 3; i < 32; ++i){
    ...
}

Diese Situationen erfordern mehr als mein Verfahren. Der Entwickler muss den Code analysieren, um zu bestimmen, was zu tun ist.

Der erste Schritt ist jedoch, die erste Verwendung zu finden. Sie können dies visuell tun, aber manchmal ist es einfacher, die Deklaration zu löschen, zu kompilieren und sie einfach wieder über die erste Verwendung zu setzen. Wenn sich diese erste Verwendung in einer if-Anweisung befindet, legen Sie sie dort ab und prüfen Sie, ob sie kompiliert wird. Der Compiler identifiziert dann andere Verwendungen. Versuchen Sie, einen Bereichsblock zu erstellen, der beide Verwendungszwecke umfasst.

Nachdem dieser mechanische Teil fertig ist, ist es einfacher zu analysieren, wo sich die Daten befinden. Wenn eine Variable in einem großen Bereichsblock verwendet wird, analysieren Sie die Situation und prüfen Sie, ob Sie dieselbe Variable nur für zwei verschiedene Dinge verwenden (z. B. ein "i", das für zwei for-Schleifen verwendet wird). Wenn die Verwendungen nicht zusammenhängen, erstellen Sie für jede dieser nicht verwendeten Verwendungen neue Variablen.

Philippe Carphin
quelle
0

Sie sollten alle Variablen oben oder "lokal" in der Funktion deklarieren. Die Antwort ist:

Es hängt davon ab, welche Art von System Sie verwenden:

1 / Eingebettetes System (insbesondere in Bezug auf Leben wie Flugzeug oder Auto): Es ermöglicht die Verwendung des dynamischen Speichers (z. B. Calloc, Malloc, Neu ...). Stellen Sie sich vor, Sie arbeiten in einem sehr großen Projekt mit 1000 Ingenieuren. Was ist, wenn sie neuen dynamischen Speicher zuweisen und vergessen, ihn zu entfernen (wenn er nicht mehr verwendet wird)? Wenn das eingebettete System längere Zeit ausgeführt wird, kommt es zu einem Stapelüberlauf und die Software wird beschädigt. Es ist nicht einfach, die Qualität sicherzustellen (der beste Weg ist, dynamischen Speicher zu sperren).

Wenn ein Flugzeug innerhalb von 30 Tagen läuft und nicht abschaltet, was passiert, wenn die Software beschädigt ist (wenn das Flugzeug noch in der Luft ist)?

2 / Die anderen Systeme wie Web, PC (haben großen Speicherplatz):

Sie sollten die Variable "lokal" deklarieren, um den Speicher mithilfe von zu optimieren. Wenn diese Systeme längere Zeit laufen und ein Stapelüberlauf auftritt (weil jemand vergessen hat, den dynamischen Speicher zu entfernen). Machen Sie einfach das Einfache, um den PC zurückzusetzen: P Es hat keine Auswirkungen auf das Leben

Dang_Ho
quelle
Ich bin mir nicht sicher, ob das richtig ist. Ich denke, Sie sagen, es ist einfacher, auf Speicherlecks zu prüfen, wenn Sie alle Ihre lokalen Variablen an einem Ort deklarieren? Das mag wahr sein, aber ich bin mir nicht sicher, ob ich es kaufe. Zu Punkt (2) sagen Sie, dass die lokale Deklaration der Variablen "die Speichernutzung optimieren" würde? Dies ist theoretisch möglich. Ein Compiler könnte die Größe des Stapelrahmens im Verlauf einer Funktion ändern, um die Speichernutzung zu minimieren, aber mir sind keine bekannt, die dies tun. In Wirklichkeit konvertiert der Compiler einfach alle "lokalen" Deklarationen in "Funktionsstart hinter den Kulissen".
QuinnFreedman
1 / Das eingebettete System erlaubt manchmal keinen dynamischen Speicher. Wenn Sie also alle Variablen über der Funktion deklarieren. Wenn Quellcode erstellt wird, kann er die Anzahl der Bytes berechnen, die im Stapel zum Ausführen des Programms benötigt werden. Mit dynamischem Speicher kann der Compiler jedoch nicht dasselbe tun.
Dang_Ho
2 / Wenn Sie eine Variable lokal deklarieren, ist diese Variable nur in der Klammer "{}" open / close vorhanden. Der Compiler kann also den Bereich der Variablen freigeben, wenn diese Variable "außerhalb des Gültigkeitsbereichs" liegt. Das kann besser sein, als alles an der Spitze der Funktion zu deklarieren.
Dang_Ho
Ich denke, Sie sind verwirrt über statisches und dynamisches Gedächtnis. Auf dem Stapel ist statischer Speicher zugeordnet. Alle Variablen, die in einer Funktion deklariert sind, unabhängig davon, wo sie deklariert sind, werden statisch zugeordnet. Dynamischer Speicher wird auf dem Heap mit so etwas wie zugewiesen malloc(). Obwohl ich noch nie ein Gerät gesehen habe, das dazu nicht in der Lage ist, empfiehlt es sich, eine dynamische Zuordnung auf eingebetteten Systemen zu vermeiden ( siehe hier ). Das hat aber nichts damit zu tun, wo Sie Ihre Variablen in einer Funktion deklarieren.
QuinnFreedman
1
Ich stimme zu, dass dies eine vernünftige Vorgehensweise wäre, aber in der Praxis ist dies nicht der Fall. Hier ist die eigentliche Zusammenstellung für etwas, das Ihrem Beispiel sehr ähnlich ist: godbolt.org/z/mLhE9a . Wie Sie sehen können, wird in Zeile 11 sub rsp, 1008Speicherplatz für das gesamte Array außerhalb der if-Anweisung zugewiesen. Dies gilt für clangund gccauf jeder Version und Optimierungsstufe, die ich ausprobiert habe.
QuinnFreedman
-1

Ich werde einige Aussagen aus dem Handbuch für gcc Version 4.7.0 für eine klare Erklärung zitieren.

"Der Compiler kann mehrere Basisstandards wie 'c90' oder 'c ++ 98' und GNU-Dialekte dieser Standards wie 'gnu90' oder 'gnu ++ 98' akzeptieren. Durch Angabe eines Basisstandards kann der Compiler akzeptiert alle Programme, die diesem Standard folgen, und solche, die GNU-Erweiterungen verwenden, die ihm nicht widersprechen. Beispielsweise deaktiviert '-std = c90' bestimmte Funktionen von GCC, die mit ISO C90 nicht kompatibel sind, wie z. B. asm und typeof keywords, jedoch nicht andere GNU-Erweiterungen, die in ISO C90 keine Bedeutung haben, z. B. das Weglassen des Mittelbegriffs eines ?: Ausdrucks. "

Ich denke, der entscheidende Punkt Ihrer Frage ist, warum gcc nicht mit C89 übereinstimmt, selbst wenn die Option "-std = c89" verwendet wird. Ich kenne die Version Ihres gcc nicht, aber ich denke, dass es keinen großen Unterschied geben wird. Der Entwickler von gcc hat uns mitgeteilt, dass die Option "-std = c89" nur bedeutet, dass die Erweiterungen, die C89 widersprechen, deaktiviert sind. Es hat also nichts mit einigen Erweiterungen zu tun, die in C89 keine Bedeutung haben. Und die Erweiterung, die die Platzierung der Variablendeklaration nicht einschränkt, gehört zu den Erweiterungen, die C89 nicht widersprechen.

Um ehrlich zu sein, wird jeder denken, dass es C89 auf den ersten Blick der Option "-std = c89" vollständig entsprechen sollte. Aber das tut es nicht. Das Problem, dass alle Variablen am Anfang besser oder schlechter deklariert werden, ist nur Gewohnheitssache.

junwanghe
quelle
Konformität bedeutet nicht, keine Erweiterungen zu akzeptieren: Solange der Compiler gültige Programme kompiliert und erforderliche Diagnosen für andere erstellt, passt er sich an.
Erinnern Sie sich an Monica
@Marc Lehmann, ja, Sie haben Recht, wenn das Wort "konform" zur Unterscheidung von Compilern verwendet wird. Wenn das Wort "konform" verwendet wird, um einige Verwendungen zu beschreiben, können Sie sagen "Eine Verwendung entspricht nicht dem Standard." Und alle Anfänger sind der Meinung, dass die Verwendung, die nicht dem Standard entspricht, einen Fehler verursachen sollte.
Junwanghe
@Marc Lehmann, übrigens gibt es keine Diagnose, wenn gcc die Verwendung sieht, die nicht dem C89-Standard entspricht.
Junwanghe
Ihre Antwort ist immer noch falsch, da die Behauptung "gcc stimmt nicht überein" nicht dasselbe ist wie "ein Benutzerprogramm stimmt nicht überein". Ihre Verwendung von konform ist einfach falsch. Außerdem war ich als Anfänger nicht der Meinung, die Sie sagen, also ist das auch falsch. Schließlich ist es nicht erforderlich, dass ein konformer Compiler nicht konformen Code diagnostiziert, und tatsächlich ist dies unmöglich zu implementieren.
Erinnern Sie sich an Monica