В каком последнем запуске цикла вы пишете array[10]
, но в массиве всего 10 элементов, пронумерованных от 0 до 9. Спецификация языка C говорит, что это «неопределенное поведение». На практике это означает, что ваша программа будет пытаться записать в тот int
размерный фрагмент памяти, который находится сразу array
в памяти. То, что происходит затем, зависит от того, что на самом деле лежит, и это зависит не только от операционной системы, но в большей степени от компилятора, от параметров компилятора (таких как настройки оптимизации), от архитектуры процессора, от окружающего кода и т. д. Он может даже варьироваться от исполнения к исполнению, например, из-за рандомизации адресного пространства (возможно, не в этом игрушечном примере, но это происходит в реальной жизни). Некоторые возможности включают в себя:
- Место не было использовано. Цикл завершается нормально.
- Местоположение использовалось для чего-то, что оказалось со значением 0. Цикл завершается нормально.
- Местоположение содержало адрес возврата функции. Цикл завершается нормально, но затем происходит сбой программы, потому что она пытается перейти к адресу 0.
- Местоположение содержит переменную
i
. Цикл никогда не заканчивается, потому что i
перезапускается с 0.
- Местоположение содержит некоторую другую переменную. Цикл заканчивается нормально, но потом происходят «интересные» вещи.
- Местоположение является недействительным адресом памяти, например, потому что оно
array
находится в конце страницы виртуальной памяти, а следующая страница не отображается.
- Демоны вылетают из твоего носа . К счастью, большинству компьютеров не хватает необходимого оборудования.
Что вы заметили в Windows, так это то, что компилятор решил поместить переменную i
сразу после массива в память, поэтому в array[10] = 0
конечном итоге присвоил i
. В Ubuntu и CentOS компилятор там не i
размещался. Почти во всех реализациях C локальные переменные группируются в памяти, в стеке памяти , за одним главным исключением: некоторые локальные переменные могут быть полностью помещены в регистры . Даже если переменная находится в стеке, порядок переменных определяется компилятором, и он может зависеть не только от порядка в исходном файле, но и от их типов (чтобы не тратить память на ограничения выравнивания, которые могли бы оставить дыры) по их именам, по некоторым хеш-значениям, используемым во внутренней структуре данных компилятора и т. д.
Если вы хотите узнать, что решил сделать ваш компилятор, вы можете сообщить ему код ассемблера. Да, и научитесь расшифровывать ассемблер (это проще, чем писать). С GCC (и некоторыми другими компиляторами, особенно в мире Unix), передайте возможность -S
создавать ассемблерный код вместо двоичного. Например, вот фрагмент ассемблера для цикла от компиляции с GCC на amd64 с опцией оптимизации -O0
(без оптимизации) с комментариями, добавленными вручную:
.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
Здесь переменная i
находится на 52 байта ниже вершины стека, а массив начинается на 48 байтов ниже вершины стека. Так что этот компилятор оказался i
перед массивом; ты бы переписалi
если бы вам пришлось писать array[-1]
. Если вы измените array[i]=0
на array[9-i]=0
, вы получите бесконечный цикл на этой конкретной платформе с этими опциями компилятора.
Теперь давайте скомпилируем вашу программу с gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Это короче! Компилятор не только отказался выделить место в стеке дляi
- он только когда-либо хранится в регистре ebx
- но он не потрудился выделить память array
или сгенерировать код для установки его элементов, потому что он заметил, что ни один из элементов когда-либо используются.
Чтобы сделать этот пример более наглядным, давайте удостоверимся, что назначения массива выполнены, предоставив компилятору то, что он не может оптимизировать. Самый простой способ сделать это - использовать массив из другого файла - из-за отдельной компиляции компилятор не знает, что происходит в другом файле (если он не оптимизирует во время компоновки, что gcc -O0
или gcc -O1
нет). Создать исходный файлuse_array.c
содержащий
void use_array(int *array) {}
и измените свой исходный код на
#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;
}
Компилировать с
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
На этот раз ассемблерный код выглядит так:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Теперь массив находится в стеке, 44 байта сверху. Как насчет i
? Это нигде не появляется! Но счетчик цикла хранится в реестре rbx
. Это не совсемi
, но адрес array[i]
. Компилятор решил, что, поскольку значение i
никогда не использовалось напрямую, не было смысла выполнять арифметику для вычисления места хранения 0 во время каждого запуска цикла. Вместо этого этот адрес является переменной цикла, и арифметика для определения границ была выполнена частично во время компиляции (умножить 11 итераций на 4 байта на элемент массива, чтобы получить 44) и частично во время выполнения, но один раз и навсегда до запуска цикла ( выполнить вычитание, чтобы получить начальное значение).
Даже на этом очень простом примере мы видели, как изменение параметров компилятора (включение оптимизации) или изменение чего-то незначительного ( array[i]
на array[9-i]
) или даже изменение чего-то явно не связанного (добавление вызова use_array
) может существенно изменить то, что генерировала исполняемая программа компилятором делает. Оптимизация компилятора может сделать много вещей, которые могут казаться неинтуитивными в программах, которые вызывают неопределенное поведение . Вот почему неопределенное поведение остается полностью неопределенным. Когда вы слегка отклоняетесь от треков, в реальных программах может быть очень трудно понять связь между тем, что делает код, и тем, что он должен был сделать, даже для опытных программистов.