Массивы и указатели в C: от объявления до двумерных массивов

Как объявить массив в C, узнать его длину через sizeof и почему имя массива ведёт себя как указатель.

Разбираем передачу массива в функцию, массив указателей, двумерные массивы и malloc, а в конце четыре частые ошибки с настоящим выводом clang и gcc.

Статью написал:
Ваня Буявец, продюсер, основатель Checkroi
Ваня Буявец
Основатель Checkroi, продюсер, эксперт в выборе онлайн-курсов
Все 3017 статей автора Подписаться на Телеграм-канал
Одобрено экспертом:
Наташа Буявец, основатель Checkroi, эксперт по онлайн-курсам
Наташа Буявец
Основательница Checkroi, продюсер Youtube-каналов, эксперт по онлайн-курсам
Все 3671 экспертное мнение Подписаться на Телеграм-канал
Обложка: Массивы и указатели в C: от объявления до двумерных массивов

Массив в C хранит несколько значений одного типа подряд в памяти. Его объявляют с размером в квадратных скобках: int a[5] = {1, 2, 3};. К элементу обращаются по индексу с нуля, a[0], а длину считают так: sizeof a / sizeof a[0]. Почти в любом выражении имя массива превращается в указатель на первый элемент, поэтому a[i] и *(a + i) означают одно и то же. В функцию массив передают вместе с длиной.

Речь про язык Си. В C# у массива есть свойство длины и методы, в C++ есть std::vector, а в Си ничего этого нет. Чем Си отличается от C++ и C#, мы разбирали отдельно. Если вы только начинаете, сначала пройдите основы языка C: первую программу, типы и циклы.

Примеры ниже собраны компилятором clang 17 с флагами -std=c17 -Wall -Wextra. Адреса в выводе у вас будут другими, остальное совпадёт.

CheckroiCheckroiПодборка курсов по C (язык Си)15 курсов • 6 школСравните цены, школы, программу и найдите выгодные предложения по обучениюСравнить→

Как объявить и заполнить массив

Рой раскладывает пронумерованные ячейки массива на доске

У массива три обязательные части: тип элементов, имя и размер. Размер задают константой, и после объявления он не меняется.

  1. Задайте размер и значения: int scores[5] = {90, 75, 88};. Элементы, которым не хватило значений, станут нулями.
  2. Не хотите считать сами, оставьте скобки пустыми: double prices[] = {199.0, 349.5, 99.9};. Длину компилятор посчитает по списку.
  3. Нужно задать пару элементов в длинном массиве? Помогут назначенные инициализаторы из C99: int flags[6] = {[2] = 1, [5] = 1};.
  4. Меняйте элемент по индексу: scores[3] = 64;. Первый индекс 0, последний на единицу меньше размера.

Макрос LEN в примере считает длину массива привычным для C способом. Работает он только там, где массив объявлен. Почему так, объясним в разделе про функции.

#include <stdio.h>

#define LEN(a) (sizeof(a) / sizeof((a)[0]))

int main(void) {
    int scores[5] = {90, 75, 88};          /* оставшиеся два элемента станут 0 */
    double prices[] = {199.0, 349.5, 99.9}; /* длину посчитает компилятор: 3 */
    int flags[6] = {[2] = 1, [5] = 1};      /* назначенные инициализаторы, C99 */

    scores[3] = 64;

    for (size_t i = 0; i < LEN(scores); i++)
        printf("scores[%zu] = %d\n", i, scores[i]);

    printf("prices: %zu элемента, %zu байт\n", LEN(prices), sizeof prices);
    printf("flags: ");
    for (size_t i = 0; i < LEN(flags); i++)
        printf("%d ", flags[i]);
    printf("\n");
    return 0;
}

Вывод:

scores[0] = 90
scores[1] = 75
scores[2] = 88
scores[3] = 64
scores[4] = 0
prices: 3 элемента, 24 байт
flags: 0 0 1 0 0 1 

Массив prices из трёх double занял 24 байта, по 8 на элемент. А вот локальный массив без инициализатора хранит мусор: int buf[100]; перед чтением нужно заполнить.

Как массив связан с указателем

Ряд ячеек памяти и стрелка-указатель на первую из них

Главное правило звучит так: в выражении имя массива превращается в указатель на его первый элемент. Исключений четыре: операнд sizeof, операнд &, операнд typeof (с C23) и строковый литерал, которым инициализируют массив. Полная формулировка есть в справочнике cppreference, раздел Array to pointer conversion.

Отсюда растёт адресная арифметика: a + 1 сдвигается не на байт, а на один элемент.

#include <stdio.h>

int main(void) {
    int a[4] = {10, 20, 30, 40};
    int *p = a;                 /* то же, что p = &a[0] */

    printf("a[2] = %d, *(a + 2) = %d, p[2] = %d\n", a[2], *(a + 2), p[2]);
    printf("a      = %p\n", (void *)a);
    printf("a + 1  = %p\n", (void *)(a + 1));
    printf("&a + 1 = %p\n", (void *)(&a + 1));
    printf("sizeof a = %zu, sizeof p = %zu\n", sizeof a, sizeof p);

    p++;                        /* указатель можно сдвинуть */
    printf("после p++: *p = %d\n", *p);
    return 0;
}
a[2] = 30, *(a + 2) = 30, p[2] = 30
a      = 0x16d37e850
a + 1  = 0x16d37e854
&a + 1 = 0x16d37e860
sizeof a = 16, sizeof p = 8
после p++: *p = 20

Посмотрите на адреса. От a до a + 1 четыре байта, ровно один int. От a до &a + 1 уже шестнадцать: &a указывает на весь массив, и шаг у него размером с массив.

Выражение Тип Что означает
a int * адрес первого элемента
&a[0] int * то же самое, записанное явно
&a int (*)[4] адрес всего массива: значение то же, шаг другой
*(a + 2) int третий элемент, как a[2]
sizeof a size_t размер массива в байтах, здесь 16

И всё же указатель и массив устроены по-разному. p можно сдвинуть через p++ или направить на другой адрес, а имени массива ничего присвоить нельзя. sizeof p возвращает размер самого указателя, на нашей 64-битной системе 8 байт. Размер данных он не показывает.

Указатели лучше закреплять на задачах с проверкой, их дают курсы по языку C из нашего каталога.

Канал основателя Checkroi Вани Буявца3 700 человек читают мой Телеграм-канал про нейросетиСобрал промпты для Claude Code и ChatGPT, разборы ИИ-инструментов и лайфхаки по продвижению бизнеса в одном местеПрисоединиться

Как передать массив в функцию

Функция в C получает указатель на первый элемент массива, копию она не создаёт. Даже если написать параметр как int data[10], внутри функции это int *data, и число в скобках никто не проверяет.

#include <stdio.h>

double average(const int *data, size_t n) {
    long sum = 0;
    for (size_t i = 0; i < n; i++)
        sum += data[i];
    return n ? (double)sum / n : 0.0;
}

void broken(int data[10]) {
    printf("внутри функции sizeof data = %zu\n", sizeof data);
}

int main(void) {
    int temps[7] = {12, 14, 9, 11, 15, 13, 10};
    printf("в main sizeof temps = %zu\n", sizeof temps);
    broken(temps);
    printf("среднее: %.2f\n", average(temps, sizeof temps / sizeof temps[0]));
    return 0;
}

При сборке clang сразу предупреждает:

ex3.c:11:70: warning: sizeof on array function parameter will return size of 'int *' instead of 'int[10]' [-Wsizeof-array-argument]
   11 |     printf("внутри функции sizeof data = %zu\n", sizeof data);
      |                                                         ^
ex3.c:10:17: note: declared here
   10 | void broken(int data[10]) {
      |                 ^
1 warning generated.

Программа печатает:

в main sizeof temps = 28
внутри функции sizeof data = 8
среднее: 12.00

В main массив занимал 28 байт, а внутри broken() sizeof вернул 8, размер указателя. Поэтому длину передают отдельным параметром, как в average(const int *data, size_t n). Слово const обещает, что функция массив не изменит: случайная запись в data[i] не скомпилируется.

У этого правила есть обратная сторона. Функция работает с исходным массивом, поэтому сортировка или обнуление внутри неё меняют данные вызывающего кода, и возвращать ничего не нужно.

Это предупреждение clang и gcc выдают даже без флагов, а многие другие полезные включает только -Wall. Как включить его в своей среде, рассказываем в статье про компилятор C для Windows, macOS и Linux.

Массив указателей и указатель на массив

Эти объявления отличаются одними скобками, и в них легко запутаться. Читать их помогает приоритет: квадратные скобки сильнее звёздочки.

  • int *ptrs[3]: массив из трёх указателей на int.
  • int (*whole)[3]: один указатель, и указывает он на массив из трёх int.
#include <stdio.h>

int main(void) {
    int x = 1, y = 2, z = 3;
    int *ptrs[3] = {&x, &y, &z};      /* массив из трёх указателей на int */

    int row[3] = {7, 8, 9};
    int (*whole)[3] = &row;           /* указатель на массив из трёх int */

    const char *days[] = {"пн", "вт", "ср"};

    printf("*ptrs[1] = %d\n", *ptrs[1]);
    printf("(*whole)[2] = %d\n", (*whole)[2]);
    printf("sizeof ptrs = %zu, sizeof whole = %zu, sizeof *whole = %zu\n",
           sizeof ptrs, sizeof whole, sizeof *whole);
    printf("days[2] = %s\n", days[2]);
    return 0;
}
*ptrs[1] = 2
(*whole)[2] = 9
sizeof ptrs = 24, sizeof whole = 8, sizeof *whole = 12
days[2] = ср

Три указателя занимают 24 байта, один указатель на массив занимает 8, а сам массив под ним 12. Массив указателей на char чаще всего встречается как список строк. Так устроены argv в main и таблицы названий вроде days.

На микроконтроллерах всё это работает по тем же правилам: скетчи Arduino пишут на C++, где массивы и указатели унаследованы от Си.

Двумерные и многомерные массивы

Двумерный массив в C устроен как массив массивов. Запись int m[2][3] означает две строки по три int. Строки лежат в памяти одна за другой: сразу после m[0][2] идёт m[1][0]. С тремя индексами и больше всё устроено так же, просто вложенность глубже.

Такой массив подходит, например, для карты клеток, по которой едет робот. Где ещё в робототехнике пишут на Си и C++, рассказываем в плане как стать робототехником с нуля.

#include <stdio.h>

#define ROWS 2
#define COLS 3

void print_matrix(int m[][COLS], size_t rows) {
    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < COLS; j++)
            printf("%3d", m[i][j]);
        printf("\n");
    }
}

int main(void) {
    int m[ROWS][COLS] = {
        {1, 2, 3},
        {4, 5, 6},
    };
    int (*row)[COLS] = m;              /* указатель на первую строку */

    print_matrix(m, ROWS);
    printf("m[1][2] = %d, row[1][2] = %d, *(*(m + 1) + 2) = %d\n",
           m[1][2], row[1][2], *(*(m + 1) + 2));
    printf("sizeof m = %zu, sizeof m[0] = %zu\n", sizeof m, sizeof m[0]);
    printf("&m[0][2] + 1 == &m[1][0]: %s\n", (&m[0][2] + 1 == &m[1][0]) ? "да" : "нет");
    return 0;
}
  1  2  3
  4  5  6
m[1][2] = 6, row[1][2] = 6, *(*(m + 1) + 2) = 6
sizeof m = 24, sizeof m[0] = 12
&m[0][2] + 1 == &m[1][0]: да

Обратите внимание на заголовок функции: int m[][COLS]. Число строк можно опустить, а число столбцов обязательно. Без него компилятор не знает, сколько байт пропустить при переходе к следующей строке.

По той же причине m в выражении становится int (*)[3], указателем на строку, а не int **. Если передать матрицу в функцию с параметром int **, clang предупредит: incompatible pointer types passing 'int[2][3]' to parameter of type 'int **'. Это другой тип с другой раскладкой памяти, и такая программа прочитает не те данные.

Двумерный массив, размер которого известен при запуске

Когда число строк и столбцов приходит из файла или от пользователя, память берут через malloc. Самый переносимый способ: один блок на rows * cols элементов и индекс i * cols + j:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t rows = 3, cols = 4;
    int *m = malloc(rows * cols * sizeof *m);
    if (m == NULL)
        return 1;

    for (size_t i = 0; i < rows; i++)
        for (size_t j = 0; j < cols; j++)
            m[i * cols + j] = (int)(i * 10 + j);

    printf("m[2][3] = %d\n", m[2 * cols + 3]);
    free(m);
    return 0;
}
m[2][3] = 23

Запись m[i * cols + j] неудобна, и в C99 появился вариант привычнее: указатель на массив переменной длины.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t rows = 3, cols = 4;

    /* один блок памяти, обращение как к обычной матрице */
    int (*m)[cols] = malloc(rows * sizeof *m);
    if (m == NULL)
        return 1;

    for (size_t i = 0; i < rows; i++)
        for (size_t j = 0; j < cols; j++)
            m[i][j] = (int)(i * 10 + j);

    printf("m[2][3] = %d, всего байт: %zu\n", m[2][3], rows * sizeof *m);
    free(m);
    return 0;
}
m[2][3] = 23, всего байт: 48

Здесь работает обычное m[i][j], но есть оговорка. В C11 и C17 массивы переменной длины необязательны, и компилятор без них определяет макрос __STDC_NO_VLA__. В C23 указатели такого типа стали обязательными, а необязательными остались только сами массивы переменной длины в локальных переменных. Подробности есть в справочном описании массивов.

В обоих вариантах память освобождают одним free(m). Вариант с отдельным malloc на каждую строку тоже рабочий, но освобождать его приходится в цикле, а строки разбросаны по памяти.

Частые ошибки при работе с массивами

Рой радуется, что нашёл ошибку в коде

Выход за границы массива

Цикл for (int i = 0; i <= 5; i++) по массиву из пяти элементов читает шестой, которого нет. C границы не проверяет: программа берёт из памяти то, что лежит рядом. У нас вместо суммы 15 напечаталось 16, у вас может выйти другое число или падение.

Решение: условие i < n, а не i <= n. Найти такую ошибку помогают инструменты, но не все. clang 17 с -Wall -Wextra здесь промолчал. gcc 13 с флагом -O2 предупредил iteration 5 invokes undefined behavior. А сборка с -fsanitize=address остановила программу прямо на ошибке:

==410316==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7e3598800034 …
READ of size 4 at 0x7e3598800034 thread T0
    #0 0x5a4727df2476 in main /tmp/ray-c-arrays/err2.c:7

Присваивание массива целиком

Строка b = a; не копирует массив, она просто не компилируется:

err1.c:4:7: error: array type 'int[3]' is not assignable

Решение: memcpy(b, a, sizeof a); из <string.h> или поэлементное копирование в цикле. Если массив часто нужно присваивать целиком, положите его в структуру. Структуры с массивом внутри присваиваются обычным =, и массив скопируется вместе с ними.

Возврат локального массива из функции

Функция int *make(void), которая возвращает свой локальный int buf[4], отдаёт адрес памяти, которая освободится при выходе из функции. clang это видит:

err3.c:3:12: warning: address of stack memory associated with local variable 'buf' returned [-Wreturn-stack-address]

Решение: пусть массив создаёт вызывающий код и передаёт его в функцию вместе с длиной. Второй путь: выделить память через malloc и не забыть про free.

sizeof внутри функции

Этот случай мы разобрали выше: в параметре массив уже стал указателем, и sizeof вернёт 8.

Встретили в функции sizeof arr / sizeof arr[0] по параметру? Почти наверняка это ошибка. Передайте длину отдельным аргументом.

Ваня БуявецКанал основателя Checkroi Вани БуявцаЗабирайте промпты и обучение по нейросетям в моём Телеграм-каналеБольше 3 700 человек уже применяют Claude Code, ChatGPT и другие нейросети в работе, учёбе, бизнесе и жизниПерейти в канал

Где освоить язык Си системно

После массивов и указателей в изучении Си обычно идут строки, структуры, динамическая память и работа с файлами. Эту часть удобнее проходить с проверкой задач и разбором ошибок. Если думаете о работе на Си, загляните в план как стать программистом на Си с нуля. Все программы по языку собраны на странице курсов по Си, а ниже пять из них.

КурсШколаСтоимость со скидкойВ рассрочкуДлитель­ностьОбзор курса от Checkroi
Инженер-программист
Перейти на сайт курса
GeekBrainsGeekBrains159 408 ₽4428 ₽/мес.12 месяцевОбзор курса
Специалист по информационной безопасности: расширенный курс
Перейти на сайт курса
НетологияНетология117 300 ₽4018 ₽/мес.13 месяцевОбзор курса
Внедрение и работа в DevSecOps
Перейти на сайт курса
OTUSOTUS88 000 ₽8800 ₽/мес.5 месяцевОбзор курса
Блокчейн
Перейти на сайт курса
GeekBrainsGeekBrains168 768 ₽4153 ₽/мес.4 месяцаОбзор курса
Инженер-программист C++
Перейти на сайт курса
GeekBrainsGeekBrains7970 ₽221 ₽/мес.1 месяцОбзор курса

Больше программ — в полном каталоге курсов по C (язык Си)

Часто задаваемые вопросы

Как узнать длину массива в C?

Разделите размер массива на размер одного элемента: sizeof a / sizeof a[0]. Это работает только там, где массив объявлен. Внутри функции параметр уже стал указателем, поэтому длину передают отдельным аргументом.

Чем массив отличается от указателя в C?

Имя массива в выражении превращается в указатель на первый элемент, но сам массив указателем не становится. sizeof у массива возвращает размер всех элементов, а у указателя только размер адреса. Указатель можно сдвинуть или направить на другой адрес, массиву ничего присвоить нельзя.

Как передать двумерный массив в функцию?

Объявите параметр с числом столбцов: void f(int m[][3], size_t rows). Число строк передают отдельно. Параметр int ** для обычной матрицы не подходит: у него другой тип и другая раскладка памяти, и компилятор предупредит о несовместимых указателях.

Можно ли изменить размер массива в C?

Обычный массив нет: размер задаётся при объявлении. Если длина меняется во время работы, память выделяют через malloc и при необходимости увеличивают через realloc, а в конце освобождают через free.

Почему программа не падает при выходе за границы массива?

C границы не проверяет, и чтение за краем массива даёт неопределённое поведение: программа может напечатать мусор, упасть или работать как будто правильно. Найти такую ошибку помогает сборка с флагом -fsanitize=address, она останавливает программу на первом обращении за границу.

Читайте Checkroi первым в Google
Добавьте Checkroi в избранные источники — и наши разборы курсов и обзоры школ будут показываться выше в вашей выдаче Google.
Оставить комментарий
0 комментариев
Форма комментария

Оставьте комментарий

Напишите, что думаете. Нам важно ваше мнение!