Работая на таких высокоуровневых языках как PHP, позволительно не знать устройство массивов для решения повседневных задач. С другой стороны, подобное понимание делает код менее магическим и даёт возможность заглядывать чуть дальше.
Реальные массивы лучше всего рассматривать на языке Cи, который, с одной стороны, достаточно простой и понятный, с другой — очень близок к железу и не скрывает от нас практически ничего. Когда мы говорим про примитивные типы данных, такие как "строка" или "число", то на интуитивном уровне все довольно понятно. Под каждое значение выделяется некоторый размер памяти в соответствии с типом, в которой и хранится само значение. А как должна выделиться память под хранение массива? И что такое массив в памяти? На уровне хранения, понятия массив не существует. Массив представляется цельным куском памяти, размер которого вычисляется по следующей формуле: количество элементов * количество памяти под каждый элемент. Из этого утверждения есть два интересных вывода:
Фактически, индекс в массиве — смещение относительно начала куска памяти, содержащего данные массива. Адрес, по которому расположен элемент под конкретным индексом, рассчитывается так: начальный адрес + индекс * количество памяти, занимаемое одним элементом (для данного типа данных). Начальный адрес, это адрес ячейки памяти, начиная с которой размещается массив. Он формируется во время выделения памяти под массив.
Пример на Си:
// Инициализация массива из пяти элементов типа int
// В этом месте резервируется память под него
// Непрерывный кусок памяти размером _количество элементов * количество байт под int_
int mark[] = {19, 10, 8, 17, 9};
// _Начальный адрес + 3 * количество байт под int_
// Так рассчитывается фактический адрес, по которому располагаются данные
mark[3]; // 17
Если предположить, что тип int
занимает в памяти 2 байта (зависит от архитектуры), то адрес элемента, соответствующего индексу 3
, вычисляется так: начальный адрес + 3 * 2. Для индекса 1
– начальный адрес + 1 * 2.
В такой формуле расчета адреса, есть ровно один способ физически разместить данные в начале доступной памяти – использовать нулевой индекс: начальный адрес + 0 * размер элемента конкретного типа = начальный адрес.
// Первый элемент
// Начальный адрес + 2 * 0 = начальный адрес
mark[0]; // 19
// Начальный адрес + 2 * 1 = начальный адрес + 2
// То есть сместились на 2 байта
mark[1]; // 10
// Начальный адрес + 2 * 2 = начальный адрес + 4
// То есть сместились на 4 байта
mark[2]; // 8
// Последний элемент
// Начальный адрес + 2 * 4 = начальный адрес + 8
// То есть сместились на 8 байт
// И сам элемент занимает 2 байта. В сумме как раз 10
mark[4]; // 9
Теперь должно быть понятно, почему индексы в массиве начинаются с нуля. 0 — означает отсутствие смещения.
Но не все данные имеют одинаковый размер. Как будет храниться массив строк? Строки ведь имеют разную длину, а значит требуют разное количество памяти для своего хранения. Один из способов сохранить строки в массиве на языке Си – создать массив массивов (тут нужно понимать, что любая строка в Си это массив символов). Вложенные массивы обязательно должны быть одного размера, невозможно обойти физические ограничения массивов. Хитрость в том, что этот размер должен быть достаточно большой, чтобы туда поместились необходимые строки.
// Массив из трех элементов, внутри которого массивы по 10 элементов
// Это значит, что здесь можно хранить 3 строки длиной не больше 10 символов
char strings[3][10] = {
"spike",
"tom",
"jerry"
};
strings[0]; // spike
В отличие от высокоуровневых языков, в которых код защищён от выхода за границу массива, в таком языке как Си, выход за границу не приводит к ошибкам. Обращение к элементу, индекс которого находится за пределами массива, вернёт данные, которые лежат в той самой области памяти, куда его попросили обратиться в соответствии с формулой выше. Чем они окажутся — никому не известно (но они будут проинтерпретированы в соответствии с типом массива. Если массив имеет тип int
, то вернётся число). Благодаря отсутствию какой-либо защиты, выход за границу массива активно эксплуатируется хакерами для взлома программ.
В динамических языках, таких как PHP или JS, устройство массивов значительно сложнее чем в си. Так как типы данных вычисляются автоматически во время выполнения кода. Массив в такой среде не может работать как в си. Неизвестно, данные каких типов окажутся внутри в процессе работы.
Массивы в таких языках содержат не сами данные, а ссылки (адреса в памяти) на них. Тогда становится не важно, что хранить. Любое значение в массиве – адрес, имеющий одинаковый размер независимо от данных, на которые он указывает. Такой подход делает массивы гибкими, но с другой стороны, более медленными.
Кроме того, массивы в динамических языках тоже динамические. То есть их размер может увеличиваться или уменьшаться в процессе работы программы. Технически это работает так: если ссылки (помним, что данные там не хранятся) в массив не помещаются, то интерпретатор внутри себя создает новый массив большего размера (обычно в два раза) и переносит все ссылки туда. Динамические массивы очень упрощают процесс разработки, но за это тоже приходится платить скоростью.
Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт