【问题标题】:What is the correct implementation of erase function in vector class矢量类中擦除功能的正确实现是什么
【发布时间】:2012-03-30 06:42:48
【问题描述】:

我正在尝试在不使用迭代器的情况下实现我自己的矢量类版本。这是问题可能需要的部分。

template <typename T>
class Vector {
public:
    ...

    ~Vector()
    {
        delete [] m_data;
    }

    ...

    void erase(size_t position)
    {
        if (position >= m_size) {
            throw std::out_of_range("erasing an element out of bounds");
        }
        --m_size;
        for (size_t i = position; i < m_size; ++i) {
            m_data[i] = m_data[i + 1];
        }
        m_data[m_size].T::~T();
    }

    ...

private:
    T* m_data;
    size_t m_size;
    ...
};

以下是cplusplus.comerase 函数std::vector 的引用:

这通过删除元素的数量有效地减少了向量的大小,在之前调用每个元素的析构函数。

所以我尝试通过调用最后一个重复元素的析构函数来实现相同的功能。 m_data[position] 的析构函数是不必要的,因为它将被下一个元素替换。

问题是向量类delete [] m_data的析构函数中的代码也会为每个元素调用析构函数,这会导致内存的双重删除和崩溃。

谁能帮助为我的矢量类编写正确的擦除函数?

【问题讨论】:

  • 你根本不应该使用array-new。相反,像std::vector 一样,分配原始内存并就地构造对象。
  • @KerrekSB 我尝试实现用于处理堆内存的简单版本以及使用它的函数的正确实现。该类是为学习目的而编写的,应该使用带有 new/delete 的内存分配/释放。我正在使用保留内存,每次达到最大值时都会翻倍。
  • 那又怎样?关键是array-new 构造对象,如果你想自己控制对象构造,那很可能不是你想要的。
  • @KerrekSB 你是对的。我只是第一次阅读时没有得到你的想法。因此,我还提出了分配和使用原始内存并在使用new/delete 的地方构造对象的解决方案。谢谢。

标签: c++


【解决方案1】:

你正在尝试的东西不会那么容易飞起来!如果你想实现像std::vector&lt;T&gt; 这样的东西,你需要做完整的事情:你需要处理原始内存并显式地构造/销毁对象。也就是说,你需要分配足够大的未初始化内存,根据需要在合适的位置构造/销毁对象,最终释放分配的内存。对于std::vector&lt;T&gt; 的玩具版本,这是一个有趣的练习,然后您会很乐意使用随编译器一起发布的版本,因为它以某种方式设法更快,实际上实现了所有功能,并且相当无错误。当然,如果您碰巧实现了标准 C++ 库的一个版本,您将需要经历整个练习。好消息是:std::vector&lt;T&gt;std::deque&lt;T&gt; 相比是微不足道的高效,您还需要相当复杂的算法版本;我不确定是否有许多实现实际上可以在std::deque&lt;T&gt; 上执行专门的版本。

不使用迭代器,顺便说一句,只是没有帮助:像std::move()(将迭代器作为参数的版本)或std::copy()(如果你不使用C++2011)这样的算法避免乱扔你的代码重复的版本。将代码包含在算法中还有一个额外的优势,即它们并非完全是微不足道的逻辑,可以根据需要很好地封装。将重复需要的代码放入算法中,使容器的实现相对简单,从而使实现的正确率更高。 ...更不用说实现有趣的优化实际上也是可行的。

【讨论】:

  • 当然,我知道标准容器、迭代器和算法的所有优点,并且每天都在使用它们。我不是要替换它们或增强它们。我现在想做的就是做一些实验并编写我自己的简单版本的矢量类,它将涵盖基本功能。
【解决方案2】:

你想要的是制作字符数组。

然后您可以使用placement new 将元素放入向量中,并在删除元素时显式调用析构函数。

   void push_back(T const& el)
   {
       makeSureThereIsSpaceForOneMore();
       new (&E[m_size]) T(el);
       ++m_size;
   }
   void erase(size_t index)
   {
       MoveStuffAround();
       E(index)->~T();
       --m_size;
   }

private:
    char* m_data;
    size_t m_size;
    ...

    T* E(size_t index){return reinterpret_cast<T*>(&m_data[index * sizeof(T)]);}
};

【讨论】:

  • 我认为保留char * 而不是T* 只会使代码复杂化,因此每个函数都需要进行强制转换。这实际上并不能解决问题,因为无论如何都会发生双重删除。
  • @ArtakBegnazaryan:它是如何实现 std::vector 的。这是唯一真正的方法。为什么不能解决双重删除?
  • 如果我们保留T* m_data 而不是char* m_data,代码将如下所示:void push_back(const T&amp; el) { makeSureThereIsSpaceForMore(); new (&amp;m_data[m_size]) T(el); ++m_size; } void erase(size_t index) { MoveStaffAround(); m_data[m_size].T::~T(); --m_size; } 这比char * 更简单。您在回答中没有提到的另一个关键点是我们应该使用 C 风格的 malloc/free 内存分配来保留/释放内存。
  • 上面的转换很容易,因此不需要通过编写获取特定元素的方法来进行强制转换。所以你认为你的代码更整洁的论点是错误的。我不喜欢将对 malloc() 和 free() 的调用添加到 C++ 代码库中的想法。许多构建系统已经检查并拒绝了此类代码,因为除非您非常小心,否则在同一应用程序中混合使用 new/delete malloc/free 会导致问题。 但是这是一种选择。
【解决方案3】:

@Artak 不要忘记检查另一个方向(擦除时)

【讨论】:

  • 其他方向是什么意思? positionsize_t 类型,它是一个无符号整数,因此保证为 &gt;= 0
【解决方案4】:

m_data[m_size] = T(); 替换m_data[m_size].T::~T(); 怎么样?

【讨论】:

    【解决方案5】:

    我想出了一个解决方案,不使用new/delete 运算符,而是使用C 风格的malloc/free。这是因为 newdelete 运算符隐式调用构造函数和析构函数,而您无法控制它们。相反,如果我们使用malloc/free,那么我们可以在需要时显式调用对象的构造函数/析构函数。

    所以在这个例子中,我们可以为erase函数中的最后一个对象显式调用析构函数,也可以为向量类的析构函数中的每个元素显式调用析构函数。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-08
      • 2021-06-24
      • 2010-10-05
      • 1970-01-01
      • 2014-10-17
      • 2018-06-08
      相关资源
      最近更新 更多