序言
在使用 C / C++ 进行编程时,许多场景都需要我们在堆上申请空间,堆内存的申请和释放都需要我们自己进行手动管理。这就存在容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题,这对长期运行的程序来说是致命的!
但在 C++11 中引入了智能指针,使我们将内存的管理交给智能指针。
1. 什么是智能指针
1.1 概念
该指针旨在自动管理动态分配的内存,减少内存泄漏和野指针的问题
。智能指针是模板类,它们的行为类似于指针,但提供了自动的内存管理功能
。
1.2 RAII 思想
智能指针在 C++ 中主要使用了 资源获取即初始化(Resource Acquisition Is Initialization, RAII)
的思想,以及所有权管理的概念。RAII
的核心思想是,资源的分配(获取)和初始化发生在对象的构造期间
,而 资源的释放(清理)则发生在对象的析构期间
。这种方式通过对象的生命周期来自动管理资源,避免了忘记释放资源(如内存泄漏)的问题。
1.3 体现 RAII 思想
在这里为大家举一个简单的例子来体现 RAII 思想
:
这是我们现在的内存管理,我们需要手动释放申请的资源:
int main()
{
int* Ptr = new int[5];
// Do Otherthing......
delete[] Ptr;
return 0;
}
现在实现一个简单的智能指针:
template <class T>
class SmartPtr
{
public:
SmartPtr(T* Ptr)
:_Ptr(Ptr)
{}
// Ohther Functions...
~SmartPtr()
{
std::cout << "Delete Ptr" << std::endl;
delete _Ptr;
}
private:
T* _Ptr;
};
int main()
{
SmartPtr<int> sp(new int(1));
return 0;
}
我们将申请的资源交给智能指针为我们管理,当程序结束时,将自动执行析构函数释放资源:
2. 三种主要的智能指针
2.1 unique_ptr
unique_ptr
是一种 独占所有权
的智能指针,意味着 同一时间内只能有一个 unique_ptr 指向给定的对象
,他直接删除了他的拷贝构造和赋值运算符重载:
注意:保留了对将亡值的赋值运算符重载,因为就修改操作后将亡值就释放了,依旧满足独占所有权的特性!
简单使用一下该指针:
void Ptr_1()
{
std::unique_ptr<MyClass> up1(new MyClass);
up1->DoSomething();
std::unique_ptr<MyClass> up2(move(up1)); // 移动构造
std::cout << up1 << std::endl; // up1 这时已被悬空
}
程序输出结果:
I am working!
0000000000000000
~MyClass()
该智能指针还是比较简单的,但是一定要注意指针被悬空后的情况!
2.2 shared_ptr
shared_ptr
是一种 共享所有权
的智能指针,允许多个 shared_ptr
实例指向同一个对象。每个 shared_ptr
都有一个与之关联的计数器,称为控制块,用于跟踪有多少个 shared_ptr
实例指向该对象。当最后一个指向对象的 shared_ptr 被销毁或重置时,对象才会被删除。
简单使用一下该指针:
void Ptr_2()
{
std::shared_ptr<MyClass> sp1(new MyClass); // 构造
std::shared_ptr<MyClass> sp2(sp1); // 拷贝构造
sp2->DoSomething();
std::cout << "The use counts = " << sp1.use_count() << std::endl; // 查看计数器
}
程序输出结果:
I am working!
The use counts = 2
~MyClass()
循环引用问题
使用该指针需要注意一个非常特殊的情况,一不小心掉入坑中!这种情况就是循环引用:
struct Node
{
Node(int val)
:_val(val)
,_next(nullptr)
,_prev(nullptr)
{}
~Node()
{
std::cout << "~Node()" << std::endl;
}
int _val;
std::shared_ptr<Node> _prev;
std::shared_ptr<Node> _next;
};
void Ptr_3()
{
std::shared_ptr<Node> sp1(new Node(1));
std::shared_ptr<Node> sp2(new Node(2));
// 相互指向
sp1->_next = sp2;
sp1->_prev = sp1;
}
运行程序,我们会发现并没有正常的未释放资源!出现问题的原因,用图来表示:
当我们函数结束时,函数栈帧销毁,这时两个指针对象调用析构函数来释放资源:
这里的析构函数并不会真正意义上调用 delete
,而是减少引用!直到引用计数为 0 才会调用 delete
,所以,这里的资源并没有真正的被释放,因为 next,prev
指针的存在,所以资源并不会被释放!
2.3 weak_ptr
weak_ptr
是一种 不拥有其所指向对象的智能指针
,它主要 用于解决 shared_ptr 之间的循环引用问题
。weak_ptr
必须与 shared_ptr
一起使用,因为它不拥有对象,所以不会增加对象的共享计数。
简单使用一下该指针:
void Ptr_4()
{
// std::weak_ptr<int> wp(new int(1)); // 错误的使用方法 weak_ptr 不能直接管理对象的生命周期
std::shared_ptr<int> sp(new int(1));
std::weak_ptr<int> wp(sp);
std::cout << wp.use_count() << std::endl;
}
现在我们使用 weak_ptr
来解决循环引用的问题:
struct Node
{
Node(int val)
:_val(val)
{}
~Node()
{
std::cout << "~Node()" << std::endl;
}
int _val;
std::weak_ptr<Node> _prev;
std::weak_ptr<Node> _next;
};
void Ptr_3()
{
std::shared_ptr<Node> sp1(new Node(1));
std::shared_ptr<Node> sp2(new Node(2));
// 相互指向
sp1->_next = sp2;
sp2->_prev = sp1;
}
将 _prev, _next
修改为 weak_ptr
来代表不进行引用计数的增加,只是简单的指向,资源现在被正常的释放!
2.4 自定义删除器
在智能指针的底层,对于资源的释放,单个就使用 delete
,数组就是使用 delete[]
,大绝大多数场景下都是没问题的。但是,总是有特殊情况:
void Ptr_5()
{
std::shared_ptr<FILE> sp(fopen("test.txt", "w"));
}
请问,这个使用 delete
可以删除吗?当然是不可以,有人会觉得,这不是在鸡蛋里挑骨头吗?其实,我们很多时候就是更应该想到极端情况,Bug
不能被消除,但可以被极力避免!我们程序的健壮性,肯定决定了我们运行的稳定性!
这时,我们就可以使用自定义删除器:
我们需要传递一个可调用对象告诉他,该怎么删除。选择很多,包括函数指针,仿函数… 在这里我们选择 lambda
,这就非常的方便!
std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* file) {fclose(file); });
3. 简单实现
在这里我们简单实现一个 shared_ptr
:
3.1 构造函数
首先,先介绍三个成员变量:
T* _Ptr;
std::atomic<int>* _RefCounts; // 引用计数(保证原子性)
std::function<void(T*)> _Del; // 自定义删除器
_Ptr
:是我们需要管理的资源_RefCounts
:计数器,记录多少指针指向该资源(本质就是int
,但是支持原子性操作)_Del
:删除器,保证资源正常的释放,有特殊删除需求可传入
一共实现了简单的三个构造函数:
// 构造函数(默认删除器)
SharedPtr(T* Ptr)
: _Ptr(Ptr)
, _RefCounts(new std::atomic<int>(1))
, _Del([](T* val) { delete val; })
{}
// 构造函数(自定义删除器)
template<class D>
SharedPtr(T* Ptr, D Del)
: _Ptr(Ptr)
, _RefCounts(new std::atomic<int>(1))
, _Del(Del)
{}
// 拷贝构造
SharedPtr(const SharedPtr<T>& sp)
: _Ptr(sp._Ptr)
, _RefCounts(sp._RefCounts)
, _Del(sp._Del)
{
++(*_RefCounts);
}
3.2 析构函数
该函数在释放资源前需要判断,当引用计数为 0 时才可释放资源,避免正在使用的指针被悬空:
// 当计数置 0 时调用
void destructor()
{
_Del(_Ptr);
_Ptr = nullptr;
delete _RefCounts;
_RefCounts = nullptr;
}
// 析构函数
~SharedPtr()
{
// 引用减少
--(*_RefCounts);
if (*_RefCounts == 0)
{
destructor();
}
}
3.3 赋值运算符重载
赋值运算符重载需要极其注意,在指向其他资源前需要对当前资源释放(引用计数减一,若为 0,才真正释放资源):
void clear()
{
// 引用减少
--(*_RefCounts);
if (*_RefCounts == 0)
{
destructor();
}
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (this != &sp) // 防止自我赋值
{
clear(); // 释放当前资源
_Ptr = sp._Ptr;
_RefCounts = sp._RefCounts;
_Del = sp._Del; // 复制删除器
++(*_RefCounts);
}
return *this;
}
3.4 其余的运算符重载
此部分为常用的运算符重载:
T& operator* ()
{
return *_Ptr;
}
T* operator->()
{
return _Ptr;
}
4. 总结
在这篇文章中我们首先介绍了智能指针的思想,之后分别介绍了常用的三种智能指针(unique_ptr, shared_ptr, weak_ptr
),最后我们简单的实现了第二个指针,希望大家有所收获!