【问题标题】:Destructor issue with delete[]delete[] 的析构函数问题
【发布时间】:2018-06-26 18:04:50
【问题描述】:

对于我正在编写的程序,我编写了一个简单的数组包装类(想法是它应该是固定大小的。我知道我可以只使用 std::vectors) 删除数组时遇到问题。
这些是我的构造函数:

template <class T>
Array<T>::Array(size_t size): m_data(new T[size]), m_size(size)
{}
template <class T>
Array<T>::Array(const std::vector<T> &vec): m_size(vec.size())
{
    std::allocator<T> a;
    m_data = a.allocate(m_size);
    for(int i = 0; i<m_size; i++)
    {
        new (m_data+i) T(vec[i]);
    }
}

这是我的析构函数:

template <class T>
Array<T>::~Array()
{
    delete[] m_data;
}

我使用 valgrind 试图弄清楚发生了什么,但它没有帮助。

==20840== Invalid read of size 8
==20840==    at 0x10ABB8: sapph_dijkstra::Array<sapph_dijkstra::Node>::~Array() (in /home/sapphie/dijkstra/test)
==20840==    by 0x1091CF: sapph_dijkstra::MinHeap::~MinHeap() (minheap.h:40)
==20840==    by 0x109021: main (test.c:20)
==20840==  Address 0x5b21318 is 8 bytes before a block of size 400 alloc'd
==20840==    at 0x4C3017F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==20840==    by 0x109F99: __gnu_cxx::new_allocator<sapph_dijkstra::Node>::allocate(unsigned long, void const*) (new_allocator.h:111)
==20840==    by 0x10AA36: sapph_dijkstra::Array<sapph_dijkstra::Node>::Array(std::vector<sapph_dijkstra::Node, std::allocator<sapph_dijkstra::Node> > const&) (in /home/sapphie/dijkstra/test)
==20840==    by 0x10A232: sapph_dijkstra::MinHeap::MinHeap(std::vector<sapph_dijkstra::Node, std::allocator<sapph_dijkstra::Node> > const&) (in /home/sapphie/dijkstra/test)
==20840==    by 0x108FD1: main (test.c:20)
==22059== Invalid free() / delete / delete[] / realloc()
==22059==    at 0x4C3173B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==22059==    by 0x10ABA4: sapph_dijkstra::Array<sapph_dijkstra::Node>::~Array() (in /home/sapphie/dijkstra/test)
==22059==    by 0x1091CF: sapph_dijkstra::MinHeap::~MinHeap() (minheap.h:40)
==22059==    by 0x109021: main (test.c:20)

(我已经删除了一些相同的错误消息)
在我完成的打印中,我发现我分配的地址是0x5b21320,所以valgrind 显示的地址确实是在那之前的8 个字节。 但我不明白为什么。我似乎无法访问它。
我错过了什么琐碎的事情吗?
我意识到对于一个我可以只使用标准向量的问题,这可能是一个过于复杂的解决方案,我可能会改变它。但我现在主要是好奇。

【问题讨论】:

  • a.allocate(m_size); 是做什么的?
  • 这是在初始化任何东西之前为数组分配内存。这样,如果 T 没有默认构造函数,而只有一个复制构造函数,则此代码仍然有效(在我的情况下,我有一个默认构造函数没有意义的类)

标签: c++ memory valgrind new-operator delete-operator


【解决方案1】:

如果内存未使用 new[] 初始化,则无法使用 delete[] 删除。这是直接调用对象的析构函数正确的一种情况(非常非常非常少数情况):

template <class T>
Array<T>::~Array()
{
    for(size_t i = 0; i < m_size; i++) {
        m_data[i].~T();
    }
    allocator.deallocate(m_data);
}

您可能还应该进行一些其他更改:

  • 分配器对象应该是Array 对象的成员,而不是其构造函数中的本地对象。虽然大多数分配器不是有状态的,但有些是有状态的,并且必须仅在构造函数中创建分配器的本地副本意味着您的分配器将失去其状态。
  • 您应该就是否希望按照delete[] 销毁对象的相同顺序销毁对象做出行政决定。在您的情况下,您需要自己指定,通过更改析构函数中 for 循环的顺序。

这就是它的样子:

template <class T>
Array<T>::~Array()
{
    for(size_t i = 0; i < m_size; i++) {
        m_data[m_size - i - 1].~T();
    }
    allocator.deallocate(m_data);
}
  • 我还会确保您避免将int 用于涉及此数组大小的任何内容。有一个论点是更喜欢int64_t 而不是size_t(尽管这会破坏 STL 的行为方式,并且需要谨慎做出决定),但您绝对不想限制自己使用带符号的 32 位数或更小(int 在大多数环境中使用)。

关于不使用delete的选择:

delete 期望传递给它的指针是它自己的离散分配对象。使用placement-new 创建的对象不会以这种方式分配。考虑以下几点:

struct point {
    int32_t x, y;
};

int main() {
    char memory[1024];
    size_t size = 128;
    point * arr = reinterpret_cast<point*>(memory);
    for(size_t i = 0; i < size; i++) {
        new(memory + i) point();
    }
    for(size_t i = 0; i < size; i++) {
        arr[i].x = 5;
        arr[i].y = 10;
    }
    for(size_t i = 0; i < size; i++) {
        //undefined behavior, we're deleting objects that weren't allocated on the heap!
        delete (arr + (size - i - 1));
    }
}

在这个例子中,根本没有动态分配内存;所有使用的内存都分配在堆栈上。如果我们在这种情况下使用delete,我们将删除指向堆栈上内存的指针,并且可能发生任何数量的(未定义的)事情,最有可能包括使我们的程序崩溃。调用析构函数允许对象清理其组件对象,而无需执行(在这种情况下,不必要的)内存释放。

for(size_t i = 0; i < size; i++) {
    //Correct behavior, doesn't delete stack memory
    arr[size - i - 1].~point();
}

即使在此内存在堆上分配的情况下,我们仍然会删除代码未明确分配的指针。这是语言所禁止的,原因很容易理解:原始内存的单个释放会擦除整个分配的内存块,不需要释放单个块。

struct point {
    int32_t x, y;
};

int main() {
    char * memory = new char[1024];
    size_t size = 128;
    point * arr = reinterpret_cast<point*>(memory);
    for(size_t i = 0; i < size; i++) {
        new(memory + i) point();
    }
    for(size_t i = 0; i < size; i++) {
        arr[i].x = 5;
        arr[i].y = 10;
    }
    for(size_t i = 0; i < size; i++) {
        //Still UB: only the first object is a discrete allocation, the rest are part of
        //that original allocation. This attempts to deallocate the same chunk of memory 128 times
        //delete (arr + (size - i - 1));

        //This is correct
        arr[size - i - 1].~point();
    }
    //We do need to deallocate the original memory allocated, but we only do this once.
    delete[] memory;
}

【讨论】:

  • 这是一个非常有趣的答案。不过我想知道;你怎么不使用delete 而不是析构函数,因为它是用operator new 分配的?我现在明白为什么我不能使用delete[]了,但是析构函数有什么好处呢?
  • @sapphie 我添加了一个关于此的部分。
【解决方案2】:

您正在混合 newdelete[] 这是未定义的行为。 std::allocator::allocate 使用operator new,而不是operator new[],所以你必须调用delete

由于您混合了分配存储的方式,因此您需要跟踪分配存储的方式,以便正确解除分配。

您可以通过将std::vector 作为类的数据成员来避免所有这些。然后你的构造函数就变成了

template <class T>
Array<T>::Array(size_t size): m_data(size) {}

template <class T>
Array<T>::Array(const std::vector<T> &vec): m_data(vec) {}

而且你得到的好处是不必存储 size 成员,因为 vector 会为你做这件事。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-28
    • 2014-06-28
    • 2014-11-22
    • 2013-06-25
    • 2011-01-12
    • 1970-01-01
    • 2017-02-06
    • 2018-06-02
    相关资源
    最近更新 更多