左值和右值
- 左值(lvalue):表达式结束后依然存在的对象,也叫做变量;
- 右值(rvalue):右值标识是临时性对象的表达式,这类对象没有指定的变量名,都是临时生成的。
引用
一个引用是它所引用对象的同义词,是其另一个变量名。
#include <iostream>
using namespace std;
int main()
{
int i = 10;//i:左值 10:右值
int& j = i;//j: 左值引用
j = 100;
cout << "i=" << i << " j= " << j << endl;
return 0;
}
以上测试输出结果:
左值引用
左值引用的声明是通过在某个类型后放置一个符号&来进行的。前文代码中的int& j = i;
便是一个左值引用。需要注意的是,在定义左值引用时,=右边的要求是一个可修改的左值。
// 在定义左值引用时,=右边的要求是一个可修改的左值。因此下面是错误的:
int& k = 10;//error C2440: “初始化”: 无法从“int”转换为“int &”
std::move
std::move将一个左值变为右值,它可以避免一些不必要的拷贝和临时对象,当赋值操作的右边是一个右值时,左值可以偷取右值里的资源,而不必去执行allocator(分配内存的动作),这就是move语义。
右值引用
在c++11之前我们对一个函数返回值取地址是错误的,但是在新语法中,我们可以使用&&符号表示对右值取引用或者使用move函数将一个左值变为右值,相应的,我们也要为对应的元素对象实现一个move构造函数或者move赋值函数的重载版本(适用容器中操作元素时)。比如在做容器的在c++ 2.0之后,容器的插入动作都提供了一个insert的重载版本,专门适用这种新语法。
//右值引用
int&& m = 5;
#include <iostream>
using namespace std;
void func(int&)
{
cout << "func(int&)" << endl;
}
void func(int&&)
{
cout << "func(int&&)" << endl;
}
int main()
{
//右值引用
int&& m = 5;
int a = 5;
int& b = a;
func(1);
func(a);
func(b);
func(move(a));
return 0;
}
结果输出:
右值引用的使用准则
以下测试结果输出为 0,因为在析构函数中将m_x 置为0了,验证了上述的结论(局部变量已调用析构函数释放对象);
#include <iostream>
using namespace std;
class A
{
public:
A(int x):m_x(x) { cout << "A::A(int)" << endl; }
~A()
{
m_x = 0;//在析构中将其置为0
cout << "A::~A()" << endl;
}
public:
int m_x;
};
A&& func2()
{
A a(5);
return move(a);
}
int main()
{
A Aobj = func2();
cout << "AObj: " << Aobj.m_x << endl;
return 0;
}
move的构造函数和move赋值函数
move构造函数对应的是拷贝构造函数:move做浅拷贝后,要将原来指针赋空(记住一定要将原来的指针赋空打断,因为容器在做插入操作时产生的临时对象生命周期结束后会调用析构函数释放指针内存,如果原来的指针没有赋空析构后容器里面已插入的指针就是悬空指针,引起崩溃,而前面指针赋空断开则不会有影响(对一个空指针delete操作相当于什么都不做)。
下面的myString中实现了move的构造函数和move赋值函数:
#include <iostream>
using namespace std;
class myString
{
public:
//Construction
myString()
:_data(nullptr)
,_len(0){}
myString(const char* p)
:_len(strlen(p))
{
init_data(p);
}
//copy Construction
myString(const myString& rhs)
{
_len = rhs._len;
init_data(rhs._data);
}
//copy assignment
myString& operator=(const myString& rhs)
{
if (this != &rhs)
{
if (_data) delete _data;
_len = rhs._len;
init_data(rhs._data);
}
return *this;
}
//move Construction
myString(myString&& str) noexcept
:_len(str._len)
,_data(str._data)
{
str._len = 0;
str._data = nullptr;
}
//move assignment
myString& operator=(myString&& str)
{
if (this != &str)
{
if (_data) delete _data;
_data = str._data;
_len = str._len;
str._len = 0;
str._data = nullptr;
}
return *this;
}
~myString()
{
if (_data)
{
delete [] _data;
_data = nullptr;
}
}
private:
void init_data(const char* p)
{
//strlen 返回的长度不包含‘\0’
_data = new char[_len + 1];
memcpy(_data,p,_len);
_data[_len] = '\0';
}
private:
char* _data;
size_t _len;
};
move版本对容器的影响
- Vector 插入元素时影响很大,因为Vector容器会进行扩容,扩容的过程中会进行拷贝(有move会节约很多时间)
- 除vector以外的其他容器插入元素时影响都不大
- deque的极端情况,比如每次都在buffer中间插入,会产生很多的拷贝动作,影响也大
以上参考: