[英]How does C++ free the memory when a constructor throws an exception and a custom new is used
我看到以下结构:
如果X
构造函数抛出, new X
将释放内存。
operator new()
可以重载。
运算符 new 重载的规范定义是void *operator new(size_t c, heap h)
和相应的operator delete
。
最常见的 operator new 重载是placement new,即void *operator new(void *p) { return p; }
void *operator new(void *p) { return p; }
您几乎总是不能在给定new
的指针上调用delete
。
这导致了一个问题:当X
构造函数抛出并使用重载的new
时,如何清理内存?
从根本上说,如果没有对应于new
操作符的delete
操作符,那么什么都不做。 在放置新的情况下也不做任何事情,因为相应的放置删除操作符是空操作。 异常没有转移:它继续它的进程,所以 new 的调用者有机会(和责任)释放分配的内存。
之所以称为 Placement new,是因为它用于将对象放置在以其他方式获取的内存中; 由于内存不是由 new 操作符获取的,因此它不太可能被删除操作符释放。 实际上,这个问题没有实际意义,因为(至少从 C++03 开始)不允许替换放置 new 运算符(它具有原型operator new(size_t, void*)
或 delete ( operator delete(void*, void*)
). 提供的placement new 操作符返回它的第二个参数,并且提供的placement delete 操作符是一个空操作。
其他new
和delete
运算符可能会被替换,无论是全局的还是特定的类。 如果调用自定义的new
运算符,并且构造函数抛出异常,并且存在相应的delete
运算符,则将在传播异常之前调用该 delete 运算符进行清理。 但是,如果没有相应的delete
运算符,则不会出错。
先举个例子:
#include <cstddef>
#include <iostream>
struct S
{
S(int i) { if(i > 42) throw "up"; }
static void* operator new(std::size_t s, int i, double d, char c)
{
std::cout << "allocated with arguments: "
<<i<<", "<<d<<", "<<c<<std::endl;
return new char[s];
}
static void operator delete(void* p, int i, double d, char c)
{
std::cout << "deallocated with arguments: "
<<i<<", "<<d<<", "<<c<<std::endl;
delete[] (char*)p;
}
static void operator delete(void* p)
{
std::cout << "deallocated w/o arguments"<<std::endl;
delete[] (char*)p;
}
};
int main()
{
auto p0 = new(1, 2.0, '3') S(42);
S* p1 = nullptr;
try
{
p1 = new(4, 5.0, '6') S(43);
}catch(const char* msg)
{
std::cout << "exception: "<<msg<<std::endl;
}
delete p1;
delete p0;
}
输出:
allocated with arguments: 1, 2, 3 allocated with arguments: 4, 5, 6 deallocated with arguments: 4, 5, 6 exception: up deallocated w/o arguments
运算符 new 重载的规范定义是
void *operator new(std::size_t, heap h)
我不明白这是如何规范的,因为它是不允许的:
好的,现在它是new
的有效放置形式:)
[basic.stc.dynamic.allocation]/1
分配函数应为类成员函数或全局函数; 如果分配函数在全局范围以外的命名空间范围中声明或在全局范围内声明为静态,则程序格式错误。 返回类型应为
void*
。 第一个参数的类型应为std::size_t
。 第一个参数不应有关联的默认参数。 第一个参数的值应解释为请求的分配大小。
[强调我的]
您可以重载要为new
的放置形式调用的分配函数,请参阅 [expr.new] (在 [basic.stc.dynamic.allocation] 中未明确允许非模板函数,但也不禁止)。 new(placement)
给出的放置在这里被推广到一个expression-list 。 特定新表达式的表达式列表中的每个表达式都作为附加参数传递给分配函数。 如果调用释放函数(例如,因为被调用的构造函数抛出异常),则将相同的参数加上前导void*
(分配函数的返回值)传递给释放函数。
[expr.new]/18 指出:
如果上面描述的对象初始化的任何部分因抛出异常而终止,已经为对象获得了存储空间,并且可以找到合适的释放函数,则调用释放函数释放正在构造对象的内存,之后异常继续在new-expression的上下文中传播。 如果找不到明确匹配的释放函数,则传播异常不会导致对象的内存被释放。 [注意:这适用于被调用的分配函数不分配内存的情况; 否则很可能导致内存泄漏。 —尾注]
和 /21
如果new 表达式调用释放函数,它将从分配函数调用返回的值作为
void*
类型的第一个参数传递。 如果调用放置解除分配函数,则传递与传递给放置分配函数相同的附加参数,即与使用 new-placement 语法指定的参数相同的参数。
和 /20
如果放置释放函数的声明具有相同数量的参数,并且在参数转换后,除第一个参数类型之外的所有参数类型都相同,则放置释放函数的声明与放置分配函数的声明匹配。 任何非布局解除分配函数都与非布局分配函数相匹配。 如果查找找到单个匹配的解除分配函数,则将调用该函数; 否则,不会调用任何释放函数。 如果查找找到了一个普通释放函数的双参数形式,并且该函数被视为放置释放函数,将被选为分配函数的匹配项,则程序格式错误。 [示例:
struct S { // Placement allocation function: static void* operator new(std::size_t, std::size_t); // Usual (non-placement) deallocation function: static void operator delete(void*, std::size_t); }; S* p = new (0) S; // ill-formed: non-placement deallocation function matches // placement allocation function
—结束示例]
回到[basic.stc.dynamic.deallocation]:
1 释放函数应为类成员函数或全局函数; 如果在全局范围以外的命名空间范围内声明释放函数或在全局范围内声明为静态,则程序格式错误。
2 每个解除分配函数应返回
void
且其第一个参数应为void*
。 一个释放函数可以有多个参数。
当构造函数抛出异常时,将调用匹配的删除。 不会为抛出的类调用析构函数,但成功调用其构造函数的类的任何组件都将调用其析构函数。
'placement new' 不是 new 的重载版本,而是 operator new 的一种变体,也是一种不能重载的变体。
See 可以在此处查看新运算符的列表以及有关重载它们如何工作的描述。
如果构造函数在使用placement new 时抛出异常,编译器就会知道使用了什么new 运算符并调用placement delete。
当作为new 表达式的一部分构造的对象的构造失败时,将调用相应的解除分配函数(如果有的话)。 例如
new X;
将使用以下一对分配/解除分配函数。
void * operator new(std::size_t);
void operator delete(void *);
同样,对于表单的新展示位置
new(&a) X;
将使用operator new
和operator delete
函数的放置版本。
void * operator new(std::size_t, void *);
void operator delete(void *, void *);
请注意,最后一个函数故意不执行任何操作。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.