定义行为像值的类


行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr。
在实现之前,我们需要:
定义一个拷贝构造函数,完成string的拷贝,而不是指向string的指针的拷贝;
定义一个拷贝赋值运算符,释放当前的string,并从右侧拷贝新的string。
定义一个析构函数来释放string

class HasPtr {

public:
HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) {} HasPtr(const HasPtr& has) : ps(new std::string(*has.ps)), i(has.i) {} HasPtr& operator = (const HasPtr& has); ~HasPtr() { delete ps; }
private:
std::string* ps;
int i;
}; HasPtr::operatpr = (const HasPtr& has) {
// 先定义一个局部变量保存右侧对象,如果右侧对象和左侧对象是同一个对象,那么先删除右侧对象就会发生错误
auto newPtr = new std::string(*has.ps);
// 此时再释放左侧对象资源
delete ps;
ps = newPtr;
i = has.i;
}

拷贝赋值运算符的实现中,为了安全起见,必须先将右侧对象拷贝到一个局部变量里,然后再删除左侧对象,然后将局部变量赋值给左侧对象。如果先删除左侧对象,此时右侧对象和左侧对象是同一个对象时,就会发生严重错误。

对于一个赋值运算符来说,一个好的方法是在左侧对象销毁之前拷贝右侧对象,因为左侧对象和右侧对象可能是同一个对象。



定义行为像指针的类

行为像指针的类,例如智能指针shared_ptr。既然要让类的行为像一个指针,那么这个类就不能在执行析构函数时简单的释放相关联的string,如果只有一个对象指向这个string时,我们才可以在这个类的析构函数中释放资源,如果有多个类指向这个string,那么其中一个类释放了这个string,其他的类就不能再复用。
因此就要用到引用计数的方法来解决这个问题。


引用计数的工作方式如下:
  • 除了初始化对象之外,每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象共享正在创建的对象共享状态,当创建一个对象时,引用计数为1,因为此时只有一个对象共享。
  • 拷贝构造函数不分配新得引用计数器,拷贝给定对象的数据成员,包括引用计数器,拷贝构造函数递增共享的计数器,表示给定对象更的状态又被一个新用户所共享
  • 拷贝赋值运算符递减左侧运算对象的引用计数器,递增右侧对象的引用计数器,如果左侧对象的引用计数器为0,则销毁左侧对象。
  • 析构函数判断引用计数是否为0,如果为0,则销毁左侧对象。

引用计数的实现:我们假设有下面的情况:
HasPtr h1;
HasPtr h2(h1);
HasPtr h3(h1);

HasPtr是一个行为像指针的类,新创建的h1的引用计数为1,创建h2,用h1初始化h2,会递增h1的引用计数值,此时h2保存了h1中的引用计数,在创建h3的时候,递增了h1的引用计数值,而且我们必须做的是要更新h2中的引用计数值,此时无法更新h2中的引用计数值。因此,我们需要将引用计数保存在动态内存中,这样原对象和其他副本对象都会指向相同的计数器,这样就可以自动更新引用计数在每个共享对象中的状态。



class HasPtr {

public:
HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0), use(new size_t(1)) {} HasPtr(const HasPtr& has) : ps(has.ps), i(has.i), use(has.use) { ++ *use;} HasPtr& operator = (const HasPtr& has); ~HasPtr();
private:
std::strin g* ps;
int i;
size_t* use; // 引用计数
}; HasPtr::HasPtr& operator = (const HasPtr& has) { ++ *has.use;
if (0 == *use) {
delete ps;
delete use;
} ps = has.ps;
i = has.i;
use = has.use;
return *this;
} HasPtr::~HasPtr() { if (--*use == 0) {
delete ps;
delete use;
}
}

注意的是,我们在拷贝一个HasPtr时,拷贝的是ps本身而不是ps指向的string。




交换操作

通常,管理资源的类除了定义拷贝控制成员之外,还会定义交换操作的函数swap。理论上来说,我们的swap函数应该是这样的:

HasPtr temp = 1;
v1 = v2;
v2 = temp;

这样的代码将v1中string拷贝了两次,但是这样做是没有必要的,我们希望swap交换指针,而不是分配string的副本:

std::string* temp = v1.ps;
v1.ps = v2.ps;
v2.ps = temp;
v1.i = v2.i;

我们将swap函数声明为HasPtr类的友元函数,这样swap就能访问HasPtr的ps和i成员:

class HasPtr {

	friend void swap(HasPtr&, HasPtr&);
// 其他定义
}; inline
void swap(HasPtr& lhs, HasPtr& rhs) {
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.i, rhs.i);
}

在swap函数中:

使用了using std::swap,如果这个类有自己的swap函数,匹配程度会高于标准库swap,会优先使用类自己的swap,如果没有,则使用标准库的swap。
swap里交换类的指针和int成员,并不会发生递归循环,HasPtr的数据成员是内置类型的,这时候会调用标准库版本的swap。


在赋值运算符中使用swap

通常,一个定义了swap的类用它swap函数来定义拷贝赋值运算符,这种运算符使用了一种拷贝并交换的技术。

HasPtr& operator = (HasPtr has) {

	swap(*this, has);
return *this;
}

在进行HasPtr类的赋值运算中,先将右侧对象拷贝到拷贝赋值运算符函数里,然后交换左侧对象的指针和右侧对象的指针,交换后,右侧对象赋值给了左侧对象,左侧对象相应的string指针也指向了右侧对象副本的对应成员,而右侧对象的string指针则指向了左侧对象的相应成员。在这个函数结束后,右侧对象的副本被销毁,于是原来左侧对象的资源被释放,而左侧对象现在保存的是右侧对象的成员。

拷贝并交换的操作,和之前的拷贝赋值运算符的实现原理是相同的,在改变左侧对象之前拷贝右侧对象。保证了这样的操作异常的安全。



C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理的更多相关文章

  1. C++ Primer : 第十三章 : 拷贝控制之对象移动

    右值引用 所谓的右值引用就是必须将引用绑定到右值的引用,我们通过&&来绑定到右值而不是&, 右值引用只能绑定到即将销毁的对象.右值引用也是引用,因此右值引用也只不过是对象的别名 ...

  2. C++ Primer : 第十三章 : 拷贝控制之拷贝、赋值与销毁

    拷贝构造函数 一个构造函数的第一个参数是自身类类型的引用,额外的参数(如果有)都有默认值,那么这个构造函数是拷贝构造函数.拷贝构造函数的第一个参数必须是一个引用类型. 合成的拷贝构造函数   在我们没 ...

  3. C++Primer 第十三章

    //1.当定义一个类时,我们显示地或隐式地指出在此类型的对象(注意这里是此类型的对象,而不包括此类型的指针)拷贝,移动,赋值,销毁时做什么.一个类通过定义五种特殊的成员函数来控制这些操作:拷贝构造函数 ...

  4. 【C++ Primer 第十三章】4. 拷贝控制示例

    拷贝控制示例 #include<iostream> #include<string> #include<set> #include<vector> us ...

  5. C++ Primer : 第十三章 : 拷贝控制示例

    /* Message.h */ #ifndef _MESSAGE_H_ #define _MESSAGE_H_ #include <iostream> #include <strin ...

  6. C++ Primer : 第十三章 : 动态内存管理类

    /* StrVec.h */ #ifndef _STRVEC_H_ #define _STRVEC_H_ #include <memory> #include <string> ...

  7. 【C++ Primer 第13章】1. 拷贝控制、赋值和销毁

    拷贝控制.赋值和销毁 如果一个构造函数的第一个参数是自身类的引用,且额外的参数都有默认值,则此构造函数是拷贝控制函数(拷贝构造函数不应该是explicit的). 如果我们没有为一个类定义拷贝构造函数, ...

  8. [C++ Primer] : 第13章: 拷贝控制

    拷贝, 赋值与销毁 当定义一个类时, 我们显示地或隐式地指定在此类型的对象拷贝, 移动, 赋值和销毁时做什么. 一个类通过定义5种特殊的成员函数来控制这些操作, 包括: 拷贝构造函数, 拷贝赋值运算符 ...

  9. 【C++】《C++ Primer 》第十三章

    第十三章 拷贝控制 定义一个类时,需要显式或隐式地指定在此类型地对象拷贝.移动.赋值和销毁时做什么. 一个类通过定义五种特殊的成员函数来控制这些操作.即拷贝构造函数(copy constructor) ...

随机推荐

  1. 【树莓派】基于TinyProxy搭建HTTP代理服务器

    一.前言 关于为什么要玩玩HTTP代理就不用我多说了.  二.搭建环境 * Linux laptop 2.6.32-45-generic #100-Ubuntu SMP Wed Nov 14 10:4 ...

  2. iOS - iOS 适配

    前言 什么是适配: 适应.兼容各种不同的情况. iOS 开发中,适配的常见种类: 1)系统适配, 针对不同版本的操作系统进行适配. 2)屏幕适配,针对不同大小的屏幕尺寸进行适配. iPhone 的尺寸 ...

  3. sizeof()和strlen()

    sizeof计算的是栈中大小 P { margin-bottom: 0.21cm; direction: ltr; color: rgb(0, 0, 0); text-align: justify } ...

  4. JVM学习——编译OpenJDK

    最近在学习<深入理解java虚拟机 第二版>这本书.书中第一部分建议大家自己编译OpenJDK.抱着学习态度也来编译个玩一玩.下面进入正题. 1.编译环境介绍 操作系统 CentOS Li ...

  5. ToolStrip添加自定义的DateTimePicker

    直接新建一个类,代码如下: [ToolStripItemDesignerAvailability(ToolStripItemDesignerAvailability.All), DefaultEven ...

  6. Naive Bayes理论与实践

    Naive Bayes: 简单有效的常用分类算法,典型用途:垃圾邮件分类 假设:给定目标值时属性之间相互条件独立 同样,先验概率的贝叶斯估计是 优点: 1. 无监督学习的一种,实现简单,没有迭代,学习 ...

  7. MVC4中的Display Mode简介

    本文地址:http://www.cnblogs.com/egger/p/3400076.html  欢迎转载 ,请保留此链接๑•́ ₃•̀๑! 今天学习MVC4时,看到一个不错的特性"vie ...

  8. putty自动登录

    如果没有公钥/密钥对,就用 PuTTYgen 创建一个,已经有了就可以忽略这一步.一个公钥/密钥对可以用在不同的服务器上,所以也不需要重复创建,关键要有足够强健的密码和安全的存放. 象先前一样输入帐户 ...

  9. Java 编程入门(词汇表)

    抽象类(abstract class):抽象类不能创建对象,主要用来创建子类.Java中的抽象类使用 abstract 修饰符定义. 抽象数据类型(abstract data type ADT):抽象 ...

  10. osg,vtk,ogre的区别

    osg使用过一年,阅读过一部分源代码,vtk也断续使用过三四年了,ogre研究的比较深入,基本上比较熟悉它的整体结构,说说个人的看法 vtk是一个算法库,里面包括了很多挺不错的算法,如果做有限元云图, ...