Перейти к содержимому
Topos Uranos

Краткое содержание

std::array требует знать при компиляции, сколько в нём будет элементов; большинство задач, напротив, не сообщают этого, пока не придут данные. Этот урок представляет std::vector, последовательность, которая растёт вместе с данными: как его создавать, как добавлять в него элементы с помощью push_back и как читать с его помощью ряды неизвестной длины. Затем идут три канонических обхода всякого программирования с коллекциями (накопление, фильтрация и преобразование), с циклом и с алгоритмами библиотеки; передача вектора в функцию и то, почему возвращать новый вектор дёшево. Завершает урок внутреннее устройство вектора: его размер и ёмкость, блок памяти, который при заполнении заменяется другим, вдвое большим, и ссылка на элемент, которая поэтому может остаться указывающей на освобождённую память.

Цели обучения

  1. Создавать std::vector, добавлять в него элементы с помощью push_back и обходить его, считывая с его помощью ряд данных неизвестной длины.
  2. Писать функции, которые принимают вектор по константной ссылке, чтобы читать его, и по ссылке, чтобы изменять его, и которые возвращают новый вектор, когда результатом является последовательность.
  3. Реализовывать обходы накопления, фильтрации и преобразования, распознавать в задаче соответствующий ей шаблон и сокращать его, когда библиотека это позволяет, с помощью std::ranges::count, std::ranges::max или std::ranges::fold_left.
  4. Объяснять на примере разницу между размером и ёмкостью вектора и то, почему ссылка на элемент может стать недействительной при добавлении другого.

Последовательность, которая растёт

Создание и заполнение вектора

Урок Массивы представил индексированную последовательность: элементы одного типа, один за другим, пронумерованные с нуля. Её современная форма фиксированного размера, std::array<int, 7>, несёт размер в типе, и компилятор должен его знать. Однако тот, кто пишет программу, вычисляющую средний балл группы, не знает, сколько в ней будет студентов. Отсюда следует потребность в последовательности, размер которой есть ещё одно данное, меняющееся во время выполнения: это вектор, std::vector<T>, из заголовочного файла <vector>, где T есть тип элементов.

Программа на C++
#include <print>#include <vector>​int main(){    std::vector<int> squares{};    std::println("В начале: размер {}, пуст: {}", squares.size(), squares.empty());    for (int k{1}; k <= 5; ++k) {        squares.push_back(k * k);    }    std::println("{}", squares);    std::println("Размер {}, первый {}, последний {}", squares.size(), squares[0], squares.back());    squares[1] = 0;    squares.pop_back();    std::println("{}", squares);}
Вывод
В начале: размер 0, пуст: true[1, 4, 9, 16, 25]Размер 5, первый 1, последний 25[1, 0, 9, 16]

Строка 6 создаёт пустой вектор целых чисел: в отличие от массива, не нужно объявлять, сколько в нём будет элементов. size() возвращает, сколько их есть, а empty() сообщает, нет ли ни одного. Цикл в строках с 8 по 10 добавляет пять квадратов с помощью push_back, которая помещает свой аргумент в конец и увеличивает размер на единицу. Строка 11 выводит весь вектор в формате диапазонов, который предыдущий урок использовал с массивами. Доступ по индексу прежний, а squares.back() сокращает squares[squares.size() - 1]. Строка 13 присваивает значение существующему элементу, а строка 14 удаляет последний с помощью pop_back. Операции этого урока, вкратце, таковы.

  • v.push_back(x) добавляет x в конец; v.pop_back() удаляет последний (для пустого вектора это неопределённое поведение).
  • v.size() есть размер, типа std::size_t, беззнакового целого для размеров, который представил урок Массивы; v.empty() равносильно v.size() == 0.
  • v[i] и v.at(i) дают элемент с индексом i, который должен удовлетворять i < v.size(); v.front() и v.back() дают первый и последний и требуют, чтобы вектор не был пуст.
  • v.resize(n) оставляет в векторе n элементов: удаляет лишние в конце или добавляет в конец элементы, инициализированные как пустыми фигурными скобками (нулями, для чисел).

Как и у массивов, v[i] не гарантирует никакого контроля, а v.at(i) гарантирует: при индексе 7 и пяти элементах он останавливает программу с сообщением vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), где названы индекс и размер. Механизм, вызывающий эту остановку, исключение, изучается в курсе «Программирование на C++ II».

Фигурные и круглые скобки

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

Программа на C++
#include <print>#include <vector>​int main(){    const std::vector<int> a{5};    const std::vector<int> b(5);    const std::vector<int> c{3, 7};    const std::vector<int> d(3, 7);    std::println("{} {} {} {}", a, b, c, d);}
Вывод
[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]

a содержит один элемент, 5; b, пять нулей; c, 3 и 7; d, три семёрки. Список в фигурных скобках говорит, таким образом, «вот элементы», а круглые скобки оставлены для того, чтобы запросить n копий одного значения, как вектор счётчиков, равных нулю.

Последний элемент пустого вектора

Пустой вектор есть законный случай (вход без данных, фильтр, который никто не проходит), и каждую формулу с его размером нужно пересматривать для этого случая; самая коварная из них v.size() - 1. Поскольку v.size() есть беззнаковое целое, вычитание 1, когда оно равно 0, даёт не −1-1, а «заворачивается», как было показано в уроке Целые числа и их представление, до наибольшего значения std::size_t.

Программа на C++
#include <cstddef>#include <print>#include <vector>​int main(){    const std::vector<int> values{};    std::println("values.size() - 1 = {}", values.size() - 1);    // Ошибочная граница: для пустого вектора условие истинно.    for (std::size_t i{0}; i <= values.size() - 1; ++i) {        std::println("{}", values[i]);    }}
Вывод
values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.

Система остановила программу сигналом 6.

Первая строка вывода есть 18 446 744 073 709 551 61518\,446\,744\,073\,709\,551\,615, то есть 264−12^{64} - 1, наибольшее значение std::size_t на этой платформе. Отсюда следует, что условие цикла, 0 <= 18446744073709551615, истинно и что первая итерация читает values[0] у вектора, в котором нет элемента 0: неопределённое поведение. То, что наблюдается, зависит от опций, как и у массивов: при компиляции без оптимизации библиотека GCC 15 контролирует индекс в operator[] и останавливает программу утверждением, которое гласит __n < this->size(); с оптимизацией этот контроль исчезает, и цикл, который не останавливается вовремя, пытается читать там, где нет ни одного элемента (подтверждено в Linux с -O2: поскольку у пустого вектора нет блока памяти, программа завершается ошибкой сегментации, без сообщения, объясняющего причину). Исправление состоит в том, чтобы записать границу без вычитания, i < values.size(), что для пустого вектора ложно с самого начала; а там, где вычитание кажется необходимым, сначала спросить values.empty().