Colocação de declaração variável em C

129

Eu pensava há muito tempo que em C todas as variáveis ​​tinham que ser declaradas no início da função. Eu sei que em C99, as regras são as mesmas que em C ++, mas quais são as regras de posicionamento de declaração variável para C89 / ANSI C?

O código a seguir é compilado com êxito com 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;
}

As declarações ce não devem scausar um erro no modo C89 / ANSI?

mcjabberz
fonte
54
Apenas uma observação: as variáveis ​​no ansi C não precisam ser declaradas no início de uma função, mas no início de um bloco. Portanto, char c = ... na parte superior do loop for é completamente legal na ansi C. Os char * s, no entanto, não seriam.
Jason Coco

Respostas:

149

Ele é compilado com êxito porque o GCC permite a declaração scomo uma extensão GNU, mesmo que não faça parte do padrão C89 ou ANSI. Se você deseja aderir estritamente a esses padrões, deve passar a -pedanticbandeira.

A declaração de cno início de um { }bloco faz parte do padrão C89; o bloco não precisa ser uma função.

mipadi
fonte
41
Provavelmente vale a pena notar que apenas a declaração de sé uma extensão (do ponto de vista do C89). A declaração de cé perfeitamente legal no C89, sem extensões necessárias.
AnT
7
@AndreyT: Sim, em C, as declarações de variáveis ​​devem ser o início de um bloco e não uma função em si; mas as pessoas confundem bloco com função, pois é o principal exemplo de bloco.
legends2k
1
Mudei o comentário com +39 votos para a resposta.
MarcH
78

Para C89, você deve declarar todas as suas variáveis ​​no início de um bloco de escopo .

Portanto, sua char cdeclaração é válida, pois está no topo do bloco de escopo do loop for. Mas, a char *sdeclaração deve ser um erro.

Kiley Hykawy
fonte
2
Muito correto. Você pode declarar variáveis ​​no início de qualquer {...}.
Artelius
5
@ Artelius Não está correto. Só se os curlies fazem parte de um bloco (não se eles são parte de uma declaração struct ou união ou um inicializador braced.)
Jens
Para ser pedante, a declaração incorreta deve ser pelo menos notificada de acordo com o padrão C. Portanto, deve haver um erro ou um aviso gcc. Ou seja, não confie que um programa possa ser compilado para significar que é compatível.
jinawee
35

O agrupamento de declarações de variáveis ​​na parte superior do bloco é provavelmente um legado devido a limitações de compiladores C primitivos antigos. Todos os idiomas modernos recomendam e às vezes até reforçam a declaração de variáveis ​​locais no ponto mais recente: onde são inicializados pela primeira vez. Porque isso elimina o risco de usar um valor aleatório por engano. Separar declaração e inicialização também impede que você use "const" (ou "final") quando puder.

Infelizmente, o C ++ continua aceitando o caminho antigo de declaração superior para compatibilidade com versões anteriores com C (uma compatibilidade C é arrastada para fora de muitas outras ...) Mas o C ++ tenta se afastar dela:

  • O design das referências em C ++ nem sequer permite esse agrupamento no topo do bloco.
  • Se você separar a declaração e a inicialização de um objeto local C ++ , pagará o custo de um construtor extra por nada. Se o construtor no-arg não existir, novamente você não poderá separar os dois!

C99 começa a mover C nessa mesma direção.

Se você está preocupado em não encontrar onde as variáveis ​​locais são declaradas, isso significa que você tem um problema muito maior: o bloco anexo é muito longo e deve ser dividido.

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

Março
fonte
Veja também como forçar declarações de variáveis ​​na parte superior do bloco pode criar falhas de segurança: lwn.net/Articles/443037
MarcH
"Infelizmente, o C ++ continua aceitando o antigo método de declaração superior para compatibilidade com versões anteriores com o C": IMHO, é apenas a maneira limpa de fazer isso. Outra linguagem "resolve" esse problema sempre inicializando com 0. Bzzt, que apenas oculta erros de lógica se você me perguntar. E há alguns casos em que você PRECISA de uma declaração sem inicialização, porque existem vários locais possíveis para a inicialização. E é por isso que o RAII do C ++ é realmente um grande problema - Agora você precisa incluir um estado não inicializado "válido" em cada objeto para permitir esses casos.
Jo Então,
1
@JoSo: Estou confuso por que você acha que ter leituras de variáveis ​​não inicializadas produz efeitos arbitrários tornará mais fácil detectar erros de programação do que fazê-los produzir um valor consistente ou um erro determinístico? Observe que não há garantia de que uma leitura de armazenamento não inicializado se comporte de maneira consistente com qualquer padrão de bits que a variável possa ter, nem mesmo que esse programa se comporte de maneira consistente com as leis usuais de tempo e causalidade. Dado algo como int y; ... if (x) { printf("X was true"); y=23;} return y;...
supercat 12/06
1
@JoSo: Para ponteiros, especialmente em implementações que interceptam operações null, all-bits-zero geralmente é um valor de interceptação útil. Além disso, em linguagens que especificam explicitamente que as variáveis ​​assumem o padrão de todos os bits zero, confiar nesse valor não é um erro . Os compiladores ainda não tendem a ficar muito malucos com suas "otimizações", mas os escritores do compilador continuam tentando se tornar cada vez mais inteligentes. Uma opção do compilador para inicializar variáveis ​​com variáveis ​​pseudo-aleatórias deliberadas pode ser útil para identificar falhas, mas apenas deixar o armazenamento mantendo seu último valor às vezes pode mascarar falhas.
Supercat
22

Do ponto de vista de manutenção, em vez de sintático, existem pelo menos três linhas de pensamento:

  1. Declare todas as variáveis ​​no início da função para que elas estejam em um só lugar e você poderá ver a lista abrangente rapidamente.

  2. Declare todas as variáveis ​​o mais próximo possível do local em que são usadas pela primeira vez, para que você saiba por que cada uma é necessária.

  3. Declare todas as variáveis ​​no início do bloco de escopo mais interno, para que fiquem fora do escopo o mais rápido possível e permitam ao compilador otimizar a memória e informar se você acidentalmente as usará onde não pretendia.

Geralmente, prefiro a primeira opção, pois acho que as outras geralmente me forçam a procurar o código das declarações. Definir todas as variáveis ​​antecipadamente também facilita a inicialização e a visualização de um depurador.

Às vezes, declararei variáveis ​​dentro de um bloco de escopo menor, mas apenas por uma boa razão, da qual tenho muito poucas. Um exemplo pode ser após a fork(), para declarar variáveis ​​necessárias apenas pelo processo filho. Para mim, esse indicador visual é um lembrete útil de seu objetivo.

Adam Liss
fonte
27
Eu uso a opção 2 ou 3, para que seja mais fácil encontrar as variáveis ​​- porque as funções não devem ser tão grandes que você não consiga ver as declarações das variáveis.
Jonathan Leffler
8
A opção 3 não é um problema, a menos que você use um compilador dos anos 70.
precisa saber é o seguinte
15
Se você usasse um IDE decente, não precisaria procurar códigos, porque deveria haver um comando IDE para encontrar a declaração para você. (F3 no Eclipse)
edgar.holleis
4
Eu não entendo como você pode garantir a inicialização na opção 1; talvez seja possível obter o valor inicial mais tarde no bloco, chamando outra função ou executando uma caclulação.
Plumenator
4
@ Plumenator: a opção 1 não garante a inicialização; Optei por inicializá-los mediante declaração, seja com seus valores "corretos" ou com algo que garanta que o código subsequente será quebrado se não estiverem configurados adequadamente. Eu digo "escolheu" porque minha preferência mudou para # 2 desde que escrevi isso, talvez porque esteja usando Java mais do que C agora e porque tenho melhores ferramentas de desenvolvimento.
Adam Liss
6

Como observado por outros, o GCC é permissivo nesse sentido (e possivelmente outros compiladores, dependendo dos argumentos com os quais são chamados), mesmo quando no modo 'C89', a menos que você use a verificação 'pedante'. Para ser honesto, não há muitas boas razões para não ter pedante; um código moderno de qualidade sempre deve ser compilado sem avisos (ou muito poucos em que você sabe que está fazendo algo específico que é suspeito do compilador como um possível erro); portanto, se você não pode compilar seu código com uma configuração pedante, provavelmente precisará de alguma atenção.

O C89 exige que as variáveis ​​sejam declaradas antes de qualquer outra instrução dentro de cada escopo; os padrões posteriores permitem uma declaração mais próxima do uso (que pode ser mais intuitiva e mais eficiente), especialmente a declaração e a inicialização simultâneas de uma variável de controle de loop nos loops 'for'.

Gaidheal
fonte
0

Como foi observado, existem duas escolas de pensamento sobre isso.

1) Declare tudo no topo das funções porque o ano é 1987.

2) Declare o mais próximo do primeiro uso e no menor escopo possível.

Minha resposta para isso é FAÇA AMBOS! Deixe-me explicar:

Para funções longas, 1) torna a refatoração muito difícil. Se você trabalha em uma base de código em que os desenvolvedores são contrários à ideia de sub-rotinas, você terá 50 declarações de variáveis ​​no início da função e algumas delas podem ser apenas um "i" para um loop for muito parte inferior da função.

Portanto, desenvolvi a declaração no topo do TEPT e tentei fazer a opção 2) religiosamente.

Voltei à opção um por causa de uma coisa: funções curtas. Se suas funções forem curtas o suficiente, você terá poucas variáveis ​​locais e, como a função é curta, se você as colocar no topo da função, elas ainda estarão próximas ao primeiro uso.

Além disso, o antipadrão de "declarar e definir como NULL" quando você deseja declarar na parte superior, mas ainda não fez alguns cálculos necessários para a inicialização, é resolvido porque as coisas que você precisa inicializar provavelmente serão recebidas como argumentos.

Então, agora, meu pensamento é que você deve declarar no topo das funções e o mais próximo possível do primeiro uso. Então AMBOS! E a maneira de fazer isso é com sub-rotinas bem divididas.

Mas se você estiver trabalhando em uma função longa, coloque as coisas mais próximas do primeiro uso, pois assim será mais fácil extrair métodos.

Minha receita é essa. Para todas as variáveis ​​locais, pegue a variável e mova sua declaração para o final, compile e mova a declaração imediatamente antes do erro de compilação. Esse é o primeiro uso. Faça isso para todas as variáveis ​​locais.

int foo = 0;
<code that uses foo>

int bar = 1;
<code that uses bar>

<code that uses foo>

Agora, defina um bloco de escopo que seja iniciado antes da declaração e mova o final até o programa compilar

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

int bar = 1;
<code that uses bar>

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

Isso não é compilado porque há mais código que usa foo. Podemos notar que o compilador foi capaz de passar pelo código que usa bar porque não usa foo. Neste ponto, existem duas opções. O mecânico é apenas mover o "}" para baixo até compilar, e a outra opção é inspecionar o código e determinar se a ordem pode ser alterada para:

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

<code that uses foo>

int bar = 1;
<code that uses bar>

Se a ordem puder ser alterada, provavelmente é isso que você deseja, pois reduz a vida útil dos valores temporários.

Outra coisa a ser observada: o valor de foo precisa ser preservado entre os blocos de código que o utilizam, ou poderia ser apenas um foo diferente em ambos. Por exemplo

int i;

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

<some stuff>

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

Essas situações precisam mais do que meu procedimento. O desenvolvedor precisará analisar o código para determinar o que fazer.

Mas o primeiro passo é encontrar o primeiro uso. Você pode fazer isso visualmente, mas às vezes é mais fácil excluir a declaração, tentar compilar e colocá-la novamente acima do primeiro uso. Se esse primeiro uso estiver dentro de uma instrução if, coloque-o lá e verifique se ele compila. O compilador identificará outros usos. Tente criar um bloco de escopo que inclua os dois usos.

Depois que essa parte mecânica é concluída, fica mais fácil analisar onde estão os dados. Se uma variável for usada em um grande bloco de escopo, analise a situação e veja se você está apenas usando a mesma variável para duas coisas diferentes (como um "i" que é usado por duas para loops). Se os usos não estiverem relacionados, crie novas variáveis ​​para cada um desses usos não relacionados.

Philippe Carphin
fonte
0

Você deve declarar todas as variáveis ​​na parte superior ou "localmente" na função A resposta é:

Depende de que tipo de sistema você está usando:

1 / Sistema Embutido (especialmente relacionado a vidas como Avião ou Carro): Permite o uso de memória dinâmica (por exemplo: calloc, malloc, new ...). Imagine que você está trabalhando em um projeto muito grande, com 1000 engenheiros. E se eles alocarem nova memória dinâmica e se esquecerem de removê-la (quando não usar mais)? Se o sistema incorporado for executado por um longo período, isso causará excesso de pilha e o software será corrompido. Não é fácil garantir a qualidade (a melhor maneira é banir a memória dinâmica).

Se um avião funcionar em 30 dias e não desligar, o que acontece se o software estiver corrompido (quando o avião ainda estiver no ar)?

2 / Os outros sistemas como web, PC (possuem grande espaço de memória):

Você deve declarar a variável "localmente" para otimizar a memória usando. Se esses sistemas funcionarem por um longo período de tempo e ocorrerem estouros de pilha (porque alguém se esqueceu de remover a memória dinâmica). Basta fazer o simples para reiniciar o PC: P Não tem impacto na vida

Dang_Ho
fonte
Não tenho certeza se isso está correto. Eu acho que você está dizendo que é mais fácil auditar vazamentos de memória se você declarar todas as suas variáveis ​​locais em um só lugar? Isso pode ser verdade, mas não tenho tanta certeza de comprar. Quanto ao ponto (2), você diz que declarar a variável localmente "otimizaria o uso da memória"? Isso é teoricamente possível. Um compilador pode optar por redimensionar o quadro da pilha ao longo de uma função para minimizar o uso de memória, mas não conheço ninguém que faça isso. Na realidade, o compilador apenas converterá todas as declarações "locais" em "início da função nos bastidores".
QuinnFreedman
1 / O sistema incorporado em algum momento não permite memória dinâmica; portanto, se você declarar todas as variáveis ​​na parte superior da função. Quando o código-fonte é criado, ele pode calcular o número de bytes necessários na pilha para executar o programa. Mas com memória dinâmica, o compilador não pode fazer o mesmo.
Dang_Ho
2 / Se você declarar uma variável localmente, essa variável só existe dentro de "{}" abrir / fechar colchete. Portanto, o compilador pode liberar o espaço da variável se ela estiver "fora do escopo". Isso pode ser melhor do que declarar tudo no topo da função.
Dang_Ho
Eu acho que você está confuso sobre memória estática vs dinâmica. A memória estática é alocada na pilha. Todas as variáveis ​​declaradas em uma função, independentemente de onde são declaradas, são alocadas estaticamente. A memória dinâmica é alocada no heap com algo parecido malloc(). Embora eu nunca tenha visto um dispositivo incapaz dele, é uma boa prática evitar a alocação dinâmica em sistemas incorporados ( veja aqui ). Mas isso não tem nada a ver com o local em que você declara suas variáveis ​​em uma função.
QuinnFreedman
1
Embora eu concorde que essa seria uma maneira razoável de operar, não é o que acontece na prática. Aqui está a montagem real para algo muito parecido com o seu exemplo: godbolt.org/z/mLhE9a . Como você pode ver, na linha 11, sub rsp, 1008está alocando espaço para toda a matriz fora da instrução if. Isto é verdade para clange gccem cada versão e otimização nível eu tentei.
QuinnFreedman
-1

Vou citar algumas instruções do manual da versão 4.7.0 do gcc para uma explicação clara.

"O compilador pode aceitar vários padrões básicos, como 'c90' ou 'c ++ 98', e dialetos GNU desses padrões, como 'gnu90' ou 'gnu ++ 98'. Ao especificar um padrão básico, o compilador aceitará todos os programas que seguem esse padrão e aqueles que usam extensões GNU que não o contradizem. Por exemplo, '-std = c90' desativa certos recursos do GCC que são incompatíveis com a ISO C90, como asm e typeof keywords, mas não outras extensões GNU que não têm um significado na ISO C90, como omitir o termo do meio de uma expressão?: ".

Eu acho que o ponto principal da sua pergunta é por que o gcc não está em conformidade com o C89, mesmo que a opção "-std = c89" seja usada. Não sei a versão do seu gcc, mas acho que não haverá grande diferença. O desenvolvedor do gcc nos disse que a opção "-std = c89" significa apenas que as extensões que contradizem o C89 estão desativadas. Portanto, não tem nada a ver com algumas extensões que não têm significado no C89. E a extensão que não restringe o posicionamento da declaração da variável pertence às extensões que não contradizem o C89.

Para ser honesto, todos pensam que ele deve estar em conformidade com C89 totalmente à primeira vista da opção "-std = c89". Mas isso não acontece. Quanto ao problema que declara que todas as variáveis ​​no início são melhores ou piores, é apenas uma questão de hábito.

junwanghe
fonte
conformar não significa não aceitar extensões: contanto que o compilador compile programas válidos e produza qualquer diagnóstico necessário para outros, ele estará em conformidade.
Lembre
@ Marc Lehmann, sim, você está certo quando a palavra "conformar" é usada para diferenciar compiladores. Mas quando a palavra "conformar" é usada para descrever alguns usos, você pode dizer "Um uso não conforma o padrão". E todos os iniciantes têm uma opinião de que os usos que não estão em conformidade com o padrão devem causar um erro.
junwanghe
@ Marc Lehmann, a propósito, não há diagnóstico quando o gcc vê o uso que não está em conformidade com o padrão C89.
junwanghe
Sua resposta ainda está errada, porque alegar "gcc não está em conformidade" não é a mesma coisa que "algum programa do usuário não está em conformidade". Seu uso da conformidade é simplesmente incorreto. Além disso, quando eu era iniciante, não era da opinião que você afirma, então isso também está errado. Por fim, não há necessidade de um compilador em conformidade diagnosticar código não conforme e, de fato, isso é impossível de implementar.
Lembre