C++ 新建[]后删除:带或不带用户析构函数

C++ 新建[]后删除:带或不带用户析构函数,c++,destructor,heap-corruption,C++,Destructor,Heap Corruption,删除指向分配了new[]的数组的指针是“未定义的行为”。我只是好奇,如果定义了析构函数,为什么下面代码中的第一个delete会导致堆损坏,否则什么也不会发生(正如我谦卑地期望的那样)。使用Visual Studio 8和GNU GCC版本4.7.2()进行测试 UB不是语言范围内可以合理化的东西,至少没有工具链、优化级别、操作系统、体系结构和处理器的详细知识 不过,可以猜测:由于没有可调用的析构函数,编译器根本就不会生成任何使代码错误在现实世界中可见的代码。这样的析构函数调用可能会命中动态分配内

删除指向分配了
new[]
的数组的指针是“未定义的行为”。我只是好奇,如果定义了析构函数,为什么下面代码中的第一个
delete
会导致堆损坏,否则什么也不会发生(正如我谦卑地期望的那样)。使用Visual Studio 8和GNU GCC版本4.7.2()进行测试


UB不是语言范围内可以合理化的东西,至少没有工具链、优化级别、操作系统、体系结构和处理器的详细知识


不过,可以猜测:由于没有可调用的析构函数,编译器根本就不会生成任何使代码错误在现实世界中可见的代码。这样的析构函数调用可能会命中动态分配内存块的头,而在单个
new a
示例中不存在该头。

不匹配
new
new[]
delete
是未定义的行为。真的,讨论到此结束


在那之后什么都可能发生。进一步分析是毫无意义的。

原因极有可能是,当您
new[]
一个类型不可破坏的数组时,实现会在从底层分配机制获得的块的开头存储数组的大小。它需要这样做,以便在您
delete[]

当它返回数组的第一个元素的地址时,它将与它分配的块的地址相差一定数量的字节,可能是
sizeof(size\u t)

作为一种节省内存的优化,对于不需要销毁的类型,它不会这样做

因此,
delete[]
将根据类型是否具有析构函数执行不同的操作。如果它这样做了,那么它将查看给定指针之前的字节,销毁那么多对象,减去偏移量并释放块。如果它没有,那么它将只释放它给出的地址。碰巧,“只释放地址”也是
delete
在没有析构函数的类型的情况下所做的,因此当您不匹配它们时,您就可以在这个实现中“避开”UB


现在,可能还有其他机制会产生与您看到的相同的结果,原则上,您可以检查这个特定版本的GCC的来源以确认。但是我很确定我记得GCC是这样做的,所以它可能仍然是这样。

从语言的角度来看,唯一重要的是它是未定义的行为。什么都可以。但这并不意味着你无法解释你的行为

首先要注意的是,关键的区别不是您提供了一个用户析构函数,而是析构函数(隐式定义的或用户定义的)不是微不足道的。基本上,析构函数是微不足道的,如果它根本不做任何事情(编译器不能认为用户提供的析构函数是微不足道的)。可能没有用户提供的析构函数,并且析构函数仍然是非平凡的情况包括子对象(成员或基)具有非平凡析构函数的任何情况

struct NonTrivialDestructor {
   std::string s;
};
struct NotTrivialEither : NonTrivialDestructor {};
话虽如此,两种情况的主要区别在于,在后一种情况下,编译器知道析构函数不会做任何事情,因此它知道调用或不调用析构函数都不会有什么区别

此语句的重要性在于,在析构函数执行(或可能执行)某些操作的情况下,编译器必须确保生成的代码根据需要调用尽可能多的析构函数。由于
new[]
返回的指针不包含分配了多少对象(因此需要销毁)的信息,因此需要在其他地方跟踪这些信息。最常见的方法是,编译器将生成分配一个额外的
size\t
对象的代码,它将值设置为对象数,并分配数组的其余部分,构造所有对象,等等。内存布局将是:

 v
 +------+---+---+---+--
 | size | T | T | T |...
 +------+---+---+---+--
        ^
new[]
返回的指针需要保留第一个对象的地址,这意味着即使基础分配器(
operator new
)返回指针
v
new[]
将在上图中返回
^

附加信息允许
delete[]
知道要调用多少构造函数,基本上执行以下操作:

pseudo-delete[](T * p) {
   size_t *size = reinterpret_cast<size_t*>(p)-1;
   for (size_t i = 0; i < *size; ++i) 
      (p+i)->~T();
   deallocate(size);
}
在这种情况下,从内存分配器获得的指针和
new[]
返回的指针位于同一位置,这意味着当计算
delete
时,用于取消分配的指针与用于分配的指针相同


需要注意的是,所有这些讨论都是关于实现细节的,这是标准中未定义的行为,一些实现可能会选择始终跟踪大小,或者使用不同的机制跟踪元素的数量,这里讨论的一切都是假的。您不能依赖此行为,但我希望它能帮助人们理解为什么标准要求匹配
new/delete
new[]/delete[]
。未定义的行为本质上是未定义的,可能意味着任何事情都有可能发生。@Joachim:虽然是真的,但机器中发生的每件事都是有原因的,即使它超出了语言的范围。UB不会神奇地关闭物理定律。这就是未定义行为的问题。该行为甚至可以在对编译器或运行库进行较小更新之间发生变化。要了解为什么某些未定义的行为似乎在一个编译器上工作,而在另一个编译器上不工作,或者为什么两个编译器的行为似乎相同(好或坏),您需要
pseudo-delete[](T * p) {
   size_t *size = reinterpret_cast<size_t*>(p)-1;
   for (size_t i = 0; i < *size; ++i) 
      (p+i)->~T();
   deallocate(size);
}
 v
 +---+---+---+--
 | T | T | T |...
 +---+---+---+--
 ^