amazzzzzing

导航

智能指针

智能指针

std::auto_ptr

#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr<int> ptr(new int);
    std::auto_ptr<int> ptr1 = ptr;

    std::cout << ptr.get() << std::endl;
    std::cout << ptr1.get() << std::endl;

    return 0;
}

std::auto_ptr的主要功能是实现自动的资源管理。

在上述操作后,ptr的值为空,也即指针的拷贝将指针的值改变了,这和普通指针的拷贝语义是不一致的。C++11标准中已将std::auto_ptr标记为弃用。

std::shared_ptr

共享指针,其支持自动资源管理并实现了拷贝的语义。

使用共享指针管理对象时,需要注意同一个对象只能由一个共享指针及其拷贝来管理,否则就会出现多次释放

共享指针的注意事项

保存智能指针时,要注意语义

考虑一个用法:

class A
{
public:
    void set(std::shared_ptr<Cfg> cfg)
    {
        cfg_ = cfg;
    }
    std::shared_ptr<Cfg> get()
    {
        return cfg_;
    }
private:
	std::shared_ptr<Cfg> cfg_;
};

由于共享指针在拷贝时,其持有的对象并没有复制,因此如果需要持久的保存一个共享指针,需要注意其持有对象共享性。

在上述用法中,由于内部保存的是一个外部传入的共享指针,因此内部并不真正持有一个独立的Cfg对象。如果外部对这个Cfg对象进行了修改,则相当于直接对内部保存的Cfg对象进行了修改,这个行为违反了set和get的语义(应该只能通过set进行修改)。同时,如果通过get获取了这个指针,则也可以直接修改内部的配置(同样违反了get语义)。

要解决这个问题,可以把传入的指针类型更改为弱指针(弱指针的语义为不持有对象)。此时,内部需要保存该配置时,必须创建一个弱指针对象的深拷贝。

在get语义中,在传出指针时,同样将指针类型更改为弱指针,与此同时,还需要加上const限定,避免外部对内部信息进行修改。

class A
{
public:
    void set(std::weak_ptr<Cfg> cfg)
    {
        cfg_ = std::make_shared<Cfg>(*cfg);
    }
    const std::weak_ptr<Cfg> get()
    {
        return cfg_;
    }
private:
	std::shared_ptr<Cfg> cfg_;
};

共享指针引起泄漏的情况

例子

使用共享指针管理对象时,需要注意避免循环引用。若两个对象均通过共享指针持有另一个对象,则由于循环引用在离开作用域时无法自动释放造成资源泄漏

循环引用的一种特殊形式是自引用,也即一个对象通过共享指针持有自己的指针。

循环引用的例子:

#include <iostream>
#include <memory>

class BB;

class AA
{
public:
    std::shared_ptr<BB> ptr_bb;
    AA() {std::cout << "AA +" << std::endl;}
    ~AA() {std::cout << "AA -" << std::endl;}
};

class BB
{
public:
    std::shared_ptr<AA> ptr_aa;
    BB() {std::cout << "BB +" << std::endl;}
    ~BB() {std::cout << "BB -" << std::endl;}
};

int main()
{
    std::shared_ptr<AA> ptr_aa(new AA);
    std::shared_ptr<BB> ptr_bb(new BB);
    ptr_aa->ptr_bb = ptr_bb;
    ptr_bb->ptr_aa = ptr_aa;

    return 0;
}

执行结果如下:

./a.out
AA +
BB +

自引用的例子如下:

#include <iostream>
#include <memory>

class AA
{
public:
    std::shared_ptr<AA> ptr_bb;
    AA() {std::cout << "AA +" << std::endl;}
    ~AA() {std::cout << "AA -" << std::endl;}
};

int main()
{
    std::shared_ptr<AA> ptr_aa(new AA);
    ptr_aa->ptr_bb = ptr_aa;

    return 0;
}

执行结果如下:

./a.out
AA +

循环引用引起泄漏的原因(技术角度)

首先从技术角度来分析引起泄漏的原因。

共享指针内部是通过引用计数来实现对资源的管理的。每当共享指针被复制一次,计数加1,每当共享指针被析构,计数减1,当计数被减至0时,才真正执行被管理对象的销毁。值得注意的是,共享指针的计数对于拷贝得到的共享指针是相同的(内部实现上,通过拷贝得到的共享指针内部均有一个指向同一个计数值的指针)。

先看一个正常的示例:

#include <memory>
#include <iostream>

class AA
{
public:
    AA() {std::cout << "AA +" << std::endl;}
    ~AA() {std::cout << "AA -" << std::endl;}
};

int main()
{
    std::shared_ptr<AA> ptr_aa(new AA());
    std::shared_ptr<AA> ptr_aa_copy;

    ptr_aa_copy = ptr_aa;

    return 0;
}

主函数中先创建了一个共享指针ptr_aa,管理一个通过new创建的对象AA。然后创建了一个共享指针ptr_aa_copy,并通过拷贝赋值的方式成为了ptr_aa的拷贝,因此这两个共享指针的引用计数均为2。在离开main函数时,一共有两个对象需要被销毁,因此引用计数最终变为0,即最终会完成资源的销毁。

以下为出现泄漏的循环引用的main函数部分:

int main()
{
    std::shared_ptr<AA> ptr_aa(new AA);
    std::shared_ptr<BB> ptr_bb(new BB);

    ptr_aa->ptr_bb = ptr_bb;
    ptr_bb->ptr_aa = ptr_aa;

    return 0;
}

在这段代码中,首先创建了两个共享指针,然后对这两个共享指针分别执行了一次拷贝,即这两个共享指针的引用计数均为2。在主函数结束时,这两个共享指针需要销毁,因此这两个共享指针对象的析构函数将被执行(注意,不是AA对象和BB对象的析构函数)。在执行ptr_aa共享指针对象的析构函数时,引用计数将由2减为1,由于引用计数不为0,其持有的AA对象不会被销毁,同样在ptr_bb共享指针对象销毁的时候,其持有的BB对象也不会被销毁。因此造成了内存的泄漏。

循环引用使得在离开作用域时,所有的共享指针的引用计数均没有降到0,从而所有被持有的对象均没有被析构。

循环引用引起泄漏的原因(语义角度)

共享指针std::shared_ptr会持有一个对象,这意味着多个共享指针持有同一个对象时,只要有一个共享指针没有被销毁时,这个被持有的对象就不会被销毁。这种持有的方式称为强引用,与其对应的是弱引用,普通的指针是一种弱引用,C++也提供了std::weak_ptr用于弱引用。

如果AA对象持有一个BB对象的共享指针,BB对象持有一个CC对象的共享指针,依次类推,则产生了CC对象的释放依赖BB对象的释放,BB对象的释放依赖AA对象的释放的这种依赖链。很容易想到,如果在程序中产生了这种依赖链,则必须将整个依赖关系设计为一个有向无环图(DAG),这样才能确保正确的释放。

循环引用的破解之法

厘清持有关系和引用关系

持有关系使用强引用,引用关系使用弱引用。

一个例子是二叉树的节点,其中三个指针leftrightparent。由于我们需要通过上层节点去管理下层节点,因此leftright引用为强引用,parent引用为弱引用。

避免依赖成环

如果一个对象持有另一个对象的共享指针,就需要注意形成的依赖关系是否成环。

下面分析一个正常的依赖的释放流程:

#include <memory>
#include <iostream>

class AA;
class BB;
class CC;

class AA
{
public:
    AA() {std::cout << "AA +" << std::endl;}
    ~AA() {std::cout << "AA -" << std::endl;}
    std::shared_ptr<BB> bb;
};

class BB
{
public:
    BB() {std::cout << "BB +" << std::endl;}
    ~BB() {std::cout << "BB -" << std::endl;}
    std::shared_ptr<CC> cc;
};

class CC
{
public:
    CC() {std::cout << "CC +" << std::endl;}
    ~CC() {std::cout << "CC -" << std::endl;}
};

int main()
{
    std::shared_ptr<AA> a(new AA());
    std::shared_ptr<BB> b(new BB());
    std::shared_ptr<CC> c(new CC());

    a->bb = b;
    b->cc = c;

    return 0;
}

这里的依赖关系为AA->BB->CC,没有形成环状依赖。main函数中一共有三个共享指针,其中aa的引用计数为1,bbcc的引用计数为2。当离开main函数时,aabbcc的析构函数均被执行。当aa的析构函数执行时,引用计数减为0,其持有的AA对象被析构,从而引起其成员bb被析构,从而b的引用计数降为1(注意,持有同一个对象的共享指针共享同一个引用计数)。当bb的析构函数执行时,引用计数减为0,其持有的BB对象被析构,从而引起其成员cc被析构,c的引用计数降为1。最终cc的析构函数执行时,引用计数减为0,其持有的CC对象被析构。全部对象被析构完毕。

因此最终执行结果为:

./a.out
AA +
BB +
CC +
AA -
BB -
CC -

posted on 2023-06-02 01:34  amazzzzzing  阅读(39)  评论(0)    收藏  举报