Stroustrup本书与C ++标准之间的明显矛盾

时间:2015-11-02 20:11:34

标签: c++ memory-management c++14

我正在尝试理解Stroustrup的“The C ++ Programming Language”(第282页)中的以下段落(重点是我的):

  

要解除分配由new分配的空间,删除和删除[]必须能够   确定分配的对象的大小。这意味着一个   使用new标准实现分配的对象将占用   比静态对象略多的空间。 至少,空间是   需要保持对象的大小。通常每个两个或多个单词   分配用于免费商店管理。大多数现代机器   使用8字节的单词。我们分配时,这种开销并不重要   许多物体或大型物体,但如果我们分配批次就很重要   免费商店中的小物件(例如,商标或积分)。

请注意,作者不会在上面突出显示的句子中区分对象是否为数组。

但根据C ++ 14第5.2.4 / 11段,我们(我的重点):

  

当new-expression调用分配函数和分配时   没有扩展,new-expression传递了空间量   请求分配函数作为类型的第一个参数   的std ::为size_t。那个论点应该不小于   正在创建的对象;它可能大于对象的大小   如果对象是数组,则仅创建

我可能会遗漏一些东西,但在我看来,我们在这两个陈述中都存在矛盾。我的理解是,所需的额外空间仅用于数组对象,而这个额外的空间将保存数组中的元素数,而不是数组的字节数

6 个答案:

答案 0 :(得分:22)

如果您在new类型上致电T,则可能会调用的重载operator new将完全传递sizeof(T)

如果您实现了自己(或分配器)的new使用了一些不同的内存存储(即,不只是转发到另一个newmalloc等的调用,当delete发生时,您会发现自己想要存储信息以便稍后清理分配。执行此操作的典型方法是获取稍大的内存块,并在开始时存储所请求的内存量,然后在您获取的内存中返回指向以后的指针。

这大致是new(和malloc)的大多数标准实现。

因此,虽然您只需要sizeof(T)个字节来存储T,但new / malloc消耗的字节数超过sizeof(T)。这就是Stroustrup所说的:每个动态分配都有实际的开销,如果你进行大量的小分配,那么开销就会很大。

有些分配器在分配之前不需要额外的空间。例如,堆栈范围的分配器,在超出范围之前不会删除任何内容。或者从固定大小的块存储中分配并使用位域来描述正在使用的块。

这里,会计信息不存储在数据附近,或者我们使会计信息隐含在代码状态(作用域分配器)中。

现在,对于数组,当operator new[]sizeof(T)*n时,C ++编译器可以免费调用T[n],其内存请求大于 <{1}}。分配。这是由编译器在请求重载内存时生成的new(非operator new)代码完成的。

传统上对具有非平凡析构函数的类型执行此操作,以便C ++运行时可以在调用delete[]时迭代每个项并在其上调用.~T()。它推出了一个类似的技巧,其中n填充到它正在使用的数组之前的内存中,然后执行指针算法以在删除时提取它。

标准不是 ,但这是一种常见的技术(clang和gcc都至少在某些平台上做过,我相信MSVC也是如此)。需要一些计算阵列大小的方法;这只是其中之一。

对于没有析构函数(如char)或微不足道的内容(如struct foo{ ~foo()=default; }),运行时不需要n,因此不必存储它所以它可以说“naw,我不会存储它”。

Here is a live example

struct foo {
  static void* operator new[](std::size_t sz) {
    std::cout << sz << '/' << sizeof(foo) << '=' << sz/sizeof(foo) << "+ R(" << sz%sizeof(foo) << ")" << '\n';
    return malloc(sz);
  }
  static void operator delete[](void* ptr) {
    free(ptr);
  }
  virtual ~foo() {}
};

foo* test(std::size_t n) {
  std::cout << n << '\n';
  return new foo[n];
}

int main(int argc, char**argv) {
  foo* f = test( argc+10 );
  std::cout << *std::prev(reinterpret_cast<std::size_t*>(f)) << '\n';
}

如果使用0参数运行,则会打印出1196/8 = 12 R(0)11

第一个是分配的元素数量,第二个是分配了多少内存(最多可添加11个元素,加上8个字节 - 我怀疑是sizeof(size_t)),最后一个是我们发生的事情在11个元素的数组开始之前找到(size_t,其值为11)。

在数组启动之前访问内存自然是未定义的行为,但我这样做是为了在gcc / clang中公开一些实现细节。关键是他们做了要求额外的8个字节(如预测的那样),并且他们确实在那里存储了值11(数组的大小)。

如果您将11更改为2,则对delete[]的调用实际上会删除错误数量的元素。

其他解决方案(存储阵列的大小)自然是可能的。例如,如果您知道您没有调用new的重载并且您知道底层内存分配的详细信息,则可以重用使用的数据来了解您的块大小确定元素的数量,从而节省额外的size_t内存。这需要知道您的底层分配器不会对您进行过度分配,并且它将已知偏移量使用的字节存储到数据指针。

或者,理论上,编译器可以构建一个单独的指针 - &gt;大小的映射。

我没有意识到执行其中任何一项的编译器,但两者都不会感到惊讶。

允许这种技术是C ++标准所讨论的。对于数组分配,允许编译器的new(非operator new)代码向operator new询问额外内存。对于非数组分配,编译器的new 允许operator new询问额外内存,它必须要求确切的数量。 (我相信内存分配合并可能有例外吗?)

如您所见,这两种情况不同。

答案 1 :(得分:21)

这两件事之间没有矛盾。分配函数获取大小,并且几乎肯定必须分配更多,以便在调用释放函数时再次知道大小。

当分配了具有非平凡析构函数的对象数组时,实现需要某种方式来知道在调用delete[]时调用析构函数的次数。允许实现与数组一起分配一些额外的空间来存储这些附加信息,尽管不是每个实现都以这种方式工作。

答案 2 :(得分:3)

两段之间没有矛盾。

标准中的段落讨论了传递给分配函数的第一个参数的规则。

Stroustrup中的段落并没有专注于第一个类型为std :: size_t的参数,而是解释了分配本身是&#34;两个或多个单词&#34;比新指示的要大,每个程序员都应该知道。

Stroustrup的解释是更低级别,这是不同的。但没有矛盾。

答案 3 :(得分:3)

标准的引用是关于传递给operator new的值; Stroustrup的引用是在讨论operator new对该值的作用。这两个人几乎是独立的;要求仅仅是分配器分配至少所需的存储空间。分配器通常分配比请求的空间更多的空间。他们对这个额外空间的处理取决于实施;通常它只是填充。请注意,即使您仔细阅读了这些要求,分配器也必须分配所请求的确切字节数,因此在&#34;下可以分配更多,如同&#34;规则,因为没有便携式程序可以检测到实际分配了多少内存。

答案 4 :(得分:2)

我不确定两者都谈论同样的事情......

似乎Stroustrup正在讨论更通用的内存分配,它本身就使用额外的数据来管理空闲/分配的块。我认为他并没有谈到传递给new的大小的价值,而是在某个较低级别发生了什么。他可能会说:当你要求10个字节时,机器可能会使用略多于10个字节。 使用标准实施似乎在这里很重要。

虽然标准谈到了传递给函数的值。

一个讨论实施而另一个没有。

答案 5 :(得分:0)

没有矛盾,因为“恰好对象的大小”是“至少是对象大小”的一种可能实现。

42号至少为42。