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 | Первый индекс | Последний индекс |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Давайте покажем это в коде (и заодно потренируемся не писать «последний элемент = 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‑массив; если нет — нужен другой инструмент, но это уже не тема сегодняшней лекции.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ