自己动手实现一个智能指针

自己动手实现一个智能指针,第1张

一、实现一个基础的智能指针类

首先,我们知道智能指针最基本的功能就是对超出作用域的对象进行释放。

所以,我们写出了下面这段代码,


class shape_wrapper {
public:
  explicit shape_wrapper(
    shape* ptr = nullptr)
    : ptr_(ptr) {}
  ~shape_wrapper()
  {
    delete ptr_;
  }
  shape* get() const { return ptr_; }

private:
  shape* ptr_;
};

看起来我们似乎成功了,但却远远达不到大家对智能指针的要求,它还缺了一些东西,

1.这个类只适用于 shape 类

2.该类对象的行为不够像指针

3.拷贝该类对象会引发程序行为异常

因为delete *** 作在析构函数中 所以如果被拷贝,那么任意一个warpper被释放都会导致底层的数据被删除 其他warpper中的ptr_ 就会变成一个野指针。

简单来说,就是拷贝之后会把这个指针的地址拷贝给别的对象,而当我们将其delete之后释放了地址空间,拷贝后的对象也不能用了。

下面我们来看看如何解决这个问题:

二、模板化和易用性

要让这个类能够包装任意类型的指针,我们需要把它变成一个类模板。

这实际上相当容易:


template 
class smart_ptr {
public:
  explicit smart_ptr(T* ptr = nullptr)
    : ptr_(ptr) {}
  ~smart_ptr()
  {
    delete ptr_;
  }
  T* get() const { return ptr_; }
private:
  T* ptr_;
};

和 shape_wrapper 比较一下,我们就是在开头增加模板声明 template ,然后把代码中的 shape 替换成模板参数 T 而已。

模板本质上并不是一个很复杂的概念。

这个模板使用也很简单,把原来的 shape_wrapper 改成 smart_ptr 就行。

目前这个 smart_ptr 的行为还是和指针还是有点差异的:

1.它不能用 * 运算符解引用

2.它不能用 -> 运算符指向对象成员

3.它不能像指针一样用在布尔表达式里

不过,这些问题也相当容易解决,加几个成员函数就可以:


template 
class smart_ptr {
public:
  …
  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  operator bool() const { return ptr_; }
}

关于bool表达式的重载用法 operator bool () 提供一个本类型到bool的隐式转换,不允许使用参数。

bool operator ==()可以分为bool operator ==( const bool& other),bool operator ==( const T& other),T代表类型。

三、拷贝构造和赋值

对于下面的代码,


smart_ptr ptr1{create_shape(shape_type::circle)};
smart_ptr ptr2{ptr1};

对于第二行,究竟应当让编译时发生错误,还是可以有一个更合理的行为?我们来逐一检查一下各种可能性。

最简单的情况显然是禁止拷贝。

我们可以使用下面的代码:


template 
class smart_ptr {
  …
  smart_ptr(const smart_ptr&)
    = delete;
  smart_ptr& operator=(const smart_ptr&)
    = delete;
  …
};

一般不想使用某函数时,可以将其设为private或者在函数声明后接=delete

这样我们就解决了一种可能出错的情况,否则,smart_ptr ptr2{ptr1};在编译时不会出错,但在运行时却会有未定义行为——由于会对同一内存释放两次,通常情况下会导致程序崩溃。

因为如果没有编写深拷贝函数,将会出现一份内存,被析构两次的问题。

当然只要编写深拷贝函数就可以了,但默认的拷贝函数一般是浅拷贝,我们不能强行让人家使用深拷贝,考虑问题要从一般性入手。

那我们是不是可以考虑在拷贝智能指针时把对象拷贝一份?不行,通常人们不会这么用,因为使用智能指针的目的就是要减少对象的拷贝,要不然我们还写什么智能指针呢。

何况,虽然我们的指针类型是 shape,但实际指向的却可能是 circle 或 triangle 之类的对象。

在 C++ 里没有像 Java 的 clone 方法这样的约定;一般而言,并没有通用的方法可以通过基类的指针来构造出一个子类的对象来。

在这里吴咏炜老师提供了一种新的思路,在拷贝时转移指针的所有权。

大致实现如下:


template 
class smart_ptr {
  …
  smart_ptr(smart_ptr& other)
  {
    ptr_ = other.release();
  }
  smart_ptr& operator=(smart_ptr& rhs)
  {
    smart_ptr(rhs).swap(*this);
    return *this;
  }
  …
  T* release()
  {
    T* ptr = ptr_;
    ptr_ = nullptr;
    return ptr;
  }
  void swap(smart_ptr& rhs)
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
  }
  …
};

ptr_=other.release()other调用release之后,other的指针指向空,并且将other的原来指针控制的对象转移到ptr_中。

拷贝构造让传入的对象失去所有权。

这类智能指针的设计就是只允许一个智能指针拥有资源。

smart_ptr(rhs).swap(*this);这一行不太好理解,首先rhs把自己维护的指针交给给临时对象smart_ptr(rhs),然后这个临时对象维护的指针和this对象维护的指针交换一下,this对象就拿到rhs维护的指针了,临时对象smart_ptr拿到this之前维护的指针,它会随着临时对象smart_ptr销毁而被delete。

上面的代码的意思是:

(1)当出现异常的时候,因为this参与了运算,所以不能够保证异常过程中this没有被修改,不能够保证this的完整性。

(2)上面的处理方法,先生成一个临时变量,在生成临时变量的时候出现异常,也是不会影响到原来赋值符号左边对象的。

上面代码里的这种惯用法保证了强异常安全性,赋值分为拷贝构造和交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。

无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。

上面实现的最大问题是,它的行为会让程序员非常容易犯错。

一不小心把它传递给另外一个 smart_ptr,你就不再拥有这个对象了……

而且这个 smart_ptr 的 other 必须保证后面再也不会用到了,不然 被other所引用的对象一旦成功被传递,所指的内存就变为 nullptr,赋值函数也是会调用这个copy构造的。

四、“移动”指针

我们先简单看一下 smart_ptr 可以如何使用“移动”来改善其行为。

我们需要对代码做两处小修改:


template 
class smart_ptr {
  …
  smart_ptr(smart_ptr&& other)
  {
    ptr_ = other.release();
  }
  smart_ptr& operator=(smart_ptr rhs)
  {
    rhs.swap(*this);
    return *this;
  }
  …
};

这里修改了两个地方:

1.把拷贝构造函数中的参数类型 smart_ptr& 改成了 smart_ptr&&;现在它成了移动构造函数。

我们输入一个即将销毁的右值,这样转移指针所有权完成移动

2.把赋值函数中的参数类型 smart_ptr& 改成了 smart_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。

现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。

operator=()的参数在接收参数的时候,会调用构造函数,如果调用的是拷贝构造,那赋值 *** 作就是拷贝,如果调用的是移动构造,那么赋值 *** 作就是移动。

根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用。

于是,我们自然地得到了以下结果:


smart_ptr ptr1{create_shape(shape_type::circle)};
smart_ptr ptr2{ptr1};             // 编译出错
smart_ptr ptr3;
ptr3 = ptr1;                             // 编译出错
ptr3 = std::move(ptr1);                  // OK,可以
smart_ptr ptr4{std::move(ptr3)};  // OK,可以

这也是 C++11 的 unique_ptr 的基本行为。

五、子类指针向基类指针的转换

其实,一个 circle* 是可以隐式转换成 shape* 的,但上面的 smart_ptr 却无法自动转换成 smart_ptr。

在我们目前给出的实现里,只需要增加一个构造函数即可——这也算是我们让赋值函数利用构造函数的好处了。

因为赋值函数的入参是利用构造函数构造出来的,构造结果是一个同类型的smart_ptr,所以才能利用同一个赋值函数实现。


  template 
  smart_ptr(smart_ptr&& other)
  {
    ptr_ = other.release();
  }

这样,我们自然而然利用了指针的转换特性:现在 smart_ptr 可以移动给 smart_ptr,但不能移动给 smart_ptr。

不正确的转换会在代码编译时直接报错。

需要注意,上面这个构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。

如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 了(见“拷贝构造和赋值”一节)。

不过,更通用的方式仍然是同时定义标准的拷贝 / 移动构造函数和所需的模板构造函数。

下面的引用计数智能指针里我们就需要这么做。

如果提供了移动构造函数而没有同步提供拷贝构造函数,则后者被默认删除。

但是上面这个例子不算是提供了移动构造函数,所以需要手动标明删除拷贝构造函数。

六、引用计数

unique_ptr 算是一种较为安全的智能指针了。

因为unique_ptr一个对象只能被一个只能指针拥有,所以它拥有以下优点:

1.不会产生内存泄漏

2.不会出现重复删除内存的情况

3.不会出现源对象在发生异常时候损坏的情况

但是,一个对象只能被单个 unique_ptr 所拥有,这显然不能满足所有使用场合的需求。

一种常见的情况是,多个智能指针同时拥有一个对象;当它们全部都失效时,这个对象也同时会被删除。

这也就是 shared_ptr 了。

unique_ptr 和 shared_ptr 的主要区别如下图所示:

 多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个计数。

当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。

我们下面就来实现一下。

我们先来写出共享计数的接口:


class shared_count {
public:
  shared_count();
  void add_count();
  long reduce_count();
  long get_count() const;
};

这个 shared_count 类除构造函数之外有三个方法:一个增加计数,一个减少计数,一个获取计数。

注意上面的接口增加计数不需要返回计数值;但减少计数时需要返回计数值,以供调用者判断是否它已经是最后一个指向共享计数的 shared_ptr 了。

我们先来实现一个简单化的版本:


class shared_count {
public:
  shared_count() : count_(1) {}
  void add_count()
  {
    ++count_;
  }
  long reduce_count()
  {
    return --count_;
  }
  long get_count() const
  {
    return count_;
  }

private:
  long count_;
};

现在我们可以实现我们的引用计数智能指针了。

首先是构造函数、析构函数和私有成员变量:


template 
class smart_ptr {
public:
  explicit smart_ptr(T* ptr = nullptr)
    : ptr_(ptr)
  {
    if (ptr) {
      shared_count_ =
        new shared_count();
    }
  }
  ~smart_ptr()
  {
    if (ptr_ &&
      !shared_count_
         ->reduce_count()) {
      delete ptr_;
      delete shared_count_;
    }
  }

private:
  T* ptr_;
  shared_count* shared_count_;
};

构造函数跟之前的主要不同点是会构造一个 shared_count 出来。

析构函数在看到 ptr_ 非空时(此时根据代码逻辑,shared_count 也必然非空),需要对引用数减一,并在引用数降到零时彻底删除对象和共享计数。

由于因为复制运算符是利用拷贝构造和swap实现的,所以我们需要一个新的 swap 成员函数,并更新一下拷贝构造和移动构造函数。

除复制指针之外,对于拷贝构造的情况,我们需要在指针非空时把引用数加一,并复制共享计数的指针。

对于移动构造的情况,我们不需要调整引用数,直接把 other.ptr_ 置为空,认为 other 不再指向该共享对象即可。

由于模板是用来实例化出具体的类,实例化出来的类就像我们平时写的不具有模板参数的类一样,彼此之间是不同,这与类的派生是不同的,因此不具有天然的友元关系。

我们需要在 smart_ptr 的定义中显式声明:


  template 
  friend class smart_ptr;

此外,我们之前的实现(类似于单一所有权的 unique_ptr )中用 release 来手工释放所有权。

在目前的引用计数实现中,它就不太合适了,应当删除。

但我们要加一个对调试非常有用的函数,返回引用计数值。

定义如下:


  long use_count() const
  {
    if (ptr_) {
      return shared_count_
        ->get_count();
    } else {
      return 0;
    }
  }

现在,我们已经完成了一个比较完整的引用计数智能指针的实现。

我们可以用下面的代码来验证一下它的功能正常:


class shape {
public:
  virtual ~shape() {}
};

class circle : public shape {
public:
  ~circle() { puts("~circle()"); }
};

int main()
{
  smart_ptr ptr1(new circle());
  printf("use count of ptr1 is %ld\n",
         ptr1.use_count());
  smart_ptr ptr2;
  printf("use count of ptr2 was %ld\n",
         ptr2.use_count());
  ptr2 = ptr1;
  printf("use count of ptr2 is now %ld\n",
         ptr2.use_count());
  if (ptr1) {
    puts("ptr1 is not empty");
  }
}

这段代码的运行结果是:

use count of ptr1 is 1
use count of ptr2 was 0
use count of ptr2 is now 2
ptr1 is not empty~circle()

上面我们可以看到引用计数的变化,以及最后对象被成功删除

七、代码列表

下面我给出了一个完整的 smart_ptr 代码列表:


#include   // std::swap

class shared_count {
public:
  shared_count() noexcept
    : count_(1) {}
  void add_count() noexcept
  {
    ++count_;
  }
  long reduce_count() noexcept
  {
    return --count_;
  }
  long get_count() const noexcept
  {
    return count_;
  }

private:
  long count_;
};

template 
class smart_ptr {
public:
  template 
  friend class smart_ptr;

  explicit smart_ptr(T* ptr = nullptr)
    : ptr_(ptr)
  {
    if (ptr) {
      shared_count_ =
        new shared_count();
    }
  }
  ~smart_ptr()
  {
    if (ptr_ &&
      !shared_count_
         ->reduce_count()) {
      delete ptr_;
      delete shared_count_;
    }
  }

  smart_ptr(const smart_ptr& other)
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_
        ->add_count();
      shared_count_ =
        other.shared_count_;
    }
  }
  template 
  smart_ptr(const smart_ptr& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template 
  smart_ptr(smart_ptr&& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      shared_count_ =
        other.shared_count_;
      other.ptr_ = nullptr;
    }
  }
  template 
  smart_ptr(const smart_ptr& other,
            T* ptr) noexcept
  {
    ptr_ = ptr;
    if (ptr_) {
      other.shared_count_
        ->add_count();
      shared_count_ =
        other.shared_count_;
    }
  }
  smart_ptr&
  operator=(smart_ptr rhs) noexcept
  {
    rhs.swap(*this);
    return *this;
  }

  T* get() const noexcept
  {
    return ptr_;
  }
  long use_count() const noexcept
  {
    if (ptr_) {
      return shared_count_
        ->get_count();
    } else {
      return 0;
    }
  }
  void swap(smart_ptr& rhs) noexcept
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
    swap(shared_count_,
         rhs.shared_count_);
  }

  T& operator*() const noexcept
  {
    return *ptr_;
  }
  T* operator->() const noexcept
  {
    return ptr_;
  }
  operator bool() const noexcept
  {
    return ptr_;
  }

private:
  T* ptr_;
  shared_count* shared_count_;
};

template 
void swap(smart_ptr& lhs,
          smart_ptr& rhs) noexcept
{
  lhs.swap(rhs);
}

template 
smart_ptr static_pointer_cast(
  const smart_ptr& other) noexcept
{
  T* ptr = static_cast(other.get());
  return smart_ptr(other, ptr);
}

template 
smart_ptr reinterpret_pointer_cast(
  const smart_ptr& other) noexcept
{
  T* ptr = reinterpret_cast(other.get());
  return smart_ptr(other, ptr);
}

template 
smart_ptr const_pointer_cast(
  const smart_ptr& other) noexcept
{
  T* ptr = const_cast(other.get());
  return smart_ptr(other, ptr);
}

template 
smart_ptr dynamic_pointer_cast(
  const smart_ptr& other) noexcept
{
  T* ptr = dynamic_cast(other.get());
  return smart_ptr(other, ptr);
}

这就是我们实现的简单的智能指针的,相信你学习了这篇文章,一定会对智能指针的底层有了更深刻的理解。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/674618.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)