delete[] 的析构函数问题

Destructor issue with delete[]

对于我正在编写的程序,我编写了一个简单的数组包装器 class(想法是它应该是固定大小的。我知道我可以只使用 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 个字节。 但我不明白为什么。我似乎无法访问它。
有什么我遗漏的小事吗?
我意识到对于我只能使用标准向量的问题,这可能是一个过于复杂的解决方案,我可能会改变它。但我现在主要是好奇。

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

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

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

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 成员,因为向量会为您存储。

如果内存未使用 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;
}