左值与右值深度解析
碎碎念
作为我的个人博客,当然会记录我的学习!其实是作为一个复习的手段,在我学完之后整理成博客然后上传,给更多的人看到。其实一直有记录的想法,但是总是因为各种事情而耽搁其实就是我懒既然花了时间和功夫来搭建这个个人博客,当然要利用起来。在帮助我对所学知识进行复习的同时,也可以帮助别人maybe我希望可以>ᴗ<。有需要挂友链的可以留言!
今天的主题如标题所示 左值与右值深度解析
什么是左值? 什么是右值? 什么是左值引用? 什么是右值引用?
左值:指的是可以出现在赋值运算符左边的值,也就是说左值是可以被赋值的。左值是一个表达式,它可以出现在赋值运算符的左边,也可以出现在赋值运算符的右边。左值是一个具体的内存位置,可以取地址。左值是一个持久的值,可以在程序的任何地方使用。
左值引用:左值引用就是给左值的引用,给左值取别名。
右值:指的是不能出现在赋值运算符左边的值,也就是说右值是不能被赋值的。右值是一个表达式,它只能出现在赋值运算符的右边。右值是一个临时的值,不能取地址。右值是一个短暂的值,只能在表达式中使用。
右值引用:右值引用就是给右值的引用,给右值取别名。
左值引用和右值引用的本质
左值引用和右值引用的本质都是减少拷贝,但是减少拷贝的方式不同。
左值引用 解决的是传参过程中和返回值过程中的拷贝
- 做参数
void f1(T x) -> void f1(const T& x) // 传参减少拷贝 - 做返回值
ps:但是要注意这里有限制 如果返回对象出了作用域不在了就不能传引用T f2() -> T& f2() // 解决的是返回值的拷贝 但是无法解决接收时的拷贝
右值引用 解决的是传参后函数内部的存储拷贝,传参后移动空间然后置空
- 做参数
void push(T&& x) // 解决push内部不再使用拷贝构造x到容器空间上,而是移动构造过去 - 做返回值
T f2() // 解决的是外面调用接收f2()返回对象的拷贝 T ret = f2(),接收到的是个右值,使用移动构造,减少了拷贝
通过右值引用,C++ 实现了移动语义,即当临时对象不再使用时,其资源可以直接从一个对象转移到另一个对象,而不需要进行昂贵的拷贝操作。右值引用通常与移动构造函数和移动赋值运算符一起使用,以允许对象的资源在不再需要时被“移动”而非复制。
emplace与push_back和insert的效率比较
大家经常上网,会看到一些关于右值引用的文章,比如emplace比push_back和insert高效,这句话是不准确的,没有深入的去分析。我觉得武断的说emplace比push_back和insert高效是非常不负责任的行为。其实emplace并没有比push_back和insert高效到哪里去。以vector为例:
// 这是c++11之后 stl容器的push_back
void push_back (const value_type& val);
void push_back (value_type&& val);
push_back有两个版本,一个是左值引用,一个是右值引用,emplace是一个模板函数,可以接受任意参数,然后在内部构造对象。如代码所示,如果是右值引用,那么就会调用右值引用的版本,如果是左值引用,就会调用左值引用的版本。那就这个来说 emplace和push_back和insert的区别在哪里呢?所以所谓的emplace比push_back和insert高效是不准确的。
emplace
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);
那么emplace真正厉害的地方在哪呢?
vector<pair<string, string>> vp;
vp.push_back(make_pair("右值", "右值"));
pair<string, string> kv("左值", "左值");
vp.push_back(kv);
vp.emplace_back(make_pair("右值", "右值")); // 修正: changed make to make_pair
vp.emplace_back(kv);
vp.emplace_back("右值", "右值"); // 体现emplace_back模板可变参数的特点
这里才可以看出emplace的优势,其它地方区别不大。
右值引用的原理
右值引用是 C++11 引入的一个重要特性,其主要作用是实现移动语义。通过右值引用,C++ 可以避免不必要的对象拷贝,显著提高程序的效率,尤其是在处理临时对象或大规模数据时。右值引用通过允许对象的资源在“将亡值”(临时对象)和左值之间转移,减少了不必要的内存开销。
以下是一个使用右值引用实现移动语义的 String 类的例子:
class String
{
public:
String(const char *str = "")
{
cout << "String(const char *str) - 普通构造" << endl;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String &s)
{
cout << "String(const String &s) - 拷贝构造" << endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
// 右值:将亡值
String(String &&s) : _str(nullptr)
{
cout << "String(String &&s) - 移动构造" << endl;
// 移动拷贝 代价小 效率高
// 传过来的是一个将亡值,将亡值是一个临时对象,临时对象的生命周期即将结束
// 将亡值的生命周期即将结束,我们可以将其资源移动过来
swap(_str, s._str);
}
String &operator=(const String &s)
{
cout << "String& operator=(const String& s) - 拷贝赋值" << endl;
if (this != &s) // 如果不是自己给自己赋值
{
char *newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
}
}
// 传入将亡值 - 进行移动拷贝
String &operator=(String &&s)
{
cout << "String& operator=(String&& s) - 移动赋值" << endl;
if (this != &s)
{
swap(_str, s._str);
}
return *this;
}
~String()
{
delete[] _str;
}
String operator+(const String &s2)
{
String ret(*this);
// ret.append(s2._str);
return ret;
// 返回的是一个将亡值
}
String &operator+=(const String &s2)
{
// this -> append(s2._str);
return *this;
// 返回的是一个左值
}
string f(const char *str)
{
string tmp(str);
return tmp;
// 返回的是一个将亡值
}
private:
char *_str;
};
int main()
{
String s1("s1");
String s2("s2");
String s3 = s1 + s2; // s1.operator+(s2)
return 0;
}
代码解析
- 普通构造函数:
String(const char *str = ""):通过字符串初始化一个新的String对象,并在堆上分配内存。这个操作通常涉及一个深拷贝。- 这里没有使用右值引用,所以每次创建一个对象时都需要复制字符串内容。
- 拷贝构造函数:
String(const String &s):通过拷贝构造一个String对象。这个构造函数会为新对象分配内存,并将原对象的内容复制过来,涉及一次深拷贝。
- 移动构造函数:
String(String &&s):这是右值引用的核心,用于移动构造。右值引用使得我们可以在临时对象的生命周期结束时,直接“窃取”它的资源(例如内存),避免了不必要的拷贝。此时,原对象的资源被置为nullptr,以防止析构函数删除它。swap(_str, s._str)使得资源从s移动到当前对象。
- 拷贝赋值运算符:
String& operator=(const String& s):通过深拷贝将一个对象的值赋给另一个对象。如果对象已经有值,会先删除原有资源,然后分配新的内存。
- 移动赋值运算符:
String& operator=(String&& s):类似于移动构造函数,它通过右值引用接收临时对象,交换资源,避免了不必要的拷贝操作。
- 析构函数:
~String():释放内存。此处的析构函数确保我们正确地清理分配的内存,避免内存泄漏。
- 加法运算符:
String operator+(const String &s2):此运算符通过拷贝构造一个新对象,并返回它。返回值是一个临时对象,因此此处会触发右值引用和移动语义。
- 加等运算符:
String &operator+=(const String &s2):返回左值,通常用来修改当前对象本身。
- 临时对象返回:
string f(const char *str):返回一个临时string对象,表示一个将亡值。在这里,返回的临时对象被移动,而不是复制。
右值引用本身并没有独立的语义,它的主要作用体现在配合移动构造函数和移动赋值运算符来实现移动语义。在需要将资源从一个对象转移到另一个对象时,右值引用提供了一种高效的方式,避免了传统的深拷贝。使用右值引用和移动语义,C++ 可以极大地提升程序的性能,尤其是在处理临时对象和大型数据结构时。
浙公网安备 33010602011771号