C++智能指针的实现原理(上)

智能指针的背景

在C98里标准库提供一个std::auto_ptr的实现,以应对C++需要程序员自己管理内存资源广泛存在的问题,诸如野指针,内存泄漏,内存重复释放等令人困扰的问题。

对于智能指针基本的几个需求

  • 自动析构。 这是最核心的特征,紧随其后的 unique_ptr, share_ptr 这些进阶版的指针封装类型无不立足于此
  • 具有基本指针类型的行为特征,引用和解引用,运算符 *, ->的支持
// your auto ptr
auto_ptr p(type);
(*p).func(); 
p->func(); //

对于第一个特征,基本的实现:

template 
class MyAutoPtr
{
public:
    MyAutoPtr(T *ptr): _ptr(ptr) {
    }
    ~MyAutoPtr() {
        delete _ptr;
    }
    T *_ptr;
};

Use:

MyAutoPtr p(new int(5)); //使用动态申请的内存可以做到自动释放,因为p是一个栈变量,其析构函数会自动调用
std::cout << *p._ptr << std::endl; 
// 但是我们要操作p的数据,不得不暴露原始的数据 _ptr, 
// 以上的设计聊胜于无,我们一旦不小心 delete p._ptr就会发生灾难,析构函数会犯重复释放内存的错误。

使用动态申请的内存可以做到自动释放,因为p是一个栈变量,其析构函数会自动调用, 但是我们要操作p的数据,不得不暴露原始的数据 _ptr

为了让智能指针的使用变得可行,我们必须隐藏 _ptr 成员变量,并且提供接口。同时利用运算符重载使得智能指针的使用变得和基本的指针行为看起来是一致的。

因此需要重载 ->和 * 运算符

第二版实现

 template 
class MyAutoPtr
{
public:
    MyAutoPtr(T *ptr): _ptr(ptr) {
    }
    ~MyAutoPtr() {
        delete _ptr;
    }

    T operator *() {
        return *get();
    }

    T * operator -> (){
        return get();
    }

    T * get() {
        return _ptr;
    }

private:
    T *_ptr;
};

除此以外,我们还需要考虑指针的复制,赋值这些行为的合理性。

  • 根据已有的智能指针复制
  • 赋值操作的行为
    为此我们为智能指针类再增加两个构造函数
MyAutoPtr(MyAutoPtr &other) ;
MyAutoPtr& operator = (MyAutoPtr & right) ;

智能指针将会出现下面这些用法

MyAuotPtr p(new Data(100, 200));
p = new Data(10,20);  // (1)
MyAuotPtr q(p); // (2)

第 (1) 行中,p丢弃了原来指向的内存,指向一块新的内存,这时候,必须将原指针指向的内存清理;
第二行根据 p复制一个q出来,两个指针指向同一片内存,如果不加处理,析构时必然会导致重复释放内存的问题。


两种指针复制的模型图.png

为了避免这两种情况,复制构造函数和赋值重载函数需要小心实现。

 MyAutoPtr(MyAutoPtr &other) {
        T* tmp_p = _ptr;
        _ptr = nullptr;
        tmp_p = right.get(); // 交出控制权
        return *this;
    }

    MyAutoPtr& operator = (MyAutoPtr & right) {
         if (right->get() != _ptr)
              delete _ptr;
         _ptr = right.get();
    }

在std标准库中的实现,对于上面两种情形都抽象了两个 过程:
release当前对象将内存的控制权交出,避免内存重复释放,
reset则是重置指针的指向,对于原来的内存块则会删掉。
这里的实现,其实就是C++11移动语义的雏形。隐含了一种资源转移的概念。

T* release();  
void reset(T * _Ptr = 0);

智能指针的缺陷

了解了auto_ptr的实现,它的缺陷也是很明显了

  • 首先,智能指针被复制或用来作为副本创建另一个智能指针时,会存在潜在的风险。
    std::auto_ptr p(new Data(100, 200));
    std::auto_ptr q = p;
    std::cout << p->a << std::endl; //错误,p不能再使用了, p._ptr 被置0了
    std::cout << q->b << std::endl;  
  • 因为auto_ptr清理内存使用操作符 delete(也只能如此,不能推导模板类型),对于一些需要 delete [] 的指针是无能为力的。它不能用来装载数组指针
  • auto_ptr不能用于标准库的容器元素,为什么?
    这是因为auto_ptr十分特殊的复制行为,标准库的容器元素复制一般是一个深拷贝,它们通常有自己的析构函数,auto_ptr发生复制的时候,会毁掉另一个。

由于以上的缺陷,C++11已经明确禁止使用 auto_ptr 来管理指针,取而代之的是
unique_ptr, shared_ptr

C++11在智能指针方面做了哪些改进

auto_ptr的缺陷主要体现在赋值和拷贝构造资源转移带来的缺陷,C++11针对这两个问题,分别祭出了两个进化版的智能指针 unique_ptr, shared_ptr 。从功能上来说几乎和auto_ptr没有太多区别,它们都能自动释放堆内存,可以像基础指针一样使用。区别在于

  • unique_ptr 如其名,它禁用了拷贝和赋值函数,程序员需要明确这个 unique_ptr 指针自诞生就不可以和其它指针分享所指对象的内存
unique_ptr(const _Myt&) = delete;
_Myt& operator=(const _Myt&) = delete;

unique_ptr实现中禁用了拷贝和赋值构造函数。

std::unique q(new int(10);
std::unique p = q; // 错误,无法复制
std::unique p(q); //错误 

但是 unique_ptr 仍然可以通过别的方式 转移它对内存资源的所有权,使用 std::move函数,这里和提供赋值拷贝构造函数返回一个左值有一些微妙的差别。

  • shared_ptr 允许多个指针共同拥有同一块内存。实现上,它和auto_ptr有些不一样。shared_ptr通过引用计数的技术实现,对象复制时,仅仅是将引用计数加1,发生内存拷贝时将引用减少。
    析构函数需要判断并维护引用计数,降到0时将它的内存清理掉。
  • weak_ptr

TODO
// weak_ptr的用法
// 其它智能指针的实现 Qt

你可能感兴趣的:(C++智能指针的实现原理(上))