JavaRush /Курсы /C++ SELF /Знакомство с массивами: T arr[N]

Знакомство с массивами: T arr[N]

C++ SELF
11 уровень , 1 лекция
Открыта

1. Введение

Когда вы пишете программу, очень быстро оказывается, что одного числа мало. Вам нужно сохранить, например, 7 значений шагов за неделю, 12 месяцев расходов, 10 оценок за контрольные. Можно, конечно, завести переменные x1, x2, x3… но это путь в тёмную сторону, где код начинает напоминать сериал на 500 серий без сценариста.

C‑массив — это «коробка» фиксированного размера, где лежит N элементов одного типа, и к каждому элементу можно обратиться по номеру (индексу).

Синтаксис T arr[N]

Запись выглядит так:

T arr[N];

Где:

  • T — тип элементов (int, double, char и т.д.)
  • arr — имя массива
  • N — количество элементов (фиксированное)

Важно сразу «словить мысль дня»: массив — это один объект, содержащий N элементов. Не «N отдельных переменных», а именно один цельный объект.

Размер задаётся сразу и не меняется

Очень частая «ментальная ловушка» новичка: кажется, что массив — это как список, который можно растягивать. Но C‑массив — не про это. Его размер задаётся в момент объявления и дальше не меняется. Если вы объявили int a[5];, то это всегда 5 ячеек, и никакого «добавить шестую» без смены инструмента не получится (про другие инструменты будет позже, но сегодня держим фокус).

В учебных задачах и в реальном коде хорошая привычка — хранить размер массива не «магическим числом», а константой времени компиляции. Тут нам подходит constexpr и тип std::size_t (мы уже обсуждали, что размеры и индексы обычно живут именно в size_t).

Мини‑пример: объявим недельный трекер шагов (наш мини‑проект на сегодня назовём StepStats).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};            // все элементы = 0
    std::cout << DAYS << '\n';       // 7
}

Обратите внимание на = {} — это не «декор», это ремень безопасности: массив сразу получает предсказуемые значения.

Массив создается компилятором

Длинна массива должна быть известна на момент компиляции программы. Да, да. Такой код работать не будет:

#include <cstddef>
#include <iostream>

int main() {
    int n;
    std::cin >> n;
    int arr[n];  // ❌ в стандартном C++ это ошибка
}

Все дело в том, что С++ очень старый язык и на момент его создания это было нормой.

2. Индексация: от 0 до N - 1

Теперь самая важная часть, из‑за которой ломаются судьбы, дедлайны и иногда клавиатуры: индексы.

У массива из N элементов допустимые индексы такие:

  • первый элемент: arr[0]
  • последний элемент: arr[N - 1]

То есть диапазон индексов: от 0 до N-1 включительно.

Это историческая традиция C/C++: индекс — это «смещение от начала». Смещение до первого элемента равно нулю, поэтому и первый индекс — 0.

Вот удобная табличка для фиксации в голове:

Количество элементов N Первый индекс Последний индекс
1
0
0
2
0
1
5
0
4
7
0
6

Давайте покажем это в коде (и заодно потренируемся не писать «последний элемент = arr[N]», потому что это неправда):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {1, 2, 3, 4, 5};

    std::cout << a[0] << '\n';       // 1
    std::cout << a[N - 1] << '\n';   // 5
}

3. Обход массива циклом: граница i < N

Когда у нас есть массив, почти всегда следующий шаг — пройти по нему циклом. И вот тут возникает легендарная ошибка «off‑by‑one»: на единицу промахнулся — и добро пожаловать в страну странностей.

Правильная форма цикла по массиву длины N:

for (std::size_t i = 0; i < N; ++i) {
    // работаем с arr[i]
}

Почему строго < N, а не <= N? Потому что arr[N] — это уже не элемент массива. Последний — arr[N - 1].

Схема, которую полезно держать в голове:

flowchart LR
    A["i = 0"] --> B{"i < N ?"}
    B -- да --> C["используем arr[i]"]
    C --> D["++i"]
    D --> B
    B -- нет --> E["конец цикла"]

И пример для StepStats: выведем все 7 значений (пока они нулевые).

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t i = 0; i < DAYS; ++i) {
        std::cout << steps[i] << ' ';    // 0 0 0 0 0 0 0
    }
    std::cout << '\n';
}

4. Инициализация и ввод массива

Переходим к теме, где новички часто получают «мусор» в выводе и начинают подозревать, что компьютер их ненавидит. Компьютер вас не ненавидит. Он просто буквально выполняет то, что вы написали.

Полная инициализация {...}

Если вы дали значения всем элементам — всё просто и красиво:

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 3;
    int a[N] = {10, 20, 30};

    std::cout << a[1] << '\n'; // 20
}

Частичная инициализация {10, 20}

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

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 5;
    int a[N] = {10, 20};   // остальные станут 0

    for (std::size_t i = 0; i < N; ++i) {
        std::cout << a[i] << ' ';    // 10 20 0 0 0
    }
    std::cout << '\n';
}

Нулевая инициализация = {}

Если вы хотите «все элементы в 0» — пишите = {}. Это один из лучших способов получить предсказуемость.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t N = 4;
    double x[N] = {};  // 0.0 0.0 0.0 0.0

    std::cout << x[0] << '\n'; // 0
}

Почему «неинициализированный» массив опасен

Важное предупреждение. Если вы напишете:

int a[5];

…то элементы не обязаны быть нулями. Там может быть что угодно. Не потому что C++ вредный, а потому что вы попросили создать массив и не сказали, какими значениями его заполнить.

Мы не будем делать пример, который «показывает мусор», потому что это будет зависеть от компилятора и среды, а учебный пример должен быть воспроизводимым. Но правило простое: если массив должен начинаться «с чистого листа», используйте = {}.

В StepStats это особенно логично: пока пользователь не ввёл шаги, значения должны быть нулевыми, а не «случайными числами, которые отлично мотивируют на спорт, но только если вы не знаете, что это мусор».

Шаблон «прочитать N значений»

Теперь сделаем первый полезный шаг в нашем StepStats: введём шаги за неделю.

Здесь важный момент: ввод через std::cin не гарантирует, что пользователь введёт корректные числа, но полноценную обработку ошибок ввода мы сегодня не усложняем — мы просто тренируем механику массива и циклов.

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) {
        std::cin >> steps[d];        // вводим 7 чисел
    }
}

А теперь добавим вывод, чтобы проверить, что «мы действительно сохранили» (не только прочитали и забыли):

#include <cstddef>
#include <iostream>

int main() {
    constexpr std::size_t DAYS = 7;
    int steps[DAYS] = {};

    for (std::size_t d = 0; d < DAYS; ++d) std::cin >> steps[d];
    for (std::size_t d = 0; d < DAYS; ++d) std::cout << steps[d] << ' ';
    std::cout << '\n';
}

5. Почему выход за границы — это хуже ошибки

В языках с жёсткими проверками границ (или в контейнерах, которые умеют проверять границы) вы обычно получите понятную ошибку. В C‑массивах всё суровее: если вы обратились к arr[N] или arr[999], компилятор часто не может это отследить, а во время выполнения вы можете получить непредсказуемое поведение.

Это называют UB (undefined behavior) — поведение, которое стандарт не обязан определять. На человеческом языке: «может работать, может падать, может тихо испортить данные, а может притвориться, что всё нормально, чтобы сломаться завтра».

Тема UB вообще огромная, но сегодня нам достаточно одного практического правила: никогда не обращайтесь к элементам вне 0..N-1. И да, это включает «случайно <= вместо <».

Кстати, сам факт, что в стандарте отдельно обсуждают индексирование и его тонкости (вроде вопросов “array subscripting and xvalues”), намекает, что операция не такая простая, как выглядит.

6. Частые ошибки в коде: примеры и профилактика

Сейчас будет раздел, который экономит часы жизни. Ошибки тут максимально человеческие: вы не «плохой программист», вы просто впервые живёте в мире, где 0 — это «первый», а N — это «уже мимо».

i <= N вместо i < N

Ошибка выглядит невинно: «я хочу пройти N элементов». Но при i <= N вы сделаете N+1 итерацию и полезете в arr[N].

Плохой код (не делайте так):

for (std::size_t i = 0; i <= N; ++i) {
    std::cout << arr[i];
}

Хороший код:

for (std::size_t i = 0; i < N; ++i) {
    std::cout << arr[i];
}

Путаница «последний элемент» и arr[N]

Это почти всегда «ошибка мышления»: кажется, что если элементов N, то последний индекс N. Но индексы начинаются с 0, так что последний — N - 1.

В StepStats последний день недели (седьмой по счёту для человека) — это индекс 6.

«Магические числа» по всему коду

Если вы пишете 7 в пяти местах, вы почти гарантированно однажды поменяете 7 на 10 в четырёх местах и забудете в пятом. Это классика.

Вместо этого держим константу:

constexpr std::size_t DAYS = 7;

И используем её везде.

Индекс в int и странные сравнения

Технически int i = 0; i < DAYS; ++i часто «работает». Но когда типы смешиваются, можно попасть в неприятные неявные преобразования.

Мы уже видели, что размеры — это обычно std::size_t, так что и индекс в таких циклах логично делать std::size_t. Это не «потому что так принято», а потому что меньше шансов случайно сравнить «знаковое» с «беззнаковым» и получить сюрпризы.

7. Типичные ошибки при работе с C‑массивами

Ошибка №1: выход за границы массива из‑за <=.
Эта ошибка особенно коварна тем, что выглядит «почти правильно», а последствия могут проявляться не сразу. Один лишний шаг цикла — и вы обращаетесь к памяти, которая не является элементом массива. Итогом может быть что угодно: от «вроде работает» до падения на ровном месте.

Ошибка №2: чтение неинициализированных элементов.
Если вы объявили int a[10]; и сразу печатаете a[0], вы читаете значение, которое вы сами не задавали. Иногда это “0”, иногда нет — и вы не должны на это рассчитывать. Если нужен предсказуемый старт, используйте = {} или заполняйте массив вводом до первого чтения.

Ошибка №3: «магические числа» вместо единой константы размера.
Когда размер массива написан как 7 в одном месте, 7 в другом и ещё 7 в условии цикла, код быстро перестаёт быть управляемым. Один раз изменили размер — и забыли поправить цикл или вывод. Константа constexpr std::size_t N = ...; делает код намного надёжнее.

Ошибка №4: неверное понимание «последнего элемента».
Новички часто пишут arr[N], думая, что это последний элемент. На самом деле последний элемент — arr[N - 1], потому что индексация начинается с нуля. Эта ошибка неприятна тем, что выглядит логично «по‑человечески», но в программировании логика тут другая.

Ошибка №5: попытка сделать массив “динамическим” через int n; cin >> n; int a[n];.
В стандарте C++ размер C‑массива должен быть известен при компиляции. Некоторые компиляторы позволяют такой код как расширение, но это не переносимо и в учебной логике обычно считается ошибкой подхода. Если размер известен заранее — используйте C‑массив; если нет — нужен другой инструмент, но это уже не тема сегодняшней лекции.

1
Задача
C++ SELF, 11 уровень, 1 лекция
Недоступна
Первое и последнее
Первое и последнее
1
Задача
C++ SELF, 11 уровень, 1 лекция
Недоступна
Реверс списка
Реверс списка
1
Задача
C++ SELF, 11 уровень, 1 лекция
Недоступна
Сдвиг расписания
Сдвиг расписания
1
Задача
C++ SELF, 11 уровень, 1 лекция
Недоступна
Экстремумы недели
Экстремумы недели
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Андрей Асеев Уровень 12
4 мая 2026
Во второй задаче последнее требование (например, через a[N - 1 - i]), оно обязательное, т.е надо использовать прямой цикл обратный не проходит даже если обрабатывать нулевой индекс вне цикла (в цикле i имеет тип std::size_t, так что условие i >= 0 в обратном цикле всегда будет истинным)