Ik dacht lang dat in C alle variabelen aan het begin van de functie gedeclareerd moesten worden. Ik weet dat in C99 de regels hetzelfde zijn als in C++, maar wat zijn de regels voor het plaatsen van variabele declaraties voor C89/ANSI C?
De volgende code is succesvol gecompileerd met gcc -std=c89
en 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;
}
Moeten de verklaringen van c
en s
geen fout veroorzaken in C89/ANSI-modus?
Antwoord 1, autoriteit 100%
Het compileert succesvol omdat GCC de declaratie van s
als een GNU-extensie toestaat, ook al maakt het geen deel uit van de C89- of ANSI-standaard. Als u zich strikt aan die normen wilt houden, moet u de vlag -pedantic
doorgeven.
De declaratie van c
aan het begin van een { }
-blok maakt deel uit van de C89-standaard; het blok hoeft geen functie te zijn.
Antwoord 2, autoriteit 48%
Voor C89 moet u al uw variabelen declareren aan het begin van een scope-blok.
Dus uw char c
-declaratie is geldig aangezien deze zich bovenaan het for loop-scope-blok bevindt. Maar de char *s
-declaratie zou een fout moeten zijn.
Antwoord 3, autoriteit 27%
Het groeperen van variabeledeclaraties bovenaan het blok is een erfenis die waarschijnlijk te wijten is aan beperkingen van oude, primitieve C-compilers. Alle moderne talen bevelen aan en dwingen soms zelfs de declaratie van lokale variabelen af op het laatste punt: waar ze voor het eerst worden geïnitialiseerd. Omdat dit het risico wegneemt dat u per ongeluk een willekeurige waarde gebruikt. Door declaratie en initialisatie te scheiden, voorkomt u ook dat u “const” (of “final”) gebruikt wanneer u dat wel zou kunnen.
C++ blijft helaas de oude, topdeclaratiemanier accepteren voor achterwaartse compatibiliteit met C (één C-compatibiliteit sleept vele andere weg…) Maar C++ probeert er afstand van te nemen:
- Het ontwerp van C++-referenties staat dergelijke top-of-the-block-groepering niet eens toe.
- Als u declaratie en initialisatie van een C++ lokaal objectscheidt, betaalt u voor niets de kosten van een extra constructor. Als de no-arg-constructor niet bestaat, mag je beide niet eens scheiden!
C99 begint C in dezelfde richting te bewegen.
Als je bang bent dat je niet kunt vinden waar lokale variabelen worden gedeclareerd, betekent dit dat je een veel groter probleem hebt: het omsluitende blok is te lang en moet worden gesplitst.
https://wiki.sei.cmu.edu/confluence/display/c/DCL19-C.+Minimize+the+scope+of+variables+and+functions
Antwoord 4, autoriteit 13%
Vanuit het oogpunt van onderhoudbaarheid, in plaats van syntactisch, zijn er ten minste drie denkrichtingen:
-
Declareer alle variabelen aan het begin van de functie, zodat ze op één plek staan en je de uitgebreide lijst in één oogopslag kunt zien.
-
Declareer alle variabelen zo dicht mogelijk bij de plaats waar ze voor het eerst worden gebruikt, zodat je weet waaromelk nodig is.
-
Declareer alle variabelen aan het begin van het binnenste scope-blok, zodat ze zo snel mogelijk buiten de scope vallen en de compiler het geheugen kan optimaliseren en je kan vertellen of je ze per ongeluk gebruikt waar je ze niet had gebruikt bedoeld.
Ik geef over het algemeen de voorkeur aan de eerste optie, omdat ik merk dat de anderen me vaak dwingen om door de code voor de verklaringen te jagen. Het vooraf definiëren van alle variabelen maakt het ook gemakkelijker om ze te initialiseren en te bekijken vanuit een debugger.
Soms declareer ik variabelen binnen een kleiner bereikblok, maar alleen om een goede reden, waarvan ik er maar heel weinig heb. Een voorbeeld kan zijn na een fork()
, om variabelen te declareren die alleen nodig zijn voor het onderliggende proces. Voor mij is deze visuele indicator een nuttige herinnering aan hun doel.
Antwoord 5, autoriteit 5%
Zoals door anderen is opgemerkt, is GCC in dit opzicht tolerant (en mogelijk andere compilers, afhankelijk van de argumenten waarmee ze worden aangeroepen), zelfs in de ‘C89’-modus, tenzij je ‘pedante’ controle gebruikt. Eerlijk gezegd zijn er niet veel goede redenen om niet pedant te zijn; moderne code van hoge kwaliteit zou altijd moeten compileren zonder waarschuwingen (of heel weinig waar je weet dat je iets specifieks doet dat verdacht is voor de compiler als een mogelijke fout), dus als je je code niet kunt laten compileren met een pedante setup, heeft het waarschijnlijk wat aandacht nodig.
C89 vereist dat variabelen worden gedeclareerd vóór andere instructies binnen elk bereik, latere standaarden maken declaratie dichter bij gebruik mogelijk (wat zowel intuïtiever als efficiënter kan zijn), met name de gelijktijdige declaratie en initialisatie van een lusbesturingsvariabele in ‘for ‘ lussen.
Antwoord 6
Zoals is opgemerkt, zijn er twee stromingen hierover.
1) Verklaar alles bovenaan de functies omdat het jaar 1987 is.
2) Verklaar het dichtst bij het eerste gebruik en in de kleinst mogelijke omvang.
Mijn antwoord hierop is: DOE BEIDE! Laat het me uitleggen:
Voor lange functies maakt 1) refactoring erg moeilijk. Als je in een codebase werkt waar de ontwikkelaars tegen het idee van subroutines zijn, dan heb je 50 variabele declaraties aan het begin van de functie en sommige daarvan zijn misschien gewoon een “i” voor een for-loop die helemaal onderaan de functie.
Daarom heb ik hier verklaring-aan-de-top-PTSD van ontwikkeld en geprobeerd optie 2) religieus te doen.
Ik kwam terug op optie één vanwege één ding: korte functies. Als je functies kort genoeg zijn, heb je weinig lokale variabelen en aangezien de functie kort is, als je ze bovenaan de functie plaatst, zullen ze nog steeds dicht bij het eerste gebruik zijn.
Ook het anti-patroon van “declare and set to NULL” wanneer u bovenaan wilt declareren, maar geen berekeningen hebt gemaakt die nodig zijn voor initialisatie, is opgelost omdat de dingen die u moet initialiseren waarschijnlijk worden ontvangen als argumenten.
Dus nu denk ik dat je bovenaan de functies moet declareren en zo dicht mogelijk bij het eerste gebruik. Dus BEIDE! En de manier om dat te doen is met goed verdeelde subroutines.
Maar als je aan een lange functie werkt, gebruik dan de dingen die het dichtst bij het eerste gebruik komen, want op die manier is het gemakkelijker om methoden te extraheren.
Mijn recept is dit. Neem voor alle lokale variabelen de variabele en verplaats de declaratie naar beneden, compileer en verplaats de declaratie naar net voor de compilatiefout. Dat is het eerste gebruik. Doe dit voor alle lokale variabelen.
int foo = 0;
<code that uses foo>
int bar = 1;
<code that uses bar>
<code that uses foo>
Definieer nu een scope-blok dat begint voor de declaratie en verplaats het einde totdat het programma compileert
{
int foo = 0;
<code that uses foo>
}
int bar = 1;
<code that uses bar>
>>> First compilation error here
<code that uses foo>
Dit compileert niet omdat er nog meer code is die foo gebruikt. We kunnen zien dat de compiler door de code kon gaan die bar gebruikt, omdat hij geen foo gebruikt. Op dit moment zijn er twee keuzes. De mechanische is om de “}” naar beneden te verplaatsen totdat deze is gecompileerd, en de andere keuze is om de code te inspecteren en te bepalen of de volgorde kan worden gewijzigd in:
{
int foo = 0;
<code that uses foo>
}
<code that uses foo>
int bar = 1;
<code that uses bar>
Als de volgorde kan worden gewijzigd, is dat waarschijnlijk wat je wilt, omdat het de levensduur van tijdelijke waarden verkort.
Nog iets om op te merken, moet de waarde van foo worden bewaard tussen de codeblokken die het gebruiken, of kan het gewoon een andere foo zijn in beide. Bijvoorbeeld
int i;
for(i = 0; i < 8; ++i){
...
}
<some stuff>
for(i = 3; i < 32; ++i){
...
}
Deze situaties hebben meer nodig dan mijn procedure. De ontwikkelaar zal de code moeten analyseren om te bepalen wat te doen.
Maar de eerste stap is het vinden van het eerste gebruik. Je kunt het visueel doen, maar soms is het gewoon gemakkelijker om de verklaring te verwijderen, probeer het te compileren en gewoon terug te zetten boven het eerste gebruik. Als dat voor het eerste gebruik in een verklaring is, zet het daar en controleer het of het compileert. De compiler identificeert dan andere toepassingen. Probeer een scope-blok te maken die beide toepassingen omvat.
Nadat dit mechanische gedeelte is gedaan, wordt het gemakkelijker om te analyseren waar de gegevens zijn. Als een variabele wordt gebruikt in een groot scope-blok, analyseer de situatie en kijk of u gewoon dezelfde variabele gebruikt voor twee verschillende dingen (zoals een “I” die voor twee wordt gebruikt voor lussen). Als het gebruik niet-gerelateerd is, maakt u nieuwe variabelen voor elk van deze niet-verwante toepassingen.