类和继承(三):句柄类(handle)

代理的创建 会 复制所代理的对象,如何避免复制
(保持多态性的前提下避免复制对象的代价)

  • 某些类应当避免复制
    • 对象很大,资源消耗多
    • 每个对象代表一种不能被轻易复制的资源,如文件
    • 其它数据结构已经存储对象的地址,把副本地址插入那些数据结构中代价会非常大
    • 对象代表位于网络连接另一端的其他对象
    • 多态性环境中只知对象基类类型而不知对象本身类型
  • 避免使用指针复制对象
    • 使用对象指针比直接使用对象要困难
    • 未初始化的指针非常危险且难以防范
    • 管理内存的硬件总要检查被复制的指针是否真的指向程序所分配的内存位置上
    • 复制未初始化的指针会导致硬件陷阱
  • 例如:

1
2
3
4
5
6
void f() {
// copy a pointer without initialization
// would crash a program
int* p; // without initialization
int* q = p; // not being definited
}
  • 多个指针指向同一个对象时应考虑何时删除此对象

handle classe(句柄类)

有时也称为smart pointer(智能指针)
绑定到所控制的类的对象上

简单示例类

  • 表示点平面坐标的类
1
2
3
4
5
6
7
8
9
class Point {
public:
Point(): xval(0), yval(0) {}
Point(int x, int y): xval(x), yval(y) {}
int x() const { return xval; }
int y() const { return yval; }
Point& x(int xv) { xval = xv; return *this; }
Point& y(int yv) { yval = yv; return *this; }
};
  • 使用一个无参构造函数和一个两个参数的构造函数而非一个缺省参数的构造函数Point(int x = 0, int y = 0): xval(x), yval(y) {}
    • 后者允许只用一个参数(另一个缺省为零)构造Point对象,而这几乎是错的

绑定到句柄

  • 将句柄h直接绑定到对象上
1
2
Point p;
Handle h(p);
  • 删除p后应该使handle无效
  • handle应该控制它所绑定的对象(创建和销毁)
  • 从效果上说handle就是一种只包含单个对象的容器

获取对象

  • handle行为上类似一个指针
    • 应阻止使用者直接获得对象的实际地址
    • 过多暴露内存分配策略,不利于改变分配的策略
    • 隐蔽真正的对象地址,避免直接重载 operator->
1
2
3
4
5
6
7
class Handle {
public:
Point* operator->();
// ...
};
Point* addr = h.operator->(); // get the object address
// overloading operator-> is to blame

引用计数型句柄(UPoint)

了解有多少句柄绑定在同一个对象上以确定何时删除对象
引用计数(use count)不能是句柄的一部分或对象的一部分

  • 定义新的类容纳引用计数和Point对象
1
2
3
4
5
6
7
8
9
10
class UPoint {
// all the members are private
friend class Handle;
Point p;
int u;
UPoint(): u(1) {}
UPoint(int x, int y): P(x, y), u(1) {}
UPoint(const Point& p0): p(p0), u(1) {}
};
  • 一个简单的Handle类实现
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
class Handle {
public:
Handle();
Handle(int, int);
Handle(const Point&);
Handle(const Handle&);
Handle& operator=(const Handle&);
~Handle();
int x() const;
int y() const;
Handle& x(int);
Handle& y(int);
private:
Upoint * up;
};
Handle::Handle(): up(new UPoint) {}
Handle::Handle(int x, int y): up(new UPoint(x, y)) {}
Handle::Handle(const Point& p): up(new UPoint(p)) {}
Handle::~Handle() {
if (--up->u == 0)
delete up;
}
// just increase the use count by 1
Handle::Handle(const Handle& h): up(h.up) { ++up->up; }
// make sure it works when two handles use the same UPoint object
Handle& Handle::operator=(const Handle& h) {
++h.up->u;
if (--up->u == 0)
delete up;
up = h.up;
return *this;
}
int Handle::x() const { return up->p.x(); }
int Handle::y() const { return up->p.y(); }

写时复制(copy on write)

  • handle改动性函数两种不同语义
1
2
3
4
Handle h(3, 4);
Handle h2 = h;
h2.x(5);
int n = h.x(); // 3 or 5 ?
  • 若句柄为指针语义,n = 5
    • handle表现像指针或引用,h和h2绑定到同一对象
1
2
3
4
5
6
7
8
Handle& Handle::x(int x0) {
up->p.x(x0);
return *this;
}
Handle& Handle::y(int y0) {
up->p.y(y0);
return *this;
}
  • 若句柄为值语义,n = 3
    • 改变h2的内容不该影响h的值
    • 必须保证所改动的UPont对象不同时被其它Handle所引用,否则复制UPoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Handle& Handle::x(int x0) {
if (up->u != 1) {
--up->u;
up = new UPoint(up->p);
}
up->p.x(x0);
return *this;
}
Handle& Handle::y(int y0) {
if (up->u != 1) {
--up->u;
up = new UPoint(up->p);
}
up->p.y(y0);
return *this;
}
  • 以下代码片段需要在每个改变UPoint对象的成员函数中重复(可设计为Handle的私有成员函数)
1
2
3
4
if (up->u != 1) {
--up->u;
up = new UPoint(up->p);
}

写时复制优点:在绝对必要时才进行复制,额外开销小

句柄类的改进

  • 前述实现的缺点:把句柄捆绑到类T的对象上必须先定义具有类型T的成员的新类
  • 当捆绑句柄到继承自T的静态类型未知的类的对象上时难以实现

将应用计数从数据中分离出来作为独立的对象

  • 抽象地表示应用计数
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
class UseCount {
public:
UseCount();
UseCount(const UseCount&);
~UseCount();
// judge whether use count would become zero
bool only();
// judge whether use count should be deleted
bool reattach(const UseCount&);
// provide a method to make this handle to be the only one
bool makeonly();
private:
int* p;
// make assignment illegal
UseCount& operator=(const UseCount&);
};
UseCount::UseCount(): p(new int(1)) {}
UseCount::UseCount(const UseCount& u): p(u.p) { ++*p; }
UseCount::~UseCount() {
if (--*p == 0)
delete p;
}
bool UseCount::only() { return *p == 1; }
bool UseCount::reattach(const UseCount& u) {
// increase u.p first to make it work when self-assignment
++*u.p;
if (--*p == 0) {
delete p;
p = u.p;
return true;
}
p = u.p;
return false;
}
bool UseCount::makeonly() {
if (*p == 1)
return false;
--*p;
p = new int(1);
return true;
}
  • 重写Handle类
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
class Handle {
public:
// the same with previous definition
Handle();
Handle(int, int);
Handle(const Point&);
Handle(const Handle&);
Handle& operator=(const Handle&);
~Handle();
int x() const;
int y() const;
Handle& x(int);
Handle& y(int);
private:
Point* p;
UseCount u;
};
Handle::Handle(): p(new Point) {}
Handle::Handle(int x, int y): p(new Point(x, y)) {}
Handle::Handle(const Point& p0): p(new Point(p0)) {}
Handle::Handle(const Handle& h): u(h.u), p(h.p) {}
Handle::~Handle() {
if (u.only())
delete p;
}
Handle& Handle::operator=(const Handle& h) {
if (u.reattach(h.u))
delete p;
p = h.p;
return *this;
}
int Handle::x() const () {
return p->x();
}
int Handle::y() const () {
return p->y();
}
Handle& Handle::x(int x0) {
if (u.makeonly())
p = new Point(*P);
p->x(x0);
return *this;
}
Handle& Handle::y(int y0) {
if (u.makeonly())
p = new Point(*P);
p->y(y0);
return *this;
}

总结

通过引入引用计数使得handle类能灵活地设计出来,而将引用计数抽象化表示使handle类能协同不同数据结构工作

UseCount类简化了实现中特定的子问题:接口设计只为简化引用计算句柄实现,而不为终端用户(end user)所用


《C++沉思录(Cplusplus Thinking)》笔记