传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
4.右值引用
4.1 左值引用和右值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址并且可以对它赋值,左值可以出现赋值符号的左边(也可以出现在赋值符号的右边),右值不能出现在赋值符号左边(只可以出现在符号的右边)。定义时const
修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用(给左值取别名)。
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;
}
什么是右值?什么是右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回,如函数返回值,这个返回值在函数表达式中存在,但是出了函数作用域这个值就会被销毁,所以不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
右值被分为
1.纯右值(内置类型表达式的值)
2.将亡值(自定义类型表达式的值)
- 案例1
#include<iostream>
// 一个函数模板
template<class T>
T fmin(T x, T y)
{
if (x < y)
{
return x;
}
return y;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
// 字面常量:10;
// 表达式的返回值 x + y;
// 函数返回值 fmin(x, y);
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;
}
- 案例2
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
// ra1为a的别名,a是左值
int& ra1 = a;
// 编译失败,因为10是右值,右值是不可以被左值引用的
//int& ra2 = 10;
// const修饰的左值引用,既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
// int&& r2 = a; // 报错
// move()左值之后,编译器会将其识别为右值
// 右值引用可以引用move()以后的左值
int&& r3 = std::move(a);
return 0;
}
- 案例3
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1
引用后,可以对rr1
取地址,也可以修改rr1
。如果不想rr1
被修改,可以用const int&& rr1
去引用,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
double x = 1.1, y = 2.2;
// 右值引用之后,会导致右值被存储到特定位置(此时我们就可以对rr1进行修改,不过修改的是特定位置的变量,此时我们可以认为rr1就是一个左值),且可以取到该位置的地址
int&& rr1 = 10;
// const修饰rr2之后,则rr2不可以被修改了
const double&& rr2 = x + y;
rr1 = 20;
// rr2 = 5.5; // 报错
return 0;
}
4.2左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是
const
左值引用既可引用左值,也可引用右值
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
4.3 右值引用使用场景和意义
左值引用的意义是什么?
1.函数传参或者是函数传返回值,使用左值引用,可以减少参数的拷贝
但是左值引用并没有完全解决问题,例如以下场景
- 场景1
// 场景1:左值引用可以解决函数传参,减少参数的拷贝
// const引用,既可以接收左值,也可以接收右值
template<class T>
void func1(const T& x)
{
}
int main()
{
// v1为左值
vector<int> v1(10, 0);
func1(v1);
// vector<int>(10,0) 是一个匿名对象,也就是一个右值(属于将亡值)
// 出了当前这一行,这个匿名对象就会被释放
func1(vector<int>(10, 0));
return 0;
}
- 场景2:
// 场景2:函数传返回值,
// x的声明周期是直到main()返回,才会被销毁
// 因此我们才可以使用左值引用返回,如果x出func2()就被销毁,那么是不可以使用左值引用返回的
template<class T>
const T& func2(const T& x)
{
// ...假设中间还做了许多操作
return x;
}
int main()
{
// v1为左值
vector<int> v1(10, 0);
func2(v1);
return 0;
}
- 场景3
// 场景三:左值引用尚未解决的问题场景
// 当ret出func3()的函数作用于就会被销毁,那么我们是不可以使用左值引用返回的(这是因为引用的变量已经被销毁了)
// 因此,此时ret返回时,就会产生临时变量,就会增加参数ret的拷贝
// 右值引用的价值之一:就是补齐这个最后一块短板,传值返回的拷贝问题
template<class T>
T func3(const T& x)
{
T ret;
// ...
return ret;
}
int main()
{
// v1为左值
vector<int> v1(10, 0);
func3(v1);
return 0;
}
- 场景4
// 但是其实也可以使用左值引用来解决场景三的问题
// 就是使用输出型参数,但是这样使用起来是很别扭的
// 假设ret的类型就是int
// 使用了输出型参数就不需要进行返回了
template<class T>
void func4(const T& x, int& ret)
{
// ...
//return ret;
}
int main()
{
// v1为左值
vector<int> v1(10, 0);
int ret = 10;
// ret是一个输出型参数
func4(v1,ret);
return 0;
}
右值引用是怎样解决左值引用的短板的?
// 首先,我们先来看下面代码运行时,其底层的拷贝原理
#include<iostream>
#include<assert.h>
using namespace std;
namespace qwy
{
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& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
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)
{
push_back(ch);
return *this;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
// 将整数转化为字符串
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
// 将value变为正数
value = 0 - value;
}
qwy::string str;
while (value > 0)
{
// 获取value的个位
int x = value % 10;
// 获取value的十位及以上
value /= 10;
str += ('0' + x); // 将x转化为对应的ascll值,并放入str
}
if (flag == false)
{
str += '-';
}
// 逆转string对象中字符串的顺序
std::reverse(str.begin(), str.end());
return str;
}
}
场景一
int main()
{
// 场景1:
// to_string()的返回值str,会先将其拷贝给一个临时变量,再将临时变量拷贝给ret
// 但是编译器会自动对其进行优化,将两次拷贝简化为一次拷贝
// 如下图所示
qwy::string ret = qwy::to_string(-1234);
return 0;
}
- 打印结果为:
string(const string& s)
– 深拷贝 - 通过打印结果我们可知,只调用了一次深拷贝,符合我们预期的结果
场景二
// 场景2:
int main()
{
// 对于场景二:编译器不敢将两次深拷贝优化为一次深拷贝
// 优化的规定一般为:传参或传返回值过程中,存在连续的构造、拷贝构造、就会被优化
// 但是具体是取决于编译器的
// 由于编译器对于如下这种情况是不进行优化的,
// 因此to_string()的返回值str会先拷贝给临时变量,再由临时变量赋值给ret,但是赋值重载的过程中,会调用拷贝函数
qwy::string ret;
// 在上面创建string对象ret和下面对ret进行赋值之间,可能对ret进行了其他操作,因此编译器不敢进行优化
ret = qwy::to_string(-1234);
return 0;
}
注:编译器没有进行优化
第一步:拷贝给临时变量,调用了一次拷贝构造,由临时变量赋值给ret,调用了赋值重载,赋值重载的函数内部调用了一次拷贝构造
打印结果为:
string(const string& s)
– 深拷贝
string& operator=(string s)
– 深拷贝
string(const string& s)
– 深拷贝
4.4移动构造和移动赋值
// 移动构造
// 所谓的移动构造就是将string对象的右值引用s与将要构造的对象进行资源交换
// 这样是不需要在移动构造内部创建新的对象
string(string&& s)
{
cout << "string(const string& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值" << endl;
swap(s);
return *this;
}
#include<iostream>
#include<assert.h>
using namespace std;
namespace qwy
{
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& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
// 所谓的移动构造就是将s与将要构造的对象进行资源交换
string(string&& s)
{
cout << "string(const string& s) -- 移动拷贝" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
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)
{
push_back(ch);
return *this;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
// 将整数转化为字符串
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
// 将value变为正数
value = 0 - value;
}
qwy::string str;
while (value > 0)
{
int x = value % 10; // 获取value的个位
value /= 10; // 获取value的十位及以上
str += ('0' + x); // 将x转化为对应的ascll值,并放入str
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
场景一:右值引用解决了拷贝构造问题(只是对于右值传参拷贝的问题)
// 使用移动拷贝进一步提升构造的效率(移动构造的效率的大于拷贝构造的)
// 这就是右值引用解决了拷贝构造问题(只是对于右值传参拷贝的问题)
int main()
{
qwy::string s1("hello world");
// 用s1来构造s2
// s1是左值,因此编译器会默认调用左值引用传参的拷贝构造
qwy::string s2(s1);
// 左值被move()之后,编译器会将其看作是右值
// move(s1)是右值,编译器会默认调用右值引用传参的移动构造
// 如果没有右值引用传参的移动构造,move(s1)也是可以被左值引用传参的拷贝构造调用
qwy::string s3(move(s1));
return 0;
}
打印结果为:
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
场景二:利用右值引用解决赋值问题
int main()
{
// 根据优化的规则:传参或传返回值过程中,存在连续的构造、拷贝构造、就会被优化
// 因此对于如下的情况,编译器并不会进行优化
// 1.编译器会将to_string()的返回值默认识别为右值,并将其移动拷贝给临时变量,
// 2.再将临时变量移动赋值给ret
qwy::string ret;
ret = qwy::to_string(-1234);
return 0;
}
打印结果:
string(const string& s)
– 移动拷贝
string& operator=(string s)
– 移动赋值
场景三:
int main()
{
list<qwy::string> lt;
qwy::string s1("111111");
// s1是左值,调用拷贝构造
lt.push_back(s1);
// "222222" 是右值(字面常量),调用移动拷贝
// 如果我们没有实现移动拷贝,那么就会调用拷贝构造
lt.push_back(qwy::string("222222"));
// "333333" 会先构造一个string的对象,再调用移动拷贝
// 如果我们没有实现移动拷贝,那么就会调用拷贝构造
lt.push_back("333333");
return 0;
}
打印结果
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
string(const string& s)
– 移动拷贝
总结
右值引用和左值引用减少拷贝的原理不太一样
1.左值引用是取别名,直接起作用。
2.右值引用是间接起作用,实现移动构造和移动赋值,在拷贝的场景中,如果是右值(将亡值),转义资源
4,5右值引用引用左值及其一些更深入的使用场景分析
- 场景1:
#include<iostream>
using namespace std;
void func1(int& x)
{
cout << "void func1(int& x)" << endl;
}
void func2(int&& x)
{
cout << "void func2(int&& x)" << endl;
}
int main()
{
int x = 1;
// 左值调用对应的func1()
func1(x);
// 右值调用对应的func2()
func2(2);
return 0;
}
// 打印结果为:
// void func1(int& x)
// void func2(int&& x)
- 场景二
void func2(int&& x)
{
cout << "void func2(int&& x)" << endl;
}
int main()
{
int x = 1;
func2(2);
func1(x); // 此处会报错,右值引用无法引用左值
return 0;
}
- 模板中的&&万能引用
// 使用模板中的&&万能引用就可以解决场景二中:左值不可以被右值引用的问题了
// 模板的右值引用我们可以认为是万能引用,
// 既可以引用右值,也可以引用左值
template<typename T>
void PerfectForward(T&& t)
{
// t是否可以被加加,详情请看下图
// t++;
}
int main()
{
perfectforward(10); // 右值
int a;
perfectforward(a); // 左值
perfectforward(std::move(a)); // 右值
// 且万能引用既可以引用const的左值,也可以引用const的右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
- 万能引用存在的问题
// 万能引用还存在如下的问题:
#include<iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用
template<typename T>
void PerfectForward(T&& t)
{
// T&& t 不论是对左值引用,还是对右值引用,t本身都是一个左值
// 因此调用Fun()函数,始终会调用参数为左值引用的Fun()函数
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
// 打印结果为:
// 左值引用
// 左值引用
// 左值引用
// const 左值引用
// const 左值引用
- 解决万能引用存在的问题
// 使用完美转发来解决上述的问题
// 具体如下:
#include<iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用
template<typename T>
void PerfectForward(T && t)
{
// 完美转发,保持t的本源属性,如果t是对左值的引用,那么就保留t为左值的属性,反之保留t为右值的属性
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
// 打印结果为:
// 右值引用
// 左值引用
// 右值引用
// const 左值引用
// const 右值引用
对list类的改造(移动构造、移动赋值)(内部使用了完美转发)
// 关于list的封装
namespace qwy
{
// 链表节点的类模板
template<class T>
struct list_node
{
list_node* _next;
list_node* _prev;
T _data;
list_node(const T& x)
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{}
// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发
// 来保证x的原生类型属性(此处使用了完美转发)
list_node(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(std::forward<T>(x))
{}
};
// 迭代器的类模板
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> Self;
node* _pnode;
__list_iterator(node* p)
:_pnode(p)
{}
Ptr operator->()
{
return &_pnode->_data;
}
Ref operator*()
{
return _pnode->_data;
}
Self& operator++()
{
_pnode = _pnode->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_pnode = _pnode->_next;
return tmp;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_pnode = _pnode->_prev;
return tmp;
}
bool operator!=(const Self& it) const
{
return _pnode != it._pnode;
}
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
};
template<class T>
class list
{
typedef list_node<T> node;
public:
typedef __list_iterator<T, T&, T*> iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
void empty_initialize()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_initialize();
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
// 拷贝构造
list(const list<T>& lt)
{
empty_initialize();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
// 左值引用的push_back
void push_back(const T& x)
{
insert(end(), x);
}
// 右值引用的push_back
void push_back(T&& x)
{
// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发
// 来保证x的原生类型属性(此处,使用了完美转发)
insert(end(), std::forward<T>(x));
}
// 左值引用的insert
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos._pnode;
node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// 右值引用的insert
iterator insert(iterator pos, T&& x)
{
// 此处右值传参之后,x是左值,为了需要保证x的原生类型属性,因此我们需要使用完美转发
// 来保证x的原生类型属性(此处使用了完美转发)
node* newnode = new node(std::forward<T>(x));
node* cur = pos._pnode;
node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._pnode->_prev;
node* next = pos._pnode->_next;
prev->_next = next;
next->_prev = prev;
delete pos._pnode;
--_size;
return iterator(next);
}
private:
node* _head;
size_t _size;
};
}
使用list::push_back
int main()
{
qwy::list<qwy::string> lt;
qwy::string s1("111111");
lt.push_back(s1);
lt.push_back(qwy::string("222222"));
lt.push_back("333333");
return 0;
}
打印结果为
string(const string& s)
– 移动拷贝
string(const string& s)
– 深拷贝
string(const string& s)
– 移动拷贝
string(const string& s)
– 移动拷贝