O que acontece com uma variável declarada e não inicializada em C? Isso tem algum valor?

139

Se no IC, escreva:

int num;

Antes de atribuir algo a num, o valor é numindeterminado?

atp
fonte
4
Hum, isso não é uma variável definida , não uma declarada ? (Desculpe se esse é o meu C ++ brilhando através ...)
SBI
6
Não. Posso declarar uma variável sem defini-la: no extern int x;entanto, definir sempre implica declarar. Isso não é verdade no C ++, com variáveis ​​de membro de classe estática que podem ser definidas sem declaração, pois a declaração deve estar na definição de classe (não declaração!) E a definição deve estar fora da definição de classe.
bdonlan
ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Parece que definido significa que você também precisa inicializá-lo.
atp

Respostas:

188

As variáveis ​​estáticas (escopo do arquivo e função estática) são inicializadas em zero:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Variáveis ​​não estáticas (variáveis ​​locais) são indeterminadas . Lê-los antes de atribuir um valor resulta em comportamento indefinido.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

Na prática, eles tendem a ter apenas algum valor sem sentido lá inicialmente - alguns compiladores podem até colocar valores fixos específicos para torná-lo óbvio ao procurar em um depurador - mas, estritamente falando, o compilador é livre para fazer qualquer coisa, desde a falha até a convocação demônios através de suas passagens nasais .

Quanto ao motivo pelo qual é um comportamento indefinido em vez de simplesmente "valor indefinido / arbitrário", há várias arquiteturas de CPU que possuem bits de sinalizador adicionais em sua representação para vários tipos. Um exemplo moderno seria o Itanium, que possui uma parte "Not a Thing" em seus registros ; é claro, os redatores do padrão C estavam considerando algumas arquiteturas mais antigas.

Tentar trabalhar com um valor com esses bits de sinalizador definidos pode resultar em uma exceção da CPU em uma operação que realmente não deveria falhar (por exemplo, adição de número inteiro ou atribuição a outra variável). E se você deixar uma variável não inicializada, o compilador poderá pegar algum lixo aleatório com esses bits de sinalizador definidos - o que significa que tocar nessa variável não inicializada pode ser mortal.

bdonlan
fonte
2
oh não, eles não são. Eles podem ser, no modo de depuração, quando você não está na frente de um cliente, em meses com um R em, se você tiver sorte
Martin Beckett
8
o que não é? a inicialização estática é exigida pelo padrão; ver ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan
2
primeiro exemplo é bom, tanto quanto eu posso dizer. Estou menos a respeito de porque o compilador pode falhar na segunda embora :)
6
@ Stuart: existe uma coisa chamada "representação de armadilha", que é basicamente um padrão de bits que não indica um valor válido e pode causar, por exemplo, exceções de hardware em tempo de execução. O único tipo C para o qual existe uma garantia de que qualquer padrão de bits é um valor válido é char; todos os outros podem ter representações de armadilhas. Como alternativa - já que o acesso à variável não inicializada é UB de qualquer maneira - um compilador em conformidade pode simplesmente verificar e decidir sinalizar o problema.
Pavel Minaev 20/10/2009
5
bdonian está correto. C sempre foi especificado com precisão. Antes de C89 e C99, um artigo da dmr especificava todas essas coisas no início dos anos 70. Mesmo no sistema embarcado mais cruel, são necessários apenas um memset () para fazer as coisas corretamente, portanto, não há desculpa para um ambiente não conforme. Eu citei o padrão na minha resposta.
DigitalRoss
57

0 se estático ou global, indeterminado se a classe de armazenamento for automática

C sempre foi muito específico sobre os valores iniciais dos objetos. Se global ou static, eles serão zerados. Se auto, o valor é indeterminado .

Esse foi o caso dos compiladores anteriores à C89 e foi especificado pela K&R e no relatório C original da DMR.

Este foi o caso em C89, consulte a seção 6.5.7 Inicialização .

Se um objeto com duração de armazenamento automático não for inicializado explicitamente, seu valor será indeterminado. Se um objeto com duração de armazenamento estático não for inicializado explicitamente, ele será inicializado implicitamente como se todo membro que tivesse um tipo aritmético tivesse recebido 0 e todo membro que tivesse um tipo de ponteiro tivesse uma constante de ponteiro nulo.

Este foi o caso em C99, consulte a seção 6.7.8 Inicialização .

Se um objeto com duração de armazenamento automático não for inicializado explicitamente, seu valor será indeterminado. Se um objeto que tenha duração de armazenamento estático não for inicializado explicitamente, então:
- se tiver um tipo de ponteiro, ele será inicializado para um ponteiro nulo;
- se tiver um tipo aritmético, é inicializado com zero (positivo ou não);
- se for um agregado, cada membro é inicializado (recursivamente) de acordo com essas regras;
- se for uma união, o primeiro membro nomeado será inicializado (recursivamente) de acordo com estas regras.

Quanto ao que exatamente indeterminado significa, não tenho certeza para C89, C99 diz:

3.17.2
valor indeterminado

, um valor não especificado ou uma representação de interceptação

Porém, independentemente do que dizem os padrões, na vida real, cada página da pilha começa como zero, mas quando o seu programa olha para qualquer autovalor de classe de armazenamento, ele vê o que foi deixado pelo seu próprio programa quando usou esses endereços pela última vez. Se você alocar muitas automatrizes, as verá eventualmente começar com zeros.

Você pode se perguntar, por que é assim? Uma resposta SO diferente lida com essa pergunta, consulte: https://stackoverflow.com/a/2091505/140740

DigitalRoss
fonte
3
indeterminado geralmente (costumava?) significa que pode fazer qualquer coisa. Pode ser zero, pode ser o valor que estava lá, pode travar o programa, pode fazer o computador produzir panquecas de mirtilo a partir do slot do CD. você não tem absolutamente nenhuma garantia. Pode causar a destruição do planeta. Pelo menos na medida em que a especificação vai ... qualquer um que fez um compilador que realmente fez algo assim seria muito mal visto B-)
Brian Postow
No rascunho C11 N1570, a definição de indeterminate valuepode ser encontrada em 3.19.2.
user3528438
É sempre assim que depende do compilador ou do sistema operacional qual o valor que ele define para a variável estática? Por exemplo, se alguém escreve um SO ou um compilador, e se também define o valor inicial por padrão para estática como indeterminado, isso é possível?
precisa saber é o seguinte
1
@AdityaSingh, o sistema operacional pode facilitar o compilador, mas, no final das contas, é responsabilidade principal do compilador executar o catálogo mundial de códigos C existente no mundo e uma responsabilidade secundária em atender aos padrões. Certamente seria possível fazê-lo de maneira diferente, mas por quê? Além disso, é complicado tornar os dados estáticos indeterminados, porque o sistema operacional realmente deseja zerar as páginas primeiro por motivos de segurança. (Variáveis Auto são apenas superficialmente imprevisível porque o seu próprio programa tem sido geralmente usando os endereços de pilha em um ponto anterior.)
DigitalRoss
@BrianPostow Não, isso não está correto. Consulte stackoverflow.com/a/40674888/584518 . O uso de um valor indeterminado causa um comportamento não especificado , não um comportamento indefinido, exceto no caso de representações de trap.
Lundin
12

Depende da duração do armazenamento da variável. Uma variável com duração de armazenamento estático é sempre implicitamente inicializada com zero.

Quanto às variáveis ​​automáticas (locais), uma variável não inicializada tem valor indeterminado . Valor indeterminado, entre outras coisas, significa que qualquer "valor" que você possa "ver" nessa variável não é apenas imprevisível, nem é garantido que seja estável . Por exemplo, na prática (ou seja, ignorando o UB por um segundo) esse código

int num;
int a = num;
int b = num;

não garante essas variáveis ae breceberá valores idênticos. Curiosamente, este não é um conceito teórico pedante, isso acontece prontamente na prática como conseqüência da otimização.

Portanto, em geral, a resposta popular de que "ela é inicializada com o que havia na memória" não é remotamente correta. O comportamento da variável não inicializada é diferente do de uma variável inicializada com lixo.

Formiga
fonte
Eu não consigo entender (bem, eu muito bem pode ) por que isso tem muito menos upvotes do que o de DigitalRoss apenas um minuto depois: D
Antti Haapala
7

Exemplo de Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Padrões suficientes, vejamos uma implementação :-)

Variável local

Padrões: comportamento indefinido.

Implementação: o programa aloca espaço na pilha e nunca move nada para esse endereço; portanto, o que quer que estivesse lá anteriormente é usado.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

ajuntar com:

gcc -O0 -std=c99 a.c

saídas:

0

e descompila com:

objdump -dr a.out

para:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

Pelo nosso conhecimento das convenções de chamada x86-64:

  • %rdié o primeiro argumento printf, portanto, a string "%d\n"no endereço0x4005e4

  • %rsié o segundo argumento printf, portanto i.

    É proveniente da -0x4(%rbp)primeira variável local de 4 bytes.

    Neste ponto, rbpestá na primeira página da pilha que foi alocada pelo kernel, para entender esse valor, devemos procurar no código do kernel e descobrir como ele define isso.

    TODO o kernel define essa memória para algo antes de reutilizá-la para outros processos quando um processo morre? Caso contrário, o novo processo seria capaz de ler a memória de outros programas concluídos, vazando dados. Consulte: Valores não inicializados são sempre um risco à segurança?

Também podemos jogar com nossas próprias modificações de pilha e escrever coisas divertidas como:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Variável local em -O3

Análise de implementação em: O que <valor otimizado> significa em gdb?

Variáveis ​​globais

Padrões: 0

Implementação: .bssseção.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

compila para:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>diz que iestá no endereço 0x601044e:

readelf -SW a.out

contém:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

que diz que 0x601044está bem no meio da .bssseção, que começa em 0x601040e tem 8 bytes de comprimento.

O padrão ELF garante que a seção nomeada .bssseja completamente preenchida com zeros:

.bssEsta seção contém dados não inicializados que contribuem para a imagem da memória do programa. Por definição, o sistema inicializa os dados com zeros quando o programa começa a ser executado. A seção não ocupa espaço no arquivo, conforme indicado pelo tipo de seção SHT_NOBITS,.

Além disso, o tipo SHT_NOBITSé eficiente e não ocupa espaço no arquivo executável:

sh_sizeEste membro fornece o tamanho da seção em bytes. A menos que o tipo de seção seja SHT_NOBITS, a seção ocupa sh_size bytes no arquivo. Uma seção do tipo SHT_NOBITSpode ter um tamanho diferente de zero, mas não ocupa espaço no arquivo.

Cabe ao kernel do Linux zerar a região de memória ao carregar o programa na memória quando ele é iniciado.

Ciro Santilli adicionou uma nova foto
fonte
4

Depende. Se essa definição for global (fora de qualquer função), numserá inicializada como zero. Se for local (dentro de uma função), seu valor é indeterminado. Em teoria, mesmo a tentativa de ler o valor tem um comportamento indefinido - C permite a possibilidade de bits que não contribuem para o valor, mas precisam ser definidos de maneiras específicas para você obter resultados definidos ao ler a variável.

Jerry Coffin
fonte
1

Como os computadores têm capacidade finita de armazenamento, as variáveis ​​automáticas geralmente são mantidas em elementos de armazenamento (sejam registros ou RAM) que foram usados ​​anteriormente para algum outro propósito arbitrário. Se uma variável desse tipo for usada antes que um valor tenha sido atribuído a ela, esse armazenamento poderá conter o que continha anteriormente e, portanto, o conteúdo da variável será imprevisível.

Como uma ruga adicional, muitos compiladores podem manter variáveis ​​em registros maiores que os tipos associados. Embora seja necessário um compilador para garantir que qualquer valor que seja gravado em uma variável e leia novamente seja truncado e / ou estendido para seu tamanho adequado, muitos compiladores farão esse truncamento quando as variáveis ​​forem gravadas e esperam que ele tenha realizada antes da variável ser lida. Em tais compiladores, algo como:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

pode resultar no wow()armazenamento dos valores 1234567 nos registradores 0 e 1, respectivamente, e na chamada foo(). Como xnão é necessário em "foo" e como as funções devem colocar seu valor de retorno no registro 0, o compilador pode alocar o registro 0 para q. Se modefor 1 ou 3, o registro 0 será carregado com 2 ou 4, respectivamente, mas se houver algum outro valor, a função poderá retornar o que estiver no registro 0 (ou seja, o valor 1234567), mesmo que esse valor não esteja dentro do intervalo de uint16_t.

Para evitar exigir que os compiladores façam um trabalho extra para garantir que variáveis ​​não inicializadas nunca pareçam manter valores fora de seu domínio e evitar a necessidade de especificar comportamentos indeterminados com detalhes excessivos, o Padrão diz que o uso de variáveis ​​automáticas não inicializadas é Comportamento Indefinido. Em alguns casos, as consequências disso podem ser ainda mais surpreendentes do que um valor fora do intervalo de seu tipo. Por exemplo, dado:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

um compilador pode inferir que, ao invocar moo()um modo maior que 3, inevitavelmente, levará o programa a invocar o comportamento indefinido, o compilador pode omitir qualquer código que só seria relevante se modefor 4 ou maior, como o código que normalmente impediria o lançamento de armas nucleares nesses casos. Observe que nem o padrão nem a filosofia moderna do compilador se importariam com o fato de o valor de retorno de "hey" ser ignorado - o ato de tentar devolvê-lo fornece ao compilador uma licença ilimitada para gerar código arbitrário.

supercat
fonte
0

A resposta básica é, sim, é indefinido.

Se você estiver vendo um comportamento estranho por causa disso, pode depender de onde é declarado. Se dentro de uma função na pilha, o conteúdo provavelmente será diferente toda vez que a função for chamada. Se for um escopo estático ou de módulo, é indefinido, mas não será alterado.

simon
fonte
0

Se a classe de armazenamento for estática ou global, durante o carregamento, o BSS inicializará a variável ou o local da memória (ML) para 0, a menos que a variável tenha inicialmente atribuído algum valor. No caso de variáveis ​​locais não inicializadas, a representação de interceptação é atribuída ao local da memória. Portanto, se algum de seus registros contendo informações importantes for sobrescrito pelo compilador, o programa poderá falhar.

mas alguns compiladores podem ter um mecanismo para evitar esse problema.

Eu estava trabalhando com a série nec v850 quando percebi que há uma representação de interceptação que possui padrões de bits que representam valores indefinidos para tipos de dados, exceto char. Quando peguei um char não inicializado, obtive um valor padrão zero devido à representação de interceptação. Isso pode ser útil para any1 usando necv850es

hanish
fonte
Seu sistema não é compatível se você obter representações de interceptação ao usar caracteres não assinados. Eles não podem conter explicitamente representações de trap, C17 6.2.6.1/5.
Lundin
-2

O valor de num será algum valor de lixo da memória principal (RAM). é melhor se você inicializar a variável logo após a criação.

Shrikant Singh
fonte
-4

Tanto quanto eu tinha ido, é principalmente dependente do compilador, mas, na maioria dos casos, o valor é assumido como 0 pelos complementadores.
Obtive o valor do lixo no caso do VC ++, enquanto o TC forneceu o valor 0. Imprimo-o como abaixo

int i;
printf('%d',i);
Rajeev Kumar
fonte
Se você obtiver um valor determinístico, por exemplo, 0seu compilador provavelmente executará etapas extras para garantir que ele obtenha esse valor (adicionando código para inicializar as variáveis ​​de qualquer maneira). Alguns compiladores fazem isso ao fazer a compilação "debug", mas escolher o valor 0para isso é uma péssima idéia, pois oculta falhas no seu código (o mais adequado seria garantir um número realmente improvável como 0xBAADF00Dalgo semelhante). Eu acho que a maioria dos compiladores deixará qualquer lixo que ocupe a memória como valor da variável (ou seja, geralmente não é considerado como 0).
SkyKing