《C++ 沉思录》阅读笔记——句柄类
在介绍句柄之前,我们先来看看代理类的一些问题,显而易见的问题是它需要复制对象,这需要内存的开销,同时,某些特殊的对象,我们也不一定能够非常合适的定义出一个优美的 copy 函数,即使是复制自身这种看起来毫无疑问的操作,在程序员的世界里也不那么优雅了。
可以想到的一个例子就是如果对象非常大,那么 copy 必然需要很大一份内存;
同时如果有一个地方需要保存该对象的所有地址,那么在对象进行复制的时候也必须把新地址添加进去,这其实对于操作这个对象的人来说,很有可能是未知的。举个例子,就好像公安局要保存每个公民的合法身份,所以每个公民都必须登记到公安局去,但是如果你站在一个家庭的角度来看,如果没有强制规定或者是法律约束,你很有可能是不知道这件事的, 所以当你生了小宝宝的时候,很有可能忘记了去公安局登记,那么这个小宝宝就不会被承认,这里,你通过 copy 出来的那个副本就是这个没有身份的小宝宝;
除此之外,还有种情况也许你也在写 copy 函数时不知所措,就是当你的类非常复杂时,你真心是不知道定义在类里的那些东西,到底该如何进行复制,或者说你也不知道复制了那些内容后会不会对别的地方产生影响。
如果不去复制对象,我们有什么现成的武器吗?有!让我们想想,在 C++ 中,除了指针,还有什么东西不需要复制内存,也能够指向相同的内容。想到了吗?对了,就是引用。
使用对象的引用,我们可以避免对对象的复制,让我们想一想,引用我们通常把它用在函数的参数里,这是为什么呢?因为在函数里我有可能需要对函数外的变量进行操作,同时我又不想复制那些变量,因为这有时会非常困难,传一个引用进去,事情一下子变的简单很多。
那么引用在这里行不行的通呢?很可惜,我们最好不要这么做,因为如果我们想使用引用来指代对象,我们在很多地方需要返回这个对象的引用,比如在函数的返回值里,可是如果把引用作为函数返回值,通常是一件危险的事情,因为我不知道我的函数会被谁调用,同时我也就不知道我的函数返回值会被拿来干什么,要命的是这个返回值居然还是个引用变量,那么意味着我把家门钥匙给了别人,甚至我连对方是谁都不知道,当然,你会说可以写成 const 类型呗,对,是可以,但是把引用作为返回值还有几个问题,比如无法返回局部变量或者临时变量的引用,同时,引用作为返回值,意味着这个函数可以被拿做运算符的左值进行调用,还有,如果返回了动态内存的引用,那么这块内存就无法释放了,等等一系列令人头疼的问题。
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 的指针一样,同时这个句柄也需要像代理类同样的功能,所以我们还需要默认构造函数、复制构造函数、赋值操作符等等,理由我就不再赘述了,不太清楚的朋友可以去看我关于代理类那篇,那么我们的句柄看起来应该是这个样子:
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 的值的方法吧,那么我们的句柄变成这样了:
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 对象的唯一副本,之后无论怎么复制句柄,都不会再产生代理类那样的多余副本了。我们就把可能存在的成百上千的副本内存,压缩到只有一份。
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;
}
这样我们的问题都迎刃而解了,接着我们来看一下,该如何实现这个句柄类的各个函数。
// 首先定义一个新的句柄,并绑定到一个新的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);
看明白这个问题的人会立刻想到,这里说的其实就是句柄到底是值语义还是指针语义。
h1 = h;
这句话就会导致 h 所指的对象被绑定到 h1 ,同时 h 解除与该对象的绑定。这就好像对象就像一个球一样,在句柄之间传来传去。
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; }
}
好了公共操作写完了,下面我们要来看看不同语义的赋值函数了,首先来看看指针语义:
Handle &Handle::x(int x0)
{p->x(x0);return *this;
}
很简单是不是,但你也要体会到这个简单背后可能带来上面提到的问题。值语义的实现稍微复杂点,毕竟涉及到一个“写时复制”:
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 ;虽然我并不认为实现的比较完美,但还算是中规中矩,它是这么干的:
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; } // 返回该引用计数是否为1bool 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 类:
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;
}
好了,我们终于要迎来结尾了,虽然上面的引用计数和句柄类的实现现在还印在脑子里,但我不确定会存多久,但是每当我想起我要解决的问题时,自然而然推出这些结论就让人很兴奋,就像数学证明一样,从开头慢慢到了这里。我是一个粗心的,希望上面的代码没有错误。现在我们来总结一下:
转载于:https://blog.51cto.com/rangercyh/1293679
《C++ 沉思录》阅读笔记——句柄类相关推荐
- 设计模式沉思录 - 读书笔记(XMind)
注:后面会不定期,以XMind的方式发布一些读书笔记. 目标:书还要是越读越薄才行!
- 软件管理沉思录读书笔记
第一部分 管理你的项目 质量之所以重要,是因为软件可能会使用十年.组织极少会弃用软件,而是通过提升和重新利用不断使用它.因此,对于软件质量的关注必须贯穿其整个生命周期. 第一章 交付高质量的产品 &q ...
- 软件开发沉思录读书笔记
软件开发中推崇敏捷,自动化测试,减少了成本加快了速度,加快了沟通和版本之间的关系,用好的沟通来换好的软件.关于多语言开发,应该根据业务领域的不同,采用适合不同领域的编程语言,同时也要注意编程语言的跨平 ...
- C++代理类,句柄(智能指针)_C++沉思录笔记
代理类 首先定义三个类: class Animal{ public:virtual void getName()=0;virtual void clone()=0; };class Cat:publi ...
- C++沉思录-句柄类1
看了下<C++沉思录>第六章的内容介绍的是句柄第一部分,采用引用计数器的方式减少内存的拷贝 动手敲了下代码加深点印象,加了点注释 class Point { public: /// ...
- c++ 沉思录笔记——句柄(第一部分)
句柄:第一部分 前言 代理类可以让我们在一个容器中存储不同类型但相互关联的对象. 这种方法需要为每一个对象创建一个代理,并要将代理存储在容器中. 创建代理将会复制所代理的对象,就像复制代理一样. 怎么 ...
- 《C++沉思录》读书笔记
<C++沉思录>读书笔记 序幕 动机 第1章 为什么我用C++ 第2章 为什么用C++工作 第3章 生活在现实世界中 类与继承 第4章 类设计者的核查表 第5章 代理类 第6章 句柄:第一 ...
- 《C++ 沉思录》学习笔记——上篇
文章目录 1. 总结(31-32) 1.1 通过复杂性获取简单性(31) 1.1.1 类库和语言语义 1.1.2 抽象和接口 1.2 说了 Hello world 后再做什么(32) 2. 技术(27 ...
- C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]
面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对 ...
- 读书笔记∣概率论沉思录 01
概率的解释基础分为两种,一是物理世界本身存在的随机性(客观概率),二是是我们由于信息不足而对事件发生可能性的度量(主观概率).基于此,形成了概率论的两大学派:频率论学派(传统数理统计学)和贝叶斯统计学 ...
最新文章
- Linux下JNI实现
- openstack newton noVNC bug 解决方法
- Android数据存储与访问
- javax.naming.NameNotFoundException:
- Java有快速打好基础的方法?
- 编程零基础做程序员,该怎么学习?首先要学习什么?
- java字符串拼接_Java 8中字符串拼接新姿势:StringJoiner
- 双网卡绑定--实现负载冗余
- [转]文本编辑软件UltraEdit v16.20官方简/繁体中文版下载+注册码和破解方法
- Linux系统时间不同步问题
- 单片机加减法计算器_单片机简易加法计算器程序
- jquery width,height,innerwidth,innerheight,outerwidth,outerheight方法
- mysql for centos下载_CentOS下载mysql哪个版本
- 简单好用的桌面隐藏工具:Desktop Curtain for Mac
- crmeb多商户二开crmeb类库二开文档services服务类【5】
- RD540/RD640出厂标配几个Riser卡?
- 路由器接口管理 控制端口 辅助端口 物理端口 逻辑端口 局域网
- 哲理小故事300篇(1—100)
- Pytorch图像预处理——归一化、标准化
- 从源头解决问题,而不是曲线救国