g++ -g -o main main.cpp -std=c++11
1 智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
C++里面的四个智能指针: auto_ptr , unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用。
1.1 shared_ptr共享的智能指针
std::shared_ptr
使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,共享被管理的对象。同一时刻可以有多个shared_ptr拥有对象的所有权,在最后一个shared_ptr对象销毁时,引用计数减为0,内存才会被释放。
shared_ptr实现包含了两部分,
一个指向堆上创建的对象的裸指针,raw_ptr
一个指向内部隐藏的、共享的管理对象。share_count_object
1.1.1 shared_ptr 的基本用法
-
初始化
通过构造函数、std::shared_ptr辅助函数和 reset 方法来初始化 shared_ptr// 智能指针初始化 std::shared_ptr<int> p1(new int(1)); std::shared_ptr<int> p2 = p1; std::shared_ptr<int> p3; p3.reset(new int(1)); if(p3) { cout << "p3 is not null"; }
我们应该优先使用 make_shared 来构造智能指针,因为他更高效
auto sp1 = make_shared<int>(100); //相当于 shared_ptr<int> sp1(new int(100));
shared_ptr 不能通过将一个原始指针直接赋值给一个智能指针来初始化,需要通过构造函数和辅助方法来初始化。
对于一个未初始化的智能指针,可以通过reset
方法来初始化,当智能指针有值的时候调用 reset 会引起引用计数减1。std::shared_ptr<int> p = new int(1); shared_ptr<int> p1; p1.reset(new int(1))
另外智能指针可以通过重载的bool类型操作符来判断。
use_count
,当前这个堆上对象被多少对象引用了,简单来说就是引用计数。#include <iostream> #include <memory> using namespace std; int main() { std::shared_ptr<int> p1; p1.reset(new int(1)); std::shared_ptr<int> p2 = p1; // 引用计数此时应该是2 cout << "p2.use_count() = " << p2.use_count()<< endl; p1.reset(); cout << "p1.reset()\n"; // 引用计数此时应该是1 cout << "p2.use_count()= " << p2.use_count() << endl; if(!p1) { cout << "p1 is empty\n"; } if(!p2) { cout << "p2 is empty\n"; } p2.reset(); cout << "p2.reset()\n"; cout << "p2.use_count()= " << p2.use_count() << endl; if(!p2) { cout << "p2 is empty\n"; } return 0; }
-
获取原始指针
当需要获取原始指针时,可以通过get方法来返回原始指针std::shared_ptr<int> ptr(new int(1)); int *p = ptr.get(); // 获取到原始指针,不小心 delete p,会造成重复释放;
谨慎使用p.get()的返回值,如果你不知道其危险性则永远不要调用get()函数。
p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
不要保存p.get()的返回值 ,无论是保存为裸指针还是 shared_ptr 都是错误的
保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误 -
指定删除器
如果用 shared_ptr 管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。#include <iostream> #include <memory> using namespace std; void DeleteIntPtr(int *p) { cout << "call DeleteIntPtr" << endl; delete p; } int main() { std::shared_ptr<int> p(new int(1), DeleteIntPtr); return 0; }
当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个
lambda
表达式,上面的写法可以改为:std::shared_ptr<int> p(new int(1), [](int *p) { cout << "call lambda delete p" << endl; delete p;});
当我们用 shared_ptr 管理动态数组时,需要指定删除器,因为 shared_ptr 的默认删除器不支持数组对象
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
1.1.2 使用 shared_ptr 注意问题
-
不要用一个原始指针初始化多个shared_ptr
int *ptr = new int; shared_ptr<int> p1(ptr); shared_ptr<int> p2(ptr); // 逻辑错误,此时use_count()的结果是1 //当p1超出作用域时会调用delete释放p内存,此时p成了悬空指针,当p2超出作用域再次delete的时候就可能会出错。
-
不要在函数实参中创建shared_ptr,对于下面的写法:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针:
shared_ptr<int> p(new int); function(p, g());
-
通过shared_from_this()返回this指针。不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构
class A { public: shared_ptr<A> GetSelf() { return shared_ptr<A>(this); // 不要这么做 } ~A() { cout << "Deconstruction A" << endl; } }; int main() { shared_ptr<A> sp1(new A); shared_ptr<A> sp2 = sp1->GetSelf(); return 0; }
运行后调用了两次析构函数。
由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr,如下所示。
class A: public std::enable_shared_from_this<A> { public: shared_ptr<A>GetSelf() { return shared_from_this(); // } ~A() { cout << "Deconstruction A" << endl; } }; int main() { shared_ptr<A> sp1(new A); shared_ptr<A> sp2 = sp1->GetSelf(); // ok return 0; }
-
避免循环引用。循环引用会导致内存泄漏
class A { public: std::shared_ptr<B> bptr; ~A() { cout << "A is deleted" << endl; } }; class B { public: std::shared_ptr<A> aptr; ~B() { cout << "B is deleted" << endl; } }; int main() { { std::shared_ptr<A> ap(new A); std::shared_ptr<B> bp(new B); ap->bptr = bp; bp->aptr = ap; } cout<< "main leave" << endl; // 循环引用导致ap bp退出了作用域都没有析构 return 0; }
循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把A和B任何一个成员变量改为weak_ptr。
1.2 unique_ptr 独占的智能指针
unique_ptr 是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr 赋值给另一个 unique_ptr 。
unique_ptr 不允许复制,但可以通过函数返回给其他的 unique_ptr,还可以通过std::move
来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。
std::make_shared是c++11的一部分,但std::make_unique不是。它是在c++14里加入标准库的。
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别
- unique_ptr可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]); ptr[9] = 9; std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
- unique_ptr指定删除器和shared_ptr有区别
unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器std::shared_ptr<int> ptr3(new int(1), [](int *p){delete p;}); // 正确 std::unique_ptr<int> ptr4(new int(1), [](int *p){delete p;}); // 错误 std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //正确
关于shared_ptr
和unique_ptr
的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就用 unique_ptr,如果希望多个智能指针管理同一个资源就用 shared_ptr。
1.3 weak_ptr 弱引用的智能指针
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用
,使引用计数失效,从而导致内存泄漏。
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁
问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
weak_ptr没有重载操作符*和->,因为它不共享指针
,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。weak_ptr还可以返回this指针和解决循环引用的问题。
1.3.1 weak_ptr 基本用法
- 通过use_count()方法获取当前观察资源的引用计数
shared_ptr<int> sp(new int(10)); weak_ptr<int> wp(sp); cout << wp.use_count() << endl; //输出1,weak不计数
- 通过expired()方法判断所观察资源是否已经释放
shared_ptr<int> sp(new int(10)); weak_ptr<int> wp(sp); if(wp.expired()) cout << "weak_ptr无效,资源已释放"; else cout << "weak_ptr有效";
- 通过lock方法获取监视的shared_ptr
std::weak_ptr<int> gw; void f() { if(gw.expired()) { cout << "gw无效,资源已释放"; } else { auto spt = gw.lock(); cout << "gw有效, *spt = " << *spt << endl; } } int main() { { auto sp = atd::make_shared<int>(42); gw = sp; f(); } f(); return 0; } gw有效, *spt = 42 gw无效,资源已释放
1.3.2 weak_ptr 返回this指针
shared_ptr 章节中提到不能直接将this指针返回shared_ptr,需要通过派生std::enable_shared_from_this
类,并通过其方法shared_from_this
来返回指针,原因是std::enable_shared_from_this类中有一个weak_ptr
,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法时,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回。
class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
return shared_from_this(); //
}
~A()
{
cout << "Deconstruction A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf(); // ok
return 0;
}
// 输出:Deconstruction A 析构了一次
在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内
部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,sp2的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
需要注意的是,获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。
1.3.3 weak_ptr解决循环引用问题
因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::weak_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所
以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
1.3.4 weak_ptr使用注意事项
-
weak_ptr在使用前需要检查合法性。
weak_ptr<int> wp; { shared_ptr<int> sp(new int(1)); //sp.use_count()==1 wp = sp; //wp不会改变引用计数,所以sp.use_count()==1 shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象 } shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;
因为上述代码中sp和sp_ok离开了作用域,其容纳的对象已经被释放了。
得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。
因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法知道自己所容纳的那个指针资源的当前状态。
weak_ptr<int> wp; shared_ptr<int> sp_ok; { shared_ptr<int> sp(new int(1)); //sp.use_count()==1 wp = sp; //wp不会改变引用计数,所以sp.use_count()==1 sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象 } if(wp.expired()) { cout << "shared_ptr is destroy" << endl; } else { cout << "shared_ptr no destroy" << endl; } // shared_ptr no destroy
1.4 智能指针安全问题
引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论:
-
多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。
比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr
std::thread td([&sp1]()){....});
又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用
void fn(shared_ptr<A>&sp) { ... } .. std::thread td(fn, sp1);
这时候必然不是线程安全的。
-
多线程代码操作的不是同一个shared_ptr的对象
这里指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕获的对象。std::thread td([sp1]()){....});
另个线程传递的shared_ptr是值传递,而非引用:
void fn(shared_ptr<A>sp) { ... } .. std::thread td(fn, sp1);
这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。
也就是说,如下操作是安全的。void fn(shared_ptr<A>sp) { ... if(..){ sp = other_sp; } else { sp = other_sp2; } }
**需要注意:所管理数据的线程安全性问题。**显而易见,所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象A自己去保证。
2 右值引用
C++11中引用了右值引用和移动语义,可以 避免无谓的复制
,提高了程序性能。
左值
是表达式结束后仍然存在的持久对象,右值
是指表达式结束时就不存在的临时对象。
区分左值和右值:能不能对表达式取地址&,如果能则为左值,否则为右值;
右值引用
就是对一个右值进行引用的类型
(右值引用是类型,与其本身是左值还是右值无关,左值右值都有可能)。因为右值没有名字,所以我们只能通过引用的方式找到它。右值引用也是引用,必须立即初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又“重获新生”(临时对象也是资源,为什么一定要销毁,现在直接拿来用),其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。
&& 的总结如下:
- 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。
- auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
- 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。
- 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
2.1 右值引用避免深拷贝,优化性能
产生一个临时变量,通过这个临时变量拷贝构造了一个新的对象,临时变量在拷贝构造完成之后就销毁了,如果深拷贝的堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。
class A
{
public:
A() :m_ptr(new int(0)) {
cout << "constructor A" << endl;
}
A(const A& a) :m_ptr(new int(*a.m_ptr)) {
cout << "copy constructor A" << endl;
}
A(A&& a) :m_ptr(a.m_ptr) {
a.m_ptr = nullptr;
cout << "move constructor A" << endl;
}
~A(){
cout << "destructor A, m_ptr:" << m_ptr << endl;
if(m_ptr)
delete m_ptr;
}
private:
int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
A a;
A b;
cout << "ready return" << endl;
if (flag)
return a;
else
return b;
}
int main()
{
{
A a = Get(false); // 正确运行
}
cout << "main finish" << endl;
return 0;
}
上面的代码中没有用到拷贝构造,取而代之的是移动构造
( Move Construct)。
移动构造函数的参数是一个右值引用
类型的参数 A&&,只是将临时对象的资源做了浅拷贝,没有进行深拷贝,避免了对临时对象的深拷贝,提高了性能。这也就是移动语义
( move 语义),右值引用的一个重要目的是用来支持移动语义的。
这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。
2.2 移动(move)语义
移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?
C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
2.3 forward 完美转发
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
Template<class T>
void func(T &&val);
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!
int &&a = 10;
int &&b = a; //错误
int &&b = std::forward<int>(a); // 正确
a是一个右值引用
,但其本身a也有内存名字,所以a本身是一个左值
,再用右值引用引用a这是不对的。
因此我们有了std::forward()
完美转发,这种T &&val中的val是左值
,但如果我们用std::forward (val),就会按照参数原来的类型转发;
template <class T>
void Print(T &t)
{
cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
Print(t);
Print(std::move(t)); // 肯定调用R
Print(std::forward<T>(t));
}
int main()
{
cout << "-- func(1)" << endl;
func(1);
int x = 10;
int y = 20;
cout << "-- func(x)" << endl;
func(x); // x本身是左值
cout << "-- func(std::forward<int>(y))" << endl;
func(std::forward<int>(y)); //
return 0;
}
-- func(1) // 1是右值
L1 // 不处理直接打印是左值,t是右值引用,是有名字的,是左值
R1 // move左值变右值
R1 // 本身是右值(传进来的是右值),t引用了它
-- func(x) // x是左值
L10 //
R10 // move转右值
L10 // 传进来的是左值
-- func(std::forward(y)) // forward(左值)结果是右值,经过 T&&t 时,原来是什么就是什么
L20
R20 //
R20 // 传进来的是右值
2.4 emplace_back
vector<string> testVec;
testVec.push_back(string(16, 'a'));
首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
其次,vector内会创建一个新的string对象,这是第二次构造。
最后在push_back结束时,最开始的临时对象会被析构。这两行代码会涉及到两次string构造和一次析构。
c++11可以用emplace_back
代替push_back
,emplace_back可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而达到优化的目的。
3 lambda 匿名函数
3.1 匿名函数语法
[捕获列表] (参数列表)->返回类型{函数体}
int main()
{
auto Add = [](int a, int b)->int { // 编译器可以推断返回类型,函数体内有多个返回类型时,无法自动推断
return a + b;
};
std::cout << Add(1, 2) << std::endl; //输出3
return 0;
}
3.2 捕获列表
int main()
{
int c = 12;
auto Add = [c](int a, int b)->int { //捕获列表加入使用的外部变量c,否则无法通过编译
// 捕获到的是c=12,在Add后面改变c的值,不会影响函数里的值
return c;
};
std::cout << Add(1, 2) << std::endl;
return 0;
}
int main()
{
int c = 12;
auto Add = [&c](int a, int b)->int {
//捕获列表改为了&c,表示按引用传递,就可以修改了;不加&表示按值传递,无法通过编译
c = a;
return c;
};
std::cout << Add(1, 2) << std::endl;
return 0;
}
- 如果捕获列表为[&],则表示所有的外部变量都按引用传递给lambda使用;
- 如果捕获列表为[=],则表示所有的外部变量都按值传递给lambda使用;
- 匿名函数构建的时候对于按值传递的捕获列表,会立即将当前可以取到的值拷贝一份作为常数,然后将该常数作为参数传递。
- 匿名函数由捕获列表、参数列表、返回类型和函数体组成;
- 可以忽略参数列表和返回类型,但不可以忽略
捕获列表
和函数体
,auto f = []{ return 1 + 2; };
[ ] | 空捕获列表,Lambda不能使用所在函数中的变量。 |
---|---|
[name_a,name_b] | 逗号分隔的名字列表,这些名字都是Lambda所在函数的局部变量。默认情况下,这些变量会被拷贝,然后按值传递,名字前面如果使用了&,则按引用传递 |
[&] | 隐式捕获列表,Lambda体内使用的局部变量都按引用 方式传递 |
[=] | 隐式捕获列表,Lanbda体内使用的局部变量都按值 传递 |
[&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量,这些变量采用值捕获的方式,其他变量则被隐式捕获,采用引用方式传递,identifier_list中的名字前面不能使用&。 |
[=,identifier_list] | identifier_list中的变量采用引用方式捕获,而被隐式捕获的变量都采用按值传递的方式捕获。identifier_list中的名字不能包含this,且这些名字面前必须使用&。 |