C++ error: "pointer being freed was not allocated


Встретил очередной вопрос на Stack Overflow от человека, изучающего язык C++. Количество подобных вопросов можно сократить, используя PVS-Studio. Человек сразу может получить ответ, не отвлекая других.


Я уже описывал здесь, что online версия анализатора PVS-Studio может существенно облегчить жизнь начинающим программистам. Рассмотрим ещё один подобный случай.


Заглянем в дискуссию "C++ error: "pointer being freed was not allocated" на сайте Stack Overflow и посмотрим на этот код:


#include <stdexcept>
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

using std::cout;
using std::endl;
using std::vector;      

typedef vector<int> ints;

void print_ints(vector<int>);
void print_ints_vec(vector<vector<int>>);
void int_part(int, vector<vector<int>>&);

int main() 
{
  vector<vector<int>> partition;
  int_part(5, partition);
  print_ints_vec(partition);

  return 0;
}

void int_part(int sum, vector<vector<int>>& res)
{
  vector<int> init_xs = vector<int>{sum};
  vector<int>* xs = &init_xs; // POINTER INITIALIZED TO vector<int>
  int current_sum = sum;

  while (true) 
  {
    current_sum = accumulate(xs->begin(), xs->end(), 0);

    if (current_sum == sum)
    {
      res.push_back(*xs);
      vector<int> next_xs;
      vector<int>::iterator it = find(xs->begin(), xs->end(), 1);
      if (it == xs->begin()) return;
      copy(xs->begin(), it, back_inserter(next_xs));
      next_xs[next_xs.size() - 1] -= 1;
      xs = &next_xs; // POINTER REASSIGNED TO ANOTHER vector<int>
    }
    else 
    {
      int tail = xs->back();
      int diff = sum - current_sum;
      int m = std::min(tail, sum - tail);
      int next_tail = current_sum + m > sum ? diff : m;
      xs->push_back(next_tail);
    }
  }
}

void print_ints(ints v) // PRINT UTILITY
{
  cout << "[ ";
  for (const int& n : v) { cout << n << "; "; }
  cout << "]" << endl;
}

void print_ints_vec(vector<ints> v) // PRINT UTILITY
{
  cout << "[ \n";
  for (const vector<int>& xs : v) { cout << "  "; print_ints(xs); }
  cout << "]" << endl;
}

Согласитесь, что не очень хочется тратить время и силы на прочтение этого текста лабораторной работы, выискивание ошибки. И не надо! Давайте перепоручим эту задачу анализатору PVS-Studio.


Вот что он сообщает: V506 Pointer to local variable 'next_xs' is stored outside the scope of this variable. Such a pointer will become invalid.


Строка:


xs = &next_xs; // POINTER REASSIGNED TO ANOTHER vector<int>

И действительно, ошибка в том, что сохраняется ссылка на объект, который будет разрушен. На эту ошибку указали другие люди и объяснили, в чём состоит проблема. Однако можно было бы обойтись вообще без ожидания ответа от более опытных коллег. Ведь объяснение ошибки вполне можно получить и из документации анализатора PVS-Studio по диагностике V506.


Вывод


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


Дополнительные ссылки:


  1. Статический анализ кода.
  2. PVS-Studio: online версия.
  3. PVS-Studio: бесплатное использование для студентов.

Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. PVS-Studio to help with schoolwork-like tasks in C and C++.

Комментарии (7)


  1. nerudo
    06.07.2022 12:55
    +8

    Осталось взять GitHub Copilot, замкнуть их друг на друга и можно идти на заслуженную пенсию.


  1. amarao
    06.07.2022 13:05
    -1

    И действительно, ошибка в том, что сохраняется ссылка на объект, который будет разрушен

    ... Rust смотрит на такое с недоумением. Без специальных выкрутасов такое невозможно. Код просто не компилируется.


    1. Akon32
      06.07.2022 15:24
      +1

      Языки со сборщиком мусора, где тоже "без специальных выкрутасов такое невозможно" появились за 10 лет до С. Печально, что Rust, обеспечивающий аналогичные и столь нужные гарантии, появился только через ~40-50 лет с тех времён.


      1. amarao
        06.07.2022 15:39
        +2

        Утверждать, что Rust обеспечивает "аналогичные и столь нужные гарантии, как GC", это, мягко говоря, understatement. Модель ownership/borrow с концепцией вычисляемых (и энфорсенных) lifetimes на несколько голов выше по утилитарности (пользе), чем просто "сборка мусора".

        Во-первых это неожиданно резко показало себя в мире многопоточности. Любой "обычный" язык с GC в условиях многопоточности чувствует себя как Си в мире безопасных указателей. Всё можно и за всё прилетает.

        Во-вторых ряд ошибочных структур данных просто перестаёт существовать (например, список с циклом). Без специальных усилий вы просто физически не можете сделать так, чтобы cons принадлежал двум разным элементам. Вы можете написать такой список, но вам придётся явно ответить как с этим жить и как его удалять.


        1. Akon32
          06.07.2022 18:49

          ряд ошибочных структур данных просто перестаёт существовать (например, список с циклом).

          А почему список с циклом считается ошибочным? Что, если предметная область требует такое (первый пришедший на ум пример - периодические дроби)? Тогда gc справится автоматически, а в Rust (как и в С) придётся писать мудрёные деструкторы, что говорит не в пользу последних. Или в Rust можно как-то системой времён жизни выразить список с циклом? Многопоточность - тоже не всегда аргумент.


          1. amarao
            07.07.2022 12:10

            Список с циклом называется граф. А ошибочным список с циклом является в том, что не ясно кто именно освобождает узел, на который ссылаются два элемента списка. (для удобства назовём их "прямой" и "загибающий").

            Если "прямой" узел решает освободить потомка, и переключиться на следующий узел, то он никак не рассчитывает на то, что кто-то ещё содержит в себе ссылку (которая после этого начинает указывать "в никуда"). Типовая сишная проблема, dangling pointer.

            Структура с кольцом вполне может быть, но в момент её создания кто-то должен ответить, что делать, если элемент освобождают, но на него несколько ссылок. Можно, например, Rc (в Rust) использовать. Если элемент "освобождают", то он уменьшает число ссылок внутри себя. Как только становится ноль - самоликвидируется.

            И хороший компилятор (Rust, Rust) просто не даст скомпилировать код, в котором возможен dangling pointer; то есть заставит использовать структуры, которые обрабатывают все такие ситуации.


  1. ReadOnlySadUser
    07.07.2022 10:10

    ИЛИ! -fsanitize=address

    Да, не в compile time конечно ошибка, но для лабораторных и пофигу)