我最近一直在尝试重载
new
和 delete
运算符,我注意到一些非常有趣的事情。
当我分配时,比如说,类
T
与 new T();
与 new T[1]()
相比,分配块的大小存在差异。差别就是sizeof(size_t)
。
然后我想到了这个传奇问题:“
delete
运算符如何知道它要删除的块的大小?”对于单个元素,这很容易,但是对于数组,则不然。
传说对于数组来说,大小放在第一个元素之前。所以我提取了它,一切都会好起来的,但我注意到,当类没有析构函数时,这不起作用。对于
int
、char
等原始类型也是如此。
不仅如此(没有析构函数),重载的
delete[]
运算符不会显示块的正确大小,但不知何故不会导致内存泄漏。
所以,我的问题是:
为什么析构函数对于修复数组分配和纠正重载的
delete[]
运算符中的大小如此重要?
重载的
delete[]
运算符中的错误大小怎么可能不会导致内存泄漏?
是否可以在没有析构函数的情况下从原始类型或类的数组中提取已分配实例的数量?
感谢上帝,我检查过 -> 此问题仅出现在 x86 和 x64 上的 VISUAL STUDIO 2019 中 编译器版本192930147
例如:https://www.onlinegdb.com/online_c++_compiler没有此行为。
!!!但是,从原语中提取仍然失败,所以这 3 个问题仍然存在!!!
问题代码:
#include <iostream>
#include <cstring>
using namespace std;
#define log(x) cout << #x << " = " << x << '\n';
class T_with_destructor
{
long long a;
double b;
public:
~T_with_destructor() {}
void* operator new (size_t s)
{
cout << "new -> "; log(s);
return malloc(s);
}
void* operator new[](size_t s)
{
cout << "new [] -> "; log(s);
return malloc(s);
}
void operator delete(void* ptr, size_t s)
{
cout << "delete -> "; log(s);
free(ptr);
}
void operator delete[](void* ptr, size_t s)
{
cout << "delete[] -> "; log(s);
free(ptr);
}
};
class T_without_destructor
{
long long a;
double b;
public:
//~T_without_destructor() {}
void* operator new (size_t s)
{
cout << "new -> "; log(s);
return malloc(s);
}
void* operator new[](size_t s)
{
cout << "new [] -> "; log(s);
return malloc(s);
}
void operator delete(void* ptr, size_t s)
{
cout << "delete -> "; log(s);
free(ptr);
}
void operator delete[](void* ptr, size_t s)
{
cout << "delete[] -> "; log(s);
free(ptr);
}
};
int main()
{
const size_t size = 3;
{
T_with_destructor* arr = new T_with_destructor[size]();
size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
cout << *size_from_arr << '\n';
delete[] arr;
}
cout << "\n\n";
{
T_without_destructor* arr = new T_without_destructor[size]();
size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
cout << *size_from_arr << '\n';
delete[] arr;
}
cout << "\n\n";
{
int* arr = new int[size]();
size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
cout << *size_from_arr << '\n';
delete[] arr;
}
cout << "\n\n";
return 0;
}
内存泄漏没有发生 -> prints -> new [] -> s = 160 delete[] -> s = 16
int main()
{
for(int i=0; i<1000000000; i++)
{
T_without_destructor* arr = new T_without_destructor[10]();
size_t* size_from_arr = (size_t*)((char*)arr - sizeof(size_t));
cout << *size_from_arr << '\n';
delete[] arr;
}
return 0;
}
那么,我是不是无意中发现了VS编译器的一个bug?
- 为什么析构函数对于修复数组分配和纠正重载的 delete[] 运算符中的大小如此重要?
编译器必须将隐式
this
指针传递给类的每个成员函数,包括析构函数。因此,当您的类有析构函数时,编译器会告诉您块的大小,以便您(知道类的大小)可以计算出每个数组成员的 this
指针的地址,并显式调用以下每个成员他们。
- 重载的delete[]运算符中不正确的大小怎么可能不会导致内存泄漏?
因为这不是用于释放块的大小。块的实际大小由操作系统存储并与块地址相关联。
给予
delete[]
运算符的块的大小是无关紧要的,因为您不必调用任何析构函数。给你不正确大小的特定 MSVC 编译器可能从未将其与块一起存储(因为它是不必要的),而只是给了你一个垃圾值。
- 是否可以在没有析构函数的情况下从原始类型或类的数组中提取已分配实例的数量?
根据这个答案,不同的操作系统有不同的方法从指针中提取块的大小。您已经知道类/基元的大小,因此您应该能够计算出数组的长度。