实现C++智能指针
在对象切片一文中,提到可使用充当智能指针的类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
,但实际指向的却应该是circle
或triangle
之类的对象。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++智能指针的更多相关文章
- enote笔记法使用范例(2)——指针(1)智能指针
要知道什么是智能指针,首先了解什么称为 “资源分配即初始化” what RAII:RAII—Resource Acquisition Is Initialization,即“资源分配即初始化” 在&l ...
- C++11 shared_ptr 智能指针 的使用,避免内存泄露
多线程程序经常会遇到在某个线程A创建了一个对象,这个对象需要在线程B使用, 在没有shared_ptr时,因为线程A,B结束时间不确定,即在A或B线程先释放这个对象都有可能造成另一个线程崩溃, 所以为 ...
- C++智能指针
引用计数技术及智能指针的简单实现 基础对象类 class Point { public: Point(int xVal = 0, int yVal = 0) : x(xVal), y(yVal) { ...
- EC笔记:第三部分:17、使用独立的语句将newed对象放入智能指针
一般的智能指针都是通过一个普通指针来初始化,所以很容易写出以下的代码: #include <iostream> using namespace std; int func1(){ //返回 ...
- 智能指针shared_ptr的用法
为了解决C++内存泄漏的问题,C++11引入了智能指针(Smart Pointer). 智能指针的原理是,接受一个申请好的内存地址,构造一个保存在栈上的智能指针对象,当程序退出栈的作用域范围后,由于栈 ...
- 智能指针unique_ptr的用法
unique_ptr是独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr,如下面错误用法: std::unique_pt ...
- 基于C/S架构的3D对战网络游戏C++框架_05搭建系统开发环境与Boost智能指针、内存池初步了解
本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...
- C++ 引用计数技术及智能指针的简单实现
一直以来都对智能指针一知半解,看C++Primer中也讲的不够清晰明白(大概是我功力不够吧).最近花了点时间认真看了智能指针,特地来写这篇文章. 1.智能指针是什么 简单来说,智能指针是一个类,它对普 ...
- C++11智能指针读书笔记;
智能指针是一个类对象,而非一个指针对象. 原始指针:通过new建立的*指针 智能指针:通过智能指针关键字(unique_ptr, shared_ptr ,weak_ptr)建立的指针 它的一种通用实现 ...
- 「C++」理解智能指针
维基百科上面对于「智能指针」是这样描述的: 智能指针(英语:Smart pointer)是一种抽象的数据类型.在程序设计中,它通常是经由类型模板(class template)来实做,借由模板(tem ...
随机推荐
- python基础-函数(lambda表达式、函数作参数、内置函数、推导式)和pip
函数进阶 今日概要: 函数名就是一个变量(扩展) 匿名函数(lambda表达式) 重点内置函数--python内置函数 推导式(一行代码生成数据) 1. 函数名就是变量 def func(): pas ...
- Android 实现人脸识别 活体检测以及人脸搜索
关于「保护伞FaceAI SDK」 Umbrella FaceAI SDK is on_device Offline Android Face Detection .Recognition .Live ...
- [Qt 基础-01] QPushButton
QPushButton 简介 QPushButton是一个很常用的一个按钮控件,主要用于创建一个可按压的按键.它显示了一 个文本和一个图标.另外,你也可以在创建时,指定一个快捷键. 基本用法 1. 创 ...
- 前端解析excel表格实现
1. 背景:在做react项目时,遇到一个解析excel的需求变更,把从原来后端解析变更为前端解析. 1.1 由于后端解析excel文件有安全隐患,因为项目中后端不允许上传文件,当然后端解析对前端来说 ...
- 写于vue3.0发布前夕的helloworld之四
OK.接上回到render: with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(msg))])} 接 ...
- JetBrains goland、pycharm、webstorm、phpstorm 对比两文件内容是否一致
对比文件 JetBrains goland.pycharm.webstorm.phpstorm 对比两文件内容是否一致 第一种 打开文件,按住键盘上的CTRL键,然后鼠标右键,点击菜单中的" ...
- mongodb 数据库操作——备份 还原 导出 导入
mongodump备份数据库 命令格式 mongodump -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -o 文件存在路径 如果没有用户,可以去掉-u和-p. 如果导出本机 ...
- golang结构体判断是否为空
前言 使用任何编程语言都会遇到判空的问题,那么Golang对于自定义的结构体类型如何判空呢? 其实空结构体可不是简单的与nil做比较哦.请看下面两种方法: package main import ( ...
- go-ini 中文文档
简介 地表 最强大.最方便 和 最流行 的 Go 语言 INI 文件操作库 灵活的数据源 不光光可以从文件读取配置,还支持 []byte 类型的纯数据读取和基于 io.ReadCloser 的流式读取 ...
- MFC编程中与编码方式有关的宏定义的使用
1 多字节字符集:char *strcpy(char *strDestination, const char *strSource); Unicode字符集:wchar_t *wcscpy(wchar ...