C++ 11 智能指针


前言

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保在离开指针所在作用域时,自动正确的销毁动态分配的对象,防止内存泄漏。它的一种通用实现技术是使用引用计数。每使用它一次,内部的引用计数加1,每析构一次,内部引用计数减一,减为0时,删除所指向的堆内存。

一、shared_ptr共享的智能指针

std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

1、shared_ptr的基本用法

1)初始化

可以通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr,代码如下:

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> p(new int(1));
    std::shared_ptr<int> p1 = p;
    std::shared_ptr<int> p2 = std::make_shared<int>(1);
    std::shared_ptr<int> p3;
    p3.reset(new int{ 1 });
    if (p2) {
        std::cout << "p3 is not null" << std::endl;
    }
    return 0;
}

我们应该优先使用make_shared来构造智能指针,因为它更高效。

不能将一个原始指针直接赋值给一个智能指针。可以看到智能指针的用法和普通的指针的用法类似,只不过不需要自己管理分配的内存。shared_ptr不能通过直接将原始指针赋值来初始化,需要通过构造函数和辅助方法来初始化。对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针中有值得时候,调用reset会使引用计数减1。另外,智能指针可以通过重载的bool类型操作符来判断智能指针是否为空(未初始化)。

#include <iostream>
#include <memory>

struct Foo {
    Foo(int n = 0) noexcept : bar(n)
    {
        std::cout << "Foo: constructor, bar = " << bar << std::endl;
    }
    ~Foo()
    {
        std::cout << "Foo: destructor, bar = " << bar << std::endl;
    }

    int getBar() const noexcept
    {
        return bar;
    }
private:
    int bar;
};

int main()
{
    std::shared_ptr<Foo> ptr = std::make_shared<Foo>(1);
    std::cout << ptr.use_count() << std::endl;
    ptr.reset();    //调用reset引用计数减1,
    if (!ptr) {     //重载的bool类型操作符来判断智能指针是否为空
        std::cout << "ptr is nullptr" << std::endl;
    }
    return 0;
}

2)获取原始指针 

当需要获取原始指针是,可以通过get方法来返回原始指针: 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr = std::make_shared<int>(1);
    int* p = ptr.get();
    std::cout << *p << std::endl;
    return 0;
}

3)指定删除器 

智能指针初始化可以指定删除器,代码如下:

#include <iostream>
#include <memory>

void DeleteIntPtr(int* p)
{
    std::cout << "DeleteIntPtr" << std::endl;
    delete p;
}


int main()
{
    std::shared_ptr<int> ptr(new int(1), DeleteIntPtr);
    ptr.reset();
    std::cout << "main" << std::endl;
    return 0;
}

当ptr调用reset,引用计数减1,变为0,将会调用删除器 DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达式,因此还可以写成这样:

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr(new int(1), [](int* p) {
        std::cout << "lambda" << std::endl;  delete p;
        });
    ptr.reset();
    std::cout << "main" << std::endl;
    return 0;
}

当我们使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象: 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr(new int[10]{1}, [](int* p) {
            std::cout << "delete[] p" << std::endl;
            delete[] p;
        });
    ptr.reset();
    std::cout << "main" << std::endl;
    return 0;
}

也可以将std::default_delete作为删除器。default_delete的内部是通过调用delete来实现功能的: 

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr(new int[10]{ 1 }, std::default_delete<int []>());
    ptr.reset();
    std::cout << "main" << std::endl;
    return 0;
}

另外,还可以通过封装一个make_shared_array方法来让shared_ptr支持数组: 

#include <iostream>
#include <memory>

template<typename T>
std::shared_ptr<T> make_shared_array(size_t size)
{
    return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}

struct Foo
{
    ~Foo()
    {
        std::cout << "Foo destructor" << std::endl;
    }
};


int main()
{
    std::shared_ptr<int> p = make_shared_array<int>(10);
    std::shared_ptr<char> p1 = make_shared_array<char>(10);
    std::shared_ptr<Foo> p2 = make_shared_array<Foo>(10);
    std::shared_ptr<Foo> p3(new Foo[10], std::default_delete<Foo []>());
    return 0;
}

2、使用shared_ptr需要注意的问题 

1)不要用一个原始指针初始化多个shared_ptr

例如:

#include <iostream>
#include <memory>

int main()
{
    int* ptr = new int(1);
    std::shared_ptr<int> p(ptr);
    std::shared_ptr<int> p1(ptr);
    return 0;
}

 这样代码没有语法错误,但是存在逻辑错误。如果p调用了reset,那么p1将会指向已经释放的内存,若使用p1,程序的行为将不可预知。

2)不要在函数实参中创建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());

3)通过shared_from_this()返回this指针

不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构:

#include <iostream>
#include <memory>

struct A {
    std::shared_ptr<A> GetSelf()
    {
        return std::shared_ptr<A>(this);
    }
};

int main()
{
    std::shared_ptr<A> sp1(new A);
    std::shared_ptr<A> sp2(sp1->GetSelf());
    return 0;
}

在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而它们之间是没有任何关系的,(https://zh.cppreference.com/w/cpp/memory/shared_ptr 说明到,只能通过复制构造或复制赋值给另一shared_ptr,才可以将所有权与另一shared_ptr共享)在这个例子中,sp1和sp2是两个分别用 this 指针构造的shared_ptr,在离开作用域之后 this 将会被 构造的两个智能指针各自析构,导致重复析构的错误。

正确返回this的shared_ptr的做法是:让目标类通过派生std::enable_shared_from_this类,然后使用基类的成员函数 shared_from_this 来返回 this 的 shared_ptr :

#include <iostream>
#include <memory>

struct A : public std::enable_shared_from_this<A> {
    std::shared_ptr<A> GetSelf()
    {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<A> sp1(new A);
    std::shared_ptr<A> sp2(sp1->GetSelf());
    return 0;
}

至于用shared_from_this()的原因,主要是因为weak_ptr。在这个例子中,在new A时,其基类部分 enable_shared_from_this 中有一个weak_ptr _Wptr,用来监测 this 指针,在调用shared_from_this方法时,会获取所监测的shared_ptr。

在vs2019中 类enable_shared_from_this 的 shared_from_this 方法是通过使用 weak_ptr 构造了一个 shared_ptr,在类shared_ptr中的构造函数中,其实现的方法是

这个实现方法和 weak_ptr的lock方法的实现 如出一辙

两者都是通过 _Construct_from_weak()方法,做到 weak_ptr 到 shared_ptr的转换,从而获得资源的临时所有权,如果这个时候原先拥有资源的shared_ptr销毁了,资源的生命周期将会被延长至这个转化得到的shared_ptr析构之前。

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sp(new int(1));
    std::weak_ptr<int> wp(sp);
    if (wp.expired()) {
        std::cout << "weak_ptr所监视的智能指针已被释放" << std::endl;
    } else {
        std::cout << "weak_ptr所监视的智能指针有效" << std::endl;
    }

    std::shared_ptr<int> sp1 = wp.lock();
    sp.reset();
    if (wp.expired()) {
        std::cout << "weak_ptr所监视的智能指针已被释放" << std::endl;
    } else {
        std::cout << "weak_ptr所监视的智能指针有效" << std::endl;
    }
    std::cout << sp1.use_count() << std::endl;
    return 0;
}

在这个例子中,在sp.reset()之前,sp1获得了资源的临时所有权,资源引用计数变为2, sp.reset()时资源引用计数减1,且清空了sp指针自身。此时只有sp1拥有资源的所有权。sp1离开作用域后,引用计数减1变为0,资源释放。

4)要避免循环引用 

智能指针最大的一个陷阱就是循环引用,循环引用会导致内存泄漏。

#include <iostream>
#include <memory>

struct B;

struct A {
    std::shared_ptr<B> bptr;
    ~A()
    {
        std::cout << "A delete" << std::endl;
    }
};

struct B {
    std::shared_ptr<A> aptr;
    ~B()
    {
        std::cout << "B delete" << std::endl;
    }
};

int main()
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    return 0;
}

测试结果是两个指针A和B都不会被删除,存在内存泄漏。循环引用导致ap和bp的引用计数为2,在离开作用域之后ap和bp的引用计数减为1,并不会减为0,导致两个指针都不会被析构,产生了内存泄漏。解决办法是把A和B任何一个成员变量改为weak_ptr。 

#include <iostream>
#include <memory>

struct B;

struct A {
    std::shared_ptr<B> bptr;
    ~A()
    {
        std::cout << "A delete" << std::endl;
    }
};

struct B {
    std::weak_ptr<A> aptr;
    ~B()
    {
        std::cout << "B delete" << std::endl;
    }
};

int main()
{
    {
        std::shared_ptr<A> ap(new A);
        std::shared_ptr<B> bp(new B);
        ap->bptr = bp;
        bp->aptr = ap;
    }
    return 0;
}

这样在对B的成员赋值时,即执行bp->aptr = ap;时,由于aptr时weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数会减为0,A指针会被析构,析构后其内部的bptr的引用计数会减为1,然后在离开作用域bp引用计数减为0,B对象也将被析构,不会发生内存泄漏。 

二、weak_ptr弱引用的智能指针

弱引用指针weak_ptr是用来监视shared_ptr的,不会使引用计数加1,它不管理shared_ptr的内部指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。weak_ptr没有重载操作符 * 和 ->, 因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。

1)weak_ptr基本用法

1、通过use_count()方法来获得当前观测资源的引用计数:

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sp(new int(1));
    std::weak_ptr<int> wp(sp);
    std::cout << wp.use_count() << std::endl;
    return 0;
}

这里输出的结果为1,weak_ptr不会使对于 new int(1) 的引用计数增加。 

2、通过expired()方法来判断所观测的资源是否已经被释放:

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sp(new int(1));
    std::weak_ptr<int> wp(sp);
    if (wp.expired()) {
        std::cout << "weak_ptr所监视的智能指针已被释放" << std::endl;
    } else {
        std::cout << "weak_ptr所监视的智能指针有效" << std::endl;
    }

    sp.reset();
    if (wp.expired()) {
        std::cout << "weak_ptr所监视的智能指针已被释放" << std::endl;
    } else {
        std::cout << "weak_ptr所监视的智能指针有效" << std::endl;
    }
    std::cout << wp.use_count() << std::endl;
    return 0;
}

3、通过lock()方法来获取所监视的shared_ptr: 

#include <iostream>
#include <memory>

std::weak_ptr<int> gwp;
void func()
{
    if (gwp.expired()) {
        std::cout << "gwp is expired" << std::endl;
    } else {
        std::shared_ptr<int> spt = gwp.lock();
        std::cout << *spt << std::endl;
    }
}
int main()
{
    {
        std::shared_ptr<int> sp = std::make_shared<int>(100);
        gwp = sp;
        func();         //输出100
        std::cout << sp.use_count() << std::endl;   //输出1
    }
    func(); //sp被释放,输出 gwp is expired
    return 0;
}

参考:以上内容均来自《深入应用C++11代码优化与工程级应用》

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值