文章目录
- 1、C++11的简介
- 2、 统一的列表初始化
- 2-1、{}初始化
- 2-2、std::initializer_list
- 3、声明
- 3-1、auto
- 3-2、decltype
- 3-3、nullptr
- 4、范围for循环
- 5、智能指针
- 6、STL中一些变化
- 6-1、STL新容器和容器新方法
- 6-2、final和override
- 7、右值引用和移动语义
- 7-1、左值、左值引用、右值、右值引用
- 7-2 、左值引用与右值引用比较
- 7-3、右值引用使用场景和意义(重点)
- 7-3-1、样例1
- 7-3-2、样例2:
- 7-3-3、样例3:(修改模拟实现的list)
- 7-4、完美转发
- 8、新的类功能
- 8-1、默认成员函数
- 8-2、类成员变量初始化
- 8-3、关键字default
- 8-4、关键字delete
- 8-5、final与override
- 9、可变参数模板
- 10、lambda表达式
- 10-1、举个样例
- 10-2、lambda表达式
- 10-3、 lambda表达式语法
- 10-3-1、 lambda表达式各部分说明
- 10-3-2、 lambda表达式各部分说明
- 10-3-4、函数对象与lambda表达式(lambda的底层)
- 总结
1、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++11的文档
小故事:
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。
2、 统一的列表初始化
2-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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,
使用初始化列表时,可添加等号(=),也可不添加。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct Point
{
int _x;
int _y;
};
int main()
{
Point p1 = { 1, 2 };
Point p2{ 2,2 };//这样在C++11是算初始化的!
int array1[] = { 1, 2, 3, 4, 5 };//初始化数组
int array2[5] = { 0 };
int array3[] { 1, 2, 3, 4, 5 };//初始化数组
int array4[5] { 0 };///运行没有报错!
//那么我们可以有多种初始化方法,来初始化变量
int a = 1;
int b = { 2 };
int c{ 3 };//也是不报错的!
//我们以前的花括号只能用来初始化数组和结构体,现在“万物”都能花括号列表初始化!!!
//前面的{}初始化变量建议大家不用使用,看得懂就行!
int* ptr = new int[10];//以前不能对new数组直接初始化
int* p = new int[10]{ 1,2,3,4 };//现在可以直接对new数组初始化了
Point* p3 = new Point[2]{ {1,2},{2,2} };
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(2001, 1, 1);
Date d2{ 2001,2,2 };{}对自定义类型是调用构造函数来初始化的
return 0;
}
2-2、std::initializer_list
std::initializer_list的文档介绍:
std::initializer_list文档
std::initializer_list是什么类型:
注意:在将常量数组进行传参的时候,接收常量数组要使用传值传参,或者const传引用传参,因为常量数组具有常属性,不能直接用引用接收,要加const
int main()
{
vector<int> v = { 1,2,3,4 };
vector<int> v1{ 1,2,3,4,5,6,7,8 };
//这里是可以无线参数的,编译器会把这些数识别成为常量数组
list<int> lt = { 1,2,3,4 };
Date d1(2001, 1, 1);
//Date d1(2001, 1, 1, 1);这里只能是三个参数,要匹配构造函数
auto il = { 1, 2, 3, 4, 5 };
cout << typeid(il).name() << endl;//单独的类型(常量数组)
vector<Date> v3 = { {1,1,1},{2,2,2},{3,3,3} };
vector<Date> vd{ {1,1,1},{2,2,2},{3,3,3} };
map<string, string> dict = { { "字符串", "string" }, { "排序", "sort" } };
v = {10, 20, 30};
return 0;
}
std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值
http://www.cplusplus.com/reference/list/list/list/
http://www.cplusplus.com/reference/vector/vector/vector/
http://www.cplusplus.com/reference/map/map/map/
http://www.cplusplus.com/reference/vector/vector/operator=/
3、声明
c++11提供了多种简化声明的方式,尤其是在使用模板时
3-1、auto
这个就不用多说了,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;
}
3-2、decltype
关键字decltype将变量的类型声明为表达式指定的类型
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret = t1 * t2;
cout << typeid(ret).name() << endl;
cout << ret << 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-3、nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 ///这里C++的NULL宏定义是0,但是0是整型,所以NULL的类型是整型了!!!
//所以C++11新增了nullptr,用于表示空指针
#else
#define NULL ((void *)0)
#endif
#endif
4、范围for循环
这个我们在前面的已经进行了非常详细的讲解,这里就不进行讲解了,请参考前面STL容器部分的课件讲解
5、智能指针
这个智能指针比较重要,我们这里三言两语讲不完,我们后面有单独的章节来讲智能指针
6、STL中一些变化
6-1、STL新容器和容器新方法
新容器:
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可
容器中的一些新方法:
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。
比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本(右值引用我们下面就会讲到):
http://www.cplusplus.com/reference/vector/vector/emplace_back/
http://www.cplusplus.com/reference/vector/vector/push_back/
http://www.cplusplus.com/reference/map/map/insert/
http://www.cplusplus.com/reference/map/map/emplace/
这里的右值引用下面会将。另外emplace还涉及模板的可变参数,我们将这里的emplace和上面的智能指针放到后面章节讲,这里大家大概看看文档就行
array:
int main()
{
array<int, 10> a1 = { 0 };
int a2[10] = { 0 };
a2[10];
a2[20];
a2[10] = 1;//这里会报错,因为编译器在数组范围的后面几个位置设置了检查位
//检查位就是:将数组范围后面的几个位置设置为特殊值,如果这些值改变了,就代表越界修改操作
a2[20] = 3;//vs2013这里不会,因为编译器对于a2的越界检查是一种抽查
//vs2022这里也会报错
a1[10];//这里array数组越界读直接报错,operator[]里面有assert(pos < sizze)
vector<int> v(10, 0);//但是这里的vector和array大差不差,用vector就足够了
v[10];//同样能够检查出越界读
return 0;
}
forward_list:
forward_list是一个单链表,但我们早就说了,单链表用的少。而且forward_list一不支持尾插尾删,二它的插入和删除都是在下标的下一个位置,所以用的极其不便。而且对比于list,forward_list就只是节省了一个指针,可以说优点微乎其微
6-2、final和override
final
1.final可以用来修饰类,修饰的类就叫做最终类,使得类无法被继承
2.还可以用来修饰虚函数,使得修饰的虚函数不能被重写
override
检查子类的虚函数是否完成重写
7、右值引用和移动语义
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
7-1、左值、左值引用、右值、右值引用
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针,下面的*p),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
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;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:临时对象、匿名对象、字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
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);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要
右值不能被修改、不能被取地址;但是右值引用可以被修改、可以被取地址
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
7-2 、左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值(因为右值不能写,const左值也不能写)
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以后的左值
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
7-3、右值引用使用场景和意义(重点)
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
左值引用的意义是什么? ——函数传参/函数传返回值 – 减少拷贝
template<class T>
void func(const T& x)//const左值引用,既可以接收左值,还可以接收右值。所以函数传参的问题全部解决了
{
}
int main()
{
vector<int> v(10, 0);
func(v);
func(vector<int>(10, 0));//临时变量、匿名对象都是右值
return 0;
}
左值引用有没有彻底解决问题?——没有
template<class T>
const T& func1(const T& x)
{
//一系列操作
return x;//可以返回T&,因为x出了作用域还在,主函数里面有x
}
int main()
{
vector<int> v(10, 0);
func1(v);
func1(vector<int>(10, 0));//临时变量、匿名对象都是右值
return 0;
}
但是,如果x是局部对象呢?
//左值引用尚未解决的问题场景/
template<class T>
//T& func2(const T& x)不能用引用返回,因为主函数没有y变量,语法上允许,但是会有问题
T func2(const T& x)
{
T y;
//一系列操作
return y;
}
int main()
{
vector<int> v(10, 0);
func2(v);
func2(vector<int>(10, 0));//临时变量、匿名对象都是右值
return 0;
}
右值引用的价值:
1.就是补齐这个最后一块短板,传值返回的拷贝问题
2.对于插入一些插入右值数据,也可以减少拷贝
右值分两种:
纯右值:内置类型表达式的值
将亡值:自定义类型表达式的值
那么我们来举例说明一下:
7-3-1、样例1
//这段代码是杨辉三角的代码,可以看到,最终我们要返回一个二维数组,如果这里面的numRows是100、1000、10000
//那么代价是非常大的,因为二维数组出了作用域就不在了,我们要临时拷贝这个二维数组,代价非常大
//vector<vector<int>> generate(int numRows) {
// vector<vector<int>> vv(numRows);
// for (int i = 0; i < numRows; ++i)
// {
// vv[i].resize(i + 1, 1);
// }
//
// for (int i = 2; i < numRows; ++i)
// {
// for (int j = 1; j < i; ++j)
// {
// vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
// }
// }
//
// return vv;
//}
// 解决方案:换成输出型参数,直接对原来的二维数组就行修改,这样可以直接拿到结果二维数组
// 但是用起来很别扭
void generate(int numRows, vector<vector<int>>& vv)
{
vv.resize(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
}
7-3-2、样例2:
namespace bit
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
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;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)字符串是自定义类型,C++没有字符串类型!
{
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];
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';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
int main()
{
价值1:补齐这个最后一块短板,传值返回的拷贝问题
bit::string ret;
ret = bit::to_string(-1234);
bit::string ret = bit::to_string(-1234);
std::string s1("hello world");
std::string s2(s1);
std::string s3(move(s1));
价值2:对于插入一些插入右值数据,也可以减少拷贝
list<bit::string> lt;
bit::string s1("111111");
//lt.push_back(move(s1));
lt.push_back(s1);
lt.push_back(bit::string("222222"));
lt.push_back("333333");
return 0;
}
但是,还是要进行拷贝,只是从两次减少成为了一次
如果采用了移动构造:
图下方,s1是左值,”2…2“和"3…3"是右值
上面的代码运行之后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值
STL中的容器都是增加了移动构造和移动赋值:
http://www.cplusplus.com/reference/string/string/string/
http://www.cplusplus.com/reference/vector/vector/vector/
7-3-3、样例3:(修改模拟实现的list)
我们之前模拟实现过list,现在我们来看看自己的list与C++库里面的list区别
下面是list的模拟代码:
#pragma once
#include <iostream>
#include <list>
#include<algorithm>
#include <assert.h>
//#include "iterator.h"
using namespace std;
template<class Iterator, class Ref, class Ptr>
class ReverseIterator
{
public:
//T,T&,T*
//const T,const T&,const T*
typedef ReverseIterator<Iterator, Ref, Ptr> self;
ReverseIterator(Iterator s)
:it(s)
{}
Ptr operator->()//返回数据的地址
{
return &(operator*());
}
Ref operator*()//返回数据
{
Iterator tmp = it;
return *(--tmp);
}
self& operator++()
{
--it;
return *this;
}
self& operator--()
{
++it;
return *this;
}
bool operator==(const self& s)const
{
return it == s.it;
}
bool operator!=(const self& s)const
{
return !operator==(s);
}
private:
Iterator it;
};
namespace bzh
{
template<class T>
struct list_node//构建链表的节点
{
list_node<T>* _prev;
list_node<T>* _next;
T _data;
list_node(const T& x)
:_prev(nullptr)
, _next(nullptr)
, _data(x)
{}
};
template<class T, class Ref, class Ptr>//封装迭代器的类
struct list_iterator
{
typedef list_node<T> node;//迭代器只需要浅拷贝,所以不写拷贝构造和析构,因为节点不属于迭代器
typedef list_iterator<T, Ref, Ptr> Self;
node* _ps;//_ps就是一个链表节点的指针
list_iterator(node* p)//迭代器类的构造函数
:_ps(p)
{}
Ref operator*()//解引用返回T&或者const T&
{
return _ps->_data;
}
Ptr operator->()//重载->返回T*或者const T*
{
return &_ps->_data;
}
Self& operator++()
{
_ps = _ps->_next;
return *this;
}
Self operator++(int)
{
Self tmp(_ps);
_ps = _ps->_next;
return _ps;
}
Self& operator--()
{
_ps = _ps->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(_ps);
_ps = _ps->_prev;
return _ps;
}
bool operator!=(const Self& it)const
{
return _ps != it._ps;
}
bool operator==(const Self& it)const
{
return _ps == it._ps;
}
};
template<class T>
class list
{
public:
typedef list_node<T> node;
typedef list_iterator<T, T&, T*> iterator;//将封装的迭代器类重命名为iterator
typedef list_iterator<T, const T&, const T*> const_iterator;//这里多了const类型
// 反向迭代器
//typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
//typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;
//reverse_iterator rbegin()
//{
// return reverse_iterator(end());
//}
//reverse_iterator rend()
//{
// return reverse_iterator(begin());
//}
iterator begin()//返回第一个节点的迭代器
{
return iterator(_head->_next);
}
iterator end()//返回最后一个节点的下一个位置的迭代器
{
return iterator(_head);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
void empty_init()//构造函数,单独写出来,下面复用即可
{
_head = new node(T());
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
template<class BZH>
list(BZH first, BZH last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& it)
{
std::swap(_head, it._head);
std::swap(_size, it._size);
}
list()
{
empty_init();
}
list(list<T>& it)
{
empty_init();
list<T> tmp(it.begin(), it.end());
swap(tmp);
}
list& operator=(list<T> it)
{
swap(it);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
size_t size()
{
return _size;
}
bool empty()
{
return _size == 0;
}
void push_back(const T& x)
{
/*node* newnode = new node(x);
node* tail = _head->_prev;
_head->_prev = newnode;
newnode->_next = _head;
newnode->_prev = tail;
tail->_next = newnode;
++_size;*/
insert(end(), x);
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator insert(iterator pos, const T& x)//在pos的前面插入
{
node* newnode = new node(x);
node* cur = pos._ps;
node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._ps->_prev;
node* next = pos._ps->_next;
prev->_next = next;
next->_prev = prev;
delete pos._ps;
--_size;
return iterator(next);
}
private:
node* _head;
size_t _size;
};
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(5);
lt.push_front(6);
list<int>::iterator rit = lt.begin();
while (rit != lt.end())
{
cout << *rit << " ";
++rit;
}
cout << endl;
//list<int>::reverse_iterator rt = lt.rbegin();
//while (rt != lt.rend())
//{
// cout << *rt << " ";
// ++rt;
//}
}
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(5);
lt.push_front(6);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << lt.size() << endl;
list<int> lt1(lt);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
//lt.clear();
//for (auto e : lt)
//{
// cout << e << " ";
//}
//cout << endl;
list<int> lt2;
lt2.push_back(10);
lt2.push_back(20);
lt2.push_back(30);
lt2.push_back(40);
cout << lt2.size() << endl;
lt = lt2;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
};
};
现在开始对比:
这里4次深拷贝多一次就是,我们在用模拟的list时,构建对象多调用了一次拷贝
可以看到,我们前面模拟实现的list全是深拷贝,而库里面的全是移动拷贝,这差距可就大了,所以我们要把模拟的list改变成使用移动拷贝和构造
list改造后版本:
#pragma once
#include <iostream>
#include <list>
#include<algorithm>
#include <assert.h>
//#include "iterator.h"
using namespace std;
template<class Iterator, class Ref, class Ptr>
class ReverseIterator
{
public:
//T,T&,T*
//const T,const T&,const T*
typedef ReverseIterator<Iterator, Ref, Ptr> self;
ReverseIterator(Iterator s)
:it(s)
{}
Ptr operator->()//返回数据的地址
{
return &(operator*());
}
Ref operator*()//返回数据
{
Iterator tmp = it;
return *(--tmp);
}
self& operator++()
{
--it;
return *this;
}
self& operator--()
{
++it;
return *this;
}
bool operator==(const self& s)const
{
return it == s.it;
}
bool operator!=(const self& s)const
{
return !operator==(s);
}
private:
Iterator it;
};
namespace bzh
{
template<class T>
struct list_node//构建链表的节点
{
list_node<T>* _prev;
list_node<T>* _next;
T _data;
list_node(const T& x)
:_prev(nullptr)
, _next(nullptr)
, _data(x)
{}
list_node(T&& x)
:_prev(nullptr)
, _next(nullptr)
//, _data(x)
, _data(move(x))//调用拷贝构造,要使用move使x变成右值
{}
};
template<class T, class Ref, class Ptr>//封装迭代器的类
struct list_iterator
{
typedef list_node<T> node;//迭代器只需要浅拷贝,所以不写拷贝构造和析构,因为节点不属于迭代器
typedef list_iterator<T, Ref, Ptr> Self;
node* _ps;//_ps就是一个链表节点的指针
list_iterator(node* p)//迭代器类的构造函数
:_ps(p)
{}
Ref operator*()//解引用返回T&或者const T&
{
return _ps->_data;
}
Ptr operator->()//重载->返回T*或者const T*
{
return &_ps->_data;
}
Self& operator++()
{
_ps = _ps->_next;
return *this;
}
Self operator++(int)
{
Self tmp(_ps);
_ps = _ps->_next;
return _ps;
}
Self& operator--()
{
_ps = _ps->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(_ps);
_ps = _ps->_prev;
return _ps;
}
bool operator!=(const Self& it)const
{
return _ps != it._ps;
}
bool operator==(const Self& it)const
{
return _ps == it._ps;
}
};
template<class T>
class list
{
public:
typedef list_node<T> node;
typedef list_iterator<T, T&, T*> iterator;//将封装的迭代器类重命名为iterator
typedef list_iterator<T, const T&, const T*> const_iterator;//这里多了const类型
// 反向迭代器
//typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
//typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;
//reverse_iterator rbegin()
//{
// return reverse_iterator(end());
//}
//reverse_iterator rend()
//{
// return reverse_iterator(begin());
//}
iterator begin()//返回第一个节点的迭代器
{
return iterator(_head->_next);
}
iterator end()//返回最后一个节点的下一个位置的迭代器
{
return iterator(_head);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
const_iterator end()const
{
return const_iterator(_head);
}
void empty_init()//构造函数,单独写出来,下面复用即可
{
_head = new node(T());
_head->_prev = _head;
_head->_next = _head;
_size = 0;
}
template<class BZH>
list(BZH first, BZH last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
void swap(list<T>& it)
{
std::swap(_head, it._head);
std::swap(_size, it._size);
}
list()
{
empty_init();
}
list(list<T>& it)
{
empty_init();
list<T> tmp(it.begin(), it.end());
swap(tmp);
}
list& operator=(list<T> it)
{
swap(it);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
_size = 0;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
size_t size()
{
return _size;
}
bool empty()
{
return _size == 0;
}
void push_back(const T& x)
{
/*node* newnode = new node(x);
node* tail = _head->_prev;
_head->_prev = newnode;
newnode->_next = _head;
newnode->_prev = tail;
tail->_next = newnode;
++_size;*/
insert(end(), x);
}
void push_back(T&& x)
{
//insert(end(), x);
insert(end(), move(x));//同理,x要变成右值,才能继续
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator insert(iterator pos, const T& x)//在pos的前面插入
{
node* newnode = new node(x);
node* cur = pos._ps;
node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator insert(iterator pos,T&& x)
{
node* newnode = new node(move(x));
//node* newnode = new node(x);
node* cur = pos._ps;
node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
node* prev = pos._ps->_prev;
node* next = pos._ps->_next;
prev->_next = next;
next->_prev = prev;
delete pos._ps;
--_size;
return iterator(next);
}
private:
node* _head;
size_t _size;
};
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(5);
lt.push_front(6);
list<int>::iterator rit = lt.begin();
while (rit != lt.end())
{
cout << *rit << " ";
++rit;
}
cout << endl;
//list<int>::reverse_iterator rt = lt.rbegin();
//while (rt != lt.rend())
//{
// cout << *rt << " ";
// ++rt;
//}
}
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_front(5);
lt.push_front(6);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << lt.size() << endl;
list<int> lt1(lt);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
//lt.clear();
//for (auto e : lt)
//{
// cout << e << " ";
//}
//cout << endl;
list<int> lt2;
lt2.push_back(10);
lt2.push_back(20);
lt2.push_back(30);
lt2.push_back(40);
cout << lt2.size() << endl;
lt = lt2;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
};
};
所以,由于多层嵌套的原因,每一层的接收变量要进行move,变成右值才能继续向下面执行
但是这样也未免太过于麻烦了,所以C++11呢有搞出来一个新东西——完美转发
7-4、完美转发
对于上面的普通类,我们要写两个函数接口来区别左值和右值调用什么合适
但是对于模板类来说,不需要区分左值和右值!
但是模板就不一样了!
模板中,&&被称为万能引用(或者叫做引用折叠),万能引用只要是可以调用对象就可以接收(可调用对象包括:左值、右值、函数指针,仿函数,下面的lambda表达式等等)
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)
{
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;
}
const的左值和右值也是可以被模板的&&接收的!!!
上面通过模板实例化出了4个函数,前面两个函数t可以++;后面两个有const属性的函数不能++
这里如果想要调用正确的4给fun函数,仅仅使用move是不行的!这个时候就是完美转发登场的时候了
完美转发:
std::forward < T > 完美转发在传参的过程中保留对象原生类型属性,原来是左值,之后传参还是左值;右值还是右值;const属性同理!
完美转发在使用的时候要注意,一定要是模板,不能够模板实例化了在使用,否则会报错
8、新的类功能
8-1、默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。
C++11 新增了两个:
移动构造函数和移动赋值运算符重载(这两个也是默认成员函数,默认成员函数就是我们不写编译器会生成一个默认的)
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
1. 如果你没有自己实现移动构造函数,并且析构函数 、拷贝构造、拷贝赋值重载都不实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造
2. 如果你没有自己实现移动赋值重载函数,并且析构函数 、拷贝构造、拷贝赋值重载都不实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
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& operator=(const Person& p)
//{
// if(this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// return *this;
//}
//~Person()
//{}
private:
bzh::string _name;自定义类型,上面有处理
int _age;//内置类型
};
int main()
{
Person s1;
Person s2 = s1;//拷贝构造
Person s3 = std::move(s1);//移动构造(如果没有移动构造就会调用拷贝构造!)
Person s4;
s4 = std::move(s2);
return 0;
}
8-2、类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了
8-3、关键字default
强制生成默认函数的关键字
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成
样例:
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(Person&& p)//这里想要在下一层继续使用并且保证原来属性就要使用完美转发
:_name(std::forward<bzh::string>(p._name))
,_age(p._age)
{}
private:
bzh::string _name;
int _age;
};
每次都这么写太麻烦了,我们可以简单点:
只针对默认成员函数!
8-4、关键字delete
禁止生成默认函数的关键字
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数
假设有一个类,我不想让这个类被拷贝,有没有什么方法?
方法一:将拷贝私有化。但是这也是存在问题的,我万一在类里面进行了拷贝了呢?
// 不想让A类对象被拷贝
class A
{
public:
void func()
{
A tmp(*this);
}
A()
{}
~A()
{
delete[] p;
}
A(const A& aa) = delete;
private:
A(const A& aa)
:p(aa.p)
{}
private:
int* p = new int[10];
};
int main()
{
A aa1;
aa1.func();/这里,有人调了类内的func函数,引发了拷贝!!!
return 0;
}
这种情况下只有到了链接阶段才报错,有没有什么方法提前报错呢?
方法二:将拷贝函数私有化并且只声明不实现
有没有更直接的方法来防止拷贝呢?
方法三:delete关键字
8-5、final与override
final
1.final可以用来修饰类,修饰的类就叫做最终类,使得类无法被继承
2.还可以用来修饰虚函数,使得修饰的虚函数不能被重写
override
检查子类的虚函数是否完成重写
这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多台章
节吧。
9、可变参数模板
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习
下面就是一个基本可变参数的函数模板:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
//void ShowList(Args&&... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
参数包:可以传递0~n个参数
我们以前只能传递1个参数,现在有了参数包可以传递0~n个参数包
template <class ...Args>
void ShowList(Args... args)
{
sizeof查看参数包中有几个参数
cout << sizeof...(args) << endl;
}
int main()
{
ShowList(1);//既然是一个模板,又是一个参数包,那么我们就可以随便传
ShowList(1, 1.1);//想传几个就传几个
ShowList(1, 1.1, string("xxxxxxxx"));
ShowList();
return 0;
}
接下来就是怎么那到参数包里面的数据了:
方法一:
void ShowList()
{
cout << endl;
}
template <class T,class ...Args>//增加一个模板参数T
//ShowList(1);
//这里就1个参数,数据1就传给val,这里的ShowList函数打印val值,然后把后面的0个参数当成参数包
//调用下面的ShowList(args...)函数,0个参数就是上面的ShowList函数,直接打印换行就结束了
//ShowList(1, 1.1);
//这里有2个参数,数据1就传给val,这里的ShowList函数打印val值,然后把后面的1个参数(也就是1.1)
//当成参数包,调用下面的ShowList(args...)函数,
//1个参数,我们继续调用这里的ShowList函数,数据1.1就传给val,ShowList函数打印val值,
//然后把后面的0个参数当成参数包调用下面的ShowList(args...)函数,
//0个参数就是上面的ShowList函数,直接打印换行就结束了
//ShowList(1, 1.1, string("xxxxxxxx"));
//这里同理,依次讲参数传给val,然后循环调用ShowList函数本身,直到所有参数都被val拿完了,调用上面的ShowList函数打印换行结束!
void ShowList(T val, Args... args)
{
cout << val << " ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 1.1);
ShowList(1, 1.1, string("xxxxxxxx"));
return 0;
}
ShowList(1);
这里就1个参数,数据1就传给val,这里的ShowList函数打印val值,然后把后面的0个参数当成参数包调用下面的ShowList(args…)函数,0个参数就是上面的ShowList函数,直接打印换行就结束了
ShowList(1, 1.1);
这里有2个参数,数据1就传给val,这里的ShowList函数打印val值,然后把后面的1个参数(也就是1.1)当成参数包,调用下面的ShowList(args…)函数,1个参数,我们继续调用这里的ShowList函数,数据1.1就传给val,ShowList函数打印val值,
然后把后面的0个参数当成参数包调用下面的ShowList(args…)函数,0个参数就是上面的ShowList函数,直接打印换行就结束了
ShowList(1, 1.1, string(“xxxxxxxx”));
这里同理,(1传递给val,1.1和string作为参数包给args,然后1.1传递给val,string作为参数包给args)依次讲参数传给val,然后循环调用ShowList函数本身,直到所有参数都被val拿完了,调用上面的ShowList函数打印换行结束!
方法二:
//方法二:库里面没有加T模板参数,所以用的就是下面的方法
template <class T>
void PrintArg(T t)//这里的PrintArg是随便起的名字,我们可以随便改
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
//(PrintArg(args), 0)这是一个逗号表达式,最终要初始化数组为0
//根据参数包的数据个数,编译器展开参数包,来推出arr数组来开辟多大空间,然后初始化,在把每一个数据调用PrintArg函数
cout << endl;
}
//template <class T>
//int PrintArg(T t)
//{
// cout << t << " ";
// return 0;//这里返回0
//}
//template <class ...Args>
//void ShowList(Args... args)
//{
// int arr[] = { PrintArg(args)... };//这样也是可以的,总之让编译器展开参数包,然后推数组开多大空间,再将每个数据调用PrintArg函数
// cout << endl;
//}
int main()
{
ShowList(1);
ShowList(1, 1.1);
ShowList(1, 1.1, string("xxxxxxxx"));
ShowList();
return 0;
}
接下来我们来看看emplace类型接口和普通的insert接口有什么不同
int main()
{
std::list<int> mylist;//这里只插入1个参数,emplace和insert接口没区别
mylist.push_back(1);
mylist.emplace_back(2);
mylist.emplace_back();
return 0;
}
int main()
{
//std::list<int> mylist;//这里只插入1个参数,emplace和insert接口没区别
//mylist.push_back(1);
//mylist.emplace_back(2);
//mylist.emplace_back();//这里有一个匿名对象,int类型的匿名对象值是0
//for (auto e : mylist)
//{
// cout << e << endl;
//}
std::list< std::pair<int, char> > mylist;
mylist.push_back(make_pair(1, 'a'));//构造+拷贝构造/移动构造
mylist.push_back(pair<int, char>(2, 'h'));
mylist.emplace_back(10, 'd');//这里可以直接这样使用,直接构造
mylist.emplace_back(make_pair(10, 'd'));//这也是可以的
return 0;
}
我们用我们自己模拟的容器来进行测试
int main()
{
pair<int, bzh::string> kv(20, "sort");
std::list< std::pair<int, bzh::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" }); // 列表初始化,调用pair的构造函数 右值 移动拷贝
return 0;
}
小结:
在面对左值时,emplace和insert系列接口都没有优化——构造+深拷贝
面对右值时,emplace系列接口有优化——直接构造;而如果实现了移动构造,insert系列接口——构造+移动构造(移动构造算是浅拷贝代价小,所以也没有优化很多);但是,如果insert系列接口没有实现移动构造,那么差距就大了
再者就是如果是日期类这种,因为不是将亡值,所以insert系列也比emplace系列接口效率低
10、lambda表达式
10-1、举个样例
我们前面了解过了仿函数,C++提出仿函数是为了抛弃函数指针,因为函数指针这个东西太恶心,太难学了
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法:
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;
}
上面这个样例如果不满足我们排序的要求,我们重新写一个出来满足要求就行,但是下面的样例呢?
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
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());
}
对于上面类的名字,价格,评价…如果我们都要进行排序,就要写n多个仿函数,然后进行调用。
所以,仿函数满足不了我们的需求了!
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式
10-2、lambda表达式
对于上面的题目要求,采用lambda表达式就可以解决
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());
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; });
}
肯定有人对这个lanbda表达式有许多不了解,我们接下来进行逐步讲解!
10-3、 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
10-3-1、 lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
int main()
{
// 进行int对象比较的lambda
// lambda--可调用对象
//[](int x, int y)->bool{return x + y; };
auto compare = [](int x, int y) ->bool {return x > y; };
//[](int x, int y) ->bool {return x > y; }这一部分整体是个对象!!!底层就是一个匿名对象
cout << compare(1, 2) << endl;
return 0;
}
int main()
{
int a = 10, b = 20;
//写法一:
auto add1 = [](int x, int y) {return x + y; };//这里的add1是一个仿函数对象
cout << add1(a,b) << endl;//add1调用operator()
//写法二:
//这里参数列表里面没有b,但是上面我们定义了b对象,这里想要使用b就要通过捕捉列表进行捕捉b
auto add2 = [b](int x) {return x + b; };//参数列表里面放对象名就表示捕捉进来了
cout << add2(a) << endl;
lambda交换数据
//这里不能够交换a和b,因为lambda函数是一个具有const属性的函数
//要想取消lambda的const属性就要加上mutable
auto swap1 = [a,b]()mutable
{
int tmp = a;
a = b;
b = tmp;
};
swap1();//这里的lambda是函数,就算没有参数要传,也要带上括号!!!
cout << a << ":" << b << endl;但是还是不会交换,因为lambda捕捉列表是外面对象的一份临时拷贝
///形参是实参的一份临时拷贝,改变形参不会改变实参!!!
auto swap2 = [&a, &b]()//这里要用引用才能交换外面的数据,用了引用就不需要管const属性了
//引用的本质是指针或者对象的地址,
//当a和b这两个别名被交换时,并没有改变a和b所引用的对象,a还是外面a的引用,b还是外面b的引用
{
int tmp = a;
a = b;//这里a被赋值为了b,但是a还是外面a的引用,b同理
b = tmp;
cout << a << ":" << b << endl;
};
swap2();
cout << a << ":" << b << endl;
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量
10-3-2、 lambda表达式各部分说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
父作用域就是指lambda表达式之外的内容
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中向上的变量(包括this)(编译器是向上查找的)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中向上的变量(包括this)(编译器是向上查找的)
[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误 。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
int x = 10; int y = 20;
auto func1 = [x, &y]()//混合捕捉
{
x = 20;
y = 30;//x不能修改,y可以修改
};
int x = 10; int y = 20;
auto func1 = [=, &y]()//除了y都是传值捕捉,只有y传引用捕捉
{
};
int x = 10; int y = 20;
auto func1 = [=, &y]()
{
cout << m << n << endl;//这里捕捉不到m和n,因为编译器都是向上查找的
};
int m = 10, n = 20;
我们继续举例,下面的例子要用到线程,linux里面的线程是C语言那套的,C++是进行了封装的
#include <thread>
void Print1(int x)
{
for (; x < 100; ++x)
{
cout << "进程1打印" << x << endl;
}
}
void Print2(int y)
{
for (; y < 100; ++y)
{
cout << "进程2打印" << y << endl;
}
}
int main()main 是主线程
{
int i = 0;
thread t1(Print1, i);t1是从线程
thread t2(Print2, i);
t1.join();//这里t1要进行join阻塞等待,因为从线程运行的时候,主线程也会向下运行,这个时候主线程就会
//直接走完,所以要进行进程阻塞等待
t2.join();
return 0;
}
这里如果要对一个i进行++,也就是我们要看到两个Print打印合计100次,就需要传值传参
#include <thread>
void Print1(int& x)//改为传引用
{
for (; x < 100; ++x)
{
cout << "进程1打印" << x << endl;
}
}
void Print2(int& y)//改为传引用
{
for (; y < 100; ++y)
{
cout << "进程2打印" << y << endl;
}
}
//int i = 0;//或者将i改为全局变量,全局变量进程共享
int main()main 是主线程
{
int i = 0;
thread t1(Print1, ref(i));
thread t2(Print2, ref(i));//这里的i要加ref()修饰,具体为什么下面会讲到
t1.join();
t2.join();
return 0;
}
上面的进程代码是我们常规逻辑写的代码,我们也有非常规逻辑写的代码:
下面代码具有线程安全问题!!!
int main()
{
int i = 0;
thread t1([&i]()
{
for (; i < 100; ++i)
{
cout << "进程1打印" << i << endl;
}
});
thread t2([&i]()
{
for (; i < 100; ++i)
{
cout << "进程2打印" << i << endl;
}
});
t1.join();
t2.join();
return 0;
}
线程安全问题我们下面也会讲的,这里就见见猪跑,也顺便了解lambda表达式更多的用法!
我们继续向下走,刚才是两个线程同时运行,如果我们是n给线程同时运行呢?
int main()
{
vector<thread> vt;
int n = 10;
cin >> n;
vt.resize(n);//默认创建的线程为空,为空也就是不执行
int i = 0;
int x = 0;//要放到外面
for (auto& t : vt)//C++库里面的线程不允许拷贝构造和拷贝赋值
///只允许移动构造和移动赋值
{
t = thread([&i, x]()创建匿名的线程对象(右值,将亡值),移动赋值给t
{
for (; i < 1000; ++i)
{
cout << "thread:" << x << "->" << i << endl;
}
});
++x;
}
for (auto& t : vt)
{
t.join();
}
return 0;
}
10-3-4、函数对象与lambda表达式(lambda的底层)
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()///一个仿函数,一个lambda表达式
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double {return monty * rate * year;
};
r2(10000, 2);
return 0;
}
lambda表达式底层的那一串数字是lambda表达式+uuid生成的,大概率保证每次生成的值不一样,所以lambda的类型只有编译器知道,我们不知道,所以只能用auto替代
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
总结
本节内容只是C++11中经常使用或者遇到的内容,虽然只是C++11的冰山一角,但是足够我们使用了,掌握好上面全部的知识点我们是足够了的,想要继续深入学习C++11的需要去官网或者C++社区找寻资料了