参考教程
lvalues and rvalues in C++
Move Semantics in C++
左值、右值
左值:可以获取地址的表达式,因为常常被放在等号左侧来被赋值所以叫左值
右值:通常为临时值,可以是常量值、函数返回值、lambda表达式等。(因为是临时值,没有被分配具体地址,所以有些地方对右值的定义是无法获取地址 )
int GetValue() {
return 10; //返回的是临时的常量,所以属于右值
}
int& GetValue2() {
int a = 10;
return a; //返回的是地址,属于左值
}
int main()
{
//1.
//等号左侧必须是左值,等号右侧可以为左值也可以为右值
int i = 10; //i为左值,10为右值
int j = i; //i为左值,也可以放在等号右
j = GetValue(); //GetValue()返回的是常数,属于右值
/*错误案例
10 = i; //10是右值,不能放在等号左侧,错误
GetValue() = j; //同上
*/
GetValue2() = j; //GetValue2()属于左值,不会报错
return 0;
}
左值引用
传统c++的引用就指左值引用。例如 int &a = i;
左值引用特性: 不能把一个右值赋值给左值引用,(毕竟&是取址作用,不能从一个没有地址的临时变量中取址)但是可以在左值引用前加const 修饰,来在右值中获取: const int &a = 1;(可以理解为因为是const ,所以直接给该临时值分配一个地址,然后赋值给&a)
void SetValue2(int& value){
} //需要一个左值引用参数
void SetValue(const int& value) {
} //加const后就可以用右值传参了
int main(){
int i = 1;
SetValue2(i);
/*错误案例
SetValue2(2); //常量2 属于右值,不能转换为左值引用
*/
SetValue(2);
return 0;
}
右值引用
C++11引入的新概念,用&&表示,例如int &&a = 1;
右值引用特性: 不能把一个左值赋值给右值引用。
void PrintName(std::string&& name) { //需要一个右值引用参数
std::cout << name << std::endl;
}
int main(){
std::string firstName = "CSSSS";
std::string lastName = "DNNNNN"; //这里firstName 和 lastName都是左值
std::string fullName = firstName + lastName; //firstName 和 lastName相加的时候用临时变量存储相加的结果,属于右值
//PrintName(fullName); 错误,不能用左值传给右值引用
PrintName(firstName + lastName);
return 0;
}
明明可以用 const &name 做到两个都兼容,为什么要用 &&name 呢 ?左值引用和右值引用分开有什么用呢,我们回到上面PrintName(std::string&& name)这个案例中,可以这样写
void PrintName(const std::string& name) { //处理左值
std::cout << "[左值] " << name << std::endl;
}
void PrintName(std::string&& name) { //处理右值,即临时值
std::cout << "[右值] " << name << std::endl;
}
int main()
{
std::string firstName = "CSSSS";
std::string lastName = "DNNNNN";
std::string fullName = firstName + lastName; //firstName 和 lastName相加的时候用临时变量存储相加的结果,属于右值
PrintName(fullName); //[左值] CSSSSDNNNNN
PrintName(firstName + lastName); //[右值] CSSSSDNNNNN
return 0;
}
我们可以用重载来分别出来左值引用和右值引用,也就是说判断该参数是不是临时值来进行分别处理。(注意第一个函数我们加了const 关键字,但C++依然可以识别到是右值引用并调用第二个函数)
左值右值最常见的作用,要属移动构造。
move语义
#include<iostream>
class String
{
public:
String() = default;
String(const char* string)
{
printf("Created!\n");
m_Size = strlen(string);
m_Data = new char[m_Size];
memcpy(m_Data, string, m_Size);
}
String(const String& other)
{
printf("Copy!\n");
m_Size = other.m_Size;
m_Data = new char[m_Size];
memcpy(m_Data, other.m_Data, m_Size);
}
~String() {
printf("Destroy!\n");
delete[] m_Data;
}
void Print()
{
for (uint32_t i = 0;i < m_Size;i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name)
{
}
void PrintName()
{
m_Name.Print();
}
private:
String m_Name;
};
int main()
{
Entity entity(String("Cherno"));
entity.PrintName();
system("pause");
return 0;
}
运行这段代码,得到的结果是
这里有一个问题,我们要生成一个entity的实体,但是构造了一次String,又拷贝构造了一次,这里一共分配空间、memcpy()了两次,挺多余的,用 移动构造就可以优化这个问题。
加入这些代码
class String
{
...
String(String&& other) noexcept //移动构造
{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size = 0;
other.m_Data = nullptr; //注意这里要把之前的实体清除,防止析构时影响到新的实体
}
...
};
class Entity
{
...
Entity(String&& name)
:m_Name((String&&)name) //显式转换成右值引用,来调用移动构造
{
}
...
};
调用了一次构造函数,一次移动构造函数,而移动构造函数又是性能消耗很小的浅拷贝,所以说优于之前的那种方案。