Effective C++ 第二版 10) 写operator delete

时间:2023-03-09 03:53:26
Effective C++ 第二版 10) 写operator delete

条款10 写了operator new就要同时写operator delete

写operator new和operator delete是为了提高效率;

default的operator new和operator delete具有通用性, 也可以在特定情况下被重写以改善性能; 特别在需要动态分配大量的很小的对象的应用程序中;

1
2
3
4
5
6
7
class 
AirplaneRep { ... }; 
// 表示一个飞机对象
class 
Airplane {
public
:
...
private
:
    
AirplaneRep *rep; 
// 指向实际描述
};

>Airplane对象只包含一个指针, 如果声明了虚函数, 则会隐式包含虚指针; 
当调用operator new来分配Airplane对象时, 得到的内存可能比存储这个指针所需要的多, 因为operator new和operator delete之间需要相互传递信息;

default版本的operator new是一种通用型的内存分频器, 可以分配任意大小的内存块; operator delete也可以释放任意大小的内存块;
operator delete需要知道要释放的内存多大(operator new分配的内存大小) e.g. 在operator new返回的内存里附带额外信息, 指明被分配的内存块的大小;

Airplane *pa = new Airplane; 得到的不是: pa——> Airplane 对象的内存; 而是: pa——> 内存块大小数据 + Airplane 对象的内存; 对于小对象, 额外的数据信息会使得动态分配对象时需要的内存大小翻倍;

Solution: 为Airplane类专门写一个operator new, 利用每个Airplane的大小相等的特点, 不需要加上附带信息;

e.g. 先让缺省的operator new分配一些大块原始内存, 每块的大小足够容纳多个Airplane对象, Airplane对象的内存块取自这些大内存块;
当前没有使用的内存块被组织成链表-*链表, 未来给Airplane使用; rep域的空间被用来存储next指针;

修改Airplane支持自定义的内存管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
class 
Airplane { 
// 修改后的类 — 支持自定义的内存管理
public
:
    
static 
void 
* operator 
new
(
size_t 
size);
...
private
:
    
union 
{
        
AirplaneRep *rep; 
// 用于被使用的对象
        
Airplane *next; 
// 用于没被使用的(在*链表中)对象
    
};
    
// 类的常量,指定一个大的内存块中放多少个Airplane 对象,在后面初始化
    
static 
const 
int 
BLOCK_SIZE;
    
static 
Airplane *headOfFreeList;
};

>operator new函数, union(rep和next占用相同空间), int指定大内存块大小, static指针(跟踪*链表的表头) - 整个类只有一个*链表;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void 
* Airplane::operator 
new
(
size_t 
size)
{
// 把“错误”大小的请求转给::operator new()处理; 详见条款8
    
if 
(size != 
sizeof
(Airplane))
        
return 
::operator 
new
(size);
    
Airplane *p = headOfFreeList;
// p 指向*链表的表头
// p 若合法,则将表头移动到它的下一个元素
    
if 
(p)
        
headOfFreeList = p->next;
    
else 
{
    
// *链表为空,则分配一个大的内存块,可以容纳BLOCK_SIZE 个Airplane 对象
        
Airplane *newBlock = 
static_cast
<Airplane*>(::operator 
new
(BLOCK_SIZE * 
sizeof
(Airplane)));
// 将每个小内存块链接起来形成一个新的*链表
// 跳过第0 个元素,因为它要被返回给operator new 的调用者
    
for 
(
int 
i = 1; i < BLOCK_SIZE-1; ++i)
        
newBlock[i].next = &newBlock[i+1];
// 用空指针结束链表
        
newBlock[BLOCK_SIZE-1].next = 0;
// p 设为表的头部,headOfFreeList 指向的内存块紧跟其后
        
p = newBlock;
        
headOfFreeList = &newBlock[1];
    
}
    
return 
p;
}

>这里的operator new管理的内存是从::operator new分配来的, 所以new-handler的处理都在::operator new之中;

1
2
Airplane *Airplane::headOfFreeList;
const 
int 
Airplane::BLOCK_SIZE = 512;

>static member的初始值缺省为0;
>这个版本的operator new为Airplane对象分配的内存比缺省operator new的少, 运行更快(2次方等级), 只需操作链表中的一对指针, 用灵活性换速度; 
Note 因为通用型的operator new必须处理各种大小的内存请求, 还要处理内部外部的碎片;

需要声明Airplane的operator delete, 因为::operator delete会假设内存包含头信息;

Note operator new 和operator delete 必须同时写;

1
2
3
4
class 
Airplane {
...
static 
void 
operator 
delete
(
void 
*deadObject,
size_t 
size);
};

传给operator delete 的是一个内存块, 如果其大小正确, 就加到*内存块链表的最前面;

1
2
3
4
5
6
7
8
9
10
11
void 
Airplane::operator 
delete
(
void 
*deadObject, 
size_t 
size)
{
    
if 
(deadObject == 0) 
return

// 见条款 8
    
if 
(size != 
sizeof
(Airplane)) { 
// 见条款 8
        
::operator 
delete
(deadObject);
        
return
;
    
}   
    
Airplane *carcass = 
static_cast
<Airplane*>(deadObject);
    
carcass->next = headOfFreeList;
    
headOfFreeList = carcass;
}

>new和delete匹配, 如果opertaor new将"错误"大小的请求转给了::operator new, 这里同样要转给::operator delete;

Note 保证基类必须有虚析构; 
如果要删除的对象是从一个没有虚析构函数的类继承来的, 那传给operator delete的size_t可能不正确; operator delete有可能工作不正确;

引起内存泄露的原因在于内存分配后指向内存的指针丢失了, 如果没有类似垃圾处理机制, 内存就不会被收回;
上面的设计没有内存泄露, operator delete没有释放, 但是每个大内存块被分成Airplane大小的块, 小块放在*链表上. 
客户调用Airplane::operator new时, 小块被*链表移除, 客户得到指向小块的指针. 客户调用operator delete时, 小块放回*链表头上;
所有的内存块要么被Airplane对象使用(客户维护内存), 要么在*链表上(内存块有指针), 因此没有内存泄露;

::operator new返回的内存块从来没有被Airplane::operator delete释放, 这种内存块叫内存池;
Note 内存泄露会无限增长, 内存池的大小不会超过客户请求内存的最大值;

可以修改Airplane的内存管理使得::operator new返回的内存自动释放, 这里不这么做的原因:

1) 自定义内存管理的初衷. 
缺省的operator new和operator delete使用了大多内存, 运行很慢. 和内存池策略相比, 跟踪和释放大内存块所写的每一个额外的字节和每一条额外的语句都会导致软件运行更慢, 内存占用更多; 在设计性能要求很高的库或程序时, 如果预计的内存池大小会在固定的合理范围内, 那采用内存池策略就很好;

2) 和处理一些不合理的程序行为有关. 
假设Airplane的内存管理程序被修改了, Airplane的operator delete可以释放任何没有对象存在的大块的内存;

1
2
3
4
5
6
7
8
9
int 
main()
{
    
Airplane *pa = 
new 
Airplane; 
// 第一次分配: 得到大块内存,生成*链表,等
    
delete 
pa; 
// 内存块空; 释放它
    
pa = 
new 
Airplane; 
// 再次得到大块内存,生成*链表,等
    
delete 
pa; 
// 内存块再次空,释放
    
//...
    
return 
0;
}

>这样的小程序比缺省的operator new和operator delete运行的还慢, 占用更多内存.

>内存池无法解决所有的内存管理问题, 但在很多情况下是适合的.

为了给不同的类实现基于内存池的功能, 需要把这种固定大小内存的分频器封装起来:

e.g. Pool类接口, 每个对象是某类对象的内存分配器 (大小在Pool的构造函数里指定)

1
2
3
4
5
6
7
class 
Pool {
public
:
    
Pool(
size_t 
n); 
// 为大小为n 的对象创建一个分配器
    
void 
* alloc(
size_t 
n) ; 
// 为一个对象分配足够内存, 遵循条款8 的operator new 常规
    
void 
free

void 
*p, 
size_t 
n); 
// 将p 所指的内存返回到内存池, 遵循条款8 的operator delete常规
    
~Pool(); 
// 释放内存池中全部内存
};

>这个类支持Pool对象的创建, 执行分配和释放, 被摧毁的操作; Pool对象被摧毁时, 会释放它分配的所有内存;

>如果Pool的析构函数调用太快, 使用内存池的对象没有全部摧毁, 对象正使用的内存消失, 造成的结果是不可预测的.

内存管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class 
Airplane {
public
:
... 
// 普通Airplane 功能
    
static 
void 
* operator 
new
(
size_t 
size);
    
static 
void 
operator 
delete
(
void 
*p, 
size_t 
size);
private
:
    
AirplaneRep *rep; 
// 指向实际描述的指针
    
static 
Pool memPool; 
// Airplanes 的内存池
};
inline 
void 
* Airplane::operator 
new
(
size_t 
size)

return 
memPool.alloc(size); }
inline 
void 
Airplane::operator 
delete
(
void 
*p, 
size_t 
size)
{ memPool.
free
(p, size); }
// 为Airplane 对象创建一个内存池,在类的实现文件里实现
Pool Airplane::memPool(
sizeof
(Airplane));

>比起之前的设计更清晰, Airplane不再和非Airplane代码混在一起. union, *链表头指针, 定义原始内存块大小的常量都归入Pool类里了;

自定义内存管理程序用来改善程序性能, 可以被封装在像Pool这样的类里;

构造, 析构函数和赋值操作符

构造函数控制对象生成时的基本操作, 对象初始化; 析构函数销毁对象, 保证它被彻底清除; 赋值操作符给对象一个新值;

这些函数要保证正确性, 一旦出错对整个类带来的影响是无尽的.