在 C++ 中,数组分配的 new 和 new[] 之间有什么区别

Had*_*ira 4 c++ arrays compiler-construction memory-management dynamic-memory-allocation

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

在 Stackoverflow 上搜索,似乎在 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(),如果您分配的是单个 struct 与 malloc_array() 版本。至少在公共 API 级别。换句话说,分配几个字节或多个字节似乎都没有关系,记录分配大小信息不会产生任何开销。

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

在 C 中,如果你实现一个二叉树,节点将用 malloc 分配,用 free 解除分配,在 C++ 中用 new 和 delete。但是如果你在 C++ 中实现像 vector 类的东西,在 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);
}
Run Code Online (Sandbox Code Playgroud)

eer*_*ika 7

为什么在 C malloc/free 中可以分配解除分配两个单个“对象”

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

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

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

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

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

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

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

编译器不能足够聪明以在引擎盖下生成适当的代码吗

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

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

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


Moo*_*uck 5

malloc 的一些巧妙实现实际上并没有跟踪每个分配的大小(通过巧妙地使用四舍五入),因此它具有极低的空间开销。他们将分配一大块内存,并且只要开发人员分配 <64 字节,它就会只使用该块的下一个 64 字节,并在其他地方标记一个位来跟踪这个 64 字节的块现在正在使用中. 即使用户只想分配 1 个字节,它也会分发整个块。这意味着每个分配只有一个开销,因此每 8 个分配都有一个共享字节的开销。几乎没有。(还有比这更聪明的策略,这只是一个简化的例子)

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

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

C++ 为您提供了“始终有效,但更慢和更大”和“仅适用于一项,但更快和更小”之间的选择,因此程序可以尽可能快地运行。