13拷贝控制
1.拷贝构造函数 2.拷贝赋值运算符 3.移动构造函数 4.移动赋值运算符 5.析构函数
13.1 拷贝赋值与销毁
13.1.1 拷贝构造函数
一个构造函数第一个参数是自身类类型的引用,且任何额外参数都有默认值,则称其为拷贝构造函数。
1
2
3
4
5
class Foo{
public:
Foo(): //默认构造函数
Foo(const Foo& ); //拷贝构造函数
}
合成拷贝构造函数
如果我们没有为一个类定义拷贝构造函数,编译器会为我们定义一个,即合成拷贝构造函数。 会将其参数的成员逐个拷贝到正在创建的对象中。
拷贝初始化
拷贝初始化不仅在我们用=定义变量,在下列情况也会发生:
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个 返回类型为非引用类型的函数返回对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员(P266)
13.1.2 拷贝赋值运算符
重载赋值运算符
重载运算符本质是函数,名字由operator关键字后接 运算符号组成 如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。
拷贝赋值运算符接受一个与其所在类相同类型的参数:其通常返回一个指向其左侧运算对象的引用。
1
2
3
4
class Foo{
public:
Foo& operator=(const Foo&);
}
13.1.3 析构函数
析构函数执行与构造函数相反的操作,构造函数初始化对象的非static数据成员,析构函数释放对象使用的资源,并销毁非static数据成员。
1
2
3
4
class Foo{
public:
~Foo();//析构函数
}
调用析构函数:
- 变量离开作用域被销毁
- 当一个对象被销毁,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- 动态分配的对象,对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束而被销毁。 当指向一个对象的引用或指针离开作用域时,析构函数不会执行
合成析构函数
析构函数体自身不直接销毁成员
13.1.4 三/五法则
需要析构函数的类也需要拷贝和赋值操作 如果只定义了析构函数,未定义拷贝构造函数和拷贝赋值运算符,会引发错误
13.1.5 使用=default
我们可以通过将拷贝控制成员定义为=default来显示地要求编译器生成合成的版本
1
2
3
class Sales_data{
Sales_data() = default;
}
13.1.6 阻止拷贝
可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
1
2
3
4
struct NoCopy{
NoCopy() = default;
NoCopy(const NoCopy& ) = delete;//阻止拷贝
}
析构函数不能是删除的成员
合成的拷贝控制成员可能是删除的
private 拷贝控制
1
2
3
4
5
6
7
class PrivateCopy{
PrivateCopy(const PrivateCopy & );
PrivateCopy & operator=(const PrivateCopy&);
public:
PrivateCopy() = default;
~PrivateCopy();//用户可以定义此类型的对象,但是无法拷贝它们;
}
拷贝构造函数和拷贝赋值运算符是private的,用户代码不能拷贝对象,但是友元和成员函数依旧可以拷贝对象;
最好还是用=delete来祖师拷贝
13.2拷贝控制和资源管理
确定此类型对象的拷贝语义,有两种:
- 类的行为看起来像一个值
它有自己的状态,当我们拷贝一个像值的对象时,副本和原对象完全独立,改变副本不会改变原对象;
- 类的行为看起来像一个指针
即共享状态,副本和原对象使用相同的底层数据,改变一个会改变另一个;
13.2.1 行为像值的类
对于类管理的资源,每个对象都该拥有一份自己的拷贝
赋值运算符
- 如果将一个对象赋予其自身,赋值运算符必须能正确工作
- 大多数赋值运算符组合了析构函数和拷贝构造函数的工作
- 好的模式是 将右侧对象拷贝到一个局部临时对象
13.2.2 行为像指针的类
需要为其定义拷贝构造函数和拷贝赋值运算符,以及析构函数 最好方法是 使用shared_ptr 来管理类中的资源,拷贝或者赋值一个shared_ptr成员会拷贝或赋值 其所指向的指针
有时候希望直接管理资源,使用引用计数,不使用shared_ptr
引用计数工作方式如下:
- 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
- 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
- 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
- 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
可以将计数器保存再动态内存中,当创建对象时,分配一个新的计数器,拷贝或赋值对象时,拷贝指向计数器的指针。
13.3交换操作
swap 与内置swap
13.4拷贝控制示例
Messages 和Folder.cpp
13.5动态管理内存类
在重新分配内存的过程中移动而不是拷贝元素 第一个机制:移动构造函数 第二个机制:move 的标准库函数,调用move来使用string 的移动构造函数
13.6对象移动
新标准一个主要特性就是可以移动而非拷贝对象的能力 某些情况下对象拷贝后就立即销毁了,在这些情况下,移动而非拷贝对象会大幅度提升性能 故新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可
标准库容器、string、和shared_ptr既支持移动也支持拷贝,IO类和unique_ptr可以移动但是不能拷贝
13.6.1右值引用
右值引用即必须绑定到右值的引用,通过&&而不是&来获得右值引用 其有一个重要性质,只能绑定到一个将要销毁的对象 一般而言 一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值 常规引用可以称之为 左值引用
1
2
3
4
5
6
int i = 42;
int &r = i; //正确,r引用i
int &&r = i;// 错误,不能将一个右值引用绑定到左值 i上
int &r2 = i*42; //错误,i*42是一个右值
const int &r3 = i*42; //正确,可以将一个const 引用绑定到一个右值上
int &&rr2 = i*42;// 正确
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久,右值短暂
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创造的临时对象
右值引用指向将要被销毁的对象,因此我们可以从绑定到右值引用的对象窃取状态
变量是左值
变量表达式都是左值 故我们不能将一个右值引用绑定到右值引用类型的变量上
1
2
int &&rr1 = 42//正确
int &&rr2 = rr1;//错误 表达式rr1是左值
标准库move函数
可以显式地将一个左值转换为对应的右值引用类型,还可以通过调用一个move函数来获得绑定到左值的右值引用,其定义在utility中
1
int &&rr3 = move(rr1);//ok
13.6.2移动构造函数和移动赋值运算符
为了使我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值函数 从给定对象”窃取”资源而不是拷贝资源
类似拷贝构造函数,移动构造函数第一个参数是该类类型的一个引用 不同于拷贝构造函数,这个引用参数在移动构造函数中是一个右值引用
1
2
3
4
5
6
7
8
//例子:
StrVec::StrVec(StrVec &&s) noexcept//移动操作不应该抛出任何异常
//成员初始化器接管s中的资源
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//令s这个对象中的指针都置为nullptr,这样对其运行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
移动操作、标准库容器和异常
由于移动操作窃取资源,它通常不分配任何资源,因此移动操作不会抛出任何异常 一种方法是在构造函数中指明noexcept,承诺一个函数不会抛出异常 noexcept出现在参数列表和初始化列表开始的冒号之间
移动赋值运算符
类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值
移后源对象必须可以析构
P475
合成的移动操作
移动右值,拷贝左值……
如果没有移动构造函数,右值也被拷贝
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来”移动”的
移动迭代器
调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器,接受一个迭代器参数,返回一个移动迭代器
13.6.2习题未看
13.6.3右值引用和成员函数
右值和左值引用成员函数
1
2
3
4
5
6
7
8
//有时候对右值进行赋值
s1+s2 = "Wow";
//为了阻止此类用法,强制左侧运算对象(即this指向的对象)是一个左值
//在参数列表后放一个引用限定符
class Foo{
public:
Foo &poerator= (const Foo&) &;//只能向 可修改的左值赋值
}
引用限定符可以是& 也可以是&& 分别指出this指向一个左值或右值
1
2
3
4
5
6
7
Foo &retFoo();//返回一个引用;retFoo调用 是一个左值
Foo retVal();//返回一个值,retVal调用是一个右值
Foo i,j;//i 和j都是左值
i = j;//正确,i是左值
retFoo() = j;//正确 ,retFoo返回左值
retVal() = j;//错误,
i = retVal();//正确,可以将一个右值作为赋值操作的右侧运算对象
引用限定符必须跟在const限定符之后
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符