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


136

好吧,我想我们都同意,根据所传递的内容,未定义以下代码所发生的情况,

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

那么记住这些事情是谁的工作呢?操作系统是否在后台保留某种类型的记录?(我的意思是,我意识到我在开始这篇文章时首先说的是发生的事情是不确定的,但事实是,“杀人狂潮”的情况没有发生,因此在实际世界中有人会记住。)



6
它从删除后的方括号中知道
JoelFan 2015年

“指针是一个数组”。不,指针永远不是数组。它们通常指向数组的第一个元素,但这是另一回事。
亚伦·麦克戴德

Answers:


99

编译器不知道它是一个数组,而是在信任程序员。删除指向intwith 的指针delete []将导致未定义的行为。您的第二个main()示例是不安全的,即使它没有立即崩溃也是如此。

编译器必须跟踪需要删除多少个对象。它可以通过过度分配足够的空间来存储数组大小来实现此目的。有关更多详细信息,请参见C ++ Super FAQ


14
实际上,使用delete []删除使用new创建的内容是可以利用的。taossa.com/index.php/2007/01/03/…–
Rodrigo

23
@Rodrigo您评论中的链接已断开,但值得庆幸的是,回溯
David Gardner

103

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

对此的答案可以追溯到C ++的根源,即C兼容语言(它不再是真正的努力。)Stroustrup的哲学是程序员不必为未使用的任何功能付费。如果他们不使用数组,则不必为每个分配的内存块承担对象数组的开销。

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

Foo* foo = new Foo;

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

由于仅设置了数组分配来承载额外的数组大小信息,因此您需要告诉运行时库在删除对象时寻找该信息。这就是为什么我们需要使用

delete[] bar;

而不只是

delete bar;

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

对于我们大多数人(包括我本人)来说,如今对一些额外的内存字节的烦恼显得古怪。但是在某些情况下,保存一些字节(可能来自大量内存块)可能很重要。


20
“如今,对一些额外的内存字节的烦恼似乎很古怪”。幸运的是,对这类人来说,裸数组也开始显得古朴,因此他们可以使用vector或boost :: array,而永远忘记delete [] :-)
Steve Jessop

28

是的,操作系统将某些内容保留在“后台”中。例如,如果您运行

int* num = new int[5];

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

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


26
+1-通常是有效点,除了通常语言运行时负责存储此元数据,而不是操作系统负责。
sharptooth

数组的大小或定义了数组的对象的大小会怎样?在该对象上执行sizeof时,它是否显示额外的4个字节?
Shree

3
否,sizeof仅显示数组的大小。 如果运行时选择使用我描述的方法来实现它,那严格来说就是实现细节,从用户的角度来看,应该屏蔽掉它。指针之前的内存不属于用户,也不会被计数。
bsdfish 2011年

2
更重要的是,在任何情况下,sizeof都不会返回动态分配的数组的真实大小。它只能返回编译时已知的大小。
bdonlan

是否可以在for循环中使用此元数据来准确地遍历数组?例如 for(int i = 0; i < *(arrayPointer - 1); i++){ }
2014年

13

这与问题非常相似,并且包含您正在寻找的许多详细信息。

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

通过执行以下代码,这在某些实现中是可见的

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

这会工作吗?在Windows和Linux中,我们没有执行此操作。
好友

1
尝试size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)改用

9

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

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


1
清晰,简短,最有用的答案!
GntS

6

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


5

我对此有类似的问题。在C语言中,您可以使用malloc()(或另一个类似的函数)分配内存,并使用free()将其删除。只有一个malloc(),它仅分配一定数量的字节。只有一个free(),它仅将指针作为参数。

那么为什么在C语言中您只能将指针移交给free,而在C ++语言中则必须告诉它是数组还是单个变量?

我了解到,答案与类析构函数有关。

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

classes = new MyClass[3];

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

这是重要的区别。如果仅使用标准类型(例如int),则不会真正看到此问题。另外,您应该记住,在new []上使用delete和在new上使用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)

1

语义上,C ++中的两个版本的delete运算符都可以“吃掉”任何指针。但是,如果给指向单个对象的指针delete[],则将导致UB,这意味着可能发生任何事情,包括系统崩溃或什么都没有发生。

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

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


1

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

编译器有时会通过分配足够多的空间来存储数组大小来跟踪需要删除多少个对象,但这并不总是必要的。

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


我只希望每个编译器都能观察到一些 C ++的ABI文档。+1是我以前访问过的链接。谢谢。
唐·韦克菲尔德


0

“未定义的行为”只是意味着语言规范对即将发生的事情没有保证。这并不是从心意上说坏事会发生。

那么记住这些事情是谁的工作呢?操作系统是否在后台保留某种类型的记录?(我的意思是,我意识到我在开始这篇文章时首先说的是发生的事情是不确定的,但事实是,“杀人狂潮”的情况没有发生,因此在实际世界中有人会记住。)

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

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

通常,C ++实现仅出于其自身目的需要记住数组的大小,通常是因为该类型具有非琐碎的析构函数。

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

free(p)

另一方面,对于具有非平凡析构函数的类型,“ 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);

-1

遍历对象数组,并为每个对象调用析构函数。我创建了这个简单的代码,女巫重载了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

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


新建数组类型时,请使用delete []。例如int * a = new int; int * b =新的int [5]; 删除 delete [] b;
Lineesh K Mohan

-3

答案:

int * pArray =新的int [5];

int size = *(pArray-1);

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.