左值:在内存有确定存储地址、有变量名,表达式结束依然存在的值,简单来说左值就是非临时对象。
右值:就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值,简单来说右值就是临时对象。
int a = 0; // 在这条语句中,a 是左值,0 是临时值,就是右值。
左值可以分为两类:常量左值 和 非常量左值
int a=10; // a 为非常量左值(有确定存储地址,也有变量名)
const int a1=10; //a1 为常量左值(有确定存储地址,也有变量名)
const int a2=20; //a2 为常量左值(有确定存储地址,也有变量名)
同理,右值也可以分为两类:常量右值 和 非常量右值
int a=10; // 10 为非常量右值
const int a1=10;
const int a2=20;
a1+a2 // (a1+a2) 为常量右值
知道了左值与右值了,那啥是左值引用与右值引用呢?
左值引用:其实就是绑定到左值的引用,通过&来获得左值引用。
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=10; //常量左值(有确定存储地址,也有变量名)
const int a2=20; //常量左值(有确定存储地址,也有变量名)
//非常量左值引用
int &b1=a; //正确,a是一个非常量左值,可以被非常量左值引用绑定
int &b2=a1; //错误,a1是一个常量左值,不可以被非常量左值引用绑定
int &b3=10; //错误,10是一个非常量右值,不可以被非常量左值引用绑定
int &b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量左值引用绑定
//常量左值引用
const int &c1=a; //正确,a是一个非常量左值,可以被非常量右值引用绑定
const int &c2=a1; //正确,a1是一个常量左值,可以被非常量右值引用绑定
const int &c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &c4=a1+a2; //正确,(a1+a2)是一个常量右值,可以被非常量右值引用绑定
总结归纳:非常量左值引用只能绑定到非常量左值上;常量左值引用可以绑定到非常量左值、常量左值、非常量右值、常量右值等所有的值类型
右值引用:其实也是绑定到右值的引用,通过&&来获得右值引用。
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
const int a2=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&b1=a; //错误,a是一个非常量左值,不可以被非常量右值引用绑定
int &&b2=a1; //错误,a1是一个常量左值,不可以被非常量右值引用绑定
int &&b3=10; //正确,10是一个非常量右值,可以被非常量右值引用绑定
int &&b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=a; //错误,a是一个非常量左值,不可以被常量右值引用绑定
const int &&c2=a1; //错误,a1是一个常量左值,不可以被常量右值引用绑定
const int &&c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &&c4=a1+a2; //正确,(a1+a2)是一个常量右值,不可以被常量右值引用绑定
总结归纳:非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。
从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&d1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被非常量右值引用绑定
int &&d2=std::move(a1); //错误,将常量左值a1转换为常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被常量右值引用绑定
const int &&c2=std::move(a1); //正确,将常量左值a1转换为常量右值,可以被常量右值引用绑定
最后可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。
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!
总共执行了2次拷贝,MyString(“Hello”)和MyString(“World”)都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。 如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。
通过加入定义转移构造函数和转移赋值操作符重载来实现右值引用(即复用临时对象):
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!
需要注意的是:右值引用并不能阻止编译器在临时对象使用完之后将其释放掉的事实,所以转移构造函数和转移赋值操作符重载函数 中都将_data赋值为了NULL,而且析构函数中保证了_data != NULL才会释放。