У тому, яким повинен бути останній цикл циклу, до якого ви пишете 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
) може суттєво змінити те, що створювала виконувана програма компілятором. Оптимізація компілятора може зробити багато речей, які можуть здатися неінтуїтивними в програмах, які викликають невизначене поведінку . Ось чому невизначена поведінка залишається повністю невизначеною. Якщо ви дещо відхиляєтесь від треків, то в реальних програмах може бути дуже важко зрозуміти зв’язок між тим, що робить код, і тим, що він повинен був зробити, навіть для досвідчених програмістів.