在 C++ 中,数组分配的 new 和 new[] 有啥区别

Posted

技术标签:

【中文标题】在 C++ 中,数组分配的 new 和 new[] 有啥区别【英文标题】:In C++, what is the difference between new and new[] for array allocations在 C++ 中,数组分配的 new 和 new[] 有什么区别 【发布时间】:2021-03-28 14:39:45 【问题描述】:

我知道 C++ 中的 free 和 delete 之间的区别。但我从来不明白的一件事是,为什么在 C 中 malloc/free 可以分配取消分配单个“对象”和数组,但在 C++ 中,我们需要使用正确的 new/delete vs new[]/delete[] 对。

在 *** 上搜索,似乎在 C++ 中,new[] 分配了额外的内存来保存已分配数组的大小,而 new 仅将内存分配给对象本身。因此,您应该注意这种额外的开销。

如果上一段确实如此,那么 malloc/free 如何处理这个开销?或者他们只是接受这个开销?如果它在 C 中是可以容忍的,为什么不能在 C++ 中呢?

另一方面,如果不是因为内存开销,而是因为调用构造函数和析构函数,编译器就不能足够聪明地在后台生成适当的代码,让程序员只写 new/delete对于单个对象和对象数组?

我正在为一种语义类似于 C++ 的玩具语言编写编译器,似乎可以让编译器决定如何仅使用 new/delete 进行分配和取消分配,但由于 C++ 使用 new/delete和 new[]/delete[],也许有一个我现在没有看到的问题。也许与多态性和虚拟表有关?

如果您好奇,我的幼稚想法是简单地分配一个整数以及对象/数组,其中该整数是数组的大小,如果是对象,则为简单的 1。然后在调用delete的时候检查整数的值,如果是1就调用析构函数。如果它大于 1,则将调用析构函数的数组迭代到数组中的每个对象。正如我所说,它似乎有效,并且会让程序员只写 new/delete 而不是 new/delete vs new[]/delete。但话又说回来,也许有一个我没有看到的问题。

编辑部分:

经过一些回答,我决定尝试提供一些伪代码和更好的背景。

在 C 语言中,内存分配通常使用 malloc() 进行,而取消分配则使用 free()。如果您要分配单个 POD、单个结构或数组, malloc() 适合所有这些情况。如果要分配单个结构,则不需要 malloc() 的不同版本,而如果要分配数组,则不需要 malloc_array() 版本。至少在公共 API 级别。换句话说,似乎分配几个字节或多个字节都没有关系,记录分配大小信息不会有任何开销。

正如你们中的许多人(包括我自己)所知道的,new 和 delete 不仅仅是分配和取消分配内存。 new 分配内存并调用构造函数,delete 调用析构函数然后释放内存。但是在 C++ 中,您需要注意分配的只是单个对象还是对象数组。如果要分配数组,则需要使用 new[]/delete[] 对。

在 C 中,如果您实现二叉树,节点将使用 malloc 分配并使用 free 取消分配,而在 C++ 中使用 new 和 delete。但是,如果你在 C++ 中实现类似向量类的东西,在 C 中你仍然会使用 malloc/free,但现在在 C++ 中你需要使用 new[]/delete[](考虑一个没有太多黑魔法的理智实现) .

考虑以下由编译器执行的伪代码。在这个伪代码中,delete 函数以某种方式访问​​ malloc 内部并知道有多少字节,这反过来可以很容易地用于计算有多少对象。由于此删除实现使用 malloc 内部来了解分配了多少内存,因此理论上不应该有记账开销。

// ClassType is a meta type only know by the compiler
// it stores a class info such as name, size, constructors and so on
void *new(ClassType c) 
    // allocates memory with malloc. Malloc() do the storage bookkeeping
    // note that somehow malloc is allocating just a single object
    c *ptr = malloc(sizeof(c));

    // now, call the constructor of the requested class
    c.constructor(ptr);

    // return the new object
    return ptr;


void *new(ClassType c, size_t n) 
    c *ptr = malloc(sizeof(c) * n);

    // iterate over the array and construct each object
    for (i = 0; i < n; ++i) 
        c.constructor(ptr[i]);
    

    return ptr;


// this delete version seems to be able to de-allocate both single
// objects and arrays of objects with no overhead of bookkeeping because
// the bookkeeping is made by malloc/free. So I would need 
// just a new/delete pair instead of new/delete vs new[]/delete[]
// Why C++ doesn't use something like my proposed implementation? 
// What low-level details prohibits this implementation from working?
void delete(ClassType c, void *ptr) 
    // get raw information of how many bytes are used by ptr;
    size_t n = malloc_internals_get_size(ptr);

    // convert the number of bytes to number of objects in the array
    n = c.bytesToClassSize(n);

    c* castedPointer = (c*) ptr;

    // calls the destructor
    for (i = 0; i < n; ++i) 
        c.destructor(castedPointer[i]);
    

    // free memory chunk
    free(ptr);

【问题讨论】:

free 不会调用析构函数,delete 会。如果析构函数中存在副作用,则基本上是在添加一个很难找到的错误。 如果您查看malloc 声明,它接受以字节为单位的大小并返回void * 类型的未初始化内存块,free 稍后发布。相反,new 构造对象,delete 破坏它们,因此它需要知道它应该作用于数组的每个元素。它可以是数组感知的,但他们选择了这样的样板方法,我不知道为什么。 我知道这一点,但它没有解释为什么 free 可以同时处理单个“对象”和数组,但在 C++ 中我们需要删除/删除 []。如果我的问题对此主题不清楚,请帮助我改进它 因此,您应该意识到这种额外的开销。 new 可能会做很多事情。 malloc 也可以。或malloc 下方的任何内容。你可以请求 2 个字节并获得 4K 分配,如果这是内存源可以提供的。 你的“天真的想法”实现了delete[]。要实现delete,根本不需要该整数,因此开销较小 【参考方案1】:

为什么在 C 中 malloc/free 可以同时分配和释放单个“对象”

Malloc 不创建任何对象。它分配不包含任何对象的“原始内存”。相应地,free 不会破坏任何对象。 new 表达式创建对象,delete 销毁对象,而delete[] 销毁对象数组。

为了让语言实现知道delete[] 需要销毁多少对象,该数字必须存储在某个地方。为了让语言实现知道delete 需要销毁多少对象,该数字不需要需要存储在任何地方,因为它始终是一个。

存储数字不是免费的,存储未使用的数字是不必要的开销。存在不同形式的删除,以便语言实现可以销毁正确数量的对象,而无需存储非数组new创建的对象数量。

那么 malloc/free 如何处理这个开销?

malloc/free 没有这种开销,因为它不创建或销毁对象。因此,没有什么需要处理的。

在存储 malloc 确实需要处理的已分配字节数方面存在类似的问题。没有用于分配或释放单个字节的类似单独函数。这可能是因为这种用例可能很少见。 Malloc 有更聪明的方法来处理存储它,因为分配比需要更多的内存是不可观察的,而这种技巧对于对象的数量是不可能的,因为对象的创建和销毁是可观察的(至少在非平凡类型的情况下) .

new 通常通过在内部使用 malloc 来处理存储分配字节数的问题。

编译器不能足够聪明地在后台生成适当的代码

并非没有某种开销,不。有开销是的,它可以。

但话又说回来,也许有一个我没有看到的问题。

我不确定这是否是您没有看到的问题,但您的想法的问题是即使分配了单个对象也会分配整数的开销。

【讨论】:

您已经回答了标题,但问题的主体要复杂得多,您的回答并没有回答真正的问题,这就是为什么new 和@ 的非数组形式987654330@ @MooingDuck 在我看来,问题的核心问题是通过注意到delete[] 必须调用几个析构函数来回答的 @M.M:OP 知道这一点,因此需要delete[]。 OP 似乎想知道为什么有delete 不能 这样做。 “似乎想知道为什么有 delete 不能做到这一点”。这是正确的。我想知道这是什么原因【参考方案2】:

malloc 的一些巧妙实现实际上并没有跟踪每次分配的大小(通过巧妙地使用舍入),因此它具有极低的空间开销。他们将分配一大块内存,并且只要开发人员分配 bit 开销,因此每 8 个分配都有一个共享的 byte 开销。几乎没有。 (还有比这更聪明的策略,这只是一个简化的例子)

newdelete 可以共享这种超低开销的实现,因为delete 知道总是销毁一个对象,而不管它实际有多少空间。这又是超快的,而且空间开销低。

delete[] 不能这样做,因为它必须准确知道要调用多少个析构函数。所以它必须跟踪数组中有多少项目,最多 std::size_t,这意味着每个 new[] 必须添加大约 4 个字节。如果数据需要对齐 >4,那么每个分配还必须在计数和数组中的第一项之间浪费填充字节。并且delete[] 因此必须知道如何查看填充,找到计数,因此它确切知道要销毁多少对象。每次分配都需要时间和更多空间。

C++ 让您可以在“始终有效,但速度较慢且更大”和“仅适用于一项,但速度更快且更小”之间进行选择,因此程序可以尽可能快地运行。

【讨论】:

【参考方案3】:

并不是所有的实现在 new/delete 与 new[]/delete[] 之间都有区别,但它们存在是因为 new 表达式在数组的情况下表现不同。问题是 new 表达式不仅仅是对 new 运算符的调用,底层运算符可能是相同的:

#include <iostream>
// class-specific allocation functions
struct X 
    static void* operator new(std::size_t sz)
    
        std::cout << "custom new for size " << sz << '\n';
        return ::operator new(sz);
    
    static void* operator new[](std::size_t sz)
    
        std::cout << "custom new[] for size " << sz << '\n';
        return ::operator new(sz);
    
;
int main() 
     X* p1 = new X;
     delete p1;
     X* p2 = new X[10];
     delete[] p2;

但它们也可能重载到不同的。对于本机编译器来说,这可能是重要的区别,如果您正在编写解释器,或者如果您不希望用户修改 new\delete,您可以以不同的方式执行此操作。在 C++ 中,new 表达式在使用提供的函数分配内存后调用一个构造函数,new[] 表达式将调用几个并存储计数。当然,如果编译器像 Java 那样拥有面向对象的内存模型,那么数组将是一个具有大小属性​​的对象。单个对象只是一个实例的数组。正如你所说,我们不需要单独的删除表达式。

【讨论】:

那么,当这些运算符被提出时,也许有一些用例可以从“激进”的不同实现中受益?它可能是。但由于我只是一个普通的程序员,我从来没有遇到过这样的情况,我可以从不同的实现中受益 @HadleySiqueira 在专门的系统中,少数对象可能不如大长数组重要,但会对堆碎片整理造成威胁:您最终会失去分配大数组的能力。此外,由于操作系统的内存模型是如何工作的,至少在 MS 运行时中,afaik 对数组的 Windows 分配实现方式不同。 Java的内存模型是面向对象的,WM碎片整理和移动的东西,“指针”会有一个创建内存对象的ID。在某个级别上,Windows 内存模型的行为方式相同,应用程序“锁定”对象并接收其地址。

以上是关于在 C++ 中,数组分配的 new 和 new[] 有啥区别的主要内容,如果未能解决你的问题,请参考以下文章

c++中的new()申请二维数组

C++ 数组和“new”关键字

c++中用new给未知大小的数组分配空间怎么弄?

c++ 动态分配二维数组 new 二维数组

关于c++中new和delete

动态分配数组时的C ++ new vs new [] [重复]