在介绍句柄之前,我们先来看看代理类的一些问题,显而易见的问题是它需要复制对象,这需要内存的开销,同时,某些特殊的对象,我们也不一定能够非常合适的定义出一个优美的 copy 函数,即使是复制自身这种看起来毫无疑问的操作,在程序员的世界里也不那么优雅了。
可以想到的一个例子就是如果对象非常大,那么 copy 必然需要很大一份内存;
同时如果有一个地方需要保存该对象的所有地址,那么在对象进行复制的时候也必须把新地址添加进去,这其实对于操作这个对象的人来说,很有可能是未知的。举个例子,就好像公安局要保存每个公民的合法身份,所以每个公民都必须登记到公安局去,但是如果你站在一个家庭的角度来看,如果没有强制规定或者是法律约束,你很有可能是不知道这件事的, 所以当你生了小宝宝的时候,很有可能忘记了去公安局登记,那么这个小宝宝就不会被承认,这里,你通过 copy 出来的那个副本就是这个没有身份的小宝宝;
除此之外,还有种情况也许你也在写 copy 函数时不知所措,就是当你的类非常复杂时,你真心是不知道定义在类里的那些东西,到底该如何进行复制,或者说你也不知道复制了那些内容后会不会对别的地方产生影响。
如果不去复制对象,我们有什么现成的武器吗?有!让我们想想,在 C++ 中,除了指针,还有什么东西不需要复制内存,也能够指向相同的内容。想到了吗?对了,就是引用。
使用对象的引用,我们可以避免对对象的复制,让我们想一想,引用我们通常把它用在函数的参数里,这是为什么呢?因为在函数里我有可能需要对函数外的变量进行操作,同时我又不想复制那些变量,因为这有时会非常困难,传一个引用进去,事情一下子变的简单很多。
那么引用在这里行不行的通呢?很可惜,我们最好不要这么做,因为如果我们想使用引用来指代对象,我们在很多地方需要返回这个对象的引用,比如在函数的返回值里,可是如果把引用作为函数返回值,通常是一件危险的事情,因为我不知道我的函数会被谁调用,同时我也就不知道我的函数返回值会被拿来干什么,要命的是这个返回值居然还是个引用变量,那么意味着我把家门钥匙给了别人,甚至我连对方是谁都不知道,当然,你会说可以写成 const 类型呗,对,是可以,但是把引用作为返回值还有几个问题,比如无法返回局部变量或者临时变量的引用,同时,引用作为返回值,意味着这个函数可以被拿做运算符的左值进行调用,还有,如果返回了动态内存的引用,那么这块内存就无法释放了,等等一系列令人头疼的问题。
1
2
3
4
5
6
7
8
9
10
11
|
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
; }
private
:
int
xval, yval;
}
|
这个类没什么高深莫测的东西,非常清晰明了。现在让我们来定义句柄,如果学习过我前一篇博文代理类的相关内容(http://rangercyh.blog.51cto.com/1444712/1291958),那么你很快便会明白,这里的句柄其实也是一个概念,我们需要一个概念类来管理 Point ,当然也包括它的子类,为此,我们需要保存一个 Point 的指针,就好像之前代理类保存了 Vehicle 的指针一样,同时这个句柄也需要像代理类同样的功能,所以我们还需要默认构造函数、复制构造函数、赋值操作符等等,理由我就不再赘述了,不太清楚的朋友可以去看我关于代理类那篇,那么我们的句柄看起来应该是这个样子:
1
2
3
4
5
6
7
8
9
10
11
|
class
handle {
public
:
Handle();
Handle(
int
,
int
);
Handle(
const
Point &);
Handle(
const
Handle &);
Handle &operator=(
const
Handle &);
~Handle();
private
:
Point *p;
}
|
我们先不去管如何处理多个句柄绑定相同的对象的问题,单看这两个类,如果我需要操作句柄来控制 Point ,那么我似乎还需要一个函数,用来返回 Point 指针给调用者,但是这里涉及到一个安全问题,如果我并不希望调用者通过句柄去访问实实在在的 Point 对象呢?既然我们已经增加了一层,那么就不该把底层再交给调用者,所以我也许不会去定义一个函数来返回 Point 指针,相反,我会增加好几个函数,去供句柄的使用来调用,这些函数都把 Point 的细节封装其中,让调用者只关心自己的内容,而不需要接触到它不想要的内容,举个例子,比如有一个调用者需要设置 x 的值,那么我是否应该把整个 Point 指针交给他,然后让他来调用 Point 的 Point &x(int xv) 函数呢?当然不行,如果我把 Point 的指针交给他了,谁知道他会怎么样去操作 Point ,也许他会直接把 Point 这个对象给删除掉,那么我句柄里保存的指针 p 也就成了悬垂指针了。所以一般来说,我会在句柄里定义一些外部会使用到的方法,可能和 Point 提供的方法类型,但一般来说不会包含 Point 的全部方法,这里我就只添加一个设置 x 的值的方法吧,那么我们的句柄变成这样了:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
handle {
public
:
Handle();
Handle(
int
,
int
);
Handle(
const
Point &);
Handle(
const
Handle &);
Handle &operator=(
const
Handle &);
Handle &x(
int
);
~Handle();
private
:
Point *p;
}
|
OK,这个句柄越来越像样子了,下面进入正题,我们需要有多个句柄指向同一个 Point 对象,而又不会产生代理类中复制代理类就会多产生一个 Point 副本的内存开销。要重复,又不要复制,看起来我们只有计数这一条路可以走了,也就是说我们记录一下指向 Point 对象的相同句柄的数量,如果这个数量为 0 了,我们就可以删掉这个 Point 的副本了,这样,我们的句柄就只会在第一次给 p 赋值时产生 Point 对象的唯一副本,之后无论怎么复制句柄,都不会再产生代理类那样的多余副本了。我们就把可能存在的成百上千的副本内存,压缩到只有一份。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
handle {
public
:
Handle();
Handle(
int
,
int
);
Handle(
const
Point &);
Handle(
const
Handle &);
Handle &operator=(
const
Handle &);
Handle &x(
int
);
~Handle();
private
:
Point *p;
int
*u;
}
|
这样我们的问题都迎刃而解了,接着我们来看一下,该如何实现这个句柄类的各个函数。
1
2
3
4
5
6
7
8
9
|
// 首先定义一个新的句柄,并绑定到一个新的Point对象,x为3,y为4
Handle h(3, 4);
// 然后通过复制构造函数,使h2也绑定到这个对象
Handle h2 = h;
// 这句话值得玩味,我们的目的到底是设置绑定的那个Point对象的x值为5
// 还是说我们只是希望这个句柄的值为5
h2.x(5);
// 这里取得的值,你究竟希望它是3,还是5呢?
int
n = h.(x);
|
看明白这个问题的人会立刻想到,这里说的其实就是句柄到底是值语义还是指针语义。
1
|
h1 = h;
|
这句话就会导致 h 所指的对象被绑定到 h1 ,同时 h 解除与该对象的绑定。这就好像对象就像一个球一样,在句柄之间传来传去。
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
|
Handle::Handle() : u(
new
int
(1)), p(
new
Point) { }
Handle::Handle(
int
x,
int
y) : u(
new
int
(1)), p(
new
Point(x, y)) { }
Handle::Handle(
const
Point &p0) : u(
new
int
(1)), p(
new
Point(p0)) { }
Handle::Handle(
const
Handle &h) : u(h.u), p(h.p) { ++*u; }
Handle & Handle::operator=(
const
handle &h)
{
// 增加=号右侧句柄的引用计数,注意,必须先增加=号右侧的引用计数,
// 否则当把句柄赋值给自己时Point就被删除了
++*h.u;
// 减少=号左侧句柄的引用计数,如果为0了,则删除绑定的对象副本
if
(--*u == 0)
{
delete
u;
delete
p;
}
u = h.u;
p = h.p;
return
*
this
;
}
Handle::~Handle()
{
if
(--*u == 0)
{
delete
u;
delete
p;
}
}
|
好了公共操作写完了,下面我们要来看看不同语义的赋值函数了,首先来看看指针语义:
1
2
3
4
5
|
Handle &Handle::x(
int
x0)
{
p->x(x0);
return
*
this
;
}
|
很简单是不是,但你也要体会到这个简单背后可能带来上面提到的问题。值语义的实现稍微复杂点,毕竟涉及到一个“写时复制”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Handle &Handle::x(
int
x0)
{
/*
这里比较的目的是如果引用计数大于1,代表有多个句柄指向该对象,
所以我们需要减少引用计数,如果引用计数为1,
代表只有这一个句柄指向这个对象,
既然,我要修改这个对象的值,那么直接改原对象就可以了。
*/
if
(*u != 1)
{
--*u;
p =
new
Point(*p);
}
p->x(x0);
return
*
this
;
}
|
完美了。两种语义下的 handle 类我们都设计完成了,而且看起来一切美好。我们设计的这个句柄类能够在运行时绑定未知类型的 Point 类及其继承,同时能够自己处理内存分配的问题,而且我们避免了代理类每次复制都拷贝对象的操作。唯一令我们还不太满意的地方是对引用计数这个变量的操作穿插在了整个 handle 类的实现当中,而且耦合的非常紧密,我们最好能把引用计数给抽离出来。在《C++ 沉思录》中给出了一种抽离引用计数的方法,它定义了一个引用计数的类,里面保存我们上面定义的引用计数变量 int *p ;虽然我并不认为实现的比较完美,但还算是中规中矩,它是这么干的:
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
|
class
UseCount
{
public
:
UseCount() : p(
new
int
(1)) { }
UseCount(
const
UseCount &u) : p(u.p) { ++*p; }
~UseCount() {
if
(--*p == 0)
delete
p; }
bool
only() {
return
*p == 1; }
// 返回该引用计数是否为1
bool
reattach(
const
UseCount &u) {
++*u.p;
if
(--*p == 0) {
delete
p;
p = u.p;
return
true
;
}
p = u.p;
return
false
;
}
bool
makeonly() {
// 用于”写时复制“,产生一个新的引用计数
if
(*p == 1) {
return
false
;
}
--*p;
p =
new
int
(1);
return
true
;
}
private
:
UseCount &operator=(
const
UseCount &);
private
:
int
*p;
}
|
然后我们可以对应修改我们的 handle 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class
handle
{
public
:
Handle() : p(
new
Point) { }
Handle(
int
x,
int
y) : p(
new
Point(x, y)) { }
Handle(
const
Point &p0) : p(
new
Point(p0)) { }
~Handle() {
if
(u.only())
delete
p; }
Handle &operator=(
const
handle &h) {
if
(u.reattach(h.u))
delete
p;
p = h.p;
return
*
this
;
}
Handle &x(
int
x0) {
if
(u.makeonly()) p =
new
Point(*p);
p->x(x0);
return
*
this
;
}
private
:
Point *p;
UseCount u;
}
|
好了,我们终于要迎来结尾了,虽然上面的引用计数和句柄类的实现现在还印在脑子里,但我不确定会存多久,但是每当我想起我要解决的问题时,自然而然推出这些结论就让人很兴奋,就像数学证明一样,从开头慢慢到了这里。我是一个粗心的,希望上面的代码没有错误。现在我们来总结一下:
在代理类中,我们学会了如何设计一种容器来存放编译时未知类型的对象,并且找到了一种内存分配的方法来做这些。但是代理类有两个问题,一个是每次复制代理类都会导致对象的复制,另一个是我们必须要从对象的设计开始就想到之后要使用代理类的问题,得为代理类留出接口,比如一个 copy 函数。在句柄这篇文章中,我们学到一种使用引用计数的方式,来避免每次复制都需要拷贝对象的操作,同时不用去修改原始的对象,因此它更加灵活。我们还看到了指针语义和值语义的区别已经如何实现两种语义的方式,明白了”写时复制“的含义。在最后我们把引用计数抽离成为一个单独的类,这样这个引用计数就可以嵌入到各个不同的句柄设计中,而不需要每个句柄都自己来控制,抽象是循序渐进的,我非常喜欢这种一气呵成的感觉。
转自:http://blog.51cto.com/rangercyh/1293679