Confusione sull'inizializzazione dell'array in C


102

In linguaggio C, se inizializza un array come questo:

int a[5] = {1,2};

quindi tutti gli elementi dell'array che non sono inizializzati esplicitamente verranno inizializzati implicitamente con zeri.

Ma, se inizializzo un array come questo:

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

produzione:

1 0 1 0 0

Non capisco, perché a[0]stampa 1invece di 0? È un comportamento indefinito?

Nota: questa domanda è stata posta in un'intervista.


35
L'espressione a[2]=1restituisce 1.
tkausl

14
Una domanda molto profonda. Mi chiedo se l'intervistatore conosce la risposta da solo. Io non. In effetti apparentemente il valore dell'espressione a[2] = 1è 1, ma non sono sicuro se ti è permesso prendere il risultato di un'espressione inizializzatrice designata come valore del primo elemento. Il fatto che tu abbia aggiunto il tag avvocato significa che penso che abbiamo bisogno di una risposta citando lo standard.
Bathsheba

15
Beh, se questa è la loro domanda preferita, potresti aver schivato un proiettile. Personalmente preferisco un esercizio di programmazione scritto (con accesso a un compilatore e debugger) da svolgere in poche ore piuttosto che domande in stile "asso" come sopra. Potrei ipotizzare una risposta, ma non credo che avrebbe alcuna base fattuale reale.
Bathsheba

1
@ Bathsheba farei il contrario, poiché la risposta qui ora risponde a entrambe le domande.
Arrivederci SE

1
@ Bathsheba sarebbe il migliore. Tuttavia darei il merito della domanda a OP, poiché ha presentato l'argomento. Ma non sta a me decidere cosa penso sia "la cosa giusta".
Arrivederci SE

Risposte:


95

TL; DR: Non credo che il comportamento di int a[5]={a[2]=1};sia ben definito, almeno in C99.

La parte divertente è che l'unico bit che ha senso per me è la parte su cui stai chiedendo: a[0]è impostato su 1perché l'operatore di assegnazione restituisce il valore che è stato assegnato. È tutto il resto che non è chiaro.

Se il codice fosse stato int a[5] = { [2] = 1 }, tutto sarebbe stato facile: è un'impostazione di inizializzazione designata a[2]per 1e tutto il resto per 0. Ma con { a[2] = 1 }abbiamo un inizializzatore non designato contenente un'espressione di assegnazione, e cadiamo in una tana di coniglio.


Ecco cosa ho trovato finora:

  • a deve essere una variabile locale.

    6.7.8 Inizializzazione

    1. Tutte le espressioni in un inizializzatore per un oggetto che ha una durata di memorizzazione statica devono essere espressioni costanti o stringhe letterali.

    a[2] = 1non è un'espressione costante, quindi adeve avere la memorizzazione automatica.

  • a è nell'ambito della propria inizializzazione.

    6.2.1 Scopo degli identificatori

    1. I tag di struttura, unione ed enumerazione hanno un ambito che inizia subito dopo la comparsa del tag in uno specificatore di tipo che dichiara il tag. Ogni costante di enumerazione ha un ambito che inizia subito dopo la comparsa del relativo enumeratore di definizione in un elenco di enumeratori. Qualsiasi altro identificatore ha uno scopo che inizia subito dopo il completamento del suo dichiaratore.

    Il dichiaratore è a[5], quindi le variabili rientrano nell'ambito della propria inizializzazione.

  • a è vivo nella propria inizializzazione.

    6.2.4 Durate di conservazione degli oggetti

    1. Un oggetto cui identificatore è dichiarata in alcun legame e senza l'identificatore della classe di archiviazione staticha durata di memorizzazione automatica .

    2. Per un tale oggetto che non ha un tipo di array di lunghezza variabile, la sua durata si estende dall'ingresso nel blocco a cui è associato fino a quando l'esecuzione di quel blocco non termina in alcun modo. (L'immissione di un blocco racchiuso o la chiamata di una funzione sospende, ma non termina, l'esecuzione del blocco corrente.) Se il blocco viene inserito ricorsivamente, viene creata ogni volta una nuova istanza dell'oggetto. Il valore iniziale dell'oggetto è indeterminato. Se per l'oggetto è specificata un'inizializzazione, essa viene eseguita ogni volta che si raggiunge la dichiarazione nell'esecuzione del blocco; in caso contrario, il valore diventa indeterminato ogni volta che viene raggiunta la dichiarazione.

  • C'è un punto di sequenza dopo a[2]=1.

    6.8 Dichiarazioni e blocchi

    1. Una piena espressione è un'espressione che non fa parte di un'altra espressione o di un declarator. Ciascuno dei seguenti è un'espressione completa: un inizializzatore ; l'espressione in una dichiarazione di espressione; l'espressione di controllo di un'istruzione di selezione ( ifo switch); l'espressione di controllo di una dichiarazione whileo do; ciascuna delle espressioni (facoltative) di una fordichiarazione; l'espressione (facoltativa) in returnun'istruzione. La fine di un'espressione completa è un punto della sequenza.

    Si noti che per esempio nel int foo[] = { 1, 2, 3 }la { 1, 2, 3 }parte è una lista brace-chiuso di initializers, ciascuno dei quali ha un punto sequenza dopo.

  • L'inizializzazione viene eseguita nell'ordine dell'elenco degli inizializzatori.

    6.7.8 Inizializzazione

    1. Ogni elenco di inizializzatori racchiuso tra parentesi graffe ha un oggetto corrente associato . Quando non sono presenti designazioni, i suboggetti dell'oggetto corrente vengono inizializzati in ordine in base al tipo di oggetto corrente: elementi della matrice in ordine crescente di pedici, membri della struttura in ordine di dichiarazione e il primo membro denominato di un'unione. [...]

     

    1. L'inizializzazione deve avvenire nell'ordine dell'elenco degli inizializzatori, ogni inizializzatore fornito per un particolare oggetto secondario sovrascrive qualsiasi inizializzatore elencato in precedenza per lo stesso oggetto secondario; tutti i suboggetti che non sono inizializzati esplicitamente devono essere inizializzati implicitamente come gli oggetti che hanno una durata di memorizzazione statica.
  • Tuttavia, le espressioni dell'inizializzatore non vengono necessariamente valutate in ordine.

    6.7.8 Inizializzazione

    1. L'ordine in cui si verificano gli effetti collaterali tra le espressioni dell'elenco di inizializzazione non è specificato.

Tuttavia, ciò lascia ancora alcune domande senza risposta:

  • I punti della sequenza sono anche rilevanti? La regola di base è:

    6.5 Espressioni

    1. Tra il punto della sequenza precedente e quello successivo un oggetto deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione . Inoltre, il valore precedente deve essere letto solo per determinare il valore da memorizzare.

    a[2] = 1 è un'espressione, ma non lo è l'inizializzazione.

    Ciò è leggermente contraddetto dall'allegato J:

    J.2 Comportamento indefinito

    • Tra due punti di sequenza, un oggetto viene modificato più di una volta, oppure viene modificato e il valore precedente viene letto diversamente che per determinare il valore da memorizzare (6.5).

    L'allegato J dice che ogni modifica conta, non solo le modifiche tramite espressioni. Ma dato che gli allegati non sono normativi, possiamo probabilmente ignorarlo.

  • Come vengono sequenziate le inizializzazioni dei suboggetti rispetto alle espressioni di inizializzazione? Tutti gli inizializzatori vengono valutati per primi (in un certo ordine), quindi i suboggetti vengono inizializzati con i risultati (nell'ordine dell'elenco degli inizializzatori)? O possono essere interfogliati?


Penso int a[5] = { a[2] = 1 }sia eseguito come segue:

  1. La memoria per aviene allocata quando viene inserito il blocco contenitore. I contenuti sono indeterminati a questo punto.
  2. L '(unico) inizializzatore viene eseguito ( a[2] = 1), seguito da un punto di sequenza. Questo memorizza 1in a[2]e ritorna 1.
  3. Che 1viene utilizzato per inizializzare a[0](la prima inizializzazione inizializza il primo sotto-oggetto).

Ma qui le cose si fanno sfumata perché gli elementi rimanenti ( a[1], a[2], a[3], a[4]) dovrebbero essere inizializzato a 0, ma non è chiaro quando: lo fa accadere prima a[2] = 1viene valutata? In tal caso, a[2] = 1"vincerebbe" e sovrascriverebbe a[2], ma quell'assegnazione avrebbe un comportamento indefinito perché non esiste un punto di sequenza tra l'inizializzazione zero e l'espressione dell'assegnazione? I punti della sequenza sono anche rilevanti (vedi sopra)? O si verifica zero inizializzazione dopo che tutti gli inizializzatori sono stati valutati? Se è così, a[2]dovrebbe finire per essere 0.

Poiché lo standard C non definisce chiaramente cosa succede qui, credo che il comportamento non sia definito (per omissione).


1
Invece di indefinito, direi che non è specificato , il che lascia le cose aperte all'interpretazione da parte delle implementazioni.
Un tizio programmatore

1
"cadiamo in una tana di coniglio" LOL! Non l'ho mai sentito per un UB o roba non specificata.
BЈовић

2
@Someprogrammerdude Non credo che possa essere non specificato (" comportamento in cui questo standard internazionale fornisce due o più possibilità e non impone ulteriori requisiti su cui viene scelto in ogni caso ") perché lo standard non fornisce davvero alcuna possibilità tra cui scegliere. Semplicemente non dice cosa succede, che credo rientri nella categoria "Il comportamento indefinito è [...] indicato in questa norma internazionale [...] dall'omissione di qualsiasi definizione esplicita di comportamento " .
melpomene

2
@ BЈовић È anche una descrizione molto carina non solo per un comportamento indefinito, ma anche per un comportamento definito che necessita di un thread come questo per essere spiegato.
gnasher729

1
@JohnBollinger La differenza è che non è possibile inizializzare effettivamente il a[0]suboggetto prima di aver valutato il suo inizializzatore e la valutazione di qualsiasi inizializzatore include un punto di sequenza (perché è una "espressione completa"). Pertanto credo che modificare il sottooggetto che stiamo inizializzando sia un gioco leale.
melpomene

22

Non capisco, perché a[0]stampa 1invece di 0?

Presumibilmente viene a[2]=1inizializzato per a[2]primo e il risultato dell'espressione viene utilizzato per inizializzare a[0].

Da N2176 (bozza C17):

6.7.9 Inizializzazione

  1. Le valutazioni delle espressioni dell'elenco di inizializzazione sono sequenziate in modo indeterminato l'una rispetto all'altra e quindi l'ordine in cui si verificano gli effetti collaterali non è specificato. 154)

Quindi sembrerebbe che anche l'output 1 0 0 0 0sarebbe stato possibile.

Conclusione: non scrivere inizializzatori che modificano al volo la variabile inizializzata.


1
Quella parte non si applica: c'è solo un'espressione di inizializzazione qui, quindi non è necessario che sia sequenziata con nulla.
melpomene

@melpomene C'è l' {...}espressione che si inizializza a[2]a 0e la a[2]=1sottoespressione che si inizializza a[2]a 1.
user694733

1
{...}è un elenco di inizializzatori con parentesi graffe. Non è un'espressione.
melpomene

@melpomene Ok, potresti essere proprio lì. Ma direi ancora che ci sono ancora 2 effetti collaterali concorrenti in modo che il paragrafo sia valido.
user694733

@melpomene ci sono due cose da mettere in sequenza: il primo inizializzatore e l'impostazione di altri elementi su 0
MM

6

Penso che lo standard C11 copra questo comportamento e dica che il risultato non è specificato e non credo che C18 abbia apportato modifiche rilevanti in quest'area.

Il linguaggio standard non è facile da analizzare. La sezione pertinente dello standard è §6.7.9 Inizializzazione . La sintassi è documentata come:

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

Si noti che uno dei termini è espressione-assegnazione e poiché a[2] = 1è indubbiamente un'espressione di assegnazione, è consentito all'interno di inizializzatori per array con durata non statica:

§4 Tutte le espressioni in un inizializzatore per un oggetto che ha una durata di archiviazione statica o di thread devono essere espressioni costanti o stringhe letterali.

Uno dei paragrafi chiave è:

§19 L'inizializzazione deve avvenire nell'ordine della lista degli inizializzatori, ogni inizializzatore fornito per un particolare sottooggetto sovrascrive qualsiasi inizializzatore precedentemente elencato per lo stesso sottooggetto; 151) tutti i suboggetti che non sono inizializzati esplicitamente devono essere inizializzati implicitamente come gli oggetti che hanno una durata di memorizzazione statica.

151) Qualsiasi inizializzatore per il suboggetto che viene sovrascritto e quindi non utilizzato per inizializzare quel suboggetto potrebbe non essere valutato affatto.

E un altro paragrafo chiave è:

§23 Le valutazioni delle espressioni della lista di inizializzazione sono in sequenza indeterminata l'una rispetto all'altra e quindi l'ordine in cui si verificano gli effetti collaterali non è specificato. 152)

152) In particolare, l'ordine di valutazione non deve essere lo stesso dell'ordine di inizializzazione del suboggetto.

Sono abbastanza sicuro che il paragrafo §23 indichi che la notazione nella domanda:

int a[5] = { a[2] = 1 };

porta a comportamenti non specificati. L'assegnazione a a[2]è un effetto collaterale e l'ordine di valutazione delle espressioni è in sequenza indeterminata l'una rispetto all'altra. Di conseguenza, non penso che ci sia un modo per fare appello allo standard e affermare che un particolare compilatore lo gestisce correttamente o in modo errato.


Esiste una sola espressione dell'elenco di inizializzazione, quindi §23 non è rilevante.
melpomene

2

La mia comprensione a[2]=1restituisce il valore 1, quindi il codice diventa

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}assegna un valore a [0] = 1

Quindi stampa 1 per a [0]

Per esempio

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;

2
Questa è una domanda [avvocato linguistico], ma questa non è una risposta che funziona con lo standard, rendendola quindi irrilevante. Inoltre ci sono anche 2 risposte molto più approfondite disponibili e la tua risposta non sembra aggiungere nulla.
Arrivederci SE

Ho un dubbio, il concept che ho postato è sbagliato? Potreste chiarirmi con questo?
Karthika

1
Tu speculi solo per ragioni, mentre c'è già una risposta molto buona con parti rilevanti dello standard. Il solo dire come potrebbe accadere non è di cosa si tratta. Riguarda ciò che lo standard dice che dovrebbe accadere.
Arrivederci SE

Ma la persona che ha pubblicato la domanda sopra ha chiesto il motivo e perché accade? Quindi ho solo lasciato cadere questa risposta, ma il concetto è corretto, giusto?
Karthika

OP ha chiesto " È un comportamento indefinito? ". La tua risposta non dice.
melpomene

1

Provo a dare una risposta breve e semplice per il puzzle: int a[5] = { a[2] = 1 };

  1. Il primo a[2] = 1è impostato. Ciò significa che l'array dice:0 0 1 0 0
  2. Ma ecco, dato che l'hai fatto tra { }parentesi, che servono per inizializzare l'array in ordine, prende il primo valore (che è 1) e lo imposta su a[0]. È come se int a[5] = { a[2] };rimanesse, dove già siamo arrivati a[2] = 1. L'array risultante è ora:1 0 1 0 0

Un altro esempio: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Anche se l'ordine è alquanto arbitrario, supponendo che vada da sinistra a destra, andrebbe in questi 6 passaggi:

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3

1
A = B = C = 5non è una dichiarazione (o inizializzazione). È un'espressione normale che analizza come A = (B = (C = 5))perché l' =operatore è associativo corretto. Questo non aiuta davvero a spiegare come funziona l'inizializzazione. L'array inizia effettivamente a esistere quando viene immesso il blocco in cui è definito, il che può trascorrere molto tempo prima che venga eseguita la definizione effettiva.
melpomene

1
" Va da sinistra a destra, ciascuno che inizia con la dichiarazione interna " non è corretto. Lo standard C dice esplicitamente " L'ordine in cui si verificano gli effetti collaterali tra le espressioni dell'elenco di inizializzazione non è specificato. "
melpomene

1
" Testate il codice del mio esempio un numero sufficiente di volte e vedete se i risultati sono coerenti " . Non è così che funziona. Sembra che tu non capisca cosa sia un comportamento indefinito. Tutto in C ha un comportamento non definito per impostazione predefinita; è solo che alcune parti hanno un comportamento definito dallo standard. Per dimostrare che qualcosa ha definito un comportamento, devi citare lo standard e mostrare dove definisce cosa dovrebbe accadere. In assenza di una tale definizione, il comportamento è indefinito.
melpomene

1
L'asserzione al punto (1) rappresenta un enorme balzo in avanti rispetto alla domanda chiave: l'inizializzazione implicita dell'elemento a [2] su 0 si verifica prima a[2] = 1che venga applicato l'effetto collaterale dell'espressione dell'inizializzatore? Il risultato osservato è come se lo fosse, ma lo standard non sembra specificare che dovrebbe essere così. Questo è il centro della controversia e questa risposta lo trascura completamente.
John Bollinger

1
"Comportamento indefinito" è un termine tecnico con un significato ristretto. Non significa "comportamento di cui non siamo veramente sicuri". L'intuizione chiave qui è che nessun test, senza compilatore, può mai mostrare un particolare programma è o non è ben comportato secondo lo standard , perché se un programma ha un comportamento indefinito, al compilatore è permesso fare qualsiasi cosa , incluso il lavoro in modo perfettamente prevedibile e ragionevole. Non è semplicemente un problema di qualità dell'implementazione in cui gli autori del compilatore documentano le cose: è un comportamento non specificato o definito dall'implementazione.
Jeroen Mostert

0

L'assegnazione a[2]= 1è un'espressione che ha il valore 1e in sostanza hai scritto int a[5]= { 1 };(con l'effetto collaterale che a[2]viene assegnato 1anche tu ).


Ma non è chiaro quando viene valutato l'effetto collaterale e il comportamento potrebbe cambiare a seconda del compilatore. Anche lo standard sembra affermare che si tratta di un comportamento indefinito che rende le spiegazioni per realizzazioni specifiche del compilatore non utili.
Arrivederci SE

@KamiKaze: certo, il valore 1 è arrivato lì per caso.
Yves Daoust

0

Credo che int a[5]={ a[2]=1 };sia un buon esempio per un programmatore che si spara al proprio piede.

Potrei essere tentato di pensare che quello che intendevi fosse int a[5]={ [2]=1 };quale sarebbe un elemento di impostazione dell'inizializzatore designato C99 da 2 a 1 e il resto a zero.

Nel raro caso in cui intendevi davvero int a[5]={ 1 }; a[2]=1;, allora sarebbe un modo divertente di scriverlo. Ad ogni modo, questo è ciò a cui si riduce il codice, anche se alcuni qui hanno sottolineato che non è ben definito quando la scrittura a[2]viene effettivamente eseguita. Il trabocchetto qui è che a[2]=1non è un inizializzatore designato ma un semplice assegnamento che a sua volta ha il valore 1.


sembra che questo argomento di avvocato linguistico chieda riferimenti a bozze standard. Questo è il motivo per cui sei downvoted (non l'ho fatto come vedi, sono downvoted per lo stesso motivo). Penso che quello che hai scritto vada benissimo, ma sembra che tutti questi avvocati linguistici qui provengano da un comitato o qualcosa del genere. Quindi non chiedono affatto aiuto, stanno cercando di verificare se la bozza copre il caso o meno e la maggior parte dei ragazzi qui si attiva se rispondi come se li aiutassi. Immagino di non cancellare la mia risposta :) Se questo argomento fosse stato messo in chiaro, sarebbe stato utile
Abdurrahim
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.