《C++ Primer》学习笔记(十三):拷贝控制

2023-11-11

拷贝、赋值与销毁

拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。虽然可以定义一个接受非const引用的拷贝构造函数,但是一般都是使用const引用作为参数。拷贝构造函数在几种情况下都会被隐式调用,因此一般不应是explicit的。

拷贝赋值运算符

class Foo{
public:
	Foo& operator=(const Foo&);//赋值运算符
}

赋值运算符通常应该返回一个指向其左侧运算对象的引用。

析构函数

析构函数释放对象使用的资源,并销毁对象的非static数据成员。

三/五法则

如果一个类需要一个析构函数时,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。

如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个拷贝赋值运算符,反之亦然。但是并不意味着需要自定义析构函数。

使用=default

可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合适的版本。当我们在类内=default修饰成员的声明时,合成的函数将隐式地声明为内联的(就像其他任何类内声明的成员函数一样)。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default

class Sales_data{
public:
	Sales_data() = default;
	Sales_data(const Sales_data&) = default;
	Sales_data& operator=(const Sales_data &);
	~Sales_data() = default;
}
//在类外定义使用=default合成的成员函数就不是默认内联的
Sales_data& Sales_data::operator=(const Sales_data&) = default;

阻止拷贝

对某些类来说,拷贝操作没有合理的意义。例如iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。

可以通过在函数的参数列表后面加上=delete来指出我们希望将该函数定义为删除的。

struct NoCopy{
	NoCopy() = default; //使用合成的默认构造函数
	NoCopy(const NoCopy&) = delete; //阻止拷贝
	NoCopy &operator=(const NoCopy&) = delete; //阻止赋值
	~NoCopy() = default; //使用合成的析构函数
}

=default不同,=delete必须出现在函数第一次声明的时候。

不能删除析构函数。

拷贝控制和资源管理

行为像值的类

为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。

类值版本的HasPtr如下所示:

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

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
	auto newp = new string(*rhs.ps); //拷贝底层string
	delete ps; //释放旧内存
	ps = newp; //从右侧运算对象拷贝数据到本对象
	i = rhs.i;
	return *this; //返回本对象
}

定义行为像指针的类

除了使用shared_ptr类来设计外,还可以设计自己的引用计数,将计数器保存在动态内存中。

class HasPtr{
public:
	//构造函数分配新的string和新的计数器,将计数器置为1
	HasPtr(const std::string &s = std::string()):
		ps(new std:string(s)), i(0), use(new std::size_t(1)) { }

	HasPtr(const HasPtr &p):
		ps(p.ps), i(p.i), use(p.use) {++*use;}

	HasPtr& operator=(const HasPtr&);

	~HasPtr();
private:
	std::string *ps;
	int i;
	std::size_t *use; //用来记录有多少个对象共享*ps的数据
}

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;//返回本对象
}

HasPtr::~HasPtr()
{
	if(--*use == 0){//如果引用计数变为0
		delete ps; //释放string内存
		delete use; //释放计数器内存
	}
}

交换操作

可以在自己的类上定义自己版本的swap,典型实现如下:

class HasPtr{
	friend void swap(HasPtr& , HasPtr&);
	//其他成员不变
}

inline void swap(HasPtr &lhs, HasPtr &rhs)
{
	using std::swap;
	//如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本
	swap(lsh.ps, rhs.ps); //交换指针,而不是string数据
	swap(lhs.i, rhs.i);//交换int成员
}

定义了swap的类通常用swap来定义它们的赋值运算符。使用了一种名为拷贝并交换 的技术,将左侧对象与右侧对象的一个副本进行交换。通过在改变左侧对象之前拷贝右侧运算对象保证了自赋值的正确。

//注意rhs是按值传递的,意味着HasPtr的拷贝构造函数
//将右侧运算对象中的`string`拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
	//交换左侧运算对象和局部变量rhs的内容
	swap(*this, rhs);//rhs现在指向本对象曾经指向的内存
	return *this; //rhs被销毁,从而delete了rhs中的指针
}

对象移动

在某些情况下,对象拷贝后就立刻被销毁了,这时移动而非拷贝对象能够大幅度提高性能。使用移动的另一个原因源于IO类或unique_ptr这样的类,这些类都包含不能被共享的资源(如指针或IO缓冲)。

标准库容器、stringshared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但是不能拷贝。

右值引用

为了支持移动操作,引入了一种新的引用类型–右值引用。通过&&来获得右值引用。右值引用只能绑定到一个将要销毁的对象。右值引用必须绑定到一个右值上。

int i = 42;
int &r = i;//正确
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i*42; //错误:i*42是一个右值
const int &r3 = i * 42; //正确:可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42; //正确:右值引用绑定到右值上

虽然不能将右值引用直接绑定到一个左值上,但可以显式地通过move来将一个左值转换为对应的右值引用,move定义在头文件utility中。

int &&rr3 = std::move(i); //ok

调用move就意味着我们将不再使用它,我们不能对移后源对象的值做任何假设。我们可以销毁一个移后源对象,也可以赋值它新值,但不能使用一个移后源对象的值。

注意:使用到move的代码应该使用std::move而不是move,这样可以避免潜在的命名冲突。

移动构造函数和移动赋值运算符

移动构造函数需要确保移后源对象处于这样一个状态:销毁它是无害的。

StrVec::StrVec(StrVec &&s) noexcept //移动操作不应抛出任何异常
	:elements(s.elements), first_free(s.first_free), cap(s.cap)
{
	//令s进入这样的状态:对其运行析构函数是安全的
	s.elements = s.first_free = s.cap = nullptr;
}

注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept

StrVec &StrVec::operator=(StrVec &&rhs) noexcept 
{
	//直接检测自赋值
	if(this != &rhs)
	{
		free(); //释放已有元素
		elements = rhs.elements;//从rhs接管资源
		first_free = rhs.first_free;
		cpa = rhs.cap;

		//令rhs进入这样的状态:对其运行析构函数是安全的
		rhs.elements = rhs.first_free = rhs.cap = nullptr;
	}
	return *this;
}

只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。

注意:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。

建议::不要随意使用移动操作。通过在类代码中小心地使用move,可以大幅度提升性能。但是如果随意在普通用户代码(与类实现代码相对)中使用移动操作,很可能导致莫名其妙的难以查找的错误。

右值引用和成员函数

除了构造函数和赋值运算符外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。

区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&

练习

  1. 解释下面的声明为什么是非法的:
Sales_data::Sales_data(Sales_data rhs);

此为一个类的拷贝构造函数,作为函数其非引用类型的参数需要进行拷贝初始化,但拷贝初始化又要调用拷贝构造函数以拷贝实参,但为了拷贝实参又需要调用拷贝构造函数,无限循环。

  1. 假定Point是一个类类型,它有一个public的拷贝构造函数,指出下面程序片段中哪些地方使用了拷贝构造函数:
Point global;
Point foo_bar(Point arg)
{
	Point local = arg, *heap = new Point(global);
	*heap = local;
	Point pa[4] = {local, *heap};
	return *heap;
}
  • foo_bar函数的参数为非引用类型,会用到拷贝构造函数。
  • 函数的返回类型为非引用类型,也需要使用拷贝构造函数。
  • 在函数体中local = arg*heap = new Point(global)Point pa[4] = {local, *heap}皆会使用到拷贝构造函数。
  • *heap = local会用到拷贝赋值运算符。
  1. 编写标准库string的简化版本,命名为String。你的类应该至少有一个默认构造函数和一个接受C风格字符串指针参数的构造函数。使用allocator为你的String类分配所需内存。
#include <iostream>  
#include <algorithm>
#include <memory>
using namespace std;
 
class String{
public:
	String(): elements(nullptr), end(nullptr) {}; 

	String(const char *p)
	{
		auto p1 = const_cast<char *>(p);
		while(*p1){
			++p1;
		}
		alloc_n_copy(p, p1);
	}

	String(const String&);
	String& operator=(const String&);
	~String(){
		if (elements)
		{
			for_each(elements, end, [this](char &rhs){alloc.destroy(&rhs);});
			alloc.deallocate(elements,end-elements);
		}
	}

private:
	static std::allocator<char> alloc; //分配内存的方法
	char *elements; //首指针
	char *end;

	std::pair<char*, char*> alloc_n_copy(const char*a, const char*b)
	{
		auto p1 = alloc.allocate(b-a);//allocate参数为分配内存的大小,这里分配b-a个char类型所占用的内存大小,返回首指针
		auto p2 = uninitialized_copy(a, b, p1);//将a到b之间的元素拷贝至p1,返回的是最后一个构造元素之后的位置
		return make_pair(p1, p2); //返回分配空间的首尾指针
	}

	void range_initializer(const char*c, const char*d) //初始化
	{
		auto p = alloc_n_copy(c,d);//拷贝并初始化新的string
		elements = p.first;
		end = p.second;
	}
};
  1. 如果sorted定义如下,会发生什么?
Foo Foo::sorted() const &{
	Foo ret(*this);
	return ret.sorted();
}

sorted不断调用自身,无限循环,造成堆栈溢出。

  1. 如果sorted定义如下,会发生什么?
Foo Foo::sorted() const & {return Foo(*this).sorted();}

会调用Foo Foo::sorted() &&

  1. 编写新版本的Foo类,其sorted函数中有打印语句,测试这个类,验证前两题的答案
#include <vector>
#include <iostream>
#include <algorithm>
 
using std::vector;
using std::sort;
 
class Foo {
public:
	Foo sorted()&&;
	Foo sorted() const&;
 
private:
	vector<int> data;
};
 
Foo Foo::sorted() &&
{
	sort(data.begin(), data.end());
	std::cout << "&&" << std::endl; // debug
	return *this;
}
 
Foo Foo::sorted() const &
{
	//    Foo ret(*this);
	//    sort(ret.data.begin(), ret.data.end());
	//    return ret;
 
	std::cout << "const &" << std::endl; // debug
 
	//    Foo ret(*this);
	//    ret.sorted();     //13.56
	//    return ret;
 
	return Foo(*this).sorted(); //13.57
}
 
int main()
{
	Foo().sorted(); // call "&&"
	Foo f;
	f.sorted(); // call "const &"
	
	system("pause");
	return 0;  
}

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《C++ Primer》学习笔记(十三):拷贝控制 的相关文章

随机推荐

  • Java web 实现页面显示数据库表格

    运行错误显示 ERROR IllegalAccessException for stop method in class org apache tomcat maven plugin tomcat7 run ExtendedTomcat 目
  • 什么是死锁?如何避免和预防死锁。

    死锁指的是两个进程在执行过程中因争夺资源而造成的僵局 当进程处于死锁状态时 他们就不能继续执行 直到外部程序的干预或者自行放弃 预防和避免的措施 1 避免资源独占 尽量避免一个进程获取了某些资源后再次请求其他资源 而应该将所有资源一次性申请
  • linux debug技巧和工具

    linux debug技巧和工具 print 优点 简单 直接 灵活运用二分法思想 缺点 需要重新编译 运行 比较费时 gdb starting the program stop at specified locations stop on
  • Vue部署提高页面访问速度,nginx代理

    文章目录 1 概述 2 步骤 3 预估加载速度对比 vue ui 1 概述 在没有压缩本地js css的文件下 部署线上环境时 访问页面加载极慢 网上搜了一下 果然有相应的解决办法 特此记录一下 还可以用cdn的方式 后面再看 2 步骤 在
  • 【毕业设计】前后端分离——实现登录注册功能

    据说 看我文章时 关注 点赞 收藏 的 帅哥美女们 心情都会不自觉的好起来 前言 作者简介 大家好我是 user from future 意思是 来自未来的用户 寓意着未来的自己一定很棒 个人主页 点我直达 在这里肯定能找到你想要的 专栏介
  • hql连表查询(多表查询)

    hql连表查询的问题 总结了一下 与大家分享 package android com bzjm test import java util List import org hibernate HibernateException impor
  • geforce experience不能登录_火炬之光2居然也要登录NS了?

    完美世界旗下已经倒闭了的Runic Games表示 火炬之光2 和 迷城之光 会登录NS 那么既然Runic Games已经倒闭了 这条消息自然是完美世界自己发出来的 而负责移植 火炬之光2 的是著名的Switch游戏移植大户Panic B
  • File分隔符挺有意思的

    package Test5 import java io File author xlj public class RemoteFile public static void main String args throws Exceptio
  • jmu-python-随机生成密码(一行代码生成题目要求的字符列表)

    jmu python 随机生成密码 题目 答案 初始版 优化版 一行代码生成题目要求的字符列表 总结 题目 答案 初始版 import random x eval input n eval input m eval input str ab
  • python PyQt5 Qt Designer 学习笔记

    转化代码 pyuic5 o untitled py untitled ui cd 目录 main 文件 from PyQt5 QtWidgets import QApplication QMainWindow import sys from
  • 【问题解决】Failed to load module script: Expected a JavaScript module script but the server respond

    全部错误内容 xxx 86eb4fa7 js 1 Failed to load module script Expected a JavaScript module script but the server responded with
  • 分类与回归树(CART)- 机器学习ML

    参考 1 统计学习方法 李航 2 https www cnblogs com en heng p 5035945 html 3 http blog csdn net baimafujinji article details 53269040
  • MindMap: Knowledge Graph Prompting Sparks Graph of Thoughts in Large Language Models

    本文是LLM系列文章 针对 MindMap Knowledge Graph Prompting Sparks Graph of Thoughts in Large Language Models 的翻译 思维导图 大型语言模型中的知识图谱提
  • 深度学习实战15(进阶版)-让机器进行阅读理解+你可以变成出题者提问

    大家好 我是微学AI 今天给大家带来一个机器阅读理解的项目 利用ERNIE的预训练模型进行微调训练 添加自己的数据集进行训练 训练好就可以利用功能进行阅读式信息抽取啦 也可以问机器一些简单的问题进行抽取 今天采用的paddle深度学习框架
  • Mybatis-plus

    1 简介 Mybatis plus是mybatis的增强工具 在mybatis的基础上只做增强不做改变 是为了简化开发和提高效率而生 mybatis plus只作用于单表的增删改查 联表操作还需要mybatis 2 Mybatis plus
  • elasticserach(一)

    文章目录 1 elasticsearch简介 1 1 正向索引和倒排索引 1 2 文档 1 3 索引和映射 1 4 elasticsearch与mysql概念对比 2 部署单点es和kibana 2 1 创建网络 2 2 拉取镜像 2 3
  • 编译原理基础知识+笔记(1)

    一 编译原理概述 1 翻译程序 把某一种语言程序 称为源语言程序 等价地转换成另一种语言程序 称为目标语言程序 的程序 2 编译程序 把某一种高级语言程序等价地转换成另一种低级语言程序 如汇编语言或机器语言程序 的程序 又分为 诊断编译程序
  • 阿里4年测试经验分享 —— 测试外包干了3年后,我废了...

    去年国庆 我分享了一次一位阿里朋友的技术生涯 大家反响爆蓬 感觉十分有意思 今天我来分享一下我另一位朋友的真实经历 是不是很想听 没错 我这位朋友是曾经外包公司的测试开发 而且一干就是三年 三年后 他说他废了 虽说废的不是很彻底 但这三年他
  • 分分钟提高效率的18个神仙网站,你都用过几个?

    1 ipaddress ip查询工具 https www ipaddress com 不知道本机IP 一进入网址就可以查到 还能查询到ip的详情信息 追踪域名 端口信息 2 json 在线解析工具 https www json cn 开发时
  • 《C++ Primer》学习笔记(十三):拷贝控制

    C Primer 学习笔记 十三 拷贝控制 拷贝 赋值与销毁 拷贝构造函数 拷贝赋值运算符 析构函数 三 五法则 使用 default 阻止拷贝 拷贝控制和资源管理 行为像值的类 定义行为像指针的类 交换操作 对象移动 右值引用 移动构造函