Массив в 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. Адреса в выводе у вас будут другими, остальное совпадёт.
Как объявить и заполнить массив

У массива три обязательные части: тип элементов, имя и размер. Размер задают константой, и после объявления он не меняется.
- Задайте размер и значения:
int scores[5] = {90, 75, 88};. Элементы, которым не хватило значений, станут нулями. - Не хотите считать сами, оставьте скобки пустыми:
double prices[] = {199.0, 349.5, 99.9};. Длину компилятор посчитает по списку. - Нужно задать пару элементов в длинном массиве? Помогут назначенные инициализаторы из C99:
int flags[6] = {[2] = 1, [5] = 1};. - Меняйте элемент по индексу:
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 |
|---|---|---|---|---|---|
| Инженер-программист Перейти на сайт курса | 159 408 ₽ | 4428 ₽/мес. | 12 месяцев | Обзор курса | |
| Специалист по информационной безопасности: расширенный курс Перейти на сайт курса | 117 300 ₽ | 4018 ₽/мес. | 13 месяцев | Обзор курса | |
| Внедрение и работа в DevSecOps Перейти на сайт курса | 88 000 ₽ | 8800 ₽/мес. | 5 месяцев | Обзор курса | |
| Блокчейн Перейти на сайт курса | 168 768 ₽ | 4153 ₽/мес. | 4 месяца | Обзор курса | |
| Инженер-программист C++ Перейти на сайт курса | 7970 ₽ | 221 ₽/мес. | 1 месяц | Обзор курса |
Больше программ — в полном каталоге курсов по C (язык Си)




