内存分配的层面
c++ 内存分配相相关函数如下表所示。
分配 释放 类属 可否重载 malloc() free() C函数 x new delete C++表达式 x ::operator new() ::operator delete() C++函数 √ allocator ::allocate() allocator ::deallocate() C++标准库 可自由设计并搭配任何容器
那么我们应该如何使用这几个呢?下面给一段测试程序:
1
2
3
4
5
6
7
8
9
10
11
12
13void* p1 = malloc(512);
free(p1);
complex<int>* p2 = new complex<int>;
delete p2;
void* p3 = ::operator new(512);
::operator delete(p3);
//GNU下的allocator
allocator<int> alloc;
int* p4 = alloc.allocate(512);
alloc.deallocate(p4, 512);
operator new/operator delete
- new背后做的事
对于下面这行代码
1 | complex* p = new complex(1,2); |
编译器会帮我们转化成下面三个动作
1 | complex* p; |
而operator new的声明为
1 | void* operator new (std::size_t size) throw (std::bad_alloc); |
源码是
1 | //__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。 被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。 |
在effective c++也提到过,第一步如果用户自定义全局operator new,那么此函数可以接管默认operator new,但是用户尽量仿照默认版本的套路,要么不断请求新的空间,要么抛出异常,要么结束程序。
对应的内存释放程序则十分简单了
1 | void operator delete(void *memoryToBeDeallocated) noexcept |
注意以下几点:
(1)对于不是类类型(class、struct 或 union)的对象,将调用全局 delete 运算符。
(2)于类类型的对象,如果重载operator delete(),则在释放对象时默认调用重载版本,可以使用作用域运算符(::)置于delete之前,显示调用全局operator delete().
(3)delete运算符在释放对象之前会调用对象析构函数。
验证汇编代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using namespace std;
class A{
int x;
public:
A(int x):x(x){
cout << "A::ctor";
}
~A(){
cout << "A::dtor";
}
};
int main(){
A* p = new A(10);
delete(p);
return 0;
}
反汇编后:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 (gdb) disas main
Dump of assembler code for function main():
0x0000000000401550 <+0>: push rbp
0x0000000000401551 <+1>: push rsi
0x0000000000401552 <+2>: push rbx
0x0000000000401553 <+3>: mov rbp,rsp
0x0000000000401556 <+6>: sub rsp,0x30
0x000000000040155a <+10>: call 0x401720 <__main>
0x000000000040155f <+15>: mov ecx,0x4 ; new start
0x0000000000401564 <+20>: call 0x401648 <_Znwy>
0x0000000000401569 <+25>: mov rbx,rax
0x000000000040156c <+28>: mov edx,0xa
0x0000000000401571 <+33>: mov rcx,rbx
0x0000000000401574 <+36>: call 0x402d20 <A::A(int)>;构造函数
0x0000000000401579 <+41>: mov QWORD PTR [rbp-0x8],rbx
0x000000000040157d <+45>: mov rbx,QWORD PTR [rbp-0x8] ;new end
0x0000000000401581 <+49>: test rbx,rbx ; delete start
0x0000000000401584 <+52>: je 0x40159b <main()+75>
0x0000000000401586 <+54>: mov rcx,rbx
0x0000000000401589 <+57>: call 0x402d60 <A::~A()> ; 析构函数
0x000000000040158e <+62>: mov edx,0x4
0x0000000000401593 <+67>: mov rcx,rbx
0x0000000000401596 <+70>: call 0x401650 <_ZdlPvy>
=> 0x000000000040159b <+75>: mov eax,0x0 ;delete end
0x00000000004015a0 <+80>: jmp 0x4015bd <main()+109>
0x00000000004015a2 <+82>: mov rsi,rax
0x00000000004015a5 <+85>: mov edx,0x4
0x00000000004015aa <+90>: mov rcx,rbx
0x00000000004015ad <+93>: call 0x401650 <_ZdlPvy>
0x00000000004015b2 <+98>: mov rax,rsi
0x00000000004015b5 <+101>: mov rcx,rax
0x00000000004015b8 <+104>: call 0x402af0 <_Unwind_Resume>
0x00000000004015bd <+109>: add rsp,0x30
0x00000000004015c1 <+113>: pop rbx
0x00000000004015c2 <+114>: pop rsi
0x00000000004015c3 <+115>: pop rbp
0x00000000004015c4 <+116>: ret
End of assembler dump.
(gdb) info line 18
Line 18 of "main.cpp" starts address 0x40155f <main()+15>
and ends 0x40157d <main()+45>.
Line 19 of "main.cpp" starts address 0x40157d <main()+45>
and ends 0x40159b <main()+75>.
从汇编代码可以看出new首先申请一块内存然后调用构造函数,而delete先调用析构函数然后在释放内存。
array new/delete[]
测试代码:
1 |
|
查看内存布局
1 | (gdb) p p |
查看delete[]的汇编代码
1 | x0000000000401637 <+231>: je 0x40168f <main()+319> |
delete[]获得对象数组的地址后,向前8个字节获得数组长度然后从最后一个对象开始析构,完成后释放整个空间。
operator new/operator delete重载
当我们需要在new的时候增加一些特殊的动作,比如输出一些调试信息等,我们需要自己对operator进行重载。例如假如我们重写了下面这个版本,输出一些信息:
1 | void* operator new(size_t size,string& info)throw(){ |
看看汇编层面:
1 | (gdb) info line 26 |
placement new:
1 | 0x00000000004015fb <+65>: lea rax,[rbp-0x30] |
placement delete:
1 | 0x0000000000401631 <+119>: test rbx,rbx |
系统就会使用我们定义的版本进行内存分配。
既然默认new 和 delete工作的很好,问什么需要重载?
- 可以用来检测运用上的错误
- 可以提高效率,节省不必要的内存,提高回收和分配的速度(比如针对某一对象的内存池)
- 可以收集对内存使用的数据统计
- operator delete主要为了处理异常,一般情况下delete会调用全局operator delete释放内存。
placement new
placement new是重载operator new的一个标准、全局的版本,它不能被自定义的版本代替(不像普通的operator new和operator delete能够被替换成用户自定义的版本)。
它的原型如下:
1 | void *operator new( size_t, void *p ) throw() { |
它不能够被自定义的版本代替(不像普通版本的operator new和operator delete能够被替换)。如果你想在已经分配的内存中创建一个对象,使用new是不行的。也就是说placement new允许你在一个已经分配好的内存中(栈或堆中)构造一个新的对象。原型中void*p实际上就是指向一个已经分配好的内存缓冲区的的首地址。
我们为什么需要placement new?
1.用placement new 解决buffer的问题
问题描述:用new分配的数组缓冲时,由于调用了默认构造函数,因此执行效率上不佳。若没有默认构造函数则会发生编译时错误。如果你想在预分配的内存上创建对象,用缺省的new操作符是行不通的。要解决这个问题,你可以用placement new构造。它允许你构造一个新对象到预分配的内存上。
2.增大时空效率的问题
使用new操作符分配内存需要在堆中查找足够大的剩余空间,显然这个操作速度是很慢的,而且有可能出现无法分配内存的异常(空间不够)。placement new就可以解决这个问题。我们构造对象都是在一个预先准备好了的内存缓冲区中进行,不需要查找内存,内存分配的时间是常数;而且不会出现在程序运行中途出现内存不足的异常。所以,placement new非常适合那些对时间要求比较高,长时间运行不希望被打断的应用程序。
placement new使用步骤
第一步 缓存提前分配
有三种方式:
1.为了保证通过placement new使用的缓存区的memory alignment(内存队列)正确准备,使用普通的new来分配它:在堆上进行分配class Task。1
char * buff = new [sizeof(Task)]; //分配内存
(请注意auto或者static内存并非都正确地为每一个对象类型排列,所以,你将不能以placement new使用它们。)
2.在栈上进行分配
1
2class Task ;
char buf[N*sizeof(Task)]; //分配内存3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址)
1
void* buf = reinterpret_cast<void*> (0xF00F);
第二步:对象的分配
在刚才已分配的缓存区调用placement new来构造一个对象。
Task *ptask = new (buf) Task第三步:使用
按照普通方式使用分配的对象:
1
2
3ptask->memberfunction();
ptask-> member;
//...第四步:对象的析构
一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。按照下面的方式调用析构函数:
1
ptask->~Task(); //调用外在的析构函数
第五步:释放
你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)如果你不打算再次使用这个缓存,你可以象这样释放它:delete [] buf;