Загружаем лабораторные…
// C++ · Лекция 06

Массивы и функции

Как обычный массив связан с указателями, что получает функция вместо массива, зачем нужен int (*)[N] и как передать функцию в сортировку.

Обычные массивы

Массив (array) — объект, состоящий из фиксированного числа элементов одного типа, расположенных подряд в памяти. Длина входит в тип массива.

Запись int a[5]; создаёт пять объектов int. Их индексы: 0…4. Размер должен быть известен во время компиляции. У локального массива без инициализации значения элементов не заданы Неинициализированные элементыЧтение неинициализированного int в стандартном учебном сценарии приводит к неопределённому поведению. Сам факт объявления допустим; опасно именно чтение значения без предварительной записи..

Списковая инициализация / aggregate initialization

int a[5]{2, 3}; задаёт первые два элемента, а остальные обнуляет. Размер можно вывести из списка: int a[]{2,3,4};.

array_init.cpp
#include <iostream>
#include <iterator>
int main() {
    int a[5]{2, 3};       // 2 3 0 0 0
    int b[]{4, 5, 6};     // размер = 3
    a[1] = 10;
    std::cout << std::size(a) << ' ' << a[1] << ' ' << b[0];
}
Инициализация массивов нулями и значениями.
Визуализация / инициализация и индексы

У массива есть один общий тип, но каждый его элемент — отдельный объект. Сравните пустой список инициализации и список из двух значений: оба варианта задают значения всем элементам. Вариант без инициализации оставьте для визуализации: безопасная программа сначала записывает значение и только затем читает его.

a[2] = 0
Пустые фигурные скобки обнуляют все элементы.
Выбранный элемент можно менять нажатием на ячейку. Модель не исполняет небезопасный код.

Размеры и границы

sizeof(a) возвращает суммарное количество байт массива. Количество элементов можно вычислить как sizeof(a) / sizeof(a[0]), либо использовать std::size(a) (C++17).

Для int a[5], если sizeof(int)=4
sizeof(a) = 5 × 4 = 20 байт

Адрес a[5] — позиция за концом. Читать и записывать a[5] нельзя.

Выход за границы — UB

a[5] и a[-1] недопустимы для массива int a[5], даже если программа иногда «работает». Встроенный массив и vector::operator[] не проверяют индекс. У vector::at() есть проверка с исключением.

Двумерные массивы: строки и столбцы

Начните с двух индексов: a[i][j] выбирает строку i и столбец j. В матрице 2×3 допустимы i=0,1 и j=0,1,2. Первый цикл выбирает строку, второй последовательно обрабатывает её элементы.

matrix_rows.cpp
#include <iostream>
int main() {
    int values[2][3]{{1, 2, 3}, {4, 5, 6}};
    for (int row = 0; row < 2; ++row) {
        int sum = 0;
        for (int column = 0; column < 3; ++column) sum += values[row][column];
        std::cout << row << ": " << sum << '\n';
    }
}
Два вложенных цикла: сначала строка, затем её элементы.
Результат0: 6 1: 15

Двумерный массив — массив, элементы которого сами являются массивами: int a[2][3] содержит 2 строки по 3 int в последовательной области памяти.

Главный вопрос — где стоит звёздочка относительно квадратных скобок. У int* p[5] первыми связываются [5]: это массив из пяти указателей. У int (*p)[5] скобки связывают *p раньше: это указатель на массив из пяти целых.

int** — не то же самое, что int (*)[5]. Первая форма — указатель на указатель; вторая — указатель на целую строку фиксированной длины.

matrix.cpp
#include <iostream>
int main() {
    int a[2][3]{{1, 2, 3}, {4, 5, 6}};
    int (*row)[3] = a; // указатель на строку
    int* first = a[0]; // указатель на int
    std::cout << row[1][2] << ' ' << first[2];
}
row + 1 шагает на 3 int, first + 1 — на 1 int.
Визуализация / строки и столбцы в памяти

Встроенная двумерная матрица — массив строк, каждая из которых тоже является массивом. Поэтому шаг указателя на строку отличается от шага указателя на отдельный int. Для обхода всех элементов удобно сохранить эту вложенность и использовать два цикла.

int a[2][3] · последовательная память

Ряд 0 и ряд 1 лежат последовательно, каждый содержит по три int.

a[1][2] = 6
&a[1] → int (*)[3]
a + 1 сдвигает на целую строку: три элемента int. int** не описывает такой массив.
Числовые адреса и размеры int здесь условные; типы соответствуют реальному C++.
Визуализация / чтение C++-объявлений

Сначала найдите имя, затем читайте связанные с ним конструкции. У массива указателей имя сначала связывается со скобками []; у указателя на массив скобки вокруг * меняют порядок. Эти типы описывают разную организацию данных и не заменяют друг друга.

a → [5] → int
Массив из пяти int.
Скобки меняют то, какая часть объявления связывается с именем первой.
ОбъявлениеСмысл
int a[5]
5 значений int
int* a[5]
5 указателей на int
int (*p)[5]
указатель на массив из 5 int
int** p
указатель на указатель на int
int a[2][3]
2 массива по 3 int
Почему важен тип строки?

Для int (*p)[3] выражение p + 1 перемещает указатель на следующую строку, то есть через 3 × sizeof(int) байт. Для int** шаг был бы по размерам указателя, а не целой строки.

Обработка последовательности: сумма и первый минимум

До обхода задайте контракт: 1…20 элементов, значения по модулю до 1000. Сумма начинается с нуля, минимум — с первого существующего элемента. При равенстве не меняйте выбранный индекс, если нужен первый минимум. Для пустого набора минимума нет: проверьте размер до обращения к a[0].

Задание: найдите сумму и первый максимум; затем суммы строк матрицы. Для индексов с нуля главная диагональ квадрата задаётся i=j, побочная — i+j=n−1. Центральный элемент нечётной матрицы учитывается один раз. Поворот и сдвиг слоя требуют отдельного алгоритма из подготовки к лабораторной.

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

Array-to-pointer conversion — неявное преобразование выражения типа «массив из N элементов T» в указатель T* на его начальный элемент в соответствующем контексте.

Когда пишем a + 2, массив a преобразуется в указатель на a[0]. Поэтому *(a + 2) обозначает a[2].

Указатель можно поставить на середину массива: int* p = a + 3;. Тогда p[-1] означает a[2] и корректен, поскольку адрес остаётся внутри того же массива Отрицательные индексыОтрицательный индекс у указателя не обязательно ошибка. Например, p = a + 3, p[-2] — это a[1]. Но a[-1] выходит за границу и является UB..

pointer_array.cpp
#include <iostream>
int main() {
    int a[5]{10, 20, 30, 40, 50};
    int* p = a + 3;
    std::cout << a[2] << ' ' << *(a + 2) << '\n';
    std::cout << p[-1] << ' ' << p[0] << '\n';
}
p[-1] корректен: это a[2], элемент внутри a.
Визуализация / сдвиг указателя и индексация

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

Начальный адрес p = a + 2
10a[0]
20a[1]
30a[2]
40a[3]
50a[4]
p[-1] ≡ *(p - 1)
Смещение относительно p
p[-1] = a[1] = 20
Хотя смещение может быть отрицательным, адрес остался внутри a.
Только p[i] с целевым элементом внутри исходного массива безопасно разыменовывать.
Что сравниваемМассив int a[5]Указатель int* p
Тип
int[5]
int*
Индекс [i]
Есть
Есть
sizeof
Размер всего массива
Размер самого указателя
Присвоить другой адрес
Нельзя: a = p
Можно: p = a
Сдвиг ++
++a нельзя
++p можно в пределах массива
Массив не равен указателю

Это разные типы. Преобразование происходит во многих выражениях, но не везде: например, sizeof(a) измеряет сам массив, а &a имеет тип «указатель на массив» (здесь int (*)[5]).

Массив при передаче в функцию

Подстройка типа параметра (array parameter adjustment) — правило объявления функций: параметр вида int a[5] интерпретируется как int* a. Указанная цифра не задаёт реального ограничения длины.

Функция не узнаёт число элементов из одного только параметра int a[]. Обычно передают длину отдельно или используют контейнер/ссылку на массив Почему нельзя перегрузить по размеру?void f(int a[3]); и void f(int a[5]); объявляют одну и ту же сигнатуру void f(int*). Перегрузка только по размерам этих параметров невозможна..

array_arguments.cpp
#include <cstddef>
#include <iostream>
void print(const int a[], std::size_t n) {
    // a имеет тип const int*
    for (std::size_t i = 0; i < n; ++i)
        std::cout << a[i] << ' ';
}
int main() {
    int a[5]{1, 2, 3, 4, 5};
    print(a, 5);
}
Размер передаём отдельно: массив внутри print превратился в указатель.
Визуализация / что на самом деле получает функция

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

int a[5]аргумент в main
f(a)выражение вызова
int*тип получаемого параметра
Вызов получает адрес a[0]. Количество элементов само по себе не передаётся.
int a[5] в заголовке функции трактуется как int* независимо от указанного числа 5.
Два объявления — одна сигнатура
void f(int a[5])исходная запись
void f(int* a)тип параметра после корректировки

Реальное число элементов массиву функции не передаётся автоматически.

Не используйте sizeof(a) для длины в такой функции

Внутри void f(int a[5]) параметр a уже имеет тип int*; sizeof(a) измерит указатель. Компилятор может предупредить об этом.

Интерфейс диапазона после указателей и ссылок

Функции обработки последовательности нужны не только элементы, но и границы допустимого обхода. Встроенный параметр вида int a[5] не передаёт функции массив длины пять: тип параметра корректируется к указателю. Поэтому одна цифра в скобках не защищает от неправильного размера. Можно передать указатель и длину отдельно, но они должны соответствовать одному действительному диапазону. Span объединяет доступ к непрерывным элементам и их количество в одном небольшом представлении. Он не владеет элементами и не копирует всю последовательность. В примере span<const int> разрешает чтение, а сумма возвращается отдельным числом. Один интерфейс принимает array и vector. Владелец исходных элементов должен оставаться живым всё время использования span. Наличие размера само по себе не делает любой индекс безопасным: в C++20 operator[] у span не обещает проверку границ. Наш обход берёт только существующие элементы, а пустой диапазон даёт сумму ноль.

code-guided-range.cpp
#include <array>
#include <iostream>
#include <span>
#include <vector>

long long sum(std::span<const int> values) {
    long long result = 0;
    for (int value : values) {
        result += value;
    }
    return result;
}

int main() {
    const std::array<int, 3> fixed{1, 2, 3};
    const std::vector<int> dynamic{4, 5};
    std::cout << sum(fixed) << ' ' << sum(dynamic) << '\n';
}
Span передаёт доступ и длину, но не владеет данными и не обещает проверку operator[].
Результат6 9

Динамические массивы и VLA

new[] возвращает указатель

new int[n]{} создаёт n элементов в динамической памяти, обнуляет их и возвращает int*. Освобождение — строго delete[] p;.

Парное выделение и освобождение / new[] / delete[]

new T[n] должно соответствовать delete[], а new T — delete. Несовпадение — UB.

new_array.cpp
#include <cstddef>
int main() {
    std::size_t n = 100;
    int* a = new int[n]{}; // 100 нулей
    a[2] = 19;
    delete[] a;
    a = nullptr;
}
Запрос может зависеть от n во время выполнения; освобождение — delete[].
Визуализация / выделение, обнуление и освобождение

Круглые и квадратные скобки в new описывают разные объекты: один int со значением или массив элементов. От этого зависит и форма освобождения памяти. Сравните ручное управление с vector: контейнер сам освобождает своё хранилище при выходе из области видимости.

0p[0]
0p[1]
0p[2]
0p[3]
0p[4]
delete[] p;
Созданные объекты можно использовать, затем необходимо освободить верной формой delete.
new T ↔ delete p; new T[n] ↔ delete[] p. Путать пары нельзя.

Почему int a[n] — не стандартный C++?

Встроенный массив int a[N] требует константного выражения размера. Запись int n; std::cin >> n; int a[n]; — VLA: некоторые компиляторы принимают её как расширение, но стандартный C++ запрещает.

Массив переменной длины / VLA

Массив, размер которого узнаётся только во время выполнения. Для переносимого C++ используем std::vector<int> a(n);.

portable_array.cpp
#include <iostream>
#include <vector>
constexpr int MAX_SIZE = 1000;
int main() {
    int n = 0;
    if (!(std::cin >> n) || n < 0 || n > MAX_SIZE) {
        std::cerr << "Invalid size\n";
        return 1;
    }
    std::vector<int> values(n);
    std::cout << values.size() << '\n';
    // int bad[n]; // VLA не входит в стандарт C++20.
}
До создания vector проверяются чтение и диапазон 0…1000. Пустой массив допустим; неверный ввод отклоняется.
Не трогайте служебную память аллокатора

Примеры чтения a[-1] и ручного delete[] v.data() из лекции демонстрируют опасные последствия, но оба действия недопустимы. Писать их в настоящей программе нельзя.

Указатели на функции и сортировка

Указатель на функцию (function pointer) — значение, позволяющее обратиться к функции определённой сигнатуры через переменную. Например, bool (*cmp)(int,int).

Чтобы передать алгоритму правило сравнения, определяем функцию bool cmp(int a, int b) и передаём её в std::sort. Выражения cmp и &cmp в этом контексте дают указатель на функцию.

Компаратор / comparator

Функция-предикат, отвечающая, должен ли первый элемент идти раньше второго.

Для std::sort(first,last,cmp) диапазон полуоткрытый: [first,last). Компаратор должен задавать строгий слабый порядок Строгий слабый порядокТребуются, в частности, иррефлексивность (cmp(x,x)==false), асимметрия и транзитивность отношения, а также транзитивность эквивалентности. Сравнение a <= b не подходит: cmp(x,x) истинно..

function_pointer_sort.cpp
#include <algorithm>
#include <iostream>
bool descending(int a, int b) {
    return a > b;
}
int main() {
    int a[]{8, 2, 9, 1, 5};
    bool (*cmp)(int, int) = &descending;
    std::sort(a, a + 5, cmp);
    for (int x : a) std::cout << x << ' ';
}
Параметр cmp — указатель на функцию сравнения.
Визуализация / передаём компаратор в алгоритм

Алгоритм получает правило сравнения отдельно от массива. Замена функции-компаратора меняет порядок, сохраняя сам вызов сортировки. Важно договориться, что именно означает «раньше»: сравнение должно быть строгим, поэтому один и тот же элемент не должен считаться предшествующим самому себе.

До сортировки
-7
2
-3
8
0
После сортировки
8
2
0
-3
-7
cmp(a,b) = a > b
sort получает правило как вызываемый объект или указатель на функцию. Здесь моделируется результат, сам алгоритм C++ не запускается.
Компаратор должен задавать строгий слабый порядок: cmp(x,x) всегда false.

Тип указателя

Объявление читается от имени: (*p) — указатель; затем (int,int) — функция с двумя параметрами; слева bool — возвращаемый тип. Типы аргументов и результата входят в тип указателя.

Чтение от имени переменной
(*p)указатель
(int,int)два аргумента
boolрезультат
bool (*p)(int, int);
overloaded_address.cpp
#include <iostream>
void f(int x) { std::cout << x; }
void f(double x) { std::cout << x; }
int main() {
    void (*p)(int) = &f;    // выбирает f(int)
    void (*q)(double) = &f; // выбирает f(double)
    p(3);
    q(2.5);
}
Контекст типа указателя разрешает перегрузку.
Адрес перегруженной функции

Если есть f(int) и f(double), контекст требуемого типа указателя может выбрать нужную перегрузку. Без такого контекста выражение &f неоднозначно. Если определение выбранной функции отсутствует, обычно возникает ошибка компоновки.

Параметры функций

Аргументы по умолчанию

Аргумент по умолчанию (default argument) — выражение, подставляемое при вызове, когда соответствующий аргумент не указан.

Обычно значения по умолчанию образуют концевой непрерывный суффикс параметров. Нельзя оставить обязательный параметр после необязательного.

default_args.cpp
#include <iostream>
int sum(int x = 1, int y = 2, int z = 3) {
    return x + y + z;
}
int main() {
    std::cout << sum() << ' ';
    std::cout << sum(4) << ' ';
    std::cout << sum(4, 8, 10);
}
Результат: 6 9 22.
Визуализация / сколько аргументов передано

Отсутствующие аргументы подставляются справа из объявления функции. Переданное значение заменяет соответствующее значение по умолчанию; пропустить первый аргумент и передать только второй таким способом нельзя. Значение по умолчанию должно быть известно в месте вызова.

x = 1по умолчанию
y = 2по умолчанию
z = 3по умолчанию
f() → 1 + 2 + 3 = 6
Смысл модели: int f(int x=1, int y=2, int z=3).
У std::sort здесь не аргумент по умолчанию

В лекции приведена удобная аналогия, но технически std::sort(first,last) и std::sort(first,last,comp) — разные перегрузки, а не просто единственная функция с третьим параметром по умолчанию.

C-подобное многоточие

Объявление void log(const char* format, ...); разрешает дополнительные аргументы разных типов. Это исторический механизм printf; ручная обработка требует <cstdarg>.

variadic.cpp
#include <cstdio>
int main() {
    std::printf("%d %.1f\n", 5, 2.5);
    // printf принимает формат и аргументы ...
}
В C-вариадических функциях типы контролируются слабее.

Строки из символов (C-строки)

Нуль-терминированная строка (null-terminated string) — последовательность символов char, конец которой обозначается первым символом \0.

Литерал "Hello" в C++ имеет тип массива const char[6]: пять букв и завершающий \0. В записи const char* s = "Hello"; массив преобразуется в указатель. Изменять сам литерал нельзя.

char text[] = "Hello"; создаёт собственный изменяемый массив. Для std::cout << s перегрузка вывода для C-строк печатает символы до первого нуля, а не адрес Почему выводится строка, а не адрес?У operator<< для const char* и char* есть специальная перегрузка: она читает символы до первого \0. Для обычных указателей на объекты вывод обычно показывает представление адреса..

c_strings.cpp
#include <cstring>
#include <iostream>
int main() {
    char s[]{'A', 'B', '\0', 'C', 'D', '\0'};
    const char* literal = "Hello";
    std::cout << s << ' ' << std::strlen(s) << '\n';
    std::cout << literal << '\n';
    // s[3] существует, но за первым \0
}
std::cout выведет AB 2 и Hello.
Визуализация / нулевой терминатор и вывод

Размер массива символов и длина C-строки — разные величины. Нулевой символ завершает строку для strlen и вывода через const char*, хотя за ним могут оставаться другие элементы массива. Изменяемая копия литерала и сам строковый литерал также имеют разные свойства.

Первый нулевой символ: позиция 4
A[0]
B[1]
C[2]
D[3]
\0[4]
F[5]
\0[6]
std::cout << s; → "ABCD"
std::strlen(s) → 4
После первого \0 в памяти могут сохраняться символы, но C-строка логически заканчивается на нём.
Модель использует массив с гарантированным терминатором, чтобы не выполнять UB.
Полная таблица ASCII (коды символов): Полная таблица ASCII

ASCII: символы 0–127

Десятичный код, шестнадцатеричный код и символ. Управляющие коды не отображаются как обычные буквы. \0 обозначает NUL с кодом 0.

Показано 128 из 128
DecHexСимволНазвание
00x00NULNUL
10x01SOHSOH
20x02STXSTX
30x03ETXETX
40x04EOTEOT
50x05ENQENQ
60x06ACKACK
70x07BELBEL
80x08BSBS
90x09TABTAB
100x0ALFLF
110x0BVTVT
120x0CFFFF
130x0DCRCR
140x0ESOSO
150x0FSISI
160x10DLEDLE
170x11DC1DC1
180x12DC2DC2
190x13DC3DC3
200x14DC4DC4
210x15NAKNAK
220x16SYNSYN
230x17ETBETB
240x18CANCAN
250x19EMEM
260x1ASUBSUB
270x1BESCESC
280x1CFSFS
290x1DGSGS
300x1ERSRS
310x1FUSUS
320x20SPACESPACE
330x21!!
340x22""
350x23##
360x24$$
370x25%%
380x26&&
390x27''
400x28((
410x29))
420x2A**
430x2B++
440x2C,,
450x2D--
460x2E..
470x2F//
480x3000
490x3111
500x3222
510x3333
520x3444
530x3555
540x3666
550x3777
560x3888
570x3999
580x3A::
590x3B;;
600x3C<<
610x3D==
620x3E>>
630x3F??
640x40@@
650x41AA
660x42BB
670x43CC
680x44DD
690x45EE
700x46FF
710x47GG
720x48HH
730x49II
740x4AJJ
750x4BKK
760x4CLL
770x4DMM
780x4ENN
790x4FOO
800x50PP
810x51QQ
820x52RR
830x53SS
840x54TT
850x55UU
860x56VV
870x57WW
880x58XX
890x59YY
900x5AZZ
910x5B[[
920x5C\\
930x5D]]
940x5E^^
950x5F__
960x60``
970x61aa
980x62bb
990x63cc
1000x64dd
1010x65ee
1020x66ff
1030x67gg
1040x68hh
1050x69ii
1060x6Ajj
1070x6Bkk
1080x6Cll
1090x6Dmm
1100x6Enn
1110x6Foo
1120x70pp
1130x71qq
1140x72rr
1150x73ss
1160x74tt
1170x75uu
1180x76vv
1190x77ww
1200x78xx
1210x79yy
1220x7Azz
1230x7B{{
1240x7C||
1250x7D}}
1260x7E~~
1270x7FDELDEL
\0код 0 · NUL
код 10 · LF
'A'код 65 · A
Если терминатора нет — опасное чтение

Функции вроде std::strlen и печать C-строки предполагают наличие \0. Если за границами массива продолжается поиск терминатора, возникает UB. Символ после первого нуля физически остаётся в массиве, но для C-строк он уже за логическим концом.

+Дополнительно⌄

Почему корректно 2[a]?

Для встроенной индексации a[i] определено через *(a + i). Сложение целого и указателя коммутативно в этой форме, поэтому i[a] синтаксически корректно и даёт тот же элемент. Это интересный факт о C/C++, но такой стиль ухудшает читаемость.

unusual_index.cpp
#include <iostream>
int main() {
    int a[]{10, 20, 30};
    std::cout << a[2] << ' ' << 2[a];
}
Оба выражения обозначают одно значение — 30.

Как сохранить размер массива при передаче?

В современном C++ можно передать параметр по ссылке на массив: template<std::size_t N> void f(int (&a)[N]);. Для обычного кода удобнее использовать std::span (невладеющее представление непрерывной последовательности, C++20), std::array (фиксированный размер) или std::vector (динамический размер).

modern_arrays.cpp
#include <array>
#include <span>
#include <vector>
void print_first(std::span<const int> a) {
    if (!a.empty()) (void)a[0];
}
int main() {
    std::array<int, 3> fixed{1, 2, 3};
    std::vector<int> dynamic(100);
    print_first(fixed);
    print_first(dynamic);
}
std::span сохраняет длину в представлении; не владеет памятью.

Почему нет гарантии про служебные байты перед массивом?

Реальные аллокаторы могут хранить метаданные в разных местах; конкретное расположение является внутренней деталью реализации. Обращение к p[-1] при p, полученном от new[], не становится допустимым из-за того, что там иногда видны «интересные числа».

Современные альтернативы для функции-обратного вызова

Кроме обычных указателей на функции, компаратором могут быть функциональный объект или лямбда. Они поддерживают передачу дополнительного состояния, а компилятор зачастую может инлайнить вызовы.

lambda_sort.cpp
#include <algorithm>
#include <vector>
int main() {
    std::vector<int> a{8, 2, 9, 1, 5};
    std::sort(a.begin(), a.end(),
        [](int x, int y) { return x > y; });
}
Лямбда — современная альтернатива отдельно объявленному cmp.

Почему не стоит использовать ... в прикладном C++

В C-подобных вариадических функциях типы дополнительных аргументов не проверяются по сигнатуре, а при использовании va_arg требуется знать фактически переданный тип с учётом стандартных продвижений. Современная альтернатива — вариадические шаблоны и безопасные обёртки.

Справочник терминов

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

array

Массив

Объект фиксированного размера из элементов одного типа, размещённых подряд.

Примеры кода

Первый минимум непустого массива

array-cases.cpp
#include <iostream>
int main() {
    constexpr int CAPACITY = 20;
    int values[CAPACITY]{};
    int n = 0;
    if (!(std::cin >> n) || n < 0 || n > CAPACITY) {
        std::cerr << "Invalid size\n";
        return 1;
    }
    for (int i = 0; i < n; ++i) {
        if (!(std::cin >> values[i]) || values[i] < -1000 || values[i] > 1000) {
            std::cerr << "Invalid value\n";
            return 1;
        }
    }
    if (n == 0) {
        std::cout << "No minimum\n";
        return 0;
    }
    int first = 0;
    for (int i = 1; i < n; ++i) {
        if (values[i] < values[first]) first = i;
    }
    std::cout << first << ' ' << values[first] << '\n';
}

Два вложенных цикла: сначала строка, затем её элементы.

matrix_rows.cpp
#include <iostream>
int main() {
    int values[2][3]{{1, 2, 3}, {4, 5, 6}};
    for (int row = 0; row < 2; ++row) {
        int sum = 0;
        for (int column = 0; column < 3; ++column) sum += values[row][column];
        std::cout << row << ": " << sum << '\n';
    }
}

Обсудить лекцию с ИИ

Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.

Посмотреть промпт