Confusão sobre a inicialização do array em C

102

Na linguagem C, se inicializar uma matriz como esta:

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

então, todos os elementos da matriz que não foram inicializados explicitamente serão inicializados implicitamente com zeros.

Mas, se eu inicializar uma matriz como esta:

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

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

resultado:

1 0 1 0 0

Não entendo, por que a[0]imprime em 1vez de 0? É um comportamento indefinido?

Nota: Esta pergunta foi feita em uma entrevista.

msc
fonte
35
A expressão a[2]=1avalia para 1.
tkausl
14
Uma pergunta muito profunda. Eu me pergunto se o entrevistador sabe a resposta por si mesmo. Eu não. Na verdade, ostensivamente, o valor da expressão a[2] = 1é 1, mas não tenho certeza se você tem permissão para considerar o resultado de uma expressão inicializadora designada como o valor do primeiro elemento. O fato de você ter adicionado a etiqueta de advogado significa que precisamos de uma resposta citando o padrão.
Bathsheba
15
Bem, se essa é a pergunta favorita deles, você pode muito bem ter se esquivado de uma bala. Pessoalmente, prefiro que um exercício de programação escrito (com acesso a um compilador e depurador) seja feito por algumas horas, em vez de perguntas do tipo "ás", como as acima. Eu poderia conjeturar uma resposta, mas não acho que teria qualquer base factual real.
Bathsheba
1
@Bathsheba eu faria o oposto, pois a resposta aqui agora responde a ambas as perguntas.
Adeus SE
1
@Bathsheba seria o melhor. Ainda assim, eu daria o crédito pela pergunta a OP, já que ele surgiu com o assunto. Mas não cabe a mim decidir o que sinto ser "a coisa certa".
Adeus SE

Respostas:

95

TL; DR: Não acho que o comportamento de int a[5]={a[2]=1};está bem definido, pelo menos em C99.

A parte engraçada é que a única parte que faz sentido para mim é a parte sobre a qual você está perguntando: a[0] está definida como 1porque o operador de atribuição retorna o valor que foi atribuído. É tudo o mais que não está claro.

Se o código fosse int a[5] = { [2] = 1 }, tudo teria sido fácil: essa é uma configuração de inicializador designada a[2]para 1e todo o resto para 0. Mas com { a[2] = 1 }temos um inicializador não designado que contém uma expressão de atribuição e caímos em uma toca de coelho.


Aqui está o que descobri até agora:

  • a deve ser uma variável local.

    6.7.8 Inicialização

    1. Todas as expressões em um inicializador para um objeto que tem duração de armazenamento estático devem ser expressões constantes ou literais de string.

    a[2] = 1não é uma expressão constante, portanto, adeve haver armazenamento automático.

  • a está no escopo em sua própria inicialização.

    6.2.1 Escopos de identificadores

    1. As tags de estrutura, união e enumeração têm escopo que começa logo após o aparecimento da tag em um especificador de tipo que declara a tag. Cada constante de enumeração tem um escopo que começa logo após o aparecimento de seu enumerador definidor em uma lista de enumeradores. Qualquer outro identificador tem escopo que começa logo após a conclusão de seu declarador.

    O declarador é a[5], portanto, as variáveis ​​estão no escopo em sua própria inicialização.

  • a está vivo em sua própria inicialização.

    6.2.4 Durações de armazenamento de objetos

    1. Um objeto cujo identificador é declarado sem ligação e sem o especificador de classe de armazenamentostatic tem duração de armazenamento automática .

    2. Para um objeto que não tem um tipo de matriz de comprimento variável, seu tempo de vida se estende desde a entrada no bloco ao qual está associado até a execução desse bloco terminar de alguma forma. (Inserir um bloco fechado ou chamar uma função suspende, mas não termina, a execução do bloco atual.) Se o bloco for inserido recursivamente, uma nova instância do objeto é criada a cada vez. O valor inicial do objeto é indeterminado. Se uma inicialização for especificada para o objeto, ela será realizada toda vez que a declaração for alcançada na execução do bloco; caso contrário, o valor se torna indeterminado cada vez que a declaração é alcançada.

  • Existe um ponto de sequência depois a[2]=1.

    6.8 Declarações e blocos

    1. Uma expressão completa é uma expressão que não faz parte de outra expressão ou de um declarador. Cada um dos seguintes é uma expressão completa: um inicializador ; a expressão em uma declaração de expressão; a expressão de controle de uma declaração de seleção ( ifou switch); a expressão de controle de uma declaração whileou do; cada uma das expressões (opcionais) de uma fordeclaração; a expressão (opcional) em uma returninstrução. O final de uma expressão completa é um ponto de sequência.

    Note-se que por exemplo int foo[] = { 1, 2, 3 }no{ 1, 2, 3 } parte é uma lista anexa-cinta de inicializadores, cada um dos quais tem um ponto sequência após ele.

  • A inicialização é realizada na ordem da lista de inicializadores.

    6.7.8 Inicialização

    1. Cada lista de inicializadores entre chaves possui um objeto atual associado . Quando nenhuma designação está presente, os subobjetos do objeto atual são inicializados em ordem de acordo com o tipo do objeto atual: elementos de array em ordem crescente de subscrito, membros de estrutura em ordem de declaração e o primeiro membro nomeado de uma união. [...]

     

    1. A inicialização deve ocorrer na ordem da lista de inicializadores, cada inicializador fornecido para um subobjeto particular substituindo qualquer inicializador listado anteriormente para o mesmo subobjeto; todos os subobjetos que não são inicializados explicitamente devem ser inicializados implicitamente da mesma forma que os objetos que têm duração de armazenamento estático.
  • No entanto, as expressões do inicializador não são necessariamente avaliadas em ordem.

    6.7.8 Inicialização

    1. A ordem em que os efeitos colaterais ocorrem entre as expressões da lista de inicialização não é especificada.

No entanto, isso ainda deixa algumas perguntas sem resposta:

  • Os pontos de sequência são relevantes? A regra básica é:

    6.5 Expressões

    1. Entre o ponto de sequência anterior e o próximo um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão . Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado.

    a[2] = 1 é uma expressão, mas a inicialização não é.

    Isso é ligeiramente contradito pelo Anexo J:

    J.2 Comportamento indefinido

    • Entre dois pontos de sequência, um objeto é modificado mais de uma vez, ou é modificado e o valor anterior é lido de outra forma que não para determinar o valor a ser armazenado (6.5).

    O Anexo J diz que qualquer modificação conta, não apenas modificações por expressões. Mas, dado que os anexos não são normativos, provavelmente podemos ignorar isso.

  • Como as inicializações de subobjeto são sequenciadas em relação às expressões do inicializador? Todos os inicializadores são avaliados primeiro (em alguma ordem) e, em seguida, os subobjetos são inicializados com os resultados (na ordem da lista de inicializadores)? Ou eles podem ser intercalados?


Acho que int a[5] = { a[2] = 1 }é executado da seguinte forma:

  1. O armazenamento para aé alocado quando o bloco que o contém é inserido. O conteúdo é indeterminado neste momento.
  2. O (único) inicializador é executado ( a[2] = 1), seguido por um ponto de sequência. Isso armazena 1em a[2]e retornos 1.
  3. Isso 1é usado para inicializar a[0](o primeiro inicializador inicializa o primeiro subobjeto).

Mas aqui as coisas ficam confuso porque os elementos restantes ( a[1], a[2], a[3], a[4]) são supostamente para ser inicializado para 0, mas não está claro quando: isso acontece antes a[2] = 1é avaliado? Se sim, a[2] = 1"ganharia" e substituiria a[2], mas essa atribuição teria comportamento indefinido porque não há ponto de sequência entre a inicialização zero e a expressão de atribuição? Os pontos de sequência são relevantes (veja acima)? Ou a inicialização zero ocorre depois que todos os inicializadores são avaliados? Se for assim, a[2]deve acabar sendo 0.

Como o padrão C não define claramente o que acontece aqui, acredito que o comportamento é indefinido (por omissão).

Melpomene
fonte
1
Em vez de indefinido, eu argumentaria que não é especificado , o que deixa coisas abertas para interpretação pelas implementações.
Algum programador cara
1
"caímos em uma toca de coelho" LOL! Nunca ouvi isso para um UB ou coisas não especificadas.
BЈовић
2
@Someprogrammerdude Eu não acho que possa ser não especificado (" comportamento onde este Padrão Internacional fornece duas ou mais possibilidades e não impõe requisitos adicionais sobre o qual é escolhido em qualquer instância ") porque o padrão realmente não fornece quaisquer possibilidades entre as quais escolher. Ele simplesmente não diz o que acontece, o que eu acredito que se enquadra no " comportamento indefinido [...] indicado nesta Norma Internacional [...] pela omissão de qualquer definição explícita de comportamento. "
melpomene
2
@ BЈовић Também é uma descrição muito boa não apenas para comportamento indefinido, mas também para comportamento definido que precisa de um tópico como este para ser explicado.
gnasher729
1
@JohnBollinger A diferença é que você não pode inicializar realmente o a[0]subobjeto antes de avaliar seu inicializador, e avaliar qualquer inicializador inclui um ponto de sequência (porque é uma "expressão completa"). Portanto, acredito que modificar o subobjeto que estamos inicializando é um jogo justo.
melpomene
22

Não entendo, por que a[0]imprime em 1vez de 0?

Provavelmente a[2]=1inicializa a[2]primeiro, e o resultado da expressão é usado para inicializara[0] .

De N2176 (rascunho C17):

6.7.9 Inicialização

  1. As avaliações das expressões da lista de inicialização são sequenciadas indeterminadamente umas em relação às outras e, portanto, a ordem em que ocorrem os efeitos colaterais não é especificada. 154)

Então, parece que a saída 1 0 0 0 0 também teria sido possível.

Conclusão: não escreva inicializadores que modifiquem a variável inicializada em tempo real.

user694733
fonte
1
Essa parte não se aplica: há apenas uma expressão inicializadora aqui, então ela não precisa ser sequenciada com nada.
melpomene
@melpomene Existe a {...}expressão que inicializa a[2]com 0, e a[2]=1a subexpressão que inicializa a[2]com 1.
user694733
1
{...}é uma lista de inicializadores com chaves. Não é uma expressão.
melpomene
@melpomene Ok, você pode estar aí. Mas eu ainda argumentaria que ainda existem 2 efeitos colaterais concorrentes, então esse parágrafo permanece.
user694733
@melpomene, há duas coisas a serem sequenciadas: o primeiro inicializador e a configuração de outros elementos para 0
MM
6

Acho que o padrão C11 cobre esse comportamento e diz que o resultado não é especificado , e não acho que o C18 tenha feito alterações relevantes nesta área.

A linguagem padrão não é fácil de analisar. A seção relevante do padrão é §6.7.9 Inicialização . A sintaxe é documentada como:

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

Observe que um dos termos é expressão de atribuição e , como a[2] = 1é indubitavelmente uma expressão de atribuição, é permitido dentro de inicializadores para matrizes com duração não estática:

§4 Todas as expressões em um inicializador para um objeto que tem duração de armazenamento estático ou thread devem ser expressões constantes ou literais de string.

Um dos parágrafos principais é:

§19 A inicialização deve ocorrer na ordem da lista de inicializadores, cada inicializador fornecido para um subobjeto particular substituindo qualquer inicializador listado anteriormente para o mesmo subobjeto; 151) todos os subobjetos que não são inicializados explicitamente devem ser inicializados implicitamente da mesma forma que os objetos que têm duração de armazenamento estático.

151) Qualquer inicializador para o subobjeto que é sobrescrito e, portanto, não usado para inicializar aquele subobjeto pode não ser avaliado.

E outro parágrafo importante é:

§23 As avaliações das expressões da lista de inicialização são sequenciadas indeterminadamente umas em relação às outras e, portanto, a ordem em que os efeitos colaterais ocorrem não é especificada. 152)

152) Em particular, a ordem de avaliação não precisa ser igual à ordem de inicialização do subobjeto.

Tenho quase certeza de que o parágrafo §23 indica que a notação na questão:

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

leva a um comportamento não especificado. A atribuição a a[2]é um efeito colateral, e a ordem de avaliação das expressões são sequenciadas indeterminadamente uma em relação à outra. Consequentemente, não acho que haja uma maneira de apelar para o padrão e alegar que um compilador específico está lidando com isso correta ou incorretamente.

Jonathan Leffler
fonte
Existe apenas uma expressão da lista de inicialização, portanto §23 não é relevante.
melpomene
2

Meu entendimento é a[2]=1retorna o valor 1, então o código se torna

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

int a[5]={1}atribuir valor para um [0] = 1

Portanto, imprime 1 para um [0]

Por exemplo

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


char str[0] = H’;
char str[1] = a’;
char str[2] = i;
Karthika
fonte
2
Esta é uma pergunta [do advogado da língua], mas não é uma resposta que funcione com a norma, tornando-a irrelevante. Além disso, existem 2 respostas muito mais detalhadas disponíveis e sua resposta parece não acrescentar nada.
Adeus SE
Estou com uma dúvida. O conceito que postei está errado? Você poderia me esclarecer com isso?
Karthika
1
Você apenas especula por razões, embora já haja uma resposta muito boa dada com partes relevantes do padrão. Apenas dizer como isso poderia acontecer não é a questão. É sobre o que o padrão diz que deve acontecer.
Adeus SE
Mas a pessoa que postou a pergunta acima perguntou o motivo e por que isso acontece? Então, só deixei cair essa resposta. Mas o conceito está correto. Certo?
Karthika
OP perguntou " É um comportamento indefinido? ". Sua resposta não diz.
melpomene
1

Tento dar uma resposta curta e simples para o quebra-cabeça: int a[5] = { a[2] = 1 };

  1. Primeiro a[2] = 1é definido. Isso significa que a matriz diz:0 0 1 0 0
  2. Mas eis que, dado que você fez isso nos { }colchetes, que são usados ​​para inicializar o array em ordem, ele pega o primeiro valor (que é 1) e o define como a[0]. É como se int a[5] = { a[2] };fosse ficar, onde já chegamos a[2] = 1. A matriz resultante é agora:1 0 1 0 0

Outro exemplo: int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };- Embora a ordem seja um tanto arbitrária, supondo que vá da esquerda para a direita, ela seguiria estas 6 etapas:

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
Batalha
fonte
1
A = B = C = 5não é uma declaração (ou inicialização). É uma expressão normal que analisa como A = (B = (C = 5))porque o =operador é associativo à direita. Isso realmente não ajuda a explicar como funciona a inicialização. A matriz realmente começa a existir quando o bloco em que está definido é inserido, o que pode demorar muito antes de a definição real ser executada.
melpomene
1
" Vai da esquerda para a direita, cada um começando com a declaração interna " está incorreto. O padrão C diz explicitamente " A ordem em que quaisquer efeitos colaterais ocorrem entre as expressões da lista de inicialização não é especificada. "
melpomene
1
Você testa o código do meu exemplo vezes suficientes e vê se os resultados são consistentes. ” Não é assim que funciona. Você parece não entender o que é um comportamento indefinido. Tudo em C tem comportamento indefinido por padrão; é só que algumas partes têm um comportamento que é definido pelo padrão. Para provar que algo tem comportamento definido, você deve citar o padrão e mostrar onde ele define o que deve acontecer. Na ausência de tal definição, o comportamento é indefinido.
melpomene
1
A afirmação no ponto (1) é um salto enorme sobre a questão-chave aqui: a inicialização implícita do elemento a [2] para 0 ocorre antes que o efeito colateral da a[2] = 1expressão do inicializador seja aplicado? O resultado observado é como se fosse, mas o padrão não parece especificar que esse seja o caso. Esse é o centro da controvérsia, e essa resposta o ignora completamente.
John Bollinger
1
"Comportamento indefinido" é um termo técnico com um significado restrito. Não significa "comportamento sobre o qual não temos certeza". O ponto-chave aqui é que nenhum teste, sem compilador, pode mostrar que um programa em particular é ou não bem-comportado de acordo com o padrão , porque se um programa tiver comportamento indefinido, o compilador tem permissão para fazer qualquer coisa - incluindo trabalhar de uma maneira perfeitamente previsível e razoável. Não é simplesmente um problema de qualidade de implementação em que os escritores do compilador documentam coisas - isso é um comportamento não especificado ou definido pela implementação.
Jeroen Mostert
0

A atribuição a[2]= 1é uma expressão que possui o valor 1e você essencialmente escreveu int a[5]= { 1 };(com o efeito colateral a[2]atribuído 1também).

Yves Daoust
fonte
Mas não está claro quando o efeito colateral é avaliado e o comportamento pode mudar dependendo do compilador. Além disso, o padrão parece afirmar que este é um comportamento indefinido, tornando as explicações para realizações específicas do compilador inúteis.
Adeus SE
@KamiKaze: claro, o valor 1 caiu lá por acidente.
Yves Daoust
0

Acredito que int a[5]={ a[2]=1 };seja um bom exemplo para um programador atirando em si mesmo no próprio pé.

Posso ficar tentado a pensar que o que você quis dizer foi int a[5]={ [2]=1 };qual seria um inicializador designado C99 definindo o elemento 2 para 1 e o resto para zero.

No caso raro em que você realmente quis dizer int a[5]={ 1 }; a[2]=1;, essa seria uma maneira engraçada de escrever. De qualquer forma, seu código se resume a isso, embora alguns aqui tenham apontado que ele não está bem definido quando a gravação a[2]é realmente executada. A armadilha aqui é que a[2]=1não é um inicializador designado, mas uma atribuição simples que tem o valor 1.

Sven
fonte
parece que este tópico de advogado de linguagem está pedindo referências de rascunhos padrão. É por isso que você teve uma votação negativa (eu não fiz isso porque você vê que recebi uma votação negativa pelo mesmo motivo). Acho que o que você escreveu está perfeitamente correto, mas parece que todos esses advogados de idiomas aqui são de comitês ou algo parecido. Então, eles não estão pedindo ajuda, eles estão tentando verificar se o draft cobre o caso ou não e a maioria dos caras aqui são acionados se você responder como se você os estivesse ajudando. Acho que vou deletar minha resposta :) Se as regras deste tópico fossem colocadas claramente, isso teria sido útil
Abdurrahim