C++中的大小释放:全局运算符delete的正确行为是什么(void*ptr,std::size_t size)
Sized deallocation in C++: What is the correct behaviour of the global operator delete(void* ptr, std::size_t size)
我不确定我是否正确理解C++中的"大小释放"。在C++14中,以下签名被添加到全局范围中:
void operator delete(void* ptr, std::size_t size) noexcept
我使用GCC 7.1.0编译以下源代码:
#include <cstdio> // printf()
#include <cstdlib> // exit(),malloc(),free()
#include <new> // new(),delete()
void* operator new(std::size_t size)
{
std::printf("-> operator ::new(std::size_t %zu)n", size);
return malloc(size);
}
void operator delete(void* ptr) noexcept
{
std::printf("-> operator ::delete(void* %p)n", ptr);
free(ptr);
}
void operator delete(void* ptr, std::size_t size) noexcept
{
std::printf("-> operator ::delete(void* %p, size_t %zu)n", ptr, size);
free(ptr);
}
struct B
{
double d1;
void* operator new(std::size_t size)
{
std::printf("-> operator B::new(std::size_t %zu)n", size);
return malloc(size);
};
void operator delete(void* ptr, std::size_t size)
{
std::printf("-> operator B::delete(void* %p, size_t %zu)n", ptr, size);
free(ptr);
};
virtual ~B()
{
std::printf("-> B::~B()");
}
};
struct D : public B
{
double d2;
virtual ~D()
{
std::printf("-> D::~D()");
}
};
int main()
{
B *b21 = new B();
delete b21;
B *b22 = new D();
delete b22;
D *d21 = new D();
delete d21;
std::printf("*****************************n");
B *b11 = ::new B();
::delete b11;
B *b12 = ::new D();
::delete b12;
D *d11 = ::new D();
::delete d11;
return 0;
}
我得到以下输出:
-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x16e3010, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x16e3010, size_t 24)
MS Visual Studio 2017为我提供了以下输出:
-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0081CDE0, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 00808868, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 00808868, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0081CDE0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 00808868, size_t 24)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 00808868, size_t 24)
Clang 5.0甚至不调用全局大小的释放operator delete
(只调用带有一个参数的operator delete
)。正如评论部分提到的T.C.,Clang需要额外的参数-fsized-deallocation
来使用大小分配,结果将与GCC相同:
-> operator B::new(std::size_t 16)
-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 16)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 24)
-> operator B::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator B::delete(void* 0x219b6c0, size_t 24)
*****************************
-> operator ::new(std::size_t 16)
-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 16)
-> operator ::new(std::size_t 24)
-> D::~D()-> B::~B()-> operator ::delete(void* 0x219b6c0, size_t 24)
对我来说,VS2017似乎有正确的行为,因为我对类特定运算符的理解是使用派生类的大小,即使在基类指针上调用了delete。我希望通过调用全局operator delete
来实现对称行为。
我查阅了ISO C++11/14标准,但我认为我没有发现任何关于全球和类本地运营商应该如何表现的具体信息(这可能只是我在解释标准的措辞时遇到了问题,因为我不是母语人士)。
有人能详细谈谈这个话题吗?
正确的行为应该是什么?
我认为前缀为delete
运算符的双冒号运算符绕过了"正确"的operator delete()
。我已经在GCC、Clang和Intel的编译器上练习了代码,他们都同意delete
运算符应该发送16字节的大小。这是因为他们似乎将C++规范解释为明确要求全局范围的删除函数,而忽略了任何动态调度。稍后将对此进行详细介绍。
发生了什么首先,让我们稍微调整一下您的原始代码,以消除一些变量:
struct B
{
double d1;
virtual ~B() = default;
};
struct D : public B
{
double d2;
};
int main()
{
B *b01 = new D();
::delete b01; // 1: The "problem" case.
D *d01 = new D();
::delete d01; // 2: The "problem" case (sanity check).
B *b02 = ::new D();
delete b02; // 3: Typical deletion.
return 0;
}
实际上,不需要任何重写来表现这种行为。我们可以查看发射的程序集来了解发生了什么。默认情况下,GCC似乎使用大小为delete
的运算符,因此上面的内容很有趣(我使用GCC 11、-O0
编译)。正如您所注意到的,编译器将sizeof(*b01)
传递给删除函数:
mov rdx, QWORD PTR [rax]
sub rdx, 16
mov rdx, QWORD PTR [rdx]
lea rbx, [rax+rdx]
mov rdx, QWORD PTR [rax]
mov rdx, QWORD PTR [rdx]
mov rdi, rax
call rdx
mov esi, 16 // Passed as the size to delete().
mov rdi, rbx
call operator delete(void*, unsigned long)
从本质上讲,查找虚拟析构函数,调用它,然后调用大小为*b01
的delete函数(注意:在标准库的情况下,这可能很好,因为堆知道实际分配的大小,并且会完全获得它)。
为了确认我们正在静态地查找当前范围中的大小,我添加了示例2,它将sizeof(*d01)
发送到第二个参数:
call rdx
mov esi, 24 // Passed as the size to delete().
mov rdi, rbx
call operator delete(void*, unsigned long)
真正有趣的地方是在"正常"情况下,示例3:
mov rdx, QWORD PTR [rax]
add rdx, 8 // Offset 8 in the vtable for b02.
mov rdx, QWORD PTR [rdx]
mov rdi, rax
call rdx
在这里,它在vtable中查找b02
,并找到"删除析构函数"。这是一个函数,它包装了我们通常认为的D
的析构函数(因为它在vttable上,我们将找到它),并在该函数执行后调用delete
运算符。例如:
// (Prolog omitted.)
call D::~D() // [complete object destructor]
mov rax, QWORD PTR [rbp-8]
mov esi, 24
mov rdi, rax
call operator delete(void*, unsigned long)
因此,我们对析构函数进行了虚拟查找,运行了正确的析构函数,然后delete
运算符为它的第二个参数获取了24字节的大小。
c++规范的对正
如果我们看一下C++(在这种情况下是C++14)规范§12.5.4(免费存储),它指出:
类特定的释放函数查找是通用释放函数查找(5.3.5)的一部分,其发生方式如下。如果删除表达式用于解除分配静态类型具有虚拟析构函数的类对象,则解除分配函数是在定义动态类型的虚拟析构符时选择的函数(12.4)。否则,如果delete表达式用于解除分配类
T
或其数组的对象,对象的静态和动态类型应相同,并且在CCD_ 18的范围中查找解除分配函数的名称。如果此查找未能找到名称,则常规解除分配函数查找(5.3.5)将继续。。。
换句话说(我的解释是),当您为B
定义虚拟析构函数时,您定义了一个隐式operator delete
,但通过调用::delete
,您实际上要求编译器忽略动态类型,而只引用当前作用域中的静态型,该类型的大小为16字节您已经选择了一个删除函数,因此编译器不需要动态查找
同样,在§5.3.5.9(删除)中:
当删除表达式中的关键字
delete
前面有一元::
运算符时,将在全局范围中查找解除分配函数的名称。否则,查找会考虑类特定的解除分配函数(12.5)。如果找不到类特定的取消分配函数,则会在全局范围中查找取消分配函数的名称。
另一种说法是,"你要求全局函数,所以我跳过了查找类特定函数的部分。">
有人可能会争辩说,MSVC行为也是有效的,因为通过所有这些,没有任何明确声明传递给delete函数的大小与函数本身密不可分当然,同样,MSVC行为使程序员不必在未定义行为雷区中导航另一个地雷,因为编译器设法从某个地方获取了实际的正确大小。然而,查看GCC发出的代码,在显式调用全局范围的delete函数时,很难收集到正确的大小。
- C++,OpenCV,尝试显示图像时"OpenCV(4.3.0) Error: Assertion failed (size.width>0 && size.height>0)"此错误
- 大于65535的C++数组[size]引发不一致的溢出
- 为什么(-1)%vector::size()总是返回0
- 在for循环中使用auto vs decltype(vec.size())来处理字符串的向量
- CLANG 编译器 说:变量"PTR"可能未初始化
- 在以唯一ptr为值的C++映射中,动态内存何时会被销毁
- 循环中的条件:为什么每次都调用strlen(),而vector.size()只调用一次
- 将 ptr 传递给 ptr 到 A 作为参数传递给 A 的函数是不好的做法吗?
- 为什么这个 std::queue/指向结构的指针列表直到 List.Size() == 0 才释放内存?
- 在函数中使用 const int size 参数创建数组会在 Visual Studio 中抛出错误 C++:表达式的计
- vector.size() 在比较中意外工作
- vector.back() 和 vector[vector.size() - 1] 之间的区别?
- 为共享 ptr 向量实现复制 c'tor?
- 返回 str vs. str.substr(0,str.size()) 在 leetcode 中给了我不同的输出
- 字符和整数中 **(ptr+1) 的值差异
- 为什么 GCC 不能假设 std::vector::size 在这个循环中不会改变?
- C++:在不中断共享的情况下通过引用传递共享 PTR?
- 为什么"(!v.empty())"比"(v.size() >0)"好?
- C++中的大小释放:全局运算符delete的正确行为是什么(void*ptr,std::size_t size)
- 为什么 gcc 4.9.0 中没有定义"void operator delete(void* ptr, std::size_t size) noexcept;"?