一、拷贝控制操作
1、拷贝构造函数:一个构造函数的一个参数是自身类类型的引用,且额外参数都有默认值
class Foo{ public:Foo(const Foo&); //拷贝构造函数;;最好是const类型;不应该是explicit的 }
- 拷贝构造函数通常会被隐式使用,所以不应该是explicit
- 如果我们没有为一个类定义拷贝构造函数,编译器会定义一个合成的拷贝构造函数
- 合成的拷贝构造函数会将其非static成员简单的拷贝到正在创建的对象中
- 拷贝初始化发生的情况
- 用=定义变量时
- 将一个对象作为实参传递给一个非引用类型的参数
- 拷贝构造函数用来初始化非引用类类型参数,所以拷贝构造函数定义必须是引用类型
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
2、拷贝赋值运算符
- 重载运算符本质上是函数,由operator关键字后接表示要定义的运算符的符号组成。
- 运算符函数也要有一个返回类型和一个参数列表
- 赋值运算符通常返回一个指向其左侧运算对象的引用
Foo& operator = (const Foo&);
3、移动构造函数
4、移动赋值运算符
5、析构函数:释放对象使用的资源,并销毁对象的非static数据成员
~Foo();
- 没有返回值,不接受参数
- 按成员初始化顺序的逆序销毁
- 调用析构函数的情况:
- 变量离开作用域时被销毁
- 一个对象被销毁时,其成员被销毁
- 容器被销毁时,其元素被销毁
- 临时变量,当创建它的完整表达式结束时被销毁
- 当指向一个对象的引用或指针离开作用域时,析构函数不会执行
- 析构函数函数体本身不直接销毁成员;在之后的隐含的析构阶段中被销毁
6、三/五法则
- 当我们决定一个类是否需要定义自己版本的拷贝控制成员时,基本原则是首先确定这个类是否需要一个析构函数
- 合成析构函数不会delete一个指针数据成员;这个时候需要自己定义一个析构函数来释放构造函数分配的内存
- 如果一个类需要自己定义的析构函数,则也需要自己定义的拷贝赋值运算符和拷贝构造函数
- 需要拷贝构造函数也需要拷贝赋值运算;反之也是;但是不一定需要自己的析构函数
7、在函数的参数列表后加上=delete指出该函数为删除的
- 析构函数不能是删除的:如果是删除的,则无法销毁此类型的对象了
- 一个类有const成员,则它不能使用合成的拷贝赋值运算符:因为不能被赋值
二、拷贝控制和资源管理
1、通常管理类外资源的类必须定义拷贝控制成员;这种类需要通过析构函数来释放对象所分配的资源
2、当编写一个复制运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后销毁左侧运算对象的现有成员就是安全的:防止自己赋值给自己
HasPtr& HasPtr::operator=(const HasPtr &rhs) {auto newp = new string(*rhs.ps);delete ps;ps = newp;i = rhs.i;return *this; }
三、对象移动
1、标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝
2、右值引用:必须绑定到右值的引用;通过&&来获得右值引用
- 右值引用只能绑定到一个将要被销毁的对象
- 可以绑定到要求转换的表达式、字面常量或是返回右值的表达式,但不能直接绑定到一个左值上
- 返回非引用类型的函数、连同算术、关系、位以及后置递增/减运算符都可以生成右值。可以把一个const的左值引用或一个右值引用绑定到这类表达式上
- 返回左值的引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是左值的表达式,可以把一个左值引用绑定到这类表达式上
- 左值有持久的状态,右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
- 右值所引用的对象将要被销毁
- 该对象没有其他用户
- 变量是左值,因此不能将一个右值引用直接绑定到一个变量上,即使这个变量时右值引用类型也不行
int &&rr3 = std::move(rr1);
- 调用move的标准库函数来获得一个绑定到左值上的右值引用
- 调用move就意味着除了对rr1赋值或销毁它外,将不能再使用它
- 使用move的代码应该使用std::move而不是move,这样可以避免潜在的名字冲突
3、移动构造函数和移动赋值运算符
StrVec::StrVec(StrVec &&s) noexcept //第一个参数为类类型的引用;其他参数都必须有默认参数:elements(s.elements), first_free(s.first_free) //使用noexcept来提示编译器不抛出异常 {s.elements = s.first_free = nullptr; //确保移后源对象主语销毁无害的状态 }
4、右值引用和成员函数
- 区分移动和拷贝的重载函数通常一个版本接收const T&;另一个版本接收&&;其他的const T&&和 T&不需要
void push_back(const &X); void push_back(&&X);
5、在参数列表后面可以放置一个引用限定符
- 对于&限定的函数,只能将它用作左值;对于&&限定的函数,只能用作右值
- 一个函数可以同时使用const和引用限定,但是引用限定符必须跟在const限定符后面
- 编译器会根据对象的左/右值属性来确定使用哪个版本的函数
- 如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符