C++进阶---C++11

C++11

  • 1)初始化
    • ①{}初始化
    • ②std::initializer_list
  • 2)声明
    • ①auto
    • ②decltype
    • ③nullptr
  • 3)范围for
  • 4)Raw string
  • 5)智能指针(MARK一下)
  • 6)STL变化
  • 7)右值引用、移动语义
    • ①错误使用
    • ②右值的应用场景
      • 移动构造
      • 移动赋值
    • ③move的使用场景
    • ④完美转发
    • ⑤总结
  • 8)类新特性
    • 类成员变量初始化
    • default
    • delete,override,final略,参考之前的博客
  • 9)可变参数模板(MARK一下)
    • emplace接口
  • 10)lambda表达式
  • 11)包装器
    • function ( MARK )
      • 应用题目
    • blind
  • 12)线程库
    • 头文件thread
      • thread
      • this_thread
    • 头文件mutex
      • mutex
      • lock_guard和unique_lock
      • recursive_mutex
      • timed_mutex
    • 头文件atomic
    • 头文件condition_variable

1)初始化

①{}初始化

struct Point
{
	int _x;
	int _y;
	Point(int x, int y)
		:_x(x)
		, _y(y)
	{}
};
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 a[] = { 1, 2, 3, 4 };
int a[] { 1, 2, 3, 4 };
//Point p = { 1, 2 };
Point p{ 1, 2 };

int* p1 = new int(0);
int* p2 = new int[5]{1,2,3,4,5};
Point* p3 = new Point[3]{{1, 1}, { 2, 2 }, { 3, 3 }};
//需要有初始化列表初始化,底层未封装这种初始化
Date d1(2022, 3, 13);
Date d2 = { 2022, 3, 15 };
Date d3{ 2022, 3, 15 };
Date{2022,3,15};

int i = 1;
int j = { 2 };
int k{ 3 };

C++11里面扩展了{}初始化使用,基本都可以使用它来初始化,但是建议还是按旧的用法来使用,一般new[]建议使用它来初始化


②std::initializer_list

initializer list用来接收{},同时支持迭代器
例如:

auto il = { 10, 20, 30 };
initializer_list<int> il={ 10, 20, 30 }//支持迭代器:
std::initializer_list<int>::iterator it;  // same as: const int* it
for ( it=il.begin(); it!=il.end(); ++it) {
	cout<< *it <<endl;
}

以vector为例:
vector的constructor函数

vector (initializer_list<value_type> il,
      const allocator_type& alloc = allocator_type());

vector中要实现新的成员函数vector(initializer_list l)

//C++11实现的initializer_list
typedef T* iterator;
vector(initializer_list<T> l)
{
	_start = new T[l.size()];
	_finish = _start + l.size();
	_endofstorage = _start + l.size();

	iterator vit = _start;
	typename initializer_list<T>::iterator lit = l.begin();
	while (lit != l.end())
	{
		*vit++ = *lit++;
	}
}

2)声明

①auto

auto最初是在C语言中的关键字,用于定义一个自动类型,在栈上,不用自动销毁,现已废弃
在C++中auto为自动推导类型
auto最常用在省略迭代器类型,比如:

initializer_list<T> l;
auto lit = l.begin();

②decltype

关键字decltype将变量的类型声明为表达式指定的类型
比如:

const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p;// p类型是int*

decltype的一些使用使用场景

template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

③nullptr

在传统C中 ‘NULL’ 是 头文件 stddef.h 中的一个宏,可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量C++98也用的NULL

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

所以我们在遇到这样的代码:

void f(int)
{
	cout<<"f(int)"<<endl;
}
void f(int*)
{
	cout<<"f(int*)"<<endl;
}
int main()
{
	f(NULL);
	f((int*)NULL);
	return 0;
}

第一次打印的是f(int)与我们想打印空指针相悖只有第二次强制类型转换才可以达到预期效果
所以在C++中引入了专门用来表示空指针的nullptr


注意

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr

3)范围for

底层其实就是迭代器,参考:C++初阶—string类的模拟实现的iterator迭代器部分

4)Raw string

以 R"( 开头, )" 结束,是可以跨越多行的字符串字面值,转义字符如 \t\n 在raw string literal中是普通的文本,而不再是转义字符

  • 因为raw string literal以 )“结束,不能在raw string literal 中再包含字符 )”。
  • 如果你想在raw string literal中包含 )",需要使用raw string literal扩展语法:const char* raw = R"d-char-sequences(raw-char-sequences)d-char-sequences"
    raw-char-sequences 是原始的文本,首尾的 d-char-sequences 是分隔符,首尾分隔符必须一样,最长为16个字符,而且 )d-char-sequences 不能在 raw-char-sequences 中出现
    之前的代码可以这样改正:const char* text = R"--(Embedded )" in string)--";

原文:C++ raw string literal

5)智能指针(MARK一下)

参考:C++进阶—智能指针

6)STL变化

  1. array a1;int a2[10];底层一样,除了实现了迭代器,同时array加了越界assert强制检查,而对于数组,编译器是抽查
  2. forward_list无实际的用处,仅仅比list少用了一个指针,且只支持push_front、pop_front
  3. unordered_map/unordered_set,map和set的hash封装,较多数据时效率更高
  4. 插入函数的右值引用版本(见下)

7)右值引用、移动语义

复习左值引用:C++初阶—C++基础入门概览引用部分
左值/左值引用:

  1. 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址
  2. 左值引用就是给左值的引用,给左值取别名

无论左值引用还是右值引用,都是给对象取别名
右值/右值引用:

  1. 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址
  2. 右值引用就是对右值的引用,给右值取别名

例如:

double x=1.1,y=1.2;//x,y是左值
//以下都是右值
12;
x+y;
min(x,y);
//以下是右值引用
int&& a=10double&& b=x+y;
double&& c=min(x,y);

注意:

  1. 赋值运算符的左操作数必须为左值
  2. 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置(这个别名就变成左值了),且可以取到该位置的地址
  3. 加了const 的右值引用不可被修改(同左值)

左值右值比较:

  1. 左值引用只能引用左值,不能引用右值,但是const左值引用既可引用左值,也可引用右值
  2. 右值引用只能右值,不能引用左值,但是右值引用可以move以后的左值,比如下面的代码:
int a=2;
int&& s=2;
//下面的语句会报错: message : 无法将左值绑定到右值引用
int&& s1=a;
int&& s2=s;
//使用move()将a转换为右值
int&& s3=std::move(a);

①错误使用

在没有右值引用的时候(C++98时不用左值引用),如何解决:
错误的思路:使用一个static的变量让每次调用的都是一个变量,然后每次使用的时候清空一下
问题:多线程下会有线程不安全的情况发生


错误使用右值引用:

string&& to_string(int val)
{
	string str;
	//...
	return move(str);
}
cout << test2::to_string(123).c_str() << endl;

造成野指针
C++进阶---C++11_第1张图片

②右值的应用场景

右值的应用场景:(以以前我们模拟实现的string为例)
对于只有左值引用形参的函数时,f(1)和f(a)均会匹配void f(const int& a),而当有右值引用形参的函数时f(1)会优先匹配void f(int&& a)

void f(const int& a)
{
	cout << "void f(const int& a)" << endl;
}

void f(int&& a)
{
	cout << "void f(int&& a)" << endl;
}
int a = 10;
f(1);
f(a);

在这里插入图片描述
移动构造

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	//this->swap(s);
	swap(s);
}

C++进阶---C++11_第2张图片
注意:出了作用域,如果返回对象不在了,不能使用引用返回(左值引用和右值引用都不可以)

移动构造

//移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动拷贝" << endl;
	//this->swap(s);
	swap(s);
}

直接接收打印:

//拷贝构造
string(const string& s)
	:_str(nullptr)
{
	cout << "string(const string& s) -- 拷贝构造" << endl;
	string tmp(s._str);
	this->swap(tmp);
}
string to_string(int val)
{
	string str;
	while (val)
	{
		int i = val % 10;
		str += ('0' + i);
		val /= 10;
	}
	reverse(str.begin(), str.end());
	return str;
}
int main()
{
	cout << test2::to_string(123).str.c_str() << endl;
	return 0;
}

先接收再打印:

int main()
{
	test2::string s = test2::to_string(123);
	cout << s.c_str() << endl;
	return 0;
}

只有拷贝构造:
C++进阶---C++11_第3张图片
分析:

  1. 第一次拷贝构造,给str产生的临时变量
  2. 第二次拷贝构造给s
  3. 编译器优化为一次拷贝构造

在这里插入图片描述


有了移动构造:
C++进阶---C++11_第4张图片
分析:

  1. 第一次将to_string的返回值str是左值(编译器优化识别为将亡值)(拷贝构造)
  2. 第二次将赋给str返回时产生的临时变量识是将亡值 (移动构造)
  3. 优化为一次,跳过临时变量,直接将堆上的资源转移给s

在这里插入图片描述

移动赋值

string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动赋值" << endl;
	swap(s);
	return *this;
}

先定义一个string类型的s,再赋值给s

int main()
{
	test2::string s;
	s = test2::to_string(123);
	cout << s.c_str() << endl;
	return 0;
}

分析:

  1. 对于已经创建的类s,如果有只有拷贝构造和拷贝赋值,就会调用两次拷贝构造+一次拷贝赋值(优化为一次拷贝构造+一次拷贝赋值)(传值是进行深拷贝,接收参数的时候就拷贝构造了一个string s对象,再用自定义的swap函数对成员三个成员变量进行交换
    C++进阶---C++11_第5张图片
  2. 如果是移动赋值+拷贝构造
    C++进阶---C++11_第6张图片
  3. 如果是拷贝赋值+移动构造
    C++进阶---C++11_第7张图片
  4. 如果是移动赋值+移动构造
    C++进阶---C++11_第8张图片

③move的使用场景

注意:如果仅仅是定义右值引用,那么对象本身不会被移走,在作为参数时会发生对象被移走
在这里插入图片描述
在这里插入图片描述
使用场景1:配合push_back使用降低开销


使用场景2:初始化列表move

class Person
{
public:
	Person(string name, string sex, int age)
		: _name(name)
		, _sex(sex)
		, _age(age)
	{}
	void swap(Person& s)
	{
		::swap(_name, s._name);
		::swap(_sex, s._sex);
		::swap(_age, s._age);
	}
Person(Person&& p)
		:_name(p._name)
		, _sex(p._sex)
		, _age(p._age)
	{
		//swap(p);
	}
#if 0
	Person(Person&& p)
		: _name("")
		, _sex("")
		, _age(p._age)
	{
		swap(p);
	}
#else
	Person(Person&& p)
	: _name(move(p._name))
	, _sex(move(p._sex))
	, _age(p._age)
	{}
#endif
private:
	string _name;
	string _sex;
	int _age;
};
Person GetTempPerson()
{
	Person s("prety", "male", 18);
	return s;
}
int main()
{
	Person s(GetTempPerson());
	return 0;
}
  1. #if 0
    C++进阶---C++11_第9张图片
  2. #if 1
    在这里插入图片描述

④完美转发

引用折叠规则:

  1. & + & -> &
  2. & + && -> &
  3. && + & -> &
  4. && + && -> &&

只要有&就是左值引用
详细请参考
C++11完美转发及实现方法详解
【C++11】引用折叠和完美转发


C++11中对于函数模板中使用右值引用语法定义的参数来说,它不再遵守右值引用形式的参数只能接收右值,不能接收左值的规则,它既可以接收右值,也可以接收左值
函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)

void Fun(int& x) { cout << "lvalue ref" << endl; }
void Fun(int&& x) { cout << "rvalue ref" << endl; }
void Fun(const int& x) { cout << "const lvalue ref" << endl; }
void Fun(const int&& x) { cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T&& t) {
	 //Func(t);       //always lvalue ref
	 Fun(std::forward<T>(t)); // can be lvalue or rvalue ref
}
int main()
{
	PerfectForward(10); // rvalue ref
	int a;
	PerfectForward(a); // lvalue ref
	PerfectForward(std::move(a)); // rvalue ref
	const int b = 8;
	PerfectForward(b); // const lvalue ref
	PerfectForward(std::move(b)); // const rvalue ref
	return 0;
}

C++进阶---C++11_第10张图片
直接使用Func(t); 会造成一直是左值引用,(参考上面int &&r=10的例子,右值引用是左值)

⑤总结

总结:

  1. C++11以后增加了两个默认成员函数:默认移动构造,默认移动赋值
  1. 左值引用通常在传参和传返回值的过程减少拷贝,一般直接是利用左值引用的语法特性,别名特性,较少拷贝
  2. 右值引用,一般是利用深拷贝的类,需要实现移动构造和移动赋值,利用移动构造和移动赋值在传参和传返回值过程中间接转移资源,减少拷贝
  3. 在STL中所有的容器都增加了移动构造和移动赋值
    C++进阶---C++11_第11张图片
  4. 这些容器在操作中添加了右值引用版本
    在这里插入图片描述

注意:

  1. 如果你没有自己实现移动构造函数且没有实现析构函数.拷贝构造、拷贝赋值重载都没有实现。那么编译器会自动生成一个默认移动构造。 默认生成的移动构造函数.对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造
  2. 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值 默认生成的移动移动赋值)时于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值 (默认移动赋伯跟上面移动构造完全类似)
  3. 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
    C++进阶---C++11_第12张图片

8)类新特性

类成员变量初始化

注意1:

class test{
private:
	std::string _name = "jads";//提供缺省值,这里不是初始化
	int _age;
};

注意1:
static 成员不能在类里初始化
但是static const成员可以在类里初始化

class test{
private:
	static int _si;
	static const int _constsi=-1;//注意这里不是缺省值
};
int test::_s1=0;

default

defaulted 函数特性仅用于类的特殊成员函数,且该特殊成员函数没有缺省参数
前面说过,只要自己实现了析构函数.拷贝构造、拷贝赋值重载,都不会生成默认的移动构造或移动赋值,但我们可以强制default让系统自己生成默认移动构造
注意: 在一个类中如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
C++进阶---C++11_第13张图片

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	//Person(const Person& p) = default;
	//Person(Person&& p) = default;
	~Person(){}
private:
	test2::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

delete,override,final略,参考之前的博客


9)可变参数模板(MARK一下)

声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数

template <class ...Args>
void ShowList(Args... args)
{}
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));

我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数
汇编代码:自动推导出来
C++进阶---C++11_第14张图片
由于参数类型不一样,不可以直接使用for循环拿到所有的参数


参考:第21课 可变参数模板(2)_展开参数包
参考:泛化之美–C++11可变模版参数的妙用
方法1:
使用value匹配第一个参数, Args… args参数包接受剩余的参数
每次递归调用ShowList(args…);每次读取一个参数

// 递归终止函数1
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;
}
// 递归终止函数2
void ShowList()
{
	cout << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}

方法2:逗号表达式展开参数包

template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... };
cout << endl;
}

{(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), ... )


使用initializer_list+lambda表达式

[&]{std::initializer_list<int>{(cout << args << endl,0)...};}();

emplace接口

template <class... Args>
 void emplace_back (Args&&... args);

emplace系列的接口,支持模板的可变参数,并且万能引用
emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
例如:

std::list< std::pair<int, char> > mylist;
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.emplace_back(10, 'a');
mylist.push_back({ 50, 'e' });

注意:不支持mylist.emplace_back({ 50, 'e' });写法


  • insertpush_back之所以慢的原因在与创建临时对象时,需要申请内存空间, 申请内存空间一向是耗时很严重的操作;之后再通过拷贝构造函数把创建的临时对象复制到vector空间中,期间的复制操作也是需要CPU时间的;
  • emplace_back之所以快的其原因是直接在vector中已有的空间上, 调用了构造函数, 节省了临时对象的内存空间申请以及移动构造函数的复制操作

emplace_back和push_back效率问题:

  1. 以自己实现的string为例,传左值的情况没有区别
    在这里插入图片描述
  2. 以自己实现的string为例,传右值的情况
  1. test2::string实现了移动构造
    在这里插入图片描述
    由于移动构造开销很小,所以本质区别不大
  2. test2::string未实现移动构造
    C++进阶---C++11_第15张图片
    emplace_back效率明显高于push_back

10)lambda表达式

仿函数使用场景:map set模拟实现取得key值,参考:C++进阶—Map和Set使用及模拟实现

格式:[capture-list] (parameters) mutable -> return-type { statement}
lambda表达式各部分

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
  1. [ var]:表示值传递方式捕捉变量var
  2. [=]:表示值传递方式捕获所有父作用域中的变量(成员函数中包括this)
  3. [&var]:表示引用传递捕捉变量var
  4. [&]:表示引用传递捕捉所有父作用域中的变量(成员函数中包括this)
  1. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  2. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
  3. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  4. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

注意:

  1. 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空
  2. 父作用域指包含lambda函数的语句块
  3. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
  4. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  5. 在块作用域以外的lambda函数捕捉列表必须为空
  6. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
  7. lambda表达式之间不能相互赋值,即使看起来类型相同
    C++进阶---C++11_第16张图片
  8. 允许使用一个lambda表达式拷贝构造一个新的副本,可以将lambda表达式赋值给相同类型的函数指针
  9. lambda表达式不应出现在未经评估的上下文中(例如decltype和sizeof等)
    因为每个lambda派生的闭包对象可以具有完全不同的类型,毕竟它们就像匿名函数一样
  • lambda对象没有默认构造函数,无法构造 lambda 实例

使用:
lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量

//省略返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;

仿函数和lambda比较:

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);
	// lamber
	auto r2 = [=](double monty, int year)->double {return monty * rate * year;
	};
	r2(10000, 2);
	return 0;
}

查看反汇编:
仿函数
C++进阶---C++11_第17张图片
lambda:C++进阶---C++11_第18张图片
注意:定义一个lambda表达式对我们而言类型是匿名的,实际对编译器不是匿名的,编译器会将他转换为一个仿函数,这里的lambda_+通过底层算法生成的uuid组成的类名字(uuid标识唯一)


这里也就解释了lambda表达式不能互相赋值的原因每个类名字不同


所以我们定义lambda表达式是这样的:

auto r2 = [=](double monty, int year)->double {return monty * rate * year;}

11)包装器

function ( MARK )

引入:
ret = func(x);
func可能是函数名,函数指针,函数对象(仿函数对象),lambda 这些可调用类型

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;
}

定义一个useF函数模板,用来封装普通函数,仿函数,lambda表达式,同时在其中定义一个static int变量count,每次打印count就输出一次他的地址
C++进阶---C++11_第19张图片
每个count的地址不同,导致模板的效率低下


function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器

template <class T> function;     // undefined
template <class Ret, class... Args> class function<Ret(Args...)>;

将上面的代码添加包装器封装:

std::function<double(double)> f1(f);
//std::function f1 = f;
// 包装仿函数对象
std::function<double(double)> f2 = Functor();
// 包装lambda
std::function<double(double)> f3 = [](double d)->double{ return d / 4; };
useF(f1, 10.11);
useF(f2, 10.11);
useF(f3, 10.11);

在这里插入图片描述
包装器还可以绑定类成员函数(MARK)

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return a + b;
	}
};
// 包装类的静态成员函数
std::function<int(int, int)> f4 = Plus::plusi;
cout << f4(1, 2) << endl;
// 包装类的非静态成员函数
std::function<double(Plus, double, double)> f5 = &Plus::plusd;
cout << f5(Plus(), 1.1, 2.2) << endl;

C++进阶---C++11_第20张图片
对于类的静态成员函数,对于类的非静态成员函数需要加上取地址符号,同时要传一个类进去,注意不能传地址,this指针是编译器自己进行传递的

应用题目

应用场景:leetcode题目:逆波兰表达式求值

blind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作


头文件
语法:

  1. 无返回值
    template
    /* unspecified */ bind (Fn&& fn, Args&&... args);
  2. 有返回值
    template
    /* unspecified */ bind (Fn&& fn, Args&&... args);

注意:

  1. Args&&… arg可以包含形如_n的占位符,比如_1表示fn的第一个参数,_2表示fn的第二个参数,以placeholder命名空间为例,_1,_2可以更换位置,但_1永远表示的第一个参数,_2表示的第二个参数,同时,占位符可以只有一个或多个,例子见下面代码

代码如下:

int Plus(int a, int b)
{
	return a + b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};
// 使用bind进行优化
// 需要绑定的参数,直接绑定值,不需要绑定的参数给 placeholders::_1 、  placeholders::_2.... 进行占位
std::function<int(int, int)> func4 = std::bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << func4(1, 3) << endl;
// 调整参数顺序
std::function<int(int, int)> func5 = std::bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << func5(1, 3) << endl;

C++进阶---C++11_第21张图片

12)线程库

头文件thread

thread

构造:
thread (const thread&) = delete;
thread& operator= (const thread&) = delete;


thread (thread&& x) noexcept;
thread& operator= (thread&& rhs) noexcept;


template
explicit thread (Fn&& fn, Args&&... args);


注意不允许拷贝构造和拷贝赋值,但是允许移动构造和移动赋值


当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

  • 函数指针
  • lambda表达式
  • 函数对象

例如lambda表达式:

int n = 0;
cin >> n;
vector<thread> works(n);
mutex mtx;
int x = 0;
for (auto& thd : works)
{
	thd = thread([&mtx,&x]() {
		for (int i = 0; i < 100000; i++)
		{
			mtx.lock();
			//cout << this_thread::get_id() << endl;
			x++;
			mtx.unlock();
		}
	});
}
for (auto& thd : works)
{
	thd.join();
}
cout << x;

注意:++操作不是原子的
C++进阶---C++11_第22张图片
需要加锁,且锁加在循环外面更好,不会频繁的切换锁
clock函数验证:

size_t begin = clock();
mtx.lock();
for (int i = 0; i < 100000; i++)
{
	//mtx.lock();
	x++;
	//mtx.unlock();
}
mtx.unlock();
size_t end = clock();
cout << end - begin << endl;

C++进阶---C++11_第23张图片
C++进阶---C++11_第24张图片

其他:

  • thread::get_id()获取指定线程的id
  • thread::join:相当于Linux pthread库中的waitpid

注意:线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参

this_thread

thread头文件包含了一个this_thread的命名空间,直接调用get_id()等函数

  • this_thread::get_id()获取当前线程的id

头文件mutex

mutex

构造

  • mutex (const mutex&) = delete;防拷贝

lock

  • 获取锁,且失败会阻塞

unlock

  • 释放锁

try_lock

  • 尝试获取锁,且若未成功获取不阻塞

lock_guard和unique_lock

题目:两个线程交替打印奇偶数
思路1:创建一把锁,两个线程竞争这把锁

int main()
{
	mutex mtx;
	int i = 1;
	thread t1([&i, &mtx](){
		while (i < 100)
		{
			mtx.lock();
			{
				cout << this_thread::get_id() << ":" << i << endl;
				i += 2;
			}
			mtx.unlock();
		}
	});
	int j = 2;
	thread t2([&j, &mtx](){
		while (j < 100)
		{
			mtx.lock();
			cout << this_thread::get_id() << ":" << j << endl;
			j += 2;
			mtx.unlock();
		}
	});
	t1.join();
	t2.join();
	return 0;
}
  • 极端情况1:当t1在某次释放锁资源时t1的时间片到了,进入等待队列,t2连续竞争到锁
    模拟
// 假设某次unlock以后,t1时间片到了,进入休眠排队
// 会导致t2连续获取到锁打印
if (i == 29)
	std::this_thread::sleep_for(std::chrono::milliseconds(3));
  • 极端思路2:主线程运行到一半时时间片到了,进入等待队列
    模拟
// 极端场景下:假设主线程执行到这里时间片用完了,进入休眠排队
// sleep模拟一下这个场景
std::this_thread::sleep_for(std::chrono::milliseconds(1));
  • 极端情况3:在t1线程执行中途时间片到了

所以使用锁不是线程安全的
推荐使用lock_guard和unique_lock
lock_guard:

  • 在构造时,互斥对象被调用线程锁定,而在销毁时,互斥对象被解锁。 它是最简单的锁,并且作为具有自动持续时间的对象特别有用,该持续时间一直持续到其上下文结束。 通过这种方式,保证了互斥对象在抛出异常的情况下被正确解锁
  • lock_guard类的底层结构:
template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT//成员变量就是锁,直接销毁
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;//私有成员变量为引用的经典场景
};

unique_lock:

  • 和lock_guard的区别是可以手动控制临时解锁加锁

使用lock_guard和unique_lock更改代码:
定义在作用域内,出作用域自动调用析构函数释放锁

{
	std::lock_guard<mutex> lock(mtx);
	//std::unique_lock lock(mtx);
	cout << this_thread::get_id() << ":" << j << endl;
	j += 2; 
}

recursive_mutex

递归互斥锁,递归程序中使用

timed_mutex

try_lock_for:

  • 尝试获取锁,最多阻塞指定时间

try_lock_until:

  • 尝试获取锁,最多阻塞到指定时间

头文件atomic

加锁有一个缺陷:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁
Windows和Linux都各自有相关的原子操作
Windows:

  • LONG InterlockedIncrement( LONG volatile* Addend);原子加一
  • LONG InterlockedDecrement( LONG volatile* Addend);原子减一
  • BOOL CAS(LONG *target, LONG new, LONG old);原子比较交换
  • LONG InterlockedExchange( LONG volatile* Target, LONG Value);原子写操作

Linux:

  • atomic tv= ATOMIC_ INIT(0); 定义一个原子变量,并初始化
  • atomic_dec(&v); 原子变量自减1
  • atomic_inc(&v); 原子变量自加1
  • atomic_read(&v); 读取原子变量的值
  • atomic_ dec and_ test(&v); 原子变量自减1,并与0比较,如果为0则返回true,否则返回false

C++11将原子操作封装为两个类 atomic 和 flag_atomic (跨平台,底层条件编译)
atomic:

  • 如bool对应atomic_bool,int对应atomic_int…
  • atomic类operator T重载了类型转换符,即可以使用atomic类模板,定义出需要的任意原子类型
    示例:C++进阶---C++11_第25张图片
    flag_atomic:

注意:

  • 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了

头文件condition_variable

condition_variable 对象不能被复制/移动(该类型的复制构造函数和赋值运算符都被删除)
wait

成员函数 功能
wait 等待直到通知
wait_for Wait for timeout or until notified
wait_untill Wait until notified or time point

template
void wait (unique_lock& lck, Predicate pred);
摘自:cplusplus

  • 在阻塞线程的那一刻,函数自动调用 lck.unlock(),允许其他被锁定的线程继续
  • 一旦被通知,该函数会解除阻塞并调用lck.lock(),使 lck 处于与调用函数时相同的状态,然后函数返回(注意最后一个互斥锁可能会在返回之前再次阻塞线程)
  • 通常,函数通过调用另一个线程中的成员 notify_one 或成员 notify_all 来通知唤醒,但是可能会存在伪唤醒情况
  • 因此,该功能的用户应确保满足其恢复条件, 如果指定了 pred,则该函数仅在 pred 返回 false 时阻塞,并且notify只能在线程变为 true 时解除阻塞
    实现类似于: while (!pred()) wait(lck);

notify:

成员函数 功能
notify_one 通知一个
notify_all 通知所有

void notify_one() noexcept;

  • 解除阻塞当前等待此条件的线程之一, 如果没有线程在等待,该函数什么也不做,如果在阻塞的线程数量大于1,则唤醒随机线程(线程间进行竞争)

重写题目:两个线程交替打印奇偶数

int main()
{
	mutex mtx;
	condition_variable cv;
	bool flag = true;
	int i = 1;
	// 打印偶数
	thread t2([&i, &mtx, &cv, &flag]() {
		while (i < 100)
		{
			std::unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return !flag; });
			cout << this_thread::get_id() << ":" << i << endl;
			i++;
			flag = true;
			cv.notify_one();
		}
	});
	// 打印奇数
	thread t1([&i, &mtx, &cv, &flag]() {
		while (i < 100)
		{
			std::unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]() {return flag; });
			cout << this_thread::get_id() << ":" << i << endl;
			i++;
			flag = false;
			cv.notify_one();
		}
	});
	t1.join();
	t2.join();
	return 0;
}

分析(这里必须要有pred参数):

  • 假如t1先竞争到锁,flag=true,return flag,pred为true,不wait,执行后续语句,假如t1先竞争到锁,flag=true,return !flag,pred为false, wait, 同时调用lck.unlock(),再次开始竞争,所以一定是t1奇数先打印
  • 为了继续打印偶数,而不是连续打印奇数,需要在t1中将flag置为false,同时通知处于cv这个条件变量下的其他处于阻塞的线程(这里只有t2),防止t1连续执行,同时!flag=true, t2得到锁后不wait,开始执行打印偶数部分,之后t2再将flag置为true,通知t1,如此往复
  • 当出现极端情况的时候:某一个线程时间片用完,处于休眠状态,而不是阻塞状态,notify_one()函数不起作用,但由于有flag,最近执行打印的线程由于pred==false一定不能连续执行,这就做到了交替打印

C++进阶---C++11_第26张图片

你可能感兴趣的:(C++,c++)