🌈个人主页 :@ꪔ小林Y
✨个人专栏 :《C++》,《C语言》,《数据结构入门------从原理到实战》,《拾光Linux》,《网络安全》
🍀代码信条 :每一行代码都是成长的脚印👣,每一次调试成功都是对坚持的回应
目录
【C++11】1
1.右值引用和移动语义
C++11之前学的引用是左值引用,C++11新增了右值引用语法特性,无论是左值引用还是右值引用,都是给对象取别名。
1.1左值和右值
- 左值(lvalue) :一个表示数据的表达式,存储在内存中,可以获取它的地址,左值可以出现在赋值符号的左边,也可以在右边。定义时const修饰符后的左值,不能给它赋值,但可以取它的地址。如存储在内存中,有明确存储地址可以取地址的对象。
cpp
int main()
{
//左值:可以取地址
//以下的p,b,c,*p,s,s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p= 10;
string s("11111");
s[0] = 'x';
}
- 右值(rvalue) :是一个数据的表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。如临时变量,字面量常量,存储于寄存器中的变量等。
cpp
//右值:不能取地址
double x = 1.1, y = 2.2;
//以下10,x+y,fmin(x,y),string("11111")都是常见的右值
10;
x + y;
fmin(x, y);
string("11111");
1.2.左值引用和右值引用
- 左值引用就是给左值取别名,例:
int& r1=x;,右值引用同样的道理,例:int&& rr1=y; - 左值引用不能直接引用右值,但是const左值可以引用右值 ,例:
const int& rx1=10; - 右值引用不能直接引用左值,但是右值引用可以引用move(左值) ,例:
int && rrx1=move(b); - 注意,变量表达式都是左值属性 ,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。

- 左值引用解决的效率问题:传左值引用传参,减少拷贝;传左值引用返回,减少拷贝(部分场景)
左值和右值的参数匹配:C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const 左值引用),实参是右值会匹配f(右值引用)。
1.3引用延长生命周期
右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生存期,但这些对象无法被修改。

1.4移动构造和移动赋值
-
移动构造函数是⼀种构造函数 ,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引用 ,但是不同的是要求这个参数是右值引用 ,如果还有其他参数,额外的参数必须有缺省值。

-
移动赋值是⼀个赋值运算符的重载 ,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。

-
对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类 ,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类,他的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。
1.5引用折叠
- 通过模板或者typedef中的类型操作可以构成引用的引用。
- 规则:右值引用的右值引用折叠成右值引用,左右其他组合均折叠成左值引用。
cpp
int main()
{
//int&& &r=i;这样直接引用是有问题的,会报错
typedef int& lref;
typedef int&& rref;
int n = 0;
//引用折叠
lref& r1 = n;//r1的类型是int&
lref&& r2 = n;//r2的类型是int&
rref& r3 = n;//r3的类型是int&
rref&& r4 = 1;//r4的类型是int&&
}
cpp
//由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)
{}
//例:
//1.没有折叠->实例化为void f1(int& x)
f1<int>(n);
f1<int>(0);//传右值会报错
//2.折叠->实例化为void f1(int& x)
f1<int&>(n);
f1<int&>(0);//传右值会报错
//3.折叠->实例化为void f1(int& x)
f1<int&&>(n);
f1<int&&>(0);//传右值会报错
//4.折叠->实例化为void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
//5.折叠->实例化为void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
cpp
//由于引用折叠限定,f2实例化以后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)
{}
//例:
//1.没有折叠->实例化为void f2(int&& x)
f2<int>(n);//报错
f2<int>(0);
//2.折叠->实例化为void f2(int& x)
f2<int&>(n);
f2<int&>(0);//报错
//3.折叠->实例化为void f2(int&& x)
f2<int&&>(n);//报错
f2<int&&>(0);
- 由此,引出了万能引用:
传左值,实例化出左值引用的Function函数
传右值,实例化右值引用的Function函数
cpp
template <class T>
void Function(T&& t)
{
int a=0;
T x=1;
cout<<&a<<endl;
cout<<&x<<endl<<endl;
}
1.6完美转发
变量表达式都是左值 属性,也就意味着一个右值被右值引用绑定后 ,右值引用变量表达式的属性是左值,我们想要保持对象的属性,就需要使用完美转发实现。
bash
template <class T> T&& forward (typename remove_reference<T>::type& arg);
bash
template <class T> T&& forward (typename remove_reference<T>::type&& arg);
cpp
template<class X>
void push_back(X&& x)
{
//完美转发是为了:
//保持x的属性往下传递
//实例化push_back(Lin::string&& x),保持右值属性往下传递
//实例化push_back(Lin::string& x),保持左值属性往下传递
insert(end(),forward<X>(x));
}
2.可变参数模板
2.1概念理解
C++11支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包:template<class... Args>; ,表示零或多个模板参数;函数参数包:void Func(Args&&... args)(args为零或多个参数);:表示零或多个函数参数。通过...触发参数包展开的过程叫包扩展
template <class ...Args> void Func(Args... args) {}//传值形参template <class ...Args> void Func(Args&... args) {}//传左值引用形参template <class ...Args> void Func(Args&&... args) {}//传万能引用形参
用省略号指出⼀个模板参数或函数参数的⼀个包 。函数参数包可以用左值引用或右值引用表示,每个参数实例化时遵循引用折叠规则。
2.2折叠表达式(c++17)
cpp
template<class ...Args>
void Print(Args... args)
{
((cout<<args<<" "),...);
cout<<"\n";
}
int main()
{
Print();
Print(1);
Print(1,string("xxxx"));
Print(1,string("xxxxx"),2.2);
return 0;
}

2.3emplace接口
empalce系列的接口均为模板可变参数 ,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
bash
template <class... Args> void emplace_back (Args&&... args);
bash
template <class... Args> iterator emplace (const_iterator position,
Args&&... args);
empalce_back和push_back对比:
push_back:需先构造临时对象,再通过拷贝 / 移动构造到容器中;
emplace_back:接收构造对象的参数包,直接在容器节点中构造对象,减少一次拷贝 / 移动。
下面场景详细看一下区别
- 相同: 例:
构造pair+拷贝/移动构造pair到list节点中data上
- 不同:例:
直接把构造pair参数包往下传,直接用pair参数包构造pair,push_back做不到
lt1.emplace_back("苹果",1);
//lt1.push_back("苹果",1);错误,要传pair或者{}隐式转换pair的值 lt1.push_back({"苹果",1});
2.4模拟实现list的emplace_back
cpp
#pragma once
namespace Lin
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(move(data)) // 移动构造或拷贝构造
{}
template<class... Args>
list_node(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{}
};
template<class T, class Ref, class Ptr>
struct list_iterator
{
using Self = list_iterator<T, Ref, Ptr>;
using Node = list_node<T>;
Node* _node;
list_iterator(Node* node)
:_node(node)
{}
// *it = 1
Ref operator*() {// ......}
Ptr operator->() {// ......}
// ++it
Self& operator++() {// ......}
Self operator++(int) {// ......}
// --it
Self& operator--() {// ......}
Self operator--(int) {// ......}
bool operator!=(const Self& s) const {// ......}
bool operator==(const Self& s) const {// ......}
};
template<class T>
class list
{
using Node = list_node<T>;
public:
using iterator = list_iterator<T, T&, T*>;
using const_iterator = list_iterator<T, const T&, const T*>;
// using iterator = list_iterator<T>;
// using const_iterator = list_const_iterator<T>;
iterator begin() {// ......}
iterator end() {// ......}
const_iterator begin() const {// ......}
const_iterator end() const {// ......}
void empty_init() {// ......}
list() {// ......}
list(initializer_list<T> il) {// ......}
template <class InputIterator>
list(InputIterator first, InputIterator last) {// ......}
list(size_t n, T val = T()) {// ......}
list(int n, T val = T()) {// ......}
~list() {// ......}
// 传统写法
// lt2(lt1)
list(const list<T>& lt) {// ......}
// lt1 = lt3;
list<T>& operator=(const list<T>& lt) {// ......}
void swap(list<T>& lt) {// ......}
void clear() {// ......}
void push_back(const T& x) {// ......}
// 不是万能引用
// 因为T是list的参数,list<Lotso::string>实例化时,T就确定了。
void push_back(T&& x) {// ......}
void push_front(const T& x) {// ......}
void pop_back() {// ......}
void pop_front() {// ......}
void insert(iterator pos, const T& x) {// ......}
void insert(iterator pos, T&& x) {// ......}
iterator erase(iterator pos) {// ......}
size_t size() const {// ......}
template<class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
// 原理:本质编译器根据可变参数模板生成对应参数的函数
/*void emplace_back(string& s)
{
insert(end(), std::forward<string>(s));
}
void emplace_back(string&& s)
{
insert(end(), std::forward<string>(s));
}
void emplace_back(const char* s)
{
insert(end(), std::forward<const char*>(s));
}
*/
template<class... Args>
iterator insert(iterator pos,Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head;
size_t _size = 0;
};
}
🎊到这儿,本期【C++11】初识------右值引用和移动语义,可变参数模板的内容就结束了。如果文中有表述不准的地方,或是你有更清晰的理解思路,强烈欢迎在评论区留言交流------ 技术路上多碰撞,才能更快进步
觉得内容对你有帮助的话,别忘了点赞❤️➕收藏🌟,方便后续回顾复习;想跟着一起系统学习数据结构的朋友,也可以点击关注,下一期我们会聚焦更进一步的学习。不见不散✌️



