c++内存管理系列:new/operator new/placement new

内存分配的层面

  • 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
    13
    void* 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
2
3
4
5
6
7
8
9
10
complex* p;
try{
void* mem = operator new(sizeof(complex)); //申请内存
p = static_cast<complex*>(mem); // 类型转换
p -> complex::complex(1, 2); // 调用构造函数
}
catch(std::bad_alloc){
//若构造函数失败,回收资源
//若内存不够,重复获取直到有足够资源或者直接抛出异常
}

而operator new的声明为

1
void* operator new (std::size_t size) throw (std::bad_alloc);

源码是

1
2
3
4
5
6
7
8
9
10
11
12
//__cdecl 是C Declaration的缩写(declaration,声明),表示C语言默认的函数调用方法:所有参数从右到左依次入栈,这些参数由调用者清除,称为手动清栈。 被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。
void *__CRTDECL operator new(size_t size) throw (std::bad_alloc) {
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0) //申请空间
if (_callnewh(size) == 0) { //若申请失败则调用处理函数
// report no memory
static const std::bad_alloc nomem;
_RAISE(nomem); //#define _RAISE(x) ::std:: _Throw(x) 抛出nomem的异常
}
return (p);
}

在effective c++也提到过,第一步如果用户自定义全局operator new,那么此函数可以接管默认operator new,但是用户尽量仿照默认版本的套路,要么不断请求新的空间,要么抛出异常,要么结束程序。

对应的内存释放程序则十分简单了

1
2
3
4
void operator delete(void *memoryToBeDeallocated) noexcept
{
std::free(ptr);
}

注意以下几点:
(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
 #include <iostream>
#include <vector>
#include <complex>
#include <memory>
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 at address 0x40155f <main()+15>
and ends at 0x40157d <main()+45>.
Line 19 of "main.cpp" starts at address 0x40157d <main()+45>
and ends at 0x40159b <main()+75>.

从汇编代码可以看出new首先申请一块内存然后调用构造函数,而delete先调用析构函数然后在释放内存。

array new/delete[]

测试代码:

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
#include <iostream>
#include <vector>
#include <complex>
#include <memory>
#include <string>
using namespace std;
class A{
int x;
public:
A(int x):x(x){
cout << "A::ctor";
}
A(){
cout << "A::A()";
}
~A(){
cout << "A::dtor";
}
void setX(int x){
this -> x = x;
}
int getX(){
return x;
}
};

int main(){
A* p = new A[10];
for(int i = 0; i < 10; i++){
p[i].setX(i);
}
for(int i = 0; i < 10; i++){
cout << p[i].getX();
}
delete[] p;
return 0;
}

查看内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(gdb) p p
$2 = (A *) 0x6e1e68
(gdb) x/128xb 0x6e1e38
0x6e1e38: 0xab 0xab 0xab 0xab 0xab 0xab 0xab 0xab
0x6e1e40: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x6e1e48: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x6e1e50: 0xee 0xfe 0xee 0xfe 0xee 0xfe 0xee 0xfe
0x6e1e58: 0x97 0x1a 0x9c 0x1e 0xaf 0x57 0x00 0x3f
0x6e1e60: 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00 //数组长度10
0x6e1e68: 0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 //p[0],p[1]
0x6e1e70: 0x02 0x00 0x00 0x00 0x03 0x00 0x00 0x00
0x6e1e78: 0x04 0x00 0x00 0x00 0x05 0x00 0x00 0x00
0x6e1e80: 0x06 0x00 0x00 0x00 0x07 0x00 0x00 0x00
0x6e1e88: 0x08 0x00 0x00 0x00 0x09 0x00 0x00 0x00 //p[8],p[9]
//....

查看delete[]的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
x0000000000401637 <+231>:   je     0x40168f <main()+319>
0x0000000000401639 <+233>: mov rax,QWORD PTR [rbp-0x10];将数组首地址移入寄存器
0x000000000040163d <+237>: sub rax,0x8;向前8个字节
0x0000000000401641 <+241>: mov rax,QWORD PTR [rax];数组长度
0x0000000000401644 <+244>: lea rdx,[rax*4+0x0];终点地址
0x000000000040164c <+252>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000401650 <+256>: lea rbx,[rdx+rax*1]
0x0000000000401654 <+260>: cmp rbx,QWORD PTR [rbp-0x10]
0x0000000000401658 <+264>: je 0x401668 <main()+280>
0x000000000040165a <+266>: sub rbx,0x4 ;从最后一个开始析构,因此减sizeof(A) = 4
0x000000000040165e <+270>: mov rcx,rbx
0x0000000000401661 <+273>: call 0x402ed0 <A::~A()>
0x0000000000401666 <+278>: jmp 0x401654 <main()+260>;循环析构
0x0000000000401668 <+280>: mov rax,QWORD PTR [rbp-0x10]
0x000000000040166c <+284>: sub rax,0x8
0x0000000000401670 <+288>: mov rax,QWORD PTR [rax]
0x0000000000401673 <+291>: add rax,0x2
0x0000000000401677 <+295>: lea rdx,[rax*4+0x0]
0x000000000040167f <+303>: mov rax,QWORD PTR [rbp-0x10]
0x0000000000401683 <+307>: sub rax,0x8
0x0000000000401687 <+311>: mov rcx,rax
0x000000000040168a <+314>: call 0x401780 <_ZdaPvy>
0x000000000040168f <+319>: lea rcx,[rip+0x298a] # 0x404020 <_ZStL6ignore+23>

delete[]获得对象数组的地址后,向前8个字节获得数组长度然后从最后一个对象开始析构,完成后释放整个空间。

operator new/operator delete重载

当我们需要在new的时候增加一些特殊的动作,比如输出一些调试信息等,我们需要自己对operator进行重载。例如假如我们重写了下面这个版本,输出一些信息:

1
2
3
4
5
6
7
8
9
10
void* operator new(size_t size,string& info)throw(){
cout << info;
return ::operator new(size);
}
void operator delete(void* p, string& info){
return ::operator delete(p);
}
string s("info");
A* p = new (s) A(10);
delete(p);

看看汇编层面:

1
2
3
4
5
(gdb) info line 26
Line 26 of "main.cpp" starts at address 0x4015fb <main()+65> and ends at 0x40162d <main()+115>.

(gdb) info line 27
Line 27 of "main.cpp" starts at address 0x40162d <main()+115> and ends at 0x401639 <main()+127>.

placement new:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x00000000004015fb <+65>:    lea    rax,[rbp-0x30]
0x00000000004015ff <+69>: mov rdx,rax
0x0000000000401602 <+72>: mov ecx,0x4
0x0000000000401607 <+77>: call 0x401550 <operator new(unsigned long long, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&)>
0x000000000040160c <+82>: mov rbx,rax
0x000000000040160f <+85>: test rbx,rbx
0x0000000000401612 <+88>: je 0x401626 <main()+108>
0x0000000000401614 <+90>: mov edx,0xa
0x0000000000401619 <+95>: mov rcx,rbx
0x000000000040161c <+98>: call 0x402e50 <A::A(int)>
0x0000000000401621 <+103>: mov rax,rbx
0x0000000000401624 <+106>: jmp 0x401629 <main()+111>
0x0000000000401626 <+108>: mov rax,rbx
0x0000000000401629 <+111>: mov QWORD PTR [rbp-0x38],rax
0x000000000040162d <+115>: mov rbx,QWORD PTR [rbp-0x38]

placement delete:

1
2
3
4
5
6
7
8
   0x0000000000401631 <+119>:   test   rbx,rbx
0x0000000000401634 <+122>: je 0x40164b <main()+145>
0x0000000000401636 <+124>: mov rcx,rbx
0x0000000000401639 <+127>: call 0x402e90 <A::~A()>
0x000000000040163e <+132>: mov edx,0x4
0x0000000000401643 <+137>: mov rcx,rbx
0x0000000000401646 <+140>: call 0x401748 <_ZdlPvy>
=> 0x000000000040164b <+145>: mov ebx,0x0

系统就会使用我们定义的版本进行内存分配。
既然默认new 和 delete工作的很好,问什么需要重载?

  • 可以用来检测运用上的错误
  • 可以提高效率,节省不必要的内存,提高回收和分配的速度(比如针对某一对象的内存池)
  • 可以收集对内存使用的数据统计
  • operator delete主要为了处理异常,一般情况下delete会调用全局operator delete释放内存。

placement new

placement new是重载operator new的一个标准、全局的版本,它不能被自定义的版本代替(不像普通的operator new和operator delete能够被替换成用户自定义的版本)。
它的原型如下:

1
2
3
void *operator new( size_t, void *p ) throw()  { 
return p;
}

它不能够被自定义的版本代替(不像普通版本的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
      2
      class Task ;
      char buf[N*sizeof(Task)]; //分配内存

      3.还有一种方式,就是直接通过地址来使用。(必须是有意义的地址)

      1
      void* buf = reinterpret_cast<void*> (0xF00F);
    • 第二步:对象的分配

      在刚才已分配的缓存区调用placement new来构造一个对象。
      Task *ptask = new (buf) Task

    • 第三步:使用

      按照普通方式使用分配的对象:

      1
      2
      3
      ptask->memberfunction();
      ptask-> member;
      //...
    • 第四步:对象的析构

      一旦你使用完这个对象,你必须调用它的析构函数来毁灭它。按照下面的方式调用析构函数:

      1
      ptask->~Task(); //调用外在的析构函数
    • 第五步:释放

      你可以反复利用缓存并给它分配一个新的对象(重复步骤2,3,4)如果你不打算再次使用这个缓存,你可以象这样释放它:delete [] buf;