目录
- 前言
- 一,左值引用和右值引用
- 二,左值引用与右值引用比较
- 三,探索引用的底层
- 四,右值引用使用场景和意义
- 4.1 解决返回值问题
- 4.2 STL容器插入接口的改变
- 五,移动语义
- 六,完美转发
- 6.1 模板中的&& 万能引用
- 6.2 forward 完美转发在传参的过程中保留对象原生类型属性
前言
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
一,左值引用和右值引用
1.什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 + 一般情况可以对它赋值,左值可以出现赋值符号的左边和右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
//左值:可以取地址
int* p = new int(0);
int b = 1;
const int c = 2;
*p = 10;
string s("11111111");
s[0];
cout << &c << endl;
cout << &s[0] << endl;
cout << &s << endl;
return 0;
}
2.什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
右值又可以分为:纯右值和将亡值。
纯右值:内置类型右值。
将亡值:类类型的右值,如匿名对象,类型转换过程中产生的临时对象。
int main()
{
//右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值:常量,临时对象,匿名对象
10;
x + y;
fmin(x, y);
string("2222222");
//err
//cout << &10 << endl;
//cout << &(x + y) << endl;
//cout << &(fmin(x + y));
return 0;
}
二,左值引用与右值引用比较
左值引用总结:
1.左值引用只能引用左值,不能引用右值。
2.但是const左值引用既可引用左值,也可引用右值。
代码示例1:
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
*p = 10;
string s("11111111");
s[0];
//左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
//左值引用引用给右值取别名
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string&& rx4 = string("2222222");
return 0;
}
代码示例2:
比如在经常使用的容器中其实也有左值右值的身影:
//void push_back(const T& x) //这里加const -> 既可以传左值,也可以传右值
string s1("3333333");
vector<string> v;
v.push_back(s1); //有名对象->传左值
v.push_back(string("3333333")); //匿名对象->传右值
v.push_back("3333333"); //单参数构造函数支持隐式类型转换,中间会产生临时变量
右值引用总结:
1.右值引用只能右值,不能引用左值。
2.但是右值引用可以move以后的左值。
代码示例:
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值:常量,临时对象,匿名对象
10;
x + y;
fmin(x, y);
string("2222222");
//右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
//右值引用引用给左值取别名
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
return 0;
}
三,探索引用的底层
在语法层面有左值引用和右值引用的概念,那在底层它们的本质是什么呢?
int main()
{
//当到汇编层时,就没有左值引用右值引用的概念了,只有指针
int x = 0;
int& r1 = x;
int&& rr1 = x + 10;
//move的本质:就是强制类型转换,只是为了通过语法层的检验而已
//string&& rrx5 = (string&&)s;
return 0;
}
转到反汇编查看:
结论:到了汇编层时,就没有引用的概念了,它们的本质都是指针。而move的本质,其实就是强制类型转换而已,只是为了通过语法层的检查。
四,右值引用使用场景和意义
4.1 解决返回值问题
引用的意义:减少拷贝,提高效率。
左值引用解决的场景:引用传参/引用传返回值
左值引用没有彻底解决的场景:传返回值
下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!接下来我们要用到以前自己模拟实现的 string 类来验证。
下面的验证都要用 VS2019 才能观察到!!
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
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)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
reserve(s._capacity);
for (auto ch : s)
push_back(ch);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
push_back(ch);
}
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];
if(_str)
{
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;
}
const char* c_str() const
{
return _str;
}
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 = 0 - value;
}
bit::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;
}
}
左值引用的短板:
但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
图解如下:
右值引用解决上述问题:
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
//移动构造
//临时创建的对象,用完就要消亡了
//深拷贝的类,移动构造才有意义
string(string&& s)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s); // 直接转移资源
}
int main()
{
bit::string ret2 = bit::to_string(1234);
return 0;
}
再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
图解如下:
不仅仅有移动构造,还有移动赋值:
在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动赋值。
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
int main()
{
bit::string ret1;
ret1 = bit::to_string(1234);
return 0;
}
图解如下:
总结:
从此以后,类似下面这种传值返回的场景随便用,因为浅拷贝的类没什么代价,深拷贝的类会走移动拷贝和移动赋值。
T func()
{
T ret;
//……
return ret;
}
4.2 STL容器插入接口的改变
int main()
{
list<bit::string> lt;
bit::string s1("111111111111111111");
// 这里调用的是拷贝构造
lt.push_back(s1);
// 下面调用都是移动构造
lt.push_back("22222222222222222222222");
lt.push_back(std::move(s1));
return 0;
}
五,移动语义
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
右值在传递过程中的退化。要验证这句话,接下来我们还是要用到以前自己模拟实现的 list 类,和 string类 来验证
下面的是在 VS2022 下的运行结果。
namespace bit
{
template <class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T())
:_next(nullptr)
,_prev(nullptr)
,_data(data)
{}
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(data)
{}
};
template <class T,class Ref,class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;//名字变得简短
Node* _node;//定义一个节点指针
ListIterator(Node* node)
:_node(node)
{}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//后置:返回之前的值
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template <class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
//iterator it(_head->_next);
//return it;
return iterator(_head->_next);//使用匿名对象
}
iterator end()
{
return iterator(_head);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
//空初始化,申请哨兵位头节点
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
list(const list<T>& lt)
{
empty_init();
//注意:使用范围for时加上const和&
for (const auto& e : lt)
{
push_back(e);
}
}
//赋值拷贝
//lt1 = lt3
list<T>& operator=( list<T> lt)
{
swap(_head, lt._head);
return *this;
}
//析构:销毁整个链表
~list()
{
clear();
delete _head;
_head = nullptr;
}
//清空当前数据 留头节点,其余节点释放
void clear()
{
auto it = begin();
while (it != end())
{
//返回删除节点的下一个节点的迭代器
it = erase(it);
}
}
//尾插:end的下一个位置
void push_back(const T& x)
{
insert(end(), x);
}
//右值引用版本
void push_back(T&& x)
{
insert(end(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;//找到当前节点
Node* newnode = new Node(x);//申请节点
Node* prev = cur->_prev;//找到前一个节点
//prev newnode cur 进行链接
newnode->_next = cur;
cur->_prev = newnode;
prev->_next = newnode;
newnode->_prev = prev;
return iterator(newnode);
}
//右值引用版本
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;//找到当前节点
Node* newnode = new Node(x);//申请节点
Node* prev = cur->_prev;//找到前一个节点
//prev newnode cur 进行链接
newnode->_next = cur;
cur->_prev = newnode;
prev->_next = newnode;
newnode->_prev = prev;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());//防止删除头节点
Node* cur = pos._node;//找到当前节点
Node* prev = cur->_prev;//找到前一个节点
Node* next = cur->_next;//找到后一个节点
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
}
假设插入以下值,观察运行结果:
int main()
{
bit::list<bit::string> lt;
bit::string s1("111111111111");
lt.push_back(s1);
lt.push_back(bit::string("222222222"));
lt.push_back("222222222");
lt.push_back(move(s1));
return 0;
}
运行结果:
疑问:在 list类中,我们已经实现了右值引用版本的插入函数和构造函数,在string类中也有移动构造,为什么传递右值,结果还是调用的深拷贝呢?
解答:因为右值引用本身的属性是左值, 只有是左值,才能转移它的资源(才能在string类中swap转移资源)。
也就是说,我们在main函数中push_back右值,在参数传递的过程中,push_back函数虽然匹配的是右值引用的那个,但是右值引用的那个参数本身是左值属性的,所以它进行下一步传递时又会去匹配那个左值引用的函数了,这就是所说的右值在传递过程中的退化。
使用move再进行一层类型转换,把过程中的左值再转换成右值,就可以解决问题。
运行结果:
六,完美转发
6.1 模板中的&& 万能引用
(1) 下面代码的模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
(2) 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
6.2 forward 完美转发在传参的过程中保留对象原生类型属性
在下面的示例中,准备了各种形式的左值和右值,但是我们要在传参的过程中保留对象的原来的属性,就要加上forward。
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(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;
}
运行结果:
到这里,我们可以进行一下简单的总结:
1.在传递右值的过程中,如果我们要保持该对象原来的属性,可以使用 move 直接强制,也可以使用 forward 进行完美转发。
2.move和forward的区别:
move用于我们确定知道是一个右值引用;
forward用于在模板中,不知道是左值还是右值引用时。