《白话C++》第10章 STL和boost,Page98 10.4.6 std::weak_ptr

本文详细解释了C++中shared_ptr如何导致循环引用,通过例子展示了结构体中的引用计数机制,以及如何在C1和C2类间创建死结以避免内存泄漏。最后,给出了双向链表和树结构的示例,引导读者理解和处理循环引用问题。
摘要由CSDN通过智能技术生成

1.问题分析

打开Excel软件,随便找三个格子(cell),在A格输入公式让其内容等于B格,让B格等于C格,让C格等于A格……,就会看到一个“循环引用”警告框。

shared_ptr的设计“命中注定”有一个重大的“缺陷”,那就是它也会产生“循环引用”问题。

shared_ptr之间会发生循环引用,问题就在于“引用计数”:

struct C2; //前置声明C2类,因为C1中药用到

struct C1
{
    ~C1() {cout << "~C1" << endl;}

    shared_ptr <C2> _c2;
};

注意,C1结构中,包含一个对C2管理的shared_ptr指针。类或结构在析构时,必须先析构它的所有成员数据,也就是说当析构C1时,需要先析构_c2;而_c2是一个shared_ptr,所以它会先检查当前_c2引用计数的值

在看C2的设计:

struct C2
{
    ~C2(){cout << "~C2" << endl;}
    shared_ptr <C1> _c1;
};

注意,C2类中含有一个C1的智能指针。也就是说当析构C2的对象时,需要先检查_c1的引用计数

是不是感觉有什么不对头,其实现在一切还合情合法,主要看如何在C1和C2之间打个死节:

void test_circular_reference()
{
    C1* c1 = new C1;
    C2* c2 = new C2;

    shared_ptr <C1> pc1(c1);
    shared_ptr <C2> pc2(c2);

    pc1->_c2 = pc2;
    pc2->_c1 = pc1;

    cout << "c1's ref count = " << pc1.use_count() << endl; //2
    cout << "c2's ref count = " << pc2.use_count() << endl; //2
}

强调一下:当我们说“智能指针”对象时,我们说的是智能指针本身,比如上例代码中的pc1和pc2,这两个智能指针对象,它们本身是栈对象(不靠new产生),它们会自动析构。

而当我们说智能指针所管理的指针(或对象)时,说的是它所拥有的裸指针或裸指针所指向的内存中存放的对象,比如例中的c1和c2。正是为了方便说明这一点,本例刻意将后者定义出来。

运行以上程序,输出两个2,没有看到C1或C2析构过程的任何输出。没错,因为某种死结,c1和c2最终都没有被删除,这个死结在9,10两行代码执行时打上。

再强调另外一件事:在智能指针对象(本身)析构时,会检查引用激素,即检查有几个智能指针正在同时管理着同一个裸指针。

如果引用计数是1,说明就只剩下当前智能指针在管理当前裸指,于是真正释放该裸指。

否则(引用计数大于1),则只是将引用计数减1,然后智能指针自个儿挥手告别世界,留下裸指所指向的堆对象继续活着。

test_circular_reference()函数结束时,依据规定将先释放pc2。pc2是一个shared_ptr,指向堆对象c2,所以需要检查现在有几个智能指针指向c2?除了pc2自身在管理c2以外,还有c1的成员数据_c2(另一个智能指针)也在c2,因此计数为2。于是pc2深情地对c2说:“我先走了,你要继续好好活着,c1家里的_c2是爱你的。”

接着要释放pc1。pc1也是一个shared_ptr,指向堆对象c1。类似的故事发生在它身上,最后它深情地对c1说:“我先走了,你要继续好好活着,c2家里的_c1是爱你的。”

事情就这样:两个智能指针pc1和pc2都被释放了,但却留下了彼此直接管理的c1和c2还活着。

如果还不清楚,看一眼这是的最初情况,如图10-8所示:

图中有两个箭头指向c1,同样有两个箭头指向c2,所以各自的引用计数都是2。后来,pc1和pc2都走了,留下了c1和c2,以及二者各自的成员数据(shared_ptr类型)_c2和_c1,如图10-9所示:

由于c1和c2一开始就托管给智能指针(pc1和pc2),可是那两个家伙已经走了,现在没有代码会去主动delete c1或delete c2,他们就这样被遗忘在程序的世界里

如果一开始就不让pc1和pc2在内部互相指向,这样的事情就不会发生,不过互相指向的事情本来就很多,比如一个双向链表中的前后节点互相指向,如图10-10所示:

用代码表达:

shared_ptr <Node> first_node = make_shared <Node> ();
shared_ptr <Node> second_node = make_shared <Node> ();

first_node->next = second_node; //next也是shared_ptr <Node>
second_node->prev = first_node; //prev也是shared_ptr <Node>

或者一颗“树”中的父子节点,如图10-11所示:

用代码表达:

shared_ptr <Node> parent_node = make_shared <Node> ();
shared_ptr <Node> child_node_1 = make_shared <Node> ();
shared_ptr <Node> child_node_2 = make_shared <Node> ();

parent_node->children.push_back(child_node_1);
parent_node->children.push_back(child_node_2);

child_node_1->parent = parent_node;
child_node_2->parent = parent_node;

以上种种都可能造成循环引用。

【课堂作业】:感受shared_ptr对象间的“循环引用”

请根据以上示意代码,完成双向链表和树各自的“Node(节点)”定义,并完整第写出测试程序。编译运行,查看是否造成循环引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值