一、右值引用
我们常规的引用也叫左值引用,我们不能将左值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式。 而所谓右值引用就是必须绑定到右值的引用,他有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定在左值上.
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() :
执行了有参构造
执行了移动构造
执行了有参构造
执行了移动构造
执行了有参构造
执行了移动构造
执行了有参构造
执行了移动构造
执行了移动构造
执行了移动构造
执行了移动构造
*/
相比于例三,执行了移动构造而不是拷贝构造,提升了一定效率