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 |...
+---+---+---+--
^