说到“循环引用”,其中“自己对自己”的引用是最直接的循环引用,如图10-12所示。
而说到“自己”,在C++语言中应该首先想到的类的“this”指针。不过,this指针是裸指针,如果我们在类中,需要传递当前对象本身,可是接受方却只能接受本类的智能指针,那该怎么办呢?比如前面提到的按个函数:
Coo::Ptr foo(int a, Coo::Ptr pcoo)
{
...
}
foo函数有个入参必须是shared_ptr <Coo> 类型,假设在Coo中需要调用该函数:
class Coo
{
public:
typedef std::shared_ptr <Coo> Ptr;
void Test()
{
foo(123, ______);
...
}
...
};
下划线处,该填写什么呢?填this? foo不接受,编译失败。
既然this是裸指针,那就从它身上创建一个shared_ptr呗:
void Test()
{
shared_ptr <Coo> shared_this(this);
foo(123, shared_this);
...
}
编译,成功!解决问题易如反掌啊,可惜乐得太早了,有大问题发生了。
先看看,如果类中this所代表的当前Coo对象,是这么创建的:
//灾难一:
int main()
{
Coo coo_1; //coo_1是个栈对象
coo_1.Test();
}
使用coo_1调用成员函数Test()时,Test()中的this就是coo_1。coo_1是一个栈对象,它会在main()函数结束时自动释放。然而,foo()函数接过去的那个shared_ptr对象,它也会尝试释放所拥有的裸指针,也就是this,而 *this就是coo_1,而coo_1……它是一个会自动释放的栈对象!一个对象会被释放两次就是灾难,如果这个对象还是栈对象,那就是史诗般的灾难了。大家可以试试,
要不小心点,使用堆对象吧:
//灾难二:
int main()
{
Coo* coo_2 = new Coo; //coo_2是个堆对象
coo_2->Test();
//coo_2->Test();
}
现在的问题是,main()函数中要不要主动delete coo_2?如果释放,就会发生重复释放的问题,如果不释放,有谁能从这几行代码中看出,当coo_2第一次调用了Test()之后,coo_2就很可能“挂掉了”呢?如果有人又调用Test()一次,他知道其实coo_2是一个尸体吗?
还好《白话C++》教会我们,如果一个类的对象需要用到shared_ptr,那就在一开始绑定shared_ptr,所以,第三个版本的灾难片呼啸而来:
//灾难三:
int main()
{
Coo::Ptr coo_3 = make_shared <Coo> ();
coo_3->Test();
}
因为绑定了shared_ptr,所以背地里所创建的Coo堆对象肯定会被释放啦,然而一运行程序,世界还是崩溃了。
问:出现过几个shared_ptr在共同管理背后所创建的Coo堆对象?
答:两个,一个是coo_3,另一个是Test()成员函数中的shared_this。
再问:那么,shared_this是从前一个智能指针创建出来的吗?
答:不是,它是从裸指针this创建出来的。
只要是从裸指针创建出来的,无论是来自this,还是来自某个智能指针的get()操作,都会让新建的智能指针,以为自己是这个裸指针的第一个保护者……
真相大白,罪魁祸首正是this这个裸指针。每次我们用this创建一个新的shared_ptr,对裸指针的计数管理都会从新从1开始。
this在关键时刻掉了链子,造成一堆管理同一对象却互不引用的shared_ptr。
手工解决的话,直觉上可能会为该类添加一个“shared_ptr”版本的this成员数据:
class Coo
{
...
private:
shared_ptr <Coo> _shared_this;
};
但应该立刻清醒过来,这是在“自己引用自己”!所以应该换成weak_ptr:
class Coo
{
...
private:
weak_ptr <Coo> _weak_this;
};
怎么初始化这个成员呢?干脆提供一个接口:
class Coo
{
...
public:
void SetWeakThis(shared_ptr <Coo> shared_this)
{
_weak_this = shared_this;
}
private:
weak_ptr <Coo> _weak_this;
};
可想而知,每次创建C的对象(智能指针)时,都得马上调用SetWeakThis():
...
shared_ptr <Coo> sp = make_shared <Coo> ();
sp->SetWeakThis(sp);
这样的代码又麻烦又丑陋。还好,标准库为我们提供了一个工具类,它有一个很长但直观的名字,叫“enable_shared_from this(允许从自身共享)”,严格上讲是一个类模板。使用方法是将它作为基类,并将当前类作为它的模板入参。
很明显,派生它是希望继承它的名字所表达的能力,所以很适合采用私用派生:
class Coo : std::enabled_shared_from_this <Coo>
{
public:
void Test()
{
foo(123, shared_from_this());
...
}
};
注意尖括号中的Coo。再注意加粗的“shared_from_this()”,这就是我们继承所得的能力,它保障传递给foo一个正确的shared_ptr<Coo>。测试一下:
//正确:
int main()
{
Coo::Ptr coo_3 = make_shared <Coo> ();
coo_3->Test();
}
一切都很正确,不存在重复释放。
让Coo认enable_shared_from_this<Coo>为父,更是注定Coo必须以智能指针面试,否则程序在运行时将因调用“shared_from_this()”而出错,原因可参考之前的分析,现在先牢记结论:
//运行:程序崩溃 int main() | //运行:程序崩溃 int main() //普通堆对象 | //运行正常 int main() |
【课堂作业】:指定要求的二叉树定义
请使用shared_ptr结合enable_shared_from_this等技术,实现一颗“二叉树”结构的定义。二叉树由节点组成。要求每个节点可以拥有一左一右的两个子节点,或者不拥有子节点(称为叶子节点),节点还需要拥有一个指针,指向父节点,最顶层的节点称为“跟节点”,它的父节点为空。要求提供从根节点开始,逐步添加左右节点以生成一棵树的功能。