重载new和delete

当应用程序对于内存分配有特殊要求时,需要重载operator new和operator delete运算符.

new表达式的工作机制

  • new表达式的工作机理

    当我们使用一条new表达式时,实际执行了三步操作.

    • 第一步,调用名为operator new(或operator new[])的标准库函数, 分配一块足够大的, 原始的, 未命名的内存空间以便存储特定类型的对象(或对象数组).
    • 运行相应的构造函数构造这些对象,并初始化这些对象
    • 对象被分配了空间并构造完成,返回一个指向该对象的指针.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Complex {
    public:
    explicit Complex(double _real = 0.0, double _vir = 0.0) : real(_real), vir(_vir) {}

    private:
    double real;
    double vir;
    };


    Complex *pc = new Complex(1, 2);

    //equal to:
    void *p = operator new(sizeof(Complex)); // 分配内存
    Complex *pc = static_cast<Complex *>(p); // 转型
    pc->Complex::Complex(1, 2); // 调用构造函数


    delete pc;

    //equal to
    pc->~Complex(); // 调用对象的析构函数
    operator delete(p); // 释放内存空间
  • delete表达式的工作机理

    当我们使用一条delete表达式时,实际执行了两步操作.

    • 对delete表达式中指针所指向的对象执行相应的析构函数
    • 调用名为operator delete(或operator delete[])的标准库函数释放对象内存空间

几个疑问

为什么array new一定要搭配array delete?(array new 即new [size])?
发生内存泄露不在于array new 分配的数组,而在于delete 表达式仅仅调用一次析构函数,而array delete表示调用多次(取决于array new分配的数组大小)析构函数.如果在对象的构造函数中进行了动态内存分配,那么需要在析构函数中进行释放,但是delete表达式仅仅调用一次而非多次析构函数,无法释放数组中所有对象分配的动态内存,因此会造成内存泄露.

定位new表达式

可以通过自定义operator new和operator delete函数来控制内存分配过程.
但是有一个operator new函数不允许被用户重载:

1
void *operator new(size_t, void*)

此形式的operator new(定位new)只供标准库使用,不允许用户重载.
该函数并不分配内存,而是直接返回void参数传入的指针;然后由new表达式负责在指定的地址初始化对象以完成整个工作.即*定位new允许我们在一个特定的,预先分配的内存地址上构造对象**.传给定位new表达式的指针可以是堆内存,也可以不是.

使用定位new表达式

定位new表达式的形式: new (place_address) type [n] {initializer list}
其中,place-address是一个指针,指向已经分配好的内存地址, type表示要构造的对象的类型, n可选参数,表示要构造的对象的个数, {initializer list}为初始化列表用于初始化对象.

  • 指针place_address指向的是堆内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void f1(){
int *p1, *p2;
p1 = new int;
p2 = new(p1)int(4);

// p1和p2指向同一块内存
cout << "p1:" << p1 << " p2:" << p2 << endl;

double *p3, *p4;
p3 = new double[N]; // 堆内存,p3指向动态分配的空间
p4 = new(p3) double[N]{1.1, 2.2, 3.0, 4.6, 6.9};

// 定位new允许我们在一个特定的,预先分配的内存地址上构造对象
for (int i = 0; i < N; i++) {
cout << p4[i] << " ";
}
cout << endl;
}

运行结果

1
2
p1:0x55821bbafe70 p2:0x55821bbafe70
1.1 2.2 3 4.6 6.9
  • 指针place_address指向的是静态内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const int BUF = 512;
const int N = 5;
char buffer[BUF];

void f2(){
int *p;
p = new(buffer) int[N]; // buffer是静态内存, 定义在任何函数之外的变量
for (int i = 0; i < N; ++i) {
p[i] = i * 4;
}
cout << "静态分配的地址buffer:" << (void *) buffer << endl;
cout << "直接使用placement new的p2地址" << p << endl;
for (int i = 0; i < N; ++i) {
cout<< p[i] << " ";
}
cout << endl;
}

运行结果

1
2
3
静态分配的地址buffer: 0x556ab55fd140
直接使用placement_new的p2地址: 0x556ab55fd140
0 4 8 12 16
  • 指针place_address指向的是栈内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void f3(){
int a;
int *p = new(&a)int;

// p指向的内存正是a所在地址
cout << "&a:" << &a << " p:" << p << endl;

string arr[N]; // 栈内存, arr中保存着定义在函数内的非static对象
string *p;
p = new(arr) string[N]{"hello", "world", "a", "good", "day"};
for (int i = 0; i < N; i++) {
cout << p[i] << " ";
}
cout << endl;
}

运行结果

1
2
&a:0x7ffe52fda13c p:0x7ffe52fda13c
hello world a good day

重载operator new和operator delete

用户可以自定义oeprator new和operator delete,但是自定义版本必须位于全局作用于或者类的作用域中.
当自定义类的operator new和operator delete时,它们是隐式静态的,无须显式声明static.operator new在对象构造之前调用,而opertator delete在对象销毁之后调用,所以这两个成员必须是静态的,而且它们不操纵类的任何数据成员.

重载类的operator new和operator 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Foo {
public:
Foo() : _id(0) {
cout << "default ctor.this=" << this << " id=" << _id << endl;
}

Foo(int i) : _id(i) {
cout << "ctor.this=" << this << " id=" << _id << endl;
}

~Foo() {
cout << "dtor.this=" << this << " id=" << _id << endl;
}


static void *operator new(size_t size);

static void operator delete(void *pdead, size_t size);

static void *operator new[](size_t size);

static void operator delete[](void *pdead, size_t size);

private:
int _id;
long _data;
string _str;
};

void *Foo::operator new(size_t size) {
Foo *p = (Foo *) malloc(size);
cout << "custom operator new" << " ";
cout << "size = " << size << endl;

return p;
}

void Foo::operator delete(void *pdead, size_t size) {
cout << "custom operator delete" << " ";
cout << "size = " << size << endl;
free(pdead);
}


void *Foo::operator new[](size_t size) {
Foo *p = (Foo *) malloc(size);
cout << "custom operator new[]" << " ";
cout << "size = " << size << endl;

return p;
}

void Foo::operator delete[](void *pdead, size_t size) {
// 析构函数调用顺序为逆序调用,从最后一个元素到第一个元素
cout << "custom operator delete[]" << " ";
cout << "size = " << size << endl;

free(pdead);
}

测试代码:

当没有定义成员 operator new和operator delete时就调用全局的operator new和operator delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test_foo1() {
// 若无members就调用globals
Foo *pf = new Foo;
delete pf;

cout << endl;

Foo *parr = new Foo[1];
delete[] parr;

cout << endl;

parr = new Foo[2];
delete[] parr;
}

在64位系统上,string占32个字节,int占4个字节,long占8个字节.因此,sizeof(Foo)=32+4+8=44,又因为44不是8的倍数,因此,在Foo中添加一些padding,最后sizeof(Foo)=48字节.
最后的运行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
custom operator new size = 48
default ctor.this=0x55bda8d2be70 id=0
dtor.this=0x55bda8d2be70 id=0
custom operator delete size = 48

custom operator new[] size = 56
default ctor.this=0x55bda8d2be78 id=0
dtor.this=0x55bda8d2be78 id=0
custom operator delete[] size = 56

custom operator new[] size = 104
default ctor.this=0x55bda8d2c2c8 id=0
default ctor.this=0x55bda8d2c2f8 id=0
dtor.this=0x55bda8d2c2f8 id=0
dtor.this=0x55bda8d2c2c8 id=0
custom operator delete[] size = 104

可以看到,这里调用了自定义的operator new和operator delete函数.此外,分别使用new Foo和new Foo[1]动态分配一个Foo对象时,打印出的size不同,这是因为在operator new[]中分配的内存的最上面有一个额外内存用于记录数组中元素的个数,以备记录调用delete []时,需要调用析构函数的次数.
当new Foo[2]时:size=104.首先两个Foo对象占用内存为48 * 2 = 96字节, 而在分配内存的最前端有一个counter记录数组中元素个数,因此,最后size = 96 + 8 = 104.即这个动态分配的数组在占用内存如下图所示:

1568898674216

此外,可以看到,在delete[]表达式中调用析构函数是逆序调用的,即数组中最后一个元素首先调用析构函数,然后倒数第二个,依次类推,直到第一个元素.

强制调用全局operator new和operator delete

1
2
3
4
5
6
7
8
9
10
void test_foo2() {
// 强制使用globals
Foo *pf = ::new Foo;
::delete pf;

cout << endl;

Foo *parr = ::new Foo[2];
::delete[] parr;
}

运行结果:

1
2
3
4
5
6
7
default ctor.this=0x55e231ba2e70 id=0
dtor.this=0x55e231ba2e70 id=0

default ctor.this=0x55e231ba32c8 id=0
default ctor.this=0x55e231ba32f8 id=0
dtor.this=0x55e231ba32f8 id=0
dtor.this=0x55e231ba32c8 id=0

重载placement new

我们可以重载class member operator new(),写出多个版本,前提是每一版本的声明都必须有独特的参数列,其中第一参数必须是size_t,其余参数是以new所指定的placement arguments为初值.出现于new(…)小括号内的便是所谓placement arguments.也有其他定义说,new()括号内有一个指针做参数时才称为placement new.

1
Foo* pf = new (300, 'c')Foo;

我们也可以重载class member operator delete()(或者称此为placement operator delete),写出多个版本.但它们绝不会被delete调用.在旧版本的编译上,只有当new所调用的ctor(构造函数)抛出exception时,才会调用这些重载版的operator delete().它只能这样被调用,主要用来归还未能完全创建成功的object所占用的memory.
下面的示例程序说明只有当new所调用的ctor抛出异常时,才会调用重载版本的operator 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Bad {
public:
Bad() {
cout << "exception" << endl;
}
};

class Foo {
public:
Foo() { cout << "Foo::Foo()" << endl; }

Foo(int) {
cout << "Foo::Foo(int)" << endl;
throw Bad();
}

// 这个是一般的operator new()的重载
void *operator new(size_t size) {
return malloc(size);
}

// 标准库已经提供的placement new()的重载形式

void *operator new(size_t size, void *start) {
return start;
}

// 自定义的placement new
void *operator new(size_t size, long extra) {
return malloc(size + extra);
}

void *operator new(size_t size, long extra, char init) {
return malloc(size + extra);
}


void operator delete(void *p, size_t) {
free(p);
cout << "operator delete(void *p, size_t)" << endl;
}

void operator delete(void *p, long) {
delete p;
cout << "operator delete(void *p, long)" << endl;
}

void operator delete(void *p, long, char) {
delete p;
cout << "operator delete(void *p, long, char)" << endl;
}
};

void test1() {
Foo start;
Foo *p1 = new Foo;
Foo *p2 = new(&start)Foo;
Foo *p3 = new(100)Foo;
Foo *p4 = new(100, 'a') Foo;
Foo *p5 = new(100)Foo(1); // 调用构造函数时会抛出异常
Foo *p6 = new(100, 'a')Foo(1);
Foo *p7 = new(&start)Foo(1);
Foo *p8 = new Foo(1);
}

运行结果:
在gnu7.4.0测试,没有调用自定义的operator delete.

new表达式的作用域查找规则

如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找.此时如果该类含有operator new成员或者operator delete成员,则相应的表达式将调用这些成员.否则,编译器在全局作用域中查找匹配的函数.此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;否则,则使用标准库定义的版本.