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
需要注意的是,上面增加模板的构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = 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);
}
参考文献
标签:02,count,const,auto,C++,other,shared,ptr,指针 来源: https://www.cnblogs.com/cscshi/p/16271702.html