在 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 开销。几乎没有。 (还有比这更聪明的策略,这只是一个简化的例子)
new
和delete
可以共享这种超低开销的实现,因为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[] 有啥区别的主要内容,如果未能解决你的问题,请参考以下文章