Menu

Массивы в C: объявление, инициализация и перебор элементов

Как объявлять и инициализировать массивы в C, индексировать их с нуля, вычислять длину через sizeof, перебирать элементы и почему чтение за концом массива — это неопределённое поведение, а не сообщение об ошибке.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Массив — это блок фиксированного размера из элементов одного типа, расположенных в памяти друг за другом. В этой непрерывности вся суть: именно поэтому индексация мгновенна, поэтому массивы и указатели так тесно связаны и поэтому 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.

«Вроде бы работает» — самый опасный вариант, потому что он означает, что баг уйдёт в релиз. Три способа защиты:

  1. Циклы с условием i < n. Большинство выходов за границы — это ошибки смещения на единицу в условии цикла.
  2. Проверяйте индексы, пришедшие извне.
  1. Собирайте код с санитайзером во время разработки. 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] = {};. Без инициализатора содержимое локального массива — неопределённый мусор.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ