Delete []怎么知道是数组?

好了,我想我们都同意下面的代码是未定义的,这取决于传递的代码,

void deleteForMe(int* pointer)
{
delete[] pointer;
}

指针可以是各种各样的东西,因此对它执行无条件 delete[]是未定义的。然而,让我们假设我们确实在传递一个数组指针,

int main()
{
int* arr = new int[5];
deleteForMe(arr);
return 0;
}

我的问题是,在这种情况下,指针 是一个数组,谁知道这一点?我的意思是,从语言/编译器的角度来看,它不知道 arr是一个数组指针还是一个指向单个 int 的指针。见鬼,它甚至不知道 arr是否是动态创建的。然而,如果我用以下方法代替,

int main()
{
int* num = new int(1);
deleteForMe(num);
return 0;
}

操作系统足够聪明,只删除一个 int,而不会通过删除超过该点的其余内存来进行某种类型的“杀戮狂欢”(相比之下,对于 strlen和一个未以 \0结尾的字符串——它将一直运行,直到它达到0)。

那么谁来记住这些事情呢?操作系统是否在后台保存某种类型的记录?(我的意思是,我意识到我在开始这篇文章的时候说发生了什么是未定义的,但事实是,“杀戮狂欢”的情节并没有发生,因此在现实世界中 某人是记忆

133726 次浏览

编译器不知道它是一个数组,它相信程序员。用 delete []删除一个指向单个 int的指针会导致未定义行为。您的第二个 main()示例是不安全的,即使它没有立即崩溃。

编译器必须跟踪有多少对象需要以某种方式删除。它可以通过过度分配来存储数组大小。有关详细信息,请参阅 超级常见问题解答

它不知道它是一个数组,这就是为什么您必须提供 delete[]而不是普通的旧 delete

这取决于负责内存分配的运行时,就像您可以使用 free 删除标准 C 中用 malloc 创建的数组一样。我认为每个编译器实现它的方式都不同。一种常见的方法是为数组大小分配额外的单元格。

然而,运行时并没有足够的智能来检测它是一个数组还是一个指针,你必须通知它,如果你错了,你要么没有正确删除(例如,ptr 而不是 array) ,或者你最终获得一个不相关的大小值并造成重大损失。

这是非常类似的 这个问题,它有许多细节,你正在寻找。

但是可以说,跟踪这些并不是操作系统的工作。实际上,跟踪数组大小的是运行时库或底层内存管理器。这通常是通过预先分配额外的内存并将数组的大小存储在该位置(大多数使用头节点)来完成的。

通过执行以下代码,在某些实现上可以看到这一点

int* pArray = new int[5];
int size = *(pArray-1);

是的,操作系统在“后台”中保留了一些内容

int* num = new int[5];

操作系统可以分配4个额外的字节,在分配的内存的前4个字节中存储分配的大小,并返回一个偏移指针(即,它分配内存空间1000到1024,但是指针返回点到1004,位置1000-1003存储分配的大小)。然后,当调用 delete 时,它可以查看传递给它的指针之前的4个字节,以查找分配的大小。

我相信还有其他方法可以跟踪分配的大小,但这只是一种选择。

到目前为止给出的答案似乎没有解决的一个问题是: 如果运行时库(实际上不是操作系统)可以跟踪数组中的内容数量,那么我们为什么还需要 delete[]语法呢?为什么不能使用单个 delete表单来处理所有删除操作?

这个问题的答案可以追溯到 C + + 作为 C 兼容语言的根源(它已经不再努力成为 C 兼容语言了)Stroustrup 的哲学是程序员不应该为他们没有使用的任何特性付费。如果它们不使用数组,那么它们就不必为每个分配的内存块承担对象数组的成本。

也就是说,如果您的代码只是这样做

Foo* foo = new Foo;

那么分配给 foo的内存空间不应该包括支持 Foo数组所需的额外开销。

由于只有数组分配被设置为携带额外的数组大小信息,因此您需要告诉运行时库在删除对象时查找该信息。所以我们需要

delete[] bar;

而不是仅仅

delete bar;

如果 bar 是指向数组的指针。

对于我们大多数人(包括我自己)来说,这些天来对于额外几个字节的内存的担忧似乎有些古怪。但是在某些情况下,节省几个字节(从可能非常多的内存块中)可能很重要。

不能对数组使用 删除,也不能对非数组使用 删除[]

deletedelete[]可能都会释放分配的内存(指向内存) ,但最大的区别是数组上的 delete不会调用数组中每个元素的析构函数。

无论如何,混合 new/new[]delete/delete[]可能是 UB。

我也有类似的问题。在 C 语言中,使用 malloc ()(或其他类似的函数)分配内存,并使用 free ()删除内存。只有一个 malloc () ,它只分配一定数量的字节。只有一个 free () ,它只是接受一个指针作为参数。

那么,为什么在 C 语言中你可以直接把指针交给 free,但是在 C + + 中你必须告诉它它是一个数组还是一个变量呢?

据我所知,答案与类析构函数有关。

如果分配类 MyClass 的实例..。

classes = new MyClass[3];

并删除它与删除,您可能只会得到的 MyClass 的第一个实例的析构函数调用。如果使用 delete [] ,则可以确保将对数组中的所有实例调用析构函数。

这是最重要的区别。如果只是使用标准类型(例如 int) ,就不会看到这个问题。另外,您应该记住,对 new []使用 delete 和对 new 使用 delete []的行为是未定义的——它可能在每个编译器/系统上的工作方式不同。

从语义上讲,C + + 中的两个版本的 delete 操作符都可以“吃掉”任何指针; 然而,如果一个指向单个对象的指针给了 delete[],那么就会产生 UB,这意味着任何事情都可能发生,包括系统崩溃或者什么都不发生。

C + + 要求程序员根据释放的主题选择删除操作符的正确版本: 数组还是单个对象。

如果编译器能够自动确定传递给 delete 操作符的指针是否是指针数组,那么在 C + + 中只有一个 delete 操作符,这对两种情况都足够了。

同意编译器不知道它是否是一个数组,这取决于程序员。

编译器有时会通过过度分配数组大小来跟踪需要删除的对象数量,但这并不总是必要的。

有关分配额外存储时的完整规范,请参考 C + + ABI (如何实现编译器) : Itanium C + + ABI: 数组运算符新 Cookies

编译器的一种方法是在 head 元素中分配更多的内存并存储元素的计数。

举例说明如何做到这一点: 给你

int* i = new int[4];

编译器将分配 sizeof (int) * 5个字节。

int *temp = malloc(sizeof(int)*5)

将以第一个 sizeof(int)字节存储 4

*temp = 4;

设置 i

i = temp + 1;

所以 i指向4个元素的数组,而不是5个。

还有

delete[] i;

将按以下方式处理

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)

答案是:

Int * pArray = new int [5] ;

Int size = * (pArray-1) ;

以上贴出的内容不正确,并产生无效值。 “-1”计数元素 在64位 WindowsOS 上,正确的缓冲区大小位于 Ptr-4字节地址中

这取决于你用新的[]表达式分配什么当你在类型或类/结构中分配构建数组时,如果你没有提供构造函数和析构函数操作符会把它当作大小“ sizeof (object) * numObjects”而不是对象数组因此在这种情况下,分配的对象数量不会存储在任何地方,然而,如果你分配对象数组并且你在你的对象中提供构造函数和析构函数而不是行为改变,新表达式会多分配4个字节并且在前4个字节中存储对象的数量,这样每个对象的析构函数都可以被调用,因此新[]表达式将返回指针向前移动4个字节,比返回内存时,删除[]表达式将首先调用一个函数模板,遍历对象数组并调用每个对象的析构函数。我已经创建了这个简单的代码,它重载了新的[]和 delete []表达式,并提供了一个模板函数来释放内存并在需要时调用每个对象的析构函数:

// overloaded new expression
void* operator new[]( size_t size )
{
// allocate 4 bytes more see comment below
int* ptr = (int*)malloc( size + 4 );


// set value stored at address to 0
// and shift pointer by 4 bytes to avoid situation that
// might arise where two memory blocks
// are adjacent and non-zero
*ptr = 0;
++ptr;


return ptr;
}
//////////////////////////////////////////


// overloaded delete expression
void static operator delete[]( void* ptr )
{
// decrement value of pointer to get the
// "Real Pointer Value"
int* realPtr = (int*)ptr;
--realPtr;


free( realPtr );
}
//////////////////////////////////////////


// Template used to call destructor if needed
// and call appropriate delete
template<class T>
void Deallocate( T* ptr )
{
int* instanceCount = (int*)ptr;
--instanceCount;


if(*instanceCount > 0) // if larger than 0 array is being deleted
{
// call destructor for each object
for(int i = 0; i < *instanceCount; i++)
{
ptr[i].~T();
}
// call delete passing instance count witch points
// to begin of array memory
::operator delete[]( instanceCount );
}
else
{
// single instance deleted call destructor
// and delete passing ptr
ptr->~T();
::operator delete[]( ptr );
}
}


// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)


// structure with constructor/ destructor
struct StructureOne
{
StructureOne():
someInt(0)
{}
~StructureOne()
{
someInt = 0;
}


int someInt;
};
//////////////////////////////


// structure without constructor/ destructor
struct StructureTwo
{
int someInt;
};
//////////////////////////////




void main(void)
{
const unsigned int numElements = 30;


StructureOne* structOne = nullptr;
StructureTwo* structTwo = nullptr;
int* basicType = nullptr;
size_t ArraySize = 0;


/**********************************************************************/
// basic type array


// place break point here and in new expression
// check size and compare it with size passed
// in to new expression size will be the same
ArraySize = sizeof( int ) * numElements;


// this will be treated as size rather than object array as there is no
// constructor and destructor. value assigned to basicType pointer
// will be the same as value of "++ptr" in new expression
basicType = MyNew int[numElements];


// Place break point in template function to see the behavior
// destructors will not be called and it will be treated as
// single instance of size equal to "sizeof( int ) * numElements"
MyDelete( basicType );


/**********************************************************************/
// structure without constructor and destructor array


// behavior will be the same as with basic type


// place break point here and in new expression
// check size and compare it with size passed
// in to new expression size will be the same
ArraySize = sizeof( StructureTwo ) * numElements;


// this will be treated as size rather than object array as there is no
// constructor and destructor value assigned to structTwo pointer
// will be the same as value of "++ptr" in new expression
structTwo = MyNew StructureTwo[numElements];


// Place break point in template function to see the behavior
// destructors will not be called and it will be treated as
// single instance of size equal to "sizeof( StructureTwo ) * numElements"
MyDelete( structTwo );


/**********************************************************************/
// structure with constructor and destructor array


// place break point check size and compare it with size passed in
// new expression size in expression will be larger by 4 bytes
ArraySize = sizeof( StructureOne ) * numElements;


// value assigned to "structOne pointer" will be different
// of "++ptr" in new expression  "shifted by another 4 bytes"
structOne = MyNew StructureOne[numElements];


// Place break point in template function to see the behavior
// destructors will be called for each array object
MyDelete( structOne );
}
///////////////////////////////////////////

只需在类中定义一个析构函数,并使用这两种语法执行代码

delete pointer


delete [] pointer

根据输出你可以找到解决方案

“未定义的行为”只是意味着语言规范不能保证会发生什么。这并不一定意味着坏事会发生。

那么谁来记住这些事情呢?操作系统是否在后台保存某种类型的记录?(我的意思是,我意识到我开始这篇文章时说发生了什么是未定义的,但事实是,“杀戮狂欢”的情节并没有发生,因此在现实世界中有人记得。)

这里通常有两层: 底层内存管理器和 C + + 实现。

大多数内存管理器都是为满足 C 语言的需求而设计的。在 C 语言中,“ free”函数不要求用户指定块的大小。因此,内存管理器将记住(除其他外)分配的内存块的大小。这可能比 C + + 实现要求的块大。通常,内存管理器将在分配的内存块之前存储它的元数据。

C + + 有一种“你只为你使用的东西付费”的文化。因此,C + + 实现通常只会在出于自己的目的需要时记住数组的大小,这通常是因为该类型具有非平凡析构函数。

因此,对于具有简单析构函数的类型,“ delete”和“ delete []”的实现通常是相同的。C + + 实现只是将指针传递给底层内存管理器。差不多

free(p)

另一方面,对于具有非平凡析构函数的类型,“ delete”和“ delete []”可能是不同的。“ delete”类似于(其中 T 是指针指向的类型)

p->~T();
free(p);

而“删除[]”的意思是。

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);