45 C++智能指针的原理与模拟实现,内存泄漏与RAII
目录
一. 使用类封装一个智能指针
智能指针是基于RAII思想构建的指针,需要在构造时候创建资源,析构的时候释放资源。这样才能保证不会有内存的泄漏。
还需要使用模板保证能够存储不同类型的变量
基本代码结构如下:在构造函数中初始化资源,析构函数中自动销毁资源。通过类的管理实现资源的开辟和销毁
#pragma once
template <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 once
template <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等锁,初始化时候进行加锁,解锁之后进行销毁
更多推荐



所有评论(0)