《C++ Primer》第13章 拷贝控制(三)

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

13.5 动态内存管理类(P464)

某些类需要在运行时分配可变大小的内存空间。这种类通常可以用使用标准库容器来保存它们的数据。有些时候,我们希望类自己进行内存分配,这种类必须定义自己的拷贝控制成员。

StrVec类的设计

我们将实现标准库 vector 的简化版本,只用于保存 string

标准库 vector 将元素保存在连续内存中。vector 预先分配足够的内存来保存可能需要的元素。vector 每个添加元素的成员函数会检查是否有足够的空间,如果有,成员函数会在下一个可用位置构造一个对象;如果没有,vector重新分配空间,将已有元素移动到新空间中,释放旧空间,并添加新元素

仿照标准库 vectorStrVec 使用 allocator 来获得原始内存,在需要添加成员时使用 allocatorconstruct 成员在原始内存中创建对象;类似的,需要删除一个元素时,使用 allocatordestory 成员来销毁元素。每个 StrVec 有三个指针成员:

  • elements :指向分配的内存中的首元素
  • first_free :指向最后一个实际元素的位置
  • cap :指向分配的内存末尾之后的位置
82ec6f9f51074f76ec7c74a05027a9b

此外,StrVec 还有一个名为 alloc ,类型为 allocator<string>静态成员,以及 4 个工具函数:

  • alloc_n_copy 会分配内存,拷贝一个给定范围中的元素
  • free 会销毁构造的元素,并释放内存
  • chk_n_alloc 保证 StrVec 至少有容纳一个新元素的空间,如果没有则调用 reallocate 来重新分配内存
  • reallocate 负责为 StrVec 分配新内存

StrVec类定义

class StrVec {
public:
	StrVec() :
		elements(nullptr), first_free(nullptr), cap(nullptr){ }
	StrVec(const StrVec &);
	StrVec &operator=(const StrVec &);
	~StrVec();
	void push_back(const string &);
	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }
	string *begin() const { return elements; }
	string *end() const { return first_free; }
private:
	static allocator<string> alloc;
	string *elements;
	string *first_free;
	string *cap;
	void chk_n_alloc() { if (size() == capacity()) reallocate(); }
	pair<string *, string *> alloc_n_copy(const string *, const string *);
	void free();
	void reallocate();
};
void StrVec::push_back(const string &s) {
	// 确保容器有足够的空间容纳新元素
	chk_n_alloc();
	// 在first_free指向的元素中构造s的副本
	alloc.construct(first_free++, s);
}

pair<string *, string *> 
StrVec::alloc_n_copy(const string *b, const string *e) {
	// 分配大小合适的空间
	auto data = alloc.allocate(e - b);
	// 返回拷贝的开始位置和尾后位置
	return { data, uninitialized_copy(b, e, data) };
}

void StrVec::free() {
	// 保证传递给deallocate的是一个先前由allocate返回的指针
	if (elements) {
		for (auto p = first_free; p != elements;) {
			// 逆序销毁旧元素
			alloc.destroy(--p);
		}
		alloc.deallocate(elements, cap - elements);
	}
}

StrVec::StrVec(const StrVec &s) {
	auto newdata = alloc_n_copy(s.begin(), s.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}

StrVec::~StrVec() {
	free();
}

StrVec &StrVec::operator=(const StrVec &s) {
    // 允许自赋值
	auto data = alloc_n_copy(s.begin(), s.end());
	free();
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}

在重新分配内存的过程中移动而不是拷贝元素

在编写 reallocate 成员之前,我们应首先明确它的功能;

  • 为一个新的、更大的 string 数组分配内存
  • 在新内存空间的前一部分构造对象,保存现有元素
  • 销毁原内存空间中的元素,并释放原内存空间

如果我们选择将元素从旧空间拷贝到新空间,会造成额外的开销。

移动构造函数和std::move

通过新标准库引入的两种机制,我们可以避免 string 拷贝。首先,一些如 string 的标准库类定义了移动构造函数,将资源移动到正在创建的对象中,并保证移后源(moved_from)对象仍然保持一个有效、可析构的状态。

第二个机制是名为 move 的标准库函数,定义在头文件 utility 中。关于 move ,目前需要知道两个关键点:在 reallocate 中我们可以通过调用 move 的方式来使用 string 的移动构造函数;我们通常不 using std::move ,而是直接调用 std::move

reallocate成员

void StrVec::reallocate() {
	// 分配原空间两倍大小的新空间
	auto newcapacity = size() ? 2 * size() : 1;
	auto newdata = alloc.allocate(newcapacity);
	auto dest = newdata;
	auto elem = elements;
	// 将数据移动到新内存
	for (size_t i = 0; i != size(); ++i) {
		alloc.construct(dest++, std::move(*elem++));
	}
	free();
	first_free = dest;
	cap = elements + newcapacity;
}  

13.6 对象移动(P470)

新标准的一个最主要特性就是可以移动而非拷贝对象。如果对象拷贝后就被销毁了,移动而非拷贝对象会大幅提高性能

前面提到的 StrVec 类在重新分配内存时使用移动而非拷贝就是一个很好的例子。此外,如 IO 类或 unique_ptr 这些对象不能拷贝,但能移动。

13.6.1 右值引用(P471)

为了支持移动操作,新标准引入了右值引用(rvalue reference)。右值引用是必须绑定到右值的引用,通过 && 来获得右值引用。右值引用有一个重要特性:只能绑定到一个将要销毁的对象。

前面我们提到过,我们不能将普通的左值引用绑定到要求转换的表达式字面常量返回右值的表达式上。右值引用有着完全相反的特性:我们可以将右值引用绑定到上述表达式上,但不能将其直接绑定到左值上:

double pi = 3.14;
double &r1 = pi;    // 正确
double &&r2 = pi;    // 错误
int &r3 = pi;    // 错误
int &r4 = pi;    // 正确
double &r5 = pi + 0.1;    // 错误
double &&r6 = pi + 0.1;    // 正确

左值持久,右值短暂

左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象

变量是左值

变量可以看作一个只有一个运算对象没有运算符表达式,变量表达式都是左值:

int &&rr1 = 42;
int &&rr2 = rr1;    // 错误,表达式rr1是左值

标准库move函数

我们可以通过调用 move 函数获得绑定到左值上的右值引用

int &&rr3 = std::move(rr1);    // 正确

move 告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。调用 move 意味着承诺:除了对 rr1 赋值或销毁外,我们将不再使用它

13.6.2 移动构造函数和移动赋值运算符(P473)

为了让我们自己的类支持移动操作,需要为其定义移动构造函数移动赋值运算符,从给定对象中“窃取资源”而非拷贝资源。

类似拷贝构造函数,移动构造函数的第一个参数是该类型的一个引用,只不过移动构造函数需要一个右值引用,其他的额外参数必须有默认实参。除了完成资源移动外,移动构造函数还要保证移后源对象处于这样一个状态销毁它是无害的

// 移动操作不应抛出异常
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 来承诺函数不抛出异常:

class StrVec{
public:
    StrVec(StrVec &&) noexcept;
};

StrVec::StrVec(StrVec &&s) noexcept: /*成员初始化器*/
{ /*成员函数体*/ }

必须同时在声明和定义中指定 noexcept

不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept 。为什么会有这个要求呢?以标准库容器 vector 为例。vector 保证,如果我们调用 push_back 时发生异常,vector 自身不会发生任何改变。由于 push_back 发生异常通常是在重新分配内存空间的时候,如果我们在重新分配过程中使用移动构造函数,并且在移动中途抛出异常,此时 vector 将不能满足自身保持不变的要求,而使用拷贝构造函数则不会有这个问题。所以除非 vector 知道移动构造函数不会抛出异常,否则再重新分配内存的过程中,它就必须使用拷贝构造函数而非移动构造函数。

移动赋值运算符

StrVec &StrVec::operator=(StrVec &&s) noexcept {
    // 检测自赋值
	if (this != &s) {
		free();    // 释放已有资源
		elements = s.elements;    // 接管资源
		first_free = s.first_free;
		cap = s.cap;
		s.elements = s.first_free = s.cap = nullptr;
	}
	return *this;
}

移后源对象必须可析构

有时在移动操作完成后,源对象会被销毁,所以我们必须保证移后源对象进入一个可析构状态。此外,还应该保证移后源仍然是有效的:可以安全地为其赋予新值,或者可以安全地使用不依赖其当前值。例如,我们从一个 string 对象移动数据后,仍然可以对它执行 emptysize 等操作。

用户不能对移后源对象地值做任何假设。

合成的移动操作

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

这部分懒得写了,建议直接看书 P475

移动右值,拷贝左值

如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。

但如果没有移动构造函数,右值也被拷贝

const 的左值引用也可以绑定到右值上。

拷贝并交换赋值运算符和移动操作

class HasPtr {
public:
    HasPtr(HasPtr &&p) noexcept: ps(p.ps), i(p.i) { p.ps = nullptr; }
	HasPtr &operator=(HasPtr rhs)
    { swap(*this, rhs); return *this; }
};

上面的拷贝并交换赋值运算符实现了拷贝赋值运算符和移动赋值运算符两种功能:当右侧运算对象为右值时,调用移动构造函数;当右侧运算对象为左值时,调用拷贝构造函数。

更新“三/五法则”:五个拷贝控制成员应该看做一个整体。

移动迭代器

新标准库定义了一种移动迭代器(move iterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算的行为来适配此迭代器,移动迭代器的解引用运算符生成一个右值引用

我们通过调用 make_move_iterator 函数将一个普通迭代器转换为一个移动迭代器。

只有在确信移后源对象没有其他用户时,才建议使用移动操作。

13.6.3 右值引用和成员函数(P481)

如果一个成员函数同时提供拷贝和移动版本,它也能从中受益:

void StrVec::push_back(const string &s) {
	chk_n_alloc();
	alloc.construct(first_free++, s);
}

void StrVec::push_back(string && s) {
	chk_n_alloc();
	alloc.construct(first_free++, std::move(s));
}

右值和左值引用成员函数

通常,我们在一个对象上调用成员函数,不管该对象是左值还是右值:

string s1 = "hello", s2 = "world";
auto n = (s1 + s2).find('a');

但这种灵活的使用方式有时可能令人费解:

(s1 + s2) = "wow";    // 语法上成立,因为重载赋值运算符本质上也是成员函数

我们显然希望阻止上述用法。新标准库允许我们在参数列表后放置一个引用限定符(reference qualifier)&&&)来要求成员函数必须由左值对象右值对象调用:

class Foo {
public:
	Foo &operator=(const Foo &) &;
};

Foo &Foo::operator=(const Foo &rhs) &{
	// ...
	return *this;
}

类似 const 限定符,引用限定符只能用于非 static 成员函数,且必须同时出现在声明和定义中。如果一个函数同时拥有 const 限定和引用限定,那么引用限定符必须跟在 const 后面

重载和引用函数

const 限定符一样,我们也可以通过引用限定符重载函数,不同点在于,如果我们定义两个及以上具有相同名字、参数列表的成员函数,就必须对每个函数都加上引用限定符,或者都不加:

class Foo {
public:
	void fun();
	void fun() const;
	void cal();
	void cal() &;    // 错误
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/259135.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

TypeScript【泛型1、泛型2、声明合并、命名空间 、模块1、模块2、声明文件简介】(五)-全面详解(学习总结---从入门到深化)

文章目录 泛型1 泛型2 声明合并 命名空间 模块1 模块2 声明文件简介 泛型1 泛型&#xff08;Generics&#xff09;是指在定义函数、接口或类的时候&#xff0c;不预先指定具体的类型&#xff0c;而在使用的时候再指定类型的一种特性 首先&#xff0c;我们来实现一个函数…

小黑南京归来,参加部里的公务员培训,有点儿社死认识了好多小伙伴的leetcode之旅13. 罗马数字转整数

小黑代码 class Solution:def romanToInt(self, s: str) -> int:chars [M, CM, D, CD, C, XC, L, XL, X, IX, V, IV,I]nums [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]map_ dict((k, v) for k,v in zip(chars, nums))# 字符串长度n len(s)# 结果变量res …

安装Redis+Redis设置成windows下的服务+windows无法启动Redis服务,报错误1067:进程意外终止,解决方法

&#xff08;一&#xff09;安装Redis 官网地址&#xff1a;Redis 不过Redis 的官网不提供 Windows 版本的下载&#xff0c;可以从Github上下载&#xff0c;Windows版本的下载地址&#xff1a; https://github.com/microsoftarchive/redis/releases/ 无需安装&#xff0c;直…

2828. 判别首字母缩略词

2828. 判别首字母缩略词 难度: 简单 来源: 每日一题 2023.12.20 给你一个字符串数组 words 和一个字符串 s &#xff0c;请你判断 s 是不是 words 的 首字母缩略词 。 如果可以按顺序串联 words 中每个字符串的第一个字符形成字符串 s &#xff0c;则认为 s 是 words 的…

比例导引(PNG)-Matlab 程序

本文提供比例导引的matlab程序&#xff0c;想要看理论的可以看书《导弹飞行力学》或者我的博客 比例导引详解 代码 %% 三维比例导引末制导clc;clear; close all;%% 设置导弹初始参数和目标参数% 总步长 length 1000000; x_m zeros(length,1); y_m zeros(length,1); z_m z…

node.js mongoose

目录 官方文档 mongoose Schema Model Query document 关系 官方文档 Mongoose v8.0.3: Getting Started mongoose Mongoose 是一个 Node.js 环境下 MongoDB 的对象建模工具。它提供了一种在应用程序中与 MongoDB 数据库进行交互的方式&#xff0c;使得开发者能够使用…

基于k6和python进行自动化性能测试

摘要&#xff1a;在性能测试中&#xff0c;达到相应的性能指标对于一个软件来说十分重要&#xff0c;在本文中&#xff0c;将介绍一种现代化性能测试工具k6。 import http from k6/http; import { sleep } from k6; export default function () {http.get(https://test-api.co…

JNI 注册

一、 JNI 静态注册 ① 在 Android 的 Java 层定义方法 : 在 MainActivity 类中定义 如下 Native 方法 ; public native String stringFromJNI();② Native 方法实现 : 下面是一个 Native 方法实现的示例 ; extern "C" JNIEXPORT jstring JNICALL Java_kim_hsl_jni_…

JMeter如何进行多服务器远程测试

JMeter是Apache软件基金会的开源项目&#xff0c;主要来做功能和性能测试&#xff0c;用Java编写。 我们一般都会用JMeter在本地进行测试&#xff0c;但是受到单个电脑的性能影响&#xff0c;往往达不到性能测试的要求&#xff0c;无法有效的模拟高并发的场景&#xff0c;那么…

css实现0.5px宽度/高度显——属性: transform: scale

在大多数设备上&#xff0c;实际上无法直接使用 CSS 来精确地创建 0.5 像素的边框。因为大多数屏幕的最小渲染单位是一个物理像素&#xff0c;所以通常只能以整数像素单位渲染边框。但是&#xff0c;有一些技巧可以模拟出看起来像是 0.5 像素的边框。 这里介绍使用&#xff1a…

Linux内核模块

文章目录 一、内核模块介绍二、模块讲解1、最简模块代码&#xff1a;2、模块三要素3、常用操作命令3.1、 lsmod&#xff1a;显示已加载模块状态3.2、 insmod&#xff1a;载入模块3.3、rmmod&#xff1a;卸载模块3.4、dmesg&#xff1a;显示信息3.5、modinfo&#xff1a;显示ker…

Azure Machine Learning - 提示工程高级技术

本指南将指导你提示设计和提示工程方面的一些高级技术。 关注TechLead&#xff0c;分享AI全维度知识。作者拥有10年互联网服务架构、AI产品研发经验、团队管理经验&#xff0c;同济本复旦硕&#xff0c;复旦机器人智能实验室成员&#xff0c;阿里云认证的资深架构师&#xff0c…

2023/12/20 work

1. 使用select完成TCP客户端程序 2. 使用poll完成TCP并发服务器 3. 思维导图

linux 内核的 lru_list 的结构

在linux的slab分配的入口slab_alloc有一个传入参数lru&#xff0c;它的作用是使每个slab对象在unused&#xff0c;但可能后面继续使用的时候&#xff0c;不需要free&#xff0c;可以先放在lru_list上。lru_list的结构为&#xff1a; struct list_lru {struct list_lru_node *n…

【Axure RP9】中继器应用及相关案例

一 中继器简介 1.1 中继器是什么 中继器&#xff08;Repeater&#xff09;是一种高级的组件&#xff08;Widget&#xff09;&#xff0c;用于显示文本、图像和其他元素的重复集合。它是一个容器&#xff0c;容器中的每一个项目称作“item”&#xff0c;由于“item”中的数据由…

C# Tcplistener,Tcp服务端简易封装

文章目录 前言相关文章前言设计代码简单使用运行结果 前言 我最近有个需求要写Tcp服务端&#xff0c;我发现Tcp服务端的回调函数比较麻烦&#xff0c;简化Tcp的服务&#xff0c;我打算自己封装一个简单的Tcp服务端。 相关文章 C# TCP应用编程三 异步TCP应用编程 C# Tcpclient…

《数据结构、算法与应用C++语言描述》- 最小输者树模板的C++实现

输者树 完整可编译运行代码见&#xff1a;Github::Data-Structures-Algorithms-and-Applications/_31loserTree 输者树&#xff1a;每一个内部节点所记录的都是比赛的输者&#xff0c;晋级的节点记录在边上。本文中&#xff0c;赢者是分数较低的那个&#xff0c;输者是分数高…

线性回归中的似然函数、最大似然估计、最小二乘法怎么来的(让你彻底懂原理)收官之篇

图1 图2 图3 图4 问1&#xff1a;为什么要引入似然函数&#xff1f; 在线性回归中引入似然函数是为了通过概率统计的方法对模型参数进行估计。简单来说&#xff0c;我们希望找到一组参数&#xff0c;使得我们观测到的数据在给定这组参数的情况下最有可能发生。 问:1&#xf…

js之零碎工具(四)

一、数组的去重 简单类型的去重 let arr [1, 2, 2, 3, 4, 4, 5]; let uniqueArr [...new Set(arr)]; console.log(uniqueArr); // 输出&#xff1a;[1, 2, 3, 4, 5]在这个例子中&#xff0c;我们首先创建了一个新的 Set 对象&#xff0c;并将数组 arr 作为参数传递给 Set 的…

深度学习中的张量维度

1 深度学习中的张量 在深度学习框架中&#xff0c;Tensor&#xff08;张量&#xff09;是一种数据结构&#xff0c;用于存储和操作多维数组。张量可以被视为一种扩展的矩阵&#xff0c;它可以具有任意数量的维度。 在深度学习中&#xff0c;张量通常被用来表示神经网络的输入…