Массив — это блок фиксированного размера из элементов одного типа, расположенных в памяти друг за другом. В этой непрерывности вся суть: именно поэтому индексация мгновенна, поэтому массивы и указатели так тесно связаны и поэтому C может выдать вам адрес нулевого элемента, а остальные вы найдёте арифметикой.
Объявление массива
Тип элемента, имя и размер в скобках:
int scores[5]; // 5 значений int
double prices[100]; // 100 значений double
char initials[3]; // 3 символа
Размер — это количество элементов, и оно должно быть известно в месте объявления массива: обычно это литерал или константа, заданная через #define. Память резервируется сразу, ничего не выделяется позже.
Только что объявленный локальный массив содержит мусор, а не нули:
Вторая строка может вывести нули, может вывести огромные числа, и результат может отличаться от запуска к запуску или между отладочной и релизной сборкой. Чтение этих значений — неопределённое поведение; инициализируйте до чтения.
Инициализация
Несколько форм, и все полезны:
Приём {0} стоит запомнить: все элементы, которые вы не перечислили, инициализируются нулём, поэтому один-единственный ноль обнуляет весь массив. Назначенные инициализаторы ([4] = 99) позволяют задать конкретные позиции и оставить остальные нулевыми — удобно для таблиц подстановки, которые по большей части пусты.
Чего сделать нельзя — так это присвоить массив после объявления:
int a[3] = {1, 2, 3};
int b[3];
b = a; // ошибка: массиву нельзя присвоить значение
memcpy(b, a, sizeof a); // а вот так его копируют
Индексация начинается с нуля
Первый элемент — arr[0], последний — arr[n-1]. Никакого arr[n] не существует.
Запустите последний цикл и посмотрите на адреса: каждый ровно на sizeof(int) байт дальше предыдущего. Элементы и правда лежат вплотную друг к другу:
индекс 0 1 2 3 4
+--------+--------+--------+--------+--------+
значение| 88 | 95 | 75 | 60 | 100 |
+--------+--------+--------+--------+--------+
смещение +0 +4 +8 +12 +16 байт
^
&scores[0] - и в него же превращается `scores`
Индексация с нуля — не произвольный выбор: она делает индекс смещением от начала, а это ровно то, что нужно адресной арифметике. scores[i] определяется как *(scores + i); см. указатели и массивы.
Как узнать длину
C нигде не хранит длину массива во время выполнения, но компилятор её знает, и sizeof даёт к ней доступ:
sizeof(arr) / sizeof(arr[0]) — стандартная идиома. Деление на arr[0], а не на жёстко вписанное sizeof(double), означает, что строка продолжит работать, если вы поменяете тип элемента.
Ловушка: это работает только там, где массив был объявлен. Передайте массив в функцию — и параметр станет указателем, так что sizeof измерит указатель, обычно 8 байт, и «длина» получится равной 1 или 2. Именно поэтому любая функция в C, принимающая массив, принимает ещё и количество элементов:
Перебор массива
Стандартная форма — цикл for от 0, пока i < n:
Обратите внимание: i < n, а не i <= n. При n элементах допустимые индексы — от 0 до n - 1, поэтому <= даёт один лишний проход и читает за концом массива. Вторая привычка, которую стоит перенять, — брать начальные значения hottest и coldest из temps[0], а не из 0: максимум, начатый с нуля, тихо ломается на данных, где все значения отрицательны.
Выход за границы — это неопределённое поведение
Это та часть C, которая удивляет пришедших из других языков. Проверки границ нет вообще: ни во время компиляции, ни во время выполнения. arr[10] для массива из пяти элементов — не ошибка, а вычисление адреса, которое компилятор выполняет без единого слова.
int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99; // пишет на 8 байт дальше конца массива
int x = arr[-1]; // читает перед началом
Дальше начинается неопределённое поведение, и его симптомы разнообразны до бесполезности:
- всё вроде бы работает, потому что задетая память никем не использовалась;
- другая переменная меняется без видимой причины;
- программа падает с ошибкой сегментации, возможно, гораздо позже;
- поведение различается между
-O0и-O2.
«Вроде бы работает» — самый опасный вариант, потому что он означает, что баг уйдёт в релиз. Три способа защиты:
- Циклы с условием
i < n. Большинство выходов за границы — это ошибки смещения на единицу в условии цикла. - Проверяйте индексы, пришедшие извне.
- Собирайте код с санитайзером во время разработки.
gcc -fsanitize=address -g prog.cпревращает большинство выходов за границы в немедленный отчёт с именем файла, номером строки и названием массива.
Массивы других типов
Тот же синтаксис работает для любого типа элементов, включая структуры:
На строке char word[6] = "hello"; стоит остановиться: строка в C — это просто массив char, последний значащий байт которого равен '\0'. Именно из-за терминатора под пять букв нужно шесть ячеек.
Кратко о массивах переменной длины
C99 разрешает массив, размер которого вычисляется во время выполнения:
int n = get_count();
int buffer[n]; // массив переменной длины
Две оговорки. Размер фиксируется в момент создания массива, так что VLA — не растущий список. И память берётся из стека, поэтому большое или контролируемое злоумышленником n может его переполнить и уронить программу — вот почему многие проекты, включая ядро Linux, запрещают VLA полностью. К тому же в C11 их поддержка необязательна для реализации.
Когда размер действительно неизвестен до выполнения, выделяйте память динамически:
Обратите внимание: arr[i] читается одинаково, будь arr массивом или указателем на выделенную память — синтаксису индексации это безразлично. Подробности — в разделе динамическая память.
Часто задаваемые вопросы
Как объявить массив в C?
Укажите тип элемента, имя и размер в квадратных скобках: int scores[5]; резервирует место под пять значений типа int. Размер должен быть константой, известной в месте объявления массива (за исключением массивов переменной длины), а элементы остаются неинициализированными, если вы не задали значения.
Как узнать длину массива в C?
sizeof(arr) / sizeof(arr[0]) — общий размер в байтах, делённый на размер одного элемента. Это работает только в той области видимости, где массив был объявлен: как только массив передан в функцию, он превращается в указатель, и sizeof измеряет уже указатель.
Что произойдёт, если выйти за конец массива в C?
Ничего вас не остановит. C не проверяет границы, поэтому arr[10] для массива из пяти элементов читает или пишет ту память, которая там оказалась. Это неопределённое поведение: может вывестись мусор, может испортиться другая переменная, программа может упасть — и всё может выглядеть работающим ровно до того момента, когда перестанет.
Как обнулить все элементы массива в C?
int arr[100] = {0}; — все элементы, которые вы не перечислили, инициализируются нулём, поэтому один ноль обнуляет весь массив. В C23 работает и int arr[100] = {};. Без инициализатора содержимое локального массива — неопределённый мусор.