C++ 智能指针

C++ STL 提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr。

  • 其中auto_ptr 是 C++98 提供的解决方案,C+11 已将其摒弃,并提出了 unique_ptr 作为 auto_ptr 替代方案。虽然 auto_ptr 已被摒弃,但在实际项目中仍可使用,但建议使用较新的 unique_ptr,因为 unique_ptr 比 auto_ptr 更加安全。

  • shared_ptr 和 weak_ptr 则是 C+11 从准标准库 Boost 中引入的两种智能指针。

C++11智能指针介绍

智能指针主要用于管理在上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个shared_ptr引用。该引用计数的内存在堆上分配,当新增一个时引用计数加1,当引用过期时计数减一。只有引用计数为0时,shared_ptr才会自动释放引用的内存资源。

对shared_ptr进行初始化时**不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。**可以通过make_shared函数或者通过构造函数传入普通指针,并可以通过get函数获得普通指针。

为什么要使用智能指针

智能指针的作用是管理一个指针,在使用普通指针时存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

普通指针存在的问题

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud."));  

auto_ptr<string> p2;  

p2 = p1; //auto_ptr不会报错

如果p1和p2是普通指针,那么两个指针将指向同一个string对象。那么在删除同一个对象两次的时候,会出错。要避免这种问题,方法有多种:

  • 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。

  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。

  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加 1,而指针过期时,计数将减 1,。当减为 0 时才调用 delete。这是 shared_ptr 采用的策略。

auto_ptr

(C++98的方案,C++11已经抛弃)auto_ptr定义在头文件中。采用所有权模式。

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.")); 

auto_ptr<string> p2; 

p2 = p1; //auto_ptr不会报错

此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

auto_ptr、 unique_ptr、 shared_ptr区别

再来一个例子:

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

int main()
{
    auto_ptr<string> films[5] = {
    auto_ptr<string>(new string("Fowl Balls")),
    auto_ptr<string>(new string("Duck Walks")),
    auto_ptr<string>(new string("Chicken Runs")),
    auto_ptr<string>(new string("Turkey Errors")),
    auto_ptr<string>(new string("Goose Eggs"))
    };
    auto_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针

    cout << "The nominees for best avian baseballl film are\n";
    for (int i = 0; i < 5; ++i)
    {
        cout << *films[i] << endl;
    }
    cout << "The winner is " << *pwin << endl;
    return 0;
}

编译时程序不会出错,但是运行时程序崩溃。因为films[2] 已经是空指针,*films[2]访问空指针时程序会崩溃。但这里如果把 auto_ptr 换成 shared_ptr 或 unique_ptr 后,程序就不会崩溃,原因如下:

使用 shared_ptr 时运行正常,因为 shared_ptr 采用引用计数,pwin 和films[2] 都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小,因此不会出现多次删除一个对象的错误。

使用 unique_ptr 时编译出错,与 auto_ptr 一样unique_ptr 也采用所有权模型,但在使用 unique_ptr 时,程序不会等到运行阶段崩溃,而在编译代码时报错

unique_ptr

从上面可见,unique_ptr 比 auto_ptr 更加安全,因为 auto_ptr 有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移,如下代码所示:

unique_ptr<string> upt(new string("lvlv"));
unique_ptr<string> upt1(upt);	//编译出错,已禁止拷贝
unique_ptr<string> upt1=upt;	//编译出错,已禁止拷贝
unique_ptr<string> upt1=std::move(upt);  //控制权限转移

auto_ptr<string> apt(new string("lvlv"));
auto_ptr<string> apt1(apt);	//编译通过
auto_ptr<string> apt1=apt;	//编译通过

这里要注意,在使用std::move将unique_ptr的控制权限转移后,不能够再通过unique_ptr来访问和控制资源了,否则同样会出现程序崩溃。我们可以在使用unique_ptr访问资源前,使用成员函数get()进行判空操作。

unique_ptr<string> upt1=std::move(upt); //控制权限转移
if(upt.get()!=nullptr)		           //判空操作更安全
{
	//do something
}

unique_ptr 悬挂指针问题

总体来说:允许临时悬挂指针的赋值,禁止其他情况的出现

  unique_ptr<string> demo(const char *s)
  {
      unique_ptr<string> temp (new string(s));
      return temp;
  }

  unique_ptr<string> ps;
  ps = demo("unique special");


  unique_ptr<string> pu1(new string("hello world"));
  unique_ptr<string> pu2;
  pu2 = pu1;//#1 not allowed
  unique_ptr<string> pu3;
  pu3 = unique_ptr<string>(new string("you"));//#2 allowed

编译器允许此种赋值方式。总之:当程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁止这么做。

如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。

如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。

可将unique_ptr储存到STL容器中,只要不调用将unique_ptr复制或赋值给另一个算法(如sort())。例如,可在程序中使用类似于下面的代码段:

unique_ptr<int> make_int(int n)
{
    return unique_ptr<int>(new int(n));
}

void show(unique_ptr<int> &p1)
{
    cout<<*a<<' ';
}

int main(){
    ...
    vector<unique_ptr<int>> vp(size);
    for(int i=0; i<vp.size();i++){
        vp[i] = make_int(rand() %1000);//copy temporary unique_ptr
    }
    vp.push_back(make_int(rand()%1000));// ok because arg is temporary
    for_each(vp.begin(),vp.end(),show); //use for_each();
}

其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋值给vp中的一个unique_ptr。

另外,如果按值而不是按引用给show()传递对象,编译器将报错,因为这将导致使用一个来自vp的非临时unique_ptr初始化p1,而这是不允许的。

unique_ptr<int> pup(make_int(rand() % 1000));   	// ok
shared_ptr<int> spp(pup);                       	// not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   	// ok

模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。

weak_ptr(弱引用智能指针)

先了解下空虚指针问题:

在这里插入图片描述

有两个指针p1和p2,指向堆上的同一个对象Object,p1和p2位于不同的线程中。假设线程A通过p1指针将对象销毁了(尽管把p1置为NULL),那p2就成了空悬指针。这是一种典型的C/C++内存错误。

使用weak_ptr能够帮助我们轻松解决上述的空悬指针问题(直接使用shared_ptr也是可以避免上面的问题)。

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sptr;
    sptr.reset(new int);
    *sptr = 10;

    std::weak_ptr<int> weak1 = sptr;

    sptr.reset(new int);
    *sptr = 5;
    
    std::weak_ptr<int> weak2 = sptr;
    
    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";
        
    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

weak_ptr不控制对象的生命期,但是它知道对象是否还活着,如果对象还活着,那么它可以提升为有效的shared_ptr(提升操作通过lock()函数获取所管理对象的强引用指针);如果对象已经死了,提升会失败,返回一个空的shared_ptr。

再来说说shared_ptr会导致的问题(只能通过weak_ptr来解决,或者压根就不用智能指针)

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

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;
    //下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构
    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;
}

运行结果:

在这里插入图片描述

可以看到由于AA和BB内部的shared_ptr各自保存了对方的一次引用,所以导致了ptr_a和ptr_b销毁的时候都认为内部保存的指针计数没有变成0,所以AA和BB的析构函数不会被调用。解决方法就是把一个shared_ptr替换成weak_ptr。

#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

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;
    //下面两句导致了AA与BB的循环引用,结果就是AA和BB对象都不会析构
    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;
}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQiORjLc-1626836845302)(C:\Users\Administrator.SC-202008231531\AppData\Local\YNote\data\l1062409377@163.com\e29b317f3dc64c13929b48466cfb4b61\clipboard.png)]

最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在能预见会出现循环引用的情况下才能使用,即这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。

个人理解:

综上: auto_ptr 、 unique_ptr 都不建议使用, 直接使用shared_ptr。但是如果出现如上循环引用问题导致不能释放的话,那么在考虑将一方更改为weak_ptr指针。

使用注意点:

所有的智能指针类都有一个explicit构造函数,以指针作为参数。比如auto_ptr的类模板原型为:

templet<class T>
class auto_ptr {
  explicit auto_ptr(X* p = 0) ; 
  ...
};

因此不能自动将指针转换为智能指针对象,必须显示调用:

shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg;//NOT ALLOWED(implicit conversion)
pd = shared_ptr<double>(p_reg);// ALLOWED (explicit conversion)
shared_ptr<double> pshared = p_reg;//NOT ALLOWED (implicit conversion)
shared_ptr<double> pshared(p_reg);//ALLOWED (explicit conversion)

对智能指针都应避免的一点:

string vacation("I wandered lonely as a child."); //heap param
shared_ptr<string> pvac(&vacation);//NO!!

pvac过期时,程序将把delete运算符用于栈内存,这是错误的!

如上个人理解: 智能指针得指向new的堆空间

make_share

C++11 中引入了智能指针, 同时还有一个模板函数 std::make_shared 可以返回一个指定类型的 std::shared_ptr, 那与 std::shared_ptr 的构造函数相比它能给我们带来什么好处呢 ?

shared_ptr<string> p1 = make_shared<string>(10, '9');  
shared_ptr<string> p2 = make_shared<string>("hello");  
shared_ptr<string> p3 = make_shared<string>(); 

make_shared优点

提高性能

shared_ptr 需要维护引用计数的信息:

  • 强引用, 用来记录当前有多少个存活的 shared_ptrs 正持有该对象. 共享的对象会在最后一个强引用离开的时候销毁( 也可能释放).
  • 弱引用, 用来记录当前有多少个正在观察该对象的 weak_ptrs. 当最后一个弱引用离开的时候, 共享的内部信息控制块会被销毁和释放 (共享的对象也会被释放, 如果还没有释放的话).

如果你通过使用原始的 new 表达式分配对象, 然后传递给 shared_ptr (也就是使用 shared_ptr 的构造函数) 的话, shared_ptr 的实现没有办法选择, 而只能单独的分配控制块:

在这里插入图片描述

每个std::shared_ptr都指向一个控制块,控制块包含被指向对象的引用计数以及其他东西。这个控制块的内存是在std::shared_ptr的构造函数中分配的。因此直接使用new,需要一块内存分配给Widget,还要一块内存分配给控制块。

如果选择使用 make_shared 的话, 情况就会变成下面这样:

在这里插入图片描述

内存分配的动作, 可以一次性完成. 这减少了内存分配的次数, 而内存分配是代价很高的操作.

make_share一次分配就足够了。这是因为std::make_shared申请一个单独的内存块来同时存放Widget对象和控制块。这个优化减少了程序的静态大小,因为代码只包含一次内存分配的调用,并且这会加快代码的执行速度,因为内存只分配了一次。另外,使用std::make_shared消除了一些控制块需要记录的信息,这样潜在地减少了程序的总内存占用。

异常安全

如果我们在调用processWidget的时候使用computePriority(),并且用new而不是std::make_shared:

processWidget(std::shared_ptr(new Widget), computePriority()); //潜在的资源泄露

就像注释指示的那样,上面的代码会导致new创造出来的Widget发生泄露。那么到底是怎么泄露的呢?调用代码和被调用函数都用到了std::shared_ptr,并且std::shared_ptr就是被设计来阻止资源泄露的。当最后一个指向这儿的std::shared_ptr消失时,它们会自动销毁它们指向的资源。如果每个人在每个地方都使用std::shared_ptr,那么这段代码是怎么导致资源泄露的呢?

答案和编译器的翻译有关,编译器把源代码翻译到目标代码,在运行期,函数的参数必须在函数被调用前被估值,所以在调用processWidget时,下面的事情肯定发生在processWidget能开始执行之前:

  • 一个Widget必须被创建在堆上。
  • std::shared_ptr(负责管理由new创建的指针)的构造函数必须被执行。
  • computePriority必须跑完。

编译器不需要必须产生这样顺序的代码。但“new Widget”必须在std::shared_ptr的构造函数被调用前执行,因为new的结构被用为构造函数的参数,但是computePriority可能在这两个调用前(后,或很奇怪地,中间)被执行。也就是,编译器可能产生出这样顺序的代码:

执行“new Widget”。 执行computePriority。 执行std::shared_ptr的构造函数。

如果这样的代码被产生出来,并且在运行期,computePriority产生了一个异常,则在第一步动态分配的Widget就会泄露了,因为它永远不会被存放到在第三步才开始管理它的std::shared_ptr中。

使用std::make_shared可以避免这样的问题。调用代码将看起来像这样:

processWidget(std::make_shared(), computePriority());

如果使用std::unique_ptr和std::make_unique来替换std::shared_ptr和std::make_shared,事实上会用到同样的理由。因此,使用std::make_unique代替new就和“使用std::make_shared来写出异常安全的代码”一样重要。

make_shared缺点

构造函数非public时,无法使用 make_shared

make_shared 虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了。

对象的内存可能无法及时回收

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存(不用make_share时,因为是两块内存,所以不存在该问题。), 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题.

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值