【C++第十六课 - C++11】列表初始化、右值引用、移动构造、移动赋值、lambda表达式
目录
- 列表初始化
- 声明
- auto
- decltype
- typeid
- nullptr
- 范围for
- 智能指针(后面讲)
- STL的变化
- 新容器
- 新接口
- 右值引用和移动语义
- 左值引用
- 右值引用
- 注意
- 移动构造
- 移动赋值
- 引用折叠/万能引用
- 完美转发
- stl变化
- 移动构造函数
- 移动赋值函数
- 默认成员函数的强制生产
- 禁止生产
- 类变成最终类,不能被继承
- 强制派生类的虚函数进行重写
- 可变参数模板
- lambda表达式
- lambda表达式书写格式
- 捕捉列表
列表初始化
这个很难评
列表是{}
一切皆可用列表初始化
int a(10)
、int b = int(1)
:这个是模板那边支持的,构造和拷贝构造
(1)在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
(2)C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
int main()
{
//普通定义
int a = 0;
//C++11列表初始化
int b = { 0 };
int c{ 0 };
int arry1[]{ 1, 2, 3, 4, 5, 6 };
Date x{ 2021,4,9 };
vector<int> v = { 1, 2, 5, 6, 8, 9 };
//构造
Date x(2001, 2, 9);
//Date x = (1994, 6, 6); 这个不行这个如果可以的话是隐式类型转换,但对于类的隐式类型转换只允许单参数的
return 0;
}
C++11的列表初始化最佳用法
Date* darr1 = new Date[3]{ {2001, 3, 29}, { 2005, 5, 30}, {2024, 8, 8} }; Date* darr2 = new Date[2]{ x, x2 };
下面用法的实现是在vector里面新增了一个构造函数,这个数组先存到常量区里面,initializer_list<value_type>
里面存了指向这个数组的指针begin、end再进行拷贝
vector<int> v = { 1, 2, 5, 6, 8, 9 };
底层
map的initializer_list
为何
pair<const string, string>
可以用pair<const char*, char*>
来初始化
声明
auto
decltype
推导类型,可以推导某个变量的类型,再用这个类型定义新的变量
取类型的时候会去掉顶层的const
typeid也是下面的规则
修饰本身是顶层const
修饰指向的内容是底层const
顶层const
底层const
decltype实际用的地方
如上图,如果有人写了上述数据,然后想要用vector存储funca的数据
auto x = funca();
vector<decltype(x)> v;
typeid
推导类型,打印类型,推导出来的类型是一个字符串,不能用它推导出来的类型再进行定义
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
范围for
智能指针(后面讲)
STL的变化
新容器
增加不好的地方
forward_list:单链表,只支持++,插入只能在当前位置的后一个插入
array:静态数组,风险:栈溢出;优势:越界读写可以立即判断
新接口
1、cbegin、cend
2、initializer_list的构造(好用)
3、push_xxx/insert/emplace等增加右值引用插入版本意义重大、提高效率
4、容器增加移动构造和移动赋值,也可以减少拷贝,提高效率
右值引用和移动语义
左值和右值的区别:能否取地址
左值引用
左值:左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+一般情况可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。左值可以取地址,一般情况下可以修改
一般能取地址就是左值的最主要特征
左值引用底层是指针,用指针存当前左值的地址
const左值引用可以给右值取别名 –
引用的意义:本质是减少拷贝
为什么有了左值引用?还需要右值引用
左值引用
1、解决传参拷贝的问题
2、解决部分返回对象拷贝问题,(除了函数作用域,返回对象还在,可以左值引用返回,减少拷贝)
没有解决的问题,返回对象是局部对象,出了函数作用域生命周期就到了,只能传值返回,就存在拷贝,如果有些对象消耗巨大。
(1)C++11出来之前,编译器已做了一部分优化
拷贝构造+拷贝构造 -> 拷贝构造
临时对象存在那?比较小存在寄存器里,比较大就存在两个栈帧之间的地方
右值引用
之前学的是左值引用:给左值取别名
右值引用:给右值取别名
值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
右值引用底层是指针,把当前右值拷贝到栈上的一个临时空间,存储这个临时空间的地址
1、对于右值引用的划分
(1)纯右值:内置类型的右值,eg:10/a+b
(2)将亡值:自定义类型的右值,eg:匿名对象、传值返回函数/obj++/to_string(-123)
注意
左值引用不用给右值取别名,但const左值引用可以给右值取别名
右值引用不能给左值取别名,但是右值引用可以给move以后的左值取别名
左值引用解决的问题
(1)传参的拷贝全解决了
(2)传返回值的问题解决了一部分(局部对象即出了作用域销毁的对象,返回的拷贝问题,没有解决)
右值引用解决的问题
(1) 解决局部对象返回的拷贝问题
右值引用都是将亡值,对其拷贝的时候直接移动拷贝(直接转移你的资源)
临时对象
比较小:寄存器
比较大:存在两个栈帧之间
移动构造
移动构造是右值引用的运用
思考
1、是不是所有的类都写移动构造
浅拷贝的类不需要移动构造,eg:日期类
深拷贝的类才需要移动构造
c++98对于函数的返回值,由两次拷贝构造 --> 优化成了一次拷贝构造(换行写就无法达成优化条件,变成了:一次拷贝构造+一次赋值)
c++11对于函数的返回值,由一次拷贝构造形成临时对象(右值)+对临时对象的移动构造 --> 优化成一次移动构造
左值是不敢动的
如果是右值将亡值(内置类型不存在转移资源),可以之间交换资源
右值引用的移动语义
合二为一,省去中间生产的临时对象
隐式的强行对move(str)识别成右值
问题:想要优化成一次移动构造,但是返回值是左值
(1)对返回值强行move,但是这样的话以前代码要都加一个move吗?这样是不好的,没有向前包容
编译器隐式的将左值的返回值move(可以这么理解,但底层不是这么做的)
对于传值返回有两种情况
1、写成一行
2、写成两行
右值引用的用处
1、传值返回
2、容器的插入
不要轻易对左值move,除非已经确定可以对左值进行move
int&& r = 10;
r++;
右值被右值引用以后,右值引用r的属性是左值
右值引用的本质:把右值拷到栈上的一段临时空间,右值引用是这段临时空间的地址,有可以修改的权限(对于常量会拷贝一份,对于其他还是一个指针指向不回去拷贝)
右值不可用被修改
为什么这么设计?
右值不能改变,那返回值那怎么转移其资源
右值被右值引用后,右值引用的属性是左值,可以被改变,这样资源才能被转移
移动赋值
移动将亡值资源,并且把不要的空间给将亡值,让将亡值帮忙释放
引用折叠/万能引用
有推演的过程
传入& && --> &
传入&& &&–> &&
只有函数模板的T才是推演出来的
对于类模板来说就不可以,类在实例化的时候T就确定了
template<typename T>
void PerfectForward(T&& t)
{}
完美转发
move
是确定的把左值转成右值
forward
:完美转发,保持属性。本身是左值继续保持左值属性;如果本身是右值,转成右值,相当于move一下2
#include<iostream>
#include<utility>
void Func(int& t)
{
std::cout << "int& t 左值" << std::endl;
}
void Func(int&& t)
{
std::cout << "int&& t 右值" << std::endl;
}
void Func(const int& t)
{
std::cout << "const int& t const左值" << std::endl;
}
void Func(const int&& t)
{
std::cout << "const int&& t const右值" << std::endl;
}
template<typename T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a);
PerfectForward(1);
const int b = 20;
PerfectForward(b);
PerfectForward(std::move(b));
return 0;
}
右值经过右值引用属性就会变成左值,可以通过forward恢复
stl变化
c++11之后所有
的容器都增加了移动构造和移动赋值
移动构造函数
编译器默认生成:内置类型完成浅拷贝。自定义类型调用它的移动构造,若它没有实现移动构造就调用它的拷贝构造
移动构造实现条件
1、没有实现异构构造
2、且没有实现析构函数、拷贝函数、拷贝赋值重载中的任意一个
移动赋值函数
移动赋值和移动构造类似
默认成员函数的强制生产
default
:
禁止生产
例如有些类不期望被拷贝 ---- 单例模式
C++98:声明拷贝函数放到private
C++11:A(const A& aa) = delete
delete
:
类变成最终类,不能被继承
final
:
强制派生类的虚函数进行重写
override
:
可变参数模板
参数包是0-N个参数
实践中的使用
emplace_back
:一次给多个参数
std::list<pair<std::string, std::string>> lt2;
pair<std::string, std::string>kv1("xxxxx", "yyyyy");
lt2.push_back(kv1);
lt2.emplace_back(kv1);
lt2.emplace_back("xxxx", "yyyy");
push_back
:
类型是单个值,没啥区别
lambda表达式
底层是仿函数
仿函数 – 降序排序
#include <iostream>
#include <vector>
#include <algorithm>
class Good
{
public:
Good(std::string name, double price, int number)
:_name(name), _price(price), _number(number)
{}
~Good()
{}
std::string _name;
double _price;
int _number;
};
struct CompareGreater
{
bool operator()(const Good& g1, const Good& g2)
{
return g1._price > g2._price;
}
};
int main()
{
std::vector<Good> v = {{"苹果", 2.5, 40}, {"香蕉", 9.6, 10}, {"猕猴桃", 7, 55} };
sort(v.begin(), v.end(), CompareGreater());
return 0;
}
lambda表达式书写格式
[capture-list] (parameters) mutable -> return-type {statement}
- lambda表达式各部分说明
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。类似仿函数的成员变量(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
导。返回值任何情况都可以省略,可以自动推导{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
lambda是局部的匿名函数对象
auto add = [](int a, int b)->int {return a+b; };
cout << add(1,2) << endl;
auto add = [](int a, int b){return a+b; };
cout << add(1,2) << endl;
std::vector<Good> v = {{"苹果", 2.5, 40}, {"香蕉", 9.6, 10}, {"猕猴桃", 7, 55} };
sort(v.begin(), v.end(), [](const Good& g1, const Good& g2) {
return g1._price > g2._price;
});
捕捉列表
int x = 5, y = 1;
auto swap2[x, y]()mutable{
int tmp = x;
cin >> x;
y = tmp;
};
swap1();
注意
- lambda函数总是一个const函数,mutable可以取消其常量性,想要改变x和y的值必须添加mutable,而添加mutable必须写参数列表
- 上述x和y并没有改变,因为是拷贝的x和y,并没有对原来的x和y进行改变
int x = 5, y = 1;
auto swap2[&x, &y]{
int tmp = x;
cin >> x;
y = tmp;
};
swap1();
上述&是传引用,不是取地址
传值捕捉当前域的所有对象
int x = 5, y = 1, m = 2, n = 6;
auto swap2[=]{
return x+y*m-n;
};
传引用捕捉当前域的所有对象
int x = 5, y = 1, m = 2, n = 6;
auto swap2[&]{
return x+y*m-n;
};
混合捕捉当前域的所有对象
m是传值捕捉,其他传引用
int x = 5, y = 1, m = 2, n = 6;
auto swap2[&, m]{
return x+y*m-n;
};
两个lambda即使实现方法意义,但是类也不同