Почему ссылка на элемент std::vector ломается после push_back

Разбираем ошибку heap-use-after-free в C++ на коротком примере. Проверим её с AddressSanitizer и исправим код, который читает элемент после роста вектора.

Содержание

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

Как воспроизвести ошибку

3 ноября вышел LLVM 23.1.5. Воспользуемся знакомым инструментом из комплекта Clang — AddressSanitizer, сокращённо ASan. Он добавляет в программу проверки и при запуске показывает, где она обращается к уже освобождённой памяти. Для опыта подойдёт и более ранний Clang с поддержкой ASan.

Сохраните пример в main.cpp. Строка с resize нужна только для эксперимента. Она заполняет все свободные места в векторе, так что для следующего числа точно понадобится новый участок памяти.

main.cpp
C++
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{42};
    scores.resize(scores.capacity());
    int& first = scores[0];

    scores.push_back(99);
    std::cout << first << '\n';
}

int& first даёт другое имя первому числу в памяти. Отдельная копия числа при этом не создаётся. Представьте, что вы записали адрес ячейки, где лежит 42. Заменить адрес в этой записи вектор за вас не сможет.

Что меняется при добавлении числа

У вектора есть размер и запас места. size() показывает, сколько элементов лежит в нём сейчас. capacity() показывает, сколько поместится до следующего переезда. Например, при размере 3 и ёмкости 4 четвёртый элемент займёт свободное место. Для пятого придётся искать участок побольше.

В нашем примере свободных мест уже нет. При вызове push_back вектор выделяет новый участок, переносит туда элементы и освобождает старый. Число 42 по-прежнему первое. Только старая ссылка first ведёт туда, где оно лежало раньше. Ошибка возникает в последней строке, когда программа пытается прочитать число по этой ссылке.

Как увидеть ошибку в терминале

Откройте терминал в папке с примером. Если у вас GCC с поддержкой AddressSanitizer, соберите и запустите программу командами ниже. Здесь g++ — компилятор C++ из GCC. Если он установлен под именем g++-16, подставьте это имя в первую команду. Для Clang с установленной библиотекой ASan подойдёт clang++ с теми же флагами.

Терминал
Bash
g++ -std=c++17 -O0 -g -fsanitize=address -fno-omit-frame-pointer main.cpp -o demo
./demo

На GCC 16.2 с включённой проверкой мы получили heap-use-after-free и READ of size 4. Первая запись означает, что программа обратилась к уже освобождённой памяти. Вторая — что она пыталась прочитать четыре байта. В нашем примере это чтение first в последней строке. Ниже, под словами freed by, можно проследить освобождение памяти до вызова push_back. Если в отчёте видны номера строк, начните с ближайшей строки своего файла.

Два способа исправить код

Если нужно запомнить счёт на тот момент, уберите амперсанд. Объявление int first = scores[0]; сохраняет самостоятельную копию числа 42. Позже сам счёт может измениться, а снимок останется прежним.

Если нужен актуальный счёт первого игрока, запомните его позицию. После добавления в конец элементы сохраняют порядок, и первый игрок по-прежнему находится под номером ноль. Вот полный исправленный пример.

main.cpp
C++
#include <iostream>
#include <vector>

int main() {
    std::vector<int> scores{42};
    scores.resize(scores.capacity());
    const auto first_index = 0;

    scores.push_back(99);
    std::cout << scores[first_index] << '\n';
}

После исправления мы заново собрали программу с той же проверкой. Она вывела 42 и завершилась без сообщения ASan. Теперь scores[first_index] обращается к текущей памяти вектора. Если же вы ещё и удаляете игроков или сортируете таблицу, старый индекс может указывать на другого человека. Тогда пригодится постоянный идентификатор игрока.

Проверьте себя

Предположим, вектор уже содержит один элемент, а его ёмкость равна четырём. Вы берёте ссылку на первый элемент и по очереди дописываете числа в конец. После какого добавления придётся заново получить ссылку?

После четвёртого добавления. Второй, третий и четвёртый элементы ещё помещаются, а пятый вызывает перенос. Если число элементов известно заранее, можно вызвать reserve до получения ссылки. Если предел заранее неизвестен, проще запоминать позицию и получать элемент заново в момент чтения.

Источники

  1. LLVM. Выпуск LLVM 23.1.5 [Электронный ресурс]. — URL: https://llvm.org/ (дата обращения: 07.11.2026).
  2. Clang. AddressSanitizer [Электронный ресурс]. — URL: https://clang.llvm.org/docs/AddressSanitizer.html (дата обращения: 07.11.2026).
  3. Рабочий проект стандарта C++. Вставка элементов в vector [Электронный ресурс]. — URL: https://eel.is/c++draft/vector.modifiers (дата обращения: 07.11.2026).
  4. Рабочий проект стандарта C++. Ёмкость vector [Электронный ресурс]. — URL: https://eel.is/c++draft/vector.capacity (дата обращения: 07.11.2026).
  5. GCC. Параметры инструментальной проверки [Электронный ресурс]. — URL: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html (дата обращения: 07.11.2026).
C++Память