En lo que debería ser la última ejecución del ciclo, debe escribir array[10]
, pero solo hay 10 elementos en la matriz, numerados del 0 al 9. La especificación del lenguaje C dice que este es un "comportamiento indefinido". Lo que esto significa en la práctica es que su programa intentará escribir en la int
pieza de memoria del tamaño que se encuentra inmediatamente después array
en la memoria. Lo que sucede entonces depende de lo que, de hecho, se encuentra allí, y esto depende no solo del sistema operativo sino también del compilador, de las opciones del compilador (como la configuración de optimización), de la arquitectura del procesador, del código circundante , etc. Incluso podría variar de una ejecución a otra, por ejemplo, debido a la aleatorización del espacio de direcciones (probablemente no en este ejemplo de juguete, pero sucede en la vida real). Algunas posibilidades incluyen:
- La ubicación no fue utilizada. El ciclo termina normalmente.
- La ubicación se usó para algo que resultó tener el valor 0. El ciclo termina normalmente.
- La ubicación contenía la dirección de retorno de la función. El ciclo termina normalmente, pero luego el programa se bloquea porque intenta saltar a la dirección 0.
- La ubicación contiene la variable
i
. El ciclo nunca termina porque se i
reinicia en 0.
- La ubicación contiene alguna otra variable. El ciclo termina normalmente, pero luego suceden cosas "interesantes".
- La ubicación es una dirección de memoria no válida, por ejemplo, porque
array
está justo al final de una página de memoria virtual y la página siguiente no está asignada.
- Demonios salen volando de tu nariz . Afortunadamente, la mayoría de las computadoras carecen del hardware necesario.
Lo que observó en Windows fue que el compilador decidió colocar la variable i
inmediatamente después de la matriz en la memoria, por lo que array[10] = 0
terminó asignando a i
. En Ubuntu y CentOS, el compilador no se ubicó i
allí. Casi todas las implementaciones de C agrupan variables locales en la memoria, en una pila de memoria , con una excepción importante: algunas variables locales se pueden colocar completamente en registros . Incluso si la variable está en la pila, el compilador determina el orden de las variables, y puede depender no solo del orden en el archivo fuente sino también de sus tipos (para evitar desperdiciar memoria en restricciones de alineación que dejarían agujeros) , en sus nombres, en algún valor hash utilizado en la estructura de datos interna de un compilador, etc.
Si desea averiguar qué decidió hacer su compilador, puede pedirle que le muestre el código del ensamblador. Ah, y aprende a descifrar ensamblador (es más fácil que escribirlo). Con GCC (y algunos otros compiladores, especialmente en el mundo Unix), pase la opción -S
de producir código ensamblador en lugar de un binario. Por ejemplo, aquí está el fragmento de ensamblador para que el bucle se compile con GCC en amd64 con la opción de optimización -O0
(sin optimización), con comentarios agregados 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
Aquí la variable i
está 52 bytes debajo de la parte superior de la pila, mientras que la matriz comienza 48 bytes debajo de la parte superior de la pila. Por lo tanto, este compilador se coloca i
justo antes de la matriz; sobrescribiría i
si le escribiera array[-1]
. Si cambia array[i]=0
a array[9-i]=0
, obtendrá un bucle infinito en esta plataforma particular con estas opciones de compilador particulares.
Ahora compilemos su programa con gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Eso es más corto! El compilador no solo se ha negado a asignar una ubicación de pila i
, solo se almacena en el registro ebx
, sino que no se ha molestado en asignar memoria array
o generar código para configurar sus elementos, porque notó que ninguno de los elementos son utilizados alguna vez
Para que este ejemplo sea más revelador, asegurémonos de que las asignaciones de la matriz se realicen proporcionando al compilador algo que no puede optimizar. Una forma sencilla de hacerlo es utilizar la matriz de otro archivo - a causa de compilación separada, el compilador no sabe lo que pasa en otro archivo (a menos que se optimiza en tiempo de enlace, que gcc -O0
o gcc -O1
no lo hace). Crear un archivo fuente que use_array.c
contenga
void use_array(int *array) {}
y cambia tu código fuente a
#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;
}
Compilar con
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Esta vez el código del ensamblador se ve así:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Ahora la matriz está en la pila, 44 bytes desde la parte superior. ¿Qué hay de i
? ¡No aparece en ningún lado! Pero el contador de bucle se mantiene en el registro rbx
. No es exactamente i
, pero la dirección de la array[i]
. El compilador ha decidido que, dado que el valor de i
nunca se usó directamente, no tenía sentido realizar operaciones aritméticas para calcular dónde almacenar 0 durante cada ejecución del ciclo. En cambio, esa dirección es la variable de bucle, y la aritmética para determinar los límites se realizó en parte en tiempo de compilación (multiplique 11 iteraciones por 4 bytes por elemento de matriz para obtener 44) y en parte en tiempo de ejecución, pero de una vez por todas antes de que comience el bucle ( realizar una resta para obtener el valor inicial).
Incluso en este ejemplo muy simple, hemos visto cómo cambiar las opciones del compilador (activar la optimización) o cambiar algo menor ( array[i]
a array[9-i]
) o incluso cambiar algo aparentemente no relacionado (agregar la llamada a use_array
) puede marcar una diferencia significativa en lo que generó el programa ejecutable por el compilador lo hace. Las optimizaciones del compilador pueden hacer muchas cosas que pueden parecer poco intuitivas en los programas que invocan un comportamiento indefinido . Es por eso que el comportamiento indefinido se deja completamente indefinido. Cuando se desvía ligeramente de las pistas, en los programas del mundo real, puede ser muy difícil entender la relación entre lo que hace el código y lo que debería haber hecho, incluso para programadores experimentados.