第13章 拷贝控制
13.1 拷贝、赋值与销毁
13.1.1 拷贝构造函数
- 如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
class Foo{
public:
Foo();//默认构造函数
Foo(const Foo&);//拷贝构造函数
};
2. 拷贝构造函数在几种情况下都会被隐式地使用,拷贝构造函数通常不应该是explicit的。
拷贝初始化
string dots(10,'.');//直接初始化
string s(dots);//直接初始化
string s2 = dots;//拷贝初始化
string null_book = "9-999-99999-9";//拷贝初始化
string nines = string(100,'9');//拷贝初始化
拷贝初始化发生在:
- 用=定义变量时会发生。
- 将一个对象作为实参传递给一个非引用类型的形参
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
- 初始化标准库容器或是调用其insert或push成员时,与之相反,用emplace成员创建的元素进行直接初始化。
重载运算符:本质上是函数,其名字由operator关键字后接表示要定义的运算符的符号组成。
13.1.4 三/五法则
- 需要析构函数的类也需要拷贝和赋值操作
class HasPtr{
public:
HasPtr(const std::string &s=std::string()):
ps(new std::string(s)),i(0){};
~HasPtr(){ delete ps; };
private:
str::string *ps;
int i;
}
HasPtr f(HasPtr hp){
HasPtr ret = hp;
return ret;
}
上面的HasPtr类中定义了一个析构函数,但使用合成拷贝构造函数和合成拷贝赋值函数,当在函数f中,HasPtr对象hp和ret指向相同的内存,在函数返回时,hp和ret都被销魂,但这个两个对象包含相同的指针值ps,此代码会导致此指针被delete两次。
2. 需要拷贝操作的类也需要赋值操作,反之亦然。
13.1.5 使用=deflault
可以通过将拷贝控制成员定义为=deflault来显示地要求编译器生成合成的版本,在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的,如果不希望合成的成员是内联函数,应该只对成员的类外定义使用=default。
13.1.6 阻止拷贝
- 定义删除的函数
1)可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝
2)在函数的参数列表后面加上=delete来指出将它定义为删除的函数,删除函数是指,虽然声明了它们,但不能以任何方式使用它们 - 析构函数不能是删除的成员
13.2.2 定义行为像指针的类
- 引用计数
引用计数的工作方式如下:
1)除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态,当创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1.
2)拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
3)析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
4)拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。
根据上述工作原理可以将计数器保存在动态内存中,当创建一个对象时,分配一个新的计数器,当拷贝或赋值对象时,拷贝指向计数器的指针。使用这种方法,副本和原对象都会指向相同的计数器。
13.6 对象移动
13.6.1 右值引用
- 所谓右值引用就是必须绑定到右值得引用,通过&&获得右值引用。右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。
- 标准库move函数,通过调用move函数可获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int &&rr1 = 42;
int &&rr2 = std::move(rr1);//rr1将会被销毁
13.6.2 移动构造函数和移动赋值运算符
- 类似于拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用,不同于拷贝构造函数的是,该引用参数在移动构造函数中是一个右值引用,与拷贝构造函数一样,任何额外的参数都必须有默认实参。一旦完成资源移动,源对象必须不再指向被移动的资源。
- 不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept,在类头文件的声明和定义中都要指定noexcept。
- 合成的移动操作,只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为其合成移动构造函数或移动赋值运算符。