
今日语录:当你真正喜欢做一件事时,自律就会成为你的本能。 ------ 武志红
文章目录
⭐一、可变模板参数
1.语法及其原理
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包(表示零或多个模板参数);函数参数包(表示零或多个函数参数)
使用方式:
• template <class ...Args> void Func(Args... args) {}
• template <class ...Args> void Func(Args&... args) {}
• template <class ...Args> void Func(Args&&... args) {}
注:我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包
在模板参数列表 中,class...或typename...指出接下来的参数表示零或多个类型列表
在函数参数列表 中,类型名后面跟...指出接下来表表示零或多个形参对象列表
函数参数包可以使用左值引用或右值引用表示,每个参数实例化时遵循引用折叠的规则
原理:跟模板类似,本质还是去实例化出对应类型和个数的多个函数
2.sizeof对可变模板参数的使用
我们可以使用sizeof...运算符去计算包中参数的个数
例:
3.包扩展
对于一个参数包,我们除了计算包中的参数个数,最主要的就是扩展这个包,那们我们该如何扩展包拿到里面参数的内容呢 ?
我们采用两种方式:1.使用递归展开包的形式 2.使用逗号表达式
递归展开包的形式:
下面我们用一段代码来举例:
cpp
void Func()
{
//递归的终止条件
cout << endl;
}
template<class T,class...Args>
void Func(T x,Args&&...args)
{
cout << x << " ";
//args是N个参数的参数包
//递归调用Func,第一个传x,剩下的N-1个传给第二个参数包
Func(args...);
}
//编译时递归解析参数
template<class...Args>
void Print(Args...args)
{
Func(args...);
}
int main()
{
int x = 1;
Print();
Print(1);
Print(1, 2);
Print(1, string("111"), 3);
return 0;
}
结果显示:
逗号表达式展开包:
cpp
template<class T>
void Func(T& t)
{
cout << t << " ";
}
template<class...Args>
void Print(Args...args)
{
int arr[] = { {Func(args),0}... };
cout << endl;
}
int main()
{
int x = 1;
Print(1);
Print(1, 2);
Print(1, string("111"), 3);
return 0;
}
运行结果与上述相同
当我们给Func函数加一个返回值,我们就不需要使用逗号表达式了,如:
cpp
int Func(T& t)
{
cout << t << " ";
return 0;
}
template<class...Args>
void Print(Args...args)
{
int arr[] = { (Func(args)... };
cout << endl;
}
4.emplace系列接口
C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数 ,功能上
兼容push和insert系列,但emplace支持直接插入构造T对象的参数 ,也可以直接在容器空间上构造T对象,因此emplace在有些场景下会提高程序运行效率
下面我们以List为例,看看emplace接口的优势所在:
cpp
#include<list>
int main()
{
std::list<std::pair<int, string>> mylist;
//push_back固定了这个参数只能是pair
mylist.push_back(make_pair(1, "香蕉"));
//emplace_back传pair没问题
mylist.emplace_back(make_pair(1, "香蕉"));
//这里直接把构造pair参数包往下传,直接⽤pair参数包构造pair
// 这⾥达到的效果是push_back做不到的
mylist.emplace_back(1,"香蕉");
for (auto e : mylist)
{
cout << e.first << ":" << e.second << endl;
}
}
区别:emplace_back是直接传参数,不构造临时对象 ;而push_back需要构造一个临时对象,然后在进行移动构造 。在资源少的情况下,两者其实没啥区别,但在资源多的情况下,emplace_back能够大大提高运行效率,因此建议用emplace系列去替代insert和push系列
🎆二、新的类功能
1.默认的移动构造和拷贝构造
在原来C++类中,有6个默认成员函数,分别是:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载
在日常实践中,最常用且重要的是前4个成员函数。C++11中又新增了两个默认成员函数:移动构造函数和移动赋值运算符重载
那么编译器是如何自动生成移动构造函数和移动复制重载函数的呢,生成条件是什么?
当我们没有自己去实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成⼀个默认移动构造。
默认生成的移动构造函数 ,对于内置类型成员会执行逐成员按字节拷贝 ,自定义类型成员 ,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
当我们没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀个,那么编译器会自动生成⼀个默认移动赋值。其对内置类型和自定义类型与上述移动构造完全类似
2.default和delete
default概念:C++11中,为了让我们更好的控制要使用的默认函数 。假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认生成。例如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
例:
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
//用default生成默认的移动构造
Person(Person&& p) = default;
private:
string _name;
int _age;
};
delete概念:如果想要限制某些默认函数的生成 ,只需在该函数声明加上=delete即可,该语法表示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
例:
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person(const Person& p) = delete;
private:
string _name;
int _age;
};
🏖️三、STL中的一些变化
C++11出现了许多新容器,但在实际中最有用的就是unordered_map和unordered_set
其次就是出现了新的接口,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造和移动赋值,还有initializer_list版本的构造等,在有些场景下提高了程序运行的效率
🎄四、lambda
1.lambda表达式概念及其用法
概念:本质是⼀个匿名函数对象 ,跟普通函数不同的是它可以定义在函数内部
lambda表达式没有类型,因此我们一般采用auto或者模板参数定义的对象去接受lambda对象
使用形式:[capture-list] (parameters)-> return type { function boby }
其中:[capture-list]表示捕捉列表 ,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数 。注:1.捕捉列表可以传值和传引用捕捉 2.捕捉列表即使为空也不能省略
(parameters)表示参数列表 ,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同()⼀起省略
->return type表示返回值类型 ,一般由编译器自己推导,没有返回值时此部分可省略。
{function boby}表示函数体 ,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使用其参数外,还可以使用所有捕获到的变量。注:函数体为空也不能省略
下面就来写一个简单的lambda表达式:
总结一下:
1.捕捉为空也不能省略
2.参数为空可以省略
3.返回值可以省略,可以通过返回对象自动推导
4.函数体不能省略
2.捕捉列表
lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
在捕捉列表中,传值捕捉传过来的值不可以进行修改,传引用捕捉过来的值可以进行修改。
有三种捕捉方式 ,第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉 ,捕捉的多个变量用逗号分割。例:[x,y,&z],在这捕捉列表中,x和y表示值捕捉,z表示引用捕捉
第二种捕捉方式是在捕捉列表中隐式捕捉 ,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表
写⼀个&表示隐式引用捕捉。例:[=],表示值捕捉;[&]表示引用捕捉
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉 ,[=,&x]表示其他变量隐式值捕捉,
x引用捕捉;[&x,y]表示其他变量引用捕捉,x和y值捕捉。注:当使用混合捕捉时,第⼀个元素必须是&或= ,并且 &混合捕捉时 ,后面的捕捉变量必须是值捕捉 ,同理 =混合捕捉时 ,后面的捕捉变量必须是引用捕捉。
cpp
int main()
{
// 只能⽤当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引⽤捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
// 隐式值捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
// 隐式引⽤捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func4 = [&, a, b]
{
//a和b是传值捕捉,不能进行修改
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func5 = [=, &a, &b]
{
//c和d是传值捕捉,不能被修改
a++;
b++;
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
当lambda 表达式如果在函数局部域中 ,它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量 ,因为静态局部变量和全局变量也不需要捕捉,可以直接使用 。因此当lambda表达式定义在全局的位置时,捕捉列表必须为空

默认情况下, lambda 捕捉列表是被const修饰的 ,也就是说传值捕捉过来的对象不能修改 ,
但当我们将mutable加在参数列表的后面就可以取消其常量性 ,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的还是形参对象,不会影响实参 。注:使用该修饰符后,参数列表不可省略(即使参数为空)

3.使用场景
在学习lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,而函数指针的类型定义起来会比较麻烦,仿函数需要定义⼀个类。而如今我们使用lambda表达式就可以去定义可调用对象,简单又方便
例:当我们想要使用不同项进行比较时,我们通常需要使用仿函数来解决,如今我们就可以使用lambda表达式来解决
cpp
struct Goods
{
string _name;
double _p;
int _v;
Goods(string name,double p,int v)
:_name(name)
,_p(p)
,_v(v)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._p < g2._p;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._p > g2._p;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._v < g2._v;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._v > g2._v;
});
return 0;
}
4.底层原理
lambda底层其实是仿函数对象 ,也就说我们写了⼀个lambda 以后,编译器会自动生成⼀个对应的仿函数的类
🏠五、包装器
1.function
概念:function 是⼀个类模板,也是⼀个包装器。它实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind 表达式等
在使用function时,需要包含头文件 #include< functional >
例:
cpp
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct fun
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main()
{
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = fun();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f1(2, 2) << endl;
cout << f1(3, 3) << endl;
return 0;
}
有两个需要注意的是:在包装静态成员函数时,成员函数要指定类域。(注:建议在成员函数前加&)
例:
cpp
class Fun
{
public:
Fun(int n = 1)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
private:
int _n;
};
int main()
{
//&可加也可以不加,但建议加上
function<int(int, int)> f = &Fun::plusi;
return 0;
}
在包装普通成员函数时,由于普通成员函数还有⼀个隐含的this指针参数,因此绑定时传对象或者对象的指针过去都可以
例:
cpp
class Fun
{
public:
Fun(int n = 1)
:_n(n)
{}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
//传指针
function<double(Fun*,double, double)> f1 = &Fun::plusd;
Fun fd;
//传对象
function<double(&fd,double,double)> f2 = &Fun::plusd;
return 0;
}
学了function包装器,我们来看一个习题:
逆波兰表达式求值
在传统写法中,我们一般是使用if语句来判断是否是操作符,然后不断进行出栈和入栈操作。但这样写的缺点就是当我们还有其它的运算时,我们重写写一份代码,大大浪费时间。
而使用function包装器,我们只需要增加map中的映射即可
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>> Fun = {
{"+",[](int x,int y){return x + y;}},
{"-",[](int x,int y){return x - y;}},
{"*",[](int x,int y){return x * y;}},
{"/",[](int x,int y){return x / y;}}
};
for(auto& e :tokens)
{
//为操作符时
if(Fun.count(e))
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int ret = Fun[e](left,right);
st.push(ret);
}
//为操作数时入栈
else
{
st.push(stoi(e));
}
}
return st.top();
}
};
2.bind
概念:bind 是⼀个函数模板,它也是⼀个可调用对象的包装器,可以把它看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回⼀个可调用对象。
功能:可以用来调整参数个数和参数顺序
在使用bind时,同样也需要包含头文件 #include< functional >
使用形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是⼀个可调用对象 ,arg_list是⼀个以逗号分隔的参数列表,对应给定的callable的参数。在调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。如:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3...这些占位符放到placeholders的⼀个命名空间中 。
例:
调整参数顺序(不常用):
cpp
using placeholders::_1;
using placeholders::_2;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
//运行结果为:50
// bind 本质返回的⼀个仿函数对象
// _1代表第⼀个实参
// _2代表第⼆个实参
//运行结果为:-50
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
return 0;
}
调整参数个数(常用):
cpp
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int main()
{
auto sub1 = bind(Sub, 100, _1);
cout << sub3(2) << endl;
auto sub2 = bind(Sub, _1, 100);
cout << sub4(2) << endl;
return 0;
}
利用bind去计算复利的lambda
我们可以通过绑定一些参数,实现出支持不同年的利率,不同金额和不同年份计算出复利的结算利息
例:

今天的分享就到这里啦,感谢读者们的观看,我们下期再见!