| 拷贝构造函数 | 用同类型的另一个对象初始化本对象会做什么 |
| 拷贝赋值函数 | 将一个对象赋予同类型的另一个对象会做什么 |
| 移动构造函数 | 同拷贝构造 |
| 移动赋值函数 | 同拷贝赋值 |
| 析构函数 | 当此类型对象销毁时做什么 |
称这些操作为拷贝控制操作。
1.拷贝构造函数:
Foo(const &Foo); // 拷贝构造函数:第一个参数是自身类型的引用,且任何额外参数都有默认值
可以是非const的,但一般总是const。
必须是引用。在传参(非引用类型)时,如果不是引用,则会无限循环的调用拷贝构造函数。
在几种情况下会被隐式的使用,通常不是explicit的
合成拷贝构造函数:当没有给这个类定义拷贝构造函数时,编译器生成的一个。
当合成拷贝构造函数不是用来阻止拷贝该类型对象(拷贝构造函数为删除 = delete)。它会将参数的成员(非static的)逐个拷贝到正在创建的对象中。每个成员的类型决定它如何拷贝,内置对象直接拷贝,数组会逐元素拷贝,类类型会使用它的拷贝构造函数。
拷贝初始化发生的情况:
- 使用=定义变量,例: string s2 = s1;
- 将一个对象作为实参,传给一个非引用类型的形参
- 一个返回类型为非引用的该类型的对象
- 用花括号列表初始化一个数组
对于初始化容器,使用insert或push会进行拷贝初始化,使用emplace会直接初始化。
在拷贝初始化过程中,编译器可以跳过拷贝/移动构造函数,直接创建对象。
2.拷贝赋值运算符:
重载运算符:参数表述运算符的运算对象。如果运算符是成员函数,左侧运算对象就绑定隐式的this参数。
赋值运算符通常返回一个指向其左侧运算对象的引用。
类没有靠背赋值运算符时,编译器会为其合成拷贝赋值运算符。性质类型合成拷贝构造函数。
3.析构函数:
构造函数初始化类中的非static成员和一些其他操作。析构函数释放对象使用的资源,并销毁对象的非static成员。
构造函数有一个初始化部分和函数体,初始化在函数体之前完成,按照他们在类中出现的顺序进行初始化。
析构函数也有一个函数体和析构部分,首先执行函数体,然后销毁成员,按初始化顺序的逆序销毁。
无论何时一个对象被销毁,都会自动调用其析构函数。
当一个对象的引用或指针离开作用域,析构函数不会被执行。
未定义自己的析构函数,编译器也会生成一个合成析构函数。
4.三/五法则:
三个基本操作就可控制类的拷贝操作:拷贝构造函数,拷贝赋值函数和析构函数。
如果一个类定义了拷贝操作,它就应该定义所有五个操作。
需要析构函数的类也需要拷贝和赋值操作。
需要拷贝操作的类也需要赋值操作,反之亦然。
5.阻止拷贝:
只能对具有合成版本的成员函数使用 = default;
有些类是不允许拷贝的,如流(istream等)。这时需要删除拷贝操作,定义删除的函数 ( = delete;)。
与 = defalut不同, = delete必须出现在第一次声明处。可以对任何函数指定delete。
析构函数是不可以指定删除的。
6.拷贝控制:
管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,首先确定该类的拷贝语义。
分为行为像值的类和行为像指针的类。
- 行为像值的类:意味着类有自己的状态,副本和原对象完全独立,改变副本不会对原对象造成影响。定义拷贝构造函数:实现成员的拷贝,而不是指针拷贝。定义析构函数:释放该成员。定义赋值函数:重新拷贝,并释放该对象的旧成员。大多数赋值运算符组合了析构函数和构造函数的工作。注意要可以实现自拷贝,先把旧成员的值拷贝出来,在销毁旧成员。
- 行为像指针的类:共享状态,副本和原对象使用同一个底层资源,改变副本也会改变原对象。拷贝成员本身,而不是它所指向的对象。类似于shared_ptr需要使用引用计数。引用计数不能单纯的作为该类的成员变量,需要存在动态内存中。拷贝操作时也进行指针一样的拷贝。计数器的变化类似shared_ptr。最后洗后函数判断计数器,为0则释放内存。
7.交换操作:
对于管理资源的类,除了定义拷贝控制成员,还需要定义一个名为swap的交换函数。
交换两个对象时,如果没有自定义的版本,会使用标准库定义的swap。
交换两个对象需要完成一次拷贝(v1拷贝给临时变量)和两次赋值(v2赋值给v1,临时变量赋值给v2)。
如果我们希望交换的元素交换指针即可完成,则使用标准库的会带来不必要的内存分配。
交换操作不会必要的,对于分配了资源的类,这是一种很重要的优化手段。
void swap(ClassA &l, CkassA &r)
{
// 交换l和r中的指针。 在swap函数应该使用swap,而不是std::swap
}
赋值运算符中使用swap :
ClassA& ClassA::operator=(ClassA a) {swap(*this, a); return*this;} // 传入的是对象所以函数结束后,a销毁
8.对象移动:
新标准一个最主要的特征是可以移动而非拷贝对象的能力。
有些类移动会提高性能,而有一些比如流和unique_ptr都包含不能共享的资源,只能移动不能拷贝。
9.右值引用:
必须绑定到右值的引用(使用&&获得右值引用)。可以自由地将一个右值引用的资源“移动”到另一个对象中。
左值和右值可以理解为赋值号的左面和右面。
左值引用不能绑定到要求转换的表达式、字面常量或是返回右值的表达式(可以使用const的左值引用)。
右值引用有着完全相反的特性:可以将右值绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。
左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
右值引用的一个重要特征---只能绑定到一个将要销毁的对象。
由于右值引用只能绑定到临时对象,得知:所有引用对象将要被销毁,该对象没有其他用户。
我们可以从绑定的右值“窃取”状态。
变量是左值,不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
10.标准库move函数:
新标准库的move函数可以 获得绑定到左值上的右值引用。
int &&rr2 = std::move(rr1); // 告诉编译器我们有个左值,但希望像右值一样处理它
调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它,调用move后,不能对移后源对象的值做任何假设。
不提供using声明,直接调用std::move而不是move。这样做可以避免潜在的名字冲突。
11.移动构造函数和移动赋值运算符:
StrVec::StrVec(StrVec &&s) noexcept // 不应抛出任何异常(在声明和定义中都要指定)
:elements(s.elements), first_free(s.first_free), cap(s.cap){
s.elememts = s.first_free = s.cap = nullptr; }
源对象(s)必须不再指向被移动的资源(elements,first_free和cap),这些资源的所有权已经归属新创建的对象(this).
忘记改变s.first_free,则销毁移后原对象会释放刚刚移动好的内存。
如果希望在vector重新分配内存时,使用定义好的的移动构造函数而不是拷贝构造函数,则移动构造函数指定为noexcept。
StrVec &StrVec::operator=(StrVec &&s) noexcept // 不应抛出任何异常
{
if (this != s){
free();
elements = s.elements; first_free = s.first_free; cap = s.cap;
s.elememts = s.first_free = s.cap = nullptr; }
return *this;}
移动源对象必须可析构。编写一个移动构造函数,必须保证移动源对象进入一个可析构的状态,StrVec使用赋值为nullptr的方式。
移后的移动源必须保证是有效的。有效指可以安全地为其赋予新值或安全的使用而不依赖当前值。不应依赖移动源对象中的数据。
编译器不会为某类合成移动操作。(一个类有自定义拷贝操作和析构函数,编译器就不会为它合成移动操作了)
如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝函数来代替移动操作。
只有一个类没有定义拷贝操作和析构函数,且所有数据成员都能移动操作,则编译器会合成移动构造函数或移动赋值运算符。
移动操作定义为删除的情况:
- 显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则会将移动操作定义为删除的函数。
- 有类成员是const或引用,析构函数是删除或不可访问的
如果移动操作可能被定义为删除的函数,编译器就不会合成它。
如果类定义了一个移动构造函数或一个移动运算符,则该类的拷贝操作会被定义为删除的。
右值使用移动,左值使用拷贝。但如果没有移动操作,右值也被拷贝(即使调用move,&&也会被转换成const &型进行拷贝)。
HasPtr& operator=(HasPtr r){ swap(*this, r); return *this;} // 既是移动运算符也是拷贝运算符,因为参数是对象本身。
12.移动迭代器:
移动迭代器适配器:通过改变给定迭代器的解引用运算符的行为来适配此迭代器。
一般来说,一个迭代器的解引用运算符返回一个指向元素的左值,而移动迭代器的解引用运算符生成一个右值。
auto moveiter = make_move_iterator(vec.begin); // 参数为普通迭代器,返回一个移动迭代器。
确定移动操作是安全的,移动源对象之后不会再使用,才能使用std::move。
13.右值引用和成员函数:
旧标准中,左值可修改,右值不可。新标准右值也可修改。希望在类中阻止右值赋值的操作,使用以下方法。
指出this是左值还是右值引用,参数列表后放置一个引用限定符。
&限定的函数只能用于左值,&&只能用于右值。
Foo &Foo::operator=(const Foo &r) & // 只能向左值赋值(左值可修改)&&表示this的右值引用赋值
{ 将r赋值给this; return *this }
const和引用限定符一起修饰一个函数时,引用限定符放在const后面。
引用限定符也区分重载版本,&&和const & 是两个函数。
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。
本文深入探讨了拷贝构造函数、拷贝赋值运算符、移动构造函数及移动赋值运算符的概念与应用。解析了它们在C++中的工作原理,包括如何管理资源、定义拷贝控制成员以及实现移动操作。同时,介绍了右值引用的作用和标准库move函数的使用。
 第13章 拷贝控制&spm=1001.2101.3001.5002&articleId=106545822&d=1&t=3&u=128366b6824041e99a85d9699256753a)
690

被折叠的 条评论
为什么被折叠?



