delete []如何知道它是一个数组?

问题描述 投票:133回答:16

好吧,我想我们都同意以下代码所发生的事情是未定义的,具体取决于传递的内容,

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)。

那么他们的工作是记住这些东西吗?操作系统是否在后台保留某种类型的记录? (我的意思是,我意识到我开始这篇文章时说过发生的事情是未定义的,但事实是,'杀戮狂欢'的情况不会发生,所以因此在实际世界中有人记得。)

c++ arrays pointers new-operator delete-operator
16个回答
98
投票

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

编译器必须跟踪需要以某种方式删除的对象数。它可以通过过度分配来存储数组大小来实现。有关更多详细信息,请参阅C++ Super FAQ


1
投票

从语义上讲,C ++中的两个版本的delete运算符都可以“吃掉”任何指针;但是,如果指向单个对象的指针被赋予delete[],则会产生UB,这意味着可能发生任何事情,包括系统崩溃或根本没有。

C ++要求程序员根据释放的主题选择正确版本的delete运算符:数组或单个对象。

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


1
投票

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

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

有关分配额外存储的完整规范,请参阅C ++ ABI(如何实现编译器):Itanium C++ ABI: Array Operator new Cookies


0
投票

您不能对数组使用delete,也不能将delete []用于非数组。


0
投票

“未定义的行为”仅仅意味着语言规范不会对将要发生的事情产生任何影响。它并不意味着会发生一些不好的事情。

那么他们的工作是记住这些东西吗?操作系统是否在后台保留某种类型的记录? (我的意思是,我意识到我开始这篇文章时说过发生的事情是未定义的,但事实是,'杀戮狂欢'的情况不会发生,所以因此在实际世界中有人记得。)

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

通常,内存管理器将记住(除其他外)分配的内存块的大小。这可能比C ++实现要求的块大。通常,内存管理器会在分配的内存块之前存储它的元数据。

C ++实现通常只记住数组的大小,如果它需要这样做是为了它自己的目的,通常是因为类型有一个非trival析构函数。

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

free(p)

另一方面,对于具有非平凡析构函数的类型,“删除”和“删除[]”可能会有所不同。 “删除”将是类似的东西(其中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);

-1
投票

嘿,这取决于你在类型或类/结构中分配构建数组时使用new []表达式分配的内容,并且你没有提供构造函数和析构函数,操作符会将其视为大小“sizeof(object)* numObjects“而不是对象数组,因此在这种情况下,分配对象的数量不会存储在任何地方,但是如果您分配对象数组并且在对象中提供构造函数和析构函数而不是行为更改,则新表达式将分配4个字节以及存储数量前4个字节的对象因此可以调用每个字节的析构函数,因此new []表达式将返回向前移动4个字节的指针,而当返回内存时,delete []表达式将首先调用函数模板,迭代通过对象数组并为每个对象调用析构函数。我已经创建了这个简单的代码,它会重载new []和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 );
}
///////////////////////////////////////////

-2
投票

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

delete pointer

delete [] pointer

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


-3
投票

答案:

int * pArray = new int [5];

int size = *(pArray-1);

上面发布的不正确并产生无效值。 “-1”计算元素在64位Windows操作系统上,正确的缓冲区大小位于Ptr - 4字节地址中


102
投票

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

对此的回答可以追溯到C ++作为C兼容语言的根源(它不再是真正的努力。)Stroustrup的理念是程序员不应该为他们没有使用的任何功能付费。如果他们不使用数组,那么他们不应该为每个分配的内存块承担对象数组的成本。

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

Foo* foo = new Foo;

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

由于只设置了数组分配来携带额外的数组大小信息,因此您需要告诉运行时库在删除对象时查找该信息。这就是我们需要使用的原因

delete[] bar;

而不仅仅是

delete bar;

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

对于我们大多数人(包括我自己)来说,关于几个额外字节的内存的烦恼现在看起来很古怪。但是仍然存在一些情况,即保存几个字节(从可能是非常多的内存块)可能很重要。


27
投票

是的,操作系统会在“背景”中保留一些内容。例如,如果你跑

int* num = new int[5];

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

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


13
投票

这与this问题非常相似,它有许多您正在寻找的细节。

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

通过执行以下代码,可以在某些实现中查看

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

9
投票

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

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


6
投票

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


5
投票

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

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

我已经知道,答案与类析构函数有关。

如果你分配一个MyClass类的实例...

classes = new MyClass[3];

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

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


3
投票

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

但是,运行时不够智能,无法检测它是否是数组或指针,您必须通知它,如果您错了,您要么不能正确删除(例如,ptr而不是数组),或者你最终得到一个不相关的大小值,并造成重大损害。


3
投票

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

示例如何完成:这里

int* i = new int[4];

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

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

4存储在第一个sizeof(int)字节中

*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)
© www.soinside.com 2019 - 2024. All rights reserved.