注意和初始化列表不同(初始化列表是类成员变量定义的地方),列表初始化是C++11支持的一种定义变量的方式。
C++98中,定义变量都需要使用=,但在C++11中,定义变量可以不用=,只用{}大括号就能定义变量(大括号里写变量的初始值)。这个特性在new数组时经常使用
如果初始化的数组存储的元素不是内置类型而是自定义类型,元素和元素之间的初始化要用逗号间隔,并且{}用大括号表示数组中的一个元素。实际上,C++11的列表初始化只推荐在new数组时使用,在其他地方使用会降低代码的可读性,并且不够美观。
C++11中,一串被用大括号{}引用的数据叫做initializer_list,是C++11新加入的数据结构
用initializer_list可以为容器赋值,随便打开一个容器的文档,在C++11标准中都能看到形参为initializer_list的构造函数
用initializer_list为容器赋值的操作也是经常被使用的,这样就不用循环调用插入接口
void test7()
{
myVector::vector<int> v1 = { 3,4,1,5 }; // 调用形参为initializer_list的构造函数
myVector::vector<int> v2;
v2 = { 3, 4, 1,2 }; // 调用形参为initializer_list赋值重载函数
for (auto e : v1)
{
cout << e << ' ';
}
cout << endl;
for (auto e : v2)
{
cout << e << ' ';
}
cout << endl;
}
形参为initializer_list的构造函数大概是这样实现的:STL中有initializer_list容器,所以用花括号括起来的数都被放到了容器中,假如要用initializer_list构造vector,首先需要为vector开辟足够的空间,使_start指向这块空间,_finish和_start相同,_endofstore指向空间的最后,接着用迭代器(或者范围for)遍历initializer_list,调用vector的push_back接口,将其中的所有数插入到vector中。
template <class T>
myVector::vector<T>::vector(const initializer_list<T>& li)
{
_start = new T[li.size()];
_finish = _start;
_endofstorage = _start + li.size();
for (auto e : li)
{
push_back(e);
}
}
形参为initializer_list的赋值重载实现:赋值重载会用新数据取代原来的旧数据,一般会用新数据构造一个新的容器,将新容器的资源与旧容器的资源交换,函数结束前,旧容器的资源被新容器带走并释放。
template <class T>
myVector::vector<T>& myVector::vector<T>::operator=(const initializer_list<T>& li)
{
vector<T> tmp(li);
swap(tmp);
return *this;
}
所以,在初始化map对象时,可以不用使用make_pair函数或者构造pair对象,因为map实现了initializer_list的构造函数,用列表初始化的方式就能构造map对象
但是如果类没有实现形参为initializer_list的构造函数,编译器会对此进行隐式类型转换,用列表中的内容构造一个类的临时对象,再用临时对象赋值。比如上面的代码,Date类没有实现形参为initializer_list的构造函数,编译器就会用列表中的内容构造一个Date临时对象,再把临时对象中的内容拷贝到d中(但编译器认为一次构造和一次拷贝构造有点多余,于是将两次构造合并为一次构造,即用列表中的内容作为实参,调用有对应形参的构造函数)。
左值:可以被取地址的值,如变量或指针解引用。右值:不能被取地址的值,如表达式,字面常量。
int a = 10;
int* p = &a; // a和p都是左值,可以被取地址
// 右值有:(字面常量)10, a, (函数调用表达式)func(a),(算术表达式)1 + 2
左值引用用来引用左值,右值引用用来引用右值。但加了const的左值引用可以引用右值,右值可以引用move后的左值。
const修饰的左值可以引用右值:因为右值不能取地址,因为不能通过地址修改数据,用左值引用右值,就可以通过左值引用修改右值数据,属于权限的放大,权限放大这样的操作是被禁止的,因为这是不安全的。
右值可以引用move后的左值:move函数改变了左值的语义,使左值变为右值
右值指的是不能被取地址的值,但引用了一个右值后,右值会被存储到可写数据区中,所以可以修改r引用,也能取r引用的地址。
我们称内置类型的右值为纯右值,称自定义类型的右值为将亡值。为什么是将亡值?自定义类型在作为函数返回值或者隐式类型转换时,会产生一个临时对象,这个临时对象具有常属性,即不能修改,不能取地址,是一个右值,临时变量的作用是:作为中间值,将自己的数据赋值给其他变量,赋值完成后临时变量销毁,即临时变量的生命周期很短
// 隐式类型转换,先用hello构造一个string的临时对象,该临时对象是一个将亡值
string str1 = "hello";
string str2 = to_string(1234);
将1234转换成string,在函数中转换的结果是一个局部变量,不能用左值引用返回局部变量,但如果拷贝一个string返回则需要深拷贝,效率低,函数将转换的结果move成右值再返回。需要使用右值的场景还有迭代器的后置++。
用一个将亡值构造或赋值一个对象,如果涉及到资源管理,需要用到深拷贝吗?既然将亡值马上会被释放,并且将亡值中的数据是我们想要的,为什么不直接把将亡值的数据掠夺?将自己的资源与将亡值交换,将亡值还会释放你的资源(当你没有资源时,需要把指针置空,否则程序会崩溃)。但要注意move不能随意使用,比如下面的代码,move了str1,用str1构造str3,此时的str1被销毁,后续的代码不能再使用str1。
myString::string::string(string&& str)
{
_str = nullptr;
_size = 0;
_capacity = 0;
swap(str);
cout << "移动构造" << endl;
}
myString::string& myString::string::operator=(string&& str)
{
swap(str); // 交换过后会释放原来的空间
cout << "移动赋值" << endl;
return *this;
}
再说一个移动构造的例子:to_string,将整形转化成string,转换的结果是一个局部变量,出了函数会销毁,所以函数要返回结果的深拷贝。
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
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;
}
}
在C++98中,函数返回str,会深拷贝一个临时对象,再用临时对象深拷贝给接收对象,但编译器会做优化,即延长str存储资源的生命周期(注意不是延长str的生命周期,str在出了函数作用域就要销毁了,只是里面存储的资源生命周期延长了),编译器直接将str的资源深拷贝到接收对象中,省略了中间过程产生的临时对象。
在C++11中,由于右值引用的出现,这个函数返回的过程不再进行深拷贝,而进行资源转移
string ret;
ret = to_string(1234); // 移动赋值
// string ret = to_string(1234); // 移动构造
str是整形转换后的字符串,函数最后的return str时,编译器会创建临时对象,将str的资源深拷贝到临时对象中,再对临时对象使用移动构造,将资源转换到接收对象ret中。但编译器还会做优化,虽然没有显式的写move函数,但是编译器会大胆的将str识别成右值,即将亡值,编译器直接将str的资源转移到接收对象ret中(转移调用移动赋值或者调用移动构造)。
(随便打开一个容器的官方文档都可以看到移动构造的接口)
总之,C++11的移动构造和移动赋值为C++带来了效率上的极大提升,是一个实用的更新。
默认生成的移动构造
现在有一个Person类,里面有两个成员,我们实现的string类和内置类型int,现只完成Person的构造函数,调用其移动构造和移动赋值,通过string的输出,验证默认生成的移动构造和移动赋值函数是否调用string的相应函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
myString::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = move(s1);
Person s4;
s4 = move(s3);
return 0;
}
Person s2 = s1,用s1对象构造s2,s1是一个左值,所以对于string,这里调用的是深拷贝。Person s3 = move(s1),move后的s1属性改变为右值,对于string,调用的是移动构造,s4 = move(s3)调用的则是移动赋值。
为Person类添加一个析构函数,使之不生成默认的移动构造和移动赋值,再次运行同样的代码。
在函数声明后加上= default,表示强制生成该函数的默认版本,= delete表示不生成该函数的默认版本,也称= delete修饰的函数为删除函数
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 <class T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
int a = 10;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 10;
PerfectForward(b); // const左值
PerfectForward(move(b)); // const右值
return 0;
}
一个模板参数T,T&&不是类型为T的右值引用,语法规定T&&为万能引用,万能引用能接收左值和右值。但在接收右值后,t会被折叠成左值。
(万能引用的折叠)
完美转发可以保持传输过程的值属性,即使t被折叠成左值,forward也能将其属性转为右值。有了完美转发,万能引用就变得名副其实了。
(完美转发保持了对象的原属性)
和移动构造,移动赋值类似,Insert有时也需要深拷贝,但是深拷贝右值可以被优化成资源的转换。
比如vector容器,存储的元素是string类型,调用vector的Insert时,插入的对象明显是一个string
template <class T>
void myVector::vector<T>::push_back(const T& val)
{
insert(end(), val);
}
(push_back复用了insert接口)
存储string的vector,模板参数是string,想接收右值,push_back的参数就需要用右值引用,但模板参数类型没有右值引用,只有万能引用,虽然万能引用也能接收右值,但接收完右值会触发折叠,右值属性变为左值,所以insert这样的接口,想要实现资源转移就需要用完美转发
首先push_back的参数使用万能模板接收右值,push_back调用了insert接口,push_back传参时,需要将接收到的右值(已经被折叠成左值了)完美转发后再传。
template <class T>
void myVector::vector<T>::push_back(T&& val)
{
insert(end(), forward<T>(val)); // 传参时注意完美转发
}
insert的参数也要用万能模板,insert使用val值时也要完美转发。
template <class T>
typename myVector::vector<T>::iterator myVector::vector<T>::insert(iterator pos, T&& val)
{
assert(pos >= _start && pos <= _finish);
// 同样也是先检查
if (size() == capacity())
{
// 为防止迭代器失效,先记录pos的相对位置
size_t n = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
pos = _start + n;
}
iterator end = _finish;
while (end > pos) // 当end小于pos时,不再挪动数据
{
*end = *(end - 1);
end--;
}
*pos = forward<T>(val); // 注意完美转发
cout << "完美转发" << endl;
_finish++;
return pos;
}
C++11支持可变参数模板,即函数模板和类模板可以接受0到N个模板参数。关于可变参数,c语言的printf就是一个典型的例子,printf函数不知道你将要传几个参数,但可以依次解析所有参数并完成函数功能。
template <class T, class ...Args>
void ShowArgs(T t, Args... args)
{
cout << typeid(t).name() << "->" << t << endl;
ShowArgs(args...);
}
template <class T>
void ShowArgs(T t)
{
cout << typeid(t).name() << "->" << t << endl;
}
Args是一个模板参数包,args是一个函数形参参数包,参数包中有任意个参数。怎么解析参数包中的参数?一般使用函数递归解析参数,函数的第一个参数为普通模板参数,第二个为模板参数包,假如调用ShowArgs函数传入的参数包中有4个参数,那么第一个参数类型会被模板T接受,剩下三个参数会被模板参数包Args接受,函数递归调用时,会将只有三个参数的函数形参参数包传给下一个函数,直到实参的参数包参数个数为1,函数就调用只有一个参数的ShowArgs函数,递归结束。
或者不写只有一个参数的ShowArgs函数,写一个无参ShowArgs函数,当实参的参数包参数个数为0时,函数会去调用无参的ShowArgs函数,递归也会结束
template <class T, class ...Args>
void ShowArgs(T t, Args... args)
{
cout << typeid(t).name() << "->" << t << endl;
if (sizeof...(args) == 0)
return;
ShowArgs(args...);
}
如果在函数中添加条件:当参数包的参数个数为0时就返回函数,是否可行?答案是不能通过编译。由于可变参数包的存在,编译器编译时会进行相关的推导,当参数包的参数个数为0时,ShowArgs没有无参的版本,最多只有一个参数t,args参数包的参数个数为0情况,所以这里编译不能通过。而计算参数包的参数个数是在运行时才能得到的,所以编译期间不能判断这一条件是否能成立。
template <class T>
void PrintArgs(T t)
{
cout << t << ' ';
}
template <class ...Args>
void ShowArgs(Args... args)
{
int arr[] = { (PrintArgs(args), 0)... };
}
int main()
{
ShowArgs(1, 2, 3.3, "string");
}
使用数组+列表初始化的方式解析参数包,列表初始化会用列表中的所有数据初始化对象,也就是说将整个参数包放到数组中,如果参数包中的参数类型都是一样的,可以不使用逗号表达式,直接int arr[] = { args… }就行,但参数包包含了不同的参数类型,而数组只能存储同一类型的元素,所以使用逗号表达式,将参数包传给解析函数PrintArgs,再以0结束逗号表达式,这样表达式结果就是0,数组得到的数也是0,数组中有几个0,参数包就有几个参数。
大多容器都有emplace接口,与insert的作用类似,emplace接收一个万能模板参数包,insert只接收指定类型的数据,所以如果容器存储自定义类型的数据,使用insert只能先构造相应类型的对象,再将该对象作为参数传给函数。而emplace接收所有类型的参数,使用该接口不用构造自定义对象,emplace的底层会用传入的参数包构造对象,但是每次调用emplace函数只能传入一个成员包含的参数个数,比如list存储pair,那么调emplace时只能传两个参数,构造一个成员。
int main()
{
std::list< std::pair<int, myString::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
cout << endl;
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort" });
return 0;
}
四次向list插入元素,两次emplace是直接构造对象,emplace插入是直接构造对象,insert则是构造临时对象再拷贝构造
C++中有这样一个库函数,sort
sort会对迭代器区间[first, last]进行排序,默认区间中的数是内置类型,即可以用<,>这样的操作符进行比较的数,并且默认是按升序进行排序。要是区间中的数为自定义类型,即不支持比较操作符进行比较的类型,使用sort排序需要再传一个仿函数
仿函数,即定义一个类,该类对()操作符进行重载,重载函数根据比较对象的大小关系返回bool值
struct ComPriceGreat
{
bool operator()(const Goods& gl, const Goods& gr) const
{
return gl._price > gr._price;
}
};
struct ComPriceLess
{
bool operator()(const Goods& gl, const Goods& gr) const
{
return gl._price < gr._price;
}
};
int main()
{
Goods goods[] = {{"苹果", 2.2, 4}, {"香蕉", 3.3, 2}, {"梨子", 1.1, 9}};
sort(goods, goods + 3, ComPriceGreat());
return 0;
}
比如有Goods这样一个类,有两个Goods对象g1,g2,很显然g1 < g2这个表达式是非法的(除非Goods类对<进行重载,即使重载也只能支持一种特定的比较),这时就能定义一个仿函数:写一个类ComPriceGreat,类名是以价格的降序进行比较的意思,类重载()操作符,重载函数返回左操作数的价格是否大于右操作数价格的结果,那么sort函数在进行Goods类对象比较时就能用comp类生成一个对象_comp(comp是一个模板参数,用来接收ComPriceGreat),接着_comp(g1, g2),这个表达式会调用ComPriceGreat重载()的函数,g1的价格大于g2的价格则返回true,sort根据返回的结果决定是否要进行调整。
虽然仿函数使得自定义类型也能用sort进行比较,但如果要用价格的降序进行排序,就有要写一个类,支持()的重载,C++11为简化这样的语法,推出了lambda表达式
书写格式:[capture-list] (parameters) mutable -> reutrn-type{statement)
captrue-list:捕捉列表,出现lambda的初始位置,用来捕捉上下文中的变量,供lambda使用
parameters:参数列表,和函数的参数列表一样,接收lambda外传入的变量,供lambda使用,如不需要接收参数,则可以连同()一起省略,
mutable:默认的lambda是一个const函数,即接收的参数为const,不能修改,mutable的意思是易变,加上mutable就可以修改参数,即参数的const属性丢失。要使用mutable就不能省略(),否则编译报错
return-type:lambda返回的参数类型,没有返回值或返回类型明确的情况下,可以省略不写,编译器会自动推导
statement:函数体,书写函数的具体功能
lambda表达式底层是一个无类型函数对象,刚刚的仿函数可以用lambda写
int main()
{
Goods goods[] = {{"苹果", 2.2, 4}, {"香蕉", 3.3, 2}, {"梨子", 1.1, 9}};
sort(goods, goods + 3, ComPriceGreat());
auto comPriceLess = [](const Goods& gl, const Goods& gr) {return gl._price < gr._price; };
sort(goods, goods + 3, comPriceLess);
return 0;
}
auto comPriceLess = [](const Goods& gl, const Goods& gr) {return gl._price < gr._price; };
[capture-list] (parameters) mutable -> reutrn-type{statement)
对照基本格式:lambda的捕捉列表没有捕捉对象,参数列表接收两个Goods类对象,没有取消参数的const属性,没有指定返回类型是bool,因为编译器可以自动推导返回值类型,函数最后返回两者比较的结果。因为lambda是一个匿名对象,从编译器的角度,lambda其实是一个对()操作符重载的类的对象,但这个类是匿名的,使用者不用构造这样一个类,只需要将重载()的函数写出即可,编译器会自动构造这样一个类,所以lambda的类型是不可知的,接收lambda函数时所用的类型是auto,至于后面的comPriceLess则是一个对象名,一个无名类的对象名,所以通过对象名就可以使用lambda表达式。
sort(goods, goods + 3, comPriceLess); // 等价于传仿函数
lambda的设计成功之处在于:捕捉列表的灵活使用。
[] 表示捕捉对象
[var] 以值传递捕捉var对象,但不可修改var
[&var] 以引用的方式捕捉var对象,可修改
[=] 以值传递捕捉父作用域中的所有变量,但不可修改
[&] 以引用的方式捕捉父作用域中的所有变量,可修改
捕捉列表可有多个捕捉项构成,并以逗号分隔,比如
[&a, &b, =],以引用的方式捕捉a和b,以值传递的方式捕捉其他变量
[a, b, &],以值传递的方式捕捉a和b,以引用的方式捕捉其他变量
但是不能用=捕捉父作用域的所有变量,再捕捉父作用域中的变量(不以值传递方式),编译会报错
lambda之间不能赋值,原因是每个lambda表达式都是不同类的对象,每个lambda表达式的类型都不同,不同类型之间不支持赋值(内置类型可能会发生隐式类型转换,但自定义类型的不同的类型对象之间要想支持赋值,就必须重载=,而lambda是一个匿名类的对象,使用者无法接触到这个匿名类,更不用说重载该匿名类的=了)
lambda表达式可以赋值给同类型的函数指针(这个语法很奇怪,并且很少使用,所以了解一下。lambda表达式的底层是一个函数对象(仿函数),该对象的类型虽然是匿名不可见的,但也不可能是一个指针,两个类型类型不相同却可能这样赋值。并且用函数对象赋值给函数指针是会报错的,而lambda却不会)
lambda和范围for一样,表面十分简洁,不用写繁琐的代码,实际上,这些繁琐的代码只是编译器代替你写了。lambda在被编译器处理后,生成了一个匿名类,该匿名类对()进行了重载,lambda就是该类的一个对象,调用lambda表达式实际是调用该匿名类operator()函数。这一点可以用汇编代码验证
虽然不能完全看懂汇编,但可以看出以函数对象的方式调用和lambda调用都会调用一个operator()函数,侧面证明了lambda的底层是一个匿名类的对象。
至于lambda的operator()之前的一长串字符串,则是一串不会重复的uuid(因为lambda底层是匿名类的operator()重载,同一个类只能重载一次,所以作为匿名类的对象,lambda不能重复
ret = func(x); 这行代码中的func可能是什么?有四种情况:
1.函数名
2.仿函数
3.lambda表达式
4.函数指针
如果这行代码在模板中
template<class F, class T>
T useF(F f, T x)
{
// 设置静态变量count,如果模板实例化成一个函数,那么count的地址都是相同的
// 如果模板实例化成多个函数,count的地址则是不同的,count也会一直变大
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
// 将x作为参数调用f
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
double (*pf)(double) = &f;
// 函数指针
cout << useF(pf, 11.11) << endl;
// 函数名
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;
}
从结果可以看出,只有参数是函数名和函数指针是,模板才会被实例化成同一个函数,因为count地址相同,并且count的值在增加,而参数为函数名(函数指针),仿函数和lambda表达式时,分别实例化了三份函数,因为count的地址不同,且count的值不再增长。但是这三个“函数”的调用方式都是一样的:使用()调用,却被实例化成不同的函数,实例化多份的效率肯定比只实例化一份的效率低,为解决这个问题,可以使用包装器。
包装器的本质是一个类模板,其声明如下
template <class Ret, class... Args>
class function<Ret(Args...)>;
Ret:函数返回值类型
Args:函数的参数类型
有了函数包装器,就能将三种不同的“函数”包装成统一的函数,这样模板就不会被实例化成多份,解决了效率低下的问题。
拿f函数举例,其返回值是double,参数也是double,那么function的Ret就是double,参数包Args只有一个double,所以包装器的类型就是function
int main()
{
// 函数名
function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
function<double(double)> func3 = [](double d)->double { return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
通过打印结果可以看到count的地址是相同的,count的值也在一直增加,所以使用包装器后,函数模板只实例化了一份函数,解决了效率低的问题。
非静态成员函数的包装:非静态成员函数的参数有一个隐藏的this指针,所以包装器的不能只接收函数显式写出的两个参数类型,还要接收对象的类型,并且是第一个接收。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
比如Plus类有两个成员函数,一个静态成员函数plusi,一个非静态成员函数plusd,包装静态成员函数的写法与包装普通函数一样,因为静态成员函数没有this指针,而非静态成员函数有this指针,在包装时要添加对象的类型
// 静态成员函数
function<int(int, int)> func1 = &Plus::plusi;
func1(10, 20);
// 非静态成员函数
function<double(Plus, double, double)> func2 = &Plus::plusd;
func2(Plus(), 10.11, 20.11);
func2比func1的模板参数多了个Plus,调用func2时需要将Plus的对象作为第一个参数。并且语法规定,在包装非静态成员函数时,必须在函数名前加上&符。
bind函数绑定器可以改变函数的参数个数和参数顺序。bind也能理解为一个函数适配器,接收一个可调用对象(函数指针,仿函数,lambda表达式),生成一个新的可调用对象,以适应新的参数列表。
比如非静态成员函数,因为this指针的存在,用包装器包装时需要三个模板参数,而其他函数包装却只需要两个模板参数,那么就可以用bind函数绑定器修改非静态成员函数的参数个数。
function<double(double, double)> func1 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
第一个参数&Plus::plusd表示原可调用对象(生成的新可调用对象与原可调用对象类型相同),如果不写&,生成的新可调用对象也不会有&,因为要包装非静态成员函数,所以这里需要&(不写&也行)。接着剩下的参数就是新可调用对象的参数列表,Plus()表示将Plus()作为原可调用对象的第一个参数,_1和_2表示占位符,也就是新可调用对象有两个参数需要传入。
如果不使用bind,非静态成员函数plusd被包装后需要传三个参数,但用bind绑定后只需要传两个参数。可以这么理解,func1是:用bind绑定后再被包装生成的可调用对象,func1只有两个参数,但实际上第一个参数在bind绑定时就已经传了,也就是Plus(),剩下两个参数等待传入。所以调用func1对象时,第一个参数总是Plus(),因为该参数被bind绑定。
_1和_2有关参数的顺序,_1参数对应着原可调用对象去除被绑定参数后的第一个参数,_2以此类推,如果_2写在了_1前面,那么调用时的第一个实参对应去除被绑定参数后的原可调用对象的第二个形参,第二个实参也就对应去除被绑定参数后的原可调用对象的第一个形参,改变了参数顺序。但改变参数顺序较少使用,bing绑定器多用于改变参数个数。