目录
(二)包扩展
一、lambda表达式
(一)lambda表达式语法
• lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。 lambda 表达式语法使用层而言没有类型,所以我们一般是用auto或者模板参数定义的对象去接收 lambda 对象。
• lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
• [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使
用,捕捉列表可以传值和传引用捕捉,具体细节下面第二小节再细讲。捕捉列表为空也不能省略。
• (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略。
• ->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
// 一个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象自动推导
// 4、函数题不能省略
auto func1 = []
{
cout << "hello bit" << endl;
return 0;
};
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
(二)捕捉列表
• lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。
• 第一种捕捉方式是在捕捉列表中显示地传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[x,y, &z] 表示x和y值捕捉,z引用捕捉。
• 第二种捕捉方式是在捕捉列表中隐式捕捉,在捕捉列表写一个=表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量。
• 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
• lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使 用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
• 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
int x = 0;
// 在全局定义lambda表达式时,捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
int main()
{
// 定义在局部的lambda表达式只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引用捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
// 隐式引用捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉1
auto func4 = [&, a, b]
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;
// 混合捕捉2
auto func5 = [=, &a, &b]
{
a++;
b++;
/*c++;
d++;*/
return a + b + c + d;
};
func5();
cout << a << " " << b << " " << c << " " << d << endl;
// 局部的静态和全局变量不能捕捉,也不需要捕捉
static int m = 0;
auto func6 = []
{
int ret = x + m;
return ret;
};
// 传值捕捉本质是一种拷贝,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
(三)lambda的应用
• 在学习 lambda 表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda 去定义可调用对象,既简单又方便。
• lambda 在很多其他地⽅用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda 的应用还是很广泛的,以后会不断接触到。
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 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
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;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
return 0;
}
(四)lambda表达式的原理
• lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。范围for底层是迭代器,lambda底层是仿函数对象,也就说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类。
• 仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕 捉的时候,编译器要看使用哪些就传那些对象。
二、可变参数模板
(一)基本语法及原理
• C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
• 基本语法:用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
• 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
• 可以使用【sizeof...】运算符去计算参数包中参数的个数。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包里有0个参数
Print(1); // 包里有1个参数
Print(1, string("xxxxx")); // 包里有2个参数
Print(1.1, string("xxxxx"), x); // 包里有3个参数
return 0;
}
// 原理1:编译本质这里会结合引用折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能支持
// 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
// 上面的一个可变参数模板等价于实现了下面四个模板
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ...
【函数模板】与【可变参数函数模板】作用类比:
① 函数模板:本来要写多个函数,现在写一个函数模板即可;
② 可变参数函数模板:本来要写多个函数模板,现在写一个可变参数模板即可。
可变参数函数模板原理还是通过编译器进行推理可变参数的类型。
(二)包扩展
• 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表(通俗来讲,就是对这些可变数量的参数进行展开和处理的一种机制)。我们通过在模式的右边放一个省略号(...)来触发扩展操作。底层的实现细节如下图所示。
• C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
示例代码如下:
不支持以数组的形式进行访问:
template <class ...Args> void Print(Args... args) { // 可变参数模板编译时解析 // 下面是运行获取和解析,所以不支持这样用 cout << sizeof...(args) << endl; for (size_t i = 0; i < sizeof...(args); i++) { cout << args[i] << " "; } cout << endl; } int main() { Print(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0; }
使用递归的方式进行可变参数的访问:
void ShowList() { // 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数 cout << endl; } template <class T, class ...Args> void ShowList(T x, Args... args) { cout << x << " "; // args是N个参数的参数包 // 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包 ShowList(args...); } // 编译时递归推导解析参数 template <class ...Args> void Print(Args... args) { ShowList(args...); } int main() { Print(); Print(1); Print(1, string("xxxxx")); Print(1, string("xxxxx"), 2.2); return 0; }
ShowList参数解析:
template <class T, class ...Args> void ShowList(T x, Args... args) { cout << x << " "; Print(args...); } //Print(1, string("xxxxx"), 2.2);调用时 //本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数 void ShowList(double x) { cout << x << " "; ShowList(); } void ShowList(string x, double z) { cout << x << " "; ShowList(z); } void ShowList(int x, string y, double z) { cout << x << " "; ShowList(y, z); } void Print(int x, string y, double z) { ShowList(x, y, z); }
其他场景下的理解:
template <class T> const T& GetArg(const T& x) { cout << x << " "; return x; } template <class ...Args> void Arguments(Args... args) {} template <class ...Args> void Print(Args... args) { // 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments Arguments(GetArg(args)...); } // 本质可以理解为编译器编译时,包的扩展模式 // 将上面的函数模板扩展实例化为下面的函数 void Print(int x, string y, double z) { Arguments(GetArg(x), GetArg(y), GetArg(z)); } int main() { Print(1, string("xxxxx"), 2.2); return 0; }
实践当中不太会对可变模版参数进行处理,但要知道有可以取每个参数的操作,实践当中要取可变参数是可以直接匹配:empalce系列接口。
(三)emplace系列接口
接口的声明如下:
template <class... Args>
void emplace_back (Args&&... args);
template <class... Args>
iterator emplace (const_iterator position, Args&&... args);
• C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container<T>,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
程序一:
#include<list> #include"string.h" // emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列 int main() { list<zyb::string> lt;//对象参数为单参数时的场景 zyb::string s1("111111111111"); // 传左值,跟push_back一样,走拷贝构造 lt.emplace_back(s1); cout << "*********************************" << endl; // 右值,跟push_back一样,走移动构造 lt.emplace_back(move(s1)); cout << "*********************************" << endl; // 直接把构造string参数包往下传,直接用string参数包构造string // 这里达到的效果是push_back做不到的 lt.emplace_back("111111111111"); cout << "*********************************" << endl; list<pair<zyb::string, int>> lt1;//对象参数为多参数时的场景 pair<zyb::string, int> kv("苹果", 1); // 跟push_back一样 // 构造pair + 拷贝/移动构造pair到list的节点中data上 lt1.emplace_back(kv); cout << "*********************************" << endl; // 跟push_back一样 lt1.emplace_back(move(kv)); cout << "*********************************" << endl; // 直接把构造pair参数包往下传,直接用pair参数包构造pair // 这⾥达到的效果是push_back做不到的 lt1.emplace_back("苹果", 1); cout << "*********************************" << endl; return 0; }
以string为例,传左值和右值的push_back和emplace_back没区别;不一样的地方在直接把构造string的参数包往下传,则会直接用string参数包构造string。push_back则是构造 + 移动构造 + 析构;两者的效率差距不大,因为移动构造的代价小。
为什么会这样呢:因为push_back为普通函数,类型已知为string,在例子中使用的是右值参数那个万能引用构造,而单参数支持隐式类型转换,也就是先会构造一个临时对象,右值引用的是这个临时对象,再使用移动构造把临时对象的资源给节点对象,最后析构掉这个临时对象。而emplace_back 是一个可变参数模板,类型还未确定,在这里例子中传过来参数后,emplace_back函数通过参数推导后会实例化为const char* 类型的函数,此时不存在构造,然后直接构造一个string给节点的data。
当传入的是多参数的pair,直接传参数的话push_back要写成:push_back({苹果,1}),而emplace_back({苹果,1})这样写则是错误的,因为emplace_back需要一个明确的类型,而这个{}不具有具体类型,只能是如下写法:emplace_back(苹果,1)。
emplace_back的浅拷贝和深拷贝的效率都会提高,特别是浅拷贝,深拷贝的效率提升不够明显。
• emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列。
• 第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。
程序二:
// List.h namespace zyb { template<class T> struct ListNode { ListNode<T>* _next; ListNode<T>* _prev; T _data; ListNode(T&& data) :_next(nullptr) , _prev(nullptr) , _data(move(data)) {} template <class... Args> ListNode(Args&&... args) : _next(nullptr) , _prev(nullptr) , _data(std::forward<Args>(args)...) {} }; template<class T, class Ref, class Ptr> struct ListIterator { typedef ListNode<T> Node; typedef ListIterator<T, Ref, Ptr> Self; Node* _node; ListIterator(Node* node) :_node(node) {} // ++it; Self& operator++() { _node = _node->_next; return *this; } Self& operator--() { _node = _node->_prev; return *this; } Ref operator*() { return _node->_data; } bool operator!=(const Self& it) { return _node != it._node; } }; template<class T> class list { typedef ListNode<T> Node; public: typedef ListIterator<T, T&, T*> iterator; typedef ListIterator<T, const T&, const T*> const_iterator; iterator begin() { return iterator(_head->_next); } iterator end() { return iterator(_head); } void empty_init() { _head = new Node(); _head->_next = _head; _head->_prev = _head; } list() { empty_init(); } void push_back(const T& x) { insert(end(), x); } void push_back(T&& x) { insert(end(), move(x)); } iterator insert(iterator pos, const T& x) { Node* cur = pos._node; Node* newnode = new Node(x); Node* prev = cur->_prev; // prev newnode cur prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(newnode); } iterator insert(iterator pos, T&& x) { Node* cur = pos._node; Node* newnode = new Node(move(x)); Node* prev = cur->_prev; // prev newnode cur prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(newnode); } template <class... Args> void emplace_back(Args&&... args) { insert(end(), std::forward<Args>(args)...); } // 原理:本质编译器根据可变参数模板生成对应参数的函数 /*void emplace_back(string& s) { insert(end(), std::forward<string>(s)); } void emplace_back(string&& s) { insert(end(), std::forward<string>(s)); } void emplace_back(const char* s) { insert(end(), std::forward<const char*>(s)); } */ template <class... Args> iterator insert(iterator pos, Args&&... args) { Node* cur = pos._node; Node* newnode = new Node(std::forward<Args>(args)...); Node* prev = cur->_prev; // prev newnode cur prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(newnode); } private: Node* _head; }; } // Test.cpp #include"List.h" #include"string.h" // emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列 int main() { zyb::list<zyb::string> lt; // 传左值,跟push_back一样,走拷贝构造 zyb::string s1("111111111111"); lt.emplace_back(s1); cout << "*********************************" << endl; // 右值,跟push_back一样,走移动构造 lt.emplace_back(move(s1)); cout << "*********************************" << endl; // 直接把构造string参数包往下传,直接用string参数包构造string // 这⾥达到的效果是push_back做不到的 lt.emplace_back("111111111111"); cout << "*********************************" << endl; zyb::list<pair<zyb::string, int>> lt1; // 跟push_back一样 // 构造pair + 拷贝/移动构造pair到list的节点中data上 pair<zyb::string, int> kv("苹果", 1); lt1.emplace_back(kv); cout << "*********************************" << endl; // 跟push_back一样 lt1.emplace_back(move(kv)); cout << "*********************************" << endl; // 直接把构造pair参数包往下传,直接用pair参数包构造pair // 这⾥达到的效果是push_back做不到的 lt1.emplace_back("苹果", 1); cout << "*********************************" << endl; return 0; }
emplace_back在编译器编译时推测出可变参数的类型后,就会一直向下传,直接给到节点的构造,以单参数传递字符串字面量为例,过程如下图所示:
注意传递参数包过程中,如果是 Args&&... args 的参数包,要用完美转发参数包,方式如下:
std::forward<Args>(args)...
否则编译时包扩展后右值引用变量表达式就变成了左值。
三、新的类功能
(一)默认的移动构造和移动赋值
• 原来C++类中,有6个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 / const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
• 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
• 如果没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
• 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
如下代码所示:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
zyb::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
(二)成员函数声明时给缺省值
成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化,这个在类和对象部分已经写过博客了,更详细的可以取查看。
(三)defult 和 delete
• C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
• 如果能想要限制某些默认函数的生成,在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)
:_name(p._name)
, _age(p._age)
{}
Person(Person&& p) = default;
//Person(const Person& p) = delete;
private:
zyb::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
(四)final 与 override
这个在继承和多态章节的博客已经进行了详细讲过了,可以去该博客进行查阅。
四、STL中的一些变化
• 下图圈起来的就是STL中的新容器,但是实际最有用的是unordered_map和unordered_set。这两个前面已经进行了非常详细的讲解,其他的了解一下即可。
• STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口和移动构造与移动赋值,还有initializer_list版本的构造等,这些前面都讲过了,还有一些无关痛痒的如cbegin/cend等需要时查查文档即可。
• 容器的范围for遍历,这个在容器部分也讲过了。
以上内容仅供分享,若有错误,请多指正。