获得动态C风格数组的大小与使用delete [].矛盾?

Mic*_*ini 36 c++ arrays heap

我到处都读到,在C++中,只能从指向那块内存的指针获取动态数组的大小.

怎么可能无法从指针获取动态数组的大小,同时可以通过delete []仅使用指针释放所有分配的内存,而无需指定数组尺寸?

delete []必须知道数组的大小,对吧?因此,此信息必须存在于某处.不应该吗?

我的推理有什么问题?

Han*_*999 29

TL; DR运算符会delete[]破坏对象并释放内存.破坏需要信息N("元素数").解除分配需要信息S("分配的存储器的大小").S始终存储,可以通过编译器扩展进行查询.只有在破坏对象需要调用析构函数时才存储N. 如果存储N,则存储它的位置取决于实现.


运营商delete []必须做两件事:

a)破坏对象(如果需要,调用析构函数)和

b)释放内存.

让我们首先讨论(de)分配,它被委托给C函数mallocfree许多编译器(如GCC).该函数malloc将要分配的字节数作为参数并返回指针.该函数free只需一个指针; 不需要字节数.这意味着内存分配函数必须跟踪已分配的字节数.可能有一个函数来查询已经分配了多少字节(在Linux中,这可以malloc_usable_size在Windows中完成_msize).这是不是你想要的,因为这并不能告诉你一个数组的大小,但分配的内存量.由于malloc不一定能为您提供与您要求的内存完全相同的内存,因此您无法从以下结果计算数组大小malloc_usable_size:

#include <iostream>
#include <malloc.h>

int main()
{
    std::cout << malloc_usable_size(malloc(42)) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

这个例子给你56,而不是42:http://cpp.sh/2wdm4

请注意,应用malloc_usable_size(或_msize)结果new是未定义的行为.

那么,我们现在讨论对象的构造破坏.在这里,您有两种删除方式:( delete对于单个对象)和delete[](对于数组).在非常旧的C++版本中,您必须将数组的大小传递给delete[]-operator.如你所述,如今,事实并非如此.编译器跟踪此信息.GCC在数组开始之前添加一个小字段,其中存储数组的大小,以便它知道必须调用析构函数的频率.您可以查询:

#include <iostream>

struct foo {
    char a;
    ~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

这段代码给你42:http://cpp.sh/7mbqq

仅适用于协议:这是未定义的行为,但使用当前版本的GCC可行.

所以,您可能会问自己为什么没有查询此信息的功能.答案是GCC并不总是存储这些信息.可能存在对象的破坏是无操作的情况(编译器能够解决这个问题).请考虑以下示例:

#include <iostream>

struct foo {
    char a;
    //~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

在这里,答案不再是42:http://cpp.sh/2rzfb

答案就是垃圾 - 代码再次是未定义的行为.

为什么?因为编译器不需要调用析构函数,所以它不需要存储信息.并且,是的,在这种情况下,编译器不会添加跟踪已创建对象的代码.只知道分配的字节数(可能是56,见上文).

  • 精彩的回答!因此,要点是:*可能不存储信息N("元素数")(仅在具有析构函数的动态数组的情况下).因此,如果需要,我们需要手动跟踪长度.*总是存储的是S("分配的内存大小").(可以查询S但结果依赖于实现).*`delete []`只需要S才能释放内存(将所有内容视为blob).它需要N只知道调用析构函数的次数.*如果存储了N,则存储它的位置取决于实现.正确? (3认同)

Lig*_*ica 26

它确实 - 分配器或其背后的一些实现细节确切地知道块的大小是什么.

但是,该信息不会提供给您或您的程序的"代码层".

该语言可以设计为这样做吗?当然!这可能是"不为你不使用的东西买单"的情况 - 你有责任记住这些信息.毕竟,知道你要求多少记忆!通常人们不希望数字的成本在调用堆栈中传递,而大多数时候,他们不需要它.

可能得到你想要的东西,像一些特定于平台的"扩展" malloc_usable_size在Linux和_msizeWindows上,虽然这些假设使用您的分配malloc并没有做任何其他的魔力,可以在延长分配块的大小最低级别.如果你确实需要它,或者使用矢量,我会说你自己跟踪它会更好.

  • @MaxLanghof _"如果我必须跟踪它,那么我必须在某些时候支付额外费用"_关键字是**如果**. (4认同)
  • @MaxLanghof不可否认,我会模糊地想知道是否有一些关键原因导致这不是stdlib的分配接口的一部分,超出了"我们cba并且不认为你应该需要这个" (4认同)
  • @MichelePiccolini你不应该真的使用`delete`或`delete []`_at all_; 这是容器和智能指针的内部,它确实将所有这些信息都隐藏起来. (2认同)
  • 它可能不知道*确切*,它可能只知道它所选择的块的大小,这可能会告诉它大小四舍五入到下一个16字节的倍数,例如.当然取决于分配器.在我评论之前,xD,@ Handy999已经将此作为答案发布. (2认同)

plu*_*ash 6

我认为其原因是三个因素的汇合.

  1. C++有一个"你只为你使用的东西付钱"的文化
  2. C++起初是C的预处理器,因此必须建立在C提供的基础之上.
  3. C++是最广泛移植的语言之一.使现有端口生活困难的功能不太可能被添加.

C允许程序员释放内存块而不指定内存块的大小来释放,但是不向程序员提供任何标准方式来访问分配的大小.此外,分配的实际内存量可能大于程序员要求的数量.

遵循"您只需为所使用的内容付费"的原则,C++实现new[]针对不同类型实现不同.通常,如果有必要,它们只存储大小,通常是因为类型具有非平凡的析构函数.

因此,虽然是,但存储了足够的信息来释放内存块,因此很难定义用于访问该信息的合理且可移植的API.根据数据类型和平台,实际请求的大小可能是可用的(对于C++实现必须存储它的类型),只有实际分配的大小可用(对于C++实现不必存储它的类型)底层内存管理器具有扩展以获取分配大小的平台,或者大小可能根本不可用(对于C++实现不必将其存储在不提供对来自信息的访问的平台上的类型)底层内存管理器).