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

Joh*_*ane 21 c++ memory-management c++14

我试图理解Stroustrup的"The C++ Programming Language"(第282页)中的以下段落(重点是我的):

要解除分配new,delete和delete []分配的空间,必须能够确定分配的对象的大小.这意味着使用new的标准实现分配的对象将占用比静态对象稍多的空间.至少需要空间来保持物体的大小.通常每个分配有两个或更多个单词用于免费商店管理.大多数现代机器使用8字节字.当我们分配许多对象或大对象时,这种开销并不重要,但是如果我们在免费商店中分配大量小对象(例如,int或Points)则可能很重要.

请注意,作者没有在上面突出显示的句子中区分对象是否是数组.

但是根据C++ 14中的段落§5.3.4/ 11,我们(我的重点):

当new-expression调用分配函数并且尚未扩展分配时,new-expression将请求的空间量作为std :: size_t类型的第一个参数传递给分配函数.该参数不得小于正在创建的对象的大小; 当对象是数组时,它可能大于正在创建的对象的大小.

我可能会遗漏一些东西,但在我看来,我们在这两个陈述中都存在矛盾.据我所知,所需的额外空间仅用于数组对象,并且这个额外的空间将保存数组中的元素数,而不是数组的大小(以字节为单位).

Yak*_*ont 22

如果调用new类型T,operator new则可以调用的重载将完全传递sizeof(T).

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

这大致是new(和mallocdo)的大多数标准实现.

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


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

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


现在,在数组的情况下,C++编译器可以自由调用operator new[],请求的内存量大于分配sizeof(T)*n时的内存量T[n].这是由编译器在请求重载内存时生成的new(非operator new)代码完成的.

这通常是在具有非平凡析构函数的类型上完成的,这样C++运行时可以在delete[]调用时迭代每个项并调用.~T()它们.它引出了一个类似的技巧,n在它使用的数组之前填充到内存中,然后执行指针算法以在删除时提取它.

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

对于没有析构函数的东西(比如char)或者一些微不足道的东西(比如struct foo{ ~foo()=default; },n运行时不需要它,所以它不需要存储它.所以它可以说"naw,我不会存储它".

这是一个实例.

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';
}
Run Code Online (Sandbox Code Playgroud)

如果以0参数运行,则打印出来11,96/8 = 12 R(0)然后11.

第一个是分配的元素数量,第二个是分配了多少内存(最多可以增加11个元素,加上8个字节 - sizeof(size_t)我怀疑),最后一个是我们恰好在数组开始之前找到的内容11个元素(a size_t11).

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

如果将其更改112,则调用delete[]将实际删除错误数量的元素.

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

或者,理论上,编译器可以构建单独的指针 - >大小映射.

我不知道有哪些编译器会执行这些操作,但是两者都不会感到惊讶.

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

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

  • @SergeyA [除此之外](http://coliru.stacked-crooked.com/a/b35020d1134cbe20).通过在我分配的数组开始之前访问1来查看11我在底部打印?这是我分配的元素数量.见96,= 11*8 + 8?这就是`new`的要求.额外的8个字节.这恰好等于数组中的元素数量. (6认同)
  • @JohnKalane没有强制要求在该额外存储中存储数组中的元素数量.它恰好是编译器实际上做的事情.问题是在`delete []`上,编译器获取指向第一个元素的指针,并且需要知道如何销毁每个元素.有许多方法可以解决这个问题(例如,一个单独的表),但最简单的方法是在缓冲区之前存储元素数量*.然后,当删除时,您知道类型和大小,因此可以迭代每个元素.标准*不强制要求*,但*它允许*. (5认同)

Dav*_*rtz 21

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

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

  • 那么实现如何知道调用析构函数的次数?它无法从分配器获取大小,因为分配器没有实现"从分配器获取大小"功能.哎呀,分配器可能会将它存储的大小向上舍入到最接近的128KB. (7认同)
  • @JohnKalane:但那些要点只涉及`new []`的*interface*,即来自外部的视图,来自客户端代码.关于存储元素数量的所有讨论都是关于`new []`的*implementation*. (2认同)