网站 制作 中心,简易网站开发,深圳seo爱好者,小程序线上商城C11#xff08;中#xff09; 1.可变参数模板1.1.使用场景 2.lambda表达式#xff08;重要#xff09;2.1.使用说明2.2.函数对象与lambda表达式 3.线程库3.1.thread3.2.atomic原子库操作3.3.mutex3.3.1.mutex的种类3.3.2.lock_guard3.3.3.unique_lock #x1f31f;#x… C11中 1.可变参数模板1.1.使用场景 2.lambda表达式重要2.1.使用说明2.2.函数对象与lambda表达式 3.线程库3.1.thread3.2.atomic原子库操作3.3.mutex3.3.1.mutex的种类3.3.2.lock_guard3.3.3.unique_lock hello各位读者大大们你们好呀 系列专栏【C的学习】 本篇内容可变参数模板可变参数使用场景lambda表达式lambda表达式使用说明函数对象与lambda表达式线程库threadatomic原子库操作mutexmutex的种类lock_guard;unique_lock ⬆⬆⬆⬆上一篇C11上 作者简介轩情吖请多多指教( •̀֊•́ ) ̖́- 1.可变参数模板 C11的新特性可变参数模板能够让我们创建可以接受可变参数的函数模板和类模板 #include iostream
using namespace std;
templateclass ...Args
void ShowList(Args ...args)
{
//Args是一个模板参数包args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
}
int main()
{ShowList();return 0;
}上面的参数args前面有省略号所以它就是一个可变模版参数我们把带省略号的参数称为“参数包”它里面包含了0到NN0个模版参数。我们无法直接获取参数包args中的每个参数的只能通过展开参数包的方式来获取参数包中的每个参数这是使用可变模版参数的一个主要特点 下面举一个栗子 #include iostream
using namespace std;
void ShowList()
{
//递归终止函数
}
templateclass 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;
templateclass T
void Print(T value)
{cout value endl;
}
templateclass ...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[_capacity1];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()
{/*listlnb::string mylist;mylist.push_back(1111);cout ---------------------- endl;mylist.emplace_back(1111);*//*listDate mylist;mylist.push_back({ 2024,11,18 });cout ---------------------- endl;mylist.emplace_back( 2024,11,18);*//*lnb::string str(hello world);vectorDate myvector;myvector.push_back({2024,11,18});cout ---------------------- endl;myvector.emplace_back(2024,11,18);*//*listpairint,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()
{vectorGoods 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 1cout for example 1:;int x 10;int y 20;auto f1 []()-int{return x y; };//具体用什么类型接受后续会讲coutf1() endl;//for example 2cout 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 3cout 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;coutx(1, 2)endl;//3auto y [](int x, int y) {return x y; };couty(1, 2)endl;//3return 0;
}从使用方式上来看函数对象与lambda表达式完全一样 实际在底层编译器对于lambda表达式的处理方式完全就是按照函数对象的方式处理的即如果定义了一个lambda表达式编译器会自动生成一个类,在该类中重载了operator()。 如果在VS2019中的话类的名称是lambda_uuid但是在VS2022下就不是了对于什么是uuid简单来说就是通用唯一标识符 (UUID) 是一种特定形式的标识符在大多数实际用途中可以安全地认为是唯一的。两个正确生成的 UUID 相同的可能性几乎可以忽略不计即使它们是由不同的各方在两个不同的环境中创建的。 3.线程库
3.1.thread 现在C11支持多线程了以前的话会因为平台的的接口问题导致移植性较差现在就可以完全解决这个问题了 ☞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();futureint 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 i0;isizeof(thread_array)/sizeof(thread_array[0]);i){thread_array[i] thread([i]()//移动赋值重载{printf(I am thread t%d\n, i1);});}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,xx endl;Sleep(1000);}
}int main()
{int a 10;thread t1(Func,ref(a));//传引用就得用reft1.detach();//使用后主线程就不需要再等待了Sleep(10000);return 0;
}在上面图中演示了detach的使用同时还有一个关注点就是如果线程函数的参数要使用引用得需要使用ref()也可以使用指针来改变对应的值我们一般线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的 #include iostream
#include thread
using namespace std;
struct Date
{void print(){cout _year / _month / _day endl;}int _year2024;int _month11;int _day19;
};
int main()
{Date d;thread t1(Date::print,d);//类成员函数作为线程函数时需要带上thist1.join();return 0;
}3.2.atomic原子库操作 在我们日常使用多线程中最担心的就是碰到线程安全问题因此我们C11就有了原子操作库 对于原子操作简单来说就是一个事件只有两种情况要么做完要么没做因此使用后原子操作库后就能保证安全 ☞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;
atomicint num0;//使用原子操作
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的种类 所有的锁对象之间不能进行拷贝也不能进行移动赋值 C11一共提供4种锁分别是mutexrecursive_mutextimed_mutexrecursive_timed_mutex ①mutex mutux是最常用的锁了它常用的函数有lock(),unlock(),try_lock() lock()可能会发生的情况 如果该锁没有被别的线程获取则获取这个锁直到unlock前一直获得这个锁 如果当前锁被其他线程已经获取那么调用线程就阻塞 如果当前锁被其他线程获取不释放而当前线程又获取了其他线程所需要的锁那么就会造成死锁 try_lock()可能会发生的情况 如果当前互斥量没有被其他线程占有则该线程锁住互斥量直到该线程调用 unlock 释放互斥量如果当前互斥量被其他线程锁住则当前调用线程返回 false而并不会被阻塞掉如果当前互斥量被当前调用线程锁住则会产生死锁 ②recursive_mutex 这个锁允许同一个线程对锁多次上锁(递归上锁来获得对锁对象的多层所有权释放锁时需要调用与该锁层次深度相同的unlock 其余两个就不多赘述了可以查文档 ③timed_mutextimed_mutex文档 ④recursive_timed_mutexrecursive_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 num0;
mutex _m;
void Func()
{int i 100;lock_guardmutex 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
{templateclass Mclass lock_guard//模拟实现{public:lock_guard(M m):_m(m){_m.lock();}~lock_guard(){_m.unlock();}private:M _m;};
}int num0;
mutex mtx;
void Func()
{int i 100;lnb::lock_guardmutex 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 num0;
mutex mtx;
void Func()
{int i 100;unique_lockmutex uq(mtx);//使用while(i--)num;
}int main()
{thread t1(Func);thread t2(Func);t1.join();t2.join();cout num endl;return 0;
}C11中的知识大概就讲到这里啦博主后续会继续更新更多C的相关知识干货满满如果觉得博主写的还不错的话希望各位小伙伴不要吝啬手中的三连哦你们的支持是博主坚持创作的动力