我不确定我是否正确理解了 C++ 中的“大小释放”。
在 C++14 中,以下签名 was added到全局范围:
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
的删除函数(注意:在标准库的情况下,这可能很好,因为堆知道实际分配的大小,并且会完全获得它)。为了确认我们正在寻找当前范围内的大小,我静态添加了示例 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
,并找到“删除析构函数”。这是一个函数,它包装了我们通常认为的析构函数(因为它在 vtable 上,我们将找到它)用于 D
, 并调用 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(免费商店),它指出:
Class-specific deallocation function lookup is a part of general deallocation function lookup (5.3.5) and occurs as follows. If the delete-expression is used to deallocate a class object whose static type has a virtual destructor, the deallocation function is the one selected at the point of definition of the dynamic type’s virtual destructor (12.4). Otherwise, if the delete-expression is used to deallocate an object of class
T
or array thereof, the static and dynamic types of the object shall be identical and the deallocation function’s name is looked up in the scope ofT
. If this lookup fails to find the name, general deallocation function lookup (5.3.5) continues...
换句话说(我的解释是),当您为
B
定义虚拟析构函数时,您定义了一个隐式 operator delete
, 但通过调用 ::delete
,您实际上要求编译器忽略动态类型,而仅引用当前范围内的静态类型,其大小为 16 字节。你已经选择了一个删除函数,所以编译器不需要动态查找一个。同样,在 §5.3.5.9(删除)中:
When the keyword
delete
in a delete-expression is preceded by the unary::
operator, the deallocation function’s name is looked up in global scope. Otherwise, the lookup considers class-specific deallocation functions (12.5). If no class-specific deallocation function is found, the deallocation function’s name is looked up in global scope.
换一种说法,“你要求的是全局函数,所以我跳过了查找特定于类的函数的部分。”
有人可能会争辩说,MSVC 的行为也是有效的,因为通过所有这些,没有任何东西明确表明传递给删除函数的大小与函数本身不可避免地相关联。当然,MSVC 行为也使编码人员不必在未定义行为雷区中导航另一个地雷,因为编译器设法从某个地方获取实际正确的大小。然而,查看 GCC 发出的代码,在显式调用全局范围的删除函数时,要收集正确的大小是“困难的”。
关于c++ - C++ 中的大小释放:全局运算符 delete(void* ptr, std::size_t size) 的正确行为是什么,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46775187/