网站按钮确定后图片怎么做,北欧做的比较好的网站,沈阳沙盘模型公司,购买wordpress现有模板文章目录 一、C发展简介二、C11简介三、列表初始化1.统一使用{}初始化2.initializer_list类 四、变量的类型推导1.auto2.decltype3.nullptr 五、范围for循环六、STL中一些变化七、final与override八、新的类功能1.新增默认成员函数2.成员变量的缺省值3.default 和 delete4.fina… 文章目录 一、C发展简介二、C11简介三、列表初始化1.统一使用{}初始化2.initializer_list类 四、变量的类型推导1.auto2.decltype3.nullptr 五、范围for循环六、STL中一些变化七、final与override八、新的类功能1.新增默认成员函数2.成员变量的缺省值3.default 和 delete4.final 和 override 九、可变参数模板十、 lambda表达式1.lambda表达式语法2.lambda表达式的捕捉列表3.lambda表达式与函数对象 十一、包装器1.function包装器2.bind 一、C发展简介
1982年Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念发明了一种新的程序语言。为了表达该语言与C语言的渊源关系命名为C。因此C是基于C语言而产生的它既可以进行C语言的过程化程序设计又可以进行以抽象数据类型为特点的基于对象的程序设计还可以进行面向对象的程序
语言的发展就像是练功打怪升级一样也是逐步递进由浅入深的过程。我们先来看下C的历史版本
阶段内容C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等C1.0添加虚函数概念函数和运算符重载引用、常量等C2.0更加完善支持面向对象新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数C3.0进一步完善引入模板解决多重继承产生的二义性问题和相应构造和析构的处理C98C标准第一个版本绝大多数编译器都支持得到了国际标准化组织(ISO)和美国标准化协会认可以模板方式重写C标准库引入了STL(标准模板库)C03C标准第二个版本语言特性无大改变主要修订错误、减少多异性C05C标准委员会发布了一份计数报告(Technical ReportTR1)正式更名C0x即计划在本世纪第一个10年的某个时间发布C11增加了许多特性使得C更像一种新语言比如正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等C14对C11的扩展主要是修复C11中漏洞以及改进比如泛型的lambda表达式auto的返回值类型推导二进制字面常量等C17在C11上做了一些小幅改进增加了19个新特性比如static_assert()的文本信息可选Fold表达式用于可变的模板if和switch语句中的初始化器等C20**自C11以来最大的发行版引入了许多新的特性比如模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)**等重大特性还有对已有特性的更新比如Lambda支持模板、范围for支持初始化等C23 制定ing
C还在不断的向后发展。但是现在公司主流使用还是C98和C11所有大家不用追求最新重点将C98和C11掌握好等工作后随着对C理解不断加深有时间可以去琢磨下更新的特性。
二、C11简介
在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞进行修复语言的核心部分则没有改动因此人们习惯性的把两个标准合并称为C98/03标准。
从C0x到C11C标准10年磨一剑第二个真正意义上的标准珊珊来迟。相比于C98/03C11则带来了数量可观的变化其中包含了约140个新特性以及对C03标准中约600个缺陷的修正这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言C11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全不仅功能更强大而且能提升程序员的开发效率公司实际项目开发中也用得比较多所以我们要作为一个重点去学习。C11增加的语法特性非常篇幅非常多我们这里没办法一 一讲解所以本节课程
主要讲解实际中比较实用的语法。
这里有个关于C发展历史的小故事 1998年是C标准委员会成立的第一年本来计划以后每5年视实际需要更新一次标准C国际标准委员会在研究C 03的下一个版本的时候一开始计划是2007年发布所以最初这个标准叫C 07。但是到06年的时候官方觉得2007年肯定完不成C 07而且官方觉得2008年可能也完不成。最后干脆叫C 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成最后在2011年终于完成了C标准。所以最终定名为C11。 完整的C11更新的语法我们可以查阅C11官方文档
C11
三、列表初始化
1.统一使用{}初始化
在C98中标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如
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;
}C11扩大了用大括号括起的列表(初始化列表)的使用范围(这里的初始化列表和构造函数中的初始化列表不同)使其可用于所有的内置类型和用户自定义的类型使用初始化列表时可添加等号()也可不添加。
struct Point
{int _x;int _y;
};
int main()
{// 内置类型也可以使用初始化列表进行初始化int x1 1;int x2{ 2 };// 自定义类型int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p{ 1, 2 };// C11中列表初始化也可以适用于new表达式中int* pa new int[4]{ 0 };return 0;
}创建对象时也可以使用列表初始化方式调用构造函数初始化
当初始化列表中的元素类型和元素个数符合构造函数参数的要求的时候初始化列表可以转化为调用构造函数来完成初始化
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(2023, 1, 1); // old style// C11支持的列表初始化这里会调用构造函数初始化Date d2{ 2023, 1, 2 };Date d3 { 2023, 1, 3 };return 0;
}
2.initializer_list类
initializer_list是C11中新增的一个类其文档介绍如下initializer_list - C Refernece (clpusplus.com) 它可以将同一类型的元素的集合即将相同元素构成的一个列表转化成一个initializer_list的对象需要注意的是initializer_list实际上是对常量区的封装–将列表中的数据识别为常量区的数据然后用类似于迭代器的begin()和end()指针指向并访问这些元素其自身不会开辟空间所以initializer_list中的数据也不能被修改。
我们可以查看initializer_list的类型代码如下
int main()
{// the type of il is an initializer_list auto il { 10, 20, 30 };cout typeid(il).name() endl;return 0;
}有了initializer_list类之后我们就可以让STL中的其他容器重载一个参数为initializer_list类型的构造函数和赋值重载函数那么我们就可以使得这些容器可以使用列表来进行初始化和赋值 #include vector
#include list
#include map
int main()
{// 列表初始化vectorint v { 1,2,3,4 };listint lt { 1,2 };// 这里{sort, 排序}会先初始化构造一个pair对象mapstring, string dict { {sort, 排序}, {insert, 插入} };// 使用大括号对容器赋值v { 10, 20, 30 };return 0;
}我们需要注意的是当列表中的元素类型和元素的个数符号构造函数的参数要求的时候即有对应的构造函数那么编译器会直接调用构造函数来完成初始化当列表中的元素不符合构造函数的参数的要求的时候即没有对应的构造函数此时会先将列表转化为initializer_list类的对象然后再调用initializer_list的构造函数来完成初始化
总结在C11之后一切的初始化都可以通过{}来完成初始化的时候可以省略赋值符号(STL中的所有容器都重载了参数类型为initializer_list的构造函数和赋值重载函数但是不包括容器适配器因为容器适配器本身不是一个容器而是由其他容器进行封装而来的)
四、变量的类型推导
1.auto
在C98中auto是一个存储类型的说明符表明变量是局部自动存储类型但是局部域中定义局部的变量默认就是自动存储类型所以auto就没什么价值了。C11中废弃auto原来的用法将其用于实现自动类型推导。这样要求必须进行显示初始化让编译器将定义对象的类型设置为初始化值的类型
int main()
{int i 10;auto p i;auto pf strcpy;cout typeid(p).name() endl;cout typeid(pf).name() endl;mapstring, string dict { {sort, 排序}, {insert, 插入} };//mapstring, string::iterator it dict.begin();auto it dict.begin();return 0;
}2.decltype
关键字decltype将变量的类型声明为表达式指定的类型。
// decltype的一些使用使用场景
templateclass T1, class T2
void F(T1 t1, T2 t2)
{decltype(t1 * t2) ret;cout typeid(ret).name() endl;
}
int main()
{const int x 1;double y 2.2;decltype(x * y) ret; // ret的类型是doubledecltype(x) p; // p的类型是int*cout typeid(ret).name() endl;cout typeid(p).name() endl;F(1, a);return 0;
}3.nullptr
由于C中NULL被定义成字面量0这样就可能回带来一些问题因为0既能指针常量又能表示整形常量。所以出于清晰和安全的角度考虑C11中新增了nullptr用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif五、范围for循环
范围for是C11提供的一个语法糖它配合auto可以让我们很方便的堆容器进行遍历它的 底层是通过替换成迭代器来实现的所以只要支持迭代器那么就一定支持范围for
int main()
{// 使用列表初始化vectorint v { 1,2,3,4,5, };// 使用范围for进行遍历for (auto e : v){cout e ;}cout endl;// 使用迭代器进行遍历 -- 二者等价//std::vectorint::iterator it v.begin();auto it v.begin();while (it ! v.end()){cout *it ;it;}cout endl;// 容器适配器stackstackint st;// 容器适配器不能使用列表来进行初始化也没有迭代器st.push(1);st.push(2);st.push(3);st.push(4);st.push(5);所以容器适配器也不能使用范围for来进行遍历//for (auto e : st)//{// cout e ;//}//cout endl;return 0;
}我们可以看到范围for最终还是被替换为迭代器 六、STL中一些变化
新容器
用橘色圈起来是C11中的一些几个新容器但是实际最有用的是unordered_map和unordered_set。其他的大家了解一下即可 新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C11的方法其主要分为如下几个方面
所有了支持const迭代器的容器都提供了cbegin和cend方法返回const迭代器–没有多大用 所有容器的插入接口都提供了emplace版本包括容器适配器–empalce主要是可变参数模板和右值引用 所以容器的构造函数都重载了移动构造和参数为initializer_list的构造(容器适配器重载了移动构造但没有重载initailizer_list的构造) 所有容器的赋值重载函数都重载了移动赋值和参数为initailizer_list的赋值不包括容器适配器 七、final与override
C11中新增两个关键字–final和override其中final可以用来修饰类函数和变量
final修饰类表示该类不能被继承
class Person final
{
public:Person(string name):_name(name){}
protected:string _name;
};class Student : public Person
{
public:Student(string name,int id):Person(name),_id(id){}
protected:int _id;
};final修饰虚函数表示该虚函数不能被重写
class Person
{
public:Person(string name):_name(name){}virtual void show () final{cout _name endl;}
protected:string _name;
};class Student : public Person
{
public:Student(string name,int id):Person(name),_id(id){}virtual void show(){cout _name : _id endl;}
protected:int _id;
};
int main()
{return 0;
}final 修饰变量表示该变量不能被修改
over只能用来修饰子类中用于重写父类虚函数的函数它的作用是检查子类是否完成了对父类虚函数的重写
八、新的类功能
1.新增默认成员函数
原来C类中有6个默认成员函数
1.构造函数
2.析构函数
3.拷贝构造函数
4.拷贝赋值重载
5.取地址重载
6.const 取地址重载
最后重要的是前4个后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C11 新增了两个移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下 如果你没有自己实现移动构造函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员则需要看这个成员是否实现移动构造如果实现了就调用移动构造没有实现就调用拷贝构造。 如果你没有自己实现移动赋值重载函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员则需要看这个成员是否实现移动赋值如果实现了就调用移动赋值没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似) 如果你提供了移动构造或者移动赋值编译器不会自动提供拷贝构造和拷贝赋值。
总结如果我们在一个类中什么也没有实现或者只有一个构造函数那么编译器会自动生成移动构造和移动赋值自动生成的对于内置类型完成值拷贝对于自定义类型时取决于自定义类型是否实现了移动构造和移动赋值实现了就调用自定义类型的移动构造和移动赋值没有实现就调用自定义类型的拷贝构造和赋值重载
2.成员变量的缺省值
C98的构造函数默认对的初始化列表对内置类型不处理所以C11允许在类定义时给成员变量赋初始值这些缺省值会在初始化列表用来初始化成员变量 3.default 和 delete
强制生成默认函数的关键字default
C11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数但是因为一些原因这个函数没有默认生成。比如我们提供了拷贝构造就不会生成移动构造了那么我们可以使用default关键字显示指定移动构造生成。
class Person
{
public:Person(const char* name lihua, int age 0):_name(name), _age(age){}//显式声明了拷贝构造Person(const Person p):_name(p._name),_age(p._age){}// 我们使用default关键字让编译器默认何时能成移动构造Person(Person p) default;
private:hdp::string _name;int _age;
};int main()
{Person p1;Person p2 p1;Person p3 std::move(p1);return 0;
}禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成在C98中是该函数设置成private并且只声明不实现这样只要其他人想要调用就会报错。
class A
{
public:A(){_p new int[10]{0};}~A(){delete[] _p;}
private:// 将拷贝构造定义为私有防止在类外进行拷贝A(const A a):_p(a._p){}int* _p;
};但是这个方法只能防止在类外进行拷贝而在类中我们仍然可以调用拷贝构造函数来完成拷贝此时编译器在编译时不会发生错误只有运行起来对同一块空间析构两次来会报错 我们如何让一个类既不能在外部被拷贝也不能在内部被拷贝呢我们只给出拷贝构造函数的声明且声明为私有这时只要调用了拷贝构造函数那么在链接时就一定会发生错误
以上是C98中防止一个类被拷贝的做法在C11中更简单只需在该函数声明加上delete即可该语法指示编译器不生成对应函数的默认版本称delete修饰的函数为删除函数。
class A
{
public:A(){_p new int[10]{ 0 };}~A(){delete[] _p;}A(const A a) delete;
private:int* _p;
};default关键字都只能对默认成员函数使用而delete关键字既可以对默认成员函数使用也可以对非默认成员函数和普通函数使用
4.final 和 override
继承和多态中的final与override关键字
这个我们在多态的博客中已经进行了详细讲解这里就不再细讲有兴趣的伙伴可以看我多态的博客 [C]多态
九、可变参数模板
在C语言中我们可以使用…来表示可变参数比如我们熟悉的printf和scanf函数 在C也同样使用这种方式。C11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板相比C98/03类模版和函数模版中只能含固定数量的模版参数可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象使用起来需要一定的技巧所以这块还是比较晦涩的。现阶段呢我们掌握一些基础的可变参数模板特性就够我们用了所以这里我们点到为止以后大家如果有需要再可以深入学习。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包args是一个函数形参参数包
// 声明一个参数包Args...args这个参数包中可以包含0到任意个模板参数。
template class ...Args
void ShowList(Args... args)
{}上面的参数args前面有省略号所以它就是一个可变模版参数我们把带省略号的参数称为“参数包”它里面包含了0到NN0个模版参数。我们无法直接获取参数包args中的每个参数的只能通过展开参数包的方式来获取参数包中的每个参数这是使用可变模版参数的一个主要特点也是最大的难点即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数所以我们的用一些奇招来一一获取参数包的值
我们先求出参数包中参数的个数在可变参数的函数模板中我们可以使用sizeof…(args)来求参数包中参数的个数 然后我们再取出参数包中的每个参数
递归函数方式展开参数包
我们将包中的第一个参数赋值给value将剩下的n-1个参数以类似于递归子问题的方式逐个取出当参数包为空时再调用最后一次自此将参数包中的参数全部取出
// 递归终止函数
template class T
void ShowList(const T t)
{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, string(sort));return 0;
}逗号表达式展开参数包
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, string(sort));return 0;
}这种展开参数包的方式不需要通过递归终止函数是直接在expand函数体中展开的, printarg不是一个递归终止函数只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式(printarg(args), 0)也是按照这个执行顺序先执行printarg(args)再得到逗号表达式的结果0。同时还用到了C11的另外一个特性——初始化列表通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… )最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数也就是说在构造int数组的过程中就将参数包展开了这个数组的目的纯粹是为了在数组构造的过程展开参数包
STL emplace相关接口函数
C11为所有容器的插入接口都新增了一个emplace版本如下 我们可以看到emplace系列的接口支持模板的可变参数和万能引用那么相较于传统的插入解决来说emplace版本的接口的优势在哪呢我们分为两种不同的情况来进行讨论
1.对于内置类型来说emplace接口和传统的插入接口在效率上来说是没有区别的因为内置类型是直接进行插入的不需要进行深拷贝不需要调用深层次的拷贝构造
2.对于需要进行深拷贝的自定义类型来说如果该类实现了移动构造则emplace接口会比传统插入接口少一次浅拷贝但是总体上二者的效率差不多如果该类没有实现移动构造则emplace接口的插入效果要远高于传统的插入接口这是因为在传统的插入接口中需要先创建一个临时对象然后将这个临时对象深拷贝或者移动拷贝到容器中而emplace则通过使用可变参数模板万能引用等技术直接在容器中构造对象避免了对象的拷贝和移动
namespace hdp
{class string{public:string(const char* str ):_size(strlen(str)), _capacity(_size){cout string(char* str) -- 构造 endl;_str new char[_capacity 1];strcpy(_str, str);}// s1.swap(s2)void swap(string s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string s){cout string(const string s) -- 深拷贝 endlreserve(s._capacity);strcpy(_str, s._str);_size s._size;_capacity s._capacity;}// 赋值重载string operator(const string s){cout string operator(string s) -- 深拷贝 endl;string tmp(s);swap(tmp);return *this;}// 移动构造string(string s){cout string(const string s) -- 移动拷贝 endl;swap(s);}// 移动赋值string operator(string s){cout string operator(string s) -- 移动赋值 endl;swap(s);return *this;}~string(){delete[] _str;_str nullptr;}char operator[](size_t pos){assert(pos _size);return _str[pos];}void reserve(size_t n){if (n _capacity){char* tmp new char[n 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str tmp;_capacity n;}}void push_back(char ch){if (_size _capacity){size_t newcapacity _capacity 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] ch;_size;_str[_size] \0;}private:char* _str nullptr;size_t _size 0;size_t _capacity 0; // 不包含最后做标识的\0};
}int main()
{pairint, hdp::string kv(20, sort);std::list std::pairint, hdp::string mylist;mylist.emplace_back(kv); // 左值mylist.emplace_back(make_pair(20, sort)); // 右值mylist.emplace_back(10, sort); // 构造pair参数包cout endl;mylist.push_back(kv); // 左值mylist.push_back(make_pair(30, sort)); // 右值mylist.push_back({ 40, sort }); // 右值return 0;
}如果hdp::string实现了移动赋值 如果hdp::string没有实现移动赋值: 3.对于不需要进行深拷贝的自定义类型来说emplace接口会比传统的插入接口少一次浅拷贝(拷贝构造),此时二者的插入效率差不多原理同上
十、 lambda表达式
1.lambda表达式语法
在C98中如果想要对一个数据集合中的元素进行排序可以使用std::sort方法。
#include algorithm
#include functional
int main()
{int array[] { 4,1,8,5,3,7,0,9,2,6 };// 默认按照小于比较排出来结果是升序std::sort(array, array sizeof(array) / sizeof(array[0]));// 如果需要降序需要改变元素的比较规则std::sort(array, array sizeof(array) / sizeof(array[0]), greaterint());return 0;
}如果待排序元素为自定义类型需要用户定义排序时的比较规则
这个时候C设计出了仿函数来替代函数指针仿函数又称为函数对象仿函数实际上就是一个普通的类只是类中重载了函数调用操作符(),这使得该类中的对象可以像函数一样去使用。
#include vector
#include algorithm
struct Goods
{string _name; //名字double _price; // 价格int _evaluate; // 评价Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
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;}
};
int main()
{vectorGoods v { { 苹果, 2.1, 5 }, { 香蕉, 3, 4 }, { 橙子, 2.2,3 }, { 菠萝, 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());sort(v.begin(), v.end(), ComparePriceGreater());
}虽然仿函数能够很好的替代函数指针但是对于上面自定义类型来说如果我们需要对商品的各种属性进行比较此时就需要写无数个仿函数如果命名又不规范那么就会给阅读代码的人带来很大的困扰
随着C语法的发展人们开始觉得上面的写法太复杂了每次为了实现一个algorithm算法都要重新去写一个类如果每次比较的逻辑不一样还要去实现多个类特别是相同类的命名这些都给编程者带来了极大的不便。因此在C11语法中出现了Lambda表达式。
lambda表达式的语法格式如下
[capture - list](parameters) mutable - return-type{ statement}lambda表达式各部分说明:
[capture-list] : 捕捉列表该列表总是出现在lambda函数的开始位置编译器根据[]来判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)参数列表。与普通函数的参数列表一致如果不需要参数传递则可以连同()一起省略mutable默认情况下lambda函数总是一个const函数mutable可以取消其常量性。使用该修饰符时参数列表不可省略(即使参数为空)。-returntype返回值类型。用追踪返回类型形式声明函数的返回值类型没有返回值时此部分可省略。返回值类型明确情况下也可省略由编译器对返回类型进行推导。{statement}函数体。在该函数体内除了可以使用其参数外还可以使用所有捕获到的变量。
**注意**在lambda函数定义中参数列表和返回值类型都是可选部分而捕捉列表和函数体可以为空。因此C11中最简单的lambda函数为[]{}; 该lambda函数不能做任何事情。
所以上面商品的排序我们可以使用lambda表达式的方式来实现代码如下
int main()
{vectorGoods 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._price g2._price; });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._evaluate g2._evaluate; });sort(v.begin(), v.end(), [](const Goods g1, const Goods g2) {return g1._evaluate g2._evaluate; });
}2.lambda表达式的捕捉列表
捕捉列表描述了上下文中那些数据可以被lambda使用以及使用的方式传值还是传引用。
1.[var]表示值传递方式捕捉变量var
传值捕捉到的参数默认是被const修饰的所以我们不能在lambda表达式的函数体中修改他们如果需要进行修改那么我们就需要使用mutable来进行修饰但是由于传值捕捉修改的是形参所以我们一般也不会修改它
int main()
{int a 0, b 1;auto add1 [](int x, int y) {return x y; };cout add1(a, b) endl;auto add2 [b](int x) {return x b; };cout add2(a) endl;auto swap1 [a, b]()mutable{int tmp a;a b;b tmp;};swap1();cout a : b endl;
}2.[]表示值传递方式捕获所有父作用域中的变量(包括this) 我们需要注意的是只能捕捉该行代码上面的部分这是由于编译器只能向上进行寻找 3.[var]表示引用传递捕捉变量var
通过引用捕捉我们就可以在lambda表达式中修改实参的值了 4.[]表示引用传递捕捉所有父作用域中的变量(包括this) 5.[this]表示值传递方式捕捉当前的this指针
除了上面的捕捉方式之外lambda表达式还支持混合捕捉 lambda表达式注意事项 1.父作用域指包含lambda函数的语句块 2.语法上捕捉列表可由多个捕捉项组成并以逗号分割。 比如[, a, b]以引用传递的方式捕捉变量a和b值传递方式捕捉其他所有变量 [a, this]值传递方式捕捉变量a和this引用方式捕捉其他变量 3.捕捉列表不允许变量重复传递否则就会导致编译错误。 比如[, a]已经以值传递方式捕捉了所有变量捕捉a重复 4.在块作用域以外的lambda函数捕捉列表必须为空。 5.在块作用域中的lambda函数仅能捕捉父作用域中局部变量捕捉任何非此作用域或者非局部变量都会导致编译报错。 6.lambda表达式之间不能相互赋值即使看起来类型相同 3.lambda表达式与函数对象
函数对象又称为仿函数即可以想函数一样使用的对象就是在类中重载了operator()运算符的类对象。lambda表达式和仿函数一样本质上也是一个可调用对象所以lambda表达式的使用方式和仿函数一样但是和仿函数不同的是lambda表达式的类型是由编译器自动生成的并且带有随机值所以我们就无法具体写出lambda表达式的类型只能使用auto进行推导。
class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
int main()
{// 函数对象double rate 0.49;Rate r1(rate);r1(10000, 2);// lambdaauto r2 [](double money, int year)-double {return money * rate * year;};r2(10000, 2);return 0;
}从使用方式上来看函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量在定义对象时给出初始值即可lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式完全就是按照函数对象的方式处理的即如果定义了一个lambda表达式编译器会自动生成一个类在该类中重载了operator()。也就是说lambda表达式的底层实际上是通过替换为仿函数类完成的
函数对象 lambda表达式 十一、包装器
1.function包装器
function包装器 也叫作适配器。C中的function本质是一个类模板也是一个包装器。
那么我们来看看我们为什么需要function呢
templateclass F, class T
T useF(F f, T x)
{static int count 0;cout count: count endl;cout count: count endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout useF(f, 11.11) endl;// 函数对象cout useF(Functor(), 11.11) endl;// lamber表达式cout useF([](double d)-double { return d / 4; }, 11.11) endl;return 0;
}我们通过count变量的地址可以发现尽管T的类型相同但是useF函数还是被实例化出了三份这是因为形参F会根据实参的不同而实例化出不同的函数也就是说形参F的类型有多个那么我们能不能让其类型变为一个从而只需要是实例化出一份函数呢此时function包装器就可以解决这个问题
function是一个可调用对象包装器它可以将函数指针仿函数和lambda表达式成员函数等可调用对象进行包装使他们具有相同的类型包装器可以像普通函数一样进行调用包装器的本质也是仿函数在C11标准中引入了std::function模板类其定义在头文件中
function的定义格式如下
std::function返回值类型(参数类型1,参数类型2,...) f;function的使用方式类似于普通类可以先定义一个function对象然后将需要调用的函数赋值给该对象也可以在定义function对象时直接使用可调用对象来进行初始化最后通过function对象来完成函数的调用如下
#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(int a, int b){return a b;}
};
int main()
{// 函数名(函数指针)std::functionint(int, int) func1 f;cout func1(1, 2) endl;// 函数对象std::functionint(int, int) func2 Functor();cout func2(1, 2) endl;// lambda表达式std::functionint(int, int) func3 [](const int a, const int b){return a b; };cout func3(1, 2) endl;// 类的静态成员函数std::functionint(int, int) func4 Plus::plusi;cout func4(1, 2) endl;// 类的非静态成员函数std::functiondouble(Plus, int, int) func5 Plus::plusd;cout func5(Plus(), 1, 2) endl;return 0;
}我们需要注意的是当function封装的是类的成员函数的时候我们需要对类域进行声明如果是非静态成员函数那么我们需要在类域前面加一个取地址符静态成员函数可加可不加但是我们建议都加上。
静态成员函数没有this指针所以function类实例化时不需要添加一个成员函数所属类的类型参数在调用时也不需要传递一个成员函数所属类的对象
// 类的静态成员函数
std::functionint(int, int) func4 Plus::plusi;
cout func4(1, 2) endl;类的非静态成员函数有隐藏的this指针所以需要传递成员函数所属类的对象我们不能显式的传递this指针所以我们传递类的对象即可
// 类的非静态成员函数
std::functiondouble(Plus, int, int) func5 Plus::plusd;
cout func5(Plus(), 1, 2) endl;需要特别注意的是这里我们需要传递的是类型和类的对象不能像下面这种的传递方式进行传递因为不能显式的传递this指针
std::functiondouble(Plus*, int, int) func5 Plus::plusd;
cout func5(Plus(), 1, 2) endl;我们可以看到经过function的包装使得函数指针仿函数lambda表达式一个类的静态成员函数具有了统一的类型–functionint(int, int);类的普通成员函数我们也可以通过后面绑定的方式使得它的类型变为functionint(int, int);
此时我们就可以解决模板实例化多份的问题了
#include functional
templateclass F, class T
T useF(F f, T x)
{static int count 0;cout count: count endl;cout count: count endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名std::functiondouble(double) func1 f;cout useF(func1, 11.11) endl;// 函数对象std::functiondouble(double) func2 Functor();cout useF(func2, 11.11) endl;// lambda表达式std::functiondouble(double) func3 [](double d)-double { return d /4; };cout useF(func3, 11.11) endl;return 0;
}包装器还有一些其他的应用场景比如下面这道OJ题目150.逆波兰表达式求值 - 力扣(LeetCode)
这道题我们传统的解答是这样的
class Solution {
public:int evalRPN(vectorstring tokens) {stackint st;for(auto str:tokens){if(str||str-||str*||str/){// 右操作数int rightst.top();st.pop();// 左操作数int leftst.top();st.pop();if(str)st.push(leftright);if(str-)st.push(left-right);if(str*)st.push(left*right);if(str/)st.push(left/right);}else{// 操作数直接入栈st.push(stoi(str));}}return st.top();}
};我们可以看到我们需要针对不同的操作符进行不同的处理需要使用多个if条件判断语句或者使用switch case语句这样写代码看起来不太舒服我们使用包装器代码如下
class Solution {
public:int evalRPN(vectorstring tokens) {stackint st;mapstring,functionint(int,int) opFuncMap{{,[](int x,int y)-int{return xy;}},{-,[](int x,int y)-int{return x-y;}},{*,[](int x,int y)-int{return x*y;}},{/,[](int x,int y)-int{return x/y;}},};for(auto str:tokens){// 不在就是操作数if(opFuncMap.count(str)0){st.push(stoi(str));}else{int right st.top();st.pop();int left st.top();st.pop();// 将运算结果入栈st.push(opFuncMap[str](left,right));}}return st.top();}
};我们将包装器定义为map的value然后使用不同的key和对应的lambda表达式类初始化map。
2.bind
std::bind函数定义在头文件中是一个函数模板它就像一个函数包装器(适配器)****接受一个可调用对象(callable object)生成一个新的可调用对象来“适应”原对象的参数列表。bind的作用就是调整可调用对象的参数–参数的顺序和参数的个数
一般而言我们用它可以把一个原本接收N个参数的函数fn通过绑定一些参数返回一个接收M个M可以大于N但这么做没什么意义参数的新函数。同时使用std::bind函数还可以实现参数顺序调整等操作。
bind的格式如下
// 原型如下
template class Fn, class... Args
/* unspecified */ bind(Fn fn, Args... args);
// with return type (2)
template class Ret, class Fn, class... Args
/* unspecified */ bind(Fn fn, Args... args);
bind(函数指针或可调用对象,参数1参数2...)可以将bind函数看作是一个通用的函数适配器它接受一个可调用对象生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式auto newCallable bind(callable,arg_list);
其中newCallable本身是一个可调用对象arg_list是一个逗号分隔的参数列表对应给定的callable的参数。当我们调用newCallable时newCallable会调用,并传给它arg_list中的参数。
arg_list中的参数可能包含形如n的名字其中n是一个整数这些参数是“占位符”(placeholders)表示newCallable的参数它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置1为newCallable的第一个参数_2为第二个参数以此类推
placeholders是C11引入的一个命名空间域它包含了一些占位符对象(placeholders),用于在使用bind绑定函数时指定某个参数在调用时传递过来的位置 bind的使用案列如下
#include functional
int Plus(int a, int b)
{return a b;
}int SubFunc(int a, int b)
{return a - b;
}class Sub
{
public:int sub(int a, int b){return a - b * x;}
private:int x 20;
};int main()
{ 表示绑定函数plus 参数分别由调用 func1 的第一二个参数指定//functionint(int, int) func1 bind(Plus, placeholders::_1, placeholders::_2);//cout func1(1, 2) endl;//functionint(int, int) func2 bind(SubFunc, placeholders::_1, placeholders::_2);//cout func2(1, 2) endl; 调整参数的顺序//functionint(int, int) func3 bind(SubFunc, placeholders::_1, placeholders::_2);//cout func3(1, 2) endl;//functionint(int, int) func4 bind(SubFunc, placeholders::_2, placeholders::_1);//cout func4(1, 2) endl;// 绑定固定参数functionint(Sub, int, int) func5 Sub::sub;cout func5(Sub(), 10, 20) endl;cout func5(Sub(), 100, 200) endl;functionint(int, int) func6 bind(Sub::sub, Sub(), placeholders::_1, placeholders::_2);cout func6(10, 20) endl;cout func6(100, 200) endl;return 0;
}bind调整参数顺序
bind可以通过调整占位符的顺序来调整参数的顺序 bind调整参数的个数
bind可以在形参列表中直接绑定具体的函数对象这样参数就会自动传递而不需要我们在调用的时候显式传递并且也不需要我们在function中的参数包中显式的声明这样我们就可以通过绑定让我们的类的普通成员函数和类的静态成员函数以及lambda表达式函数指针一样定义为统一的类型了 bind在实际开发过程中使用不多我们了解一下即可。