C++拷贝控制
1.拷贝控制核心
对类所管理的资源进行拷贝的过程时需要使用到拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符以及析构函数,其中关系如下:
左值 | 右值 | 操作 | 调用 |
---|---|---|---|
当前类新对象 | 已初始化的对象 | 构造 | 拷贝构造或移动构造函数 |
已初始化对象 | 已初始化的对象 | 赋值 | 拷贝赋值或移动赋值运算符 |
其中拷贝构造和移动构造函数复制产生了新对象,赋值和移动赋值复制了右值对象并销毁了左值原对象。
可以把赋值的过程看成是拷贝和销毁的组合。如果有必要自己实现上述五种函数的任何一个,应该将其他4种也重写。
2.拷贝控制成员
2.1 拷贝构造函数
参数1必须为当前类的引用,因为如果是传值,此时又会发生拷贝,会造成死循环。
1 |
|
2.2 拷贝赋值运算符
1 |
|
2.2.1 自赋值安全
示例代码:
1 |
|
此时如果发生自赋值,m_data指向的内存先被释放,再去调用memcpy拷贝会访问非法内存区域。
2.2.2 异常安全
示例代码:
1 |
|
new运算可能会引用堆空间不足而抛出异常,会暂停当前函数执行,而this->m_data指向的内存区域被释放了,可能造成运行时错误。
2.2.3 安全写法
示例代码:
1 |
|
2.3 移动构造函数
1 |
|
将右值对象的内容窃取后,需要将右值对象置于可以被正确析构的状态。
2.4 移动赋值运算符
1 |
|
移动操作尽量保证不抛出异常,因为一旦出现了异常,很难恢复原来的状态。
2.5 copy/move and swap
2.5.1 原理
拷贝/移动赋值运算符 == 拷贝/移动构造函数 + 析构函数
拷贝/移动都需要将右值资源拷贝一份或直接占为己有,再将原左值资源销毁。
2.5.2 优点
- 更多的去复用已存在的代码,增加可维护性
- 天生自赋值、异常安全
- 一份赋值运算符重载函数代码拷贝和移动均适用
2.5.3 例子
1 |
|
3.移动语义
3.1 存在的意义
1 |
|
严格按照C++语法来分析,上述代码发生了两次拷贝构造,简化含义就是将局部对象tmp的资源交给对象s。两次拷贝实际上是多余的,严重影响效率。移动语义就是通过移动资源所有权,避免发生拷贝的方式来提升效率。
3.2 右值引用
为了使用移动语义的思路来提高效率,最简单的方式就是提供具有移动功能的拷贝控制成员函数,当待拷贝的对象为右值的时候,就调用带有移动语义的拷贝构造函数;当待赋值的对象为右值的时候,就调用带有移动语义的赋值运算符。而为了与原拷贝控制成员构成重载关系,需要在参数列表处作出区分,参数从左值引用类型变为右值引用类型。
1 |
|
右值引用延长了右值的生命期,也就是说右值引用类型实际上是左值,如下面代码:
1 |
|
其中std::move函数将左值强制转换成右值,内部使用static_cast<&&>实现转换。C++11规定,返回值为右值引用的函数调用为右值。std::mov_if_noexcept()如果类移动操作不抛出异常则返回右值,否则返回左值。
3.3 新标准下的return
C++11新标准下,函数中返回参数对象或者是局部对象时,会发生两次重载决议,第一次将要返回的对象当作右值进行匹配,如果匹配失败,再按照左值去返回。
C++17新标准下,返回局部对象的过程直接被省略,如下代码:
1 |
|
上述代码等价于:
1 |
|
3.补充关键字
3.1 delete关键字
有些对象不应该被拷贝,如IO对象,因为流是唯一的,无法进行同步。所以可以将不想被拷贝的类的拷贝控制成员函数指定为delete的。
1 |
|
3.2 default关键字
让编译器合成相关的函数,只适合一些不进行资源管理的类。
3.3 explicit关键字
1 |
|
在C++中,如果的构造函数只有一个参数时,那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象。而explicit关键字就是禁止这一过程的发生。