《c++ primer笔记》第十三章 拷贝控制

这篇具有很好参考价值的文章主要介绍了《c++ primer笔记》第十三章 拷贝控制。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、拷贝、赋值与销毁

1.1拷贝构造函数

​ 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都由默认值,则此构造函数成为拷贝构造函数。拷贝构造函数在某些情况下会被隐式地使用,所以不能定义为expicit

合成拷贝构造函数

合成某某函数一般出现在我们没定义该函数时,比如当没有定义任何构造函数时,编译器会给出一个合成默认构造函数。但是即使定义了其它构造函数,合成拷贝构造函数任然会被编译器定义。

​ 每个成员的类型决定了它如何拷贝;对于类类型,会使用其拷贝构造函数进行拷贝;对于内置类型的成员则直接拷贝。

​ 拷贝初始化发生的场景:

  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员

​ 某些类类型会对它们分配的对象使用拷贝初始化,比如在初始化标准库容器或是调用其insertpush成员时,容器会对其元素进行拷贝初始化;使用emplace则是直接进行初始化。

参数和返回值

​ 在函数调用的过程中,非引用类型的参数要进行拷贝初始化,这也解释了为什么拷贝构造函数的第一个参数必须是引用类型,如果不是,那么为了调用拷贝构造函数,就必须拷贝它的实参,为了拷贝实参,又需要调用拷贝构造函数,造成了无限循环。

拷贝初始化的限制

​ 如果使用的初始化值要求通过一个explicit的构造函数进行类型转换,那么使用拷贝初始化还是直接初始化就不是无关紧要的了。第一行代码采用直接初始化,第二行错误,vector中接收大小参数的构造函数时explicit的,就无法进行隐式拷贝构造。第三行f的参数使用拷贝初始化。第四行错误,道理与第二行相同。最后一行显式的使用一个exlicit构造函数。

1.vector<int> v1(10);
2.vector<int> v2 = 10;
3.void f(vector<int>);
4.f(10);
5.f(vector<int>(10));

编译器可以绕过拷贝构造函数

​ 下面的第二行代码直接忽略了拷贝构造函数,直接创建了对象,但是类里面必须存在拷贝构造函数。(这样做不知道能否提升一点性能?)

string null_book = "999-99-9";
string null_book("999-99-9");

1.2拷贝赋值运算符

​ 与拷贝构造函数一样,如果没有定义编译器会合成一个。

重载赋值运算符

​ 重载运算符本质上是函数。对于一些运算符,包括赋值运算符,必须定义为成员函数,当一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。对于一个二元运算符,右侧运算对象作为显式参数传递。==一般赋值运算符函数接收的同类型参数是以引用的形式,这样是为了与内置类型的赋值操作保持一致。==赋值运算符通常应该返回一个指向其左侧运算对象的引用。

class foo {
	foo &operator=(const foo&);
}

合成拷贝赋值运算符

​ 如果没有其它用途,合成拷贝赋值运算符会将其右侧对象的每个非static成员赋予左侧运算对象的对应成员,返回一个指向其左侧运算对象的引用。

1.3析构函数

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

析构函数完成什么工作

​ 在一个构造函数中,成员的初始化时在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。成员在销毁时依赖其类型,类类型销毁会调用自身的析构函数,由于内置类型没有析构函数,所以什么都不用做。对于合成析构函数,函数体为空,要注意析构函数体自身并不直接销毁成员,成员时在析构函数体之后隐含的析构阶段被销毁的

1.4三/五法则

​ 三指的是控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符和析构函数。新标准增加了两个额外的函数,一个类还可以定义移动构造函数和一个移动赋值运算符。C++并不要求我们必须定义这5个函数。

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

class HasPtr {
public:
	HasPtr(const &s = string());
	~HasPtr() { delete ps; } // 错误,HasPtr需要一个拷贝构造函数和一个拷贝赋值运算符
}

上面代码中自定义了析构函数,但是没有定义拷贝构造函数和一个拷贝赋值运算符,那么在下面情况:

HasPtr f(Hasptr hp) { // 值传递,将被拷贝
	HasPtr ret = hp;
	return ret;
} // ret 和 hp都会被销毁,调用HasPtr的析构函数

当函数体结束后,由于ret和hp都包含一个相同的指针值,将会导致指针被delete两次。

1.5阻止拷贝

​ 在某些情况下,一些类比如iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。想要阻止拷贝,只不定义拷贝构造函数时没有用的,因为编译器会给出合成的函数。新标准允许我们将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。(删除函数:虽然声明了,但是不能以任何方式使用

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

析构函数不能是删除的成员

​ 如果给析构函数加上了delete,那么在任何时候都不能调用该函数,造成了一个对象永远无法被销毁。如果一个类有某个成员删除了析构函数,也不能定义该类的变量或临时对象,但是可以动态的分配该类。

class foo {
	foo() = default;
	~foo() = delete;
}
foo f; //错误,析构函数是删除的,不能定义类的变量或临时对象
foo *f = new foo(); // 正确,但是不能delete p

合成的拷贝控制成员可能是删除的

​ 书上给了一大堆规则,其实看它后面的总结就行:如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

private拷贝控制

​ 新标准之前一般通过把拷贝构造函数或拷贝赋值运算符声明为私有的方式来阻止拷贝,由于析构函数是公有的,所以可以定义类型的对象,但是不能进行拷贝。由于友元和成员函数在这种情况下任然可以拷贝对象,所以可以通过只声明不定义的方式来阻止它们拷贝。(如果要阻止拷贝尽量使用=default的方式

二、拷贝控制和资源管理

2.1行为像值的类

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

class HasPtr {
public:
	HasPtr(const string &s = string()) :
		ps(new string(s), i(0)) { }
	HasPtr(const HasPtr &p) : 
		ps(new string(*p.ps), i(p.i)) { }
	HasPtr& operator=(const HasPtr &);
	~HasPtr() { delete ps;}
private:
	string *ps;
	int i;
}

类值拷贝赋值运算符

​ 赋值操作会销毁左侧运算对象的资源,拷贝构造函数会从右侧运算对象拷贝数据。通过先拷贝右侧运算对象,可以处理自赋值情况,并且保证在异常发生时左侧对象处于一个有意义的状态。

HasPtr &HasPtr::opertator=(const HasPtr &rhs) {
	auto newp = new string(*rhs.ps);
	delete ps; // 释放旧内存
	ps = newp;
	i = rhs.i;
	return *this;
}

2.2行为像指针的类

​ 对于行为类似指针的类,需要定义拷贝构造函数和拷贝赋值运算符,拷贝指针成员本身而不是其指向的对象。shared_ptr的性质能很方便的让一个类展现类似指针的行为,核心就是里面的一个指针计数器,下面我们不使用shared_ptr,通过定义引用计数的方式让类像指针一样。

class HasPtr {
public:
	HasPtr(const string &s = string()) :
		ps(new string(s), i(0), use(new size_t(1))) { } // 注意这里分配use的时候采用动态内存的方式,好处式当拷贝或复制对象时,副本和原对象都会指向相同的计数器。
	HasPtr(const HasPtr &p) : 
		ps(p.ps), i(p.i), use(p.user)) { ++*user }
	HasPtr& operator=(const HasPtr &);
	~HasPtr() { }
private:
	string *ps;
	int i;
	size_t *use; // 计数器,记录当前有多少个对象共享*ps的成员
}

HasPtr::~HasPtr() {
	if(--*use == 0) { // 只有在计数器为0时才释放资源
		delete ps;
		delete use;
	}
}

HasPtr &HasPtr::operator=(const HasPtr &rhs) {
	++*rhs.use;
	if(--*user == 0) {
		delete ps;
		delete use;
	}
	ps = rhs.ps;
	i = rhs.i;
	use = rhs.use;
	return *this;
}

三、对象移动

​ 很多情况下发生的对象拷贝在拷贝结束后就被销毁,使用移动而非拷贝会大幅度提高性能。标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝

3.1右值引用

​ 右值引用就是必须绑定到右值的引用,使用&&右值引用只能绑定到一个将要销毁的对象。对于常规引用(左值),不能绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特征。

int i = 42;
int &r = i;
int &&rr = i; // 错误,i是一个左值,右值引用无法绑定
int &r2 = i * 42; // 错误,i * 42是一个右值
const int &r3 = i * 42; // 可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42;
  • 返回左值:
    1. 赋值
    2. 下标
    3. 解引用
    4. 前置递增/递减运算符
  • 返回右值
    1. 算术
    2. 关系
    3. 后置递增/递减运算符

对于返回右值,我们可以将const的左值引用绑定到对象上。

左值持久,右值短暂

​ 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。因此右值引用表示所引用的对象将要被销毁,该对象没有其他用户。这两个特性意味使用右值引用的代码可以自由地接管所引用的对象的资源。

标准库move函数

​ 虽然不能将右值引用绑定到一个左值上,但是可以显式地将一个左值转换为对应的右值引用类型。可以通过新标准的move函数获得绑定到左值的右值引用。

int r = 2;
int &&r3 = std::move(r);

这样意味着对于源对象r,除了对它重新赋值或者销毁,都不会再使用它。

3.2移动构造函数和移动赋值函数

​ 类似拷贝构造函数,移动构造函数第一个参数还是引用,不过式右值引用,其它的参数必须右默认实参。除了完成资源移动,移动构造函数必须确保移动后源对象处于被销毁后无害状态,一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源的所有权已经归属于新创建的对象。

StrVec::StrVec(StrVec &&s) noexcept 
	:elements(s.elements), first_free(s.first_free), cap(s.cap)
								{ // 移动操作不抛出异常
	s.elements = s.first_free = s.cap = nullptr;
}

移动赋值运算符

​ 移动赋值运算符与移动构造函数一样,不抛出任何异常。

StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
	//检测自赋值
	if(this != &rhs) {
		free();
		elements = rhs.elements;
		first_free = rhs.first_free;
		cap = rhs.cap;
		rhs.elements = rhs.first_free = rhs.cap = nullptr;
	}
    return *this;
}

移后源对象必须可析构

合成的移动操作

​ 与拷贝操作不同,编译器不会为某些类合成移动操作,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。当一个类没有移动操作,函数匹配时就会使用拷贝操作来代替移动操作。

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

struct X {
	int i; // 内置类型可以移动
	std::string s; // string定义了自己的移动操作
};

struct hasX {
	X mem; // X有合成的移动操作
};
// 使用合成的移动构造函数
X x, x2 = std::move(x); 
hasX hx, hx2 = std::move(hx);

移动操作永远不会隐式定义为删除的函数,如果显式的要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。

没有移动构造函数,右值也能被拷贝

calss foo {
public:
	foo() = default;
	foo(const foo&);
};

foo x;
foo y(x); // x是一个左值,调用拷贝构造函数
foo z(std::move(x)); // 因为没有定义移动构造函数,所以只能调用拷贝构造函数

因为可以将foo&&转换成一个const foo&,所以z的初始化才可以未定义移动构造函数的情况下调用拷贝构造函数。一般情况下,拷贝构造函数满足对应的移动构造函数的要求

赋值运算符聚合拷贝和移动

class HasPtr {
public:
	// 添加的移动构造函数
	HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i), {p.ps = 0;}
	// 该赋值运算符既是拷贝赋值运算符也是移动赋值运算符
	HasPtr &operator=(HasPtr rhs) {
		swap(*this, rhs);
		return *this;
	}
}

上面代码的赋值运算符接收一个非引用的参数,意味此参数要进行拷贝初始化,要么使用移动构造函数或者拷贝构造函数–左值被拷贝,右值被移动。

移动迭代器

​ 新标准库定义了一种移动迭代器,通过改变给定迭代器的解引用运算符的行为来适配迭代器。一般一个迭代器的解引用运算符返回的是一个指向元素的左值,但是移动迭代器的解引用符生成是一个右值引用。通过调用标准库make_move_iterator函数将一个普通的迭代器转换未一个移动迭代器。

3.3右值引用和成员函数

右值和左值引用成员函数

​ 为了维持向后兼容性,新标准库类仍然允许向右值赋值,但是我们可以通过操作强制左侧运算对象(this指向的对象)是一个左值。在参数列表后放置一个引用限定符&或者&&分别指出this可以指向一个左值或右值,

class Foo {
public:
	Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值	
};
Foo &Foo::operator=(const Foo &rhs) & {
	return *this;
}

一个函数可以同时用const和引用限定,引用限定符必须跟随在const限定符之后

class Foo {
public:
	Foo someMem() const &;
}

重载和引用函数

​ 综合引用限定符和const可以区分一个成员函数的重载版本。

class Foo {
public:
	Foo fn() &&; // 可用于可改变的右值
	Foo fn() const &; // 可用于任何类型的Foo
}

Foo Foo::fn()  && {
	sort(data.begin(),data.end());
	return *this;
}

Foo Foo::fn() const & {
	// 由于本对象是一个const或者左值,不能按照原址进行排序,所以需要先进行拷贝
	Foo ret(*this);
	sort(ret.data.begin(), ret.data.end());
	return ret;
}

在定义const成员函数时,可以定义两个版本,唯一的差别就是有const限定而另一个没有,引用限定的函数则不一样,如果定义了两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符文章来源地址https://www.toymoban.com/news/detail-424559.html

class Foo {
public:
	Foo fn() &&;
	Foo fn() const; //错误:必须加上引用限定符
	
	using Comp = bool(const int&, const int&);
	Foo fn(Comp*); // 正确
	Foo fn(Comp*) const;
}

到了这里,关于《c++ primer笔记》第十三章 拷贝控制的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《微服务实战》 第十三章 JWT

    【项目实战】Spring boot整合JWT、Vue案例展示用户鉴权 【微服务实战】JWT JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。 基于JSON的开发标准 用户信息加密到token里,服务器不保存任何用户信息 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式

    2024年02月06日
    浏览(77)
  • 第十三章_Redis中的BigKey

    MoreKey案例 大批量往redis里面插入2000W测试数据key  Linux Bash下面执行,插入100W # 生成100W条redis批量设置kv的语句(key=kn,value=vn)写入到/tmp目录下的redisTest.txt文件中 for((i=1;i=100*10000;i++)); do echo \\\"set k$i v$i\\\" /tmp/redisTest.txt ;done; 通过redis提供的管道--pipe命令插入100W大批量数据 结合自己

    2024年02月03日
    浏览(45)
  • 第十三章,枚举与泛型例题

    例题1 结果   例题2 结果   例题3 结果     例题4 结果 例题5  结果 例题6  结果 例题7  结果 例题8  结果

    2024年02月06日
    浏览(48)
  • 第十三章 Unity 移动和旋转(上)

    移动和旋转是游戏对象最频繁地操作。我们上个章节简单介绍了Cube的移动和旋转。移动是修改transform的position属性,旋转是修改transform的eulerAngles(欧拉角)属性,两者属性值均可以使用Vector3向量来实现。需要大家注意的是,transform.forward和Vector3.forward的区别(参考坐标系是

    2024年02月05日
    浏览(54)
  • 精读《图解密码技术》——第十三章 PGP

      PGP是一款由个人编写的密码软件,PGP是为了保护处于极端状况下的人们的隐私而开发的,如果这些人的信息被窃听,那么可能是性命攸关的大事件。   OpenPGP是对密文和数字签名格式进行定义的标准规格。   GNU Privacy Guard ( GnuPG、GPG)是一款基于OpenPGP标准开发的密码学

    2024年02月05日
    浏览(54)
  • Vue3——第十三章(插槽 Slots)

    这里有一个 FancyButton 组件,可以像这样使用: 而 FancyButton 的模板是这样的: slot 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。 最终渲染出的 DOM 是这样: 通过使用插槽, FancyButton 仅负责渲染外层的 button (以及相应的样式),而

    2024年02月07日
    浏览(56)
  • 《TCP IP网络编程》第十三章

    Linux 中的 send recv:          send 函数定义:         recv 函数的定义:         send 和 recv 函数的最后一个参数是收发数据的可选项,该选项可以用位或(bit OR)运算符(| 运算符)同时传递多个信息。send recv 函数的可选项意义: MSG_OOB:发送紧急消息 :     

    2024年02月15日
    浏览(43)
  • 第十三章 opengl之模型(导入3D模型)

    使用Assimp并创建实际的加载和转换代码。Model类结构如下: Model类包含一个Mesh对象的vector,构造器参数需要一个文件路径。 构造器通过loadModel来加载文件。私有函数将会处理Assimp导入过程中的一部分,私有函数还存储了 文件路径的目录,加载纹理时会用到。 Draw函数的作用:

    2024年02月05日
    浏览(48)
  • 第十三章 实现组件库按需引入功能

    组件库会包含几十甚至上百个组件,但是应用的时候往往只使用其中的一部分。这个时候如果全部引入到项目中,就会使输出产物体积变大。按需加载的支持是组件库中必须考虑的问题。 目前组件的按需引入会分成两个方法: 经典方法:组件单独分包 + 按需导入 + babel-plug

    2024年02月11日
    浏览(54)
  • PMP项目管理-[第十三章]相关方管理

    定义:定期识别项目相关方,分析和记录他们的利益、参与度、相互依赖性、影响力和对项目成功的潜在影响的过程 作用:使项目团队能够建立对每个相关方或相关方群体的适当关注 1 权高利低 -- 让他爽    2 权低利高 -- 常告知 3 权高利高 -- 重管理    4 权低利低 -- 仅监

    2024年02月03日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包