C++智能指针

Introduction

C++ 程序设计中使用堆内存是非常常用的操作,堆内存的申请和释放都由程序员自己管理。但使用普通指针,容易造成内存泄露(忘记释放)、二次释放、程序发生异常时内存泄露等问题等。所以 C++11 就引入了智能指针。

C++ 中的构造函数和析构函数十分强大,可以使用构造和析构解决上面的内存泄漏问题。就算发生了异常,也能够保证析构函数成功执行。但是在多个引用和多线程的时候还是会出现问题。

C++11 中引入了智能指针(Smart Pointer),它利用了一种叫做 RAII(资源获取即初始化)的技术将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。这使得智能指针实质是一个对象,行为表现的却像一个指针。

Tip: What is meant by Resource Acquisition is Initialization (RAII)?

Answer:

It's a really terrible name for an incredibly powerful concept, and perhaps one of the number 1 things that C++ developers miss when they switch to other languages. There has been a bit of a movement to try to rename this concept as Scope-Bound Resource Management (范围绑定的资源管理), though it doesn't seem to have caught on just yet.

When we say 'Resource' we don't just mean memory - it could be file handles, network sockets, database handles, GDI objects... In short, things that we have a finite supply of and so we need to be able to control their usage. The 'Scope-bound' aspect means that the lifetime of the object is bound to the scope of a variable, so when the variable goes out of scope then the destructor will release the resource. A very useful property of this is that it makes for greater exception-safety. For instance, compare this:

>RawResourceHandle* handle=createNewResource();
>handle->performInvalidOperation(); // Oops, throws exception
>...
>deleteResource(handle); // oh dear, never gets called so the resource leaks

With the RAII one

>class ManagedResourceHandle {
>public:
ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
~ManagedResourceHandle() {delete rawHandle; }
... // omitted operator*, etc
>private:
RawResourceHandle* rawHandle;
>};

>ManagedResourceHandle handle(createNewResource());
>handle->performInvalidOperation();

In this latter case, when the exception is thrown and the stack is unwound, the local variables are destroyed which ensures that our resource is cleaned up and doesn't leak.

智能指针分别分为shared_ptr、unique_ptr和weak_ptr三种, 使用时需要引用头文件<memory>, C++98 中还有auto_ptr,基本被淘汰了,不推荐使用。而 C++11 中shared_ptr和weak_ptr都是参考boost库实现的。

Implementation

Talk is cheap, show me the code !

下面我们将展示三个智能指针的具体实现。

share_ptr

template<typename T>
class Share_ptr {
public:
Share_ptr(): _ptr(nullptr), _refCount(nullptr){};
explict Share_ptr(T *obj): _ptr(obj), _refCount(new int(1)) {};

Share_ptr(Share_ptr &other) noexcept : _ptr(other._ptr), _refCount(&(++(*other._refCount))) {};

Share_ptr(Share_ptr &&obj) noexcept : _ptr(obj._ptr), _refCount(obj._refCount) {
obj._ptr = nullptr;
}

~Share_ptr() noexcept {
if(_ptr && --*_refCount == 0) {
delete _ptr;
delete _refCount;
}
};

Share_ptr& operator=(Share_ptr other) noexcept {
using std::swap;
swap(this, other);
return this;
};

T& operator*() {
if(*_refCount == 0)
return (T)0;
return *_ptr;
};

T* operator->() {
if(*_refCount == 0)
return nullptr;
return _ptr;
};

inline int getCount() const {
return *_refCount;
}
private:
T *_ptr;
int *_refCount;

void swap(Share_ptr& lhs, Share_ptr& rhs) {
using std::swap;

swap(lhs._ptr, rhs._ptr);
swap(lhs._refCount, rhs._refCount);
}
};

Notice: 上述代码为share_ptr的简易实现(省略了reset、operate bool之类的函数),有几个点值注意的是:

  1. 使用int* _refCount引用计数作为成员变量,可以使同一资源的share_ptr的引用计数是同一个
  2. 拷贝构造函数先将_refCount的值增加后再付给本对象
  3. 析构函数需要判断指针是否有效,同时当引用计数为1时,先自减,然后delete相关指针
  4. 赋值构造函数使用Copy and Swap惯用法,传值作为参数,然后通过自定义的swap函数交换两者的引用计数以及资源指针

unique_ptr

template<typename T>
class Unique_ptr {
public:
explicit Unique_ptr(T *ptr = nullptr) noexcept : _ptr(ptr) {}

~Unique_ptr() {
if(_ptr) {
delete _ptr;
}
}

Unique_ptr(Unique_ptr &&p) noexcept: _ptr(p._ptr) {};

Unique_ptr& operator=(Unique_ptr &&rhs) noexcept {
if(this != rhs) {
if(_ptr) {
delete _ptr;
}

_ptr = rhs._ptr;
rhs._ptr = nullptr;
}

return *this;
};

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

private:
T *_ptr;
};

unique_ptr是独占资源的,所以不需要拷贝构造函数以及赋值构造函数,但是需要移动构造函数以及移动赋值构造函数,在移动赋值构造函数中需要判断对象是否为自己从而排除自赋值的情况。

weak_ptr

为了破除循环引用,需要同时支持两个智能指针 shared_ptrweak_ptr,所以引用计数也要有两个:

  1. uses强引用,记录被多少个shared_ptr掌握;
  2. weaks弱引用,记录被多少个weak_ptr掌握,再+1;

uses何时改变:

  • shared_ptr构造时,+1;
  • shared_ptr拷贝赋值时,+1;
  • weak_ptr.lock(),发生weak到shared的晋级,+1;
  • shared_ptr析构时,-1;

weaks何时改变:

  • weak_ptr构造时,+1;
  • weak_ptr拷贝赋值时,+1;
  • weak_ptr析构时,-1;
  • 当uses减为0时,weaks - 1;

我们会发现weaks,在初始化时会多加一个1,所以当uses减为0时,weaks再-1。这样就平衡了。之所以加1是为了保持接口的一致性。

如果要实现weak_ptr就得引入数据块以及控制块的概念:

智能指针

所以当我们要释放资源时,需要释放两块内存,分别是数据块和控制块。

  • 由于弱引用是不控制数据块资源的,所以是否释放数据块仅需观察强引用uses,当uses==0时,释放数据块。
  • 至于控制块的释放,自然是要等uses=0且weaks也=0时,还记得吗,在分析引用计数何时改变时,介绍到weaks初始化会多加1,当uses =0时,再自减。因此当weaks=0时,uses必等于0,所以只需判断weaks是否等于0即可。

通过上面的分析,得出一个注意事项,那就是weak_ptr在使用前很可能uses==0,导致数据块已经被释放了,因此weak_ptr并不控制数据块的生命周期。

对于控制块:

//控制块类
class ControlBlockBase {
public:
size_t GetStrongRefCount() const {
return strong_ref_count_;
}
size_t GetWeakRefCount() const {
return weak_ref_count_;
}

virtual void IncStrongRef() = 0;

virtual void DecStrongRef() = 0;

virtual void IncWeakRef() = 0;

virtual void DecWeakRef() = 0;

virtual ~ControlBlockBase() = default;

protected:
size_t strong_ref_count_ = 0;
size_t weak_ref_count_ = 0;
};
//控制块指针类
template <typename Y>
class ControlBlockPointer : public ControlBlockBase {
public:
ControlBlockPointer(Y* ptr) : ptr_(ptr) {
}

void IncStrongRef() override {
++strong_ref_count_;
}

void DecStrongRef() override {
--strong_ref_count_;
if (strong_ref_count_ == 0) {
delete ptr_;
ptr_ = nullptr;
}
}

void IncWeakRef() override {
++weak_ref_count_;
}

void DecWeakRef() override {
--weak_ref_count_;
}

~ControlBlockPointer() override {
if (ptr_) {
delete ptr_;
}
}

private:
Y* ptr_;
};

template <typename T>
class WeakPtr {
public:
////////////////////////////////////////////////////////////////////////////////////////////////
// Constructors

WeakPtr() {
}

WeakPtr(const WeakPtr& other) : ptr_(other.ptr_), block_(other.block_) {
if (block_) {
block_->IncWeakRef();
}
}

template <typename Up>
WeakPtr(const WeakPtr<Up>& other) : ptr_(other.ptr_), block_(other.block_) {
if (block_) {
block_->IncWeakRef();
}
}

WeakPtr(WeakPtr&& other) {
Dispose();
ptr_ = other.ptr_;
block_ = other.block_;

other.ptr_ = nullptr;
other.block_ = nullptr;
}

template <typename Up>
WeakPtr(WeakPtr<Up>&& other) {
Dispose();
ptr_ = other.ptr_;
block_ = other.block_;

other.ptr_ = nullptr;
other.block_ = nullptr;
}

// Demote `SharedPtr`
// #2 from https://en.cppreference.com/w/cpp/memory/weak_ptr/weak_ptr
WeakPtr(const SharedPtr<T>& other) : block_(other.block_) {
if (block_) {
ptr_ = other.ptr_;
block_->IncWeakRef();
}
}

////////////////////////////////////////////////////////////////////////////////////////////////
// `operator=`-s

WeakPtr& operator=(const WeakPtr& other) {
Dispose();
if (other.block_) {
other.block_->IncWeakRef();
}
ptr_ = other.ptr_;
block_ = other.block_;
return *this;
}

template <typename Up>
WeakPtr& operator=(const WeakPtr<Up>& other) {
Dispose();
if (other.block_) {
other.block_->IncWeakRef();
}
ptr_ = other.ptr_;
block_ = other.block_;
return *this;
}

WeakPtr& operator=(WeakPtr&& other) {
Dispose();
ptr_ = other.ptr_;
block_ = other.block_;

other.ptr_ = nullptr;
other.block_ = nullptr;
return *this;
}

template <typename Up>
WeakPtr& operator=(WeakPtr<Up>&& other) {
Dispose();
ptr_ = other.ptr_;
block_ = other.block_;

other.ptr_ = nullptr;
other.block_ = nullptr;
return *this;
}

template <typename Y>
WeakPtr& operator=(const SharedPtr<Y>& other) {
Dispose();
if (other.block_) {
other.block_->IncWeakRef();
}
ptr_ = other.ptr_;
block_ = other.block_;
return *this;
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Destructor

~WeakPtr() {
Dispose();
ptr_ = nullptr;
block_ = nullptr;
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Modifiers

void Reset() {
Dispose();
ptr_ = nullptr;
block_ = nullptr;
}
void Swap(WeakPtr& other) {
std::swap(ptr_, other.ptr_);
std::swap(block_, other.block_);
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Observers

size_t UseCount() const {
if (block_) {
return block_->GetStrongRefCount();
}
return 0; // *this is empty
}
bool Expired() const {
return (UseCount() == 0);
}
SharedPtr<T> Lock() const {
if (Expired()) {
return SharedPtr<T>();
}
return SharedPtr<T>(*this);
}

private:
T* ptr_ = nullptr;
ControlBlockBase* block_ = nullptr;

void Dispose() { // this method is actually deleting the block
if (block_) {
block_->DecWeakRef();
}
if (block_) {
if (block_->GetStrongRefCount() == 0 &&
block_->GetWeakRefCount() == 0) { // may be "delete nullptr"
delete block_;
block_ = nullptr;
}
}
}

template <typename Y>
friend class SharedPtr;

template <typename Y>
friend class WeakPtr;
};

完整代码可参照:Implementation of std smart pointers : unique_ptr, shared_ptr, weak_ptr and boost's intrusive_ptr (github.com)