Posizionamento della dichiarazione variabile in C


129

Ho pensato a lungo che in C tutte le variabili dovevano essere dichiarate all'inizio della funzione. So che in C99, le regole sono le stesse di C ++, ma quali sono le regole di posizionamento delle dichiarazioni variabili per C89 / ANSI C?

Il codice seguente viene compilato correttamente con gcc -std=c89e 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;
}

Le dichiarazioni ce non dovrebbero scausare un errore in modalità C89 / ANSI?


54
Solo una nota: le variabili in ansi C non devono essere dichiarate all'inizio di una funzione ma piuttosto all'inizio di un blocco. Quindi, char c = ... nella parte superiore del tuo ciclo for è completamente legale in ansi C. I caratteri *, tuttavia, non lo sarebbero.
Jason Coco,

Risposte:


149

Si compila correttamente perché GCC consente la dichiarazione di sestensione GNU, anche se non fa parte dello standard C89 o ANSI. Se si desidera aderire rigorosamente a tali standard, è necessario passare la -pedanticbandiera.

La dichiarazione call'inizio di un { }blocco fa parte dello standard C89; il blocco non deve essere una funzione.


41
Vale probabilmente la pena notare che solo la dichiarazione di sè un'estensione (dal punto di vista C89). La dichiarazione di cè perfettamente legale in C89, non sono necessarie estensioni.
AnT

7
@AndreyT: Sì, in C, le dichiarazioni delle variabili dovrebbero essere @ l'inizio di un blocco e non una funzione in sé; ma le persone confondono il blocco con la funzione poiché è l'esempio principale di un blocco.
legends2k

1
Ho spostato il commento con +39 voti nella risposta.
MarcH

78

Per C89, è necessario dichiarare tutte le variabili all'inizio di un blocco ambito .

Pertanto, la char cdichiarazione è valida in quanto si trova nella parte superiore del blocco dell'ambito del ciclo for. Ma la char *sdichiarazione dovrebbe essere un errore.


2
Abbastanza corretto Puoi dichiarare le variabili all'inizio di qualsiasi {...}.
Artelius,

5
@Artelius Non del tutto corretto. Solo se i curlies fanno parte di un blocco (non se fanno parte di una dichiarazione struct o union o di un inizializzatore rinforzato).
Jens,

Solo per essere pedanti, la dichiarazione errata dovrebbe essere almeno notificata secondo lo standard C. Quindi dovrebbe essere un errore o un avviso in gcc. Cioè, non fidarti che un programma può essere compilato nel senso che è conforme.
Jinawee,

35

Raggruppare le dichiarazioni delle variabili nella parte superiore del blocco è un'eredità probabilmente a causa delle limitazioni dei vecchi compilatori C primitivi. Tutte le lingue moderne raccomandano e talvolta impongono persino la dichiarazione delle variabili locali all'ultimo punto: dove vengono inizializzate per la prima volta. Perché questo elimina il rischio di utilizzare un valore casuale per errore. Separare la dichiarazione e l'inizializzazione ti impedisce anche di usare "const" (o "final") quando puoi.

C ++ purtroppo continua ad accettare il vecchio modo di dichiarazione superiore per la retrocompatibilità con C (una compatibilità C trascina fuori da molti altri ...) Ma C ++ cerca di allontanarsi da esso:

  • La progettazione di riferimenti C ++ non consente nemmeno tale cima del raggruppamento di blocchi.
  • Se separi la dichiarazione e l'inizializzazione di un oggetto locale C ++, pagherai il costo di un costruttore aggiuntivo per nulla. Se il costruttore no-arg non esiste, non puoi nemmeno separare entrambi!

C99 inizia a spostare C nella stessa direzione.

Se sei preoccupato di non trovare dove vengono dichiarate le variabili locali, significa che hai un problema molto più grande: il blocco che la racchiude è troppo lungo e dovrebbe essere diviso.

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



Vedi anche come forzare le dichiarazioni variabili nella parte superiore del blocco può creare falle di sicurezza: lwn.net/Articles/443037
MarcH,

"C ++ purtroppo continua ad accettare il vecchio modo di dichiarare in alto per la retrocompatibilità con C": IMHO, è solo il modo pulito per farlo. Altre lingue "risolvono" questo problema inizializzando sempre con 0. Bzzt, che maschera solo errori logici se me lo chiedi. E ci sono molti casi in cui AVETE BISOGNO di una dichiarazione senza inizializzazione perché ci sono più posizioni possibili per l'inizializzazione. Ed è per questo che la RAII di C ++ è davvero un enorme dolore nel culo - Ora è necessario includere uno stato "non valido" non inizializzato in ciascun oggetto per consentire questi casi.
Jo So,

1
@JoSo: Sono confuso perché pensi che la lettura di variabili non inizializzate produca effetti arbitrari renderà più facili da rilevare gli errori di programmazione che farli produrre un valore coerente o un errore deterministico? Si noti che non esiste alcuna garanzia che una lettura della memoria non inizializzata si comporterà in modo coerente con qualsiasi modello di bit che la variabile avrebbe potuto contenere, né che un tale programma si comporti in modo coerente con le consuete leggi del tempo e della causalità. Dato qualcosa di simile int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat

1
@JoSo: per i puntatori, specialmente su implementazioni su cui trap trap null, zero-bit è spesso un utile valore trap. Inoltre, nei linguaggi che specificano esplicitamente che le variabili sono predefinite su all-bit-zero, fare affidamento su quel valore non è un errore . I compilatori non tendono ancora a diventare troppo stravaganti con le loro "ottimizzazioni", ma gli autori di compilatori continuano a cercare di diventare sempre più intelligenti. Un'opzione del compilatore per inizializzare le variabili con variabili pseudo-casuali intenzionali potrebbe essere utile per identificare i guasti, ma il semplice fatto di lasciare la memoria con il suo ultimo valore a volte può mascherare i guasti.
supercat

22

Da un punto di vista della manutenibilità, piuttosto che sintattico, ci sono almeno tre treni di pensiero:

  1. Dichiara tutte le variabili all'inizio della funzione in modo che si trovino in un unico posto e potrai vedere l'elenco completo a colpo d'occhio.

  2. Dichiara tutte le variabili il più vicino possibile al luogo in cui sono state utilizzate per la prima volta, così saprai perché ognuna è necessaria.

  3. Dichiarare tutte le variabili all'inizio del blocco dell'ambito più interno, quindi usciranno dall'ambito il più presto possibile e consentiranno al compilatore di ottimizzare la memoria e dirti se le usi accidentalmente dove non avevi previsto.

In genere preferisco la prima opzione, poiché trovo che gli altri spesso mi costringano a cercare il codice per le dichiarazioni. La definizione anticipata di tutte le variabili semplifica inoltre l'inizializzazione e la visualizzazione da un debugger.

A volte dichiarerò variabili all'interno di un blocco di ambito più piccolo, ma solo per un buon motivo, di cui ne ho pochissime. Un esempio potrebbe essere dopo a fork(), per dichiarare le variabili necessarie solo per il processo figlio. Per me, questo indicatore visivo è un utile promemoria del loro scopo.


27
Uso l'opzione 2 o 3, quindi è più facile trovare le variabili, perché le funzioni non dovrebbero essere così grandi da non poter vedere le dichiarazioni delle variabili.
Jonathan Leffler,

8
L'opzione 3 non è un problema, a meno che non si utilizzi un compilatore degli anni '70.
edgar.holleis,

15
Se hai usato un IDE decente, non avresti bisogno di andare a caccia di codice, perché ci dovrebbe essere un comando IDE per trovare la dichiarazione per te. (F3 in Eclipse)
edgar.holleis,

4
Non capisco come sia possibile garantire l'inizializzazione nell'opzione 1, a volte è possibile ottenere il valore iniziale solo successivamente nel blocco, chiamando un'altra funzione o eseguendo una caclulazione.
Plumenatore,

4
@Plumenator: l'opzione 1 non garantisce l'inizializzazione; Ho scelto di inizializzarli al momento della dichiarazione, o ai loro valori "corretti" o a qualcosa che garantirà la rottura del codice successivo se non impostati correttamente. Dico "scelto" perché la mia preferenza è cambiata in # 2 da quando ho scritto questo, forse perché sto usando Java più di C ora, e perché ho strumenti di sviluppo migliori.
Adam Liss,

6

Come notato da altri, GCC è permissivo a questo proposito (e forse altri compilatori, a seconda degli argomenti con cui sono chiamati) anche quando si è in modalità "C89", a meno che non si usi il controllo "pedante". Ad essere onesti, non ci sono molti buoni motivi per non essere pedanti; il codice moderno di qualità dovrebbe sempre essere compilato senza avvisi (o pochissimi in cui sai che stai facendo qualcosa di specifico che è sospetto per il compilatore come un possibile errore), quindi se non riesci a compilare il tuo codice con una configurazione pedante probabilmente avrai bisogno di qualche attenzione.

C89 richiede che le variabili vengano dichiarate prima di qualsiasi altra affermazione all'interno di ciascun ambito, le norme successive consentono una dichiarazione più vicina all'uso (che può essere sia più intuitiva che più efficiente), in particolare la dichiarazione simultanea e l'inizializzazione di una variabile di controllo del ciclo nei cicli 'for'.


0

Come è stato notato, ci sono due scuole di pensiero su questo.

1) Dichiarare tutto al top delle funzioni perché l'anno è il 1987.

2) Dichiarare il più vicino al primo utilizzo e nel più piccolo ambito possibile.

La mia risposta a questa è DO ENTRAMBE! Lasciatemi spiegare:

Per le funzioni lunghe, 1) rende molto difficile il refactoring. Se lavori in una base di codice in cui gli sviluppatori sono contrari all'idea delle subroutine, allora avrai 50 dichiarazioni variabili all'inizio della funzione e alcune potrebbero essere solo una "i" per un for-loop che è proprio parte inferiore della funzione.

Da questo ho quindi sviluppato la dichiarazione al vertice del PTSD e ho provato a fare l'opzione 2) religiosamente.

Sono tornato all'opzione 1 per una cosa: funzioni brevi. Se le tue funzioni sono abbastanza brevi, allora avrai poche variabili locali e poiché la funzione è breve, se le metti in cima alla funzione, rimarranno comunque vicine al primo utilizzo.

Inoltre, l'anti-pattern di "declare e impostato su NULL" quando si desidera dichiarare in alto ma non sono stati effettuati alcuni calcoli necessari per l'inizializzazione viene risolto perché le cose che è necessario inizializzare verranno probabilmente ricevute come argomenti.

Quindi ora il mio pensiero è che dovresti dichiarare in cima alle funzioni e il più vicino possibile al primo utilizzo. Quindi ENTRAMBI! E il modo per farlo è con subroutine ben divise.

Ma se stai lavorando su una funzione lunga, metti le cose più vicine al primo utilizzo perché in questo modo sarà più facile estrarre i metodi.

La mia ricetta è questa Per tutte le variabili locali, prendi la variabile e sposta la sua dichiarazione verso il basso, compila, quindi sposta la dichiarazione appena prima dell'errore di compilazione. Questo è il primo utilizzo. Fallo per tutte le variabili locali.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Ora, definisci un blocco ambito che inizia prima della dichiarazione e sposta la fine fino alla compilazione del programma

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

int bar = 1;
<code that uses bar>

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

Questo non viene compilato perché c'è altro codice che utilizza foo. Possiamo notare che il compilatore è stato in grado di esaminare il codice che utilizza bar perché non utilizza foo. A questo punto, ci sono due scelte. Quello meccanico è semplicemente spostare "}" verso il basso fino a quando non viene compilato, e l'altra scelta è ispezionare il codice e determinare se l'ordine può essere modificato in:

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

<code that uses foo>

int bar = 1;
<code that uses bar>

Se l'ordine può essere cambiato, è probabilmente quello che vuoi perché accorcia la durata dei valori temporanei.

Un'altra cosa da notare, il valore di foo deve essere preservato tra i blocchi di codice che lo usano, o potrebbe essere solo un foo diverso in entrambi. Per esempio

int i;

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

<some stuff>

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

Queste situazioni richiedono più della mia procedura. Lo sviluppatore dovrà analizzare il codice per determinare cosa fare.

Ma il primo passo è trovare il primo utilizzo. Puoi farlo visivamente, ma a volte è più semplice eliminare la dichiarazione, provare a compilare e rimetterla al di sopra del primo utilizzo. Se il primo utilizzo si trova all'interno di un'istruzione if, inseriscilo e verifica se viene compilato. Il compilatore identificherà quindi altri usi. Prova a creare un blocco ambito che comprenda entrambi gli usi.

Dopo aver eseguito questa parte meccanica, diventa più semplice analizzare la posizione dei dati. Se una variabile viene utilizzata in un blocco di ambito di grandi dimensioni, analizzare la situazione e vedere se si sta utilizzando la stessa variabile per due cose diverse (come una "i" che viene utilizzata per due per i loop). Se gli usi non sono correlati, creare nuove variabili per ciascuno di questi usi non correlati.


0

Dovresti dichiarare tutte le variabili in alto o "localmente" nella funzione. La risposta è:

Dipende dal tipo di sistema che stai utilizzando:

1 / Sistema incorporato (in particolare legato a vite come aereo o auto): consente di utilizzare la memoria dinamica (ad es. Calloc, malloc, new ...). Immagina di lavorare in un progetto molto grande, con 1000 ingegneri. Cosa succede se allocare nuova memoria dinamica e si dimenticano di rimuoverla (quando non la utilizza più)? Se il sistema incorporato funziona a lungo, si verificherà un overflow dello stack e il software si corromperà. Non è facile assicurarsi della qualità (il modo migliore è vietare la memoria dinamica).

Se un aeroplano funziona in 30 giorni e non si spegne, cosa succede se il software è danneggiato (quando l'aereo è ancora in volo)?

2 / Gli altri sistemi come web, PC (hanno un ampio spazio di memoria):

Dovresti dichiarare la variabile "localmente" per ottimizzare la memoria usando. Se questi sistemi funzionano a lungo e si verificano overflow dello stack (perché qualcuno ha dimenticato di rimuovere la memoria dinamica). Fai semplicemente la cosa semplice per ripristinare il PC: P Non ha alcun impatto sulla vita


Non sono sicuro che sia corretto. Immagino che stai dicendo che è più facile controllare le perdite di memoria se dichiari tutte le variabili locali in un unico posto? Questo può essere vero, ma io non sono così sicuro lo compro. Per quanto riguarda il punto (2), dici che la dichiarazione locale della variabile "ottimizzerebbe l'utilizzo della memoria"? Ciò è teoricamente possibile. Un compilatore potrebbe scegliere di ridimensionare il frame dello stack nel corso di una funzione per ridurre al minimo l'utilizzo della memoria, ma non sono a conoscenza di ciò che lo fa. In realtà, il compilatore convertirà semplicemente tutte le dichiarazioni "locali" in "inizio funzione dietro le quinte".
QuinnFreedman

1 / Il sistema incorporato a volte non consente la memoria dinamica, quindi se si dichiarano tutte le variabili in cima alla funzione. Quando viene creato il codice sorgente, è possibile calcolare il numero di byte necessari nello stack per eseguire il programma. Ma con la memoria dinamica, il compilatore non può fare lo stesso.
Dang_Ho

2 / Se si dichiara una variabile localmente, quella variabile esiste solo all'interno della parentesi aperta "{}". Quindi il compilatore può rilasciare lo spazio della variabile se quella variabile "fuori campo". Potrebbe essere meglio che dichiarare tutto al top della funzione.
Dang_Ho

Penso che tu sia confuso sulla memoria statica vs dinamica. La memoria statica è allocata nello stack. Tutte le variabili dichiarate in una funzione, indipendentemente da dove sono dichiarate, sono allocate staticamente. La memoria dinamica è allocata sull'heap con qualcosa di simile malloc(). Anche se non ho mai visto un dispositivo che non è in grado di farlo, è buona norma evitare l'allocazione dinamica sui sistemi incorporati ( vedi qui ). Ma questo non ha nulla a che fare con il punto in cui dichiari le tue variabili in una funzione.
QuinnFreedman

1
Anche se concordo sul fatto che questo sarebbe un modo ragionevole di operare, non è ciò che accade nella pratica. Ecco l'assemblaggio reale per qualcosa di molto simile al tuo esempio: godbolt.org/z/mLhE9a . Come puoi vedere, alla riga 11, sub rsp, 1008sta allocando spazio per l'intero array al di fuori dell'istruzione if. Questo è vero per clange gccad ogni livello di versione e l'ottimizzazione ho provato.
QuinnFreedman,

-1

Citerò alcune affermazioni del manuale per la versione 4.7.0 di gcc per una chiara spiegazione.

"Il compilatore può accettare diversi standard di base, come 'c90' o 'c ++ 98', e dialetti GNU di tali standard, come 'gnu90' o 'gnu ++ 98'. Specificando uno standard di base, il compilatore accetterà tutti i programmi che seguono quello standard e quelli che usano estensioni GNU che non lo contraddicono. Ad esempio, '-std = c90' disattiva alcune funzionalità di GCC che sono incompatibili con ISO C90, come le parole chiave asm e typeof, ma non altre estensioni GNU che non hanno un significato in ISO C90, come omettere il termine medio di un'espressione?: ".

Penso che il punto chiave della tua domanda sia che perché non gcc sia conforme a C89 anche se viene usata l'opzione "-std = c89". Non conosco la versione del tuo gcc, ma penso che non ci saranno grandi differenze. Lo sviluppatore di gcc ci ha detto che l'opzione "-std = c89" significa solo che le estensioni che contraddicono C89 sono disattivate. Quindi, non ha nulla a che fare con alcune estensioni che non hanno significato in C89. E l'estensione che non limita il posizionamento della dichiarazione variabile appartiene alle estensioni che non contraddicono C89.

Ad essere onesti, tutti penseranno che dovrebbe essere totalmente conforme a C89 a prima vista dell'opzione "-std = c89". Ma non lo fa. Per quanto riguarda il problema che dichiara che tutte le variabili all'inizio sono migliori o peggiori è solo una questione di abitudine.


conformarsi non significa non accettare estensioni: fintanto che il compilatore compila programmi validi e produce qualsiasi diagnostica richiesta per altri, è conforme.
Ricorda Monica, il

@Marc Lehmann, sì, hai ragione quando la parola "conform" è usata per differenziare i compilatori. Ma quando la parola "conforme" è usata per descrivere alcuni usi, puoi dire "Un uso non è conforme allo standard". E tutti i principianti hanno un'opinione che gli usi che non sono conformi allo standard dovrebbero causare un errore.
Junwanghe,

@Marc Lehmann, a proposito, non c'è diagnostica quando gcc vede l'uso che non è conforme allo standard C89.
Junwanghe,

La tua risposta è ancora sbagliata, perché affermare che "gcc non è conforme" non è la stessa cosa di "alcuni programmi utente non sono conformi". L'utilizzo di conform è semplicemente errato. Inoltre, quando ero un principiante non ero dell'opinione che affermi, quindi anche questo è sbagliato. Infine, non è necessario per un compilatore conforme diagnosticare un codice non conforme e, di fatto, questo è impossibile da implementare.
Ricorda Monica il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.