push_back  —  одна из типичных операций при работе с динамическими массивами  —  векторами.

Этим методом элементы добавляются в конец вектора, размер которого при необходимости динамически изменяется.

Изучим нюансы работы push_back(), его применение в реальных сценариях.

push_back(): основы

push_back() является функцией-членом класса std::vector. Ею добавляется элемент в конец вектора, размер которого при этом увеличивается на единицу. Начнем с простого примера:

#include <vector>
#include <iostream>

int main() {
std::vector<int> numbers;
numbers.push_back(42);
std::cout << "Vector size: " << numbers.size() << std::endl;
std::cout << "Last element: " << numbers.back() << std::endl;
return 0;
}

В этом коде создается пустой вектор, к нему добавляется число 42, затем выводятся размер вектора и добавленный элемент. Не так просто, как кажется на первый взгляд.

push_back(): управление памятью

При вызове push_back() случается вот что:

  1. Вектором проверяется, достаточно ли у него емкости для добавления другого элемента.
  2. Если места недостаточно, им выделяется новый кусок памяти, побольше.
  3. Имеющиеся элементы копируются им в новую ячейку памяти.
  4. Новый элемент добавляется им в конец.
  5. Обновляются размер и емкость.

Этим процессом обеспечивается динамическое увеличение вектора при сохранении непрерывной памяти, что важно для производительности. Разберем каждый этап.

Емкость против размера

Емкость вектора  —  это объем выделенной ему памяти, а его размер  —  количество элементов, которые в нем сейчас содержатся. Когда push_back() применяется к элементу и размер равен емкости, вектору нужно увеличиваться.

std::vector<int> vec;
std::cout << "Initial capacity: " << vec.capacity() << std::endl;

for (int i = 0; i < 10; ++i) {
vec.push_back(i);
std::cout << "Size: " << vec.size() << ", Capacity: " << vec.capacity() << std::endl;
}

Запустите этот код и увидите, что емкость не увеличивается с каждым push_back(). Обычно она удваивается, когда вектору нужно увеличиваться, и количество перераспределений сокращается.

Процесс перераспределения

Когда вектору нужно увеличиваться, им обычно:

  1. Выделяется новый блок памяти, обычно вдвое больше текущей емкости.
  2. Копируются все имеющиеся элементы в новую ячейку.
  3. Высвобождается старая память.

Это дорогой процесс, особенно для крупных векторов. Поэтому при помощи push_back() оптимизируют код.

Оптимизация push_back(): производительность

push_back() удобен, но самым эффективным оказывается не всегда. Вот сценарии, где пригодятся альтернативы.

reserve(), когда известен размер

Если количество добавляемых элементов известно, память выделяется заранее при помощи reserve():

std::vector<int> optimized_vec;
optimized_vec.reserve(1000000);

for (int i = 0; i < 1000000; ++i) {
optimized_vec.push_back(i);
}

Таким подходом предотвращаются многократные перераспределения, у больших векторов значительно повышается производительность.

insert() для вставки нескольких элементов

insert() эффективнее при добавлении сразу нескольких элементов:

std::vector<int> vec = {1, 2, 3};
std::vector<int> to_insert = {4, 5, 6};

vec.insert(vec.end(), to_insert.begin(), to_insert.end());

Этот метод быстрее множественных вызовов push_back(), особенно при необходимости перераспределений.

Реальные применения: где хорош push_back()

Рассмотрим практические сценарии, в которых push_back() оказывается незаменимым:

Создание динамического списка задач

Пользователи добавляют в него задачи, которые эффективно сохраняются программой:

#include <vector>
#include <string>
#include <iostream>

class Task {
public:
Task(std::string desc) : description(std::move(desc)), completed(false) {}
std::string description;
bool completed;
};

class TodoList {
private:
std::vector<Task> tasks;

public:
void addTask(const std::string& description) {
tasks.push_back(Task(description));
}

void displayTasks() const {
for (const auto& task : tasks) {
std::cout << (task.completed ? "[X] " : "[ ] ") << task.description << std::endl;
}
}
};

int main() {
TodoList myList;
myList.addTask("Learn C++ push_back");
myList.addTask("Write an article");
myList.addTask("Share knowledge");

myList.displayTasks();
return 0;
}

В этом примере новые задачи легко добавляются в список при помощи push_back(), не нужно управлять памятью или менять размер массивов вручную.

Реализация простого графа

Графы  —  фундаментальные структуры данных в информатике. Ими часто обозначаются сети, взаимосвязи или пути. Вот как, используя push_back(), реализуется базовое представление графа в виде списка смежности:

#include <vector>
#include <iostream>

class Graph {
private:
std::vector<std::vector<int>> adjacencyList;

public:
Graph(int vertices) : adjacencyList(vertices) {}

void addEdge(int from, int to) {
adjacencyList[from].push_back(to);
// Для неориентированного графа выполнилось бы и
// «adjacencyList[to].push_back(from);»
}

void printGraph() const {
for (int i = 0; i < adjacencyList.size(); ++i) {
std::cout << "Vertex " << i << " is connected to: ";
for (int neighbor : adjacencyList[i]) {
std::cout << neighbor << " ";
}
std::cout << std::endl;
}
}
};

int main() {
Graph g(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 3);

g.printGraph();
return 0;
}

Здесь благодаря push_back() к структуре графа динамически добавляются ребра, отчего она делается гибкой и легко изменяемой.

Продвинутые методы: Emplace_back и семантика перемещения

Освоив push_back(), изучим более продвинутые методы и выжмем максимум производительности.

Emplace_back(): создание элементов на месте

С emplace_back() элементы конструируются непосредственно в хранилище вектора, чем потенциально избегаются лишние копирования или перемещения:

#include <vector>
#include <string>

class Person {
public:
Person(std::string name, int age) : name_(std::move(name)), age_(age) {}
private:
std::string name_;
int age_;
};

int main() {
std::vector<Person> people;

// Использование «push_back»
people.push_back(Person("Alice", 30));

// Использование «emplace_back»
people.emplace_back("Bob", 25);

return 0;
}

В этом примере при помощи emplace_back() объект Person создается прямо в векторе, в то время как с push_back() он сначала создается, а затем перемещается в вектор.

Семантика перемещения: эффективные передачи

Семантикой перемещения ресурсы передаются от одного объекта другому без глубокого копирования. Это особенно полезно при работе с push_back():

#include <vector>
#include <string>
#include <iostream>

int main() {
std::vector<std::string> words;
std::string long_word = "supercalifragilisticexpialidocious";

// Использование семантики перемещения
words.push_back(std::move(long_word));

std::cout << "Vector content: " << words[0] << std::endl;
std::cout << "Original string: " << long_word << std::endl;

return 0;
}

После этой операции long_word остается в допустимом, но не заданном состоянии, его содержимое эффективно перемещается в вектор.

Заключение: о мощи push_back()

Мы изучили нюансы push_back()  —  от базового применения до продвинутых техник и реальных сценариев. Этот метод является основой динамического управления данными на C++, благодаря которой операции над векторами выполняются гибко и эффективно.

Обобщим ключевые моменты:

  1. При помощи push_back() элементы добавляются в конец вектора, память выделяется автоматически.
  2. Понимая взаимосвязи размера и емкости, можно оптимизировать операции над векторами.
  3. Когда размеры известны, производительность значительно повышается применением reserve().
  4. В реальных сценариях вроде списков задач и графов обнаруживается универсальность push_back().
  5. С продвинутыми техниками, такими как emplace_back() и семантика перемещения, эффективность еще выше.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: C++ push_back: Comprehensive Guide

Предыдущая статьяНаблюдаемость как суперспособность
Следующая статьяTypeScript: от нулевого до продвинутого уровня. Часть 1