02 | C++ 自己动手实现智能指针

2022/5/27 1:21:09

本文主要是介绍02 | C++ 自己动手实现智能指针,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

0. 前言

RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 是 C++ 特有的资源管理方式,依托栈和析构函数对所有资源(包括堆)进行管理。实际上就是利用一个类来实现一个来管理资源,将资源和类对象的生命周期进行绑定,这样就可以不用再手动释放资源。

1. auto_ptr

先看下上一节给出的类:

enum class shape_type
{
    circle,
    triangle,
    rectangle
};

class shape{};

class circle : public shape{};

class triangle : public shape{};

class rectangle : public shape{};

shape *create_shape(shape_type type)
{
    switch (type)
    {
    case shape_type::circle:
        return new circle();
    case shape_type::rectangle:
        return new rectangle();
    case shape_type::triangle:
        return new triangle();
    default:
        return nullptr;
    }
}

/**
 * 可以完成智能指针的最基本的功能:利用 RAII 机制,对超出作用域的对象自动进行释放。但缺了:
 * 1. 只适用于 shape 类
 * 2. 行为不像指针
 * 3. 拷贝该类对象会引发程序异常
 */
class shape_wrapper
{
public:
    explicit shape_wrapper(shape *ptr = nullptr) : ptr(ptr) {}

    ~shape_wrapper() { delete ptr; }

    shape *get() const { return ptr; }

private:
    shape *ptr;
};
  • 这个类只适用于 shape 类(模板)
  • 没有指针行为 (相应运算符重载)
  • 拷贝该类对象会引发异常(拷贝构造、赋值运算符)

下面依次弥补上述三个问题:

template <typename T>
class auto_ptr{
public:
    // 构造函数
    explicit auto_ptr(T* ptr = nullptr):ptr_(ptr){}
    
    // 析构函数
    ~auto_ptr() { delete ptr_; }

    T* get() const { return ptr_; }

    // 重载运算符 *
    T& operator*() const { return *ptr_; }
    // 重载运算符 ->
    T* operator->() const { return ptr_; }
    // 重载 bool
    operator bool() const { return ptr_; }
private:
    T* ptr_;
};

对于拷贝构造函数和赋值,关键问题是如何定义其行为。假设有下面代码:

// C++11的语法,对象初始化可以统一用大括号
auto_ptr<shape> ptr1{ create_shape(shape_type::circle) };
auto_ptr<shape> ptr2{ptr1};

对于第二行,当前没有定义拷贝构造函数,会在编译时报错。
最简单的情况就是禁止拷贝和赋值,即

auto_ptr( const auto_ptr& ) = delete;
auto_ptr& operator=(const auto_ptr&) = delete;

实际上这会解决一类错误:由于拷贝构造函数浅拷贝两个对象的指针指向的是同一内存,在析构时会对这片内存释放两次,从而导致错误。

我们可以考虑在拷贝时把对象拷贝一份,也就是深拷贝,但智能指针的目的就是要减少对象的拷贝。

可以尝试在拷贝时转移指针的所有权,大致如下:

template <typename T>
class auto_ptr{
public:
    // 构造函数
    explicit auto_ptr(T* ptr = nullptr):ptr_(ptr){}
    
    // 析构函数
    ~auto_ptr() { delete ptr_; }

    T* get() const { return ptr_; }

    // 重载运算符 *
    T& operator*() const { return *ptr_; }
    // 重载运算符 ->
    T* operator->() const { return ptr_; }
    // 重载 bool
    operator bool() const { return ptr_; }

    // 拷贝构造函数
    // 通过 other 对象调用 release 方法来释放 other 对象对指针的所有权,同时将所有权赋给新构造的对象。这类指针的设计就是只允许一个智能指针拥有资源
    auto_ptr(auto_ptr& other){ 
        _ptr = other.release(); 
    }

    // 赋值分为拷贝构造和交换两步,一场只可能发生在第一步。如果第一步拷贝发生了异常,this 没有参与运算不会受到影响,无论拷贝构造成功与否,结果只有赋值成功和没有效果两种状态,不会发生因为赋值破坏当前对象
    auto_ptr& operator=(auto_ptr &rhs)
    {
        // 先把 rhs 维护的指针交给 auto_ptr 临时对象 auto_ptr(rhs),然后将临时对象与 this 对象交换,临时对象拿到 this 之前维护的指针,它会随着临时对象的销毁而被 delete,而 rhs 指针在拷贝的过程中也失去了原本资源(release)。
        auto_ptr(rhs).swap(*this);
        return *this; 
    }

    T* release()
    {
        T* ptr = _ptr;
        _ptr = nullptr;
        return ptr;
    }

    void swap(auto_ptr& rhs)
    {
        std::swap(_ptr, rhs._ptr);
    }
private:
    T* _ptr;
};

赋值函数中还有一个类似于 if ( this!= &rhs )的判断,这种判断异常安全性不够好,如果赋值过程中发生异常,this 对象的内容可能已经被破坏了。

上述代码本质上是 C++98 的 auto_ptr 的定义,auto_ptr 在 C++17 时已经被正式从 C++ 标准里删除了。
上面实现最大的问题是,它的行为会让程序员很容易犯错,一不小心把资源指针传递给了另外一个智能指针,原本指针就不再拥有这个对象了。

此外,上述代码是不支持容器操作的。
在 C++03 标准下,有如下一个 demo:

int main()
{
    std::auto_ptr<int> iptr(new int(1));
    std::vector<std::auto_ptr<int>> integer_vec;
    
    integer_vec.push_back(iptr);
    return 0;
}

由于在C++03标准中还没有引入移动语义,只能以push_back函数向vector中添加元素。

实际上上述代码无法编译通过,会报如下错误信息:

/c++/9.0/ext/new_allocator.h:146:9: error: no matching function for call to ‘std::auto_ptr::auto_ptr(const std::auto_ptr&)’
{ ::new((void *)__p) _Tp(__val); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

先给出错误原因:由于类std::auto_ptr 没有提供const std::auto_ptr&类型的复制构造函数。

我们查看之前自己写的 auto_ptr 发现拷贝构造函数参数不是 const 修饰的,而实际上 std::auto_ptr 的设计者想使 auto_ptr 的复制构造函数具备移动构造函数的属性(如果不懂右值、移动等内容,可以看看右值引用的正确用法),这就是使得 auto_ptr 复制构造函数参数不能由 const 修饰,否则参数指向的资源无法移动到新创建的对象中。
std::auto_ptr 的核心代码如下:

template <typename _Tp>
class auto_ptr
{
private:
  _Tp *_M_ptr;
public:
  typedef _Tp element_type;

  explicit auto_ptr(element_type *__p = 0) throw() : _M_ptr(__p) { }

  auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) {} 

  template <typename _Tp1>
  auto_ptr(auto_ptr<_Tp1>& __a) throw() : _M_ptr(__a.release()) {}

  auto_ptr &operator=(auto_ptr& __a) throw() {
    reset(__a.release());
    return *this;
  }

  ~auto_ptr() { delete _M_ptr; }

  element_type* release() throw() {
    element_type *__tmp = _M_ptr;
    _M_ptr = 0;
    return __tmp;
  }

  void reset(element_type *__p = 0) throw() {
    if (__p != _M_ptr) {
      delete _M_ptr;
      _M_ptr = __p;
    }
  }
  //...
};

而在 push_back 函数中,输入参数 __x 是const std::auto_ptr&类型,能接受iptr:

template<typename _Tp, typename _Alloc> void std::vector<_Tp, _Alloc>::push_back(const value_type& __x);

在 push_back 函数内部会调用 _Alloc_traits::construct 函数来构造一个新的 std::auto_ptr 对象 obj,然后将这个 obj 放到 integer_vec 中

_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
  • 因为要构造obj,那么必要会调用std::auto_ptr的复制构造函数,且输入参数是__x;
  • 但由于 __x 是 const std::auto_ptr& 类型,而 std::auto_ptr 的复制构造函数输入类型是 std::auto_ptr&,接受不了 __x 作为输入,因此会导致 construct 函数执行失败。出现上述的错误。

2. unique_ptr

C++11 引入移动语义,提出了 std::unique_ptr,才真正地完成了 std::auto_ptr 的设计意图,而原本的 std::auto_ptr 也被标记为deprecated。

由于 std::unique_ptr 对象管理的资源,不可共享,只能在 std::unique_ptr 对象之间转移,因此类 std::unique_ptr 就禁止了复制构造函数、赋值表达式,仅实现了移动构造函数等。

对于上节自己实现的 auto_ptr,仅需要做点小修改就能实现 unique_ptr 基本功能:

template <typename T>
class unique_ptr{
  …
  template <typename U>
  unique_ptr(unique_ptr<U>&& other)
  {
    ptr_ = other.release();
  }

  unique_ptr& operator=(unique_ptr rhs)
  {
    rhs.swap(*this);
    return *this;
  }
  …
};
  • 把拷贝构造函数中的参数类型 unique_ptr& 改成了 unique_ptr&&,现在它成了移动构造函数。
  • 把赋值函数中的参数类型 unique_ptr& 改成了 unique_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。
  • 现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。

根据 C++ 的规则,如果提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用(但建议手动禁用), 于是可以得到以下结果:

unique_ptr<shape> ptr1{create_shape(shape_type::circle)};
unique_ptr<shape> ptr2{ptr1};             // 编译出错
unique_ptr<shape> ptr3;
ptr3 = ptr1;                              // 编译出错
ptr3 = std::move(ptr1);                   // OK
unique_ptr<shape> ptr4{std::move(ptr3)};  // OK

同时多态中,一个 circle* 是可以隐式转换成 shape* 的,为了使 unique_ptr 自动转换成 unique_ptr,移动构造函数中增加了模板代码。

需要注意的是,上面增加模板的构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 。更通用的方式仍然是同时定义标准的 拷贝/移动构造函数和所需的模板构造函数。

关于 unique_ptr 更进一步的介绍,可以看这篇文章 从auto_ptr到unique_ptr,是C++的成长

3. shared_ptr

unique_ptr 是一种较为安全的智能指针。但一个对象只能被单个 unique_ptr 所拥有,如果是多个智能指针同时拥有一个对象,当它们全部都失效时,这个对象也同时会被删除,需要用到 shared_ptr。

unique_ptr 与 shared_ptr 主要区别如下:

多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个计数。当最后一个指向对象(和共享计数)的 shared_ptr 析构时,需要删除对象和共享计数。

// 共享计数的接口
class shared_count {
public:
  shared_count();
  void add_count();         // 增加计数
  long reduce_count();      // 减少计数,返回值供调用者判断是否是最后一个指向共享计数的 shared_ptr
  long get_count() const; 
};

真正多线程安全的版本需要用到其他知识,目前先实现一个简单化的版本:

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 <typename T>
class shared_ptr{
public:
    explicit shared_ptr(T* ptr = nullptr) : ptr_(ptr)
    {
        if (ptr)
            shared_count_ = new shared_count();
    }
    
    ~shared_ptr()
    {
        if (ptr_ && !shared_count_->reduce_count()) {
            delete ptr_;
            delete shared_count_;
        }
    }

    shared_ptr(const shared_ptr& other)
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            other.shared_count_->add_count();
            shared_count_ = other.shared_count_;
        }
    }
  
    template <typename U>
    shared_ptr(const shared_ptr<U>& other)
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            other.shared_count_->add_count();
            shared_count_ =other.shared_count_;
        }
    }
    
    template <typename U>
    shared_ptr(shared_ptr<U>&& other)
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            shared_count_ =other.shared_count_;
            other.ptr_ = nullptr;
        }
    }

    void swap(shared_ptr& rhs)
    {
        std::swap(ptr_, rhs.ptr_);
        std::swap(shared_count_, rhs.shared_count_);
    }

private:
    T* ptr_;
    shared_count* shared_count_;
};
  • 构造函数中会构造一个 shared_count 用于计数。
  • 析构函数在看到 ptr_ 非空时,需要对引用数减一,并在引用数降到零时彻底删除对象和共享计数。
  • 对于拷贝构造函数,需要将引用计数加一,对于移动构造函数,不需要调整引用计数,但要将 other.ptr_ 置空。

上述代码会报如下错误:

fatal error: ‘ptr_’ is a private member of ‘shared_ptr’

错误原因是模板的各个实例间并不天然就有 friend 关系,因此不能互访私有成员 ptr_ 和 shared_count_。需要显式声明友元模板:

template <typename T>
friend class shared_ptr;

此外对于 unique_ptr/auto_ptr 中用 release 函数来手工释放所有权,在 shared_ptr 中不适用了,应当添加一个返回引用计数的函数:

long use_count()
{
    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()
{
    shared_ptr<circle> ptr1(new circle());
    printf("use count of ptr1 is %ld\n", ptr1.use_count());

    shared_ptr<shape> 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()

指针类型转换

对应于 C++ 里的不同的类型强制转换:

  • static_cast
  • reinterpret_cast
  • const_cast
  • dynamic_cast

智能指针需要实现类似的函数模板。实现本身并不复杂,但为了实现这些转换,需要添加构造函数,使在对智能指针内部的指针对象赋值时,使用一个现有的智能指针的共享计数。如下所示:

template <typename U>
smart_ptr(const smart_ptr<U>& other, T* ptr)
{
    ptr_ = ptr;
    if (ptr_) {
        other.shared_count_->add_count();
        shared_count_ = other.shared_count_;
    }
}

实现一个 dynamic_pointer_cast :

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

完整的 shared_ptr 代码:

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 <typename T>
class shared_ptr {
public:
    template <typename U>
    friend class shared_ptr;

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

    shared_ptr(const shared_ptr& other)
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            other.shared_count_->add_count();
            shared_count_ = other.shared_count_;
        }
    }
  
    template <typename U>
    shared_ptr(const shared_ptr<U>& other) noexcept
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            other.shared_count_->add_count();
            shared_count_ = other.shared_count_;
        }
    }
    
    template <typename U>
    shared_ptr(shared_ptr<U>&& other) noexcept
    {
        ptr_ = other.ptr_;
        if (ptr_) {
            shared_count_ = other.shared_count_;
            other.ptr_ = nullptr;
        }
    }
  
    template <typename U>
    shared_ptr(const shared_ptr<U>& other, T* ptr) noexcept
    {
        ptr_ = ptr;
        if (ptr_) {
            other.shared_count_->add_count();
            shared_count_ = other.shared_count_;
        }
    }
  
    shared_ptr& operator=(shared_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(shared_ptr& rhs) noexcept
    {
        std::swap(ptr_, rhs.ptr_);
        std::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 <typename T>
void swap(shared_ptr<T>& lhs, shared_ptr<T>& rhs) noexcept
{
    lhs.swap(rhs);
}

template <typename T, typename U>
shared_ptr<T> static_pointer_cast( const shared_ptr<U>& other) noexcept
{
    T* ptr = static_cast<T*>(other.get());
    return shared_ptr<T>(other, ptr);
}

template <typename T, typename U>
shared_ptr<T> reinterpret_pointer_cast( const shared_ptr<U>& other) noexcept
{
    T* ptr = reinterpret_cast<T*>(other.get());
    return shared_ptr<T>(other, ptr);
}

template <typename T, typename U>
shared_ptr<T> const_pointer_cast( const shared_ptr<U>& other) noexcept
{
    T* ptr = const_cast<T*>(other.get());
    return shared_ptr<T>(other, ptr);
}

template <typename T, typename U>
shared_ptr<T> dynamic_pointer_cast( const shared_ptr<U>& other) noexcept
{
    T* ptr = dynamic_cast<T*>(other.get());
    return shared_ptr<T>(other, ptr);
}

参考文献

  • 现代 C++ 实战30讲
  • 智能指针auto_ptr、unique_ptr、shared_ptr区别
  • 从auto_ptr到unique_ptr,是C++的成长


这篇关于02 | C++ 自己动手实现智能指针的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程