Одномерные массивы

Урок разбирает массивы 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 и проверять код санитайзерами.

Проверьте себя
1. Что произойдёт при обращении nums[5] к массиву int nums[5]?
AПрограмма выведет последний элемент
BКомпилятор выдаст ошибку
CПрограмма обратится к чужой памяти за пределами массива — без всякой проверки
Dnums[5] вернёт 0
2. Почему sizeof(arr) внутри функции не даёт длину массива?
Asizeof не работает в функциях
BПри передаче в функцию массив «вырождается» в указатель, и sizeof возвращает размер указателя
CМассивы вообще нельзя передавать в функции
Dsizeof всегда возвращает 1