拷贝控制
1.五种特殊成员函数
- 拷贝构造函数
- 拷贝赋值函数
- 移动构造函数
- 移动赋值函数
- 析构函数
2.拷贝构造函数
定义:第一个参数是自身类类型的引用,且任何额外参数都有默认值。一般用const,且不用explicit
- 如果没有拷贝构造函数,系统会送一个。就算定义了其他构造函数(合成构造函数)
- 将一个对象作为是实參传递给一个非引用类型的形参**(作为参数)**
- 从一个返回类型为非引用类型的函数返回一个对象**(作为返回值)**
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员 string s{}(花括号括起来)
- 个对象用于给另外一个对象进行初始化(常称为赋值初始化);(赋值)
//直接初始化 - 要求编译器用最普通的函数匹配
string dots(10,'.');
//拷贝初始化 -要求将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
string s2 = dots;
2.注意:
- 拷贝构造函数自己的参数必须是引用类型!!!
- 为了调用拷贝构造函数,我们必须拷贝它的实参。:执行拷贝函数时,如果不是引用类型,就需要使用一个实参的拷贝。
- 但为了拷贝实参,我们又需要调用拷贝构造函数. :但是为了这个实参的拷贝,我们就有需要调用这个实参的拷贝函数.
3.拷贝赋值运算符
- 重载 = 号
- 参数和返回值一般是当前类的引用类型,参数加上const
- 如果自己不写,系统会默认送一个合成拷贝赋值运算符
- 把右侧对象(实参)的非static成员赋值到左侧对象上(当前类)
- 对于数组就是一个个赋值
- 返回一个指向左侧对象的引用类型
- 在使用前需要判断是否为自身,是就不做处理,不是需要释放原来的资源
4.析构函数
如果有指针,那么就需要自己定义.如果没有,那么可以用合成析构函数
5.三/五法则
- 如果自己写了析构函数,那么也需要自己定义拷贝构造函数和重载拷贝赋值运算符
- 如果写了拷贝构造函数,那么需要重载拷贝赋值运算符
6.=default
- 作用: 加在类内声明前,显示指定要系统合成默认的(构造)函数,且为内联的
- 如果不想要指定内联,那么应该在类外定义的时候指定
7.=delete
- 阻止拷贝,如果加在拷贝函数后面,**那么就不可能再调用到他们.**是在告诉编译器,我们不需要这个函数
- 析构函数不能有
=delete**
- 合成拷贝成员可能是删除的,如果类内有数据成员不能默认构造,拷贝,复制或者销毁.那么就会把相应的函数置为=delete
8.行为像值的类/行为像指针的类
- 像值的类: 每个元素都是深拷贝(如string 需要重新new一个)
- 像指针的类:不能说一个资源释放就释放对应的资源,最好是用智能指针来管控
9.对象移动
- 在新对象创建拷贝后,需要销毁旧对象时.用移动的方式会更好(移动后原来的就直接无效)
- 有一些不能被共享的(IO类,unique_ptr类),不能拷贝,但是能移动
- 移动不应该抛出任何异常,所以一般设置为noexcept
- 移动源必须是可析构的,进行移动后移动源保存有效,可析构状态
10.右值引用
- 必须绑定到右值上, 用&&来表示.
- 只能绑定到一个将要销毁的对象,所以可以自由的将一个右值引用的资源"移动"到另外一个对象上
- 左值持久,右值短暂
int &&r1 = 42; //正确
int &&r2 = r1; //错误,r1是一个左值!
- 可以对一个左值对象转换为一个对应类型的右值引用(move).但是就必须保证后序只能对他进行赋值和销毁
int &&r3 = std::move(r1); //可行
11.移动构造函数/ 移动赋值函数
-
编译器可以合成移动构造函数和移动赋值运算符.
只有当一个类没有定义任何自己的拷贝控制成员,且类的每个非static成员都可以移动时才会默认合成
-
但是如果自己定义了拷贝构造,拷贝赋值运算符和析构,那么系统就不会给你生成,移动都会变为拷贝来完成
如果不满足就会被定义为
=default
,这时候调用会报错
xxx::xxx(xxx &&s) noexcept
{
}
//移动赋值函数
xxx &xxx::operator=(xxx &&s) noexcept
{
}
12.移动迭代器
- 移动迭代器的解引用运算符生成的是一个右值引用
- 可以通过
make_move_iterator
来使得一个普通迭代器转换为一个移动迭代器,原迭代器能干的,移动迭代器也行 - 特别的: 移动迭代器可以用 uninitialized_copy(未初始化的拷贝)