⚙️ C++

Методы массивов и коллекций в C++: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

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

Но просто хранить данные — мало. Тебе нужно уметь с ними работать: найти самого высокого ученика, посчитать средний рост, добавить новичка, удалить того, кто перешёл в другую школу, отсортировать всех по алфавиту. Именно для этого существуют методы массивов и коллекций — готовые инструменты, которые делают всю рутинную работу за тебя.

Аналогия: Представь, что массив — это коробка с разноцветными кубиками. Методы — это твои руки: ты можешь взять кубик (доступ по индексу), положить новый (добавление), выкинуть старый (удаление), переставить все кубики по цветам (сортировка) или посчитать, сколько красных (поиск). Без методов тебе пришлось бы каждый раз писать сложные инструкции для рук — а с ними всё просто и быстро.

В C++ есть два основных типа контейнеров: статические массивы (размер фиксирован, как в старом шкафу — сколько полок, столько и есть) и динамические коллекции (например, std::vector — как эластичный рюкзак, который растягивается, когда ты кладёшь туда новые вещи).

Зачем тебе это знать? Потому что 90% программ работают с наборами данных: список пользователей, результаты игры, температура за неделю, координаты точек на карте. Умение управлять этими наборами — базовый навык программиста.

Теория: как это работает изнутри

Давай заглянем «под капот» и поймём, как устроены массивы и коллекции в C++.

Статический массив (C-style array)

Это самый простой тип. Ты выделяешь память на стеке (быстрая память, но ограниченная) и говоришь: «Вот тебе 10 ячеек для целых чисел». Размер фиксирован навсегда. Внутри — это просто последовательная область памяти. Чтобы получить элемент №3, компьютер берёт адрес начала массива и прибавляет 3 * sizeof(int) байт. Всё очень быстро, но негибко.

int scores[5] = {10, 20, 30, 40, 50}; // 5 ячеек, и ни больше, ни меньше
// scores[0] = 10, scores[4] = 50
// scores[5] — уже ошибка, выходим за границу!

std::vector (динамический массив)

Это «умный» массив из стандартной библиотеки C++. Он хранит данные в куче (более медленная, но большая память). Когда ты добавляешь элемент, а места нет, vector автоматически выделяет новый кусок памяти в 2 раза больше, копирует туда все старые элементы и удаляет старый кусок. Это называется реаллокация.

Важно: Реаллокация — дорогая операция. Если ты знаешь, что будет много элементов, лучше сразу зарезервировать место через reserve().

Другие коллекции (std::list, std::set, std::map)

  • std::list — двусвязный список. Каждый элемент хранит указатели на следующий и предыдущий. Быстро вставлять/удалять в середине, но медленно искать (нужно идти по цепочке).
  • std::set — множество. Хранит уникальные элементы в отсортированном порядке. Внутри — красно-чёрное дерево. Быстрый поиск, но нельзя изменить элемент (только удалить и добавить новый).
  • std::map — словарь. Пары «ключ-значение». Тоже дерево. Идеально для поиска по ключу.

Все коллекции поддерживают итераторы — специальные объекты, которые «ходят» по элементам, как курсор в тексте. Это универсальный способ перебора.

Синтаксис в C++: все формы и варианты

Давай разберём основные методы на примере std::vector — самой популярной коллекции. Я покажу синтаксис, а ты запоминай.

Подключение и создание

#include <vector>  // обязательно подключить библиотеку
#include <iostream>
#include <algorithm> // для sort, find и других алгоритмов

int main() {
    // Пустой вектор
    std::vector<int> numbers;
    
    // Вектор с 5 нулями
    std::vector<int> zeros(5);
    
    // Вектор с 5 десятками
    std::vector<int> tens(5, 10);
    
    // Вектор из списка инициализации
    std::vector<int> values = {1, 2, 3, 4, 5};
    
    return 0;
}

Основные методы (таблица-справочник)

Метод Что делает Пример Сложность
push_back(value) Добавляет элемент в конец vec.push_back(42); O(1) в среднем
pop_back() Удаляет последний элемент vec.pop_back(); O(1)
size() Возвращает количество элементов int n = vec.size(); O(1)
empty() Проверяет, пуст ли вектор if (vec.empty()) O(1)
clear() Удаляет все элементы vec.clear(); O(n)
at(index) Доступ к элементу с проверкой границ vec.at(2) = 100; O(1)
operator[] Доступ без проверки (быстрее, но опаснее) vec[2] = 100; O(1)
front() Первый элемент int first = vec.front(); O(1)
back() Последний элемент int last = vec.back(); O(1)
insert(pos, value) Вставляет элемент в позицию (по итератору) vec.insert(vec.begin()+2, 99); O(n)
erase(pos) Удаляет элемент по итератору vec.erase(vec.begin()+1); O(n)
reserve(n) Зарезервировать память на n элементов vec.reserve(1000); O(n)
resize(n) Изменить размер (добавить нули или удалить) vec.resize(10); O(n)

Алгоритмы из <algorithm>

Это не методы вектора, а свободные функции, которые работают с любыми коллекциями через итераторы.

Функция Что делает Пример
sort(begin, end) Сортирует по возрастанию std::sort(vec.begin(), vec.end());
reverse(begin, end) Переворачивает порядок std::reverse(vec.begin(), vec.end());
find(begin, end, value) Ищет первое вхождение, возвращает итератор auto it = std::find(vec.begin(), vec.end(), 42);
count(begin, end, value) Считает количество вхождений int c = std::count(vec.begin(), vec.end(), 7);
fill(begin, end, value) Заполняет диапазон значением std::fill(vec.begin(), vec.end(), 0);
min_element(begin, end) Итератор на минимальный элемент auto min = std::min_element(vec.begin(), vec.end());
max_element(begin, end) Итератор на максимальный элемент auto max = std::max_element(vec.begin(), vec.end());

Примеры кода — от простого к сложному

Теперь самое интересное — живой код. Каждый пример я разберу по строчкам.

Пример 1: Создание и базовые операции

#include <iostream>
#include <vector>

int main() {
    // Создаём вектор с оценками учеников
    std::vector<int> grades = {5, 4, 3, 5, 4, 2};
    
    // Выводим размер
    std::cout << "Количество оценок: " << grades.size() << std::endl;
    
    // Добавляем новую оценку в конец
    grades.push_back(5);  // теперь {5,4,3,5,4,2,5}
    
    // Удаляем последнюю
    grades.pop_back();    // снова {5,4,3,5,4,2}
    
    // Меняем элемент по индексу
    grades[1] = 5;        // исправляем двойку на пятёрку? Нет, индекс 1 — это 4
    
    // Выводим все через цикл с индексом
    for (int i = 0; i < grades.size(); i++) {
        std::cout << grades[i] << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Объяснение: Мы создали вектор, узнали его размер, добавили и удалили элемент, изменили один элемент и вывели всё на экран. Обрати внимание: grades.size() возвращает size_t (беззнаковый тип), поэтому в цикле мы используем int i — это нормально для небольших размеров.

Пример 2: Перебор через итераторы и range-based for

#include <iostream>
#include <vector>

int main() {
    std::vector<std::string> fruits = {"яблоко", "банан", "апельсин"};
    
    // Способ 1: классический итератор
    std::cout << "Способ 1: ";
    for (auto it = fruits.begin(); it != fruits.end(); ++it) {
        std::cout << *it << " ";  // *it — разыменование итератора
    }
    std::cout << std::endl;
    
    // Способ 2: range-based for (C++11 и новее) — самый удобный!
    std::cout << "Способ 2: ";
    for (const std::string& fruit : fruits) {
        std::cout << fruit << " ";
    }
    std::cout << std::endl;
    
    // Способ 3: изменяем элементы через ссылку
    for (std::string& fruit : fruits) {
        fruit += " (вкусный)";  // добавляем к каждому фрукту
    }
    
    // Выводим изменённый список
    std::cout << "После изменения: ";
    for (const auto& fruit : fruits) {
        std::cout << fruit << " | ";
    }
    std::cout << std::endl;
    
    return 0;
}

Объяснение: Итераторы — это как пальцы, которыми мы перебираем элементы. begin() указывает на первый, end() — на следующий за последним (не на последний!). Range-based for — это «синтаксический сахар», который скрывает итераторы. Если хочешь изменить элемент, используй ссылку &.

Пример 3: Поиск элемента

#include <iostream>
#include <vector>
#include <algorithm>  // для std::find

int main() {
    std::vector<int> data = {10, 20, 30, 40, 50, 30, 60};
    
    // Ищем число 30
    auto it = std::find(data.begin(), data.end(), 30);
    
    // Проверяем, нашли ли
    if (it != data.end()) {
        // Вычисляем индекс (позицию)
        int index = std::distance(data.begin(), it);
        std::cout << "Нашли 30 на позиции: " << index << std::endl;
        std::cout << "Значение: " << *it << std::endl;
    } else {
        std::cout << "30 не найдено" << std::endl;
    }
    
    // Ищем 100 (которого нет)
    auto it2 = std::find(data.begin(), data.end(), 100);
    if (it2 == data.end()) {
        std::cout << "100 нет в массиве" << std::endl;
    }
    
    return 0;
}

Объяснение: std::find проходит по всем элементам, пока не найдёт нужный. Возвращает итератор на первый найденный. Если не нашёл — возвращает end(). std::distance считает расстояние между двумя итераторами — так мы получаем индекс.

Пример 4: Сортировка и реверс

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9, 3};
    
    std::cout << "Исходный: ";
    for (int n : numbers) std::cout << n << " ";
    std::cout << std::endl;
    
    // Сортировка по возрастанию
    std::sort(numbers.begin(), numbers.end());
    std::cout << "По возрастанию: ";
    for (int n : numbers) std::cout << n << " ";
    std::cout << std::endl;
    
    // Сортировка по убыванию (через reverse или компаратор)
    std::sort(numbers.begin(), numbers.end(), std::greater<int>());
    std::cout << "По убыванию: ";
    for (int n : numbers) std::cout << n << " ";
    std::cout << std::endl;
    
    // Реверс (переворот)
    std::reverse(numbers.begin(), numbers.end());
    std::cout << "После реверса: ";
    for (int n : numbers) std::cout << n << " ";
    std::cout << std::endl;
    
    return 0;
}

Объяснение: std::sort использует быструю сортировку (introsort) — очень эффективно. По умолчанию сортирует по возрастанию. Для убывания передаём std::greater<int>() — это функтор (объект-функция). std::reverse просто переворачивает порядок.

Пример 5: Вставка и удаление в середине

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // Вставляем 100 на позицию 2 (между 2 и 3)
    vec.insert(vec.begin() + 2, 100);
    // Теперь: 1, 2, 100, 3, 4, 5
    
    std::cout << "После вставки: ";
    for (int n : vec) std::cout << n << " ";
    std::cout << std::endl;
    
    // Удаляем элемент на позиции 3 (число 3)
    vec.erase(vec.begin() + 3);
    // Теперь: 1, 2, 100, 4, 5
    
    std::cout << "После удаления: ";
    for (int n : vec) std::cout << n << " ";
    std::cout << std::endl;
    
    // Вставляем несколько элементов сразу
    std::vector<int> add = {10, 20, 30};
    vec.insert(vec.begin() + 1, add.begin(), add.end());
    // Теперь: 1, 10, 20, 30, 2, 100, 4, 5
    
    std::cout << "После вставки диапазона: ";
    for (int n : vec) std::cout << n << " ";
    std::cout << std::endl;
    
    return 0;
}

Объяснение: insert и erase работают с итераторами. begin() + 2 — это итератор на третий элемент (индексация с 0). Вставка и удаление в середине — дорогие операции (O(n)), потому что нужно сдвигать все элементы после позиции.

Пример 6: Работа с пользовательскими структурами

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

// Создаём структуру "Студент"
struct Student {
    std::string name;
    int age;
    double grade;  // средний балл
};

int main() {
    // Создаём вектор студентов
    std::vector<Student> group;
    
    // Добавляем студентов
    group.push_back({"Иван", 17, 4.5});
    group.push_back({"Мария", 16, 5.0});
    group.push_back({"Пётр", 18, 3.8});
    group.push_back({"Анна", 17, 4.2});
    
    // Сортируем по имени (алфавиту)
    std::sort(group.begin(), group.end(), 
              [](const Student& a, const Student& b) {
                  return a.name < b.name;
              });
    
    std::cout << "Студенты по алфавиту:" << std::endl;
    for (const auto& s : group) {
        std::cout << s.name << ", " << s.age << " лет, балл: " << s.grade << std::endl;
    }
    
    // Находим студента с максимальным баллом
    auto best = std::max_element(group.begin(), group.end(),
                                  [](const Student& a, const Student& b) {
                                      return a.grade < b.grade;
                                  });
    
    std::cout << "
Лучший студент: " << best->name << " с баллом " << best->grade << std::endl;
    
    // Фильтруем: оставляем только тех, кто старше 16
    std::vector<Student> adults;
    for (const auto& s : group) {
        if (s.age > 16) {
            adults.push_back(s);
        }
    }
    
    std::cout << "
Студенты старше 16:" << std::endl;
    for (const auto& s : adults) {
        std::cout << s.name << " (" << s.age << " лет)" << std::endl;
    }
    
    return 0;
}

Объяснение: Здесь мы работаем с вектором структур. Для сортировки используем лямбда-функцию — это анонимная функция, которая определяет правило сравнения. max_element тоже требует компаратор. Фильтрацию делаем вручную через цикл — в C++ нет встроенного метода filter, как в Python.

Пример 7: Многомерный вектор (матрица)

#include <iostream>
#include <vector>

int main() {
    // Создаём матрицу 3x4 (3 строки, 4 столбца)
    // Вектор векторов
    std::vector<std::vector<int>> matrix = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    // Выводим матрицу
    std::cout << "Матрица:" << std::endl;
    for (int row = 0; row < matrix.size(); row++) {
        for (int col = 0; col < matrix[row].size(); col++) {
            std::cout << matrix[row][col] << "	";
        }
        std::cout << std::endl;
    }
    
    // Считаем сумму всех элементов
    int sum = 0;
    for (const auto& row : matrix) {
        for (int val : row) {
            sum += val;
        }
    }
    std::cout << "Сумма всех элементов: " << sum << std::endl;
    
    // Добавляем новую строку
    matrix.push_back({13, 14, 15, 16});
    std::cout << "После добавления строки:" << std::endl;
    for (const auto& row : matrix) {
        for (int val : row) {
            std::cout << val << " ";
        }
        std::cout << std::endl;
    }
    
    return 0;
}

Объяснение: std::vector<std::vector<int>> — это вектор, каждый элемент которого — тоже вектор. Так мы получаем двумерный массив. Доступ к элементу: matrix[строка][столбец]. Можно добавлять целые строки через push_back.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаешь Python, тебе будет проще. Давай сравним основные операции.

Операция C++ (std::vector) Python (list) Комментарий
Создание std::vector<int> v = {1,2,3}; v = [1,2,3] В C++ нужно указывать тип
Добавление в конец v.push_back(4); v.append(4) Похоже, но названия разные
Длина v.size() len(v) В C++ это метод, в Python — функция
Доступ по индексу v[2] или v.at(2) v[2] В C++ at() проверяет границы
Срез (slice) Нет встроенного, нужно копировать через итераторы v[1:3] Python намного удобнее для срезов
Сортировка std::sort(v.begin(), v.end()); v.sort() или sorted(v) В C++ нужно передавать итераторы
Поиск std::find(...) — возвращает итератор v.index(3) — возвращает индекс или ошибку В Python проще, но нет итераторов
Удаление по значению Нет прямого метода, нужно комбинировать find и erase v.remove(3) Python удобнее
Удаление по индексу v.erase(v.begin() + 2); del v[2] или v.pop(2) В C++ нужно использовать итераторы
Проверка вхождения std::find(...) != v.end() 3 in v Python — один из самых удобных языков
Максимум/минимум std::max_element(...) max(v) В C++ нужно разыменовывать итератор
Типизация Строгая: вектор только одного типа Динамическая: можно смешивать типы В C++ всё должно быть одного типа
Главное отличие: C++ даёт тебе полный контроль над памятью и производительностью, но требует больше кода. Python — для быстрого прототипирования, но медленнее. Выбирай инструмент под задачу!

Типичные ошибки и подводные камни

Даже опытные программисты иногда спотыкаются на этих граблях. Давай разберём самые частые ошибки.

Ошибка 1: Выход за границы массива

std::vector<int> v = {1, 2, 3};
std::cout << v[10];  // ОПАСНО! Компилятор не ругнётся, но программа может упасть
std::cout << v.at(10); // БЕЗОПАСНО! Выбросит исключение std::out_of_range

Решение: Используй at() для безопасного доступа, особенно когда индекс приходит от пользователя. Или проверяй index < v.size().

Ошибка 2: Итератор инвалидируется

std::vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin() + 2;  // указывает на 3
v.push_back(100);  // может произойти реаллокация!
// it теперь указывает в никуда — опасно использовать!
std::cout << *it;  // НЕПРАВИЛЬНО!

Решение