在分配为不同类型的数组上使用 delete[] 是否安全?

Is it safe to use delete[] on an array that was allocated as a different type?

为了使用 placement new 而不是自动尝试调用默认构造函数,我使用 reinterpret_cast<Object*>(new char[num_elements * sizeof(Object)]) 而不是 new Object[num_elements] 分配数组.

但是,我不确定应该如何删除数组以便正确调用析构函数。我是否应该遍历元素,为每个元素手动调用析构函数,然后将数组转换为 char* 并在其上使用 delete[],如下所示:

for (size_t i = 0; i < num_elements; ++i) {
     array[i].~Object();
}
delete[] reinterpret_cast<char*>(array);

或者如果我不为每个元素手动调用析构函数就足够了,只是依靠 delete[] 来做到这一点,因为数组的类型是 Object*,比如 delete[] array?

我担心的是,并非每个平台都能够以这种方式正确确定数组中的元素数量,因为我没有分配使用大小合适的类型的数组。 An answer 关于 "how delete[] knows the size of the operand" 的问题表明 delete[] 的可能实现是存储分配元素的数量(而不是字节数)。

如果 delete[] 确实以这种方式实现,这表明仅使用 delete[] array 会尝试删除太多元素,因为创建数组时 char 元素多于有多少 Object 个元素适合它。所以在那种情况下,删除数组的唯一可靠方法是手动调用析构函数,将数组转换为 char*,然后使用 delete[].

然而,实现它的另一种合乎逻辑的方法是存储 数组的大小 以字节为单位,而不是元素的数量,然后在调用 delete[],用数组的大小除以类型的大小,得到调用析构函数的元素数量。如果使用此方法,则只需使用 delete[] array,其中 array 的类型为 Object* 就足够了。

所以我的问题是:如果数组最初没有分配正确的类型,我能否依靠 delete[] 正确调用操作数数组中元素的析构函数?


这是我使用的代码:

template <typename NumberType>
NeuronLayer<NumberType>::NeuronLayer(size_t num_inputs, size_t num_neurons, const NumberType *weights)
    : neurons(reinterpret_cast<Neuron<NumberType>*>(new char[num_neurons * sizeof(Neuron<NumberType>)])),
      num_neurons(num_neurons), num_weights(0) {
    for (size_t i = 0; i < num_neurons; ++i) {
        Neuron<NumberType> &neuron = neurons[i];
        new(&neuron) Neuron<NumberType>(num_inputs, weights + num_weights);
        num_weights += neuron.GetNumWeights();
    }
}

template <typename NumberType>
NeuronLayer<NumberType>::~NeuronLayer() {
    delete[] neurons;
}

template <typename NumberType>
NeuronLayer<NumberType>::~NeuronLayer() {
    for (size_t i = 0; i < num_neurons; ++i) {
        neurons[i].~Neuron();
    }
    delete[] reinterpret_cast<char*>(neurons);
}

这是管理动态对象数组的可移植代码。它本质上是 std::vector:

void * addr = ::operator new(sizeof(Object) * num_elements);
Object * p = static_cast<Object *>(addr);
for (std::size_t i = 0; i != num_elements; ++i)
{
    ::new (p + i) Object(/* some initializer */);
}

// ...

for (std::size_t i = 0; i != num_elements; ++i)
{
    std::size_t ri = num_elements - i - 1;
    (p + ri)->~Object();
}

::operator delete(addr);

这是一般模式,如果您想进行非常低级别的控制,您应该如何组织动态存储。结果是动态数组永远不应该是一种语言特性,而是在库中更好地实现。正如我上面所说,这段代码与名为 std::vector<Object>.

的现有标准库小工具几乎相同

Object* 上调用 delete[] 将为 new[] 分配的每个对象调用一次析构函数。 new Object[N] 通常在实际数组之前存储 N,并且 delete[] 当然知道去哪里查找。

您的代码不存储该计数。它不能,因为它是一个未指定的实现细节,其中以及如何存储计数。正如您推测的那样,有两种明显的方式:元素计数和数组大小,以及一种明显的位置(在数组之前)。即便如此,也可能存在对齐问题,并且您无法预测尺寸使用的是什么类型。

此外,new unsigned char[N] 是一个特例,因为 delete[] 不需要调用 char 的析构函数。在那种情况下 new[] 根本不需要存储 N。因此,即使 new Object[N] 会存储一个尺寸,您甚至无法确定存储的尺寸。