45 C++智能指针的原理与模拟实现,内存泄漏与RAII
⭐上篇文章:44 C++智能指针详解:安全高效管理内存-CSDN博客
⭐代码仓库:橘子真甜 (yzc-YZC) - Gitee.com
目录
一. 使用类封装一个智能指针
二. 智能指针解决多次析构的方式
2.1 早期版本 auto_ptr
2.2 unique_ptr
2.4 weak_ptr
三. 内存泄漏与RAII
3.1 内存泄漏
3.2 RAII思想
一. 使用类封装一个智能指针
智能指针是基于RAII思想构建的指针,需要在构造时候创建资源,析构的时候释放资源。这样才能保证不会有内存的泄漏。
还需要使用模板保证能够存储不同类型的变量
基本代码结构如下:在构造函数中初始化资源,析构函数中自动销毁资源。通过类的管理实现资源的开辟和销毁
#pragma oncetemplate <class T>
class mySmartPtr
{
public:mySmartPtr(const T *ptr): _ptr(ptr) {}~mySmartPtr(){if (_ptr != nullptr)delete _ptr;_ptr = nullptr}private:T *_ptr;
};
但是我们还不能使用,因为没有重载operator* 和 operator->

改进后的代码如下:
#pragma oncetemplate <class T>
class mySmartPtr
{
public:mySmartPtr(const T *ptr): _ptr(ptr) {}~mySmartPtr(){if (_ptr != nullptr)delete _ptr;_ptr = nullptr;}T& operator*(){return *_ptr;}T *operator->(){return _ptr;}private:T *_ptr;
};
重新测试主程序,运行结果如下。

此时仍然有问题,假如有两个智能指针管理同一个对象。这样同一块资源就会被析构两次,这样就会非法访问内存。
二. 智能指针解决多次析构的方式
2.1 早期版本 auto_ptr
上篇文章提到:auto_ptr是通过转移管理权来防止多次析构的,不过现在已经被弃用了。因为有更好的方式,auto_ptr转移管理权会容易导致原指针变为野指针
不过auto_ptr实现的思路还是可以学习的!转移管理权首先要考虑转移的指针是否为空指针
拷贝构造:直接让转移的指针指向我管理的空间即可,然后让自己置空。
调用 p1 = p2 即赋值的时候:则需要释放转移指针的资源并置空,然后执行上面操作。
是不是有点抽象?可以看下面的图画


代码如下:
template <class T>
class myAutoPtr
{
public:myAutoPtr(T *ptr): _ptr(ptr) {}~mySharedPtr(){//直接调用release释放自己即可release();}// 拷贝构造函数myAutoPtr(myAutoPtr<T> &aptr): _ptr(aptr._ptr){aptr._ptr = nullptr;}// operator重载myAutoPtr<T> &operator=(myAutoPtr<T> &aptr){if (this != aptr){if(aptr != nullptr){delete _ptr;_ptr = aptr._ptr;aptr._ptr = nullptr;}}return *this;}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}private:T *_ptr;
};
2.2 unique_ptr
unique_ptr通过防止拷贝来保证只有一个智能指针对象管理一个对象资源,这样就不会造成多次析构的问题了!解决方法简单粗暴,但是好用。
代码如下:
template <class T>
class myUniquePtr
{
public:myUniquePtr(T *ptr): _ptr(ptr) {}~myUniquePtr(){if (_ptr != nullptr)delete _ptr;_ptr = nullptr;}// 禁用拷贝构造和赋值重载myUniquePtr(myUniquePtr<T> &mup) = delete;myUniquePtr<T> &operator=(myUniquePtr<T> &mup) = delete;T &operator*(){return *_ptr;}T *operator->(){return _ptr;}private:T *_ptr;
};
2.3 shared_ptr⭐
共享指针通过引用计数这个变量来保证多次析构的问题,每有一个指针指向当前对象,引用计数就++。当引用计数为0,即没有指针指向这个对象的时候就销毁资源。
不过这样也有了新的问题!如何保证引用计数的线程安全问题?
C++11内部是通过原子变量的方式来保证的!原子变量作为引用计数,原子变量++ --操作都是线程安全的
这里使用mutex互斥锁的方式来实现保护shared_ptr中的引用计数
如果使用mutex保护引用计数,需要如何定义count呢?
int count 不能这样,因为每一个指针都有自己的count。static int count也不行,因为这样会导致不同的类型有相同的count。int & 也不行,因为管理比较复杂。
使用 int *在堆中创建资源是最好管理的!
代码如下:
部分函数:
//增加引用计数void add_ref_count(){//注意加锁std::unique_lock<std::mutex> ul(_lock);(*_count)++;}
// 释放自己的资源,引用计数--,为0释放所有资源,不为0,啥都不做void release(){bool flag = false;{std::unique_lock<std::mutex> ul(_lock);if (--(*_count) == 0){// 说明引用计数为0,需要释放指向的对象if (_ptr != nullptr)delete _ptr;_ptr = nullptr;// 同时需要释放引用计数delete _count;_count = nullptr;// 处于加锁状态,不可释放锁的资源// 通过标志位来释放锁资源flag = true;}}// 释放锁资源if (flag)delete _lock;}
类代码如下:
注意:拷贝和赋值的时候需要将所有成员变量都拷贝
release时候需要注意_ptr是否为空,如果为空就不需要释放数据
如果赋值的指针有着自己管理的资源,需要先释放自己资源然后再管理新资源
template <class T>
class mySharedPtr
{
public:mySharedPtr(T *ptr): _ptr(ptr), _count(new int), _lock(new std::mutex) {}~mySharedPtr(){release();}mySharedPtr(mySharedPtr<T> &mup): _ptr(mup._ptr), _count(mup._count),_lock(mup._lock) // 初始化ptr和count保证二者一致{// 增加引用计数add_ref_count();}mySharedPtr<T> &operator=(mySharedPtr<T> &mup){if (this != &mup){// 如果自己有管理的对象的话,需要释放自己的资源release();// 更新所有的资源_ptr = mup._ptr;_count = mup._count;_lock = mup._lock;// 增加引用计数add_ref_count();}return *this;}T &operator*(){return *_ptr;}T *operator->(){return _ptr;}private:// 增加引用计数void add_ref_count(){// 注意加锁std::unique_lock<std::mutex> ul(*_lock);(*_count)++;}// 释放自己的资源,引用计数--,为0释放所有资源,不为0,啥都不做void release(){bool flag = false;{std::unique_lock<std::mutex> ul(*_lock);if (--(*_count) == 0 && _ptr != nullptr){// 说明引用计数为0,需要释放指向的对象if (_ptr != nullptr)delete _ptr;_ptr = nullptr;// 同时需要释放引用计数delete _count;_count = nullptr;// 处于加锁状态,不可释放锁的资源// 通过标志位来释放锁资源flag = true;}}// 释放锁资源if (flag)delete _lock;}private:T *_ptr;int *_count; // 共享计数std::mutex *_lock; // 互斥锁
};
测试代码与运行结果
#include <iostream>
#include "smartptr.hpp"int main()
{int *a = new int(2);mySharedPtr<int> ptr1(a);mySharedPtr<int> ptr2(ptr1);mySharedPtr<int> ptr3(new int(8));ptr3 = ptr1;std::cout << "*ptr1:" << *ptr1 << std::endl;std::cout << "*ptr2:" << *ptr2 << std::endl;std::cout << "*ptr3:" << *ptr3 << std::endl;*ptr1 = 12345;std::cout << "*ptr1:" << *ptr1 << std::endl;std::cout << "*ptr2:" << *ptr2 << std::endl;std::cout << "*ptr3:" << *ptr3 << std::endl;
}
可以看到,这些指针管理同一个对象,并且没有

2.4 weak_ptr
weak_ptr是用于解决shared_ptr循环引用的问题。在上篇文章中已经详细说明,这里就不过多介绍和实现了。
三. 内存泄漏与RAII
3.1 内存泄漏
内存泄漏是由于某进程由于代码失误或者其他错误操作造成的。比如 动态开辟了资源,使用完毕了却不释放,一直占用内存却不释放。又比如,父子进程中,子进程退出后,父进程不去回收子进程的资源,这样也会造成内存泄漏等。
这些占用内存又没有使用资源是没有用的,如果一个系统长时间运行,并且有导致内存泄漏的代码。最后整个系统会由于内存泄漏卡死。
解决内存泄漏需要从三个方面来说:
1 规范写代码,申请资源需要释放
2 使用如智能指针这样的技术来保证无用的资源被释放
3 代码运行后可以使用一些工具如 valgrind
3.2 RAII思想
RAII思想是:资源获取即初始化,资源可以自动销毁。用于减少内存泄漏
比如智能指针就是 在构造函数中完成资源初始化,当使用完毕后通过析构函数自动销毁
又比如 lock_guard unique_guard等锁,初始化时候进行加锁,解锁之后进行销毁
