Одномерные массивы
Урок разбирает массивы C: как они объявляются, как лежат в памяти непрерывным блоком, почему C не проверяет границы и как защититься от выхода за пределы.
Массив в C — это непрерывный блок ячеек одного типа. C не хранит его длину и не проверяет границы: выход за пределы массива не вызовет ошибку, а молча испортит соседнюю память.
Массив объявляется с указанием типа и размера. Индексация начинается с нуля, поэтому у массива из n элементов индексы идут от 0 до n-1:
int nums[5] = {10, 20, 30, 40, 50};
printf("%d\n", nums[0]); // 10 — первый
printf("%d\n", nums[4]); // 50 — последний
nums[2] = 99; // меняем третий элемент
Все элементы лежат подряд в памяти, без промежутков. Именно поэтому индексация работает мгновенно: адрес элемента вычисляется как «начало массива плюс индекс умножить на размер типа».
Как работает под капотом
Массив — это просто этикетка на непрерывном участке памяти. Доступ по индексу — это арифметика адресов под капотом:
int nums[5] = {10, 20, 30, 40, 50}; sizeof(int)=4
Индекс: 0 1 2 3 4
Адрес: 0x100 0x104 0x108 0x10C 0x110
+----+ +----+ +----+ +----+ +----+
| 10 | | 20 | | 30 | | 40 | | 50 |
+----+ +----+ +----+ +----+ +----+
nums[i] -> адрес = 0x100 + i * 4
nums[2] -> 0x100 + 2*4 = 0x108 -> 30
ОПАСНО:
nums[5] -> 0x100 + 5*4 = 0x114 -> ЧУЖАЯ память!
(массив кончился на индексе 4)
В C нет встроенной длины массива и нет проверки границ. Обращение nums[5] или nums[100] компилятор пропустит, а программа прочитает или запишет соседнюю память. В лучшем случае это мусор, в худшем — повреждение данных или уязвимость. Длину массива нужно помнить и хранить отдельно.
Частые ошибки
- Выход за границы. Самая опасная ошибка C.
nums[5]при размере 5 — обращение за пределы. - Off-by-one в цикле.
for (i = 0; i <= 5; i++)вместоi < 5заходит на несуществующий элемент. - Попытка узнать длину через sizeof внутри функции. При передаче в функцию массив «вырождается» в указатель, и
sizeofдаёт размер указателя, а не массива. - Частичная инициализация.
int a[5] = {1, 2};— остальные элементы станут нулями, но новички этого не ожидают.
Best practices
- Храните длину массива рядом и передавайте её в функции отдельным параметром:
void process(int *arr, int len). - В цикле используйте границу
i < len, никогдаi <= len. - Для безопасного вычисления числа элементов в той же области, где объявлен массив, применяйте
sizeof(arr) / sizeof(arr[0]). - Проверяйте инструментами:
-fsanitize=addressловит выходы за границы при тестовых прогонах.
Python проверяет границы и кидает ошибку, но смоделируем именно поведение C — «нет проверки границ» — чтобы понять цену этой свободы.
# Имитируем массив C фиксированной длины БЕЗ авто-проверки границ
nums = [10, 20, 30, 40, 50]
length = len(nums) # длину храним отдельно, как в C
def c_get(arr, i, n):
# как в C: проверка границ — наша ответственность
if 0 <= i < n:
return arr[i]
return "ВЫХОД ЗА ГРАНИЦЫ (в C это чужая память!)"
print(c_get(nums, 2, length)) # 30
print(c_get(nums, 5, length)) # предупреждение — индекса 5 нет
Та же логика на Python ▶ — мы вручную проверяем границу, потому что в C никто этого за нас не сделает. Обращение nums[5] в C прошло бы молча и испортило память.
Массивы переменной длины и инициализация
Размер обычного массива в C должен быть известен заранее. Но иногда он становится известен только во время работы — например, пользователь вводит количество элементов. Стандарт C99 добавил массивы переменной длины (VLA): int arr[n];, где n — переменная. Удобно, но с оговорками: такой массив живёт на стеке, а стек невелик, поэтому большой или зависящий от ввода размер может его переполнить. Поэтому для крупных или неизвестных заранее объёмов предпочитают кучу через malloc. Полезно помнить и про инициализацию: запись int a[100] = {0}; обнуляет весь массив, а int a[100]; без инициализации оставляет мусор. Обнулённый старт почти всегда безопаснее и избавляет от целого класса ошибок с неинициализированными данными.
Итоги
Массив в C — непрерывный блок элементов одного типа с индексами от 0 до n-1. Доступ по индексу — это арифметика адресов. Главная особенность и опасность: C не хранит длину и не проверяет границы, поэтому выход за пределы молча портит память. Защита — хранить длину отдельно, использовать границу i < len и проверять код санитайзерами.