文章目录
一、C++发展简介
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序
语言的发展就像是练功打怪升级一样,也是逐步递进,由浅入深的过程。我们先来看下C++的历史版本
阶段 | 内容 |
---|---|
C with classes | 类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等 |
C++1.0 | 添加虚函数概念,函数和运算符重载,引用、常量等 |
C++2.0 | 更加完善支持面向对象,新增保护成员、多重继承、对象的初始化、抽象类、静态成员以及const成员函数 |
C++3.0 | 进一步完善,引入模板,解决多重继承产生的二义性问题和相应构造和析构的处理 |
C++98 | C++标准第一个版本,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国标准化协会认可,以模板方式重写C++标准库,引入了STL(标准模板库) |
C++03 | C++标准第二个版本,语言特性无大改变,主要:修订错误、减少多异性 |
C++05 | C++标准委员会发布了一份计数报告(Technical Report,TR1),正式更名C++0x,即:计划在本世纪第一个10年的某个时间发布 |
C++11 | 增加了许多特性,使得C++更像一种新语言,比如:正则表达式、基于范围for循环、auto关键字、新容器、列表初始化、标准线程库等 |
C++14 | 对C++11的扩展,主要是修复C++11中漏洞以及改进,比如:泛型的lambda表达式,auto的返回值类型推导,二进制字面常量等 |
C++17 | 在C++11上做了一些小幅改进,增加了19个新特性,比如:static_assert()的文本信息可选,Fold表达式用于可变的模板,if和switch语句中的初始化器等 |
C++20 | **自C++11以来最大的发行版,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)**等重大特性,还有对已有特性的更新:比如Lambda支持模板、范围for支持初始化等 |
C++23 |
|
C++还在不断的向后发展。但是:现在公司主流使用还是C++98和C++11,所有大家不用追求最新,重点将C++98和C++11掌握好,等工作后,随着对C++理解不断加深,有时间可以去琢磨下更新的特性。
二、C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。
从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程
主要讲解实际中比较实用的语法。
这里有个关于C++发展历史的小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
完整的C++11更新的语法我们可以查阅C++11官方文档:
三、列表初始化
1.统一使用{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
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扩大了用大括号括起的列表(初始化列表)的使用范围,(这里的初始化列表和构造函数中的初始化列表不同)使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
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 };
// C++11中列表初始化也可以适用于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
// C++11支持的列表初始化,这里会调用构造函数初始化
Date d2{ 2023, 1, 2 };
Date d3 = { 2023, 1, 3 };
return 0;
}
2.initializer_list类
initializer_list是C++11中新增的一个类,其文档介绍如下: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()
{
// 列表初始化
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = { 10, 20, 30 };
return 0;
}
我们需要注意的是,当列表中的元素类型和元素的个数符号构造函数的参数要求的时候,即有对应的构造函数,那么编译器会直接调用构造函数来完成初始化,当列表中的元素不符合构造函数的参数的要求的时候,即没有对应的构造函数,此时会先将列表转化为initializer_list类的对象,然后再调用initializer_list的构造函数来完成初始化
总结:在C++11之后,一切的初始化都可以通过{}来完成,初始化的时候可以省略赋值符号(STL中的所有容器都重载了参数类型为initializer_list的构造函数和赋值重载函数,但是不包括容器适配器,因为容器适配器本身不是一个容器,而是由其他容器进行封装而来的)
四、变量的类型推导
1.auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推导。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}
2.decltype
关键字decltype将变量的类型声明为表达式指定的类型。
// decltype的一些使用使用场景
template<class 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的类型是double
decltype(&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既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
五、范围for循环
范围for是C++11提供的一个语法糖,它配合auto可以让我们很方便的堆容器进行遍历,它的 底层是通过替换成迭代器来实现的,所以只要支持迭代器,那么就一定支持范围for
int main()
{
// 使用列表初始化
vector<int> v = { 1,2,3,4,5, };
// 使用范围for进行遍历
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
// 使用迭代器进行遍历 -- 二者等价
//std::vector<int>::iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 容器适配器stack
stack<int> 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中一些变化
新容器
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。其他的大家了解一下即可
新方法
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,其主要分为如下几个方面:
所有了支持const迭代器的容器都提供了cbegin和cend方法返回const迭代器–没有多大用
所有容器的插入接口都提供了emplace版本,包括容器适配器–empalce主要是可变参数模板和右值引用:
所以容器的构造函数都重载了移动构造和参数为initializer_list的构造(容器适配器重载了移动构造,但没有重载initailizer_list的构造)
所有容器的赋值重载函数都重载了移动赋值和参数为initailizer_list的赋值,不包括容器适配器
七、final与override
C++11中新增两个关键字–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个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
-
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
-
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
-
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
总结:如果我们在一个类中什么也没有实现或者只有一个构造函数,那么编译器会自动生成移动构造和移动赋值,自动生成的对于内置类型完成值拷贝,对于自定义类型时,取决于自定义类型是否实现了移动构造和移动赋值,实现了就调用自定义类型的移动构造和移动赋值,没有实现就调用自定义类型的拷贝构造和赋值重载
2.成员变量的缺省值
C++98的构造函数默认对的初始化列表对内置类型不处理,所以C++11允许在类定义时给成员变量赋初始值,这些缺省值会在初始化列表用来初始化成员变量
3.default 和 delete
强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用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
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不实现,这样只要其他人想要调用就会报错。
class A
{
public:
A()
{
_p = new int[10]{0};
}
~A()
{
delete[] _p;
}
private:
// 将拷贝构造定义为私有,防止在类外进行拷贝
A(const A& a)
:_p(a._p)
{}
int* _p;
};
但是这个方法只能防止在类外进行拷贝,而在类中我们仍然可以调用拷贝构造函数来完成拷贝,此时编译器在编译时不会发生错误,只有运行起来对同一块空间析构两次来会报错:
我们如何让一个类既不能在外部被拷贝,也不能在内部被拷贝呢,我们只给出拷贝构造函数的声明,且声明为私有,这时只要调用了拷贝构造函数,那么在链接时就一定会发生错误:
以上是C++98中防止一个类被拷贝的做法:在C++11中更简单,只需在该函数声明加上=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++也同样使用这种方式。C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包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。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
STL emplace相关接口函数
C++11为所有容器的插入接口都新增了一个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) -- 深拷贝" << endl
reserve(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()
{
pair<int, hdp::string> kv(20, "sort");
std::list< std::pair<int, 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表达式语法
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用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]), greater<int>());
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()
{
vector<Goods> 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算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
lambda表达式的语法格式如下:
[capture - list](parameters) mutable -> return-type{ statement}
lambda表达式各部分说明:
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
**注意:**在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
所以,上面商品的排序我们可以使用lambda表达式的方式来实现,代码如下:
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._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);
// lambda
auto 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呢?
template<class 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表达式,成员函数等可调用对象进行包装,使他们具有相同的类型,包装器可以像普通函数一样进行调用,包装器的本质也是仿函数,在C++11标准中引入了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::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, int, int)> func5 = &Plus::plusd;
cout << func5(Plus(), 1, 2) << endl;
return 0;
}
我们需要注意的是,当function封装的是类的成员函数的时候,我们需要对类域进行声明,如果是非静态成员函数,那么我们需要在类域前面加一个取地址符,静态成员函数可加可不加,但是我们建议都加上。
静态成员函数没有this指针,所以function类实例化时不需要添加一个成员函数所属类的类型参数,在调用时也不需要传递一个成员函数所属类的对象
// 类的静态成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
类的非静态成员函数有隐藏的this指针,所以需要传递成员函数所属类的对象,我们不能显式的传递this指针,所以我们传递类的对象即可
// 类的非静态成员函数
std::function<double(Plus, int, int)> func5 = &Plus::plusd;
cout << func5(Plus(), 1, 2) << endl;
需要特别注意的是,这里我们需要传递的是类型和类的对象,不能像下面这种的传递方式进行传递,因为不能显式的传递this指针
std::function<double(Plus*, int, int)> func5 = &Plus::plusd;
cout << func5(&Plus(), 1, 2) << endl;
我们可以看到,经过function的包装,使得函数指针,仿函数,lambda表达式一个类的静态成员函数具有了统一的类型–function<int(int, int)>;类的普通成员函数我们也可以通过后面绑定的方式使得它的类型变为function<int(int, int)>;
此时,我们就可以解决模板实例化多份的问题了:
#include <functional>
template<class 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::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lambda表达式
std::function<double(double)> func3 = [](double d)->double { return d /
4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
包装器还有一些其他的应用场景,比如下面这道OJ题目:150.逆波兰表达式求值 - 力扣(LeetCode)
这道题我们传统的解答是这样的:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(auto str:tokens)
{
if(str=="+"||str=="-"||str=="*"||str=="/")
{
// 右操作数
int right=st.top();
st.pop();
// 左操作数
int left=st.top();
st.pop();
if(str=="+")
st.push(left+right);
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(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>> opFuncMap=
{
{"+",[](int x,int y)->int{return x+y;}},
{"-",[](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是C++11引入的一个命名空间域,它包含了一些占位符对象(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 的第一,二个参数指定
//function<int(int, int)> func1 = bind(Plus, placeholders::_1, placeholders::_2);
//cout << func1(1, 2) << endl;
//function<int(int, int)> func2 = bind(SubFunc, placeholders::_1, placeholders::_2);
//cout << func2(1, 2) << endl;
调整参数的顺序
//function<int(int, int)> func3 = bind(SubFunc, placeholders::_1, placeholders::_2);
//cout << func3(1, 2) << endl;
//function<int(int, int)> func4 = bind(SubFunc, placeholders::_2, placeholders::_1);
//cout << func4(1, 2) << endl;
// 绑定固定参数
function<int(Sub, int, int)> func5 = &Sub::sub;
cout << func5(Sub(), 10, 20) << endl;
cout << func5(Sub(), 100, 200) << endl;
function<int(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在实际开发过程中使用不多,我们了解一下即可。