第十三章 拷贝控制
转载自https://github.com/applenob/Cpp_Primer_Practice,看C++primer的时用的笔记。自己做了一些补充,感谢前人的总结!
这章比较难,初学C++的可以结合《Essential C++》的第五章学习。回过头来再看这一章会轻松一点。
拷贝控制操作(copy control):
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值函数(move-assignement operator)
- 析构函数(destructor)
拷贝、赋值和销毁
拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo{ public: Foo(const Foo&); }
- 合成的拷贝构造函数(synthesized copy constructor):会将参数的成员逐个拷贝到正在创建的对象中。
- 拷贝初始化:
- 将右侧运算对象拷贝到正在创建的对象中,如果需要,还需进行类型转换。
- 通常使用拷贝构造函数完成。
string book = "9-99";
- 出现场景:
- 用
=
定义变量时。 - 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员。
- 用
拷贝赋值运算符
- 重载赋值运算符:
- 重写一个名为
operator=
的函数. - 通常返回一个指向其左侧运算对象的引用。
Foo& operator=(const Foo&);
- 重写一个名为
- 合成拷贝赋值运算符:
- 将右侧运算对象的每个非
static
成员赋予左侧运算对象的对应成员。
- 将右侧运算对象的每个非
析构函数
- 释放对象所使用的资源,并销毁对象的非
static
数据成员。 - 名字由波浪号接类名构成。没有返回值,也不接受参数。
~Foo();
- 调用时机:
- 变量在离开其作用域时。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时,其元素被销毁。
- 动态分配的对象,当对指向它的指针应用
delete
运算符时。 - 对于临时对象,当创建它的完整表达式结束时。
- 合成析构函数:
- 空函数体执行完后,成员会被自动销毁。
- 注意:析构函数体本身并不直接销毁成员。
三/五法则
- 需要析构函数的类也需要拷贝和赋值操作。
- 需要拷贝操作的类也需要赋值操作,反之亦然。
使用=default
- 可以通过将拷贝控制成员定义为
=default
来显式地要求编译器生成合成的版本。 - 合成的函数将隐式地声明为内联的。
阻止拷贝
- 大多数类应该定义默认构造函数、拷贝构造函数和拷贝赋值运算符,无论是隐式地还是显式地。
- 定义删除的函数:
=delete
。 - 虽然声明了它们,但是不能以任何方式使用它们。
- 析构函数不能是删除的成员。
- 如果一个类有数据成员不能默认构造、拷贝、复制或者销毁,则对应的成员函数将被定义为删除的。
- 老版本使用
private
声明来阻止拷贝。
拷贝控制和资源管理
-
类的行为可以像一个值,也可以像一个指针。
-
行为像值:对象有自己的状态,副本和原对象是完全独立的。
-
行为像指针:共享状态,拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。
赋值运算符
//赋值运算符组合了析构函数和拷贝构造函数的工作,先在底层生成对象空间,删除类内原有的内存空间,然后指向新分配的指针。 HasPtr& operator=(const HasPtr& hp){ auto new_p=new std::string (*hp.ps);//拷贝底层string delete ps;//释放旧内存 ps=new_p;//从右侧运算对象拷贝数据到本对象 i=hp.i; return *this;//返回本对象 }
-
交换操作
- 管理资源的类通常还定义一个名为
swap
的函数。 - 经常用于重排元素顺序的算法。
- 用
swap
而不是std::swap
。
对象移动
- 很多拷贝操作后,原对象会被销毁,因此引入移动操作可以大幅度提升性能。
- 在新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可。
- 标准库容器、
string
和shared_ptr
类既可以支持移动也支持拷贝。IO
类和unique_ptr
类可以移动但不能拷贝。
右值引用
- 新标准引入右值引用以支持移动操作。
- 通过
&&
获得右值引用。 - 只能绑定到一个将要销毁的对象。
- 常规引用可以称之为左值引用。
- 左值持久,右值短暂。
move函数:
int &&rr2 = std::move(rr1);
move
告诉编译器,我们有一个左值,但我希望像右值一样处理它。- 调用
move
意味着:除了对rr1
赋值或者销毁它外,我们将不再使用它。
移动构造函数和移动赋值运算符
- 移动构造函数:
- 第一个参数是该类类型的一个引用,关键是,这个引用参数是一个右值引用。
StrVec::StrVec(StrVec &&s) noexcept{}
- 不分配任何新内存,只是接管给定的内存。
- 移动赋值运算符:
StrVec& StrVec::operator=(StrVec && rhs) noexcept{}
合成的移动操作:
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。如string
中有移动操作,就可以生成合成的移动操作。
如果显示的要求编译器生成=default的移动操作,且编译器不能移动所有成员时,编译器会将移动操作定义为删除的函数。
一个类是否定义了自己的移动操作对拷贝操作如何合成有影响。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
- 移动右值,拷贝左值。
- 如果没有移动构造函数,右值也被拷贝。
- 更新三/五法则:如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。
- 移动迭代器:通过改变给定迭代器的解引用运算符的行为来适配此迭代器
make_move_iterator
函数讲一个普通迭代器转换为一个移动迭代器。
- 建议:小心地使用移动操作,以获得性能提升。
- 在编写移动操作时,必须确保以后原对象进入一个可析构的状态
右值引用和成员函数
- 区分移动和拷贝的重载函数通常有一个版本接受一个
const T&
,而另一个版本接受一个T&&
。 - 引用限定符:
- 在参数列表后面防止一个
&
,限定只能向可修改的左值赋值而不能向右值赋值。
- 在参数列表后面防止一个
Foo& operator=(const Foo&) &;//防止对右值赋值
引用限定符可以是&和&&,对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值:
Foo &retFoo();//返回一个引用;retFoo调用是一个左值
Foo retVal();//返回一个值,retval调用是一个右值
Foo j;
retFoo()=j; //正确,retfoo返回一个左值
retVal()=j; //错误,retval()返回一个右值
j=retval(); //正确,我们可以将一个右值作为赋值操作的右侧运算对象
一个函数可以同时用const和引用限定,引用限定符&必须跟随在const限定符后。
当定义const成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定而另一个没有。引用限定的函数不同。如果定义两个或两个以上相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者都不加。