一、统一的列表初始化
1.1 { }初始化概念
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加:
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
int a{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
创建对象时也可以使用列表初始化方式调用构造函数初始化
cpp
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); // old style
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
1.2 std::initializer_list
std::initializer_list的介绍文档:
https://cplusplus.com/reference/initializer_list/initializer_list/
std::initializer_list的类型:
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
STL容器使用初始化列表:
cpp
int main()
{
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// 这里{"sort", "排序"}会先初始化构造一个pair对象
v = { 10, 20, 30 };
return 0;
}
以 vector 为例,下面是 vector 关于初始化列表的构造与赋值重载:
模拟实现的vector也支持{}初始化和赋值
cpp
namespace Flash
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector(initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
}
vector<T>& operator=(initializer_list<T> l)
{
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
二、范围for循环
关于范围for循环,也就是迭代器的部分,类似于上方模拟实现vector的构造函数时使用的初始化列表,使用一个 for(auto e : l) 其实就代替了红框中循环部分的逻辑。
三、右值引用和移动语义
3.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值与左值引用?
除去定义,简单地说,可以取地址的就是左值。左值引用就是给左值的引用,给左值取别名。
cpp
int main()
{
// 以下的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;
return 0;
}
什么是右值与右值引用?
不能取地址的是右值,右值引用就是对右值的引用,给右值取别名。
cpp
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
return 0;
}
左值可以出现在赋值符的左边,右值不可以出现在赋值符的左边:
cpp
// 这里编译会报错:error C2106: "=": 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
左值引用总结
-
左值引用只能引用左值,不能引用右值。
-
但是const左值引用既可引用左值,也可引用右值
cpp
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以后的左值
cpp
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
int a = 10;
int&& r2 = a;//error C2440: "初始化": 无法从"int"转换为"int &&"message : 无法将左值绑定到右值引用
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
3.2 左值引用与右值引用的意义
3.2.1 左值引用的意义
首先,要意识到左值是传入变量的别名。
那么当要实现某个函数时,而函数内部是对该传入参数的修改并需要返回该参数,那么传入的参数的生命周期仍未停止,这样返回时返回的就是同一个变量,不需要再一次拷贝构造。
传入参数采用左值引用:
cpp
int func(int& x)
{
x += 10;
cout << "x的地址为:" << &x << endl;
return x;
}
int main()
{
int f = 10;
cout << "f的地址为:" << &f << endl;
func(f);
cout << "f = " << f << endl;
return 0;
}
传入参数不采用左值引用:
可以看到使用左值引用时,传入参数和返回值是同一地址,既可以减少传入参数的拷贝,也可以减少返回值时的拷贝。在数据量大的时候减少拷贝构造极为明显。
3.2.2 右值引用的意义
例1:左值引用的缺陷例举
但是,当返回值是局部变量时,这种优势就不存在了,出了作用域生命周期就到了,需要传值返回,这样就存在拷贝。
cpp
class Solution
{
public:
vector<vector<int>> generate(int numRows)
{
vector<vector<int>> vv;
vv.resize(numRows);
for (size_t i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 0);
vv[i].front() = vv[i].back() = 1;
}
for (size_t i = 0; i < vv.size(); ++i)
{
for (size_t j = 0; j < vv[i].size(); ++j)
{
if (vv[i][j] == 0)
{
vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
}
}
}
return vv;
}
};
看到上述代码是杨辉三角的解决代码,不需要关注其逻辑,但是它是一个二维数组,而且随每一个一维数组的元素个数随其元素下标增加而增加,大致如下图:
例2:将亡值与移动构造
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。这里别人的资源一般说的是生命周期即将结束的右值,即将亡值。
资料来源:比特科技。
3.3 完美转发
3.3.1 模板中的&& 万能引用
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
cpp
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)//t即可以接收左值也可以接收右值,但是最后都会变成左值
{
Fun(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;
}
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
3.3.2 std::forward 完美转发
cpp
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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::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;
}
3.4总结
C++11的更新,基本STL容器插入接口函数也增加了右值引用版本:
所以在使用库中的语法时无需特别注意,当自己需要创建某个类时,要根据实际情况区分需不需要写有关右值引用的函数重载。
四、可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现
阶段,我们掌握一些基础的可变参数模板特性就足够了,所以这里我们点到为止。
下面就是一个基本可变参数的函数模板:
cpp
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数
包",它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包 的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
4.1递归函数方式展开参数包
以下是是递归函数展开参数包的样例,也可以说是模板,每次设置两个传入参数并使用第一个参数逐个接收剩余参数包的第一个值,然后再把剩余参数包继续向下一个函数递归,直到参数包为空进入递归终止函数。
cpp
template <class T>
void ShowList(const T& t)
{s
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T&& value, Args&&... args)//写成右值引用可以当成万能引用,左右值都可以传
{
cout << value << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
4.2 逗号表达式展开参数包
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行
printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性------初始化列
表,通过初始化列表来初始化一个变长数组,{(printarg(args), 0)...}将会展开成
((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ) ,**最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。**由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
cpp
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
五、lambda 表达式
在网页浏览商品时,有很多排序的选项,如价格、评分、综合......
当选择不同的评分时,有着不同的评价体系,那么这种例子在程序中怎么实现呢?
写一个商品类,包含价格、评分、综合的相关信息,假设都为 int 型,然后使用仿函数。
cpp
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,
都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,
这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
5.1 lambda 表达式
cpp
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price > g2._price; });
使用lambda表达式,可以直接在main函数中使用 sort 函数完成比较。
5.2 lambda 语法
lambda:[捕捉列表](参数1, 参数2, ...)->返回值类型{函数体};
其中,捕获列表可以为空、参数列表桶()可一同省略、返回值类型可省略、"->" 可省略。
捕获列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用,因为涉及的参数过多,下面直接放图片。
例:[](const Goods& g1, const Goods& g2){ return g1._price < g2._price; }
六、包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
cpp
// 使用方法如下:
#include <functional>//必包的头文件
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lambda表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}