一、右值引用
我们常规的引用也叫左值引用,我们不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。 而所谓右值引用就是必须绑定到右值的引用,他有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定在左值上.
int i = 42; int& r = i;//正确,常规左值引用
int&& rr = i; //错误,不能将一个右值引用绑定在左值上
int& r2 = i * 42;//错误,不能将左值引用绑定到一个右值上
const int& r3 = i * 42;//正确,可以将一个指向常量的引用绑定在一个右值上
int&& rr2 = i * 42;//正确,将一个右值引用绑定在右值上
右值引用一般只能绑定在临时对象,有如下特点:
1、所引用的对象将要被销毁
2、该对象没有其它用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用地对象的资源
NOTE: 变量表达式都是左值,因此我们不能将一个右值引用绑定到一个右值引用类型的变量上!!
int &&r1 = 40;//r1是一个右值引用类型的变量,它绑定了一个字面值
int &&r2 = r1;//非法!,右值引用不能绑定变量
二、标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值的右值引用,此函数定义在头文件utility中。
int &&rr3 = std::move(rr1)//rr1可以是任何值(右值、左值、引用),返回一个右值。
调用了move就意味着承诺:除了对rr1赋值或者销毁它外,我们将不再使用它。在调用move之后,我们不能对移后的源对象的值做任何假设。
Note:使用move的代码应该使用std::move,而不是用因为有了using namespace std 就不写,这可以避免潜在的名字冲突。
三、移动构造函数
StrVec::StrVec(StrVec &&s) noexcept:elements(s.elements),first_free(s.first_free),cap(s.cap) { s.elements = s.first_free = s.cap =nullptr; }
noexcept的作用就是告诉标准库我们的构造函数不抛出任何异常。
这个移动构造函数会先接管给定StrVec中的内存,在接管内存后,将给定对象中的指针都置为nullptr,避免移动后,旧对象被销毁析构、指针被delete。
NOTE: 不会抛出异常移动构造函数和移动赋值运算符必须标记为noexcept !!
因为标准库会因为是否有noexcept而使用或避免使用它。假设我们有一个装类对象的vector,当我们push_back的时候引起了重新分配内存,这时候,标准库有两个选择,拷贝构造函数和移动构造函数。使用拷贝构造函数相对来说更安全,因为如果在将旧空间上的对象一一拷贝到新空间的过程中,出现了异常,标准库可停止使用新分配的内存空间,这样即使出现异常,vector的内容还是没有改变,顶多就是最新的push_back没成功。而使用移动构造函数时,因为移动的过程会把旧空间的对象析构,一旦在这个过程中出现问题,旧空间的内容被破坏而新空间的内容也异常。所以如果不声明移动构造函数为noexcept,标准库会优先使用拷贝构造。
四、移动赋值运算符
StrVec &StrVec::operator= (StrVec &&rhs) noexcept { if (this!=&rhs)//排除自赋值 { free();//释放已有元素 elements = rhs.elements; first_free = rhs.first_free; cap = rhs.cap; rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; }
先检查this指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们可直接返回。如果不同,我们释放左侧运算对象所使用的内存,并接管给定对象的内存,最后将给定对象的指针置为nullptr。
五、移动构造和拷贝构造
一个类如果定义了拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。
同理,如果类定义了一个移动构造函数、移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
当一个类既有移动构造函数,也有拷贝构造函数,编译器会通过普通函数匹配规则来确定使用哪个
StrVec v1,v2;
v1 = v2;//v2是左值,使用拷贝构造函数
v2 = std::move(v1);//std::move返回的是右值,虽然实参转换为const后能匹配上拷贝构造函数,但移动构造形参是右值,能直接匹配上,为最佳匹配
如果StrVec
六、移动构造实例
class Test_contruct { public: int data_int; int* p; Test_contruct() { data_int = 0; p = new int(0); cout << "执行了默认构造" << endl; } //无参构造 Test_contruct(int var) { data_int = var; p = new int(0); cout << "执行了有参构造" << endl; } //有参构造 Test_contruct(const Test_contruct& point) { data_int = point.data_int; p = new int(*(point.p)); cout << "执行了拷贝构造" << endl; }//拷贝构造 Test_contruct(Test_contruct&& point) noexcept:data_int(point.data_int) , p(point.p) { cout << "执行了移动构造" << endl; point.p = nullptr; }//移动构造 Test_contruct operator=(const Test_contruct& point) //=运算符重载 { Test_contruct temp; temp.data_int = point.data_int; temp.p = new int(*(point.p)); cout << "执行了赋值拷贝运算符" << endl; return temp; } ~Test_contruct() { delete p; } };
例一:
Test_contruct Test01() { auto b = Test_contruct(2); return b; } int main() {cout << "Test01() :" << endl;
auto t01 = Test01();
} /*输出为: Test01() : 执行了有参构造 执行了移动构造 */
就是先在函数Test01内执行了有参构造函数,然后在函数返回的时候执行移动构造函数,构造t01。
例二:
Test_contruct Test02() { return Test_contruct(2); } int main() { cout << "Test02() :" << endl; auto t02 = Test02(); } /*输出结果 Test02() : 执行了有参构造 */
如果在return的时候才构造的话,就相当于直接在main函数里执行有参构造,无移动构造
例三:
void Test03() { vector arr; arr.reserve(3); for (int i = 0; i < 4; ++i) { auto temp = Test_contruct(i); arr.push_back(temp); } } int main() {cout << "Test03() :" << endl;
Test03();
} /*输出结果 Test03() : 执行了有参构造 执行了拷贝构造 执行了有参构造 执行了拷贝构造 执行了有参构造 执行了拷贝构造 执行了有参构造 执行了拷贝构造 执行了移动构造 执行了移动构造 执行了移动构造 */
我们先创建了一个vertor并预留3个位置的空间,然后再循环里,先给temp执行一次有参构造函数,然后用push_back的方式加到vector中,此时会执行拷贝构造函数。当循环到了第四次后,此时push_back时,必须先将原vector的3个元素移动到一个新的空间(因为原空间只能容纳三个元素),故有了三次移动构造。
例四:
void Test04() { vector arr; arr.reserve(3); for (int i = 0; i < 4; ++i) { arr.push_back(Test_contruct(i)); /* 相当于: auto temp = Test_contruct(i); arr.push_back(std::move(temp)); */ } } int main() {cout << "Test04() :" << endl;
Test04();
} /*输出结果 Test04() : 执行了有参构造 执行了移动构造 执行了有参构造 执行了移动构造 执行了有参构造 执行了移动构造 执行了有参构造 执行了移动构造 执行了移动构造 执行了移动构造 执行了移动构造 */
相比于例三,执行了移动构造而不是拷贝构造,提升了一定效率