欢迎光临
我们一直在努力

【C++进阶】C++11:列表初始化、右值引用与移动语义、完美转发全解析

在这里插入图片描述

🎬 博主名称:键盘敲碎了雾霭

🔥 个人专栏: 《C语言》《数据结构》 《C++》
《Matlab》
《Python》

⛺️指尖敲代码,雾霭皆可破


在这里插入图片描述

文章目录

  • 一、C++历史
  • 二、列表初始化{}
    • 2.1 C++98的{}
    • 2.2 C++11
    • 2.3 initializer_list
  • 三、右值引用与移动语义
    • 3.1 左值与右值
    • 3.2 左值引用和右值引用
    • 3.3 引用延长生命周期
    • 3.4 左值和右值的参数匹配
  • 四、右值引用和移动语义的使用场景
    • 4.1 左值引用使用场景
    • 4.2 移动构造和移动赋值
    • 4.3 右值引用和移动语义解决传值返回问题
      • 4.3.1 只有拷贝构造,没有移动构造
      • 4.3.2 有拷贝构造,也有移动构造
      • 4.3.3 只有拷贝构造和拷贝赋值,没有移动构造和移动赋值
      • 4.3.4 既有拷贝构造和拷贝赋值,也有移动构造和移动赋值
    • 4.4 左值引用不足
  • 五、类型分类
  • 六、引用折叠
  • 七、完美转发
  • 文章结语

一、C++历史

在这里插入图片描述 C++11是C++的第二个主要版本,并且是从C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由1SO 在 2011年 &月12 日采纳前,人们曾使用名称“C++0x”,因为它曾被期待在2010年之前发布。C++03与C++11期间花了&年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新一次。 参考文档: https://legacy.cplusplus.com/ https://en.cppreference.com/

二、列表初始化{}

2.1 C++98的{}

在C++98,{}用来初始化数组和结构体

struct Point
{
int _x;
int _y;
};
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
Point p = { 1,2 };
return 0;
}

支持单参数类型转换

class Date
{
public:
Date(int year = 2026, int month = 6, int day = 14)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day )";
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Date d1 = 2026;
return 0;
}

2.2 C++11

内置类型可以用{}初始化,支持多参数初始化

class Date
{
public:
Date(int year = 2026, int month = 6, int day = 14)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day )";
}
private:
int _year;
int _month;
int _day;
};

int main()
{
int x = { 2 };
Date d1 = { 2026,3,5 };//构造加拷贝构造
const Date& d2 = { 2026 };//构造
return 0;
}

用{}可以省略=

struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year = 2026, int month = 6, int day = 14)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day )";
}
private:
int _year;
int _month;
int _day;
};

int main()
{
Point p{ 1,2 };
int t{ 3 };
Date d3{ 2026,4,7 };
const Date& d4{ 2026,7,8 };
}

C++11试图实现一切对象皆可用初始化

int main()
{
Date d1 = { 2026,3,5 };
vector<Date> v1;
v1.push_back(d1);
v1.push_back({ 2027,3,4 });
map<string, string> m1;
m1.insert({ "aaa","bbb" });
}

2.3 initializer_list

对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持 C++11库中提出了一个std::initializer_list的类 这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束在这里插入图片描述 参考文档:https://legacy.cplusplus.com/reference/initializer_list/initializer_list/?kw=initializer_list

以vector为例 在这里插入图片描述

int main()
{
vector<int> v = { 1,2,3 };
vector<int> v1{ 1,2,3 };
vector<int> v2({ 1,2,3 });
//这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
map<string, string> m1 = { {"aaa","bbb"},{"ccc","ddd"} };
}

三、右值引用与移动语义

3.1 左值与右值

核心区别:是否能取地址

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
  • 右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址(临时变量,字面量常量,存储于寄存器中的变量)

int main()
{
//左值
int x = 10;
int y = 10;
int* p = new int(0);
int b = 1;
const int c = 10;
*p = 10;
string s1("aaaa");
s1[0] = 'x';
cout << (void*) & s1[0] << endl;

//右值
10;
x + y;
fuc(x);//传值返回,构造了临时对象
string("aaaa");
}

3.2 左值引用和右值引用

Type&rl= x;Type&&rr1= y 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。 注意:

  • 左值引用不能直接引用右值,但是const左值引用可以引用右值
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)

//左值引用
int*& ret = p;
int& ret1 = b;
int& ret2= b;
string& ret3 = s1;
char& ret4 = s1[0];

//右值引用
int&& tmp = 10;
int&& tmp1 = x+y;
int&& tmp2 = fuc(x,y);
string&& tmp3 = string("aaaa");

//交叉使用
int&& tmp4 = move(b);
const int& tmp5= 10;

  • 一般为了匹配右值引用,形参用const T&修饰,左值被move,move是库里面的一个函数模板,本质内部是进行强制类型转换,相当于产生了临时对象,不会影响原来的属性
  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值

int main()
{
int&& r = 10;
int& r1 = r;
//int&& r2 = r;
}

3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,Const的左值引用也能延长临时对象生存期,但这些对象无法被修改。 通过下面两个案例可以看到在不同的位置析构,说明延长了临时对象的生命周期

KeyBreak::string("111111");
cout << "xxxxxxxxx"<<endl;

在这里插入图片描述

const KeyBreak::string& s1=KeyBreak::string("111111");
cout << "xxxxxxxxx"<<endl;

在这里插入图片描述

3.4 左值和右值的参数匹配

  • C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配
  • C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配(左值引用),实参是const左值会匹配(const左值引用),实参是右值会匹配(右值引用)。 注意:右值引用变量在用于表达式时属性是左值

void Fuc(int &x)
{
cout << "左值引用" << endl;
}
void Fuc(const int &x)
{
cout << "const 左值引用" << endl;
}
void Fuc(int &&x)
{
cout << "右值引用" << endl;
}
int main()
{
int x = 10;
const int y = 10;
Fuc(x);
Fuc(y);
Fuc(3);
Fuc(move(x));
int&& ret = 100;
Fuc(ret);
Fuc(move(ret));
}

四、右值引用和移动语义的使用场景

4.1 左值引用使用场景

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回(比如下面这道题)

415.字符串相加 https://leetcode.cn/problems/add-strings/submissions/730885753/

class Solution
{
public:
string addStrings(string num1, string num2)
{
int end1=num1.size()1;
int end2=num2.size()1;
int next=0;
string s1;
while(end1>=0||end2>=0)
{
int x=end1>=0?num1[end1]'0':0;
int y=end2>=0?num2[end2]'0':0;
int ret=x+y+next;
s1+=(ret%10+'0');
next=ret/10;
}
if(next)
s1+='1';
reverse(s1.begin(),s1.end());
return s1;
}
};

C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为addStrings这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。

4.2 移动构造和移动赋值

  • 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像string/Vvector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率

namespace KeyBreak
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str+_size;
}

string(const char* str="")
:_size(strlen(str))
,_capacity(_size)
{
cout << "构造" << endl;
_str = new char[_capacity+1];
strcpy(_str, str);
}
void swap(string&s)
{
std::swap(_str,s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
{
cout << "拷贝构造" << endl;
reserve(s._capacity);
for (auto e : s)
{
push_back(e);
}
}
string(string&& s)
{
cout << "移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "拷贝赋值" << endl;
if (&s != this)
{
_str[_size] = '\\0';
_size = 0;
reserve(s._capacity);
for (auto e : s)
{
push_back(e);
}
}
return *this;
}
string& operator=(string&& s)
{
cout << "移动赋值" << endl;
swap(s);
return *this;
}
void reserve(int n)
{
if (n >= _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
~string()
{
delete[] _str;
_str = nullptr;
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _size == 0 ? 4 : 2 * _capacity;
reserve(newcapacity+1);
}
_str[_size] = ch;
_size++;
_str[_size] = '\\0';
}
char& operator[](size_t pos)
{
return _str[pos];
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
size_t size()const
{
return _size;
}
const char* c_str()const
{
return _str;
}
private:
char* _str=nullptr;
size_t _size=0;
size_t _capacity = 0;
};
}

  • 测试代码

int main()
{
KeyBreak::string s1 = "11111111111";
KeyBreak::string s2 = s1;
KeyBreak::string s3 = KeyBreak::string("22222");//被优化了,构造+引动构造被优化了
KeyBreak::string s4 = move(s1);
return 0;
}

结果 在这里插入图片描述 可以看出移动构造和移动赋值是转移资源 在这里插入图片描述

4.3 右值引用和移动语义解决传值返回问题

4.3.1 只有拷贝构造,没有移动构造

在vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。如图所示。在这里插入图片描述 注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。变为直接构造,如图所示。 在这里插入图片描述

4.3.2 有拷贝构造,也有移动构造

在vs2019debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。 在这里插入图片描述 vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图所示 在这里插入图片描述

4.3.3 只有拷贝构造和拷贝赋值,没有移动构造和移动赋值

一次拷贝构造,一次拷贝赋值,在vs2019,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现 在这里插入图片描述

4.3.4 既有拷贝构造和拷贝赋值,也有移动构造和移动赋值

一次移动构造,一次移动赋值,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名 在这里插入图片描述

4.4 左值引用不足

左值引用可以解决以下问题:

  • 左值引用和右值引用最终目的减少拷贝、提高效率
  • 左值引用还可以修改参数/返回值,方便使用。

不足: 部分函数返回场景,只能传值返回,不能左值引用返回。(当前函数局部对象,出了当前函数作用域生命周期到了,就销毁了,不能用左值引用返回,只能传值返回),如果编译器没优化,中间有两次拷贝,浪费资源 解决方法:

  • 输出型参数,先在外面创建一个对象,在函数内接收
  • 编译器的优化
  • 新标准新语法处理(右值引用和移动意义):拷贝赋值优化,从此编译器的优化成了锦上添花,不再是雪中送碳

总结: 深拷贝的自定义类型:如vector/string/map…实现移动构造和移动赋值是有很大的价值的 浅拷贝的自定义类型:如Date/pair<int,int>…不需要实现移动构造和移动赋值是有很大的价值的,但是有编译器的优化,合三为一,也减少了资源

五、类型分类

  • C++l1以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
  • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。比如:10、nullptr、str1+str2、a++等.
  • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,比如move(x)、static_cast<X&&>(x)
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
  • 文档:https://zh.cppreference.com/cpp/language/value_category

在这里插入图片描述

六、引用折叠

  • C++中不能直接定义引用的引用如int&&&r=i;.这样写会直接报错,通过模板或typedef中的类型操作可以构成引用的引用。

template<class T>
void fuc(T& x)
{

}
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& ret = n;
rref& ret1 = n;
lref&& ret2 = n;
rref&& ret3 = 0;
fuc<int>(n);
//fuc<int>(0);//报错,void f1(int&x)
fuc<int&>(n);
//fuc<int&>(0);//报错,void f1(int&x)
fuc<int&&>(n);
//fuc<int&&>(0);//报错,void f1(int&x)
fuc<const int&>(n);
fuc<const int&>(0);

}

  • 通过模板或typedef中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。

template<class T>
void fuc(T&& x)
{

}

int main()
{
int n = 0;
fuc<int>(0);
//fuc<int&>(0);//报错
fuc<int&>(n);
fuc<int&&>(0);
fuc<const int&>(n);
fuc<const int&>(0);
return 0;
}

  • T&&×参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
  • Fuc(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Fuc,实参是右值,实例化出右值引用版本形参的Fuc。

template<class T>
void Fuc(T&& x)
{
int a = 0;
T x = a;
cout << &a << endl;
cout << &x << endl;
}
int main()
{
Fuc(10);//T:int,模板实例化为voidFunction(int&&t)
int a = 0;
Fuc(a);//T:int&,模板实例化为voidFunction(int&t)
Fuc(move(a));//T:int,模板实例化为voidFunction(int&& t)
const int b = 0;
Fuc(b);//T:const int &,模板实例化为voidFunction(const int&t)
Fuc(move(b));//T:const int ,模板实例化为voidFunction(const int&&t)
}

七、完美转发

  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现 在这里插入图片描述
  • 完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用返回。

有了完美转发和万能引用,就可以改造自己实现的链表,链表章节有疑问的看这篇 【C++初阶】双向循环链表:List底层结构的完整实现剖析

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace A
{
template<class T>
struct ListNode
{
ListNode() = default;
template<class X>
ListNode(X&& x )
:_data(forward<X>(x))
, _next(nullptr)
, _pre(nullptr)
{

}

T _data;
ListNode* _next;
ListNode* _pre;
};

template<class T, class Ref, class Def>
struct list_iterator
{
typedef ListNode<T> Node;
typedef list_iterator iterator;
list_iterator(Node* p)
:_node(p)
{

}
list_iterator(const list_iterator& tmp)
{
_node = tmp._node;
}
Ref operator*()
{
return _node->_data;
}
Def operator->()
{
return &(_node->_data);
}
iterator& operator++()
{
_node = _node->_next;
return *this;
}
iterator operator()
{
_node = _node->_pre;
return *this;
}
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
iterator operator(int)
{
iterator tmp(*this);
_node = _node->_pre;
return tmp;
}
bool operator!=(const iterator& it)const
{
return it._node != _node;
}
bool operator==(const iterator& it)const
{
return it._node == _node;
}
Node* _node;
};

/*template<class T>
struct list_const_iterator
{
typedef ListNode<T> Node;
typedef list_const_iterator iterator;
list_const_iterator( const Node*p)
:_node(p)
{

}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
iterator& operator++()
{
_node = _node->_next;
return *this;
}
iterator operator–()
{
_node = _node->_pre;
return *this;
}
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
iterator operator–(int)
{
iterator tmp(*this);
_node = _node->_pre;
return tmp;
}
bool operator!=(const iterator& it)const
{
return it._node!=_node;
}
bool operator==(const iterator& it)const
{
return it._node==_node;
}
const Node* _node;
};*/

template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;

list()
{
_head = new Node;
_head->_next = _head;
_head->_pre = _head;
_size = 0;
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_pre = _head;
_size = 0;
}
list(const list& lt)
{
empty_init();
const_iterator it = lt.begin();

while (it != lt.end())
{
push_back(*it);
it++;
}
}
void Swap(list& tmp)
{
std::swap(_head, tmp._size);
std::swap(_head, tmp._head);
}
list& operator=(list tmp)
{
Swap(tmp);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
it++;
}
}
template<class X>
void push_back(X&& x)
{
//Node*newnode = new Node(x);
//Node* tail = _head->_pre;
//tail->_next = newnode;
//_head->_pre = newnode;
//newnode->_pre = tail;
//newnode->_next = _head;
//_size++;
insert(end(), forward<X>(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
template<class X>
void insert(iterator pos, X&& x)
{
Node* cur = pos._node;
Node* pre = cur->_pre;
Node* newnode = new Node(forward<X>(x));
cur->_pre = newnode;
pre->_next = newnode;
newnode->_next = cur;
newnode->_pre = pre;
_size++;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* pre = pos._node->_pre;
Node* next = pos._node->_next;
pre->_next = next;
next->_pre = pre;
delete pos._node;
return next;
}
void pop_back()
{
erase(end());
}
void pop_front()
{
erase(begin());
}
iterator begin()
{
iterator it(_head->_next);
return it;
}
iterator end()
{
iterator it(_head);
return it;
}
const_iterator begin()const
{
const_iterator it(_head->_next);
return it;
}
const_iterator end()const
{
const_iterator it(_head);
return it;
}
size_t size()const
{
return _size;
}
bool empty()const
{
return _size == 0;
}
list(initializer_list<T> li)
{
empty_init();
for (auto& e : li)
{
push_back(e);
}
}
private:
Node* _head;
size_t _size;
};
}

结合前面的知识,进一步体会在实践的应用,链表所更改的地方如下: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

  • 测试代码

int main()
{
A::list<KeyBreak::string> T;
KeyBreak::string s("111111111");
T.push_back(s);
T.push_back("1111111");
T.push_back(move(s));
}

结果如下: 在这里插入图片描述


文章结语

感谢你读到这里~我是「键盘敲碎了雾霭」,愿这篇文字帮你敲开了技术里的小迷雾 💻

如果内容对你有一点点帮助,不妨给个暖心三连吧👇 👍 点赞 | ❤️ 收藏 | ⭐ 关注 (听说三连的小伙伴,代码一次编译过,bug绕着走~)

你的支持,就是我继续敲碎技术雾霭的最大动力 🚀

🐶 小彩蛋:

/^ ^\\
/ 0 0 \\
V\\ Y /V
/ – \\
/ |
V__) ||

摸一摸毛茸茸的小狗,赶走所有疲惫和bug~我们下篇见 ✨

赞(0)
未经允许不得转载:171主机测评 » 【C++进阶】C++11:列表初始化、右值引用与移动语义、完美转发全解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址