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<MyString> 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的核心是临时变量(右值)的拷贝和赋值
上述代码存在内存泄漏:!!!!!!!