对象切片一文中,提到可使用充当智能指针的类shape_wrapper,可以简化资源的管理,从根本上消除资源(包括内存)泄漏的可能性,本节来看下如何将shape_wrapper改造成一个完整的智能指针。

先来看下这个类:

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 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<shape>就行

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

  • 它不能用*运算符引用
  • 它不能用->运算符指向对象成员
  • 它不能像指针一样用在布尔表达式里

这也很好解决,加几个成员函数就可以:

template <typename T>
class smart_ptr {
public:

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

拷贝构造和赋值

暂且称拷贝构造和赋值简称为拷贝。

假设有下面的代码:

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

对于第二行,究竟应当让编译时发生错误,还是可以有个更合理的行为呢?

  • 最简单的情况显然就是禁止拷贝
template <typename T>
class smart_ptr {

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

};

禁用这两个函数非常简单,但却解决了一种可能出错的情况。否则,smart_ptr<shape>ptr2{ptr1};在编译时不会出错,但在运行时却会有未定义行为 -- 由于会对同一内存释放两次,通常情况下会导致程序崩溃

  • 那是不是可以考虑在拷贝智能指针时把对象拷贝一份?不行

    因为使用智能指针的目的就是减少对象的拷贝,何况,我们的指针类型是shape,但实际指向的却应该是circletriangle之类的对象。C++里也没有像Java的clone方法的约定。

    一般而言,并没有通用的方法可以通过基类的指针来构造出一个子类的 对象。

  • 要么试试在拷贝时转移智能指针的所有权?

    大致实现如下:

template <typename T>
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_);
}

};

在拷贝构造函数中,通过调用other的release方法来释放它对指针的所有权。在赋值函数中,则通过拷贝构造产生一个临时对象并调用swap来交换对指针的所有权。

如果你学到的赋值函数还有一个类似于 if (this != &rhs) 的判断的话,那种用法更啰嗦,而且异常安全性不够好——如果在赋值过程中发生异常的话,this 对象的内容可能已经被部分破坏了,对象不再处于一个完整的状态。

上面代码的这种惯用法则保证了强异常安全性: 赋值分为拷贝构造交换两步,异常只可能在第一步发生;而第一步如果发生异常的话,this 对象完全不受任何影响。无论拷贝构造成功与否,结果只有赋值成功和赋值没有效果两种状态,而不会发生因为赋值破坏了当前对象这种场景。

上面实现的最大问题在于,它的行为会让程序员非常容易犯错。一不小心把它传递给另一个smart_ptr,就不再拥有这个对象....

"移动"指针?

先来简单看下smart_ptr如何使用"移动"来改善其行为的。

template <typename T>
class smart_ptr {

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

};

这里修改了两个地方:

  • 把拷贝构造函数中的参数类型smart_ptr&改成了smart_ptr&&,现在就是移动构造函数了

  • 在赋值函数中的参数类型smart_ptr&改成了smart_ptr,在构造参数时直接生成新的智能指针,从而不再需要在函数体中构造临时对象。现在赋值函数的行为是移动还是拷贝,完全依赖于构造参数时走的是移动构造还是拷贝构造。

根据 C++ 的规则,如果我提供了移动构造函数而没有手动提供拷贝构造函数,那后者自动被禁用(记住,C++ 里那些复杂的规则也是为方便编程而设立的)。于是,我们自然地得到了以下结果:

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

这就自然多了,这也是C++11的unique_ptr的基本行为

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

一个circle*可以隐式转换成shape*的,但上面的smart_ptr<circle> 却是无法自动转换为smart_ptr<shape>,这个行为显然也是不够"自然"

只需要额外加一点模板代码,就能实现这一行为。在我们目前给出的实现里,只需要增加一个构造函数即可——这也算是我们让赋值函数利用构造函数的好处了

  template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.release();
}

自然而然利用了指针的转换特性:现在 smart_ptr<circle> 可以移动给 smart_ptr<shape>,但不能移动给 smart_ptr<triangle>。不正确的转换会在代码编译时直接报错。

需要注意,上面这个构造函数不被编译器看作移动构造函数,因而不能自动触发删除拷贝构造函数的行为。如果我们想消除代码重复、删除移动构造函数的话,就需要把拷贝构造函数标记成 = delete 了(见“拷贝构造和赋值”一节)。不过,更通用的方式仍然是同时定义标准的拷贝 / 移动构造函数和所需的模板构造函数。下面的引用计数智能指针就需要这么做。

引用计数

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();
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 <typename T>
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 成员函数:

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

赋值函数可以跟前面一样,保持不变,但拷贝构造和移动构造函数是需要更新一下的:

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

除复制指针之外,对于拷贝构造的情况,我们需要在指针非空时把引用数加一,并复制共享计数的指针。对于移动构造的情况,我们不需要调整引用数,直接把other.ptr_置为空,认为 other 不再指向该共享对象即可。

不过,上面的代码有个问题:它不能正确编译。编译器会报错,像:

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

错误原因是模板的各个实例间并不天然就有 friend 关系,因而不能互访私有成员 ptr_ 和 shared_count_。我们需要在 smart_ptr 的定义中显式声明:

  template <typename U>
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<circle> ptr1(new circle());
printf("use count of ptr1 is %ld\n",
ptr1.use_count());
smart_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);
}

在前面的验证代码后面我们可以加上:

  smart_ptr<circle> ptr3 =
dynamic_pointer_cast<circle>(ptr2);
printf("use count of ptr3 is %ld\n",
ptr3.use_count());

编译会正常通过,同时能在输出里看到下面的结果:

use count of ptr3 is 3

最后,对象仍然能够被正确删除。

代码列表

完整的 smart_ptr 代码列表:

#include <utility>  // 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 <typename T>
class smart_ptr {
public:
template <typename U>
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 <typename U>
smart_ptr(const smart_ptr<U>& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
other.shared_count_->add_count();
shared_count_ = other.shared_count_;
}
}
template <typename U>
smart_ptr(smart_ptr<U>&& other) noexcept
{
ptr_ = other.ptr_;
if (ptr_) {
shared_count_ =
other.shared_count_;
other.ptr_ = nullptr;
}
}
template <typename U>
smart_ptr(const smart_ptr<U>& 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 <typename T>
void swap(smart_ptr<T>& lhs,
smart_ptr<T>& rhs) noexcept
{
lhs.swap(rhs);
} template <typename T, typename U>
smart_ptr<T> static_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = static_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
} template <typename T, typename U>
smart_ptr<T> reinterpret_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = reinterpret_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
} template <typename T, typename U>
smart_ptr<T> const_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = const_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
} template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(
const smart_ptr<U>& other) noexcept
{
T* ptr = dynamic_cast<T*>(other.get());
return smart_ptr<T>(other, ptr);
}

会发现我在代码里加了不少 noexcept。这对这个智能指针在它的目标场景能正确使用是十分必要的

实现C++智能指针的更多相关文章

  1. enote笔记法使用范例(2)——指针(1)智能指针

    要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...

  2. C++11 shared_ptr 智能指针 的使用,避免内存泄露

    多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...

  3. C++智能指针

    引用计数技术及智能指针的简单实现 基础对象类 class Point { public: Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) { ...

  4. EC笔记:第三部分:17、使用独立的语句将newed对象放入智能指针

    一般的智能指针都是通过一个普通指针来初始化,所以很容易写出以下的代码: #include <iostream> using namespace std; int func1(){ //返回 ...

  5. 智能指针shared_ptr的用法

    为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...

  6. 智能指针unique_ptr的用法

    unique_ptr是独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,如下面错误用法: std::unique_pt ...

  7. 基于C/S架构的3D对战网络游戏C++框架 _05搭建系统开发环境与Boost智能指针、内存池初步了解

    本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...

  8. C++ 引用计数技术及智能指针的简单实现

    一直以来都对智能指针一知半解,看C++Primer中也讲的不够清晰明白(大概是我功力不够吧).最近花了点时间认真看了智能指针,特地来写这篇文章. 1.智能指针是什么 简单来说,智能指针是一个类,它对普 ...

  9. C++11智能指针读书笔记;

    智能指针是一个类对象,而非一个指针对象. 原始指针:通过new建立的*指针 智能指针:通过智能指针关键字(unique_ptr, shared_ptr ,weak_ptr)建立的指针 它的一种通用实现 ...

  10. 「C++」理解智能指针

    维基百科上面对于「智能指针」是这样描述的: 智能指针(英语:Smart pointer)是一种抽象的数据类型.在程序设计中,它通常是经由类型模板(class template)来实做,借由模板(tem ...

随机推荐

  1. Mybatis 返回自增主键的id

    Mybatis 返回自增主键的idkeyProperty=id:封装到对象中的id字段当中keyColumn=id:封装到数据库的id这一列order=AFTER:在新增语句之后执行 方法一 < ...

  2. QT5笔记: 15. 其他显示组件的常用功能

    其他显示组件的常用功能 代码 #include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget ...

  3. Docker安装mongoDB及使用教程

    一.mongoDB是什么? MongoDB是一个NoSQL的非关系型数据库 ,支持海量数据存储,高性能的读写. mongoDB的特点(或使用场景) 1.支持存储海量数据:(例如:直播中的打赏数据): ...

  4. Linux - 服务器磁盘 Raid & 分区 & 挂载

    一.流计算服务器 有一台流处理服务器(系统盘:2*600G.数据盘:6*600G)分区挂载如下: 设备名 分区 大小 挂载点 文件系统类型 磁盘用途 分区类别 /dev/sda /dev/sda1 3 ...

  5. 如何学好.net core?

    https://www.zhihu.com/question/348740859/answer/842656513

  6. css 保留后端 textarea 中的换行与空格字符

    原文链接:https://blog.jijian.link/2020-10-22/css-pre/ 如果后台使用 textarea 输入内容,在前段显示需要保留换行符与空白字符,该如何做? 常规方法 ...

  7. MySQL 是否可以用 Docker 容器化?

    容器 容器是为了解决 "在切换运行环境时,如何保证软件能够正常运行",容器是轻量级应用代码包,它包含在任何环境中运行所需的所有元素的软件包.容器可以虚拟化操作系统,包含依赖项,例如 ...

  8. go math/rand包详解

    go math/rand package rand import "math/rand" rand包实现了伪随机数生成器. math_rand go官方标准文档 随机数从资源生成. ...

  9. 往EXCEL粘贴超长整数字段

    写一个表格的HTML <table border="1"> <tr> <td>1</td> <td>1234567890 ...

  10. 原生JS实现虚拟列表(不使用Vue,React等前端框架)

    好家伙,   1. 什么是虚拟列表 虚拟列表(Virtual List)是一种优化长列表渲染性能的技术.当我们需要展示成千上万条数据时,如果一次性将所有数据渲染到DOM中,会导致页面卡顿甚至崩溃.虚拟 ...