C++11常用语法-贰

1.完美转发

完美转发指的是在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数

完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转发给目标函数,而不产生额外的开销,就好像转发者不存在一样

函数模板在向其它函数传递自身形参时,如果相应实参是左值,它就应该被转发成左值;如果相应实参是右值,它就应该被转发为右值
这样做是为了保留在其它函数针对转发而来的参数的左右值属性进行不同的处理(比如:左值实施拷贝语义,右值实施移动语义)

结合代码分析:

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<class T>
void PerfecFword(T t){ fun(t); }//只负责传递t这个参数

int main()
{
	//10本身是一个右值
	PerfecFword(10);

	//n是一个左值
	int n = 10;
	PerfecFword(n);

	//n2是一个const类型的左值
	const int n2 = 10;
	PerfecFword(n2);
	
	//将n2转化成一个const右值
	PerfecFword(move(n2));
	system("pause");
	return 0;
}

在这里插入图片描述

进行修改后:
在这里插入图片描述

2.lambda表达式

2.1为什么需要lambda表达式

在一些场景之下,使代码的编写更加的简洁,如下所示,排序的时候可以省略仿函数:

void test()
{
	vector<int> arr = { 5, 2, 4, 7, 8, 95, 21, 456 };

	cout << "常规写法" << endl;
	sort(arr.begin(), arr.end(),greater<int>());

	for (auto&e : arr)
		cout << e << " ";
	cout << endl;

	vector<int> arr2= { 5, 2, 4, 7, 8, 95, 21, 456 };

	cout << "lambda 写法" << endl;
	sort(arr2.begin(), arr2.end(), [](const int &x, const int &y)->bool
	{
		return x > y;
	});

	for (auto&e : arr)
		cout << e << " ";
	cout << endl;
}

在这里插入图片描述

2.2格式

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

[capture-list] :
捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据 [] 来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

(parameters):
参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略

mutable:
默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:
返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{statement}:
函数体在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

其实lambda表达式就是一个匿名函数

2.3用法分析

1.mutable改变捕捉列表之中默认的变量属性(默认为conost)
在这里插入图片描述
2.定义和使用
在这里插入图片描述

2.4捕捉列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this),不能捕捉还未定义的变量
在这里插入图片描述

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
在这里插入图片描述

[this]:表示值传递方式捕捉当前的this指针

2.5注意点

1.父作用域指包含lambda函数的语句块,不一定是直接父类作用域,也可以是嵌套父域
在这里插入图片描述

2.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

3.捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

4.在块作用域以外的lambda函数捕捉列表不能写具体的变量,可以写=或者&捕捉全局变量
在这里插入图片描述

5.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域的非局部变量都会导致编译报错(可以捕捉全局变量)。
在这里插入图片描述
6. lambda表达式之间不能相互赋值,即使看起来类型相同,但是可以拷贝
在这里插入图片描述
7.C++实现lambda表达式,实际是创建一个仿函数类
在这里插入图片描述

3.线程库

3.1线程库的介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。

C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。

函数名功能
thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn,
args1, args2,
…)
构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

3.2使用时的注意点

1.线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

2.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

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

4.thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行

5.可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:

采用无参构造函数构造的线程对象(没有提供线程函数,即这个线程本身就是无效的)
线程对象的状态已经转移给其他线程对象(资源被转移)
线程已经调用jion或者detach结束(线程已结束,或者已经分离)

3.3线程函数的参数

1.线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
在这里插入图片描述
在这里插入图片描述

注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
在这里插入图片描述

3.4线程资源回收

启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?thread库给我们两种选择

3.4.1join()方式

主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。
由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系了,因此一个线程对象只能使用一次join(),否则程序会崩溃

因此:采用jion()方式结束线程时,jion()的调用位置非常关键(比如在return后面等待,就会出问题)为了避免该问题,可以采用RAII的方式对线程对象进行封装

RAII :利用对象声明周期来管理线程资源,保证线程资源正常销毁
资源获取立即初始化
在构造函数中初始化资源
在析构函数中销毁资源

//RAII 资源获取立即初始化
//在构造函数中初始化资源
//在析构函数中销毁资源
class ThreadMange
{
public:
	
	ThreadMange(thread &t)//初始化
		:_thread(t)
	{}

	~ThreadMange()
	{
		if (_thread.joinable())//判断线程是否还在
			_thread.join();
	}


private:
	thread & _thread;
};
void tfunc1()
{
	while (1)
	{
		cout << "tfunc()" << endl;
		Sleep(20);
	}
}

void tfunc2(int a)
{
	while (1)
	{
		cout << "tfunc2(int)" << endl;
		Sleep(20);
	}
}

void testThread()
{
	//线程对象创建,并传入线程函数
	thread t1(tfunc1);
	thread t2(tfunc2,10);

	//线程等待
	//t1.join();
	//t2.join();

	//线程自动管理
	ThreadMange tm1(t1);
	ThreadMange tm2(t2);
}

3.4.2 detach()方式

该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收

3.5并发和并行的区别

并发:一段时间内,多个对象执行多个任务,多对多的一种状态

并行:一段时间内,一个对象执行多个任务,一对多的状态

3.6原子性操作库

3.6.1 atomic

在这里插入图片描述

当对临界资源进行操作时,容易造成线程安全问题。C++98中传统的解决方式就是对临界资源进行加锁处理.但是加锁有一个缺陷,没有竞争到锁的线程,会陷入阻塞状态,影响程序的效率

因此C++11中引入了原子操作,所谓原子操作:指定不会被打断,本身就是线程安全的操作

程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问.程序员可以使用atomic类模板,定义出需要的任意原子类型

atomic < T > :把T类型的数据封装成原子操作

为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了

atomic<int> sum(0);//通过atomic将sum定义成原子类型

void func(int number)
{
	for (int i = 0; i < number; i++)
	{
		sum++;//此时两个线程对其进行操作,就不会出现线程安全问题了
	}
}

int main()
{
	int number;
	cin >> number;


	thread t1(func, number);
	thread t2(func, number);

	t1.join();
	t2.join();

	cout << sum << endl;

	system("pause");
	return 0;
}

3.6.2锁

如果是变量,可以用atomic将其构造成原子类型,如果是一段代码,需要保证其安全性就需要用到锁了

1.lock、unlock:

线程函数调用lock()时,可能会发生以下三种情况:

如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
如果当前互斥量被当前调用线程锁住,则会产生死锁(同一线程,不可以连续调用两次锁)

线程函数调用try_lock()时,可能会发生以下三种情况:

如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉(非阻塞式)
如果当前互斥量被当前调用线程锁住,则会产生死锁

int sum = 0;
mutex Lock;//定义锁

void func(int number)
{
	for (int i = 0; i < number; i++)
	{
		Lock.lock();//阻塞加锁:如果没有抢到锁,陷入阻塞状态
	
		//Lock.try_lock(); 非阻塞加锁:如果每有抢到锁,直接返回加锁失败
		
		//不可多次加锁,如果当前线程拥有该锁,再次进行加锁,会导致死锁状态
		sum++;
		Lock.unlock();//解锁
	}
}

2.recursive_mutex:
允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,ecursive_mutex 的特性和mutex 大致相同。

int sum = 0;
recursive_mutex Lock;//定义递归锁
void func(int number)
{
for (int i = 0; i < number; i++)
{
Lock.lock();//可以多次调用锁,同时也需要多次解锁
Lock.lock();
Lock.lock();
sum++;
Lock.unlock();//解锁
Lock.unlock();//解锁
Lock.unlock();//解锁
}
}

3.timed_mutex:
比 mutex 多了两个成员函数,try_lock_for(),try_lock_until()

try_lock_for()
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

try_lock_until()
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

int sum = 0;

timed_mutex Lock;
chrono::milliseconds timeout(100);//100毫秒
void func(int number)
{
	for (int i = 0; i < number; i++)
	{
		
		//Lock.try_lock_for(timeout);//接收一个时间范围
		Lock.try_lock_until(chrono::steady_clock::now() + timeout);//接收一个时间点
		sum++;
		Lock.unlock();
	}
}

4.recursive_timed_mutex
是2、3两种锁的结合

5.lock_guard(守卫锁):
加锁和解锁的过程,通过类的构造和析构来自动控制加锁和释放锁(RAII思想)

lock_gurad 是 C++11 中定义的模板类。
定义如下:

template<class _Mutex>
class lock_guard
{
public:

	// 构造函数加锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}

	//通过析构函数释放锁
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}

	//防拷贝和防赋值
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;

private:
	_Mutex& _MyMutex;
};

使用方法:

int sum = 0;
mutex Lock;
void func(int number)
{
	for (int i = 0; i < number; i++)
	{
		lock_guard<mutex> lg(Lock);
		sum++;
	}
}

通过上述代码可以看到,lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。
lock_guard的缺陷: 太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock。

6. unique_lock
与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所管理的互斥量的指针)。

总结:
在这里插入图片描述

4. 条件变量

condition_variable

等待:
需要注意的是,传入wait的锁不能是lock_guard,这是因为lock_guard没有解锁接口

unconditional (1)	
void wait (unique_lock<mutex>& lck);

predicate (2)	
template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred); -> Predicate pred返回false进行等待,返回true不进入等待

唤醒:

Notify one -> 唤醒一个
notify_all -> 唤醒一批

测试:要求两个线程交替打印1-200的数字

bool flag = true;
	int i = 1;
	int j = 2;
	int num = 200;
	mutex mtx;
	condition_variable cond;

	thread t1([&]
	{
		while (i <= 200)
		{
			unique_lock<mutex> lock(mtx);
			cond.wait(lock, [&]()->bool{return flag; });//不能使用guard_lock,因为guard_lock不能自己解锁
			cout << i << endl;
			i += 2;
			flag = false;
			cond.notify_one();
		}
	}
	);

	thread t2([&]
	{
		while (j <= 200)
		{
			unique_lock<mutex> lock(mtx);
			cond.wait(lock, [&]()->bool{return !flag; });//不能使用guard_lock,因为guard_lock不能自己解锁
			cout << j << endl;
			j += 2;
			flag = true;
			cond.notify_one();
		}
	}
		);

	t1.join();
	t2.join();
  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值