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您自己的(或分配器),其使用了一些不同的记忆存储器(即不只是转发到另一个调用new或malloc等),你会发现自己想存储信息清理分配后,当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_t值11).
在数组启动之前访问内存自然是未定义的行为,但我这样做是为了在gcc/clang中公开一些实现细节.关键是他们确实要求额外的8个字节(如预测的那样),并且他们确实碰巧将值存储在11那里(数组的大小).
如果将其更改11为2,则调用delete[]将实际删除错误数量的元素.
其他解决方案(存储阵列的大小)自然是可能的.例如,如果您知道您没有调用重载,new并且您知道底层内存分配的详细信息,则可以重用其使用的数据来了解块大小以确定元素数量,从而节省额外size_t的内存.这需要知道您的底层分配器不会对您进行过度分配,并且它将已知偏移量使用的字节存储到数据指针.
或者,理论上,编译器可以构建单独的指针 - >大小映射.
我不知道有哪些编译器会执行这些操作,但是两者都不会感到惊讶.
允许这种技术是C++标准所讨论的.对于数组分配,允许编译器new(非operator new)代码请求operator new额外的内存.对于非数组分配,编译器new是不会允许问operator new了额外的内存,它必须要求的确切数额.(我相信内存分配合并可能有例外吗?)
如您所见,这两种情况不同.
Dav*_*rtz 21
这两件事之间没有矛盾.分配函数获取大小,并且几乎肯定必须分配更多,以便在调用释放函数时再次知道大小.
当分配具有非平凡析构函数的对象数组时,实现需要某种方式来知道在调用时调用析构函数的次数delete[].允许实现与数组一起分配一些额外的空间来存储这些附加信息,尽管不是每个实现都以这种方式工作.
| 归档时间: |
|
| 查看次数: |
1872 次 |
| 最近记录: |