为了定义这些成员,我们首先必须确定此类型对象的拷贝语义。一般来说,有两种选择:可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针。

  1. 类的行为像一个,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然。
  2. 行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然。

13.2.1 行为像值的类

定义一个HasPtr类,该类需要

  • 定义一个拷贝构造函数,完成string的拷贝,而不是拷贝指针
  • 定义一个析构函数来释放string
  • 定义一个拷贝赋值运算符来释放对象当前的 string,并从右侧运算对象拷贝string

HasPtr的类值版本如下:

class HasPtr{
public:
HasPtr(const &string &s = string()) :
ps(new string(s)), i(0){}
//对于ps指向的string,每个HasPtr都有自己的拷贝
HasPtr(const HasPtr &p):
ps(new string(*p.ps)), i(p.i){}
HasPtr& operator=(const HasPtr &p);
~HasPtr() {delete ps;} //记得释放内存
private:
string *ps;
int i;
};

类拷贝赋值运算符的编写

赋值运算符组合了析构函数和构造函数的操作

这些操作需要以正确的顺序执行

  1. 先拷贝右侧对象

    • 处理自赋值情况
    • 异常安全
  2. 释放左侧对象的资源
  3. 更新左侧对象
HasPtr& HasPtr::operator=(const HasPtr &rhs){
string *newp = new string(*rhs.ps);//拷贝底层string
delete ps; //释放旧内存
ps = newp; //更新左对象
i = rhs.i;
return *this; //返回本对象
}

在定义赋值运算符时要注意两点:

  • 能处理自赋值。
  • 大多数赋值运算符组合了析构函数和拷贝构造函数的工作。

如果操作顺序错误,先销毁左侧对象再拷贝,就无法处理自赋值

13.2.2 定义行为像指针的类

需要定义拷贝构造函数和拷贝运算符,拷贝指针而不是string

析构函数要释放string,在本例中,只有当最后一个指向string的ps被销毁后才能释放底层string的内存,需要用到shared_ptr

使用shared_ptr的关键作用之一是引用计数,记录指向对象的指针有多少个,在此我们不直接使用shared_ptr,而是自主实现引用计数,加强理解

引用计数

工作方式

  • 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
  • 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
  • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。

在哪里存放计数器?

不能作为类的成员

HasPtr p1("Hi");
HasPtr p2(p1);
HasPtr p3(p1);

创建p3时,我们可以递增p1的计数器并把它拷贝到p3中,但是,p2的计数器没有变化

所以:要让所有对象共享计数器的底层数据,需要把计数器定义在动态内存中

定义一个使用引用计数的类

指针版本的HasPtr

class HasPtr{
public :
//构造函数分配新的构造器,初始化为1
HasPtr(const string &s = string()) :
ps(new string(s)), i(0), use(new int(1)){}
//拷贝构造函数拷贝所有成员,计数器+1
HasPtr(const HasPtr &p) :
ps(p.ps), i(p.i), use(p.use){++*use;}
HasPtr& operator=(const HasPtr&);
~HasPtr();
private :
string *ps;
int i;
int *use; //记录有多少个共享*ps的对象
};
//析构函数
HasPtr::~HasPtr(){
//递减计数器,计数==0时释放内存
if(--*use == 0){
delete ps; //释放string内存
delete use; //释放计数器内存
}
}
//拷贝赋值
HasPtr& HasPtr::operator=(const HasPtr& rhs){
++*rhs.use; //递增右侧引用计数
if(--*use == 0){ //递减左侧
delete ps;
delete use;
}
ps = rhs.ps; //更新左侧数据
i = rhs.i;
use = rhs.use;
return *this; //返回本对象
}

13-2 c++拷贝控制和资源管理的更多相关文章

  1. 【C++ Primer 第13章】2. 拷贝控制和资源管理

    拷贝控制和资源管理 • 类的行为像一个值.意味着它应该有自己的状态,当我们拷贝一个像值得对象时,副本和原对象是完全独立的,改变副本不会对原对象有任何影响. • 行为像指针的类则共享状态.当我们拷贝一个 ...

  2. C++ 拷贝控制和资源管理,智能指针的简单实现

    C++ 关于拷贝控制和资源管理部分的笔记,并且介绍了部分C++ 智能指针的概念,然后实现了一个基于引用计数的智能指针.关于C++智能指针部分,后面会有专门的研究. 通常,管理类外资源的类必须定义拷贝控 ...

  3. [C++]类的设计(2)——拷贝控制(拷贝控制和资源管理)

      1.类的行为分类:看起来像一个值:看起来想一个指针.     1)类的行为像一个值,意味着他应该有自己的状态.当我们拷贝一个像值的对象时,副本和原对象是完全独立的.改变副本不会对原有对象有任何影响 ...

  4. C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理

    定义行为像值的类 行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr. 在实现之前,我们需要: 定义一个拷贝构造函数,完成stri ...

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

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

  6. 【c++ Prime 学习笔记】第13章 拷贝控制

    定义一个类时,可显式或隐式的指定在此类型对象上拷贝.移动.赋值.销毁时做什么.通过5种成员函数实现拷贝控制操作: 拷贝构造函数:用同类型的另一个对象初始化本对象时做什么(拷贝初始化) 拷贝赋值算符:将 ...

  7. Chapter13:拷贝控制

    拷贝控制操作:拷贝构造函数.拷贝赋值运算符.移动构造函数.移动赋值运算符.析构函数. 实现拷贝控制操作的最困难的地方是首先认识到什么时候需要定义这些操作. 拷贝构造函数: 如果一个构造函数的第一个参数 ...

  8. 【C++】C++的拷贝控制

    目录结构: contents structure [-] 拷贝.赋值与销毁 拷贝构造函数 拷贝初始化 参数和返回值 拷贝赋值运算符 析构函数 三五法则 拷贝控制和资源管理 交换操作 对象移动 右值引用 ...

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

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

  10. 《C++ Primer》笔记 第13章 拷贝控制

    拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么.拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么.析构函数定义了当此类型对象销毁时做什么.我们称这些操作为拷贝控制 ...

随机推荐

  1. 微服务全链路跟踪:grpc集成jaeger

    微服务全链路跟踪:grpc集成zipkin 微服务全链路跟踪:grpc集成jaeger 微服务全链路跟踪:springcloud集成jaeger 微服务全链路跟踪:jaeger集成istio,并兼容u ...

  2. Java——计算1~N之间所有奇数之和

    2024/07/15 1.题目 2.解题 1.题目 2.解题 import java.util.Scanner; public class Main { public static void main ...

  3. maven依赖拉取小技巧

    依赖对应的dependency搜索库 https://mvnrepository.com/ 前往搜索  点击对应的依赖版本复制xml的代码  然后idea中刷新maven即可拉出来

  4. C++容器算法

    容器算法 <algorithm>是c++自带的容器算法,提供一系列实用的算法.在谈到容器算法,我们大概率会用到谓词predicate,谓词返回的类型是布尔类型(bool)可以是lambda ...

  5. 电商API接口应该如何使用?

    从定义上看,API接口是指预先定义的一组规则和协议,允许不同的软件应用之间相互通信和交换数据. 目前,市场中电商行业用到API接口的场景较多,电商API接口则是专门针对电子商务应用场景所提供的API, ...

  6. Linux 常见编辑器

    命令行编辑器 Vim Linux 上最出名的编辑器当属 Vim 了.Vim 由 Vi 发展而来,Vim 的名字意指 Vi IMproved,表示 Vi 的升级版.Vim 对于新手来说使用比较复杂,不过 ...

  7. Mac 打开软件提示‘“xxx”已损坏,无法打开。您应该将它移到废纸篓。’解决方法

    产生错误的原因是软件没有签名.使用下面的命令给软件签名就好了. sudo xattr -rd com.apple.quarantine /Applications/xxx.app

  8. Maven 换源

    ~/.m2/settings.xml <settings> ... <mirrors> ... <mirror> <id>alimaven</id ...

  9. 通过DashScope API调用将多种模态转换为向量

    本文介绍如何通过模型服务灵积DashScope进行 多模态向量生成 ,并入库至向量检索服务DashVector中进行向量检索. 模型服务灵积DashScope,通过灵活.易用的模型API服务,让各种模 ...

  10. 7.22-27MY&MS&ORA等SQL数据库提权

    7.22-27MY&MS&ORA等SQL数据库提权 思路:在利用系统溢出漏洞无果情况下,可以采用数据库提权: 前提:数据库服务开启,且获得数据库最高权限账号密码:除Access数据库外 ...