左值引用和右值引用
在C++中,左值是一个表示数据的表达式,我们可以获取它的地址,一般可以对它赋值,通常可以出现在左边或右边,左值引用就是对左值的引用,相当于给左值起了一个别名。
例子:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值同样也是一个表示数据的表达式,如:字面常量、表达式的返回值、函数的返回值。我们不能获取它的地址,不能对它赋值。右值可以出现在赋值符号的右边,但不能出现在左边。右值引用就是对右值的引用,相当于给右值起了一个别名。
例子:
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左、右值引用的区别
关于左值引用的总结
1. 左值引用可以绑定左值,但不能绑定右值。
2. const左值引用既可以绑定左值又可以绑定右值。
例子:
int a = 10;
int& ra1 = a; // 左值引用
//int& rn = 10; ---编译器报错,因为左值引用不能绑定右值
const int& rn = 10; // const左值引用可以绑定右值
const int& ra2 = a; // const左值引用当然也可以绑定左值
关于右值引用的总结
1. 右值引用可以绑定右值,但不能绑定左值。
2. 右值引用可以绑定左值调用move后的返回结果。
例子:
int&& rn = 10; // 右值引用当然可以绑定右值
int a;
//int&& ra1 = a; ---编译器报错,因为右值引用不能绑定左值
int&& ra2 = std::move(a); // 右值引用可以绑定左值调用move后的返回结果
// 请注意,左值在调用move后,它本身依然是左值,还是不能被右值引用绑定
右值引用的使用场景
在前面的学习中,我们对左值引用和右值引用有了最基本的了解,可能有同学会好奇了,左值引用好像就能绑定左值和右值了,为什么c++11还要引入右值引用这个技术呢?所以接下来让我们看看左值引用的短板,以及右值引用是如何弥补这一短板的吧!
首先,回忆一下我们以前学习过的左值引用的使用场景,因为左值引用相当于给变量起了别名,所以当我们传递参数或者返回函数的结果时,可以用左值引用来减少不必要的复制。但是,单就这两种场景,左值引用就真的能够完全胜任吗?
我们知道,函数中定义的局部变量出了函数作用域就会被释放,那么如果我们使用左值引用返回这个局部变量,就会出现悬空引用的问题,于是在没有右值引用之前,我们不得不直接返回值,所以左值引用不能对这种情况进行优化。
而右值引用正是为了处理这一场景而出现的,C++中,我们把右值分为纯右值和将亡值,如下图这个例子中,变量ret即将被销毁,就属于将亡值。刚刚一门一直在说不必要的复制,那具体是复制什么呢?
我们知道,ret作为一个字符串类型的变量,我们需要为它开辟一块内存空间,然而这个将亡值在出了函数作用域之后就被销毁了,要怎么把返回值传递给s呢?其实这涉及到一个小知识点:当函数返回一个局部变量时,编译器会创建一个临时变量来持有返回值。
所以说,这个例子其实是这样:编译器创建临时变量时调用拷贝构造进行一次深拷贝,开辟了一块内存空间把字符串存了进去,然后ret这个局部变量被销毁时,自己的内存空间也被释放;临时变量赋值给s时调用赋值重载再进行一次深拷贝,有开辟了一块内存空间,把字符串存了进去,然后临时变量的内存空间也被释放。
不难发现,这个过程中,我们进行了两次完全没有必要的深拷贝,而且释放这些空间也是有一定消耗的。机智如你肯定已经发现了端倪,既然这个ret和临时变量本来就快被销毁了,那为啥还要专门给它们开辟内存呢?直接这样,把ret的内存空间转移给临时变量,再把临时变量的内存空间转移给s不就完了吗?是的!其实两步就是传说中的移动构造和移动赋值重载。
那这和右值又有什么关系呢?这是因为转移内存空间这种事还是有点危险的,如果所有类型的变量都能这样做不就乱套了吗!所以移动语义只能通过右值引用来完成,因为应用场景本来就也只是对将亡值进行资源转移嘛。

因为移动语义能够弥补左值引用不能返回局部变量的短板,大量减少不必要的深拷贝和释放空间,所以stl容器基本都引入了移动构造和移动赋值重载:


移动语义的简单实现
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
#include <cassert>
#include <algorithm>
using namespace std;
namespace MySTL
{
// 我们为了方便测试移动语义自己写的string类
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str) -- 构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
/*string tmp(s);
swap(tmp);*/
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)-- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
MySTL::string to_string(int x)
{
MySTL::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
}
int main()
{
MySTL::string s;
s = MySTL::to_string(1234);
return 0;
}
万能引用和完美转发
在学习什么是万能引用之前,让我们先来看一个简单的小例子:
void test(int& val)
{
std::cout << "left val: " << val << std::endl;
}
void test(int&& val)
{
std::cout << "right val: " << val << std::endl;
}
int main()
{
int a = 2;
test(a);
test(2);
return 0;
}
可以看到,我们对test函数进行了重载,既可以传入左值引用也可以传入右值引用,这当然没有问题,但是当我们的函数不止有一个参数时,如果还想同时支持左值引用和右值引用,就需要重载多次了,比方说有两个参数时:(&,&);(&,&&);(&&,&);(&&,&&),当有n个参数时,就要重载2^n次,显然是不现实的,我们有更方便的做法:
template<typename T>
void test(T&& val)
{
std::cout << "left or right val: " << val << std::endl;
}
此时我们的函数既能传入左值引用又能传入右值引用,当不止一个参数时,多一个模板参数就行了,因为这种方式支持传入各种类型的变量,所以被称为万能引用。
那么这个万能引用这么方便,有没有什么需要注意的地方呢?还真有,请看下面的例子:
#include <iostream>
void test1(int& val)
{
std::cout << "left val: " << val << std::endl;
}
void test1(int&& val)
{
std::cout << "right val: " << val << std::endl;
}
void test(int&& val)
{
test1(val);
}
int main()
{
int a = 2;
test(2);
return 0;
}
![]()
结果有些奇怪,我们往函数中传了一个右值引用进去,但是我们想使用它的时候,它却变成了左值引用,这个现象就是所谓的引用折叠,这肯定是会导致某些情况下出现不符合预期的情况的,于是完美转发应运而生:
void test(int&& val)
{
test1(std::forward<int&&> (val));
}
![]()
完美转发通常是和万能引用一起使用的:
#include <iostream>
void test1(int& val)
{
std::cout << "left val: " << val << std::endl;
}
void test1(int&& val)
{
std::cout << "right val: " << val << std::endl;
}
template<typename T>
void test(T&& val)
{
test1(std::forward<T> (val));
}
int main()
{
int a = 2;
test(a);
test(2);
return 0;
}

这样我们的函数就能够传入左值或右值,并且完美转发函数,不会出现不符预期的情况了。
移动语义和万能引用总结
最后我们来做个小总结:
移动语义指的是允许将资源的所有权从一个对象转移到另一个对象,而不需要复制资源,通常通过右值引用实现,能够提升性能,特别是在处理大型对象时。
而万能引用是指一个模板参数可以接受左值或右值,允许函数接收任何类型的值,并在内部决定如何处理它,通常和完美转发一起使用。
在编程开发中,它们通常一起使用,从而灵活和高效地对资源进行管理。

3547

被折叠的 条评论
为什么被折叠?



