10.23 – Знакомство с std::vector
В предыдущем уроке мы представили std::array
, который обеспечивает функциональность встроенных фиксированных массивов C++ в более безопасной и удобной форме.
Аналогичным образом стандартная библиотека C++ предоставляет функциональные возможности, которые делают более безопасной и простой работу с динамическими массивами. Это средство называется std::vector
.
В отличие от std::array
, который точно соответствует базовой функциональности фиксированных массивов, std::vector
содержит некоторые дополнительные возможности. Это помогает сделать std::vector
одним из самых полезных и универсальных инструментов в вашем наборе инструментов C++.
Знакомство с std::vector
Представленный в C++03, std::vector
предоставляет функциональность динамического массива, которая обеспечивает собственное управление памятью. Это означает, что вы можете создавать массивы, длина которых устанавливается во время выполнения, без необходимости явно выделять и освобождать память с помощью операторов new
и delete
. std::vector
находится в заголовке <vector>
.
Объявить std::vector
просто:
#include <vector>
// в объявлении не нужно указывать длину
std::vector<int> array;
// использовать список инициализаторов для инициализации массива (до C++11)
std::vector<int> array2 = { 9, 7, 5, 3, 1 };
// использовать унифицированную инициализацию для инициализации массива
std::vector<int> array3 { 9, 7, 5, 3, 1 };
// как и в случае с std::array, тип может быть опущен, начиная с C++17
std::vector array4 { 9, 7, 5, 3, 1 }; // вывести к std::vector<int>
Обратите внимание, что как в неинициализированном, так и в инициализированном случае вам не нужно указывать длину массива во время компиляции. Это связано с тем, что std::vector
будет динамически выделять память для своего содержимого, сколько потребуется.
Как и в std::array
, доступ к элементам массива может осуществляться с помощью оператора []
(который не проверяет границы) или функции at()
(которая выполняет проверку границ):
array[6] = 2; // без проверки границ
array.at(7) = 3; // проверяет границы
В любом случае, если вы запрашиваете элемент, который находится за пределами массива, вектор не будет автоматически изменять свой размер.
Начиная с C++11, вы также можете присваивать значения std::vector
, используя список инициализаторов:
array = { 0, 1, 2, 3, 4 }; // ok, длина массива теперь 5
array = { 9, 8, 7 }; // ok, длина массива теперь 3
В этом случае вектор автоматически изменит размер, чтобы соответствовать количеству предоставленных элементов.
Самоочистка предотвращает утечку памяти
Когда переменная вектор выходит за пределы области видимости, она (при необходимости) автоматически освобождает память, которую она контролирует. Это не только удобно (поскольку вам не нужно делать это самостоятельно), но и помогает предотвратить утечки памяти. Рассмотрим следующий фрагмент:
void doSomething(bool earlyExit)
{
int *array{ new int[5] { 9, 7, 5, 3, 1 } }; // выделяем память с помощью new
if (earlyExit)
return; // выходит из функции без освобождения памяти, выделенной выше
// здесь что-то делаем
delete[] array; // никогда не вызывается
}
Если для earlyExit
установлено значение true
, массив никогда не будет освобожден, и произойдет утечка памяти.
Однако если array
является std::vector
, этого не произойдет, потому что память будет освобождена, как только массив выйдет за пределы области видимости (независимо от того, завершится функция раньше или нет). Это делает std::vector
более безопасным в использовании, чем самостоятельное выделение памяти.
Векторы запоминают свою длину
В отличие от встроенных динамических массивов, которые не знают длину массива, на который они указывают, std::vector
отслеживает свою длину. Мы можем запросить длину вектора с помощью функции size()
:
#include <iostream>
#include <vector>
void printLength(const std::vector<int>& array)
{
std::cout << "The length is: " << array.size() << '\n';
}
int main()
{
std::vector array { 9, 7, 5, 3, 1 };
printLength(array);
return 0;
}
Приведенный выше пример печатает:
The length is: 5
Как и в случае с std::array
, size()
возвращает значение вложенного типа size_type
(полный тип в приведенном выше примере будет std::vector<int>::size_type
), который является целочисленным значением без знака.
Изменение размера вектора
Изменение размера встроенного динамически размещаемого массива сложно. Изменить размер std::vector
так же просто, как вызвать функцию resize()
:
#include <iostream>
#include <vector>
int main()
{
std::vector array { 0, 1, 2 };
array.resize(5); // устанавливаем размер 5
std::cout << "The length is: " << array.size() << '\n';
for (int i : array)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
Этот код печатает:
The length is: 5
0 1 2 0 0
Здесь следует отметить два момента. Во-первых, когда мы изменили размер вектора, значения существующих элементов были сохранены! Во-вторых, новые элементы инициализируются значением по умолчанию для типа (которое для int
равно 0).
Размер векторов может быть уменьшен:
#include <vector>
#include <iostream>
int main()
{
std::vector array { 0, 1, 2, 3, 4 };
array.resize(3); // устанавливаем размер 3
std::cout << "The length is: " << array.size() << '\n';
for (int i : array)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
Этот код печатает:
The length is: 3
0 1 2
Изменение размера вектора требует больших вычислительных ресурсов, поэтому вы должны стремиться минимизировать количество таких операций. Если вам нужен вектор с определенным количеством элементов, но вы не знаете значений элементов в момент объявления, вы можете создать вектор со значениями элементов по умолчанию:
#include <iostream>
#include <vector>
int main()
{
// Используя прямую инициализацию, мы можем создать вектор из 5 элементов,
// каждый элемент равен 0. Если мы используем инициализацию с фигурными
// скобками, у вектора будет 1 элемент со значением 5.
std::vector<int> array(5);
std::cout << "The length is: " << array.size() << '\n';
for (int i : array)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
Эта программа печатает:
The length is: 5
0 0 0 0 0
Мы поговорим о том, почему прямая инициализация и инициализация с фигурными скобками обрабатываются по-разному в уроке «16.7 – Список инициализаторов std::initializer_list
». Практическое правило: если тип представляет собой какой-то список, и вы не хотите инициализировать его списком, используйте прямую инициализацию.
Уплотнение логических значений
У std::vector
есть еще один крутой трюк. Существует специальная реализация для std::vector
типа bool
, которая сжимает 8 логических значений в один байт! Это происходит за кулисами и не меняет способ использования std::vector
.
#include <vector>
#include <iostream>
int main()
{
std::vector<bool> array { true, false, false, true, true };
std::cout << "The length is: " << array.size() << '\n';
for (int i : array)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
Этот код печатает:
The length is: 5
1 0 0 1 1
Еще не всё
Обратите внимание, что это вводная статья, предназначенная для ознакомления с основами std::vector
. В уроке «11.11 – Емкость и стековое поведение std::vector
» мы рассмотрим некоторые дополнительные возможности std::vector
, включая разницу между длиной и емкостью вектора, и более подробно рассмотрим, как std::vector
обрабатывает выделение памяти.
Заключение
Поскольку переменные типа std::vector
обеспечивают собственное управление памятью (что помогает предотвратить утечки памяти), запоминают свою длину и могут легко изменять размер, мы рекомендуем использовать std::vector
в большинстве случаев, когда необходимы динамические массивы.