C++智能指针剖析

智能指针

什么是智能指针?

程序里有一些指针管理着资源,比如文件指针,指向malloc动态分配空间的指针。这些指针在使用完后必须被释放,否则有内存泄露问题。而程序员往往会因为程序执行流复杂而疏忽了释放这些指针指向的资源,所以,智能指针智能在自动释放资源。

这里的概念和RAII很相似,用类管理资源,构造即分配,析构即释放。

最简单的模型

template<class T> 
class AutoPtr {
public:  
    AutoPtr(T* ptr = NULL)  
    : _ptr(ptr)  
    {} 
    ~AutoPtr() {      
       if(_ptr)  
         delete _ptr;  
    } 
private:   
    T* _ptr; 
};

智能指针的版本

auto_ptr

第一个版本是auto_ptr,多个智能指针之间是浅拷贝的方式。当多个智能指针指向同一块资源,会有多次释放的错误问题,因此auto_ptr同时只允许一个智能指针对象管理资源,发生复制/拷贝对象时,把资源的管理权限交出去。

template <typename T>
class AutoPtr {
public:
    AutoPtr(T* ptr = NULL)
        :_ptr(ptr)
    {
    }
    ~AutoPtr()
    {
        if (_ptr) {
            delete _ptr;
        }
    }
    AutoPtr(AutoPtr<T>& ap)
        :_ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }
    AutoPtr<T>& operator=(AutoPtr<T>& ap) {
        if (this != &ap) {
            if (_ptr) {
                delete _ptr;
            }
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }
    T& operator*() {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
private:
    T * _ptr;
};

但如此又引入新的问题

    AutoPtr<int> ap1(new int(5));
    AutoPtr<int> ap2(ap1);
    cout << *ap2 << endl;
    cout << *ap1 << endl;

ap2复制ap1,此时ap2拥有资源,ap1指针被置空。本想复制一份指针对象,却把自己搞丢了。所以禁用auto_ptr.

scoped_ptr

scoped的做法就很粗暴了,直接禁止智能指针对象的赋值和拷贝。

private:
    //对象不可调用
    ScopedPtr(const ScopedPtr<T>& p);
    ScopedPtr<T>& operator=(const ScopedPtr<T>& p);

以此为原型,诞生出scoped_array,该类管理一个动态申请内存的数组。

调用scoped_array

int n;
boost::scoped_array<int> array(new int[n]);
for(int i=0; i<n; i++){
    cout<<array[i]<<endl;
}

shared_ptr

目前最好的智能指针版本,采用引用计数。
模拟实现一下:

template <typename T>
class Delete {
public:
    void operator ()(T*& p) {
        if (p) {
            delete p;
            p = NULL;
        }
    }
};

class FClose {
public:
    void operator()(FILE*& p)
    {
        if (p) {
            fclose(p);
            p = NULL;
        }
    }
};


template <typename T, class Dx = Delete<T>>
class SharedPtr {
public:
    SharedPtr(T* ptr = NULL)
        :_ptr(ptr),_pCount(NULL)
    {
        if (ptr) {
            _pCount = new int(1);
        }
    }
    ~SharedPtr()
    {
        if (_pCount && 0 == --*_pCount) {
            //定制删除器
            Dx()(_ptr);
            delete _pCount;
        }
    }

    SharedPtr(const SharedPtr<T>& sp)
        :_ptr(sp._ptr), _pCount(sp._pCount)
    {
        if (_pCount) {
            ++(*_pCount);
        }
    }
    //考虑this可能为NULL或者指向资源,sp为空或指向资源
    SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
        if (this != &sp) {
            if (_pCount && 0 == --*_pCount) {
                //独占销毁
                Dx()(_ptr);
                delete _pCount;
            }
            _ptr = sp._ptr;
            _pCount = sp->_pCount;
            if (_pCount) {
                ++(*_pCount);
            }
        }
        return *this;
    }

    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
private:
    T * _ptr;
    int * _pCount;//引用计数
};

测试用例:

void TestShared()
{
    SharedPtr<ListNode<int> > ap1(new ListNode<int>(10));
    SharedPtr<int> ap(new int(50));
    SharedPtr<FILE, FClose> ap3(fopen("xx.txt", "w"));
}

循环引用问题

看了很多网上的解释,大都对循环引用过程的解释有问题。

主要的误区是分不清智能指针的析构和资源自己的析构函数什么时候被调用。

本着探索精神,在vs2017中不断使用F11调试,得出了和结果一致的解释,如有不妥欢迎评论。

先看一个正常使用的例子

template <class T>
struct ListNode {
    shared_ptr<ListNode<T> > _pPre;
    shared_ptr<ListNode<T> > _pNext;
    T _data;
    ListNode(const T& data)
        :_pNext(NULL), _pPre(NULL),_data(data)
    {
        cout << this << endl;
    }
};
void Test(){
    shared_ptr<ListNode<int> > sp1(new ListNode<int>(10));//node1
    shared_ptr<ListNode<int> > sp2(new ListNode<int>(20));//node2
    sp1->_pNext = sp2;
}

结果分析:

034CB100        //node1
034C5658
Destory 034CB100        //node1析构
Destory 034C5658

shared_ptr为其管理的每一块资源设置一个引用计数,sp1的Next指针指向sp2,sp2的引用计数被增加到2.

此时sp1的引用比sp2的引用:1:2

出了Test函数,首先调用sp2的析构函数, 智能指针的析构只做一件事 ,–当前资源的引用,若为0则销毁。所以此时sp2的引用变为1.

接着调用了sp1的析构,此时sp1:sp2 = 0 : 1.这时sp1引用为0,调用node1的真正析构函数,Node1生命周期结束之后需要销毁在Node1里创建的智能指针对象 ,因此调用Next智能指针的析构函数,也就是调用sp2的析构函数,–sp2的引用后发现为0,再调用node2的析构销毁node2。

循环引用

//循环引用
void Test()
{
    shared_ptr<ListNode<int> > sp1(new ListNode<int>(10));
    shared_ptr<ListNode<int> > sp2(new ListNode<int>(20));
    //此时sp1,sp2引用为1
    sp1->_pNext = sp2;
    sp2->_pPre = sp1;
    //此时sp1,sp2引用均为2
}

出了Test函数调用sp2的析构函数,sp1:sp2 = 2 : 1.

再调用sp1的析构函数,sp1 : sp2 = 1 : 1.

引用计数均不为0,所以不会调用node本身的析构函数,造成内存泄露问题。

标准库为了解决循环引用问题

库的实现版本通过原子操作实现线程安全。并且是维护了一个use计数,一个weak计数。shared_ptr管理的资源里面若有指向同类资源的指针替换为weak智能指针。
weak指针表达对资源的临时引用权限,其引用的资源随时可能被销毁。在需要使用weak所指对象时,升级为shared_ptr,若对象已经被销毁,则升级失败。
这样在析构资源时,只需要判断use计数就可以了,use为0立即析构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在引用中,给出了一个程序的代码片段,这段代码涉及到了内存方面的问题。在这段代码中,函数`div()`用于进行两个整数的除法运算,如果除数为0,则会抛出`invalid_argument`异常。在`Func()`函数中,首先通过`new`关键字分别创建了两个`int`类型的指针变量`p1`和`p2`,然后调用了`div()`函数进行除法运算,并打印结果。最后,通过`delete`关键字释放了指针变量所指向的内存空间。 现在我们来分析一下这段程序中可能存在的内存方面的问题: 1. 如果在创建`p1`的语句后发生异常,那么`p1`指向的内存空间将无法被正确释放,造成内存泄漏。 2. 如果在创建`p2`的语句后发生异常,同样会导致`p2`指向的内存空间无法被正确释放,造成内存泄漏。 3. 如果在调用`div()`函数时发生异常,那么由于`p1`和`p2`指向的内存空间没有被正确释放,同样会造成内存泄漏。 为了解决这些内存问题,可以使用智能指针来管理动态分配的内存。智能指针可以自动管理内存的释放,避免内存泄漏的问题。在C++中,可以使用`std::shared_ptr`来管理动态分配的内存。它会在没有引用指向这块内存时自动释放它。 现在我们来回答相关问题: 相关问题: 1. 如果在创建`p1`的语句后发生异常,该如何处理? 在创建`p1`的语句后,可以将其包装在一个`std::shared_ptr`中,这样即使发生异常,智能指针也会自动释放其所指向的内存空间。 2. 如果在创建`p2`的语句后发生异常,该如何处理? 同样地,可以将`p2`包装在一个`std::shared_ptr`中,这样即使发生异常,智能指针也会自动释放其所指向的内存空间。 3. 如果在调用`div()`函数时发生异常,该如何处理? 可以使用`std::shared_ptr`来管理`p1`和`p2`,这样即使在调用`div()`函数时发生异常,智能指针也会自动释放`p1`和`p2`所指向的内存空间。 4. 如果多个线程同时进行拷贝操作,会出现什么问题? 在引用中,如果多个线程同时进行拷贝操作,可能会出现数据不一致的问题。这是因为在多线程环境下,共享指针的引用计数可能会出现竞争条件,导致引用计数不正确,从而无法正确地管理内存。 5. 如何使用智能指针来管理使用`new[]`创建的对象? 在引用中,如果使用`new[]`创建了对象,可以使用`std::shared_ptr`来管理这些对象。需要注意的是,`std::shared_ptr`默认使用`delete`来释放内存,而不是`delete[]`。所以,需要自定义删除器来使用`delete[]`来释放数组对象。 以上是关于C++智能指针的面试题的回答。如果您还有其他

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值