【问题标题】:How does delete[] know it's an array?delete[] 怎么知道它是一个数组?
【发布时间】:2010-10-16 18:20:32
【问题描述】:

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

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

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

【问题讨论】:

标签: c++ arrays pointers new-operator delete-operator


【解决方案1】:

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

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

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

Foo* foo = new Foo;

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

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

delete[] bar;

而不仅仅是

delete bar;

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

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

【讨论】:

  • “这些天对额外的几个字节内存的大惊小怪似乎很古怪”。幸运的是,对于这些人来说,裸数组也开始看起来很古怪,所以他们可以只使用向量或 boost::array,而永远忘记 delete[] :-)
【解决方案2】:

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

编译器确实必须跟踪有多少对象需要以某种方式删除。它可以通过过度分配足够的空间来存储数组大小来做到这一点。详情请见C++ Super FAQ

【讨论】:

【解决方案3】:

是的,操作系统会在“后台”保留一些内容。例如,如果你运行

int* num = new int[5];

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

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

【讨论】:

  • +1 - 一般有效点,但通常是语言运行时负责存储此元数据,而不是操作系统。
  • 数组的大小或定义了数组的对象的大小会发生什么变化?当您对该对象执行 sizeof 时,它会显示额外的 4 个字节吗?
  • 不,sizeof 只显示数组的大小。 如果运行时选择使用我描述的方法来实现它,这严格来说是一个实现细节,从用户的角度来看,应该被屏蔽。指针之前的内存不“属于”用户,也不被计算在内。
  • 更重要的是,sizeof 在任何情况下都不会返回动态分配数组的真实大小。它只能返回编译时已知的大小。
  • @Sam 虽然它可能在某些环境中工作,但它是一个实现细节,不应依赖它。我怀疑实际的数组长度无论如何都会被存储,它很可能是为支持数组而分配的内存块的大小。
【解决方案4】:

这与this 问题非常相似,其中包含您正在寻找的许多细节。

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

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

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

【讨论】:

  • 这行得通吗?在 windows 和 linux 中我们没有得到这个工作。
  • 改用size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)
【解决方案5】:

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

不管怎样,混合new/new[]delete/delete[]可能是UB。

【讨论】:

  • 清晰、简短且最有用的答案!
【解决方案6】:

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

【讨论】:

    【解决方案7】:

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

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

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

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

    classes = new MyClass[3];
    

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

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

    【讨论】:

    • 我不认为这对标准类型来说不是问题,因为它不仅仅是解构,而是释放内存。您最终会遇到元素 1+ 的内存泄漏。
    【解决方案8】:

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

    但是,runtime 不够聪明,无法检测它是数组还是指针,你必须通知它,如果你弄错了,你要么没有正确删除(例如,ptr 而不是数组),否则您最终会获得与大小无关的值并造成重大损害。

    【讨论】:

      【解决方案9】:

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

      如何做到这一点的示例: 这里

      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)
      

      【讨论】:

        【解决方案10】:

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

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

        如果编译器可以自动判断传递给删除操作符的指针是否为指针数组,那么在 C++ 中将只有一个删除操作符,这两种情况都足够了。

        【讨论】:

          【解决方案11】:

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

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

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

          【讨论】:

          • 我只希望每个编译器都能观察到一些记录的 C++ ABI。 +1 链接,我之前访问过。谢谢。
          【解决方案12】:

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

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

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

          大多数内存管理器都是为了满足 C 语言的需求而设计的。在 C 中,“免费”功能不需要用户指定块的大小。因此,内存管理器将记住(除其他外)分配的内存块的大小。这可能比 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);
          

          【讨论】:

            【解决方案13】:

            数组不能使用delete,非数组不能使用delete []

            【讨论】:

            • 我认为您的意思是不应该,因为您的普通编译器不会检测到滥用行为。
            【解决方案14】:

            嘿嘿,这取决于您在分配内置类型或类/结构的数组时使用 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 );
            }
            ///////////////////////////////////////////
            

            【讨论】:

              【解决方案15】:

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

              delete pointer
              
              delete [] pointer
              

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

              【讨论】:

              • 新建数组类型时使用 delete []。例如 int* a = new int; int* b = 新的 int[5];删除一个;删除[] b;
              【解决方案16】:

              答案:

              int* pArray = new int[5];

              int size = *(pArray-1);

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

              【讨论】:

                猜你喜欢
                • 2011-11-23
                • 1970-01-01
                • 2017-03-14
                • 1970-01-01
                • 2023-03-20
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多