rvalue & lvalue
左值引用右值引用
右值引用的好处
左值就是通过变量名指向具体地址的值,如普通变量,指针,和返回值为引用的函数调用;右值就是不指向具体地址的值,如常量,临时变量,计算表达式(的中间结果),返回值不为引用的函数调用。左值在生存期持续存在,而右值要么不存在,要么只是暂时存在。在表达式中,左值可以出现在等号的左右两边,但是右值只能存在于等号的右边
or
C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 右值是指临时的对象,它们只在当前的语句中有效。
对右值的取地址是错误的,因为内存中不存在这样一块确定的区域;同时,取地址得到的也是右值,如下
int var = 10;
int* bad_addr = &(var + 1); // ERROR: lvalue required as unary '&' operand
int* addr = &var; // OK: var is an lvalue
&var = 40; // ERROR: lvalue required as left operand// of assignment
一般而言,对右值的引用是错误的,如
int &a = 5; // ERROR
std::string& sref = std::string(); // ERROR: invalid initialization of// non-const reference of type// 'std::string&' from an rvalue of// type 'std::string'
这些被称为“左值引用”。非常量左值引用不能分配右值,因为这需要无效的右值到左值转换
可以为常量左值引用分配右值。因为它们是常量,所以不能通过引用修改值,因此不存在修改右值的问题。这使得非常常见的 C++ 习惯用法成为可能,即通过对函数的常量引用来接受值,从而避免了不必要的临时对象复制和构造。 如
void foo(const string& str);//可以通过以下方法调用
string mystr("123");
foo(mystr);
foo(string("123"));
第二种调用方法就是相当于常量左值引用分配右值
移动构造函数允许将右值对象拥有的资源移动到左值中,而无需创建其副本。
代码示例
class MyString {
private:char* _data;size_t _len;void _init_data(const char *s) {_data = new char[_len + 1];memcpy(_data, s, _len);_data[_len] = '\0';}
public:MyString() {_data = NULL;_len = 0;}MyString(const char* p) {_len = strlen(p);_init_data(p);}MyString(const MyString& str) {_len = str._len;_init_data(str._data);std::cout << "Copy Constructor is called! source: " << str._data << std::endl;}MyString& operator=(const MyString& str) {if (this != &str) {_len = str._len;_init_data(str._data);}std::cout << "Copy Assignment is called! source: " << str._data << std::endl;return *this;}virtual ~MyString() {if (_data != NULL) {std::cout << "Destructor is called! " << std::endl; free(_data);}}
};int main() { MyString a; a = MyString("Hello"); std::vector vec; vec.push_back(MyString("World"));
}
运行结果
Copy Assignment is called! source: Hello
Destructor is called!
Copy Constructor is called! source: World
Destructor is called!
Destructor is called!
Destructor is called!
这里调用了两次拷贝构造函数,MyString(“Hello”)和MyString(“World”)都是临时对象,临时对象被使用完之后会被立即析构。这里一共发生了几次内存分配,拷贝,释放呢?a创造时没有发生内存分配,首先临时变量MyString(“Hello”)发生一次内存分配,拷贝过程中有一次内存分配加拷贝,用完之后然后析构释放内存。另一个临时变量也一样。
如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。
通过加入定义转移构造函数和转移赋值操作符重载来实现右值引用(即复用临时对象):
MyString(MyString&& str) { std::cout << "Move Constructor is called! source: " << str._data << std::endl; _len = str._len; _data = str._data; str._len = 0; str._data = NULL; // ! 防止在析构函数中将内存释放掉}MyString& operator=(MyString&& str) { std::cout << "Move Assignment is called! source: " << str._data << std::endl; if (this != &str) { _len = str._len; _data = str._data; str._len = 0; str._data = NULL; // ! 防止在析构函数中将内存释放掉} return *this; }
这里引入了右值引用符号&&
,运行结果
Move Assignment is called! source: Hello
Move Constructor is called! source: World
Destructor is called!
Destructor is called!
这里就避免了很多不必要的拷贝和分配操作,但是注意,这里临时对象用完之后依然会调用析构函数,所以需要将临时对象的相关地址内存给置为nullptr
所以move constructor的核心是临时变量(右值)的拷贝和赋值