C++11(中)
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【C++的学习】
📝📝本篇内容:可变参数模板;可变参数使用场景;lambda表达式;lambda表达式使用说明;函数对象与lambda表达式;线程库;thread;atomic原子库操作;mutex;mutex的种类;lock_guard;unique_lock
⬆⬆⬆⬆上一篇:C++11(上)
💖💖作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-
1.可变参数模板
C++11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板
#include <iostream>
using namespace std;
template<class ...Args>
void ShowList(Args ...args)
{
//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
}
int main()
{
ShowList();
return 0;
}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点
下面举一个栗子
#include <iostream>
using namespace std;
void ShowList()
{
//递归终止函数
}
template<class T,class ...Args>
void ShowList(T value,Args... args)
{
cout << value << endl;
//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
ShowList(args...);
}
int main()
{
ShowList(1,2,3,4,5,6,7,8,9,10);
return 0;
}
还可以使用逗号表达式来展开参数包
#include <iostream>
#include <string>
using namespace std;
template<class T>
void Print(T value)
{
cout << value << endl;
}
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = {(Print(args),0)...};
}
int main()
{
ShowList(1,string("hello world"),3, 4.1, 5, 6, 7, 8, 9, 10);
return 0;
}
我们的的数组元素其实会初始化,{(Print(args), 0)…}将会展开成((Print(arg1),0),(Print(arg2),0),(Print(arg3),0), etc… ),在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包
1.1.使用场景
在我们的很多STL容器中,也使用到了可变参数模板
上面两个就是最简单的例子,我们可以看到emplace_back函数就是使用了可变参数包,同时他还是万能引用
那它和我们的push_back有啥区别呢?
让我们来看一下它的底层先,我们在VS2022下演示
我们选中emplace_back,然后按下F12,此时会跳转到下图
此时再跳转到_Emplace_one_at_back
此时再进行跳转到_Emplace_reallocate
我们可以看见construct,继续跳转
到底了,无法再继续跳转了,其实它的底层就是使用了定位new,如果是不知道什么是定位new可以看一下我这篇博客☞C++内存管理
不知道大家有没有看过我们侯捷大佬的《STL源码剖析》,在这本书的第二章的空间配置器中就讲到了construct的底层,我可以给大家看一下,具体的编译器和具体的版本实现会不一样,但是大差不差
接下来我们回归主题,来研究一下,emplace_back有啥区别,我们首先用到我们自己模拟实现的string
#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <cassert>
using namespace std;
namespace lnb
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _arr;
}
iterator end()
{
return _arr + _size;
}
const iterator begin()const
{
return _arr;
}
const iterator end()const
{
return _arr + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(const char* str)" << endl;
if (_size == 0)
{
_capacity = 4;
}
_arr = new char[_capacity+1];
strcpy(_arr, str);
}
string(const string& str)
{
cout << "string(const string& str)——深拷贝" << endl;
_arr = new char[str._capacity + 1];
strcpy(_arr, str._arr);
_size = str._size;
_capacity = str._capacity;
}
string(string&& str)
:_arr(nullptr),
_size(0),
_capacity(0)
{
cout << "string(const string&& str)——移动拷贝" << endl;
swap(str);//这边之所以可以传递到左值引用,是因为当实参传给str后,有了左值的属性
}
const string& operator=(const string& str)
{
cout << "const string& operator=(const string& str)—赋值重载" << endl;
if (this != &str)
{
char* tmp = new char[str._capacity + 1];
delete[] _arr;
_arr = tmp;
strcpy(_arr, str._arr);
_capacity = str._capacity;
_size = str._size;
}
return *this;
}
const string& operator=(string&& str)
{
cout << "const string& operator=(string&& str)——移动赋值" << endl;
swap(str);
return *this;
}
~string()
{
delete[] _arr;
_size = _capacity = 0;
}
const char* c_str(void)
{
return _arr;
}
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _arr[pos];
}
char& operator[](size_t pos)const
{
assert(pos < _size);
return _arr[pos];
}
void Print(const string& str)
{
for (size_t i = 0; i < str._size; i++)
{
cout << str[i] << " ";
}
cout << endl;
iterator bg = str.begin();
while (bg != str.end())
{
cout << *bg << endl;
bg++;
}
}
void reserve(size_t size)
{
char* tmp = new char[size + 1];
strcpy(tmp, _arr);
delete _arr;
_arr = tmp;
_capacity = size;
}
void resize(size_t size, char c = '\0')
{
if (size <= _size)
{
_arr[size] = '\0';
_size = size;
}
else
{
if (size > _capacity)
{
reserve(size);
}
size_t i = _size;
while (i < size)
{
_arr[i] = c;
i++;
}
_size = size;
_arr[_size] = '\0';
}
}
void swap(string& str)
{
std::swap(_arr, str._arr);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
private:
char* _arr;
size_t _size;
size_t _capacity;
static const size_t npos = -1;
};
}
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;
}
Date(Date&& d)
{
cout << "Date(Date&& d)——移动拷贝" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)——拷贝构造函数" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
/*list<lnb::string> mylist;
mylist.push_back("1111");
cout << "----------------------" << endl;
mylist.emplace_back("1111");*/
/*list<Date> mylist;
mylist.push_back({ 2024,11,18 });
cout << "----------------------" << endl;
mylist.emplace_back( 2024,11,18);*/
/*lnb::string str("hello world");
vector<Date> myvector;
myvector.push_back({2024,11,18});
cout << "----------------------" << endl;
myvector.emplace_back(2024,11,18);*/
/*list<pair<int,lnb::string>> mylist;
mylist.push_back(make_pair(10,lnb::string("1111")));
cout << "-------------------------------" << endl;
mylist.emplace_back(1,lnb::string("1111"));*/
return 0;
}
其实emplace_back依靠的是定位new的调用,实际效率差不多,可以无脑使用emplace
2.lambda表达式(重要)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
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());
return 0;
}
每当我们需要使用各种算法时就会需要用到仿函数,但是次数一旦多了就会比较复杂和不便,因此出现了lambda表达式
2.1.使用说明
格式说明:
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}
①lambda表达式各部分说明
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
(parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是const函数,不能对其捕获的参数进行修改,mutable可以取消其常量性。使用该修饰符时,参数列表不能省略(即使参数为空)
->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值可以省略。返回值类型明确的情况下也可以省略,由编译器对返回类型进行推导
{}:函数体,在函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
最简单的lambda函数:[]{}
②捕获列表说明
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕获变量var
[&]:表示引用传递捕获所有父作用域中的变量(包括this)
[this]:b表示值传递方式捕获当前的this指针
注意:
① 在块作用域以外的lambda函数捕捉列表必须为空
②语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
③捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:
[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
看接下来的栗子
#include <iostream>
using namespace std;
struct Date
{
public:
Date(int year,int month,int day)
:_year(year),
_month(month),
_day(day)
{
}
Date(const Date& d)
{
//for example 3
[this,&d] {
_year = d._year;
_month = d._month;
_day = d._day;
}();
}
int _year;
int _month;
int _day;
};
int main()
{
int out = 10;
{
//for example 1
cout << "for example 1:";
int x = 10;
int y = 20;
auto f1 = [=]()->int{return x + y; };//具体用什么类型接受后续会讲
cout<<f1() << endl;
//for example 2
cout << "for example 2:";
[=](int a, int b)mutable
{
a = 10;
b = 20;//不使用mutable可以修改
x = 11;//不使用mutable无法修改
out = 10;//也可以捕获
cout << "a=" << a << ",b=" << b << ",x=" << x << ",out=" << out << endl;
}(100,200);
//for example 3
cout << "for example 3:";
Date d1(2024, 11, 18);
Date d2(d1);
cout << d2._year << ":" << d2._month << ":" << d2._day << endl;
}
return 0;
}
#include <iostream>
using namespace std;
int main()
{
auto f1=[]() {return 1; };
auto f2(f1);//可以拷贝构造
//f1 = f2;//不能进行相互赋值
int(*fptr)(void) = f1;//可以赋值给相同类型的函数指针
return 0;
}
可以看出lambda表达式实际是一个匿名函数
2.2.函数对象与lambda表达式
#include <iostream>
using namespace std;
struct Add
{
int operator()(int x,int y)
{
return x + y;
}
};
int main()
{
Add x;
cout<<x(1, 2)<<endl;//3
auto y= [](int x, int y) {return x + y; };
cout<<y(1, 2)<<endl;//3
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
如果在VS2019中的话,类的名称是lambda_uuid,但是在VS2022下就不是了,对于什么是uuid,简单来说就是通用唯一标识符 (UUID) 是一种特定形式的标识符,在大多数实际用途中可以安全地认为是唯一的。两个正确生成的 UUID 相同的可能性几乎可以忽略不计,即使它们是由不同的各方在两个不同的环境中创建的。
3.线程库
3.1.thread
现在C++11支持多线程了,以前的话会因为平台的的接口问题,导致移植性较差,现在就可以完全解决这个问题了
☞thread文档
我们首先来看一下thread类的基本使用以及常用的调用接口
首先是构造函数
如果构造函数不带任何参数,没有提供线程函数,该对象实际没有任何线程,没什么用处
在上面的代码中还用到了get_id()成员函数,它其实是一个std下封装的一个类,返回值为id类型
接下来,我们提供一下线程函数,来正式使用一下线程
线程函数一般有三种方式提供,分别是lambda表达式,函数对象,函数指针
#include <iostream>
#include <thread>
#include <future>
using namespace std;
void Func(int x,int y)
{
cout << "x:" << x << ",y:" << y << endl;
cout << "I am thread t2" << endl;
}
struct Add
{
int operator()(int x, int y)
{
cout << "I am thread t3" << endl;
return x + y;
}
};
int main()
{
//for example 1-lambda表达式作为线程函数
thread t1([]()
{
cout << "I am thread t1" << endl;
}
);
//for example 2-函数指针作为线程函数
int a = 10;
int b = 20;
thread t2(&Func,a,b);
//for example 3-函数对象作为线程函数
thread t3(Add(), a, b);
t1.join();//等待线程
t2.join();
t3.join();
future<int> result = async(Add(),a,b);//获取线程的返回值
cout <<"thread t3 return->"<<result.get() << endl;
cout << "Main thread" << endl;
return 0;
}
顺序看着比较乱,这是因为线程都是在同时进行的,没有进行同步互斥操作,并且我们代码最后需要等待线程,使用join,不然线程还没执行完,主线程就结束了
我们的thread是防拷贝的,不允许拷贝构造和赋值重载,但是可以移动构造和移动赋值,即将一个线程的状态转给其他线程对象,转移期间不影响线程的执行
#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
int main()
{
//模仿线程池
thread thread_array[5];
for (int i=0;i<sizeof(thread_array)/sizeof(thread_array[0]);i++)
{
thread_array[i] = thread([i]()//移动赋值重载
{
printf("I am thread t%d\n", i+1);
}
);
}
for (int i = 0; i < 5; i++)
{
thread_array[i].join();
}
return 0;
}
我们可以通过joinable,来判断线程是否有效,对于一下任意情况,线程无效:
①采用无参构造函数构造的线程对象
②线程对象的状态已经转移给其他线程对象
③线程已经调用jionable或者detach结束
其中第三点可能不太理解detach,我们来演示一下,它其实就是不需要主线程等待了
#include <iostream>
#include <thread>
#include <Windows.h>
using namespace std;
void Func(int& x)
{
while (1)
{
cout << "thread 1,x="<<x<< endl;
Sleep(1000);
}
}
int main()
{
int a = 10;
thread t1(Func,ref(a));//传引用就得用ref
t1.detach();//使用后主线程就不需要再等待了
Sleep(10000);
return 0;
}
在上面图中演示了detach的使用,同时还有一个关注点,就是如果线程函数的参数要使用引用,得需要使用ref(),也可以使用指针来改变对应的值,我们一般线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的
#include <iostream>
#include <thread>
using namespace std;
struct Date
{
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year=2024;
int _month=11;
int _day=19;
};
int main()
{
Date d;
thread t1(&Date::print,&d);//类成员函数作为线程函数时需要带上this
t1.join();
return 0;
}
3.2.atomic原子库操作
在我们日常使用多线程中最担心的就是碰到线程安全问题,因此我们C++11就有了原子操作库,
对于原子操作,简单来说就是一个事件只有两种情况,要么做完,要么没做,因此使用后原子操作库后就能保证安全
☞atomic原子库文档
原子类型只能从其模板参数中进行构造,不允许拷贝构造,移动构造以及赋值重载等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值重载等。
符重载默认删除掉了。
#include <iostream>
#include <thread>
using namespace std;
int num = 0;
void Func()
{
int i = 100;
while(i--)
num++;
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << num << endl;
return 0;
}
上图中的内容为没有使用原子打印出的结果,我们的理想结果为200,但是结果显而易见,不过在多次尝试中,也很多次的达到了200,但这样是有线程安全问题的
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> num=0;//使用原子操作
void Func()
{
int i = 100;
while(i--)
num++;
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << num.load() << endl;
return 0;
}
如上代码就没问题了,我们如果需要知道num原本的类型值,就可以使用load成员函数,有的同学可能会考虑到使用锁,确实没问题,我们后面就会讲解,但是对于使用锁的话,效率比较低,而且锁一不注意就会造成死锁,而我们的原子操作可以保证高效率和安全。
3.3.mutex
3.3.1.mutex的种类
所有的锁对象之间不能进行拷贝,也不能进行移动赋值
C++11一共提供4种锁,分别是mutex,recursive_mutex,timed_mutex,recursive_timed_mutex
①mutex:
mutux是最常用的锁了,它常用的函数有:lock(),unlock(),try_lock()
lock()可能会发生的情况:
如果该锁没有被别的线程获取,则获取这个锁,直到unlock前,一直获得这个锁
如果当前锁被其他线程已经获取,那么调用线程就阻塞
如果当前锁被其他线程获取不释放,而当前线程又获取了其他线程所需要的锁,那么就会造成死锁
try_lock()可能会发生的情况:
- 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
- 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
- 如果当前互斥量被当前调用线程锁住,则会产生死锁
②recursive_mutex:
这个锁允许同一个线程对锁多次上锁(递归上锁),来获得对锁对象的多层所有权,释放锁时需要调用与该锁层次深度相同的unlock
其余两个就不多赘述了,可以查文档
③timed_mutex:timed_mutex文档
④recursive_timed_mutex:recursive_timed_mutex文档
3.3.2.lock_guard
在我们使用锁的过程中,难免会遇到忘记解锁,因此出现了lock_guard类模板,它主要是通过RAII的方式,对其管理的锁进行了封装,在需要加锁的地方,只需要用上述介绍的任意锁实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;
int num=0;
mutex _m;
void Func()
{
int i = 100;
lock_guard<mutex> guard(_m);//使用
while(i--)
num++;
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << num<< endl;
return 0;
}
上面为使用了库中的lock_guard,其实它的写法很简单,我自己也写了一lock_guard模拟实现
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;
namespace lnb
{
template<class M>
class lock_guard//模拟实现
{
public:
lock_guard(M& m)
:_m(m)
{
_m.lock();
}
~lock_guard()
{
_m.unlock();
}
private:
M& _m;
};
}
int num=0;
mutex mtx;
void Func()
{
int i = 100;
lnb::lock_guard<mutex> guard(mtx);//使用
while(i--)
num++;
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << num<< endl;
return 0;
}
3.3.3.unique_lock
与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作,与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数
#include <iostream>
#include <thread>
#include <atomic>
#include <mutex>
using namespace std;
int num=0;
mutex mtx;
void Func()
{
int i = 100;
unique_lock<mutex> uq(mtx);//使用
while(i--)
num++;
}
int main()
{
thread t1(Func);
thread t2(Func);
t1.join();
t2.join();
cout << num<< endl;
return 0;
}
🌸🌸C++11(中)的知识大概就讲到这里啦,博主后续会继续更新更多C++的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪