内存管理-智能指针

问题引出:c++继承了c那高效而又灵活的指针,使用起来稍微不小心就会导致内存泄漏,悬挂指针,越界访问等问题,比如以下代码:

int *p = new int;
... 
if(xxx)
    goto loop;
delete p;

以上代码,很明显很容易导致内存泄漏问题,比如当满足条件进行跳转,或者当delete p;前的操作出现异常导致程序退出,这些都会导致动态内存没有正确释放掉,另外的当一个复杂程序,如果由于不小心delete/free了多次同一指针的话,这种错误编译能够通过,但在运行时会出现程序崩溃错误,另外还有可能忘了对指针进行delete导致内存泄漏,而智能指针可以在哪些变量或对象在退出作用域时–不管是正常退出或异常退出–也能够进行相应的资源释放工作;
注:delete/free一个空指针总是没有错误的;
以下就总结一下标准库中的几种智能指针包括:auto_ptr , shared_ptr , unique_ptr , weak_ptr ;都包含在头文件memory中;

1: auto_ptr

存在多种的智能指针,其中最有名的应该是c++98标准中改的自动指针“auto_ptr”,【注:c++11标准中声明废弃该智能指针,而应选用新的智能指针实std::unique_ptr,目前c++11中新增的智能指针有unique_ptr, shared_ptr, weak_ptr】,auto_ptr其实是一种类模板,但其重载了operator*和operator->,auto_ptr对象的行为类似指针。其构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针,创建auto_ptr对象的示例方法如下代码。c++保证当那些动态分配内存的指针变量离开作用域时,会调用auto_ptr的析构函数,进而使用delete操作符释放动态申请的指针所指向的内存资源;

/* 不能使用赋值的方法进行初始化一个auto_ptr,因为根据一般指针创建auto_ptr的构造函数被声明为explict,
必须进行显式的调用构造函数进行对象的创建,注意不是不可以赋值 */
auto_ptr<int> p(new int(12));  
auto_ptr<ClassA> pA(factory.create()); // 本质上还是传入一个普通指针

auto_ptr的注意事项与常见操作:
1: auto_ptr所有权的转移
即auto_ptr的对象所有权是独占的,不可能有两个auto_ptr对象同时拥有同一个动态对象的所有权【这个要求也是在提醒编码时要注意防范以同一个对象为初值,将两个auto_ptr初始化】;因为auto_ptr所有权独占这个条件,导致auto_ptr的copy构造函数和赋值运算符函数的工作也有所不同;示例代码:

auto_ptr<ClassA> ptr1(new ClassA);
auto_ptr<ClassA> ptr2(ptr1); // 调用拷贝构造函数进行对象的创建
auto_ptr<ClassA> ptr3(new ClassA);
auto_ptr<ClassA> ptr4;
...
ptr4 = ptr3; // 调用赋值运算符

解释:第一个语句中ptr1拥有new出来的那个对象的所有权,在第二个语句中,拥有权由ptr1转交给ptr2。此后,ptr2就拥有了那个new出来的对象的所有权,ptr1不再拥有它,只剩下一个null指针在手即“是去了所有权,只剩一个null指针”;上面的赋值操作也是相同的道理,ptr4拥有了原先ptr3拥有的对象,ptr3后来只生一个null指针;
注:如果ptr3被赋值之前正拥有另一个对象,赋值动作发生时会调用delete,将该对象删除;

可能发生拥有权的转移有函数转交到另一个函数的情况:
1:某函数是数据的终点: auto_ptr以传值得方式被当作一个参数传递给某函数,然后调用端获得了这个auto_ptr的拥有权,如果函数不再将它传递出去,它所指的对象就会在函数退出时被删除;

void sink(auto_ptr<ClassA>); // sink() gets ownership

2:某函数是数据的起点,当一个auto_ptr被返回,其拥有权便被转交给调用端了;

auto_ptr<ClassA> f()
{
    auto_ptr<ClassA> ptr(new ClassA);
    ...
    return ptr; // transfet ownership to calling function
}

void g()
{
    auto_ptr<ClassA> p;
    for(int i=0; i<10; i++)
    {
        p = f(); // p gets ownership of the returned object
            // previous rerurned object of f() gets deleted
        ...
    }
} // lasr-owned object of p gets deleted

由于函数调用导致的所有权转交问题,所以函数中的用法一定要注意:当五一转交所有权时,不要再参数列中使用auto_ptr,也不要以它作为返回值;

可以创建const型auto_ptr对象来终结所有权的转交;
如:

const auto_ptr<int> p(new int());
auto_ptr<int> q(new int);
*p = 12; // OK,关键词const并非意味不能更改对象,而是意味不能更改auto_ptr的拥有权而已;
// 这个时候const型的auto_ptr更像是T* const p,而不是const T* P,尽管语法上看上去比较像后者;
p = q; // Error,想要转交所有权,出现compile-time error

当auto_ptr作为类的成员变量之一时,别忘了要重写拷贝构造函数和赋值运算符函数,这两个如果是缺省的都会有所有权转交的发生;

auto_ptr使用注意小结:
1: auto_ptr之间不能共享所有权;
2:auto_ptr不能指向数组;
3:auto_ptr不要作为容器的成员;
4:根据一般指针生成一个auto_ptr的构造函数被声明为explict,所以不要一下方式进行对象的创建;

auto_ptr<int> p = new int(12); // ERROR
auto_ptr<int> p(new int(12)); // OK

注:auto_ptr的接口与一般指针非常相似:operator*用来提领其所指对象,operator->用来指向对象中的成员,然而,*所有的指针算术(包括++)都没有定义;

2: shared_ptr

是一种引用计数型的智能指针,允许多个指针指向同一个对象,包装了new操作符在对上分配的动态对象,与auto_ptr或unique_ptr不同的是,shared_ptr可以被自由的拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时,才删除被包装的动态分配的对象;shared_ptr可以被安全的放在标准容器中,是STL容器中存储指针的最标准解法;
shared_ptr代码示例

shared_ptr<string> p1; // 可以指向string的shared_ptr,默认初始化时保存着一个空指针nullptr
shared_ptr<list<int> > p2; // 可以只想list<int>的shared_ptr

shared_ptr和unique_ptr都支持的操作:

shared_ptr<T> sp / unique_ptr<T> up; // 空智能指针,可以指向类型T的对象
p // 将p用作一个条件进行判断,若p指向一个对象,则为true
*p // 解引用,获得它所指向的对象
p->mem // 等价于(*p).mem
p.get() // 返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p,q) / p.swap(q) // 交换p和q的指针

shares_ptr独有的操作:

make_shared<T>(args); // 返回一个shared_ptr,指向一个动态分配的类型为T的对象,是用args初始化此对象;
shared_ptr<T>p(q); // p是qshared_ptr q的拷贝,此操作会递增q中的计数器,q中国的指针必须能转换为T*
p = q; // p和q都是shared_ptr,所保存的指针必须能互相转换,此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放
p.unique(); // 若p.use_count();为1,返回true,否则返回false
p.use_count(); // 返回与p共享对象的智能指针数量,主要用于调试作用

示例代码:

if(p) // p为shared_ptr,相当于if(p==nullptr)
shared_ptr<int> p1 = make_shared<int>(42); 
// 零初始化
shared_ptr<int> p2 = make_shared<int>(); 

/* 
    p6指向一个动态分配的空vector<string>通常用auto定义一个对象来
    保存make_shared的结果,方式简单,有编译器去识别;
*/
auto p3 = make_shared<vector<string> >(); 

// 错误,对于接受指针参数的智能指针构造函数是explict,必须直接初始化形式,即等号改为括号
shared_ptr<int> p4 = new int(102); 

shared_ptr<int> clone(int p)
{
    // 错误,这里要求的是普通指针隐式的转换为shared_ptr<int>,但该构造函数是explict的
    return new int(p); 
    // 正确的是 return shared_ptr<int>(new int(p));
}

每个shared_ptr都有一个关联的计数器,通常称其为引用计数,拷贝一个shared_ptr,将它作为参数传递给一个函数,作为函数的返回值,用一个shared_ptr去初始化另一个shared_ptr,这几种情况都会递增它所关联的计数器;
当给shared_ptr赋予一个新值,或shared_ptr被销毁,例如一个局部的shared_ptr离开其作用域是,计数器就会递减;
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象;

void use_factory(T args)
{
    shared_ptr<Foo> p = factory(arg);
    // 使用p
    return p; // 返回p时,引用计数进行了递增操作
} 
/*
 p离开了作用域,但它指向的内存不会被释放掉,因为上面return的时候计数加1,不为0,
    不会进行相应的资源释放回收
*/

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为只能指针默认使用delete释放它所关联的对象;
定义和改变shared_ptr的其它方法

shared_ptr<T> p(q); // p管理内置指针q所指向的对象,q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u); // p从unique_ptr u那里接管了对象的所有权,将u置为空
shared_ptr<T> p(q, d); // p接管了内置指针q所指向的对象的所有权,q必须能转换为T*类型,p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d); // p是shared_ptr p2的拷贝唯一的区别是p将用可调用兑现gd来代替delete
p.reset(); // 若p是唯一指向其对象的shared_ptr,reset会释放此对象,若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空,若还传递了参数d,将会调用d而不是delete来释放q
p.reset(q);
p.reset(q, d);

使用我们自己的释放操作
默认情况下,shared_ptr假定它们直线改的是动态内存,因此,当一个shared_ptr被销毁时,它默认的对它管理的指针进行delete操作,对于一些数据库或网络的连接释放,我们需要自己定义函数代替默认的delete操作,比如:

void end_connection(connection *p)
{
    disconnect( *p );
}

void f(destination &d)
{
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
}

智能指针使用建议:
1:不使用相同的内置指针值初始化(或reset)多个智能指针;
2:不delete get()返回的指针;
3:不使用get()初始化或reset另一个智能指针;
4:如果使用get()返回的指针,记住当最后一个对应的之恩那个指针销毁后,对应的指针就变为无效了;
5:如果使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器;

3: unique_ptr

unique_str是在c++11标准中定义的用来取代auto_ptr的新的智能指针,不仅能够代理new创建的单个对象,也能够代理new[ ]创建出来的数组对象,这点是auto_ptr不能实现的;

一个unique_ptr“拥有”它所指向的对象,某个时刻只能有一个unique_ptr指向一个给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁;

unique_ptr<string> pStr(new string("Hello"));
unique_ptr<int> p1;
unique_ptr<int> p2(new int(42));
p1 = p2; // 错误,unique_ptr不支持普通赋值
unique_ptr<int> p3(p2); // 错误,unique_ptr不支持普通拷贝

**// 由于一个unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作,
// 但是有一个例外就是,unique_ptr可以拷贝或赋值一个将要被销毁的unique_ptr,最常见的就是从函数返回一个unique_ptr**
unique_ptr<int> clone(int p)
{
    return unique_ptr<int>(new int(p)); // 创建一个匿名对象用于返回
}

unique_ptr的独有操作:

unique_ptr<T> u1; // 空unique_ptr,可以指向类型为T的对象,u1会使用delete来释放它的指针,u2会使用一个类型为D的可调用对象释放它的指针
unique_ptr<T, D> u2;
unique_ptr<P, D> u(d); // 空unique_ptr,指向类型T的对象,用类型为D的对象d代替delete
u = nullptr; // 释放u指向的对象,将u置为空
u.release(); // u放弃对指针的控制权,返回指针,并将u置为空
u.reset(); // 释放u指向的对象
u.reset(q); // 如果提供了内置指针q,令u指向这个对象,否则将u置为空
u.reset(nullptr);
// 以上方法,release或reset可以将指针的所有权从一个(非const)unique_ptr转移到另一个unique; 比如:
unique_ptr<string> p2(p.release()); // 所有权从p1转移给p2
unique_ptr<string> p3(new string("Hello Test"));
p2.reset(p3.release()); // reset释放了p2原来指向的内存,将所有权从p3转移到p2

*// 注意:如果不用另一个智能指针来保存release返回的指针,那我们的程序就要负责资源的释放,比如:*
auto p = p2.release();
// ...
delete p; *// 对于返回给普通的指针类型或auto,需要手工的添加delete操作*

类似shared_ptr,unique_ptr默认情况下用delete释放它指向的对象,与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器;

4: weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使用weak_ptr指向对象,对象也还是会被释放,weak_ptr是一种“弱共享”对象;
注:weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的助手,因为它不具有普通指针的行为,没有重载operator*和operator->【注:这是故意不重载的,因为它不共享指针,不能操作资源,这就是称之为weak_ptr的原因】,它的最大作用在于协助shared_ptr工作,weak_ptr的构造和析构都不会影响shared_ptr的引用计数,只是一个静静的观察者;
weak_ptr的操作:

weak_ptr<T> w; // 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp); // 与shared_ptr sp指向相同对象的weak_ptr,sp指向的类型必须能转换为类型T
w = p; // p可以是一个shared_ptr或一个weak_ptr,赋值后w与p共享对象
w.reset(); // 将w置为空
w.use_count(); // 与w共享对象的shared_ptr的数量
w.expired(); // 若w.use_count()为0,返回true,否则返回false
w.lock(); // 如果expired为true,返回一个空shared_ptr,否则返回一个指向w的对象的shared_ptr 

创建一个weak_ptr时,要用一个shared_ptr来初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);

创建wp不会改变p的引用计数,wp指向的对象可能被释放掉,由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,该函数检查weak_ptr指向的对象是否仍存在,如果存在,lock返回一个指向共享对象的shared_ptr;

if(shared_ptr<int> np = wp.lock())
{
    // 在if中,np与p共享对象
    // ...
}

补充资料:weak_ptr用于解决循环引用和自引用对象问题 【review at 2015-10-03】

weak_ptr
1 、weak_ptr是用来解决循环引用和自引用对象
从前面的例子我们可以看出,引用计数是一种很便利的内存管理机制,但是有一个很大的缺点,那就是不能管理循环引用或自引用对象(例如链表或树节点),为了解决这个限制,因此weak_ptr被引入到boost的智能指针库中。

2、weak_ptr并不能单独存在
它是与shared_ptr同时使用的,它更像是shared_ptr的助手而不是智能指针,因为它不具备普通指针的行为,没有重载operator *和->操作符,这是特意的。这样它就不能共享指针,不能操作资源,这正是它“弱”的原因。它最大的作用是协助shared_ptr工作,像旁观者那样观察资源的使用情况。

3、 weak_ptr获得资源的观察权
weak_ptr可以从一个shared_ptr或另外一个weak_ptr构造,从而获得资源的观察权,但weak_ptr并没有共享资源,它的构造并不会引起引用计数的增加,同时它的析构也不会引起引用计数的减少,它仅仅是观察者。

4、 weak_ptr可以被用于标准容器库中的元素
weak_ptr实现了拷贝构造函数和重载了赋值操作符,因此weak_ptr可以被用于标准容器库中的元素,例如:在一个树节点中声明子树节点std::vector

weak_ptr常见用途:

结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例所属对象的访问,但是,不参与引用计数。 如果您想要观察对象但不需要其保持活动状态,请使用该实例。 在某些情况下需要断开 shared_ptr 实例间的循环引用。 头文件:<memory>
weak_ptr的用法如下:

weak_ptr用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。weak_ptr并没有重载operator->operator *操作符,因此不可直接通过weak_ptr使用对象。提供了expired()lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空“shared_ptr)。循环引用的场景:如二叉树中父节点与子节点的循环引用,容器与元素之间的循环引用等。
智能指针的循环引用

循环引用:
“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。下面给段代码来说明循环引用:

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

class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
  //weak_ptr<B> pb;
  shared_ptr<B> pb;
  void doSomthing()
  {
    //      if(pb.lock())
    //      {
    //
    //      }
  }

  ~A()
  {
    cout << "kill A\n";
  }
};

class B
{
public:
  //weak_ptr<A> pa;
  shared_ptr<A> pa;
  ~B()
  {
    cout <<"kill B\n";
  }
};

int main(int argc, char** argv)
{
  shared_ptr<A> sa(new A());
  shared_ptr<B> sb(new B());
  if(sa && sb)
  {
    sa->pb=sb;
    sb->pa=sa;
  }
  cout<<"sa use count:"<<sa.use_count()<<endl;
  return 0;
}

上面的代码运行结果为:sa use count:2, 注意此时sa,sb都没有释放,产生了内存泄露问题!!!
即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。

解决循环引用有下面有三种可行的方法:

一般来讲,解除这种循环引用有下面有三种可行的方法( 参考 ):
1 . 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
2 . 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
3 . 使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr

强引用和弱引用

一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
使用weak_ptr来打破循环引用
代码如下:

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

class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public
  weak_ptr<B> pb;
  //shared_ptr<B> pb;
  void doSomthing()
  {
    if(pb.lock())
    {

    }
  }

  ~A()
  {
    cout << "kill A\n";
  }
};

class B
{
public:
  //weak_ptr<A> pa;
  shared_ptr<A> pa;
  ~B()
  {
    cout <<"kill B\n";
  }
};

int main(int argc, char** argv)
{
  shared_ptr<A> sa(new A());
  shared_ptr<B> sb(new B());
  if(sa && sb)
  {
    sa->pb=sb;
    sb->pa=sa;
  }
  cout<<"sb use count:"<<sb.use_count()<<endl;
  return 0;
}

参考资料:
http://www.tuicool.com/articles/6j2yy2z
《C++ 标准程序库》 侯捷 孟岩 译
《C++ Primer》(第5版) 王刚 杨巨峰 译
《Boost程序库完全开发指南》(第3版) 罗剑锋 著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值