文章目录
初始化列表
对象可以用大括号来初始化
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表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
- 注意: 在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。