文章目录
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增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以我们这里主要讲解实际中比较实用的语法。
小故事:
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()
11、包装器
11-1、function包装器(类模板)
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。用于统一类型
那么我们来看看,我们为什么需要function呢?
//ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)//这里会实例化出很多函数,因为这里f是一直变化的
{
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;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
通过上面的程序验证,我们会发现useF函数模板实例化了三份
能不能减少模板实例化的次数呢?
包装器可以很好的解决上面的问题
我们先来看看function的模板原型
std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
看着和前面的lambda和可变参数模板一样有点怪
样例:
#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;
}
};
//function底层实现应该也是由lambda实现的,
//template <class Ret, class... Args>
//class function<Ret(Args...)>; //这是一个类
//类的对象要cout << func3(1, 2) << endl这样调用底层就是一个仿函数
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;//第一个int是返回值,然后括号里面的是参数列表
//std::function<int(int, int)> func20;
//func20 = f;
//std::function<int(int, int)> func21(f);//这些方法都可以
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
Functor ft;
std::function<int(int, int)> func31(ft);
std::function<int(int, int)> func30(Functor());这里用匿名对象是不行的!!!
cout << func2(1, 2) << endl;
cout << func31(1, 2) << endl;
//cout << func30(1, 2) << endl;
// lamber表达式
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;
Plus plus;//有名对象
std::function<double(Plus, double, double)> func6 = &Plus::plusd;
cout << func6(plus, 1.1, 2.2) << endl;
std::function<int(int, int)> func7 = [&plus](int x, int y)->int {return plus.plusd(x, y); };
cout << func7(5, 5) << endl;
return 0;
}
template<class F, class T>
T useF(F f, T x)//使用了function,就只需要实例化一个函数出来
{
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(function<int(double)>(f), 11.11) << endl;
// 函数对象
Functor ft;
cout << useF(function<int(double)>(Functor()), 11.11) << endl;
cout << useF(function<int(double)>(ft), 11.11) << endl;
// lambda表达式
cout << useF(function<int(double)>([](double d)->double { return d / 4; }), 11.11) << endl;
return 0;
}
11-2、bind包装器(函数模板)
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作
用于调整参数的个数以及顺序
bind原型
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
//fn是可调用对象,args是可调用对象的参数列表
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);
其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
private:
int x = 20;
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);
//如果我们using namespace placeholders展开这个命名空间域,上面就只需要_1,_2即可
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<void(int, int, int)> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
function<int(int, int)> func6 = std::bind(sub, placeholders::_1, placeholders::_2);
cout << func6(1, 2) << endl;
//调整参数顺序
function<int(int, int)> func7 = std::bind(sub, placeholders::_2, placeholders::_1);
cout << func6(1, 2) << endl;
function<bool(int, int)> gt = bind(less<int>(), placeholders::_2, placeholders::_1);
//less<int>()仿函数
cout << gt(1, 2) << endl;
Sub s;
//固定绑定参数
function<int(Sub, int, int)> func8 = &Sub::sub;
cout << func8(Sub(), 10, 20) << endl;
cout << func8(Sub(), 100, 200) << endl;
function<int(int, int)> func9 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func9(100, 200) << endl;//不需要传3个参数
// 绑定成员函数
function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
// 参数调换顺序
function<int(int, int)> func4 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1);
cout << func3(1, 2) << endl;
cout << func4(1, 2) << endl;
return 0;
}
文章目录
12、线程库
12-1、thread类的简单介绍
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件
C++11中的线程类
注意:linux中线程的本质是一个轻量级进程
每一个线程都有独立的栈结构(寄存器也有)
线程的栈区,寄存器…是各自有的,但是它的堆区,静态区…是共享的,所以线程之间的通信成本低
wait():进程都是树形结构的,所有的进程都是父子关系(0号进程创建1号进程,1号进程创建2号进程…),且进程不需要同时运行,子进程可以做子进程的事,父进程可以做父进程的事。子进程结束了,它的资源要释放,而这个工作就是由父进程来做,所以父进程要wait等待子进程结束做后续工作;如果父进程先走了,不等待回收子进程资源,就会触发托孤,而孤儿院就是1号进程,所有的父进程先走了,子进程没有回收就被托孤给1号进程
jion() :线程是同时运行的。假设我们thread了3个线程,这三个线程都是从线程,我们的main才是主线程,但是由于线程是同时运行的,所以从线程在运行,主线程也在运行,这个时候主线程一旦运行,很有可能就直接将程序结束了,所以线程需要join阻塞,等从线程走完之后,主线程在来继续执行
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn,args1, args2,…) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
先来看看最常见的线程和进程:
注意:
- 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态
- 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程
#include <thread>
int main()
{
//std::thread t1;
//cout << t1.get_id() << endl;
int n,m;// n个线程,执行m次
cin >> n >> m;
vector<thread> v;
v.resize(n);//这里就创建的n个线程对象,但都是的,什么都没有
//for(auto e : v)//这是不能的,thread线程不让我们调用拷贝构造和赋值拷贝
for (auto& e : v)
{
e = thread([m]() {
for (size_t i = 0; i < m; ++i)
{
cout << this_thread::get_id() << ":" << i << endl;//this_thread是一个命名空间域
}
});
}
for (auto& e : v)
{
e.join();
}
return 0;
}
get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个结构体:
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
void *_Hnd; /* Win32 HANDLE */
unsigned int _Id;
} _Thrd_imp_t;
- 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行
线程函数一般情况下可按照以下三种方式提供:
函数指针
lambda表达式
函数对象
#include <iostream>
using namespace std;
#include <thread>
void ThreadFunc(int a)
{
cout << "Thread1" << a << endl;
}
class TF
{
public:
void operator()()
{
cout << "Thread3" << endl;
}
};
int main()
{
// 线程函数为函数指针
thread t1(ThreadFunc, 10);
// 线程函数为lambda表达式
thread t2([]{cout << "Thread2" << endl; });
// 线程函数为函数对象
TF tf;
thread t3(tf);
t1.join();
t2.join();
t3.join();
cout << "Main thread!" << endl;
return 0;
}
- thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
- 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束
12-2、线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
#include <thread>
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
12-2、原子性操作库(atomic)
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦
比如:C++98中传统的解决方式:可以对共享修改的数据可以加锁保护
int value = 1;//线程在同一个进程地址空间,堆区,常量区,静态区共享,这里的全局变量value可以被线程共享
mutex mtx;
void func1(int n)
{
for (size_t i = 0; i < n; ++i)
{
mtx.lock();
++value;
mtx.unlock();
}
}
void func2(int n)
{
mtx.lock();
for (size_t i = 0; i < n; ++i)
{
//mtx.lock();//这里外面加锁比里面加锁更高效,而且同样不会报错
++value;
//mtx.unlock();
//因为加锁也需要时间消耗,假如下面t1线程抢到资源,进入锁里面了,那么t2就会在锁那里阻塞等待t1结束
//然后t2才能向下执行,但是阻塞是要保存t2的上下文结构的,由于t1就执行一句++value;就解锁了
这就导致了,t2的上下文可能刚刚保存好,没一会就又被拿到寄存器上面继续执行了
这就导致频繁切换上下文了!!!
//如果不止++value;一句代码,比如是map的插入,这个时候里面加锁才高效
}
mtx.unlock();
}
int main()
{
int m = 100000000;
//为什么t1和t2两个线程可以调用同一份函数func1
//1、每一个线程有独立的栈结构,两个线程调用func1会形成独立的函数栈帧
//2、func1函数编译好之后是放在代码段里面的,代码段在线程中是共享的
thread t1(func1, m);
thread t2(func1, 2*m);
//thread t1(func1, 10000);
//thread t2(func2, 20000);
t1.join();
t2.join();
cout << value << endl;
return 0;
}
虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入的原子操作类型,使得线程间数据的同步变得非常高效
12-2-1、CAS操作
CAS操作就是让某个操作,比如++可以变成原子的,windows和linux等操作系统会提供CAS操作接口
12-2-2、atomic
atomic就是C++中对上面CAS等一系列的封装
在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问
更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型
atomic<int> value = 0;//包了头文件之后,就这样使用,atomic效率也会慢一点,因为最后写值回去可能失败,要进入循环再来
//但是可以并行运行,加锁是串行运行的
void func1(int n)
{
for (size_t i = 0; i < n; ++i)
{
++value;
}
}
int main()
{
int m = 100000000;
thread t1(func1, m);
thread t2(func1, 2 * m);
t1.join();
t2.join();
cout << value << endl;
return 0;
}
当然,上面代码还是有些小瑕疵的,我们因为有多个线程要对value进行++,因为线程是共享全局变量(静态区共享)的,所以把value定义成为了全局变量,那么有没有什么办法不使用全局变量达到效果呢?
这个时候我们的lambda表达式就登场了:
int main()
{
int m = 100000000;
atomic<int> value = 0;
auto func1 = [&value](int m) {//这里的m是下面t1和t2传的参数
for (size_t i = 0; i < m; ++i)
{
++value;
}
};
thread t1(func1, m);
thread t2(func1, 2 * m);
t1.join();
t2.join();
cout << value << endl;
return 0;
}
所以,线程中,栈上面的变量也不一定是安全的,如果能够被多个线程捕捉到(lambda表达式),那么它就不是安全的,可能被修改
注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
12-3、lock_guard与unique_lock
unlock会唤醒一个lock
12-3-1、mutex的种类
在C++11中,Mutex总共包了四个互斥量的种类:
1. std::mutex
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:
函数名 | 函数功能 |
---|---|
lock() 上锁 | 锁住互斥量 |
unlock() 解锁 | 释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
注意,线程函数调用lock()时,可能会发生以下三种情况:
如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
线程函数调用try_lock()时,可能会发生以下三种情况:
如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
2. std::recursive_mutex
其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
3. std::timed_mutex
比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()
try_lock_for() : 相对时间。比如多少分钟之后
try_lock_until() : 绝对时间。某年某月某天某时某分某秒
try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
4. std::recursive_timed_mutex
12-3-2、lock_guard
简单来说,lock_guard提供了RAII,可以自动析构解锁,不怕抛异常的问题了
int main()
{
int m = 100000000;
mutex mtx;
atomic<int> value = 0;
//auto func1 = [&](int m) {
// for (size_t i = 0; i < m; ++i)
// {
// ++value;
// //mtx.lock();
// lock_guard<mutex> lock(mtx);
// cout << value << endl;
// //mtx.unlock();
// }
//};
auto func1 = [&](int m) {
for (size_t i = 0; i < m; ++i)
{
{//这个时候加匿名作用域,出了作用域lock_guard<mutex> lock(mtx);自动解锁,value就不进来了
lock_guard<mutex> lock(mtx);
cout << value << endl;
}
++value;//value本来就是原子的,不需要锁,但是lock_guard<mutex> lock(mtx);把value也锁了,虽然不会报错,但还是多此一举了
}
};
thread t1(func1, m);
thread t2(func1, 2 * m);
t1.join();
t2.join();
cout << value << endl;
return 0;
}
通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock
12-3-2、unique_lock
简单来说,unique_lock和上面的lock_guard差不多,也提供了RAII,但是它能够让我们主动解锁
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的
unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题
与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:
上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)
13、面试题
13-1、并发与并行的区别?
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行
相同点:并发和并行的目标都是最大化CPU的使用率,将cpu的性能充分压榨出来。
不同点:
1、并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在
2、并行要求程序能够同时执行多个操作,而并发只是要求程序“看着像是”同时执行多个操作,其实是交替执行
总结:
并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力
单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中
在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段
13-2、两个线程交替打印,一个打印奇数,一个打印偶数(题目)
这个题看着很简单,我们来试一下:
存在问题的代码:
欸,怎么有个100???
发生访问冲突了,而且这样写是不能避免的
但是存在效率问题:
我们改成通知形式的,t1执行完通知t2,t2执行完通知t1:
13-2-1、条件变量
条件变量本身是不安全的,创建时要配合锁来使用
头文件:
#include<condition_variable>
wait : 在阻塞时会把锁解掉,在回来时会把锁加上
int main()
{
int n = 100000000;
int i = 0;
mutex mtx;
condition_variable cv;//条件变量,可以做到互相通知
bool flag = true;
//flag == false让t1运行
auto func1 = [&i, &mtx, &cv, &flag]() {
while (i < n)
{
unique_lock<mutex> lock(mtx);
//while(flag==true)//偶数阻塞
// cv.wait(lock);//阻塞 —— 会解锁
cv.wait(lock, [&i] {return i % 2; });//这样也可以
cout << "thread 1 ->" << i << endl;
++i;
flag = true;
cv.notify_one();//就算这一次没有通知到下面的线程2,flag是true,也会在上面阻塞
}
};
//flag == true让t2运行
auto func2 = [&i, &mtx, &cv, &flag]() {//t2先运行,因为先打印0 //也就是说t1先进去要阻塞
while (i <= n)
{
unique_lock<mutex> lock(mtx);
//while (flag == false)//奇数阻塞
// cv.wait(lock);
cv.wait(lock, [&i] {return i % 2 != 1; });//这种方法返回false阻塞,返回true就不阻塞
//奇数返回false才阻塞
cout << "thread 2 ->" << i << endl;
++i;
flag = false;//将flag改成false,这样t2就一定会在自己的while里面wait!
cv.notify_one();//通知一个线程
//cv.notify_all();//通知全部线程
}
};
thread t1(func1);
thread t2(func2);
t1.join();
t2.join();
return 0;
}
14、总结
本节内容只是C++11中经常使用或者遇到的内容,虽然只是C++11的冰山一角,但是足够我们使用了,掌握好上面全部的知识点我们是足够了的,想要继续深入学习C++11的需要去官网或者C++社区找寻资料