[C++]通过智能指针的实现理解RAII

Koarz
2024-01-11 / 0 评论 / 0 阅读 / 正在检测是否收录...

C以及C++的内存管理是让人很头疼的,指针满天飞,或者是忘了及时释放内存导致内存泄漏,多线程情况下加了锁忘记unlock...一些其他语言例如java有gc来防止内存泄漏,C++也有类似机制,那就是RAII:资源获取即初始化(Resource Acquisition Is Initialization)
关于RAII的具体介绍可以看cppreference,简单来讲就是将需要管理的资源与对象的生命周期绑定,当对象析构时可以自动(通过析构函数)释放所管理的资源。
RAII应用的典型就是智能指针了,接下来我会讲讲智能指针的实现以及原理,让你理解这一工具(可以叫工具吗?我不知道)

首先讲讲程序运行时的堆栈结构吧
我们熟知的main函数以及其他各种函数都是在栈上运行的,具体结构类似这样:
heapstack
在栈上储存着普通的变量,或者对象什么的,图中的情况就是类似这样一段代码运行时的情况

void fun2() {
 int a{};
 SomeClass b;
 ...
}

void fun1() {
 ...
 fun2();
 ...
}

int main() {
 ...
 fun1();
 ...
}

如果fun2执行完毕,那么图中fun1以下的地方都会被“移除”,也就是出栈了,接着继续执行fun1中的剩余代码。
于是我们可以知道,fun2内部的所有变量(a,b...)的生命周期都随着fun2执行结束而结束了,那么如果b就是一个std::unique_ptr,这时候b所管理的资源也就随着b的析构而被释放了。如果你没看懂的话我们可以将这个例子写得更具体一点。

#include <iostream>
using namespace std;

class A {
public:
 A() { cout << "A()\n"; }
 ~A() { cout << "~A()\n"; }
};

void fun2() {
 cout << "fun2 begin\n";
 A b;
 cout << "fun2 end\n";
}

void fun1() {
 cout << "fun1 begin\n";
 fun2();
 cout << "fun1 end\n";
}

int main() {
 cout << "main begin\n";
 fun1();
 cout << "main end\n";
 return 0;
}


这样对象的生命周期是不是就很直观了,我们就可以理解std::unique_ptr在执行过程中做了什么。
这里给出一个简单的unique_ptr的实现:

#pragma once

namespace koarz {

template <typename T> class unique_ptr;

} // namespace koarz

template <typename T> class koarz::unique_ptr {
private:
  T *ptr_;

public:
  unique_ptr() : ptr_(nullptr){};
  unique_ptr(T *ptr) : ptr_(ptr){};
  unique_ptr(koarz::unique_ptr<T> &) = delete;
  unique_ptr &operator=(koarz::unique_ptr<T> &) = delete;
  unique_ptr(koarz::unique_ptr<T> &&) noexcept;
  unique_ptr &operator=(koarz::unique_ptr<T> &&) noexcept;
  T *operator->() const { return ptr_; }
  T &operator*() const { return *ptr_; }
  T *get() const { return ptr_; }
  T *release() {
    T *tmp = ptr_;
    ptr_ = nullptr;
    return tmp;
  }
  void reset(T *ptr = nullptr) {
    if (ptr_ != ptr) {
      delete ptr_;
    }
    ptr_ = ptr;
  }
  ~unique_ptr() { delete ptr_; }
};

template <typename T>
koarz::unique_ptr<T>::unique_ptr(koarz::unique_ptr<T> &&other) noexcept {
  ptr_ = other.ptr_;
  other.ptr_ = nullptr;
}

template <typename T>
koarz::unique_ptr<T> &
koarz::unique_ptr<T>::operator=(koarz::unique_ptr<T> &&other) noexcept {
  if (ptr_ != other.ptr_) {
    delete ptr_;
  }
  ptr_ = other.ptr_;
  other.ptr_ = nullptr;
  return *this;
}

可以看到在析构函数中呢,我们对unique_ptr管理的内存进行了释放,也就是在执行上边代码的~A的时候释放了这块内存,fun2就可以改成这样:

void fun2() {
  koarz::unqiue_ptr<int> a(new int(10));
  cout << *a << endl;
}
// 我并没有实现make_unique所以我这里直接new了,但是一般使用智能指针我们会使用
// unique_ptr<int> a = make_unique<int>(10);智能指针的资源不应该在构造期间构造

这段代码肯定会输出10然后析构掉new出来的内存,这就是智能指针管理内存的方式也就是利用RAII技术完成资源管理的一个例子。
RAII的应用有很多
std::unique_ptr 及 std::shared_ptr 用于管理动态分配的内存,或以用户提供的删除器管理任何以普通指针表示的资源;
std::lock_guard、std::unique_lock、std::shared_lock 用于管理互斥体。
最后再贴上shared_ptr的简单实现,你可以试着理解shared_ptr与unique_ptr的不同在哪。

#pragma once

#include <atomic>

namespace koarz {

template <typename T> class shared_ptr;
} // namespace koarz

template <typename T> class koarz::shared_ptr {
private:
  T *ptr_{nullptr};
  std::atomic_uint *count_{nullptr};
  void release() {
    if (count_ != nullptr) {
      (*count_)--;
      if (*count_ == 0) {
        delete count_;
        delete ptr_;
        count_ = nullptr;
        ptr_ = nullptr;
      }
    }
  }

public:
  shared_ptr() : ptr_(nullptr), count_(nullptr){};
  shared_ptr(T *ptr) : ptr_(ptr) { count_ = new std::atomic_uint(1); };
  shared_ptr(const koarz::shared_ptr<T> &) noexcept;
  shared_ptr &operator=(const koarz::shared_ptr<T> &) noexcept;
  shared_ptr(koarz::shared_ptr<T> &&) noexcept;
  shared_ptr &operator=(koarz::shared_ptr<T> &&) noexcept;
  T *operator->() const { return ptr_; }
  T &operator*() const { return *ptr_; }
  T *get() const { return ptr_; }

  void reset(T *ptr = nullptr) {
    if (ptr == ptr_ || ptr == nullptr)
      return;
    ptr_ = ptr;
    if (count_ != nullptr) {
      *count_ = 1;
    } else {
      count_ = new std::atomic_uint(1);
    }
  }
  void swap(koarz::shared_ptr<T> &);
  int get_count() { return *count_; }
  ~shared_ptr();
};

template <typename T>
koarz::shared_ptr<T>::shared_ptr(const koarz::shared_ptr<T> &other) noexcept {
  if (other.ptr_ != this->ptr_) {
    release();
    ptr_ = other.ptr_;
    count_ = other.count_;
    (*count_)++;
  }
}

template <typename T>
koarz::shared_ptr<T> &
koarz::shared_ptr<T>::operator=(const koarz::shared_ptr<T> &other) noexcept {
  if (other.ptr_ != this->ptr_) {
    release();
    ptr_ = other.ptr_;
    count_ = other.count_;
    (*count_)++;
  }
  return *this;
}

template <typename T>
koarz::shared_ptr<T>::shared_ptr(koarz::shared_ptr<T> &&other) noexcept {
  if (other.ptr_ != this->ptr_) {
    release();
    ptr_ = other.ptr_;
    count_ = other.count_;
  }
}

template <typename T>
koarz::shared_ptr<T> &
koarz::shared_ptr<T>::operator=(koarz::shared_ptr<T> &&other) noexcept {
  if (other.ptr_ != this->ptr_) {
    release();
    ptr_ = other.ptr_;
    count_ = other.count_;
  }
  return *this;
}

template <typename T> koarz::shared_ptr<T>::~shared_ptr() {
  if (count_ != nullptr) {
    (*count_)--;
    if (*count_ == 0) {
      delete ptr_;
      delete count_;
    }
  }
}
// 这段代码当然没有完整实现,你可以试着让它更加完善

shared_ptr只有在引用计数为0的情况下才会delete掉管理的内存,而拷贝时会增加引用计数,一般情况下shared_ptr对象析构时只会减少引用计数,这样就保证了最后一个共享这块内存的对象析构时才会释放这块内存。

0

评论 (0)

取消