c++里左值和右值
从大三看到研二,左值右值居然每看每新,我理解是因为左值和右值本身名字太抽象了,和他们实际意义有区别,导致我每次都记不住。
总的来说,需要明确什么是左值右值,以及为什么要引入右值引用。
左值右值定义
首先,左值 ≠ 等式左边的值,右值 ≠ 等式右边的值,更为贴切的是,左值在内存中有属于自己的地址,可以被修改,而右值并没有。
int a = 5;
//在这里a是左值,5是右值
string s1 = "hello";
string s2 = "world";
string s3 = s1 + s2;
//s1 + s2是右值,s1 s2 s3都是左值
右值引用的例子
在没有右值引用的年代,大家都使用着左值引用
#include <iostream>
#include <vector>
using namespace std;
void print(string& s){cout<<s<<endl;
}
int main() {string s1 = "hello";string s2 = "world";string s3 = s1 + s2;print(s1); //代码不会报错
}
但如果我们使用
print(s1+s2);
就会发现有这样的报错,意思是期待的参数类型为左值,而我们传入了右值。
那怎么能让右值也能被传入呢,我们在原来的print函数里可以改为
void print(string& s){cout<<"lval "<<s<<endl;
}
void print(string&& s){cout<<"rval "<<s<<endl;
}
int main() {string s1 = "hello";string s2 = "world";string s3 = s1 + s2;print(s1);print(s1+s2);
}
就会发现原来print(s1+s2)的地方已经不报错了,输出为
这就是因为实现了函数重载,因为s1是左值,使用的print(string& s)这个函数,而s1+s2是右值,使用的是print(string&& s)这个函数。
右值引用的意义
那么问题来了,我们费尽心思提出左值和右值的区别,不辞辛苦用&和&&来区分,到底是为了什么?
原因很简单,是为了彰显左值右值的不同。如果一个变量是右值,就代表着离死不远,我们可以随意使用它的资源(这为移动语义提供了很大的意义)。但如果一个变量是左值,就代表我们不能轻易修改它,因为可能还有别的函数在使用着。
一个例子
这个例子讲的是重写了一个String类,同时有个entity类,拥有一个String name的成员变量。
step1
#include <iostream>
#include <cstring>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("Copied!\n");m_Size = other.m_Size;m_Data = new char[m_Size];memcpy(m_Data, other.m_Data, m_Size);}~String() {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(int argc, const char* argv[]) {Entity entity(String("Cherno"));entity.PrintName();return 0;
}
输出结果为:
Created!
Copied!
Cherno
这里一共发生了两步:
- 创建临时对象String(“cherno”) 因此调用了String(const char* string),值得注意的是String(“cherno”)是一个右值
- entity参数赋值,将刚创建的对象赋值给String(const String& other)
但显然在这里临时对象是没有必要一直存在的,并且多次copy也会带来性能负影响。因此我们希望,如果能不调用这个拷贝构造函数就好了
step2
让我们略微改变一些,首先是为String类增加了一个参数为string&&的构造函数,并且将Entity的参数改为String &&
String(String&& other) {printf("Moved!\n");m_Size = other.m_Size;m_Data = other.m_Data;other.m_Data = nullptr;other.m_Size = 0;}~String() {printf("Destroyed!\n");delete[] m_Data;}Entity(String&& name): m_Name(name) {}// Entity(const String& name)// : m_Name(name) {}
这样操作后输出为:
Created!
Copied!
Destroyed!
Cherno
Destroyed!
就会发现虽然没有报错,但是也没有调用我们的String(String&& other)
这是因为实际上接受右值的函数在参数传进来后其右值属性就退化了,所以给m_Name的参数仍然是左值,还是会调用复制构造函数。
所以只能:
Entity(String&& name):m_Name((String&&)name) {}
但c++提供了更加优雅的方法:
Entity(String&& name):m_Name(std::move(name)) {}
小总结
因此看起来,移动构造主要是实现了一个浅拷贝,然后将原来的对象给置为nullptr。当然这并不仅仅如此,还能体现所有权。
完美转发
一些题外话
在有一些场景下,我们希望参数不是被copy,而是可以直接传入本体。那么为什么不使用指针呢?
#include <iostream>
using namespace std;class MyClass {
public:MyClass() {cout << "MyClass constructor called" << endl;}~MyClass() {cout << "MyClass destructor called" << endl;}
};void funcA(MyClass* ptr) {cout << "funcA finished" << endl;
}void funcB(MyClass* ptr) {cout << "funcB finished" << endl;
}int main() {MyClass* obj = new MyClass(); // 动态分配内存funcA(obj); funcB(obj); return 0;
}
输出的结果为:
在这个例子里,会发现一直没有调用Myclass的析构函数。这有可能会带来内存泄漏。
- https://www.bilibili.com/video/BV1Aq4y1t73p/?spm_id_from=333.337.search-card.all.click&vd_source=6514110304e8e37750c40fc5b39ac964
- https://www.cnblogs.com/zhangyi1357/p/16018810.html