条款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这样的类里;

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

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

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

Effective C++ 第二版 10) 写operator delete的更多相关文章

  1. Effective C++ 第二版 8) 写operator new 和operator delete 9) 避免隐藏标准形式的new

    条款8 写operator new 和operator delete 时要遵循常规 重写operator new时, 函数提供的行为要和系统缺省的operator new一致: 1)正确的返回值; 2 ...

  2. Effective C++ 第二版 5)new和delete形式 6) 析构函数里的delete

    内存管理 1)正确得到: 正确调用内存分配和释放程序; 2)有效使用: 写特定版本的内存分配和释放程序; C中用mallco分配的内存没有用free返回, 就会产生内存泄漏, C++中则是new和de ...

  3. 条款十: 如果写了operator new就要同时写operator delete

    为什么有必要写自己的operator new和operator delete? 答案通常是:为了效率.缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也 ...

  4. Effective Java 第二版 Enum

    /** * Effective Java 第二版 * 第30条:用enum代替int常量 */ import java.util.HashMap;import java.util.Map; publi ...

  5. Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数

    条款17 在operator=中检查给自己赋值的情况 1 2 3 class  X { ... }; X a; a = a;  // a 赋值给自己 >赋值给自己make no sense, 但 ...

  6. Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承

    条款40 通过分层来体现"有一个"或"用...来实现" 使某个类的对象成为另一个类的数据成员, 实现将一个类构筑在另一个类之上, 这个过程称为 分层Layeri ...

  7. Effective C++ 第二版 31)局部对象引用和函数内new的指针 32)推迟变量定义

    条款31 千万不要返回局部对象的引用, 不要返回函数内部用new初始化的指针的引用 第一种情况: 返回局部对象的引用; 局部对象--仅仅是局部的, 在定义时创建, 在离开生命空间时被销毁; 所谓生命空 ...

  8. 《Effective Java第二版》总结

    第1条:考虑用静态工厂方法代替构造器 通常我们会使用 构造方法 来实例化一个对象,例如: // 对象定义 public class Student{ // 姓名 private String name ...

  9. Effective C++ 第二版 1)const和inline 2)iostream

    条款1 尽量用const和inline而不用#define >"尽量用编译器而不用预处理" Ex. #define ASPECT_R 1.653    编译器永远不会看到AS ...

随机推荐

  1. BZOJ 1037: [ZJOI2008]生日聚会Party( dp )

    dp(i, j, a, b)表示选了i个男生, j个女生, 后缀中男生比女生多a(最多), 女生比男生多b(最多). dp(i+1, j, a+1, max(0, b-1)) += dp(i, j, ...

  2. String字符串需要掌握的几个问题

    一.两种定义方式的区别: String str = "hello";      先在堆内存中查找是否已经有"hello",若有,将str指向已存在的它即可:若堆 ...

  3. hadoop搭建杂记:Linux下hadoop的安装配置

    VirtualBox搭建伪分布式模式:hadoop的下载与配置 VirtualBox搭建伪分布式模式:hadoop的下载与配置 由于个人机子略渣,无法部署XWindow环境,直接用的Shell来操作, ...

  4. 做了一个图片等比缩放的js

    做了一个图片等比缩放的js 芋头 发布在view:8447   今天改了一下博客的主题,发现博客主题在ie6下变样了,后来发现是因为某篇文章里的某个图片太大了撑开了容器,导致样式错位,前几天公司需求里 ...

  5. ThinkPHP第二十六天(JQuery操作select,SESSION和COOKIE)

    1.JQuery操作select,假设<select id="my"> A:双击选项<option>事件,应该是select的dbclick事件. B:获得 ...

  6. java_httpservice

    http://blog.csdn.net/maosijunzi/article/details/41045181

  7. Use eplipse to develop Python project

    Source: This is the example how to use eclipse and python. http://www.360doc.com/content/15/0206/10/ ...

  8. 《Pointers On C》读书笔记(第二章 基本概念)

    1.从源代码到生成可执行程序的过程整体上可以分为两个阶段:编译和链接.其中,编译过程大致上又可分为:预处理.编译和汇编.预处理阶段主要对源代码中的预处理指令(包含宏定义指令<如 #define& ...

  9. 类比的方法学习Performance_schema

    引用自:http://www.javacoder.cn/?p=332 MySQL在5.6版本中包含了一个强大的特性——performance-schema,合理的使用这个数据库中的表,能为我们解决一些 ...

  10. Noip2013错误避免

    很多的时候,我们会说,这道题我会做,算法想出来了,但是这里那里少了一些判断,导致一分未得,或是说变量名错误,或者说干脆是文件名错误.这些都不是理由,如果火箭发射半空爆炸,可以说是控制器中一个运算符错误 ...