C++ 智能指针

目录
1. 产生的原因-内存泄漏
2. 智能指针简介
3. auto_ptr
4. unique_ptr
5. shared_ptr
6. weak_ptr

参考链接:智能指针auto_ptr 参考shared_ptr参考

1. 产生的原因-内存泄漏

当我们当从堆中申请了内存后,如果不释放空间,就发生内存泄漏。内存泄漏的情景主要有几种:

* new和delete没有匹配。
* 没有正确清楚嵌套对象指针。
* 释放对象数组的时候没有使用方括号。
* 指向对象的指针数组不等同于对象数组(数组中的每个对象为指针)。
* 缺少拷贝构造函数或者没有重载赋值操作符,导致按值传递,两次释放相同的内存。
* 没有将基类的析构函数定义为虚函数。

然而,即便写出了清晰并且带有错误验证的代码,有时候仍会出现问题。比如和别人合作写代码的时候,合作者可能就会在完美的程序中增加一个提早返回的语句,导致申请的内存空间无法释放。针对以上原因,C++推出了智能指针。

这里顺便提一个概念,野指针。野指针表示指向被释放的或者访问受限内存的指针。使用野指针很容易导致内存泄漏。 在使用指针的时候除了避免内存泄漏,野指针也需要尽量避免的。产生原因主要有几个:

  • 指针变量没有初始化。
  • 指针被free或者delete后,没有置为NULL。
  • 指针操作超过变量作用范围。

2. 智能指针简述

智能指针定义在memoery文件中,是一个RAII(Resource Acquisition is initialization)的类模型。智能指针类的构造函数中传入一个普通指针,析构函数中释放传入的指针。因为智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。

3. C++98 auto_ptr

auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题,如下所示。

int* p = new int(100);
try
{
    doSomething();
    cout << *p << endl;
    delete p;
}
catch(){}

当doSomething();部分抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄露,因而引入auto_ptr。

3.1 auto_ptr的使用

在使用auto_ptr的时候,我们实际上是创建一个auto_ptr类型的局部对象。该局部对象析构时,会将自身所拥有的指针空间释放,从这个角度避免了内存泄露。另外,auto_ptr重载了’*‘和’->’,运算符,可以像普通指针那样进行提领操作。

#include <iostream>
#include <string>
#include <memory>
class Test
{
public:
	Test(int a = 0 ) : m_a(a){
		std::cout << "Calling constructor" << std::endl;
	}
	~Test( )
	{
		std::cout << "Calling destructor" << std::endl;
	}
public:
	int m_a;
};


//抛出异常
void Fun(int a, int b, int &c)
{
	if( a == 0 )
	{
		throw -1;
	}
	c = b / a;
	return;
}


//测试
int main( )
{
	try{
		std::auto_ptr<Test> p(new Test(5)); 
		int c = 0; Fun(0, 3, c);
		std::cout << p->m_a << std::endl;//提领操作
	}
	catch(...){
		std::cout << "catch()" << std::endl;
	}
	return 0;
}

/*
Output:

Calling constructor
Calling destructor
catch()
*/

使用auto_ptr的时候有几个问题需要注意:

(a) auto_ptr的构造函数为explicit,阻止了一般指针隐式类型转换为auto_ptr的构造,所以如下的创建方式是编译不过的。

int* p = new int(1);
auto_ptr<int> ap = p;//无法进行隐式变换

(b) 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时应避免像下例所示的多个auto_ptr对象管理同一个指针。

int* np = new int(1);
auto_ptr<int> p1(np);
auto_ptr<int> p2(np);

© auto_ptr的析构函数删除对象使用delete而不是delete[],所以auto_ptr不能用来管理数组指针。

int *p = new int[100];
auto_ptr<int> ap(p);//仅仅释放第一个元素空间,造成内存泄漏

(d) C++中对一个空指针NULL执行delete操作是安全的,所以在auto_ptr的析构函数中无须判断它所拥有指针是否为空。

3.2 auto_ptr的拷贝构造和赋值

auto_ptr要求对所拥有的指针完全占有,也就是说,一个一般指针不能同时被两个auto_ptr所拥有。这也意味着auto_ptr在拷贝构造和赋值运算符重载时要做特殊处理。具体的做法是对所有权进行了完全转移,在拷贝和赋值时,剥夺原auto_ptr拥有权(置空),赋予当前auto_ptr对指针的拥有权。

由于会修改原对象,所以auto_ptr的拷贝构造函数以及赋值运算符重重载函数的参数是引用而不是常(const)引用。这时候就需要注意以下几个问题:

(a) auto_ptr对象被拷贝或者被赋值后,已经失去了对原指针的所有权,此时,对这个auto_ptr的读取操作是不安全的。尤其是当auto_ptr作为函数参数按值传递,传入的实参auto_ptr对指针的所有权会转移到函数临时的auto_ptr对象上。临时auto_ptr在函数退出时被析构,则当函数调用结束时,原实参所指向的对象已经被删除了。

void func(auto_ptr<int> ap)
{
	cout << *ap << endl;
}

auto_ptr<int> ap(new int(1));
func(ap);
cout << *ap1 << endl;//错误,函数调用结束后,ap1已经不再拥有任何对象了

因此,要避免使用auto_ptr对象作为函数参数按值传递,按引用传递在调用函数是不会发生所有权转移,但是无法预测函数体内的操作,有可能在函数体内进行了所有权的转移。因此按引用传递auto_ptr作为函数参数也是不安全的。如果不得不使用auto_ptr对象作为函数参数时,尽量使用const引用传递参数。

(b) auto_ptr支持所拥有的指针类型之间的隐式类型转换。

class base{};
class derived: public base{};
auto_ptr<base> apbase = auto_ptr<derived>(new derived);//auto_ptr<derived>隐式转换到auto_ptr<base>

© auto_ptr对象不能作为STL容器元素。C++的STL容器对于容器元素类型的要求是有值语义,即可以赋值和复制。但是auto_ptr在赋值和复制时都进行了特殊操作。

3.3 auto_ptr的相关操作

T* get(); //获得auto_ptr所拥有的指针。
T* release(); //释放auto_ptr的所有权,并将所有用指针返回。
void reset(T* ptr=0); // 接收所有权,接收之前拥有其它指针的话,必须先释放其空间。

4. C++11 unique_ptr

unique_ptr和auto_ptr类似,都是同一时刻只能有一个unique_ptr指向给定对象。但是它禁止拷贝语义,只支持移动语义。

4.1 unique_ptr的使用方法

unique_ptr不能拷贝,也不能赋值,只能通过move()转换所有权或者通过reset()重置所有权。另外,它还可以通过release方法释放所有权。

#include <iostream>
#include <memory>

class Test
{
public:
	Test(int a = 0 ) : m_a(a){
		std::cout << "Calling constructor" << std::endl;
	}
	~Test( )
	{
		std::cout << "Calling destructor" << std::endl;
	}
public:
	int m_a;
};

//测试
int main() {
    std::unique_ptr<Test> uptr(new Test(10));  //绑定动态对象
    //std::unique_ptr<Test> uptr2 = uptr;  //不能赋值
    //std::unique_ptr<Test> uptr2(uptr);  //不能拷贝
    std::cout << "uptr->ma: " << uptr->m_a << std::endl;

    std::unique_ptr<Test> uptr2 = std::move(uptr); //转换所有权
    //std::cout << "uptr->ma: " << uptr->m_a << std::endl;
    std::cout << "uptr2->ma: " << uptr2->m_a << std::endl;
    //uptr2.release(); //释放所有权
    return 0;
}//离开作用域是自动析构

/*
Output:

Calling constructor
uptr->ma: 10
uptr2->ma: 10
Calling destructor
*/

上例需要注意的是,如果我们取消掉release()函数的注释,两个指针都失去了对象的所有权,这时候之前的Test对象就无法被销毁。

5. C++11 shared_ptr

shared_ptr 是一个标准的共享所有权的智能指针, 允许多个指针指向同一个对象,主要是为了解决auto_ptr和unique_ptr在对象所有权上的局限性。因为加入了计数机制,也就产生了额外的开销:

  • shared_ptr对象除了包括一个所拥有对象的指针外, 还必须包括一个引用计数代理对象的指针。

  • 时间上的开销主要在初始化和拷贝操作上, *和->操作符重载的开销跟auto_ptr是一样。

5.1 何时需要shared_ptr?

  • 程序不知道自己需要使用多少对象. 如使用窗口类, 使用 shared_ptr 为了让多个对象能共享相同的底层数据.

  • 程序不知道所需对象的准确类型.

  • 程序需要在多个对象间共享数据.

5.2 shared_ptr的使用方法

每一个shared_ptr的拷贝都指向相同的内存。每拷贝一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

#include <iostream>
#include <memory>

int main() {
    {
        int a = 10;
        //使用make_shared函数初始化。
        std::shared_ptr<int> ptra = std::make_shared<int>(a);

        //智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。
        std::shared_ptr<int> ptra2(ptra);
        std::cout << ptra.use_count() << std::endl;

        int b = 20;
        int *pb = &a;
        //std::shared_ptr<int> ptrb = pb;  //error
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);

        //ptra2原来指向的对象引用计数减1(如果为0, 释放内存), ptrb指向的对象的引用计数加1
        ptra2 = ptrb; //assign,拷贝构造
        pb = ptrb.get(); //获取原始指针

        std::cout << ptra.use_count() << std::endl;
        std::cout << ptrb.use_count() << std::endl;
    }
}

//Output: 2 1 2

5.3 注意事项

a. 不能将指针直接赋值给一个智能指针,一个是类,一个是指针。

std::shared_ptr<int> p4 = new int(1);//ERROR

b. 不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。

c. shared_ptr作为对象成员变量时,应避免循环引用。

假设a对象中含有一个shared_ptr指向对象b, 对象b中含有一个shared_ptr 指向对象a, 并且 a, b 对象都是堆中分配的。当m_spa被销毁时, 对象a的use_count从2变为1; 对象b同理。至此,a和b失去联系,但是却无法继续销毁自身,如下所示。

#include <iostream>
#include <memory>
class CB;
class CA;

class CA{
public:
    CA() { }
    ~CA() { std::cout << "~CA()" << std::endl;}

    void Register(const std::shared_ptr<CB>& sp){
        m_sp = sp;
    }
private:
    std::shared_ptr<CB> m_sp;
};

class CB{
public:
    CB() { };
    ~CB() { std::cout << "~CB()" << std::endl;};

    void Register(const std::shared_ptr<CA>& sp){
        m_sp = sp;
    }
private:
    std::shared_ptr<CA> m_sp;
};


int main(){
   std::shared_ptr<CA> spa(new CA());
   std::shared_ptr<CB> spb(new CB()); 
   spa->Register(spb);
   spb->Register(spa);
   std::cout << spb.use_count() << std::endl;//2
   std::cout << spa.use_count() << std::endl;//2
}
//两个对象都没有调用析构函数(都没有被销毁)

解决此方法是使用 weak_ptr 替换 shared_ptr,在weak_ptr小节介绍。

d. shared_ptr 不支持数组, 如果使用数组, 需要自定义删除器。

// 下例是一个利用 lambda 实现的删除器
std::shared_ptr<int> sps(new int[10], [](int *p){delete[] p;});

//对于数组元素的访问, 需使要使用 get 方法取得内部元素的地址后, 再加上偏移量取得
for (size_t i = 0; i < 10; i++)
	*((int*)sps.get() + i) = 10 - i;

6. C++11 weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作。

weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

6.1 weak_ptr的使用

  • use_count():观测资源的引用计数

  • expired():等价于use_count()==0,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。

  • lock():从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。
    当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。

#include <iostream>
#include <memory>

int main() {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        std::cout << sh_ptr.use_count() << std::endl;//1

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;//1

        if(!wp.expired()){//判断wp是否指向对象
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;//2
        }
    }
    //delete memory
}

6.2 利用weak_ptr规避循环引用

如果忘记了循环引用问题,可以到[shared_ptr[(#5)部分回顾,这里主要给出解决方案。

#include <iostream>
#include <memory>
class CB;
class CA;

class CA{
public:
    CA() { }
    ~CA() { std::cout << "~CA()" << std::endl;}

    void Register(const std::shared_ptr<CB>& sp){
        m_sp = sp;
    }
private:
    std::weak_ptr<CB> m_sp;//将这里设置为weak_ptr
};

class CB{
public:
    CB() { };
    ~CB() { std::cout << "~CB()" << std::endl;};

    void Register(const std::shared_ptr<CA>& sp){
        m_sp = sp;
    }
private:
    std::shared_ptr<CA> m_sp;
};


int main(){
   std::shared_ptr<CA> spa(new CA());
   std::shared_ptr<CB> spb(new CB()); 
   spa->Register(spb);
   spb->Register(spa);
   std::cout << spb.use_count() << std::endl;//1
   std::cout << spa.use_count() << std::endl;//2
}

/*
成功调用析构函数,销毁对象
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值