Na qual deve ser a última execução do loop, você escreve array[10]
, mas existem apenas 10 elementos na matriz, numerados de 0 a 9. A especificação da linguagem C diz que esse é um "comportamento indefinido". O que isso significa na prática é que seu programa tentará gravar na int
parte de tamanho de memória que fica imediatamente depois array
na memória. O que acontece, então, depende do que realmente existe, e isso depende não apenas do sistema operacional, mas também do compilador, das opções do compilador (como configurações de otimização), da arquitetura do processador, do código circundante , etc. Pode até variar de execução para execução, por exemplo, devido à aleatorização do espaço de endereço (provavelmente não neste exemplo de brinquedo, mas acontece na vida real). Algumas possibilidades incluem:
- A localização não foi usada. O loop termina normalmente.
- A localização foi usada para algo que passou a ter o valor 0. O loop termina normalmente.
- A localização continha o endereço de retorno da função. O loop termina normalmente, mas o programa falha porque tenta pular para o endereço 0.
- A localização contém a variável
i
. O loop nunca termina porque i
reinicia em 0.
- A localização contém outra variável. O loop termina normalmente, mas depois acontecem coisas "interessantes".
- O local é um endereço de memória inválido, por exemplo, porque
array
fica no final de uma página de memória virtual e a página seguinte não é mapeada.
- Demônios voam do seu nariz . Felizmente, a maioria dos computadores não possui o hardware necessário.
O que você observou no Windows foi que o compilador decidiu colocar a variável i
imediatamente após a matriz na memória e array[10] = 0
acabou atribuindo-a i
. No Ubuntu e CentOS, o compilador não foi colocado i
lá. Quase todas as implementações C agrupam variáveis locais na memória, em uma pilha de memória , com uma grande exceção: algumas variáveis locais podem ser colocadas inteiramente em registradores . Mesmo que a variável esteja na pilha, a ordem das variáveis é determinada pelo compilador e pode depender não apenas da ordem no arquivo de origem, mas também de seus tipos (para evitar desperdiçar memória com restrições de alinhamento que deixariam buracos) , em seus nomes, em algum valor de hash usado na estrutura de dados interna de um compilador, etc.
Se você quiser descobrir o que o seu compilador decidiu fazer, você pode pedir para mostrar o código do assembler. Ah, e aprenda a decifrar o assembler (é mais fácil do que escrevê-lo). Com o GCC (e alguns outros compiladores, especialmente no mundo Unix), passe a opção -S
de produzir código assembler em vez de binário. Por exemplo, aqui está o snippet do assembler para o loop compilar com o GCC no amd64 com a opção de otimização -O0
(sem otimização), com comentários adicionados manualmente:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
Aqui, a variável i
está 52 bytes abaixo do topo da pilha, enquanto a matriz inicia 48 bytes abaixo do topo da pilha. Portanto, esse compilador foi colocado i
logo antes da matriz; você substituiria i
se escrevesse array[-1]
. Se você mudar array[i]=0
para array[9-i]=0
, obterá um loop infinito nessa plataforma específica com essas opções específicas do compilador.
Agora vamos compilar seu programa com gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Isso é mais curto! O compilador não apenas se recusou a alocar um local de pilha i
- apenas está armazenado no registro ebx
- como também não se preocupou em alocar memória para array
ou gerar código para definir seus elementos, porque percebeu que nenhum dos elementos são sempre usados.
Para tornar este exemplo mais revelador, vamos garantir que as atribuições da matriz sejam executadas, fornecendo ao compilador algo que não é possível otimizar. Uma maneira fácil de fazer isso é usar a matriz de outro arquivo - por causa da compilação separada, o compilador não sabe o que acontece em outro arquivo (a menos que seja otimizado no momento do link, o que ocorre gcc -O0
ou gcc -O1
não). Crie um arquivo de origem use_array.c
contendo
void use_array(int *array) {}
e mude seu código fonte para
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
Ajuntar com
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Desta vez, o código do assembler fica assim:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Agora a matriz está na pilha, a 44 bytes da parte superior. Que tal i
? Não aparece em lugar nenhum! Mas o contador de loop é mantido no registro rbx
. Não é exatamente i
, mas o endereço do array[i]
. O compilador decidiu que, como o valor de i
nunca foi usado diretamente, não havia sentido em executar aritmética para calcular onde armazenar 0 durante cada execução do loop. Em vez disso, esse endereço é a variável do loop, e a aritmética para determinar os limites foi realizada em parte no tempo de compilação (multiplique 11 iterações por 4 bytes por elemento da matriz para obter 44) e parcialmente no tempo de execução, mas de uma vez por todas antes do início do loop ( faça uma subtração para obter o valor inicial).
Mesmo neste exemplo muito simples, vimos como alterar as opções do compilador (ativar a otimização) ou alterar algo menor ( array[i]
para array[9-i]
) ou até alterar algo aparentemente não relacionado (adicionar a chamada para use_array
) pode fazer uma diferença significativa no que o programa executável gerou pelo compilador faz. As otimizações do compilador podem fazer muitas coisas que podem parecer não intuitivas em programas que invocam um comportamento indefinido . É por isso que o comportamento indefinido é deixado completamente indefinido. Quando você se desvia um pouco das trilhas, em programas do mundo real, pode ser muito difícil entender a relação entre o que o código faz e o que deveria ter feito, mesmo para programadores experientes.