当前位置: 首页 > news >正文

C++11特性

🌻个人主页:路飞雪吖~

       🌠专栏:C/C++


目录

一、统一的列表初始化

🌟{ } 初始化

🌟std::initializer_list

二、变量类型推到

🌟 auto

🌟decltype

🌟nullptr

三、范围for循环

四、右值引用和移动语义

🌟左值引用和右值引用

🌟左值引用和右值引用的比较

小贴士:

🌟右值引用使用场景和意义

🌟右值引用 引用左值的深入使用场景分析

🌟完美转发

​编辑

五、新的类功能

🌟默认成员函数

🌟类成员变量初始化 

🌟强制生成默认函数的关键字default

🌟禁止生成默认函数的关键字delete

🌟继承和多态中的final与override关键字

六、可变参数模板

🌟STL容器中的empalce相关接口函数

七、lambda表达式

🌟C++98中的一个例子

🌟lambda表达式

🌟lambda表达式语法

🌟捕获列表说明

小贴士:    mutable虽然可以去掉const属性,可以修改函数体里面的变量,但是一般不用    因为怕误用,怕有一些人认为修改了lambda函数体里面的变量,就可以影响外面的    其实是不影响外面的。

🌟函数对象与lambda表达式

八、包装器

🌟bind


一、统一的列表初始化

🌟{ } 初始化

在C++98中,标准允许使用花括号 { } 对数组或者结构体元素进行统一的列表初始值设定:

struct Point
{int _x;int _y;
};int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用列表初始化时,可添加等号(=),也可不添加:  

创建对象也可以使用列表初始化方式调用构造函数初始化

struct Point
{int _x;int _y;
};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;}
private:int _year;int _month;int _day;
};int main()
{//列表初始化(=都可以省略)int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };//Point p = { 1, 2 };int x1 = 1;int x2 = { 2 };多参数的隐式类型转换中间会产生临时变量//Date d1 = { 2024,10,17 };//临时对象去拷贝构造,被优化为构造//const Date& d1 = { 2024,10,17 };//临时对象+拷贝构造//C++11中列表初始化也可以适用于new表达式中int* pa = new int[4]{0};Point p  { 1, 2 };int x2  { 2 };// C++11支持的列表初始化,这里会调用构造函数初始化Date d1  { 2024,10,17 };const Date& d1  { 2024,10,17 };return 0;
}

🌟std::initializer_list

std::initializer_list 一般是作为构造函数的参数,C++11对STL中的不少容器增加;

std::initializer_list 作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。

int main()
{vector<int> v = { 1,2,3,4 };list<int> lt = { 1,2 };// 这里{"sort", "排序"}会先初始化构造一个pair对象map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// 使用大括号对容器赋值v = {10, 20, 30};return 0;
}

二、变量类型推到

🌟 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将 其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。

int main()
{int i = 10;auto p = &i;auto pf = strcpy;cout << typeid(p).name() << endl;cout << typeid(pf).name() << endl;map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();return 0;
}

🌟decltype

关键字decltype将变量的类型声明为表达式指定的类型。(推出这个类型,可以用这个类型来声明)

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{decltype(t1 * t2) ret;cout << typeid(ret).name() << endl;
}
int main()
{const int x = 1;double y = 2.2;decltype(x * y) ret; // ret的类型是doubledecltype(&x) p;      // p的类型是int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a');return 0;
}

🌟nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

C++里面 void*int* 是不同的类型,需要强制转换;

C语言里面 void* 类型指针,是可以直接隐式类型转换成其他指针的,C++不行,使用C++新增了nullptr

三、范围for循环

范围 for 的底层为迭代器,可自动++,自动赋值:

int main()
{int a[] = { 2,6,7,8,9,4,6,1,3 };for (auto e : a)// a 自动++,并自动赋值给 e{cout << e << " ";}cout << endl;
}

四、右值引用和移动语义

🌟左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了右值引用语法特性,所以我们之前学习的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

左值:

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址并且可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{//以下p、b、c 、*p都是左值//左值(是一个表达式):可以取地址int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("1111111");s[0];//返回对象的别名// char& operator[](size_t i)// {    return _str[i];      }//返回str指向堆上的空间,不是一个临时的空间cout << &c << endl;//左值可以取地址cout << &s[0] << endl;//左值可以取地址//左值引用给左值取别名int& r1 = b;int*& r2 = p;int& r3 = *p;return 0;
}

 右值: (通常为常量、匿名对象、临时对象)

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{//以下几个为右值(数据表达式):不能取地址//通常为常量、匿名对象、临时对象//本质:右值都是一些临时性的对象(临时存储的对象)double x = 1.1, y = 2.2;10;x + y;//用临时对象来存储+之后的结果fmin(x, y);//用临时对象存储返回值的结果//double fmin(double x, double y)两个值进行比较再进行传值返回string("1111111");//匿名对象,生命周期只在这一行,出了作用域就销毁了//cout << &10 << endl;//不能取地址//cout << &(x + y) << endl;//不能取地址//cout << &(fmin(x, y)) << endl;//不能取地址//cout << &(string("1111111")) << endl;//不能取地址//右值引用给右值取别名int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);return 0;
}

注意:

<1> 右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址。(右值还是有地址,有空间的,只不过这些空间,是编译器自己开的一块临时空间,编译器的语法不允许使用,在编译检查的时候限制这个用法)

int main()
{double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5;  // 报错return 0;
}

🌟左值引用和右值引用的比较

左值引用总结:

    1、左值引用只能引用左值,不能引用右值;

    2、但是const左值引用即可引用左值,也可引用右值。

int main()
{//以下p、b、c 、*p都是左值//左值(是一个表达式):可以取地址int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("1111111");s[0];//右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("1111111");//左值引用给右值取别名:不能直接取别名(引用),//                     但是const 左值引用可以//临时对象、匿名对象具有常性(const)//不加const权限就放大了/*int& rx1 = 10;int& rx2 = x + y;int& rx3 = fmin(x, y);string& rx4 = string("11111111");*/const int& rx1 = 10;const double& rx2 = x + y;const double& rx3 = fmin(x, y);const string& rx4 = string("11111111");//void push(const T& x)//const既能接收左值,也能接收右值vector<string> v;string s1("11111");v.push_back(s1);v.push_back(string("11111"));//右值对象v.push_back("111111");//单参数构造函数,支持隐式类型转换}

右值引用总结:

    1、右值引用只能引用右值,不能引用左值

    2、但是右值引用可以move以后的左值。

int main()
{//以下p、b、c 、*p都是左值//左值(是一个表达式):可以取地址int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("1111111");s[0];//右值double x = 1.1, y = 2.2;10;x + y;fmin(x, y);string("1111111");//右值引用 给 左值取别名:不能直接取别名(引用),//                        但是move(左值)以后右值引用可以引用//move:返回右值引用的一个值int&& rrx1 = move(b);int*&& rrx2 = move(p);int&& rrx3 = move(*p);string&& rrx4 = move(s);//string&& rrx5 = s;//语法上检查不通过(左值、右值底层都是指针)string&& rrx5 = (string&&)s;//move本质其实就是强制类型转换return 0;
}
小贴士:

<1> 底层汇编的实现和上层语法表达的意义,有时是背离的,所以不要结合到一起理解去,相互佐证;

int main()
{//在会汇编层(底层)面上,没有左值引用和右值引用的概念//都是指针,如果左值、右值引用写错只是语法检查通不过int x = 0;int& r1 = x;int&& rr1 = x + 10;// x+10右值,要用右值引用,不然语法上会报错return 0;
}

<2> 在会汇编层(底层)面上,没有左值引用和右值引用的概念,都是指针,如果左值、右值引用写错只是语法检查通不过。

<3> 引用的意义:减少拷贝
       左值引用解决的场景:引用传参/引用传返回值
       左值引用没有彻底解决的场景:传返回值

🌟右值引用使用场景和意义

 •  移动构造:临时创建的对象,不能取地址,用完就要消亡,深拷贝的类,移动构造才有意义。

 •  右值通常是一些字面常量、表达式返回值、匿名对象、临时空间存储;

 •  纯右值:内置类型右值(常量10、整型a+b)

 •  将亡值:类类型内置的右值(匿名对象、类型转换中间产生的临时对象)

// 引用的意义:减少拷贝
// 左值引用解决的场景:引用传参/引用传返回值
// 左值引用没有彻底解决的场景:传返回值namespace xlf
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造// s2(s1)//左值拷贝/右值拷贝//要开空间拷贝数据string(const string& s):_str(nullptr){cout << "string(const string& s) -- 深拷贝" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造// 临时创建的对象,不能取地址,用完就要消亡// 深拷贝的类,移动构造才有意义//右值拷贝//右值通常是一些字面常量、表达式返回值、匿名对象、临时空间存储//纯右值:内置类型右值(常量10、整型a+b)//将亡值:类类型内置的右值(匿名对象、类型转换中间产生的临时对象)string(string&& s){cout << "string(string&& s) -- 移动拷贝" << endl;swap(s);//抢占资源}// 赋值重载string& operator=(const string& s){cout << "string& operator=(const string& s) -- 深拷贝" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动拷贝" << endl;swap(s);return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0; // 不包含最后做标识的\0};xlf::string to_string(int value)
{bool flag = true;if (value < 0){flag = false;value = 0 - value;}xlf::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;
}}

 左值引用使用场景:

void func1(xlf::string s)//传值
{}
void func2(const xlf::string& s)//传引用
{}
int main()
{xlf::string s1("hello world");// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);func2(s1);// string operator+=(char ch) 传值返回存在 深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}

 左值引用的短板

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:xlf::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

 

右值引用和移动语义:

  •  xlf::string 中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那就不用做深拷贝了,所以叫移动拷贝,就是窃取别人的资源来构造自己。

  •   不仅仅有移动构造还有移动赋值:

🌟右值引用 引用左值的深入使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:在有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用一个左值时,可以通过move函数将左值转化为右值。c++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

int main()
{xlf::string s1("hello world");// 这里s1是左值,调用的是拷贝构造xlf::string s2(s1);// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的// 资源被转移给了s3,s1被置空了。xlf::string s3(std::move(s1));return 0;
}

🌟完美转发

void Fun(int& x) {cout << "左值引用" << endl;}void Fun(const int& x) { cout << "const 左值引用" << endl; }void Fun(int&& x) {cout << "右值引用" << endl;}void Fun(const int&& x) { cout << "const 右值引用" << endl;}// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)// T&& -> 传什么T&&就替换成什么
{Fun(t);
}int main()
{PerfectForward(10);// 右值int a;PerfectForward(a); // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);// const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

这时我们会发现输出的全是 const/左值引用 ,这是为什么呢?万能引用在接收左值/右值后,引用类型限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发:

std::forward 完美转发在传参的过程中保留对象原生类型属性。

template<typename T>
void PerfectForward(T&& t)// T&& -> 传什么T&&就替换成什么
{//模板实例化是左值引用,保持属性直接传给Fun//模板实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传给Fun//Fun(t);//所以不能直接传//完美转发:保持属性进行传递Fun(forward<T>(t));//类模板//在传参的过程中保持了t的原生类型属性//完美转发的内部要识别是左值/右值引用//如果是左值引用就不用管了,如果是右值引用对象就要move(强制转换)一下
}

完美转发实际就是一个类模板:由编译器自己写

void PerfectForward(int& t)
{Fun(t);
}void PerfectForward(int&& t)
{Fun(move(t));
}void PerfectForward(const int& t)
{Fun(t);
}void PerfectForward(const int&& t)
{Fun(move(t));
}

完美转发实际中的使用场景:

template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x));}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x));}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, const T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;
};int main()
{List<bit::string> lt;lt.PushBack("1111");lt.PushFront("2222");return 0;
}

五、新的类功能

🌟默认成员函数

原来C++类中,有6个默认成员函数:

•  构造函数

•  析构函数

•  拷贝构造函数

•  拷贝赋值重载

•  取地址重载

const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。 C++11 新增了两个:移动构造函数移动赋值运算符重载

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

• 移动构造函数:

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。

• 移动赋值重载函数

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)

• 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
public:Person(const char* name = "1111111", int age = 0):_name(name), _age(age){}//自动生成拷贝构造和移动构造//Person(Person&& p) = default;//右值//Person(const Person& p) = default;//左值//Person& operator=(Person&& p) = default;//右值//Person& operator=(const Person& p) = default;//左值//写了析构就不生成移动构造了//默认生成移动构造的条件://没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载//中的任意一个/*~Person(){}*///析构、拷贝构造、赋值重载在真实的类里面是一体化的,即//需要显式写析构,说明有资源需要释放// 1、说明需要显式写拷贝构造和赋值重载// 2、说明需要显式写移动构造和移动赋值private:xlf::string _name;//(Person自身没有资源要管理,自己的自定义类型的成员有资源管理的需求)//在这个场景下,自动生成移动构造是为了自定义类型准备的//如果在自定义类型里面没有自动生成移动构造,就会调用拷贝构造,效率就会变低int _age;
};//自动生成移动构造,对于Data这样的类,其实没啥意义和拷贝构造的功能是一样的
//自动生成移动构造,对于Person这样的类是很有意义的,
//因为Person是右值时,它内部的string也是右值,string就可以走移动构造,效率就提高了int main()
{Person s1;//拷贝构造Person s2 = s1;//深拷贝Person s3 = std::move(s1);//移动构造(抢占s1的资源)Person s4;s4 = std::move(s2);//移动赋值return 0;
}

🌟类成员变量初始化 

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这 个我们在类和对象默认就讲了,这里就不再细讲了。有疑问的小伙伴,可以在之前的文章里面查看哦~

🌟强制生成默认函数的关键字default

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。

class Person
{
public:Person(const char* name = "1111111", int age = 0):_name(name), _age(age){}//	//自动生成拷贝构造和移动构造
//
//	//只声明不实现,声明为私有
//	//C++98
//private:
//	Person(const Person& p);
//	Person(const Person& p);Person(const Person& p) = default;Person(const Person& p) = default;~Person(){}private:xlf::string _name;int _age;
};//Person::Person(const Person& p)
//{}int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);Person s4;s4 = std::move(s2);return 0;
}

🌟禁止生成默认函数的关键字delete

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

class Person
{
public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p) = delete;
private:bit::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}

🌟继承和多态中的final与override关键字

这个我们在继承和多态已经看过了哟~这里就不再细讲,有疑问的小伙伴就去前面的文章找找哦~

六、可变参数模板

C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧。

下面就是一个基本可变参数的函数模板:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args... args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数 包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的, 只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特 点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变 参数,所以我们的用一些奇招来一一获取参数包的值。

递归函数方式展开参数包:

//可变模板参数
//参数类型可变
//参数个数可变
//template<class ...Args>
//void ShowList(Args... args)
//{
//	//可变参数模板编译时解析
//	cout << sizeof...(args) << endl;
//	//下面运行获取和解析,所以不支持这样用
//	for (size_t i = 0; i < sizeof...(args); i++)
//	{
//		cout << args[i] << " ";
//	}
//	cout << endl;
//}//参数匹配
//写无参的
void Print()
{cout << endl;
}template<class T,class ...Args>
void Print(T&& x, Args&&... args)
{cout << x << " ";Print(args...);//递归// 当args...参数为0时,就调用无参,就结束递归//运行时才判断,所以 sizeof...(args) == 0 就到不到结束条件// 编译时递归不能解决//if (sizeof...(args) == 0)//递归结束条件? (错)//	return;
}//编译时递归推到解析参数
template<class ...Args>
void ShowList(Args&&... args)
{Print(args...);
} //编译器推倒的
//void Print(double x)
//{
//	cout << x << " ";
//	Print();
//}
//
//void Print(const char* x, double z)
//{
//	cout << x << " ";
//	Print(z);
//}
//
//void Print(int x, const char* y, double z)
//{
//	cout << x << " ";
//	Print(y , z);
//}int main()
{ShowList();ShowList(1);ShowList(1, "xxxxx");ShowList(1, "xxxxx", 2.2);return 0;
}

 逗号表达式展开参数包:

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg 不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

这种就地展开参数包的方式 实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行 printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列 表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包

template <class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}template <class ...Args>
void ShowList(Args... args)
{int arr[] = { PrintArg(args)... };cout << endl;
}// //编译推演生成下面的函数
//void ShowList(int x, char y, std::string z)
//{
//	int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };
//	cout << endl;
//}int main()
{//ShowList(1);//ShowList(1, 'A');ShowList(1, 'A', std::string("sort"));return 0;
}

🌟STL容器中的empalce相关接口函数

 我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对 insertemplace 系列接口的优势到底在哪里呢?

•  emplace_back 支持可变参数,拿到构建pair对象的参数后自己去创建对象;

•  emplace_back:能接收左值也可以接收右值, 不支持插入多个值
•  总体而言更高效,推荐使用

•  可以直接传插入的对象,也可以传构造这个对象的参数包
•  传构造这个对象的参数包高效一些,它会直接构造
 (1) 对于是深拷贝时,如果是右值的话,
     直接构造 、 构造+移动构造(代价很低) 这两个的效率差不多;
     如果是左值的话,就更高效了
 
 (2) 对于浅拷贝的类,没有移动构造的概念,

       例:日期类 不管左值还是右值都是 构造+拷贝构造。

int main()
{list<xlf::string> lt;//左值    xlf::string s1("11111111111111");lt.emplace_back(s1);// 构造+拷贝构造//右值lt.emplace_back(move(s1));//构造+移动构造//直接把构造string参数包往下传,直接用string参数构造stringlt.emplace_back("11111111111111");//构造lt.emplace_back(10, 'x');//构造///list<pair<xlf::string, int>> lt1;//构造 pair + 拷贝/移动构造pair到list的节点中的data上pair<xlf::string, int> kv("小路飞", 1);lt1.emplace_back(kv);//构造+拷贝构造lt1.emplace_back(move(kv));//构造+拷贝构造//直接吧构造pair参数包往下传,直接用pair参数包构造pairlt1.emplace_back("小路飞", 1);//构造return 0;
}

七、lambda表达式

🌟C++98中的一个例子

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法:

int main()
{int array[] = { 4,1,8,5,3,7,0,9,2,6 };// 默认按照小于比较,排出来结果是升序std::sort(array, array + sizeof(array) / sizeof(array[0]));// 如果需要降序,需要改变元素的比较规则std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

#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;
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

🌟lambda表达式

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());//价格升序sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._price < g2._price;});//返回值类型:boolsort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)-> bool{return g1._price < g2._price;});sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){return g1._evaluate < g2._evaluate;});return 0;
}

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函 数。

🌟lambda表达式语法

                                           捕捉列表       参数                               返回值           函数体
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

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

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

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

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

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

注意:

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

int main()
{auto add1 = [](int x, int y)->int {return x + y; };cout << add1(1, 2) << endl;auto func1 = []()->int {cout << "hello xlf" << endl;cout << "hello world" << endl;return 0;};func1();//返回值类型可自动推到,所以可以省略//无参数可以省略auto func2 = []{cout << "hello xlf" << endl;cout << "hello world" << endl;return 0;//有返回值可以省略返回类型};cout << func2() << endl;return 0;
}int main()
{// 最简单的lambda表达式, 该lambda表达式没有任何意义[]{}; // 省略参数列表和返回值类型,返回值类型由编译器推导为intint a = 3, b = 4;[=]{return a + 3; }; // 省略了返回值类型,无返回值类型auto fun1 = [&](int c){b = a + c; }; fun1(10)cout<<a<<" "<<b<<endl;// 各部分都很完善的lambda函数auto fun2 = [=, &b](int c)->int{return b += a+ c; }; cout<<fun2(10)<<endl;// 复制捕捉xint x = 10;auto add_x = [x](int a) mutable { x *= 2; return a + x; }; cout << add_x(10) << endl; return 0;
}

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调 用,如果想要直接调用,可借助auto将其赋值给一个变量。

🌟捕获列表说明

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

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

•  [=]:表示值传递方式捕获所有父作用域中的变量(包括this)

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

•  [&]:表示引用传递捕捉所有父作用域中的变量(包括this)

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

注意:

   •  父作用域指包含lambda函数的语句块

   •  语法上捕捉列表可由多个捕捉项组成,并以逗号分割

          比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量                         [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

   •  捕捉列表不允许变量重复传递,否则就会导致编译错误:

          比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

   •  在块作用域以外的lambda函数捕捉列表必须为空

   •  在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都会导致编译报错。

   •  lambda表达式之间不能相互赋值,即使看起来类型相同

void (*PF)();
int main()
{auto f1 = []{cout << "hello world" << endl; };auto f2 = []{cout << "hello world" << endl; };// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了//f1 = f2;   // 编译失败--->提示找不到operator=()// 允许使用一个lambda表达式拷贝构造一个新的副本auto f3(f2);f3();// 可以将lambda表达式赋值给相同类型的函数指针PF = f2;PF();return 0;
}
int main()
{int a = 0, b = 1;auto swap1 = [](int& x, int& y){//只能用当前lambda局部域和捕捉的对象int temp = x;x = y;y = temp;};swap1(a, b);// 捕捉列表// 传值捕捉本质是一种拷贝,并且const修饰了// mutable相当于去掉const属性,可以修改了// 但是修改了不会影响外面被捕捉的值,因为只是外面变量的一种拷贝auto swap2 = [a, b]() mutable//传值(const)捕捉的值是不能改变的{int temp = a;a = b;b = temp;};swap2();auto swap2 = [&a, &b]() //引用捕捉(不是const,是别名){int temp = a;a = b;b = temp;};return 0;
}
int x = 0;//全局
int main()
{//只能用当前lambda局部域和捕捉的对象和全局对象int a = 0, b = 1, c = 2, d = 3;//所有值传值捕捉auto func1 = [=]{int ret = a + b + c + d;return ret;};//所有值引用捕捉auto func1 = [&]{a++;b++;c++;d++;int ret = a + b + c + d + x;//全局的x也可以用return ret;};//混合捕捉1auto func1 = [&a, b]// a 传引用捕捉, b 传值(const)捕捉(b 不能修改,加mutable可修改){a++;//b++;int ret = a + b ;return ret;};//混合捕捉2//所有值以引用方式捕捉,d用传值捕捉auto func1 = [& , d]{a++;b++;c++;//d++;int ret = a + b + c + d;return ret;};// 注意:// mutable虽然可以去掉const属性,可以修改函数体里面的变量,但是一般不用// 因为怕误用,怕有一些人认为修改了lambda函数体里面的变量,就可以影响外面的// 其实是不影响外面的。//混合捕捉3//所有值以传值方式捕捉,d用引用捕捉auto func1 = [= , &d]//如果用来lambda就表示在函数体里面 a b c 都先++了,{                 //ret的值是a b c ++之后的值,但是里面abc对外面是没有影响的//a++;        // &d 才对外面的 d 有影响//b++;        // 所以 mutable 要慎用//c++;d++;int ret = a + b + c + d;return ret;};return 0;
}
小贴士:
    mutable虽然可以去掉const属性,可以修改函数体里面的变量,但是一般不用
    因为怕误用,怕有一些人认为修改了lambda函数体里面的变量,就可以影响外面的
    其实是不影响外面的。

🌟函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的 类对象。

// lambda 匿名函数对象
// 捕捉列表的对象是成员变量存在lambda类对象中
// 捕捉的本质是构造函数的初始化参数class Rate
{
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};int main()
{// 函数对象double rate = 0.015;Rate r1(rate);r1(10000, 2);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};auto r3 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。 函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可 以直接将该变量捕获到。

 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如 果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

八、包装器

C++中的function本质是一个类模板,也是一个包装器。

ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;return 0;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份。

包装器可以很好的解决上面的问题:

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参
//#include<functional>function: 对可调用对象进行包装
包装器:可进行类型统一
返回值和参数是相同的
//
int f(int a, int b)
{return a + b;
}struct Functor
{
public:int operator()(int a, int b){return a + b;}
};class Plus
{
public:static int plusi(int a, int b){return a + b;}//含有隐藏的thisdouble plusd(double a, double b){return a + b;}
};int main()
{//包装可调用对象function<int(int, int)> f1 = f;//函数指针function<int(int, int)> f2 = Functor();//仿函数对象function<int(int, int)> f3 = [](int a, int b) {return a + b; };//lambdacout << f1(1, 1) << endl;cout << f2(1, 1) << endl;cout << f3(1, 1) << endl;//语法规定://非静态的成员函数,成员函数要取函数指针时,要加 & 符号//包装静态成员函数function<int(int, int)> f4 = &Plus::plusi;//成员函数的函数名受类域的限制cout << f4(1, 1) << endl;//包装非静态成员函数//Plus* 隐含的thisfunction<double(Plus*, double, double)> f5 = &Plus::plusd;Plus pd;cout << f5(&pd, 1.1, 1.1) << endl;//传指针function<double(Plus, double, double)> f6 = &Plus::plusd;cout << f6(pd, 1.1, 1.1) << endl;//传有名对象cout << f6(Plus(), 1.1, 1.1) << endl;//传匿名对象// Plus* 和 Plus 为什么都可以? // function的本质:(不是直接传参,而是转换成指针之后,取调对应的函数)// 接收到函数指针(plusd)以后,// function里面作为类似成员变量的方式,把函数指针存起来// 存起来了以后,f6(pd, 1.1, 1.1) 调用的是operater(),// operater()再来调用对应的可调用对象// 可调用对象里面又去调用 &Plus::plusd 这个函数 return 0;
}

有了包装器,如何解决模板的效率低下,实例化多份的问题呢?

#include <functional>
template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{
// 函数名std::function<double(double)> func1 = f;cout << useF(func1, 11.11) << endl;// 函数对象std::function<double(double)> func2 = Functor();cout << useF(func2, 11.11) << endl;// lamber表达式std::function<double(double)> func3 = [](double d)->double{ return d / 
4; };cout << useF(func3, 11.11) << endl;return 0;
}

包装器的其他一些场景:

class Solution {
public:
int evalRPN(vector<string>& tokens) {stack<int> st;for(auto& str : tokens){if(str == "+" || str == "-" || str == "*" || str == "/"){int right = st.top();st.pop();int left = st.top();st.pop();switch(str[0]){case '+':st.push(left+right);break;case '-':st.push(left-right);break;case '*':st.push(left*right);break;case '/':st.push(left/right);break;}}else{// 1、atoi itoa// 2、sprintf scanf// 3、stoi to_string C++11st.push(stoi(str));}}return st.top();
}
};// 使用包装器以后的玩法
class Solution {
public:
int evalRPN(vector<string>& tokens) {stack<int> st;map<string, function<int(int, int)>> opFuncMap = {{ "+", [](int i, int j){return i + j; } },{ "-", [](int i, int j){return i - j; } },{ "*", [](int i, int j){return i * j; } },{ "/", [](int i, int j){return i / j; } }};for(auto& str : tokens){if(opFuncMap.find(str) != opFuncMap.end()){int right = st.top();st.pop();int left = st.top();st.pop();st.push(opFuncMap[str](left, right));}else{// 1、atoi itoa// 2、sprintf scanf// 3、stoi to_string C++11st.push(stoi(str));}}return st.top();
}
};

🌟bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作。

// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。 调用bind的一般形式:auto newCallable = bind(callable,arg_list); 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

#include<functional>
// bind绑定:对可调用对象调整(顺序、个数)参数
// 可调用对象:函数指针、lambda、仿函数
// 绑定看起来没有返回值,实际上是有返回值的,
// 它会返回一个可调用对象(调整的参数对这个参数进行调整顺序、个数)
// bind 本质返回一个仿函数对象int Sub(int a, int b)
{return (a - b) * 10;
}int SubX(int a, int b, int c)
{return (a - b - c) * 10;
}class Plus
{
public:static int plusi(int a, int b){return a + b;}//含有隐藏的thisdouble plusd(double a, double b){return a + b;}
};//placeholders命名空间的名字
//placeholders::_1  参数using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{//调整参数顺序//auto sub1 = bind(Sub, placeholders::_1, placeholders::_2);auto sub1 = bind(Sub, _1, _2);cout << sub1(10, 5) << endl;// 调整参数顺序// _1代表第一个实参// _2代表第二个实参// ...auto sub2 = bind(Sub, _2, _1);cout << sub2(10, 5) << endl;// 调整参数个数auto sub3 = bind(Sub, 100, _1);cout << sub3(5) << endl;auto sub4 = bind(Sub, _1, 100);cout << sub4(5) << endl;auto sub5 = bind(SubX, 100, _1, _2);cout << sub3(5, 1) << endl;auto sub6 = bind(SubX, _1, 100, _2);cout << sub6(5, 1) << endl;function<double(Plus, double, double)> f6 = &Plus::plusd;Plus pd;cout << f6(pd, 11, 1.1) << endl;cout << f6(Plus(), 11, 1.1) << endl;// bind 一般用于,绑死一些固定参数//auto func1 = [](double rate, double monty, int year)->double {return monty * rate * year;};auto func1 = [](double rate, double monty, int year)->double {double ret = monty;for (int i = 0; i < year; i++){ret += ret * rate;}return ret - monty;};function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);cout << func3_1_5(1000000) << endl;cout << func5_1_5(1000000) << endl;cout << func10_2_5(1000000) << endl;cout << func20_3_5(1000000) << endl;return 0;
}

如若对你有帮助,记得点赞、收藏、关注哦!

若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢^ ^ ~


http://www.mrgr.cn/news/59235.html

相关文章:

  • 了解光耦合器输入输出关系---腾恩科技
  • 功能测试的方向
  • 微服务网关Zuul
  • Java全栈经典面试题剖析5】JavaSE高级 -- 集合
  • iOS 本地存储地址(位置)
  • 内置数据类型、变量名、字符串、数字及其运算、数字的处理、类型转换
  • 基本Linux命令
  • 混淆矩阵注解
  • 挑战Java面试题复习第1天,坚持就是胜利
  • 【WPF】作为一个WPF开发者你所应该知道关于Avalonia的二三事
  • Redis 基础 问题
  • 网关三问:为什么微服务需要网关?什么是微服务网关?网关怎么选型?
  • 跨境支付,哪些国产数据库能接得住?
  • 透视 @Transactional 的隔离级别:四大隔离机制让事务更安全!
  • SMA-BP时序预测 | Matlab实现SMA-BP黏菌算法优化BP神经网络时间序列预测
  • (done) 什么 RPC 协议? remote procedure call 远程调用协议
  • 无告知搜索算法(Python)
  • FCN深度学习语义分割开山之作——学习笔记
  • 更强的可操作性!Midjourney两大重要功能更新!
  • STL二分查找
  • 3285. 找到稳定山的下标
  • Qt的信号槽机制学习一
  • 常见存储器及其特点
  • 【C++习题】12.滑动窗口_将 x 减到 0 的最小操作数
  • vue3+vite 部署npm 包
  • PG数据库之事务处理