/ 拷贝赋值运算符
cout << *p1.p << " " << *p2.p << " " << *p3.p << endl; // 输出 8 8 8
*p2.p = 2; // 修改任意一个对象(p1 p2 p3),共享数据成员p
cout << *p1.p << " " << *p2.p << " " << *p3.p << endl; // 输出 2 2 2
}
13.3 交换操作
通常一次交换需要一次拷贝和两次赋值。
struct Value
{
...
//Value& operator=(const Value &);
Value& operator=(Value); // 拷贝并交换,另一种拷贝赋值方法
friend void swap(Value&, Value&); // 交换元素,标记友元以访问私有成员
}
inline void swap(Value &lv, Value &rv)
{
swap(lv.p, rv.p); // 直接交换指针即可
}
// 参数是按值传递,所以会调用拷贝构造函数创建rv
Value& Value::operator=(Value rv)
{
swap(*this, rv); // 将左侧运算对象和rv交换
return *this; // 在函数结束后rv被销毁(即原来的值)。
}
13.5 动态内存管理
在重新分配内存过程中,移动元素而不是拷贝元素
创建一个新的内存 先构造一部分对象,将原来的元素直接移动过来 销毁原来内存的元素,释放空间
13.6 移动对象
标准库容器、string 和 shared_ptr 既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值引用
常规引用称之为左值引用。右值引用和左值引用能绑定的值互补。 只能绑定到一个将要销毁的对象。可以接管所引用的对象的资源,“窃取”对象的状态。
int i = 123;
int &r = i; // 正确
int &&rr1 = 123; // 正确
int &&rr2 = i; // 错误,右值引用不能绑定左值
int &r2 = i * 2; // 错误,左值引用不能绑定右值
const int &r3 = i * 2; // 正确,常引用可以绑定右值
int &&rr3 = i * 2; // 正确,绑定到乘法结果
13.6.2 移动构造函数和移动赋值运算符
第一个参赛是右值引用,额外参数必须有默认实参。 资源完成移动后,源对象必须不再指向被移动的资源(指针设置nullptr),因为这些资源所有权已经给新对象了。 移动通常不分配资源,所以不会抛出异常。标记noexcept。 标准库容器能对异常发生时,其自身的行为提供保证。 标记了noexcepte就会使用移动构造函数,否则会用拷贝构造函数。
在重新分配内存发生异常时:
移动构造函数已经修改了旧容器,而新容器元素也不存在。 拷贝构造函数的旧容器没有改变,新分配的容器可以直接释放。 不同于拷贝操作,编译器不会为某些类合成移动操作。
如果定义了自己的拷贝构造函数、拷贝赋值或析构函数,就不会合成移动构造函数和移动赋值运算符。 只有类没有定义任何版本的拷贝控制成员,且每个非static数据成员都可以移动时,编译器才会合成移动构造函数和移动赋值运算符。 不同于拷贝操作,移动操作永远不会隐式定义为删除。 显式要求生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除。 如果类定义了一个移动构造函数或一个移动赋值运算符,合成拷贝构造函数和拷贝赋值会被定义为删除的。 用拷贝构造函数代替移动构造函数一般是安全的。
struct Value
{
Value(Value &&v) noexcept : p(v.p) { v.p = nullptr; } // 移动构造函数,移动完后,要将源指向置空
Value& operator=(Value);
};
// 既是移动赋值运算符(参数为右值时),也是拷贝赋值运算符(参数为左值时)
Value& Value::operator=(Value rv)
{
swap(*this, rv);
return *this;
}
void main()
{
Value v1(111);
Value v2(std::move(v1)); // 移动构造函数
cout << *v2.p << endl; // 在移动完后,v1的所有权移交给v2,如果调用v1会产生异常
Value v3;
v3 = v2; // v2作为左值,调用拷贝,v2还是存在有效的
cout << *v3.p << endl;
v3 = std::move(v2); // 使用std::move绑定右值,调用移动操作,v2失效
cout << *v3.p << endl;
}
移动迭代器:解引用运算符生成一个右值引用。通过标准库的make_move_iterator函数将普通迭代器转换成移动迭代器。
// 将begin()到end()之间元素复制到:first作为开始的未初始化内存。
uninitialized_copy(begin(),end(),first);
// 使用移动,原来的迭代器不可以再使用了。
uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);
13.6.3 右值引用和成员函数
可以为成员函数提供移动和拷贝版本。
// 标准容器中push_back提供两个版本:
void push_back(const T&); // 拷贝:绑定任意类型的T
void push_back( T&&); // 移动:只绑定可修改的右值
int i = 5;
vector
vi;
vi.push_back(i); // 调用push_back(const int &)
vi.push_back(2); // 调用push_back(int &&)
引用限定符(reference qualifier):
& :说明只可以操作左值。 &&:说明只可以操作右值。
// 旧标准中,如果有右值引用操作,会产生如下问题:
string s1 = "aaa", s2 = "bbb";
s1 + s2 = "as"; // 正确,但没有意义。
// 使用引用限定符可以解决问题:
class A
{
public:
A sorted() &&; // 参数列表之后,添加&&,对象为右值,即可以原址直接修改内容。
A sorted() const &; // 在const之后添加&,对象为con