句柄:第一部分

前言

  • 代理类可以让我们在一个容器中存储不同类型但相互关联的对象。
  • 这种方法需要为每一个对象创建一个代理,并要将代理存储在容器中。
  • 创建代理将会复制所代理的对象,就像复制代理一样。

怎么避免复制呢?
句柄类:允许在保持代理的多态行为的同时,还可以避免进行不必要的复制。

6.1 问题

对于某些类来说,能够避免复制其对象是很有好处的。
1)对象会很大,复制起来消耗太大;
2)每个对象代表一种不能轻易被复制的资源,比如文件。
3)某些其他的数据结构已经存储了对象的地址,把副本的地址插入到那些数据结构中代价会非常大,或者根本不可能。
4)这个对象代表着位于网络连接另一端的其他对象。
5)我们可能处于一个多态性的环境中,我们能够知道对象的基类的类型,但是不知道对象本身的类型或者怎样复制这种类型的对象。

一种方法:
让我们在避免某些缺点(如缺乏安全性)的同时能够获取指针的某些优点,尤其是能够在保持多态性的前提下避免复制对象的代价。

c++的解决办法就是定义一个适当的类。
由于这些类的对象通常被绑定到它们所控制的类的对象上,所以这些类常称为handle类(handle classes)。因为这些 handle的行为类似指针,所有有时候人们也叫它们只能指针。然而两者还是有很大差别,只有在极其有限的情况下才能把两者是做相同。

6.2 一个简单的类

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;
};
Point p;
int x = p.x(); //把p的x坐标复制到x中
p.x(42); // 将p的x坐标设置为42,这个操作会返回调用对象的引用。
p.x(42).y(23); // 将p的x坐标设置为42,y坐标设置为23

6.3 绑定到句柄

从Point对象初始化 handle 应该完成那些任务呢?

Point p;

Handle h§; // 这应该是什么含义?

浅显的说,我们希望将句柄h直接绑定到对象p上。(绑定:用p初始化h?)

将handle直接绑定到对象p上,则我们的handle最好与p的内存分配和释放无关。

handle应该“控制”它所绑定的对象,也就是handle应该创建和销毁对象。

两种选择:

1)可以创建自己的Point对象并把它赋给一个handle去进行复制;// Handle h§; // 创建一个p的副本,并将handle绑定到该副本!

  1. 可以把用于创建Point对象的参数传递给Handle。// Handle h0(123, 456); // 创建绑定到新分配的坐标为(123,456)的Point的handle

因此,我们想要Handle类的构造函数和Point类一样。

6.4 获取对象

假设我们有一个绑定到point对象的handle,应该怎么访问这个 point呢?

要是一个handle在行为上类似一个指针,则可以使用 operator-> 将Handle的所有操作转发给相应的Point操作来执行:

class Handle {
public:Point* operator->();// ...
};

作用:把所有的Point操作都通过operator->转发了。

缺点:没有简单的办法禁止一些操作,也没有办法选择地改写一些操作。

例如,如果我们希望handle能够对对象的分配和回收拥有完全的控制权,那么最好组织用户可以直接获取对象的实际地址。
但是如果我们handle有operator->操作,则可以使用 c++ Point* addr = h.operator->();从而获得底层Point对象的地址。

所以,如果想把Point对象的真实地址隐藏起来,就必须避免使用operator->,而且必须明确地选择让我们的Handle类支持哪些Point操作。

6.5 简单的实现

开始实现自己的Handle类!

class Handle {
public:Handle();Handle(int,int);Handle(const Point&);Handle(const Handle&);Handle& operator= (const Handle&);~Hanlde();
public:int x() const;Handle& x(int);int y() const;Handle& y(int);
};

6.6 引用计数型句柄

之所以用句柄,原因之一就是避免不必要的对象复制。也就是允许多个句柄绑定到单个对象上。

通常使用引用计数(use_count)来了解有多少个句柄绑定到同一个对象上,只有这样才能确定应该在合适删除对象。

1)这个引用计数不是能是句柄的一部分。如果这么干,那么每一个句柄都必须知道跟它一起被绑定到同一个对象的其他所有句柄的位置,
如此才能去更新其他句柄的引用计数数据。

2)不能让引用计数成为对象的一部分,因为那样要求我们重写已经存在的对象类。

因此,我们必须定义一个新的类来容纳一个 引用计数Point类。我们称之为UPoint

这个类纯粹是为了实现而设计的,因此把它的所有成员设置为 private,并且将我们的Handle类设置为有元。

我们生成一个 UPoint对象时,其引用计数始终为1,因为该对象的地址将会马上存储在一个Handle对象中。(由于UPoint类的所有成员都是private的,
所以如果有一个UPoint对象被生成,则必然是其友元类Handle要求的,因此可以肯定,UPoint对象的地址将会马上被Handle对象保存。)

另一方面,我们希望能够以创建Point类的全部方式创建UPoint对象,所以把Point的构造函数照搬过来:

class UPoint{
private: // 所有成员都是privatefriend class Handle;Point p_;int u_; // 引用计数数据UPoint() : u(1) { }UPoint(int x, int y) : Point(x, y), u_(1) { }UPoint(const Point& p) : p_(p), u_(1) { }
};

除此之外,我们将通过直接引用UPoint对象的方式操作UPoint对象。(?)

开始完善Handle类

class Handle {
public:Handle() : up_(new UPoint) { }Handle(int x, int y) : up_(new UPoint(x, y)) { }Handle(const Point& p) : up_(new UPoint(p)) { }Handle(const Handle& h) : up_(h.up_) { ++up_->u; } // 复制构造函数,只用把引用计数加1,这样原先的句柄和副本都指向相同的UPoint对象。Handle& operator= (const Handle& h) {// 首先递增右侧句柄指向对象的引用计数++h.up_->u;// 然后递减左侧句柄所指向对象的引用计数if (--up_->u == 0)delete up_;up_ = h.up_;return *this;}~Hanlde() { // 递减引用计数,如果发现计数为0,就删除 UPoint对象if (--up->u_ == 0) {delete up_;}}
public:int x() const { return up_->p.x(); }Handle& x(int x);int y() const { return up_->p.y(); }Handle& y(int);private:// 添加的UPoint *up_;
};

然而,当我们开始考虑改动性的函数时,事情就变得有趣了。原因是:这里我们必须做出决定,我们的句柄需要值语义还是指针语义?

6.7 写时复制

从实现的角度看,我们将Handle类设计成“无需对Point对象进行复制”的形式。
可关键问题是:是否希望句柄类在用户面前的行为也是这样的。
例如:

Handle h(3, 4);
Handle h2 = h;  // 复制 Handle
h2.x(5);        // 修改 Point
int n = h.x();  // 3? or 5?

如果Handle采用值语义,那么 n= 3;采用指针语义 n= 5;

如果采用指针语义,则永远不必复制 UPoint对象。

Handle& Handle::x(int x0) {up_->p.x(x0);return *this;
}
Handle& Handle::y(int y0) {up_->p.y(x0);return *this;
}

如果采用值语义,就必须保证所改动的哪个UPoint对象不能同时被任何其他的Handle所引用。
这也不难,只要看看引用计数即可。如果是1,则说明Handle是唯一一个使用该UPoint对象的句柄;
其他情况下,就必须复制另外一个UPoint对象,使其引用计数变成1:其他情况下,就必须复制 UPoint 对象,使其引用计数变为 1。

// 完整代码!!!!!!!
#include <iostream>
#include <memory>using namespace std;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;
};class UPoint {
private: // 所有成员都是privatefriend class Handle;Point p_;int u_; // 引用计数数据UPoint() : u_(1) {}UPoint(int x, int y) : p_(x, y), u_(1) {}explicit UPoint(const Point &p) : p_(p), u_(1) {}
};class Handle {
public:Handle() : up(new UPoint) {}Handle(int x, int y) : up(new UPoint(x, y)) {}explicit Handle(const Point &p) : up(new UPoint(p)) {}Handle(const Handle &h) : up(h.up) {++up->u_;} // 复制构造函数,只用把引用计数加1,这样原先的句柄和副本都指向相同的UPoint对象。Handle &operator=(const Handle &h) {// 首先递增右侧句柄指向对象的引用计数++h.up->u_;// 然后递减左侧句柄所指向对象的引用计数if (--up->u_ == 0)delete up;up = h.up;return *this;}~Handle() { // 递减引用计数,如果发现计数为0,就删除 UPoint对象if (--up->u_ == 0) {delete up;}}public:int x() const { return up->p_.x(); }int y() const { return up->p_.y(); }Handle &x(int x0) {if (up->u_ != 1) {--up->u_;up = new UPoint(up->p_);}up->p_.x(x0);return *this;}Handle &y(int y0) {if (up->u_ != 1) { // 如果引用计数不为1,说明还有其他对象引用它,那么复制一个新的 up,使其引用计数为 1--up->u_;up = new UPoint(up->p_);}up->p_.y(y0);return *this;}private:// 添加的UPoint *up;
};

这一技术通常成为 copy on write(写时复制),当需要改变对象的时候,才开始复制对象。

优点:只有在绝对必要时才进行复制,从而避免了不必要的复制。而且额外开销也只有一点,在涉及句柄的类库中,这一技术经常用到。

6.8 讨论

句柄所绑定的对象,如果打算用于面向对象编程,通常句柄也应该有指针语义。但另一方面,绝大多数支持变长字符串的语言都提供数语义。

c++ 沉思录笔记——句柄(第一部分)相关推荐

  1. C++代理类,句柄(智能指针)_C++沉思录笔记

    代理类 首先定义三个类: class Animal{ public:virtual void getName()=0;virtual void clone()=0; };class Cat:publi ...

  2. 《C++沉思录》读书笔记

    <C++沉思录>读书笔记 序幕 动机 第1章 为什么我用C++ 第2章 为什么用C++工作 第3章 生活在现实世界中 类与继承 第4章 类设计者的核查表 第5章 代理类 第6章 句柄:第一 ...

  3. C++沉思录-句柄类1

    看了下<C++沉思录>第六章的内容介绍的是句柄第一部分,采用引用计数器的方式减少内存的拷贝 动手敲了下代码加深点印象,加了点注释 class Point { public:     /// ...

  4. 《C++ 沉思录》学习笔记——上篇

    文章目录 1. 总结(31-32) 1.1 通过复杂性获取简单性(31) 1.1.1 类库和语言语义 1.1.2 抽象和接口 1.2 说了 Hello world 后再做什么(32) 2. 技术(27 ...

  5. 读书笔记∣概率论沉思录 01

    概率的解释基础分为两种,一是物理世界本身存在的随机性(客观概率),二是是我们由于信息不足而对事件发生可能性的度量(主观概率).基于此,形成了概率论的两大学派:频率论学派(传统数理统计学)和贝叶斯统计学 ...

  6. 《C++沉思录》学习笔记1

    文章目录 前言 一.类 二.改进,实现关闭跟踪输出的功能 三.改进,实现跟踪输出到不同设备的功能 四.不用类来实现上述功能 C++优秀的面向对象的核心本质 参考资料 前言 作为高级语言,C已经很强大了 ...

  7. 《C++沉思录 第2版》

    内容简介: <C和C++经典著作•C++沉思录:Ruminations on C++>基于作者在知名技术杂志发表的技术文章.世界各地发表的演讲以及斯坦福大学的课程讲义整理.写作而成,融聚了 ...

  8. 00. 微服务架构沉思录

    微服务架构沉思录 1. 微服务架构沉思录 1.1 微服务架构中的进程间通信 1.1.1 进程间通信技术 1.1.1.1 基于同步请求响应的通信机制 1.1.1.1.1 REST 的好处 1.1.1.1 ...

  9. 整理2年多的沉思录-人生说

    这是学习笔记的第 2471篇文章 我对于"沉思录"这个名词有着很深的执念,曾经想写一本DBA沉思录题材的书,也做过一些有关沉思录名词的技术演讲.马可·奥勒留的伟大之处则在于他在很早 ...

最新文章

  1. 日常动手之:用python画行情图
  2. Vue多个组件映射到同一个组件,页面不刷新?
  3. 洛谷 P3835: 【模板】可持久化平衡树
  4. Kali Linux 更新了1.0.9a,第一时间分享
  5. 逗号,句号。问号?叹号!顿号、冒号:人名分隔·
  6. 【JavsScript】webapp的优化整理
  7. java.net.SocketException: Connection reset 问题分析
  8. php 服务器监控源码,PHP自动Get监控源码
  9. Mac OS 电信3G上网设置
  10. 二维码解析易语言代码
  11. 大学生学科竞赛管理系统/竞赛管理系统的设计与实现
  12. c++ 容器、继承层次、句柄类
  13. 3.#技术|Android抓包的原理分析和实际操作,fiddler+Xposed+TrustMe++
  14. Flink 实战问题(五):The transaction timeout is larger than the maximum value allowed by the broker
  15. 一加5t Android 第三方系统上BL锁
  16. 联想笔记本电脑主板分析与维修(型号G50-70M版号NM-A273) 问题:按电源开关不开机无反应,充电指示灯不亮
  17. 有限新息率FRI信号模型
  18. ArcGIS 地理数据库(GDB)/Tolerance/Resolution简介
  19. ipynb转化py文件
  20. [译] 讨论 JS ⚡:文档

热门文章

  1. 为什么粘滞定位失效问题
  2. 好用的三维绘图软件学习绘制圆柱体和长方体
  3. 科达 MTS转码服务器 任意文件读取漏洞
  4. 分享24个上传下载 和32个社区论坛ASP.NET源码,总有一款适合您
  5. Docker + fastapi (macOS)
  6. 简单剖析学校打卡网页的定位方式
  7. 常见的百度搜索指令:site,domain,intitle,inurl
  8. 计算机无法上网的软件故障,win10系统部分软件无法正常连网的原因及解决方法...
  9. 联想td350服务器做系统,ThinkServer TD350服务器_ThinkServer TD350 S2609v3 R110i _服务器知识学堂-中关村在线...
  10. go语言提取html,Go语言使用正则表达式提取网页文本