c++ 智能指针

智能指针解决的问题

1.忘记 delete 内存:会导致内存泄漏问题,且除非是内存耗尽否则很难检测到这种错误。
2.使用已经释放掉的对象:如果能够记得在释放掉内存后将指针置空并在下次使用前判空,尚可避免这种错误。
3.同一块内存释放两次:如果有两个指针指向相同的动态分配对象,则很容易发生这种错误。
4.发生异常时的内存泄漏:若在 new 和 delete 之间发生异常,则会导致内存泄漏。

适用范围

C++11 中提供了三种智能指针,分别是 shared_ptr , unique_ptr 和 weak_ptr 。shared_ptr 允许多个指针指向同一个对象,unique_ptr 则“独占”所指向的对象,weak_ptr 则是和share_ptr 相辅相成的伴随类。

这三种类型都定义在头文件memory中。类似vector,智能指针也是模板,需要在尖括号内给出类型信息。shared_ptr 和 unique_ptr 的使用方式和普通指针类似,都可以使用*和->等运算符。

shared_ptr的用法。智能指针shared_ptr的用法
weak_ptr可以解决shared_ptr的循环引用问题。
unique_ptr最像裸指针,但更为安全,保证资源的释放,不能复制只能移动。

基本语法

#include <iostream>
#include <memory>

class Person
{
public:
    Person(int v) 
    {
        value = v;
        std::cout << "Person " << value << " is constructed."<< std::endl;
    }
    ~Person() 
    {
        std::cout << "Person " << value << " is destructed." << std::endl;
    }

    int value;
};

int main()
{
	

## //shared_ptr的使用

    // 初始化方式 1,默认初始化的智能指针保存着一个空指针
    std::shared_ptr<Person> p1;

    // 初始化方式 2,使用标准库函数 make_shared,make_shared 传递的构造参数必须与 Person 的某个构造函数相匹配
    // Person 2 的引用计数为 1,p2 指向它
    auto p2 = std::make_shared<Person>(2);

    // 初始化方式 3,使用 new 返回的指针来初始化智能指针,此种方式必须使用直接初始化
    // Person 3 的引用计数为 1,p3 指向它
    std::shared_ptr<Person> p3(new Person(3));

    // 将 shared_ptr 作为一个条件判断,若指向一个对象,则为 true。 p1 没有指向一个对象,故 !p1 为 true
    if (!p1)
    {
        // 赋值操作会递增右侧操作数的引用计数,递减左侧操作数的引用计数
        // Person 2的引用计数为 2,p1、p2 指向它
        p1 = p2;
        
        // Person 2的引用计数为 1,p1 指向它
        // Person 3的引用计数为 2,p2、p3 指向它
        p2 = p3;

        // 解引用智能指针,获得它指向的对象
        // 输出结果为 Person 2
        std::cout << "This is Person " << (*p1).value << std::endl;
        // 输出结果为 Person 3
        std::cout << "This is Person " << p2->value << std::endl;

        // 交换 p1 和 p2 的指针
        // Person 2的引用计数为 1,p2 指向它
        // Person 3的引用计数为 2,p1、p3 指向它
        p1.swap(p2);

        // 当智能指针中有值的时候,调用 reset() 会使引用计数减 1
        // Person 3的引用计数为 1,p3 指向它
        p1.reset();
        // Person 3的引用计数为 0,被销毁
        p3.reset();
    }


## //unique_ptr的使用

    // unique_ptr 的初始化方式同 shared_ptr,不一一列出
    // make_unique 函数是 C++14 中才加入的
    // Person 4的引用计数为 1,p4 指向它
    auto p4 = std::make_unique<Person>(4);
    // Person 5的引用计数为 1,p5 指向它
    std::unique_ptr<Person> p5(new Person(5));

    // 某个时刻只能有一个 unique_ptr 指向一个对象
    // 所以,unique_ptr 不支持拷贝,也不支持赋值
    // std::unique_ptr<Person> p5(p4); // 错误,不支持拷贝
    // p5 = p4; // 错误,不支持赋值

    // release函数使得 p4 放弃对指针的控制权,返回指针并置 p4 为空
    // Person 4 的引用计数为 1,p6 指向它
    std::unique_ptr<Person> p6(p4.release()); 


	

## //weak_ptr的使用

    // weak_ptr 指向一个由 shared_ptr 管理的对象,但不会改变 shared_ptr 的引用计数。 weak_ptr 不控制所指对象的生存期,所以,即使有weak_ptr指向对象,对象也还是会被释放
    std::weak_ptr<Person> p7(p2);

    // 由于 weak_ptr 所指对象可能不存在,所以我们不能用 weak_ptr,直接访问对象,而必须调用 lock(),若不存在,则返回一个空 shared_ptr; 若存在,则返回weak_ptr所指对象的 shared_ptr
    
    // Person 2的引用计数为 2,p2、p8 指向它
    if (auto p8 = p7.lock())
    {
        // use_count() 函数返回共享 weak_ptr 所指对象的 shared_ptr 数量
        // 这里的输出结果为 2
        std::cout << p7.use_count() << std::endl;
    }

}



以上代码的输出结果为:

Person 2 is constructed.
Person 3 is constructed.
This is Person 2
This is Person 3
Person 3 is destructed.
Person 4 is constructed.
Person 5 is constructed.
2
Person 4 is destructed.
Person 5 is destructed.
Person 2 is destructed.

shared_ptr原理

当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete。所有管理同一个裸指针的shared_ptr,都共享一个引用计数器,每当一个shared_ptr被赋值(或拷贝构造)给其它shared_ptr时,这个共享的引用计数器就加1,当一个shared_ptr析构或者被用于管理其它裸指针时,这个引用计数器就减1,如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源。

下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*和->操作符。

#include <iostream>
#include <memory>

template<typename T>
class SmartPointer
{
private:
    T * _ptr;
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) :
        _ptr(ptr) 
    {
        if (_ptr) 
        {
            _count = new size_t(1);
        }
        else 
        {
            _count = new size_t(0);
        }
    }

    SmartPointer(const SmartPointer &ptr)
    {
        if (this != &ptr)
        {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            ++(*this->_count);
        }
    }

    SmartPointer& operator=(const SmartPointer &ptr) 
    {
        if (this->_ptr == ptr._ptr)
        {
            return *this;
        }

        if (this->_ptr) 
        {
            --(*this->_count);
            if (this->_count == 0) 
            {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        ++(ptr._count);
        this->_count = ptr._count;
        return *this;
    }

    T& operator*() 
    {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);

    }

    T* operator->() 
    {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer() 
    {
        --(*this->_count);
        if (0 == *this->_count )
        {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count() 
    {
        return *this->_count;
    }
};

int main() 
{
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        std::cout << sp.use_count() << std::endl;
        std::cout << sp3.use_count() << std::endl;
}

这个智能指针的简单实现模仿的是 share_ptr 的行为,不难发现,引用计数的存在会带来一些性能影响:

  • shared_ptr 的尺寸是裸指针的两倍:因为内部既包含一个指向该资源的裸指针,也包含一个指向该资源的引用计数的裸指针。
  • 引用计数的内存必须动态分配
  • 引用计数的递增和递减必须是原子操作:原子操作一般比非原子操作慢。我们的实现版本里为了简单起见没有实现原子操作。

shared_ptr 的循环引用问题

B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个 shared_ptr,此时A和B的生命周期互相由对方决定,都无法从内存中销毁。

举个循环引用的简单例子。

#include <iostream>
#include <memory>
using namespace std;


class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
shared_ptr<BB> m_bb_ptr; //!
};

class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};

int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
    // BB先出作用域(后进先出),B的引用计数减少为1,不为0,所以堆上的BB空间没有被释放,且BB持有的AA也没有机会被析构,AA的引用计数也完全没减少
    // AA后出作用域,同理AA的引用计数减少为1,不为0,所以堆上AA的空间也没有被释放
AA::AA() called
BB::BB() called
ptr_a use_count: 1
ptr_b use_count: 1
ptr_a use_count: 2
ptr_b use_count: 2

如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用弱智能指针weak_ptr即可。

#include <iostream>
#include <memory>
using namespace std;


class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
weak_ptr<BB> m_bb_ptr; //!
};

class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};

int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
    // BB先出作用域(后进先出),B的引用计数减少为1,为0,所以堆上的BB空间被释放,且BB持有的AA被析构,AA的引用计数也减1
    // AA后出作用域,同理AA的引用计数减1,为0,所以堆上AA的空间也被释放

AA::AA() called
BB::BB() called
ptr_a use_count: 1
ptr_b use_count: 1
ptr_a use_count: 2
ptr_b use_count: 1
BB::~BB() called
AA::~AA() called

weak_ptr的作用就是:在需要时变出一个shared_ptr,在其它时候不干扰shared_ptr的引用计数。

1.weak_ptr用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。
2.weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。
3.weak_ptr提供了expired()与lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空“shared_ptr)。

std::shared_ptr<int> sh = std::make_shared<int>();
// 用一个shared_ptr初始化
std::weak_ptr<int> w(sh);
// 变出shared_ptr
std::shared_ptr<int> another = w.lock();
// 判断weak_ptr所观察的shared_ptr的资源是否已经释放
bool isDeleted = w.expired();

shared_ptr 关联与独立

多个共享指针指向同一个空间,它们的关系可能是关联(我们所期望的正常关系)或是独立的(一种错误状态)。
只有用一个shared_ptr为另一个shared_ptr赋值或拷贝时,才将这连个共享指针关联起来,直接使用地址值会导致各个shared_ptr独立。

    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2(sp1), sp3;
    sp3 = sp1;
    //一个典型的错误用法
    shared_ptr<int> sp4(sp1.get()); 
    cout << sp1.use_count() << " " << sp2.use_count() << " " 
    << sp3.use_count() << " " << sp4.use_count() << endl;
    //输出 3 3 3 1

sp1,sp2,sp3是相互关联的共享指针,共同控制所指内存的生存期,sp4虽然指向同样的内存,却是与sp1,sp2,sp3独立的,sp4按自己的引用计数来关联内存的释放。

谨慎使用p.get()的返回值

p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:

  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的 。保存为裸指针不知什么时候就会变成空悬指针 ,保存为shared_ptr则产生了独立指针。
  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

shared_ptr注意事项

指定删除器

std::shared_ptr可以指定删除器的一个原因是其默认删除器不支持数组对象。

不要用一个原始指针初始化多个shared_ptr,原因在于会造成二次销毁

  int *p5 = new int;
    std::shared_ptr<int> p6(p5);
    std::shared_ptr<int> p7(p5);// logic error

直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏

// 声明
void f(A *p1, B *p2);

// 使用
f(new A, new B);

上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏

如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏

//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(shared_ptr<A> (new A),shared_ptr<B>(new B));

因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:

1)new A发生在shared_ptr< A >构造发生之前

2)new B发生在shared_ptr< B >构造发生之前

3)两个shared_ptr的构造发生在函数f的调用之前

在满足上面三条性质的前提下,各操作的顺序可以任意执行

若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏

//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(make_shared<A>(),make_shared<B>());

依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行。如果make_shared< A > 构造完成了,make_shared< B >中抛出异常,那么A的资源能被正确释放。与上面用new来初始化的情形对比,make_shared保证了第二new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,故在异常发生时,能正确释放资源。

总结:请总是使用make_shared来生成shared_ptr

禁止通过shared_from_this()返回this指针

这样做可能也会造成二次析构

避免循环引用

内存无法释放

new的普通指针与shared_ptr转换

如图所示,这会发生什么情况?答案是输出的会是随机数,因为经过func函数后,我们用p初始化的临时智能指针已经被析构了,引用计数先+1,后-1。所以经过func函数后,p指向的对象被释放,再解引用自然无法得到我们想要的结果。

#include<iostream>
#include <memory>
using namespace std;
void func(shared_ptr<int>)
{
    ;
}
int main()
{
    int a = 5;
    auto p = new int(5);
    func(shared_ptr<int>(p));
    cout << *p << endl;
    return 0;
}

这种情况下,正确的做法如图所示:一开始就使用智能指针。

#include<iostream>
#include <memory>
using namespace std;
void func(shared_ptr<int>)
{
    ;
}
int main()
{
    //int a = 5;
    auto p = make_shared<int>(5);
    func(shared_ptr<int>(p));
    cout << *p << endl;
    return 0;
}

make_shared用法

C++11智能指针中make_shared存在的必要性

除了以下情况尽量使用make_shared:
1、 make_shared只能针对new出来的,对于使用工厂创建出来的对象无能为力。
2、 需要定制删除器时,make_shared无能为力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值