【问题标题】:Proper Way to Handle Vector of Pointers to Derived Classes?处理派生类指针向量的正确方法?
【发布时间】:2020-08-06 06:22:42
【问题描述】:

我不知道如何正确地拥有一个没有内存泄漏的派生类向量。我尝试了以下方法,但有问题:

#include <iostream>
#include <vector>
using namespace std;

struct base {};

struct derived : public base
{
  derived() {}
};

struct Layer
{
  vector<base*> effects;

  Layer() {}

  ~Layer()
  {
    for(int ii = 0; ii < effects.size(); ii++)
    {
      cout << "called effect deleter" << endl;
      delete effects[ii];
    }
  }
};

int main()
{
  vector<Layer> layers;
  for(int i = 0; i < 10; i++)
  {
    layers.push_back(Layer());
    layers[i].effects.push_back(new derived());
    cout << i << endl;
  }
}

当我编译并运行这段代码时,我得到以下输出:

0
called effect deleter
1
called effect deleter
called effect deleter

我很困惑。为什么它只打印 0 和 1 而不是 0 到 9?如果我删除 Layer 析构函数,我会不会有内存泄漏?处理这种情况的正确方法是什么?

【问题讨论】:

  • Layer 没有关注Rule of 3/5/0,因此当layers 增加其容量并且必须复制现有Layer 实例时,它可能会导致问题

标签: c++ c++11 vector memory-management c++-standard-library


【解决方案1】:

如果base *b,但*b实际上是derived,那么delete b;是即时UB,因为base::~base()不是virtual。本质上,您尝试删除*bbase 部分(由*b 的类型引导,即base),但是,因为析构函数不是虚拟的,所以您忘记销毁derived首先部分。这会导致可怕的事情发生(可能是某种堆栈损坏?)。修复它:

struct base {
    virtual ~base() = default;
};

另外,Layer::Layer(Layer const&amp;)(隐式定义的复制构造函数)被破坏了,因为它复制了参数中的base* 指针。当std::vector&lt;Layer&gt; 需要调整其存储大小时,将调用此复制构造函数,这需要分配一块新的连续内存并从旧内存中移动构造新的Layers,然后销毁旧内存。除了 a)Layer 没有移动构造函数(用户声明的析构函数阻止它的生成),所以“移动”Layers 只是复制它们,并且 b)复制 Layers 在概念上是有缺陷的,因为当一个Layer 被销毁,它将delete 其所有bases,然后另一个Layer 稍后将再次尝试删除它们。禁用Layer复制并写入其移动。

struct Layer {
    std::vector<base*> effects;
    Layer() = default;
    Layer(Layer const&) = delete;
    Layer(Layer&&) = default;
    Layer &operator=(Layer const&) = delete;
    Layer &operator=(Layer &&other) {
        std::swap(this->effects, other.effects);
        // what used to be this->effects will be deleted when other is destroyed
        return *this;
    }

    ~Layer() {
        for(int ii = 0; ii < effects.size(); ii++) {
            std::cout << "called effect deleter\n"; // endl is usually unnecessary, and "\n" is portable
            delete effects[ii];
        }
    }
};

道德是:使用智能指针:)。

struct effect_deleter {
    void operator()(base *b) {
        std::cout << "called effect deleter\n";
        delete b;
    }
};
struct Layer {
    std::vector<std::unique_ptr<base, effect_deleter>> effects;
    // 3 constructors, 2 operator=s, and 1 destructor
    // all doing the right thing, "for free"
    // "Rule of 5" becomes "Rule of 0"
};

int main() {
    std::vector<Layer> layers;
    for(int i = 0; i < 10; i++) {
        layers.emplace_back(); // using push_back(Layer()) constructs a temporary Layer, then constructs the actual Layer in the vector by moving from the temporary; emplace_back just passes the arguments (here nothing) on to the constructor of Layer
        layers[i].effects.emplace_back(new derived()); // similar
        std::cout << i << "\n";
    }
}

【讨论】:

    【解决方案2】:

    您不能在 c++ 中将其作为直向量执行 - 不同的派生类具有不同的大小,因此,当复制构造函数将元素放入数组中时,您会得到切片(经典对象切片)。

    正确执行此操作最不痛苦的方法是使用shared_ptr 的向量到基类。也可以使用 unique_ptr 的数组,但您必须非常小心,因为诸如基于范围的 for 循环之类的东西可以将数据的所有权转移到临时迭代变量。

    【讨论】:

    • 好的,但是 OP 知道这一点并且正在使用指针向量。切片不是正在发生的事情。这里还有其他问题。
    • 声明析构函数为虚拟
    • @jscmerge 这是一回事。不过,这个问题是破碎的万花筒。这并不能解决所有问题。
    猜你喜欢
    • 1970-01-01
    • 2020-02-11
    • 1970-01-01
    • 2016-07-03
    • 1970-01-01
    • 1970-01-01
    • 2011-11-30
    • 1970-01-01
    • 2021-07-27
    相关资源
    最近更新 更多