O que acontece com uma variável não inicializada declarada em C? Tem um valor?

Se no CI escrever:

int num; 

Antes de atribuir algo a num , o valor de num indeterminado?

Variáveis ​​estáticas (escopo de arquivo e function estática) são inicializadas para zero:

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

Variáveis ​​não estáticas (variables ​​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 algum valor absurdo inicialmente – alguns compiladores podem até mesmo colocar valores fixos específicos para torná-lo óbvio ao procurar um depurador – mas, falando estritamente, o compilador está livre para fazer qualquer coisa, desde bater até invocar demônios através de suas passagens nasais .

Quanto ao motivo de um comportamento indefinido em vez de simplesmente “valor indefinido / arbitrário”, existem várias arquiteturas de CPU que possuem bits de sinalização adicionais em sua representação para vários tipos. Um exemplo moderno seria o Itanium, que tem um bit “Not a Thing” em seus registros ; é claro, os redatores de padrões C estavam considerando algumas arquiteturas mais antigas.

A tentativa de trabalhar com um valor com esse conjunto de bits de sinalização pode resultar em uma exceção de CPU em uma operação que realmente não deve falhar (por exemplo, adição de inteiro ou atribuição a outra variável). E se você for e deixar uma variável não inicializada, o compilador pode pegar algum lixo random com esses bits de sinalização definidos – o que significa que tocar essa variável não inicializada pode ser fatal.

0 se estático ou global, indeterminado se a class de armazenamento for auto

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

Este foi o caso dos compiladores pré-C89 e foi assim especificado por K & R e no relatório C original do DMR.

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

Se um object que possui duração de armazenamento automático não for inicializado explicitamente, seu valor será indeterminado. Se um object que tem duração de armazenamento estático não é inicializado explicitamente, ele é inicializado implicitamente como se todos os membros que possuíssem tipo aritmético fossem atribuídos a 0 e cada membro que possui o tipo de ponteiro fosse atribuído a uma constante de ponteiro nulo.

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

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

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

3.17.2
valor indeterminado
quer um valor não especificado ou uma representação de armadilha

Mas, independentemente do que os padrões dizem, na vida real, cada página de pilha realmente começa como zero, mas quando seu programa olha para qualquer valor de class de armazenamento auto , ele vê o que foi deixado pelo seu próprio programa quando usou esses endereços de pilha pela última vez . Se você alocar muitos arrays auto você os verá eventualmente começarem com zeros.

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

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

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

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

não garante que as variables a e b recebam valores idênticos. Curiosamente, isso 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 “é inicializada com qualquer lixo na memory” não é nem remotamente correta. O comportamento da variável não inicializada é diferente daquele de uma variável inicializada com lixo.

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

Padrões suficientes, vamos olhar para uma implementação 🙂

Variável local

Padrões: comportamento indefinido.

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

 #include  int main() { int i; printf("%d\n", i); } 

ajuntar com:

 gcc -O0 -std=c99 ac 

saídas:

 0 

e descompila com:

 objdump -dr a.out 

para:

 0000000000400536 
: 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 400552: b8 00 00 00 00 mov $0x0,%eax 400557: c9 leaveq 400558: c3 retq

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

  • %rdi é o primeiro argumento printf, assim a string "%d\n" no endereço 0x4005e4

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

    Vem de -0x4(%rbp) , que é a primeira variável local de 4 bytes.

    Neste ponto, a rbp está na primeira página da pilha que foi alocada pelo kernel, então, para entender esse valor, nós rbp o código do kernel e descobriríamos para que ele serve.

    TODO faz o kernel definir essa memory para algo antes de reutilizá-lo para outros processos quando um processo morre? Se não, o novo processo seria capaz de ler a memory de outros programas acabados, vazando dados. Veja: Valores não inicializados são sempre um risco de segurança?

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

 #include  int f() { int i = 13; return i; } int g() { int i; return i; } int main() { f(); assert(g() == 13); } 

Variáveis ​​globais

Padrões: 0

Implementação: seção .bss .

 #include  int i; int main() { printf("%d\n", i); } gcc -00 -std=c99 ac 

compila para:

 0000000000400536 
: 400536: 55 push %rbp 400537: 48 89 e5 mov %rsp,%rbp 40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 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 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 diz que i no endereço 0x601044 e:

 readelf -SW a.out 

contém:

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

que diz 0x601044 está bem no meio da seção .bss , que começa em 0x601040 e tem 8 bytes de comprimento.

O padrão ELF, então, garante que a seção denominada .bss esteja completamente preenchida com zeros:

.bss Esta seção contém dados não inicializados que contribuem para a imagem de memory 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 SHT_NOBITS 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_size Esse membro fornece o tamanho da seção em bytes. A menos que o tipo de seção seja SHT_NOBITS , a seção ocupa bytes sh_size no arquivo. Uma seção do tipo SHT_NOBITS pode ter um tamanho diferente de zero, mas não ocupa espaço no arquivo.

Então, cabe ao kernel do Linux zerar essa região de memory ao carregar o programa na memory quando ele for iniciado.

Depende. Se essa definição for global (fora de qualquer function), então num será inicializado para zero. Se é local (dentro de uma function), então seu valor é indeterminado. Em teoria, até 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 da leitura da variável.

A resposta básica é sim, é indefinida.

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

Se a class de armazenamento for estática ou global, durante o carregamento, o BSS inicializa a variável ou o local de memory (ML) para 0, a menos que a variável receba inicialmente algum valor. No caso de variables ​​não inicializadas locais, a representação de trap é atribuída à localização da memory. Portanto, se algum dos seus registros contendo informações importantes for sobrescrito pelo compilador, o programa poderá falhar.

mas alguns compiladores podem ter mecanismos para evitar tal problema.

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

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

Como uma ruga adicional, muitos compiladores podem manter variables ​​em registradores que são maiores que os tipos associados. Embora seja necessário um compilador para garantir que qualquer valor que seja gravado em uma variável e lido de volta seja truncado e / ou extendido em sinal para seu tamanho adequado, muitos compiladores executarão esse truncamento quando variables ​​forem gravadas e esperam que ele tenha foi executada antes que a variável seja 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 muito bem resultar em wow() armazenar os valores 1234567 nos registradores 0 e 1, respectivamente, e chamar foo() . Como x não é necessário dentro de “foo”, e como as funções devem colocar seu valor de retorno no registrador 0, o compilador pode alocar o registrador 0 para q . Se o mode for 1 ou 3, o registrador 0 será carregado com 2 ou 4, respectivamente, mas se for algum outro valor, a function poderá retornar o que estava no registrador 0 (ou seja, o valor 1234567), mesmo que esse valor não esteja dentro do registrador. intervalo de uint16_t.

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

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

um compilador poderia inferir que, ao invocar moo() com um modo que é maior que 3, inevitavelmente, levar ao programa invocando Undefined Behavior, o compilador pode omitir qualquer código que seria relevante apenas se o mode for 4 ou maior, como o código o que normalmente impediria o lançamento de armas nucleares em tais casos. Note que nem o Standard, nem a moderna filosofia de compilador, se importariam com o fato de que o valor de retorno de "hey" é ignorado - o ato de tentar retorná-lo concede ao compilador uma licença ilimitada para gerar código arbitrário.

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

Tanto quanto eu tinha ido, é principalmente depende do compilador, mas em geral, na maioria dos casos, o valor é pré-assumido como 0 pelos compliers.
Eu tenho valor de lixo no caso de VC ++ enquanto TC deu valor como 0. Eu imprimi-lo como abaixo

 int i; printf('%d',i);