Массивы и функции
Как обычный массив связан с указателями, что получает функция вместо массива, зачем нужен int (*)[N] и как передать функцию в сортировку.
Обычные массивы
Массив (array) — объект, состоящий из фиксированного числа элементов одного типа, расположенных подряд в памяти. Длина входит в тип массива.
Запись int a[5]; создаёт пять объектов int. Их индексы: 0…4. Размер должен быть известен во время компиляции. У локального массива без инициализации значения элементов не заданы Неинициализированные элементыЧтение неинициализированного int в стандартном учебном сценарии приводит к неопределённому поведению. Сам факт объявления допустим; опасно именно чтение значения без предварительной записи..
int a[5]{2, 3}; задаёт первые два элемента, а остальные обнуляет. Размер можно вывести из списка: int a[]{2,3,4};.
#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];
}У массива есть один общий тип, но каждый его элемент — отдельный объект. Сравните пустой список инициализации и список из двух значений: оба варианта задают значения всем элементам. Вариант без инициализации оставьте для визуализации: безопасная программа сначала записывает значение и только затем читает его.
#include <iostream>
#include <iterator>
int main() {
int values[4]{};
std::cout << std::size(values) << '\n';
for (int value : values) std::cout << value << ' ';
std::cout << '\n';
}#include <iostream>
int main() {
int values[5]{2, 3};
values[1] = 10;
for (int value : values) std::cout << value << ' ';
std::cout << '\n';
}Размеры и границы
sizeof(a) возвращает суммарное количество байт массива. Количество элементов можно вычислить как sizeof(a) / sizeof(a[0]), либо использовать std::size(a) (C++17).
Адрес a[5] — позиция за концом. Читать и записывать a[5] нельзя.
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. Первый цикл выбирает строку, второй последовательно обрабатывает её элементы.
#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';
}
}Двумерный массив — массив, элементы которого сами являются массивами: int a[2][3] содержит 2 строки по 3 int в последовательной области памяти.
Главный вопрос — где стоит звёздочка относительно квадратных скобок. У int* p[5] первыми связываются [5]: это массив из пяти указателей. У int (*p)[5] скобки связывают *p раньше: это указатель на массив из пяти целых.
int** — не то же самое, что int (*)[5]. Первая форма — указатель на указатель; вторая — указатель на целую строку фиксированной длины.
#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];
}Встроенная двумерная матрица — массив строк, каждая из которых тоже является массивом. Поэтому шаг указателя на строку отличается от шага указателя на отдельный int. Для обхода всех элементов удобно сохранить эту вложенность и использовать два цикла.
Ряд 0 и ряд 1 лежат последовательно, каждый содержит по три int.
#include <iostream>
int main() {
int values[2][3]{{1, 2, 3}, {4, 5, 6}};
int (*row)[3] = values;
++row;
std::cout << (*row)[0] << ' ' << row[0][2] << '\n';
}#include <iostream>
int main() {
int values[2][3]{{1, 2, 3}, {4, 5, 6}};
int total = 0;
for (const auto& row : values) {
for (int value : row) total += value;
}
std::cout << total << '\n';
}Сначала найдите имя, затем читайте связанные с ним конструкции. У массива указателей имя сначала связывается со скобками []; у указателя на массив скобки вокруг * меняют порядок. Эти типы описывают разную организацию данных и не заменяют друг друга.
#include <iostream>
int main() {
int values[3]{4, 5, 6};
int* pointers[3]{&values[0], &values[1], &values[2]};
int (*whole)[3] = &values;
std::cout << *pointers[1] << ' ' << (*whole)[1] << '\n';
}#include <iostream>
#include <type_traits>
using Row = int[3];
int main() {
Row matrix[2]{{1, 2, 3}, {4, 5, 6}};
Row* row = matrix;
static_assert(std::is_same_v<decltype(row), int (*)[3]>);
std::cout << row[1][2] << '\n';
}| Объявление | Смысл |
|---|---|
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..
#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';
}Индекс указателя отсчитывается от его текущего положения. Поэтому отрицательный индекс иногда корректен: важен итоговый элемент внутри исходного массива. Отдельно различайте адрес сразу за массивом и адрес за допустимой границей: первый можно использовать как конец диапазона, но разыменовывать его тоже нельзя.
#include <iostream>
int main() {
int values[]{10, 20, 30, 40, 50};
int* current = values + 3;
std::cout << current[-1] << ' ' << current[0] << '\n';
std::cout << (current - values) << '\n';
}#include <iostream>
int main() {
int values[]{4, 5, 6};
const int* end = values + 3;
for (const int* current = values; current != end; ++current) {
std::cout << *current << ' ';
}
std::cout << '\n';
}| Что сравниваем | Массив 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*). Перегрузка только по размерам этих параметров невозможна..
#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);
}Объявление параметра с квадратными скобками выглядит как массив, но такой параметр корректируется до указателя. Число внутри скобок не передаёт длину вызываемой функции. Если длина нужна, передайте её явно или используйте ссылку на массив: во втором варианте размер остаётся частью типа.
#include <iostream>
#include <type_traits>
void inspect(int values[7]) {
static_assert(std::is_same_v<decltype(values), int*>);
std::cout << values[1] << '\n';
}
int main() {
int values[]{10, 20, 30};
inspect(values);
}#include <cstddef>
#include <iostream>
template<std::size_t N>
void inspect(const int (&values)[N]) {
std::cout << N << ' ' << values[N - 1] << '\n';
}
int main() {
int values[]{10, 20, 30};
inspect(values);
}Реальное число элементов массиву функции не передаётся автоматически.
Внутри void f(int a[5]) параметр a уже имеет тип int*; sizeof(a) измерит указатель. Компилятор может предупредить об этом.
Интерфейс диапазона после указателей и ссылок
Функции обработки последовательности нужны не только элементы, но и границы допустимого обхода. Встроенный параметр вида int a[5] не передаёт функции массив длины пять: тип параметра корректируется к указателю. Поэтому одна цифра в скобках не защищает от неправильного размера. Можно передать указатель и длину отдельно, но они должны соответствовать одному действительному диапазону. Span объединяет доступ к непрерывным элементам и их количество в одном небольшом представлении. Он не владеет элементами и не копирует всю последовательность. В примере span<const int> разрешает чтение, а сумма возвращается отдельным числом. Один интерфейс принимает array и vector. Владелец исходных элементов должен оставаться живым всё время использования span. Наличие размера само по себе не делает любой индекс безопасным: в C++20 operator[] у span не обещает проверку границ. Наш обход берёт только существующие элементы, а пустой диапазон даёт сумму ноль.
#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';
}Динамические массивы и VLA
new[] возвращает указатель
new int[n]{} создаёт n элементов в динамической памяти, обнуляет их и возвращает int*. Освобождение — строго delete[] p;.
new T[n] должно соответствовать delete[], а new T — delete. Несовпадение — UB.
#include <cstddef>
int main() {
std::size_t n = 100;
int* a = new int[n]{}; // 100 нулей
a[2] = 19;
delete[] a;
a = nullptr;
}Круглые и квадратные скобки в new описывают разные объекты: один int со значением или массив элементов. От этого зависит и форма освобождения памяти. Сравните ручное управление с vector: контейнер сам освобождает своё хранилище при выходе из области видимости.
#include <iostream>
int main() {
int* single = new int(5);
int* values = new int[3]{};
values[1] = *single;
std::cout << values[0] << ' ' << values[1] << '\n';
delete single;
delete[] values;
}#include <iostream>
#include <vector>
int main() {
int count = 4;
std::vector<int> values(count);
values.back() = 9;
std::cout << values.size() << ' ' << values.front()
<< ' ' << values.back() << '\n';
}Почему int a[n] — не стандартный C++?
Встроенный массив int a[N] требует константного выражения размера. Запись int n; std::cin >> n; int a[n]; — VLA: некоторые компиляторы принимают её как расширение, но стандартный C++ запрещает.
Массив, размер которого узнаётся только во время выполнения. Для переносимого C++ используем std::vector<int> a(n);.
#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.
}Примеры чтения a[-1] и ручного delete[] v.data() из лекции демонстрируют опасные последствия, но оба действия недопустимы. Писать их в настоящей программе нельзя.
Указатели на функции и сортировка
Указатель на функцию (function pointer) — значение, позволяющее обратиться к функции определённой сигнатуры через переменную. Например, bool (*cmp)(int,int).
Чтобы передать алгоритму правило сравнения, определяем функцию bool cmp(int a, int b) и передаём её в std::sort. Выражения cmp и &cmp в этом контексте дают указатель на функцию.
Функция-предикат, отвечающая, должен ли первый элемент идти раньше второго.
Для std::sort(first,last,cmp) диапазон полуоткрытый: [first,last). Компаратор должен задавать строгий слабый порядок Строгий слабый порядокТребуются, в частности, иррефлексивность (cmp(x,x)==false), асимметрия и транзитивность отношения, а также транзитивность эквивалентности. Сравнение a <= b не подходит: cmp(x,x) истинно..
#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 << ' ';
}Алгоритм получает правило сравнения отдельно от массива. Замена функции-компаратора меняет порядок, сохраняя сам вызов сортировки. Важно договориться, что именно означает «раньше»: сравнение должно быть строгим, поэтому один и тот же элемент не должен считаться предшествующим самому себе.
#include <algorithm>
#include <iostream>
bool descending(int left, int right) { return left > right; }
int main() {
int values[]{8, 2, 9, 1, 5};
bool (*compare)(int, int) = descending;
std::sort(values, values + 5, compare);
for (int value : values) std::cout << value << ' ';
std::cout << '\n';
}#include <algorithm>
#include <cstdlib>
#include <iostream>
bool by_magnitude(int left, int right) {
if (std::abs(left) != std::abs(right))
return std::abs(left) < std::abs(right);
return left < right;
}
int main() {
int values[]{2, -1, -2, 1};
std::sort(values, values + 4, by_magnitude);
for (int value : values) std::cout << value << ' ';
std::cout << '\n';
}Тип указателя
Объявление читается от имени: (*p) — указатель; затем (int,int) — функция с двумя параметрами; слева bool — возвращаемый тип. Типы аргументов и результата входят в тип указателя.
#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) — выражение, подставляемое при вызове, когда соответствующий аргумент не указан.
Обычно значения по умолчанию образуют концевой непрерывный суффикс параметров. Нельзя оставить обязательный параметр после необязательного.
#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);
}Отсутствующие аргументы подставляются справа из объявления функции. Переданное значение заменяет соответствующее значение по умолчанию; пропустить первый аргумент и передать только второй таким способом нельзя. Значение по умолчанию должно быть известно в месте вызова.
#include <iostream>
int sum(int x = 1, int y = 2, int z = 3) { return x + y + z; }
int main() {
std::cout << sum() << ' ' << sum(4) << ' '
<< sum(4, 8, 10) << '\n';
}#include <iostream>
int sum(int x, int y = 10);
int sum(int x, int y) { return x + y; }
int main() {
std::cout << sum(3) << ' ' << sum(3, 4) << '\n';
}В лекции приведена удобная аналогия, но технически std::sort(first,last) и std::sort(first,last,comp) — разные перегрузки, а не просто единственная функция с третьим параметром по умолчанию.
C-подобное многоточие
Объявление void log(const char* format, ...); разрешает дополнительные аргументы разных типов. Это исторический механизм printf; ручная обработка требует <cstdarg>.
#include <cstdio>
int main() {
std::printf("%d %.1f\n", 5, 2.5);
// printf принимает формат и аргументы ...
}Строки из символов (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. Для обычных указателей на объекты вывод обычно показывает представление адреса..
#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
}Размер массива символов и длина C-строки — разные величины. Нулевой символ завершает строку для strlen и вывода через const char*, хотя за ним могут оставаться другие элементы массива. Изменяемая копия литерала и сам строковый литерал также имеют разные свойства.
#include <cstring>
#include <iostream>
int main() {
char text[]{'A', 'B', '\0', 'C', 'D', '\0'};
std::cout << text << '\n';
std::cout << std::strlen(text) << ' ' << sizeof(text) << '\n';
}#include <iostream>
#include <string>
int main() {
char word[] = "cat";
word[0] = 'b';
std::string text{"A\0B", 3};
std::cout << word << '\n';
std::cout << text.size() << ' ' << text[2] << '\n';
}ASCII: символы 0–127
Десятичный код, шестнадцатеричный код и символ. Управляющие коды не отображаются как обычные буквы. \0 обозначает NUL с кодом 0.
| Dec | Hex | Символ | Название |
|---|---|---|---|
| 0 | 0x00 | NUL | NUL |
| 1 | 0x01 | SOH | SOH |
| 2 | 0x02 | STX | STX |
| 3 | 0x03 | ETX | ETX |
| 4 | 0x04 | EOT | EOT |
| 5 | 0x05 | ENQ | ENQ |
| 6 | 0x06 | ACK | ACK |
| 7 | 0x07 | BEL | BEL |
| 8 | 0x08 | BS | BS |
| 9 | 0x09 | TAB | TAB |
| 10 | 0x0A | LF | LF |
| 11 | 0x0B | VT | VT |
| 12 | 0x0C | FF | FF |
| 13 | 0x0D | CR | CR |
| 14 | 0x0E | SO | SO |
| 15 | 0x0F | SI | SI |
| 16 | 0x10 | DLE | DLE |
| 17 | 0x11 | DC1 | DC1 |
| 18 | 0x12 | DC2 | DC2 |
| 19 | 0x13 | DC3 | DC3 |
| 20 | 0x14 | DC4 | DC4 |
| 21 | 0x15 | NAK | NAK |
| 22 | 0x16 | SYN | SYN |
| 23 | 0x17 | ETB | ETB |
| 24 | 0x18 | CAN | CAN |
| 25 | 0x19 | EM | EM |
| 26 | 0x1A | SUB | SUB |
| 27 | 0x1B | ESC | ESC |
| 28 | 0x1C | FS | FS |
| 29 | 0x1D | GS | GS |
| 30 | 0x1E | RS | RS |
| 31 | 0x1F | US | US |
| 32 | 0x20 | SPACE | SPACE |
| 33 | 0x21 | ! | ! |
| 34 | 0x22 | " | " |
| 35 | 0x23 | # | # |
| 36 | 0x24 | $ | $ |
| 37 | 0x25 | % | % |
| 38 | 0x26 | & | & |
| 39 | 0x27 | ' | ' |
| 40 | 0x28 | ( | ( |
| 41 | 0x29 | ) | ) |
| 42 | 0x2A | * | * |
| 43 | 0x2B | + | + |
| 44 | 0x2C | , | , |
| 45 | 0x2D | - | - |
| 46 | 0x2E | . | . |
| 47 | 0x2F | / | / |
| 48 | 0x30 | 0 | 0 |
| 49 | 0x31 | 1 | 1 |
| 50 | 0x32 | 2 | 2 |
| 51 | 0x33 | 3 | 3 |
| 52 | 0x34 | 4 | 4 |
| 53 | 0x35 | 5 | 5 |
| 54 | 0x36 | 6 | 6 |
| 55 | 0x37 | 7 | 7 |
| 56 | 0x38 | 8 | 8 |
| 57 | 0x39 | 9 | 9 |
| 58 | 0x3A | : | : |
| 59 | 0x3B | ; | ; |
| 60 | 0x3C | < | < |
| 61 | 0x3D | = | = |
| 62 | 0x3E | > | > |
| 63 | 0x3F | ? | ? |
| 64 | 0x40 | @ | @ |
| 65 | 0x41 | A | A |
| 66 | 0x42 | B | B |
| 67 | 0x43 | C | C |
| 68 | 0x44 | D | D |
| 69 | 0x45 | E | E |
| 70 | 0x46 | F | F |
| 71 | 0x47 | G | G |
| 72 | 0x48 | H | H |
| 73 | 0x49 | I | I |
| 74 | 0x4A | J | J |
| 75 | 0x4B | K | K |
| 76 | 0x4C | L | L |
| 77 | 0x4D | M | M |
| 78 | 0x4E | N | N |
| 79 | 0x4F | O | O |
| 80 | 0x50 | P | P |
| 81 | 0x51 | Q | Q |
| 82 | 0x52 | R | R |
| 83 | 0x53 | S | S |
| 84 | 0x54 | T | T |
| 85 | 0x55 | U | U |
| 86 | 0x56 | V | V |
| 87 | 0x57 | W | W |
| 88 | 0x58 | X | X |
| 89 | 0x59 | Y | Y |
| 90 | 0x5A | Z | Z |
| 91 | 0x5B | [ | [ |
| 92 | 0x5C | \ | \ |
| 93 | 0x5D | ] | ] |
| 94 | 0x5E | ^ | ^ |
| 95 | 0x5F | _ | _ |
| 96 | 0x60 | ` | ` |
| 97 | 0x61 | a | a |
| 98 | 0x62 | b | b |
| 99 | 0x63 | c | c |
| 100 | 0x64 | d | d |
| 101 | 0x65 | e | e |
| 102 | 0x66 | f | f |
| 103 | 0x67 | g | g |
| 104 | 0x68 | h | h |
| 105 | 0x69 | i | i |
| 106 | 0x6A | j | j |
| 107 | 0x6B | k | k |
| 108 | 0x6C | l | l |
| 109 | 0x6D | m | m |
| 110 | 0x6E | n | n |
| 111 | 0x6F | o | o |
| 112 | 0x70 | p | p |
| 113 | 0x71 | q | q |
| 114 | 0x72 | r | r |
| 115 | 0x73 | s | s |
| 116 | 0x74 | t | t |
| 117 | 0x75 | u | u |
| 118 | 0x76 | v | v |
| 119 | 0x77 | w | w |
| 120 | 0x78 | x | x |
| 121 | 0x79 | y | y |
| 122 | 0x7A | z | z |
| 123 | 0x7B | { | { |
| 124 | 0x7C | | | | |
| 125 | 0x7D | } | } |
| 126 | 0x7E | ~ | ~ |
| 127 | 0x7F | DEL | DEL |
Функции вроде std::strlen и печать C-строки предполагают наличие \0. Если за границами массива продолжается поиск терминатора, возникает UB. Символ после первого нуля физически остаётся в массиве, но для C-строк он уже за логическим концом.
Дополнительно⌄
Почему корректно 2[a]?
Для встроенной индексации a[i] определено через *(a + i). Сложение целого и указателя коммутативно в этой форме, поэтому i[a] синтаксически корректно и даёт тот же элемент. Это интересный факт о C/C++, но такой стиль ухудшает читаемость.
#include <iostream>
int main() {
int a[]{10, 20, 30};
std::cout << a[2] << ' ' << 2[a];
}Как сохранить размер массива при передаче?
В современном C++ можно передать параметр по ссылке на массив: template<std::size_t N> void f(int (&a)[N]);. Для обычного кода удобнее использовать std::span (невладеющее представление непрерывной последовательности, C++20), std::array (фиксированный размер) или std::vector (динамический размер).
#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);
}Почему нет гарантии про служебные байты перед массивом?
Реальные аллокаторы могут хранить метаданные в разных местах; конкретное расположение является внутренней деталью реализации. Обращение к p[-1] при p, полученном от new[], не становится допустимым из-за того, что там иногда видны «интересные числа».
Современные альтернативы для функции-обратного вызова
Кроме обычных указателей на функции, компаратором могут быть функциональный объект или лямбда. Они поддерживают передачу дополнительного состояния, а компилятор зачастую может инлайнить вызовы.
#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; });
}Почему не стоит использовать ... в прикладном C++
В C-подобных вариадических функциях типы дополнительных аргументов не проверяются по сигнатуре, а при использовании va_arg требуется знать фактически переданный тип с учётом стандартных продвижений. Современная альтернатива — вариадические шаблоны и безопасные обёртки.
Справочник терминов
Термины встречаются внутри глав; здесь можно быстро найти определение.
Массив
Объект фиксированного размера из элементов одного типа, размещённых подряд.
Примеры кода
Первый минимум непустого массива
#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';
}Два вложенных цикла: сначала строка, затем её элементы.
#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';
}
}Обсудить лекцию с ИИ
Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.