Почему первый элемент за пределами определенного массива по умолчанию равен нулю?
Я готовлюсь к выпускному экзамену для вступления в класс C++. Наш профессор дал нам эту задачу для практики:
Объясните, почему код дает следующий результат:
120 200 16 0
using namespace std;
int main()
{
int x[] = {120, 200, 16};
for (int i = 0; i < 4; i++)
cout << x[i] << " ";
}
Пример ответа на проблему был:
Оператор cout просто циклически перебирает элементы массива, индекс которых определяется приращением цикла for. Размер элемента не определяется инициализацией массива. Цикл for определяет размер массива, который превышает количество инициализированных элементов, в результате чего последний элемент по умолчанию равен нулю. Первый цикл for печатает элемент 0 (120), второй - элемент 1 (200), третий цикл - элемент 2 (16), а четвертый цикл печатает значение массива по умолчанию, равное нулю, поскольку для элемента 3 ничего не инициализируется. точка i теперь превышает условие, и цикл for завершается.
Я немного сбит с толку, почему этот последний элемент вне массива всегда «по умолчанию» равен нулю. Чтобы поэкспериментировать, я вставил код проблемы в свою IDE, но изменил цикл for на
5 ответов
Я немного смущен тем, почему этот последний элемент вне массива всегда «по умолчанию» равен нулю.
В этой декларации
int x[] = {120, 200, 16};
массив
x
имеет ровно три элемента. Таким образом, доступ к памяти за пределами массива вызывает неопределенное поведение.
То есть эта петля
for (int i = 0; i < 4; i++)
cout << x[i] << " ";
вызывает неопределенное поведение. В памяти после последнего элемента массива может быть что угодно.
С другой стороны, если массив был объявлен как
int x[4] = {120, 200, 16};
то есть с четырьмя элементами последний элемент массива, не имеющий явного инициализатора, действительно будет инициализирован нулем.
По умолчанию он не равен нулю. Образец ответа неверен. Неопределенное поведение не определено; значение может быть 0, оно может быть 100. Доступ к нему может вызвать ошибку seg или привести к форматированию вашего компьютера.
Что касается того, почему это не ошибка, это потому, что С++ не требуется для проверки границ массивов. Вы можете использовать вектор и использовать
at
функция, которая выбрасывает исключения, если вы выходите за границы, а массивы — нет.
Это вызывает неопределенное поведение, это единственный правильный ответ. Компилятор ожидает, что ваш массив будет содержать ровно три элемента, то, что вы видите в выводе при чтении четвертого целого числа, неизвестно, а в некоторых системах/процессорах может вызвать аппаратное прерывание, вызванное попыткой чтения неадресуемой памяти (система не знает, как это сделать). получить доступ к физической памяти по такому адресу). Компилятор может зарезервировать для
x
память из стека или может использовать регистры (поскольку она очень маленькая). Тот факт, что вы получаете 0, на самом деле случаен. С помощью дезинфицирующего средства адресов в clang (опция -fsanitize=address) вы можете увидеть это:
https://coliru.stacked-crooked.com/a/993d45532bdd4fc2
короткий вывод:
==9469==ERROR: AddressSanitizer: stack-buffer-overflow
Вы можете исследовать это еще дальше в проводнике компилятора с неоптимизированным GCC: https://godbolt.org/z/8T74cr83z (включает asm и вывод программы)
. В этой версии вывод
120 200 16 3
потому что GCC помещается в стек после массива.
Вы увидите, что gcc генерирует следующую сборку для вашего массива:
mov DWORD PTR [rbp-16], 120 # array initializer
mov DWORD PTR [rbp-12], 200
mov DWORD PTR [rbp-8], 16
mov DWORD PTR [rbp-4], 0 # i initializer
так что действительно - есть четвертый элемент со значением 0. Но на самом деле это инициализатор, и к моменту считывания в цикле он имеет другое значение. Компиляторы не изобретают дополнительные элементы массива; в лучшем случае после них останется неиспользуемое пространство стека.
См. уровень оптимизации этого примера - его
-O0
- так последовательная отладка с минимальными оптимизациями; вот почему
i
хранится в памяти, а не в регистре, сохраняемом вызовом. Начните добавлять оптимизации, скажем
-O1
и вы получите:
mov DWORD PTR [rsp+4], 120
mov DWORD PTR [rsp+8], 200
mov DWORD PTR [rsp+12], 16
Дополнительные оптимизации могут полностью оптимизировать ваш массив, например, развертывание и простое использование непосредственных операндов для настройки вызовов
cout.operator<<
. В этот момент undefined-behavior будет полностью виден компилятору, и ему нужно будет что-то придумать. (Регистры для элементов массива были бы правдоподобны в других случаях, если бы значения массива всегда были доступны только с помощью постоянного (после оптимизации) индекса.)
Исправление ответа
Нет, по умолчанию он не равен 0. Это поведение undefined. Просто получилось 0 в этом условии, этой оптимизации и этом компиляторе. Попытка доступа к неинициализированной или нераспределенной памяти является поведением undefined.
Поскольку это буквально «не определено», и в стандарте больше нечего сказать об этом, ваш вывод сборки не будет последовательным. Компилятор может сохранить массив в регистре SIMD, кто знает, что получится на выходе?
Цитата из примера ответа:
и четвертый цикл печатает значение массива по умолчанию, равное нулю, поскольку для элемента 3 ничего не инициализировано.
Это самое неправильное утверждение. Я предполагаю, что в коде опечатка, и они хотели это исправить.
int x[4] = {120, 200, 16};
и ошибочно сделал это
Почему это не ошибка?
Это не ошибка, потому что так работает стек. Вашему приложению не нужно выделять память в стеке, чтобы использовать ее, она уже ваша. Вы можете делать со своим стеком все, что пожелаете. Когда вы объявляете переменную следующим образом:
int a;
все, что вы делаете, это говорите компилятору: «Я хочу, чтобы 4 байта моего стека были для , пожалуйста, не используйте эту память ни для чего другого». во время компиляции. Посмотрите на этот код:
#include <stdio.h>
int main() {
int a;
}
Сборка:
.file "temp.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6 /* Init stack and stuff */
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret /* Pop the stack and return? Yes. It generated literally no code.
All this just makes a stack, pops it and returns. Nothing. */
.cfi_endproc /* Stuff after this is system info, and other stuff
we're not interested. */
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 11.1.0-1ubuntu1~20.04) 11.1.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
Прочитайте комментарии в коде для объяснения.
Итак, вы можете видеть, что ничего не делает. И если я включу оптимизацию, компилятор даже не будет создавать стек и выполнять все эти действия, а вместо этого вернется напрямую.
x - это переменная, которая является целым числом со знаком. Требуется 4 байта, пожалуйста, продолжайте объявление после пропуска этих 4 байтов (и выравнивания).
Переменные в языках высокого уровня (стека) существуют только для того, чтобы сделать «распределение» стека более систематическим и удобным для чтения. Объявление переменной не является процессом выполнения. Он просто учит компилятор, как распределять стек между переменными и соответствующим образом подготавливать программу. При выполнении программа выделяет стек (это процесс времени выполнения), но уже жестко запрограммировано, какие переменные получают какую часть стека. Например. переменная
Вы, как пользователь, можете использовать стек так свободно, как хотите; но вы не можете . Вы всегда должны объявлять переменную или массив, чтобы сообщить об этом компилятору.
Проверка границ
В таких языках, как Go, даже если ваш стек принадлежит вам, компилятор вставит дополнительные проверки, чтобы убедиться, что вы случайно не используете необъявленную память. Это не делается в C и C++ из соображений производительности, и это приводит к более частому возникновению ужасного неопределенного поведения и ошибки сегментации.
Раздел кучи и данных
Куча — это место, где хранятся большие данные. Здесь не хранятся никакие переменные, только данные; и одна или несколько ваших переменных будут содержать указатели на эти данные. Если вы используете материал, который вы не выделили (сделанный во время выполнения), вы получите ошибку сегментации.
Раздел «Данные» — это еще одно место, где можно хранить данные. Здесь можно хранить переменные. Он хранится вместе с вашим кодом, поэтому превышение выделения довольно опасно, так как вы можете случайно изменить код программы. Поскольку он хранится вместе с вашим кодом, он, очевидно, также выделяется во время компиляции. На самом деле я мало что знаю о безопасности памяти в разделе данных. По-видимому, вы можете превысить его без жалоб ОС, но я не знаю больше, так как я не системный хакер и не имею сомнительной цели использовать это в злонамеренных целях. В принципе, я понятия не имею о превышении выделения в разделе данных. Надеюсь, кто-нибудь прокомментирует (или ответит) об этом.
Вся сборка, показанная выше, скомпилирована GCC 11.1 на языке C на компьютере с Ubuntu. Это на C, а не на C++, чтобы улучшить читабельность.
Размер элемента не определяется инициализацией массива. Цикл for определяет размер массива, который превышает количество инициализированных элементов, поэтому по умолчанию последний элемент равен нулю.
Это в корне неверно. Из раздела 11.6.1p5 стандарта С++17 :
Массив неизвестных границ, инициализированный заключенным в фигурные скобки списком инициализаторов, содержащим предложения инициализатора , где
n
должен быть больше нуля, определяется как имеющий n элементов (11.3.4). [ Пример :int x[] = { 1, 3, 5 };
объявляет и инициализирует x как одномерный массив, состоящий из трех элементов, так как размер не указан и имеется три инициализатора. — конец примера ]
Таким образом, для массива без явного размера инициализатор определяет размер массива.
for
цикл читает за конец массива, что приводит к неопределенному поведению .
Тот факт, что 0 печатается для несуществующего 4-го элемента, является просто проявлением неопределенного поведения. Нет никакой гарантии, что это значение будет напечатано. На самом деле, когда я запускаю эту программу, я получаю 3 в качестве последнего значения при компиляции с
-O0
и 0 при компиляции с
-O1
.