【问题标题】:Getting size of dynamic C-style array vs. use of delete[]. Contradiction? [duplicate]获取动态 C 样式数组的大小与使用 delete[]。矛盾? [复制]
【发布时间】:2019-02-20 10:16:55
【问题描述】:

我到处读到,在 C++ 中,仅从指向该内存块的指针获取动态数组的大小是不可能的。

怎么可能没有办法仅从指针获取动态数组的大小,同时又可以释放仅在指针上使用delete [] 分配的所有内存,而无需需要指定数组大小吗?

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

我的推理有什么问题?

【问题讨论】:

  • 底层内存分配器知道它分配的块的大小,但没有返回该信息的标准函数。有时有非标准的 malloc 实现特定方式,但它们本质上是不可移植的。
  • 为什么这很有趣?使用std::vector,不用担心delete[]
  • @n.m.我没有得到告诉你“不要打扰”的答案。如果有人问一个问题,背后是有原因的。原因不一定是“实际原因”。可能是好奇,可能是我想用机器代码实现编译器,或者其他什么。即使我们假设一个人应该总是有实际理由来提出问题,我相信总是有可能提出至少一两个可以使用新知识的案例。在某些情况下,我不能或不想使用 STL。
  • "如果有人问一个问题,背后是有原因的。"这些理由不一定成立。如果你想学习如何有效地使用 C++,那么不要为 delete 和指针而烦恼是正确的。它们的级别太低了,这些低级别的零重要性的东西有数不胜数。试图全部学习它们是浪费时间。如果您处于无法使用std::vector 的情况(应该非常很少见),您可能需要查看一个典型的实现以了解它是如何工作的,以便复制其中的一些。
  • @n.m.我不认为 SO 仅限于“学习如何有效使用 C++”的人。否则语言律师标签将毫无用处。 有人必须处理所有这些“低级的事情”,那么为什么要问他们这么不合理呢?

标签: c++ arrays


【解决方案1】:

TL;DR 运算符delete[] 破坏对象并释放内存。销毁需要信息 N(“元素数量”)。解除分配需要信息 S(“分配的内存大小”)。 S 总是被存储并且可以被编译器扩展查询。只有在破坏对象需要调用析构函数时才存储 N。如果存储了 N,则存储位置取决于实现。


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

a) 销毁对象(如有必要,调用析构函数)和

b) 释放内存。

让我们首先讨论(de)allocation,它 被许多编译器(如 GCC)委派给 C 函数 mallocfree。函数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;
}

这个例子给你 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;
}

此代码为您提供 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;
}

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

答案就是垃圾——代码又是未定义的行为。

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

【讨论】:

  • 非常有趣的答案!我现在想知道malloc_usable_size 会给你第二个指针什么?如果它是正确的答案,这些信息会存储在哪里?
  • 你可以试试这个,但这是未定义的行为。对于 gcc,new 基于malloc,它可以工作。但是,如果您考虑一个不可破坏类型的数组(第一个示例),ptr 肯定不会指向 malloc 分配的内存的开头(因为开头的隐藏字段) .因此,我希望 malloc_usable_size 不起作用。
  • 精彩的答案!因此,要点是: * 信息 N(“元素数”)可能不会被存储(仅在具有析构函数的动态数组的情况下)。因此,如果需要,我们需要手动跟踪长度。 * 相反,总是存储的是 S(“分配的内存大小”)。 (可以查询 S,但结果取决于实现)。 * delete[] 只需要 S 来释放内存(将所有内容都视为 blob)。它只需要 N 就知道调用析构函数的次数。 * 如果 N 被存储,它的存储位置取决于实现。对吗?
  • @Michele Piccolini:是的,非常好。
  • @Michele Piccolini:你喜欢这样吗?
【解决方案2】:

它确实 - 分配器,或者它背后的一些实现细节,确切地知道块的大小。

但该信息并未提供给您或您程序的“代码层”。

可以设计语言来做到这一点吗?当然!这可能是“不要为你不使用的东西付费”的例子——记住这些信息是你的责任。毕竟,知道你需要多少内存!很多时候,人们不希望在调用堆栈中传递一个数字的成本,而大多数时候,他们不需要它。

一些特定于平台的“扩展”可以满足您的需求,例如 Linux 上的 malloc_usable_size 和 Windows 上的 _msize,尽管这些假设您的分配器使用了 malloc并且没有做任何其他可能在最低级别扩展分配块大小的魔法。如果你真的需要它,我会说你最好还是自己跟踪它……或者使用向量。

【讨论】:

  • @MaxLanghof “如果我必须跟踪它,那么我必须在某个时候支付额外费用” 关键字是如果
  • @MaxLanghof 诚然,我很想知道是否有一些关键原因导致这不是标准库分配接口的一部分,超出了“我们 cba 并且不认为你应该需要这个”
  • @MichelePiccolini 您评论中的第一个问题与手头的主题无关 - 如果您已经知道,那么delete[]指向数组的指针是正确的数组和能够推断指针指向数组还是单个对象是两个不同的、不相关的东西。换句话说,如果有一个运算符来推断delete[] 使用的大小,如果用于非数组分配,它肯定会有UB,因此它不允许您决定指针是指向数组分配还是不是。
  • @MichelePiccolini 你不应该真的使用deletedelete[] 在所有,虽然;这是容器和智能指针的内部,它们确实很好地隐藏了所有这些信息。
  • 它可能不知道确切,它可能只知道它选择的块的大小,这可能会告诉它大小四舍五入到下一个 16 字节的倍数,例如例子。当然取决于分配器。 xD,@Handy999 在我发表评论之前已经将此作为答案发布。
【解决方案3】:

我认为这是三个因素共同作用的原因。

  1. C++ 有“只为使用的东西付费”的文化
  2. C++ 最初是作为 C 的预处理器,因此必须构建在 C 提供的之上。
  3. C++ 是最广泛移植的语言之一。不太可能添加使现有端口难以为继的功能。

C 允许程序员在不指定要释放的内存块大小的情况下释放内存块,但不为程序员提供任何访问分配大小的标准方法。此外,实际分配的内存量可能远远大于程序员要求的内存量。

遵循“你只为你使用的东西付费”的原则,C++ 实现对不同的类型以不同的方式实现new[]。通常,它们仅在必要时才存储大小,通常是因为该类型具有重要的析构函数。

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

【讨论】:

    【解决方案4】:

    此答案仅适用于 Microsoft Visual Studio。

    有一个函数叫做_msize,它会返回一个指针的malloced / calloced / realloced大小。

    可以在malloc.h头文件中找到,参数为:

    size_t _msize(
       void *memblock
    );
    

    我不确定 gcc 中是否有等价物。应该有。

    【讨论】:

    • 啊,有用于 linux 的 malloc_usable_size()。
    • 您知道这些是否可靠,或者可能会出现哪些情况并可能使这些不可靠的使用?
    • 我在测试它时注意到的一件事是,我在一个 25 长的字符数组上调用了 malloc,然后在数组上调用了 malloc_usable_size。它报告使用了 40 个字节。这向我表明,它实际上报告了分配的容器大小,该大小足以包含数组。
    【解决方案5】:

    如果delete[] 在调用它时不必知道数组的大小,那么您的整个论点就会分崩离析。并且delete[] 在调用时不必知道数组的大小。它只需要知道块的大小就可以让其他人使用,绝对不需要它在调用delete[]时让其他人可以使用。

    例如,delete[] 我将一个大块切成若干小块。这些块中的每一个只需要一个指向知道大小的控制块的指针。如果除了控制块之外的任何块首先传递给delete[],那么delete[] 不知道刚刚释放的块有多大,直到以后才知道。

    delete[] 知道块在其生命周期内每个任意点的大小并不是绝对必要的,这足以使您的论点无效。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-12-26
      • 2017-11-06
      • 2013-10-22
      • 2010-12-21
      • 1970-01-01
      • 2012-08-16
      相关资源
      最近更新 更多