文章目录
- 一、左值与左值引用
- 二、右值与右值引用
- 三、 左值引用与右值引用比较
- 四、右值引用使用场景和意义
- 1.左值引用的短板
- 2.移动构造和移动赋值
- 3.STL中右值引用的使用
- 五、万能引用与完美转发
- 1.万能引用
- 2.完美转发
一、左值与左值引用
在C++11之前,我们把数据分为常量和变量,在C++11之后,我们将数据分为左值和右值
此外,传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,还可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边,定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址,如下:
// 以下的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;
二、右值与右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址,如下:
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
右值引用就是对右值的引用,给右值取别名
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
注意事项:
1.为什么函数返回值是右值:当函数返回的是一个局部变量的时候,因为局部变量出了函数作用域生命周期就会结束,所以返回时会将该变量拷贝到寄存器中,然后返回这个寄存器中的内容,而寄存器中的变量是临时变量,临时变量具有常量,属于右值。其实在函数建立栈帧的时候,不仅会有参数的压栈,还会有返回值的压栈,即在两个函数的栈帧之后的一个空间在存贮函数的返回值。通过这个中间值将函数的返回值进行返回。
2.为什么右值不能取地址:在C++中,右值则是一个临时使用的,不可寻址的内存空间,右值没有独立的内存空间,它只是存储在寄存器或者其他临时内存中的一个值,我们也不能将右值放入内存,因为右值没有确定的内存位置,所以右值不能取地址
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
int main()
{
int x = 1, y = 2;
int&& rr1 = 10;
const int&& rr2 = x + y;
rr1++;
cout << &rr1 << endl;
cout << rr1 << endl;
cout << &rr2 << endl;
return 0;
}
rr2 = 5; // 报错
所以如果我们不希望改变右值引用,我们就需要将右值引用定义为const右值引用
三、 左值引用与右值引用比较
左值引用只能引用左值,不能引用右值。但是const左值引用既可引用左值,也可引用右值,因为 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;
}
四、右值引用使用场景和意义
1.左值引用的短板
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?
我们先看看左值引用的两个作用:
1.修改实参的值
2.引用做参数/函数返回值可以减少拷贝
我们可以把函数形参定义为实参的引用,这样函数在传参时就不用拷贝构造形参了,从而提高程序的效率,特别是对于需要深拷贝的自定义类型来说。左值引用作为返回值的效果也一样,当返回的对象出了作用域还存在时,直接使用引用返回可以减少一次拷贝构造:
void func1(string s)
{}
void func2(const string& s)
{}
int main()
{
string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。因为局部变量出了函数作用域就不存在了,此时引用就是一个野指针
template <class T>
T func(const T& x)
{
T tmp;
//...
return tmp;
}
这种情况下编译器会使用这个临时对象拷贝构造一个临时对象,然后再返回这个临时对象,也就是说,这样会比引用返回多一次拷贝构造,当局部对象是一个需要进行深拷贝的自定义类型的时候,比如vector<vector>,拷贝构造的代价就会很大,所以右值引用的提出就是为了补足左值引用存在的这些短板
2.移动构造和移动赋值
为了更好的演示左值引用和右值引用对拷贝构造的优化,我们自己实现一个string类,在拷贝构造/赋值重载函数中进行打印相关的信息便于观察:
namespace hdp
{
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)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
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(string&& s)
// :_str(nullptr)
// , _size(0)
// , _capacity(0)
//{
// cout << "string(string&& s) -- 移动语义" << endl;
// swap(s);
//}
移动赋值
//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;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
现在假设我们要实现一个to_string函数,代码如下:
namespace hdp
{
hdp::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
hdp::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
我们可以看到,由于to_string函数的返回值是一个局部的对象,所以我们这里只能使用传值返回,而传值返回对于string来说需要进行深拷贝
其实这里程序的执行结果和我们预想的并不一样,正常的情况应该是str先拷贝构造一个临时对象,然后再由这个临时对象来拷贝构造ret,所以应该是两个拷贝构造(),上面的结果第一次构造是to_string函数内部构造str,第二次是函数返回时调用拷贝构造,而我们实现的拷贝构造函数中又调用了一次构造函数,所以打印了两个构造。但是编译器的优化只能适用于部分场景,对于很多场景还是会拷贝构造产生临时对象
尽管编译器进行了优化,这里还是会有一次拷贝构造,那么我们能不能想办法将str的资源直接赋值给s,中间不产生拷贝构造呢,此时我们就需要用到右值引用 了
C++11中的右值可以分为两种:
1.纯右值:内置类型表达式的值
2.将亡值:自定义类型表达式的值:所谓的将亡值就是指声明周期马上就要结束 的值,一般来说匿名对象,临时对象,move后的自定义类型都可以看做将亡值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
我们需要注意的是,上面我们说右值不能取地址其实是右值的严格定义,但其实将亡值也是可以被当做右值看待的,而将亡值有独立的内存空间,可以取地址。既然将亡值的声明周期马上就要结束了,那么在拷贝构造中我么可以直接将将亡值的资源拿过来给我们自己使用,这样就需要需要进行深拷贝了。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
这样我们就重载了一个右值引用版本的构造函数–移动构造,这样当参数为右值的对象需要进行拷贝构造的时候就会调用此函数,在函数中,我们直接交换两个对象的资源,从而使得深拷贝变成了浅拷贝,提供了程序的效率
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己
我们使用移动构造之后,我们之前的程序就会减少深拷贝的次数
由此,我们通过移动构造将深拷贝变成了浅拷贝
但是我们需要注意的是,只有当参数为右值时才会调用移动构造,当实参为左值的时候还是会调用拷贝构造函数,因为编译器不知道我们是否还会对左值进行操作,所以它不敢拿走左值的资源来构造新的对象
移动赋值和移动构造同理,只是移动赋值中将亡值还需要释放之前的资源,不过这个过程是自动的
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
有人提出这样的一个观点:右值引用延长了变量的声明周期,这种说法是不准确的,因为右值引用只是将变量的资源转移给了另一个变量,让它的资源能够不随着变量的销毁而释放,而该变量本身的生命周期是没有变化的
【总结】
1.左值引用让形参称为实参的别名,直接减少拷贝
2.右值引用是通过实现移动构造和移动赋值,将将亡值的资源进行转移,间接的减少拷贝(浅拷贝的类不需要进行资源的转移,所以也没有移动赋值和移动拷贝)
3.STL中右值引用的使用
C++11在设计出右值引用之后,为STL所有容器都提供了移动构造和移动赋值,包括容器适配器
此外,还提供了右值版本的插入接口:
所以,以后如果我们要向容器中插入需要深拷贝的自定义类型的数据时,我们尽量使用匿名构造对象进行插入,这样调用的就是右值插入接口,元素会调用移动拷贝函数完成拷贝,从而提高程序的效率
我们可以将我们自己实现的list类支持右值版本的插入接口,部分代码如下:
#pragma once
#include <assert.h>
#include <algorithm>
namespace hdp
{
// 定义节点结构
template<class T>
struct list_node
{
list_node<T>* _prev;
list_node<T>* _next;
T _data;
// 构造
list_node(const T& x)
:_prev(nullptr)
, _next(nullptr)
, _data(x)
{}
// 移动构造
list_node(T&& x)
:_prev(nullptr)
, _next(nullptr)
, _data(move(x))
{}
};
// 封装迭代器
// 同一个类模板实例化出的两个类型
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
template<class T, class Ref, class Ptr>
// 迭代器类
struct __list_iterator
{
typedef list_node<T> node; // 重命名为list节点
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 it._pnode != _pnode;
}
// 重载等于
bool operator==(const Self& it) const
{
return _pnode == it._pnode;
}
};
// 定义list类
template<class T>
class list
{
// list 节点
typedef list_node<T> node;
public:
typedef __list_iterator<T, T&, T*> iterator; //迭代器
typedef __list_iterator<T, const T&, const T*> const_iterator; //const迭代器
// 迭代器
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
// iterator it(_head);
// return it;
// 匿名对象构造
return iterator(_head);
}
// const 迭代器
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
// 创建哨兵节点
void empty_initialize()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
// 构造 不是list<T>的原因,构造函数名和类名相同,而list<T>是类型
list()
{
empty_initialize();
}
// 迭代器构造
template<class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_initialize();
while (first != last)
{
push_back(*first);
++first;
}
}
// 拷贝构造
// lt2(lt1)
list(const list<T>& lt)
{
empty_initialize();
for (const auto& e : lt)
{
push_back(e);
}
}
// 拷贝构造现代写法
list(list<T>& lt)
{
empty_initialize();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// 赋值重载
list<T>& operator=(const list<T>& lt)
{
if (this != <)
{
clear();
for (const auto& e : lt)
{
push_back(e);
}
}
return *this;
}
// 赋值重载现代写法
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
// 交换
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
size_t size() const
{
return _size;
}
bool empty() const
{
// return _head->_next == _head;
// return _head->_prev == _head;
return _size == 0;
}
// 析构
~list()
{
clear();
delete _head;
_head = nullptr;
}
// 清理
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
_size = 0;
}
// 尾插
void push_back(const T& x)
{
//node* newnode = new node(x);
//node* tail = _head->_prev;
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
insert(end(), x);
}
// 尾插--右值版本
void push_back(T&& x)
{
insert(end(), move(x));
}
// 头插
void push_front(T& x)
{
insert(begin(), move(x));
}
// 头插 -- 右值版本
void push_front(const T& x)
{
insert(begin(), x);
}
// 尾删
void pop_back()
{
//earse(end()->prev);
erase(--end());
}
// 头删
void pop_front()
{
erase(begin());
}
// 在pos之前插入数据
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos._pnode;
node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// 在pos之前插入数据 -- 右值版本
iterator insert(iterator pos, T&& x)
{
node* newnode = new node(move(x));
node* cur = pos._pnode;
node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
// 删除pos位置的数据
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 = 0; // 保存节点个数
};
}
list的函数调用逻辑如下:
我们需要注意的是,右值函数的形参的类型都是T&& x,而不是const T&& x,这是因为最终在hdp::string类中我们需要将x的资源转移给别人,这就要求x必须是可以修改的,能够交换_str,_size,_capacity三个指针,此外,右值引用x之所以能够被修改是因为给右值取别名之后,右值会被存储起来,右值引用虽然引用的是右值,但是右值引用本身是左值,所以当我们继续往下一层进行传递参数的时候,我们需要将x重新move为右值,否则下一层调用时就会调用参数为左值的函数
五、万能引用与完美转发
1.万能引用
我们上面都是单独定义一个参数为右值引用的函数,然后让编译器根据实参的类型来判断调用左值引用还是右值引用的函数,我们能不能让函数能够根据实参的类型自动实例化出对应的不同函数呢,万能引用就实现了这个功能
万能引用是一个函数模板,且函数的形参类型为右值引用,对于这样的函数模板模板,编译器能够自动根据实参的类型–左值/const 左值/右值/const右值,自动推演实例化出不同参数形参分别为左值引用/const左值引用/右值引用/const右值引用的函数
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
const int b = 8;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
我们可以看到,无论实参是什么类型,模板函数都能正确的接收并实例化出对应的引用类型,所以我们把形参为右值引用的函数模板你叫做万能引用,其中,当实参为左值或者const左值的时候,T&&会被实例化为T& 或者const T&,我们称其为引用折叠,即将&&折叠为&
2.完美转发
尽管完整引用能够接收任何类型的参数,但是这里还是存在一个很大的问题,万能引用实例化后函数的形参的属性全都是左值,如果实参为左值/const左值,那么实例化函数的形参是左值/const左值,如果实参是右值/const右值,虽然实例化函数的形参是右值引用/const右值引用,但是右值引用本身是左值,所以就会出现下面的情况:
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)
{
Fun(t);
}
int main()
{
int a;
const int b = 8;
PerfectForward(a); // 左值
PerfectForward(b); // const 左值
PerfectForward(10); // 右值
PerfectForward(std::move(b)); // const 右值
return 0;
}
此外,我们这里也不能简单的将t move后传递给Fun函数,因为这样会让t全部变为右值,又满足不了实参为左值的情况
为了在传参的时候能够保留对象的原始类型,C++设计了 完美转发–forward
【总结】
1.为了弥补左值引用局部对象返回会发生拷贝构造问题,C++11设计出了右值引用,右值引用可以通过移动构造和移动赋值来实现资源转移,将深拷贝转化为浅拷贝,从而提高了效率,此外,还为STL容器提供了右值版本的插入接口,由于右值引用本身是左值,所以函数参数往下一层传递时不能保证参数仍为右值,所以提供了move,可以将左值变为右值
2.为了使得函数模板能够同时接收const左值和const右值并正确实例化为对应的引用类型,C++11设计了万能引用,但是无论是左值引用还是右值引用,其本身是左值,所以往下一层传递时不能保证其类型了,此时move也不能够解决问题了,所以C++11设计了完美转发,来保证传参过程中原生类型属性能够保持不变