0
点赞
收藏
分享

微信扫一扫

C++11一些新功能

柠檬的那个酸_2333 2022-05-03 阅读 51

文章目录

初始化列表

对象可以用大括号来初始化

int main()
{
	int x = { 10 };
	vector<vector<int>> v = { {1,2,3}, {2,3,4},{3,4,5} };
}

decltype

decltype是根据表达式的实际类型推演出定义变量时所用的类型
在这里插入图片描述

它推演出lambda表达式的类型是上面lamda+uuid。

default & delete

比如我们写了一个需要传参的构造函数之后,还想要系统自动编写的默认构造函数,这时候就可以用上default。

正常来讲我们实现了foo(int a),编译器是不会帮我们再实现一个默认构造函数的。但是我们确实需要这个默认构造函数,可以加上default。

class foo
{
	foo(int a)
	{
		;
	}

	foo() = default;
};

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

两种写法等价:

c++11
class foo
{
	foo() = delete;
	foo(const foo&) = delete;
	foo& operator=(const foo&) = delete;
};


c++98
class foo
{
private:
	foo();
	foo(const foo&);
	foo& operator=(const foo&);
};

右值引用

参考这篇文章
右值引用

lambda表达式

其实这三个东西都可以看成一个东西,是历史演变的过程。
函数指针太难写了,于是c++98有了仿函数。仿函数又有点不好用,又出现了lambda表达式。

lambda表达式书写格式:

lambda表达式各部分说明

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  2. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  5. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
  6. 注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分, 而捕捉列表和函数体可以为空。
    因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

关于捕获列表有以下写法:
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

举个例子:
用lambda表达式写swap函数,有下面几种写法

int a = 10, b = 20;
auto ret1 = [&] {int t = a; a = b; b = t; };
auto ret2 = [&a, &b] {int t = a; a = b; b = t; };
auto ret3 = [](int& a, int& b) {int t = a; a = b; b = t; };

ret1();
ret2();
ret3(a, b);

第一种写法就是说:ret1以上的且在此作用域的所有变量都被引用传递了。也就是说上面作用域内所有变量此lambda表达式都可以用,且是引用类型。

第二种写法是:捕获了a的引用和b的引用,因此ret2可以使用这两个变量

第三种写法是:没有捕获任何值,以传参的方式传入a的引用和b的引用。

lambda的原理

刚刚说过lambda是仿函数演变来的,因此lambda的原理还是调用仿函数。

看一下汇编代码可以发现,它调用的还是operator(),和仿函数一样。
在这里插入图片描述

lambda表达式的类型

lambda表达式在使用的时候可以认为是没有类型的,但是我们还是可以了解一下。

这就是lamba表达式的类型。

在这里插入图片描述
uuid是可以表示资源在系统唯一的一个编码
在这里插入图片描述

可变参数包

可变参数包支持可以传入多个参数
Args&&… args代表的就是万能引用+参数包

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

这里讲一下为什么人们说emplace_back会比push_back高效。原因和可变参数包有关系。

这是vector的push_back的函数声明,一个传引用,一个是传右值引用。
在这里插入图片描述
写个代码分析一下:


class Person
{
public:
	Person() = default;
	Person(string _name, string _sex, int _age)
	{
		name = _name;
		sex = _sex;
		age = _age;
	}
	Person(const Person& p)
	{
		cout << "Person(const Person& p) " << "深拷贝" << endl;
		name = p.name;
		sex = p.sex;
		age = p.age;
	}

	Person(Person&& p)
	{
		cout << "Person(Person&& p) 移动构造" << endl;
		name = move(p.name);
		sex = move(p.sex);
		age = p.age;
	}
	string name;
	string sex;
	int age;
};


vector<Person> v;
v.reserve(10);
Person p("lisi", "male", 18);
v.push_back({ "zhangsan", "male", 18 });//右值引用
v.push_back(p);//左值引用
v.emplace_back("lisi", "male", 18);

对于第一个右值引用的写法,需要先构建一个匿名Person对象(一次深拷贝),然后再进行移动构造。

对于第二个左值引用的写法,需要先构建一个匿名对象Person(一次深拷贝),然后再进行拷贝构造(两次深拷贝)

对于第三种的emplace_back,由于支持可变参数,因此我们可以直接传参数而不是传对象,这里就少了一次构建匿名对象的开销。因此对于第三种写法,只需要一次构造函数即可。

在这里插入图片描述
ps:vector要先reserve一下,否则插入数据vector增容的时候需要深拷贝数据。

如果你写成下面这种,即使你用emplace_back效率也没有得到提高。因此从你写emplace_back也能看出你是否了解c++

emplace_back({xxx});//传匿名对象,右值
emplace_back(p);//传对象,左值

包装器

下面这段代码就是包装器。语法结构如下:

function<int(int, int)> f1 = [](int a, int b) {return a + b; };

个人认为包装器是一个很好用的东西。包装器可以为可调用对象包上统一的外壳,使它们都变成同一种类型:包装器类型。(其实就是C语言里面的函数指针数组, 但是函数指针数组实在太难写了,这个好用)

比如说:如果要实现加减乘除的计算器。写四个if-elseif-else是可以的,但是不够优美。

可以把lambda包装成包装器,然后放进map里面。这么写非常优美!!!
在这里插入图片描述
小感想:
如果没有包装器,我们想想,函数指针又难写,仿函数还要实例化出对象才能调用,lambda表达式又没有类型。怎么想我们都写不出那么优美的代码。

C++线程库

c++11之前在linux下写多线程是用POSIX线程库,在windows下用的是windows自己的库。C++11在语言层面对这两个库封装了一下,使代码有了可移植性。
在这里插入图片描述

thread

thread用于创建和使用线程,thread里面有thread类和this_thread命名空间。thread需要创建对象才可以使用,this_thread是命名空间,所有不需要创建对象。

其实这个线程库和POSIX线程库代码写法差别挺大的,只是原理相同而已。
在这里插入图片描述

在这里插入图片描述

constructor

在这里插入图片描述
上面这四条都挺重要的。
第一条说的是线程可以创建一个空对象,它什么也不干。

第二条说的是可以给线程传万能引用的类型,一般都是传可调用对象(函数指针,仿函数,lambda, function包装器),后面是参数包的可变参数,一般用来传参数线程执行逻辑的参数。

第三条说的是线程拷贝构造函数被禁用了

第四条说的是线程有一个移动赋值。

一般线程在c++可以这么创建:

vector<thread> v;
v.resize(3);  创建三个线程,它们啥也不干,也就是第一个构造函数
for (auto& td : v)
{
	给之前构建好的线程分配任务啦
	创建一个匿名线程对象,第一个参数传的时可调用对象,叫线程打印一下自己的id
	然后调用了移动赋值,move资源到td里面
	td = thread([&] {cout << td.get_id() << endl; });
}

让主线程等待所有其他线程
for (auto& td : v)
{
	td.join();
}

join和detach

join用于主线程阻塞等待其他线程退出,和POSIX pthread一样。

detach是让线程分离的,detach之后的线程变成后台线程。其所有权和控制权将会交给c++运行库。同时,C++运行库保证,当线程退出时,其相关资源的能够正确的回收。 和POSIX pthread也差不多。唯一差别就是c++11的detach需要用对象去调用。

但是有一点挺不同的,c++11join和detach两个只能用其一,不能同时用。POSIX 是可以同时使用的。

举个例子:
可以让主线程等待所有的线程退出。用join

vector<thread> v;
v.resize(3); 
for (auto& td : v)
{
	td = thread(f);
}
for (auto& td : v)
{
	td.join();
}

用detach就这么写:

vector<thread> v;
v.resize(3); 
for (auto& td : v)
{
	td = thread(f);
	td.detach();
}
这个while只是我想看一下线程是否运行成功才加的,因为不加主线程退出太快了
while (1) {}

如果要选择使用detach,就必须在执行线程后立刻detach。否则有可能线程对象被析构了,但是此时线程还没有被执行完,导致抛异常。
在这里插入图片描述

原因是:C++在析构thread对象的时候,如果发现thread是joinable的,它就会抛异常,也就是上图。

看了一些文章,尽量用join,join比较好控制。

atomic

原子库,这个库很牛。它可以让一些不是原子的操作变成原子的。比如加和减,这样在多线程操作临界资源的时候,不需要加锁。

在这里插入图片描述

由于++不是原子的,直接加会有线程安全问题

int sum;
void f()
{
	for (int i = 0; i < 100000; i++)
		sum++;
}

thread td1(f);
thread td2(f);
td1.join();
td2.join();
cout << sum;

本来应该sum = 200000的,但是由于线程安全,导致答案错误。
在这里插入图片描述
可以进行加锁操作,但是效率比较低。

把int sum改成atomic< int > sum即可,这样sum++就是原子的了。

mutex

mutex是c++11有关锁的头文件
在这里插入图片描述
普通加锁就不说了。说一下两个很重要的锁

lock_guard和unique_lock

万一我们加上锁之后忘记解锁了,程序就崩了。
为了解决这个问题,有了lock_guard模板.
在这里插入图片描述

lock_guard在创建对象时自动加锁,销毁对象时自动解锁。

有个缺陷:不能手动解锁,很死板。而且也不能用于条件变量当中,我们知道:条件变量等待时要传入一把锁,等待的时候要把这把锁释放掉的。如果使用了lock_guard模板,wait的时候无法释放锁,直接造成死锁。

mutex mtx;//只是声明对象
lock_guard<mutex> lg(mtx);

于是有了unique_lock.unique_lock就是lock_guard的基础上,允许用户或者其他人加锁和解锁。

mutex mtx;
unique_lock<mutex> lock(mtx);

condition_variable

在这里插入图片描述

wait等价于pthread_cond_wait
notify_one等价于pthread_cond_signal

wait接口:
在这里插入图片描述
主要用第二个写法,predicate是以…为依据的意思。

用代码来讲:

while (!pred()) wait(lck)

notify_one接口:就是唤醒的意思,让wait的线程醒来看一下自己的依据成立了没有**(也就是条件变量成立了没有)**
在这里插入图片描述

用一道题来练习条件变量如何使用:
让两个线程依次往屏幕打印,线程1打印奇数,线程2打印偶数。

代码如下:测试结果正确

void f1(mutex& mtx, bool& flag, condition_variable& cv)
{
	for (int i = 0; i <= 100; i += 2)
	{
		unique_lock<mutex> lock(mtx);
		cv.wait(lock, [&flag]()->bool {return flag; });
		cout << this_thread::get_id() << " :  " << i << endl;
		flag = false;
		cv.notify_one();
	}
}

void f2(mutex& mtx, bool& flag, condition_variable& cv)
{
	for (int i = 1; i <= 99; i += 2)
	{
		unique_lock<mutex> lock(mtx);
		cv.wait(lock, [&flag]()->bool {return !flag; });
		cout << this_thread::get_id() << " : " << i << endl;
		flag = true;
		cv.notify_one();
	}
}

int main()
{
	condition_variable cv;
	mutex mtx;
	bool flag = true;
	thread t1(f1, ref(mtx), ref(flag), ref(cv));
	thread t2(f2, ref(mtx), ref(flag), ref(cv));
	t1.join();
	t2.join();
}

上面那种写法太长了,而且对于thread传参传引用的时候,还必须加上ref(),否则它不认识。

可以用lambda表达式来传参,简单很多。

int main()
{
	int n = 100;
	mutex mtx;
	condition_variable cv;
	bool flag = true;

	// 偶数
	thread t2([&](){
		int j = 2;
		for (; j < n;)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]()->bool{return !flag; });  // false
			cout << j << endl;
			j += 2;
			flag = true;
			cv.notify_one();
		}
	});

	// 奇数
	thread t1([&](){
		int i = 1;
		for (; i < n;)
		{
			unique_lock<mutex> lock(mtx);
			cv.wait(lock, [&flag]()->bool{return flag; }); // true
			cout << i << endl;
			i += 2;
			flag = false;
			cv.notify_one();
		}
	});


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

	return 0;
}

写这个条件变量记住一点就好:
依据为true我才执行,依据不为true我就一直wait到依据为true为止。

由于我想先打印奇数再打印偶数。因此打印奇数的线程一开始依据应该是true,打印偶数的依据应该是false。

举报

相关推荐

0 条评论