目录
前言:
C++的智能指针主要是针对原生指针在内存管理上出现的一些问题而设计的。我们以动态空间开辟为例,大型项目中,当频繁使用new开辟空间时,可能会存在空间开辟失败而抛异常,这时在频繁使用 try—catch 可能就比较麻烦,还有动态开辟空间时中间碰上异常问题还可能出现内存泄漏的问题。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
/*问题一: p1、p2、p3....动态开辟空间,可能存在异常(开辟失败),这里显然不方便使用 tyr—catch 一一判断*/
int* p1 = new int;
int* p2 = new int;
int* p3 = new int;//问题二: 若这里div函数抛异常导致下面 delete 未执行,空间没被释放
cout << div() << endl;delete p1;
delete p2;
delete p3;
}
int main()
{
try
{
Func();
}
catch (exception& e) //C++动态库中的异常体系(父子进程关系)
{
cout << e.what() << endl;
}
return 0;
}
解决上面问题可采用RAII思想或者智能指针来管理资源。
一,RAII思想
RAII 是一种利用对象生命周期来控制程序资源的简单技术。这个原则的基本思想是通过对象的构造函数来获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
RAll 思想实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大特点:
1,不需要显式地释放资源,对象的生命周期结束时自动释放,避免了内存泄漏的风险。
2,采用这种方式,对象所需的资源在其生命期内始终保持有效,但对象生命周期一旦结束,资源将自动释放,运用时一定要注意。
#include <iostream>
using namespace std;
template<class T>
class SmartPtr //使用RAII思想设计的SmartPtr类
{
public:
//构造函数保存资源
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{ }
//析构函数释放资源
~SmartPtr()
{
if (_ptr)
{
cout << "delete _ptr" << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
/*如果new int抛出了std::bad_alloc异常,那么构造函数将会立即终止执行,这里没有 try—catch 处理异常,程序将会终止。new抛出异常后,SmartPtr的构造函数并没有完成其执行,析构函数不会被调用,可放心使用*/
SmartPtr<int> p1(new int);
SmartPtr<int> p2(new int);
SmartPtr<int> p3(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e) //C++动态库中的异常体系(父子进程关系)
{
cout << e.what() << endl;
}
return 0;
}
二,智能指针
2-1,智能指针的介绍
C++中的智能指针运用的是 RAII 思想,它是行为类似于指针的类对象,封装了原始指针并提供了自动内存管理的功能,从而实现了RAII的思想。上面SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过“ -> ”去访问所指空间中的内容,因此,这里还需重载“ *、-> ”才可将其像指针一样去使用。
template<class T>
class SmartPtr
{
public:
//构造函数保存资源
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{ }
//析构函数释放资源
~SmartPtr()
{
if (_ptr)
{
cout << "delete _ptr" << endl;
delete _ptr;
}
}//指针行为
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; } //使用时原型:operator->()->
private:
T* _ptr;
};
智能指针即两大特性:1,RAII特性。 2,重载operator*和opertaor->具有像指针的行为。
C++98最开始先提供了 auto_ptr 智能指针,但它存在很大的问题,后来C++11标准库提供了三种常用的智能指针:unique_ptr 、shared_ptr 、weak_ptr
2-2,auto_ptr智能指针
C++98版本的库中就提供了auto_ptr的智能指针,但它却并不常用,因为这个智能指针存在严重的问题。
auto_ptr的实现原理:运用管理权转移的思想,被拷贝对象将资源管理权转移给拷贝对象,导致被拷贝对象悬空。
int main()
{
auto_ptr<int> sp1(new int(1));
//将sp1的所有资源全部给sp2,sp1悬空,类似与移动语义
auto_ptr<int> sp2(sp1); //赋值(=)与之同理,这里不在演示*sp2 += 10;
*sp1 += 10; //运行报错,sp1将资源转移给了sp2,sp1现在如同空指针
return 0;
}
auto_ptr的模拟实现如下:
namespace bit
{
template<class T>
class auto_ptr
{
public:
//构造与析构
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{ }
~auto_ptr()
{
if (_ptr)
{
std::cout << "delete _ptr" << std::endl;
delete _ptr;
}
}
//赋值与拷贝构造
//常识,避免无限拷贝,重载赋值运算符时的返回参数和形参都必须使用引用
auto_ptr<T>& operator=(auto_ptr<T>& it)
{if (_ptr) //注意释放内存空间
{
delete _ptr;
}
_ptr = it._ptr;
it._ptr = nullptr;
return *this;
}
auto_ptr(auto_ptr<T>& it)
{
*this = it;
}T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};}
运行示例:
int main()
{
//拷贝构造
bit::auto_ptr<int> p1(new int(1));
bit::auto_ptr<int> p2(p1);
//赋值运算符
bit::auto_ptr<int> p3(new int(2));
bit::auto_ptr<int> p4(new int(3));
p3 = p4;
*p2 += 10;
*p3 += 10;
cout << *p2 << " " << *p3 << endl;
return 0;
}
auto_ptr 由于这种危险机制,很多公司都禁止使用它,一旦不小心就访问空指针。
2-3,unique_ptr智能指针
C++11考虑到 auto_ptr 的危险机制,开始提供更靠谱的 unique_ptr指针智能。unique_ptr 属于独占所有权的智能指针,禁止了拷贝赋值操作,用于管理单个对象的生命周期,适用于不需要拷贝的场景。
unique_ptr的实现原理:在普通使用基础上简单粗暴的禁止了拷贝赋值。常用的使用方法通常有两种,如下:
class A
{
public:
A(int a = 0, int b = 0)
{
cout << "A(int a, int b)" << endl;
cout << "a = " << a << " " << "b = " << b << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
//正确使用方法一:使用new关键字
unique_ptr<int> sp1(new int(1));
unique_ptr<A> sp2(new A(1, 2));
//正确使用方法二:使用make_unique。这是C++14所提供的
unique_ptr<A> sp3 = make_unique<A>(1, 2);
// 错误使用
//unique_ptr<int> sp4(sp1); 拷贝构造,编译错误
//unique_ptr<int> sp5 = new int(0); 赋值运算符,编译错误
//sp2 = sp1; 赋值运算符,编译错误
return 0;
}
unique_ptr模拟实现如下:
namespace bit
{
template <class T>
class unique_ptr
{
public:
//构造与析构
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{ }
~unique_ptr()
{
if (_ptr)
{
std::cout << "delete _ptr" << std::endl;
delete _ptr;
}
}
//采用C++11方式禁止赋值与拷贝构造
unique_ptr<T>& operator=(unique_ptr<T>& it) = delete;
unique_ptr(unique_ptr<T>& it) = delete;T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
}
有些人对于这块设计可能有疑惑,这里完全没必要禁掉赋值与拷贝构造,实现深拷贝即可解决这种问题。我们要知道,智能指针的设计目的跟迭代器一样,本身资源都不是自己的,只是代为持有,方便访问时修改数据,所以它们拷贝的时候都是希望指向同一个资源,即浅拷贝,如存在it1, it2两个指向同一个容器中数据的迭代器, it1对其修改时it2所指向的数据发生同样变化。智能指针或迭代器不像容器般单独对数据单独做出管理,即不能实现深拷贝。它们只是一个“ 辅助器 ”。
2-4,shared_ptr智能指针
C++11中shared_ptr智能指针完善了unique_ptr不能拷贝的问题,即shared_ptr 允许自由拷贝且还更靠谱、更完全。
shared_ptr的原理:通过引用计数的方式来实现多个 shared_ptr 对象之间共享一份资源。由于发生拷贝问题时多个对象之间的释放会出问题,所以shared_ptr使用引用计数解决多次释放的问题。shared_ptr在其内部给每个资源都维护了一份计数,用来记录该份资源被几个对象共享,当对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
引用计数器支持多个拷贝管理同一个资源,最后一个析构对象释放资源。这里要注意,我们不能使用静态引用计数器来计数。静态引用计数器属于这个类,属于这个类的所有对象,当我们创建不同对象且没有实现拷贝时,这里就会出问题。这里的引用计数器设计需求是每个资源一个引用计数,无论多少个对象管理一个资源都使用一个引用计数器,而不是整个类全部是一个引用计数。
shared_ptr设计的思路是每个对象存一个指向引用计数器的指针。拷贝时引用计数器加一,析构释放资源时引用计数器减一,当引用计数器减到0(根据自己设计需求设计,这可以减到1或其它数释放,原理都是一样的)时说明管理同一份资源的对象已是最后一个,可以释放资源。
namespace bit
{
template <class T>
class shared_ptr
{
public:
//构造与析构//注意:const型指针不能赋值非const指针,因为会导致const指针所指向的数据被改变
//share_ptr(const T* ptr = nullptr)下面初始化失败
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1)) //创建引用计数器
{ }
~shared_ptr()
{
release();
}
//赋值与拷贝构造
shared_ptr<T>& operator=(const shared_ptr<T>& it)
{
//这里不能用类判断,因为这里判断的是_ptr,而不是整个类
//if (*this == it)
if (_ptr != it._ptr)
{
//先判断
release();
//后赋值
_ptr = it._ptr;
_pcount = it._pcount;
(*_pcount)++;
}
return *this;
}
shared_ptr(const shared_ptr<T>& it)
{
_ptr = it._ptr;
_pcount = it._pcount;
(*_pcount)++;
}T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }//获取计数器的计数
int use_count()
{
return *_pcount;
}
//得到智能指针
T* get() const
{
return _ptr;
}
private:
void release()
{
(*_pcount)--;//_ptr与_pcount是同步的,当该释放_ptr时一定到了该释放_pcount
if (*_pcount == 0) //说明是最后一个对象,可以释放空间
{
delete _pcount;
std::cout << "delete _ptr" << std::endl;
delete _ptr;
}
}
T* _ptr; //智能指针
int* _pcount; //引用计数器
};
}
解说:上面的设计中,每当我们创建一个对象时,构造函数会完成引用计数器的创建,析构函数通过release判断释放释放资源。赋值这块,拷贝构造实现简单,它的使用场景只是单纯的对象数据的赋值以及计数器的加一。赋值运算符情况没有那么简单,应用它时需要考虑赋值前指针所指向的空间,这里做出的判断与析构函数的性质一样,都是通过引用计数器计数解决。
运行示例:
int main()
{
bit::shared_ptr<int> p;
//拷贝构造
bit::shared_ptr<int> p1(new int(1));
bit::shared_ptr<int> p2(p1);
//赋值运算符
bit::shared_ptr<int> p3(new int(3));
bit::shared_ptr<int> p4(new int(4));
p3 = p4;
p2 = p3;cout << "p1.get(): " << p1.get() << endl;
cout << "p1.use_count: " << p1.use_count() << endl;
cout << "p3.get(): " << p3.get() << endl;
cout << "p3.use_count: " << p3.use_count() << endl;
return 0;
}
shared_ptr的缺陷
shared_ptr的缺陷是会出现循环引用,导致内存泄漏。具体的我们先看以下代码。
struct ListNode
{
int _val;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;ListNode(int val = 0)
:_val(val)
{}~ListNode()
{
cout << "~ListNode()" << endl;
}
};int main()
{
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);//问题就在于下面n1、n2的互相连接
n1->_next = n2;
n2->_prev = n1;
return 0;
}//自己设计的bit::shared_ptr一样,都是同样效果,这里不再演示
情况1:n1、n2互相连接后的输出
情况2:n1->_next = n2或n2->_prev = n1只执行一个后的输出
分析:我们先来分析没有内存泄漏的情况二。最开始创建对象n1、n2时,引用计数器是1。当只有n1->_next = n2没有n2->_prev = n1时,n1的计数器不变,n2的计数器变为2,当n1、n2的生命周期结束时,n2优先释放空间,计数器变成1,空间不会正常释放;n1释放空间时计数器变成0,空间正常释放,当内部析构释放到 _next 时,_next所指向的n2里面的计数器会减一变成0,开始释放n2所占用的空间,不存在内存泄漏的情况。
我们再来看情况一。当 n1->_next = n2 和 n2->_prev = n1 互相指向时,n1、n2的计数器都会加一变成2。n1结点被n2结点中的 _prev 管着,只有n2的 _prev 析构,n1结点的计数器才会减一,n1结点才会释放空间。这里的_prev是n2的成员,n2中 _prev 的析构必须要n2结点析构时才能正常执行。n2结点的析构被n1结点中的 _next 管着,只有n1的 _next 析构,n2才能正常析构。这里的_next是n1的成员,n1的 _next 正常析构必须要n1结点析构时才能正常执行,这又回到了最开始情况,即无限次循环,最终导致谁也不会释放谁,这就叫做循环引用。
针对上面情况,循环引用可理解为此过程:n2结点释放 -> _prev析构 -> n1结点释放 -> _next析构 -> n2结点释放.......
总的来说,当两个或多个 shared_ptr智能指针对象互相引用关系般的管理将会出现循环引用问题,也就是说即便使用了智能指针还会面临内存泄漏的问题。C++11采用 weak_ptr弱引用智能指针解决 shared_ptr问题。
2-5,weak_ptr弱引用智能指针
weak_ptr 的设计目的是专门解决 shared_ptr引起的循环引用问题,它不支持RAII,不参与资源管理,都是为 shared_ptr而做“ 配角 ”的。
weak_ptr 内部其实也存在着一个引用计数,即弱引用计数。C++中,shared_ptr 和 weak_ptr 底层会使用一个额外的控制块来管理两个引用计数。一个用于记录有多少个 shared_ptr 指向同一个对象,即强引用计数;一个用于记录有多少个 weak_ptr 指向同一个对象,即弱引用计数。强引用计数决定所管理的对象资源是否被释放,一旦减为 0 ,内部就会释放对象资源,无论弱引用计数是否为 0。弱引用计数减为 0 ,表示没有 weak_ptr 指向该对象,底层不会进行任何操作,只有当两个计数器都减为0 时,控制块才会被释放。
shared_ptr 引起循环引用的主要原因是多个对象之间的相关联,赋值运算符会将计数器加一。weak_ptr 的解决方案本质是当发生此种情况赋值时,不增加的强引用计数,只增加弱引用计数,这样一来即便发生多个 sheared_ptr对象相关联也不会引起析构函数的正常调用。
weak_ptr的简单设计封装主要针对是对 shared_ptr拷贝构造和赋值运算符重载的设计。当存在多个shared_ptr对象引用特性般的相关联,内部相关联的智能指针要使用 weak_ptr,它的基础操作会在 shared_ptr上进行。
weak_ptr简单设计
namespace bit
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}/*下面的拷贝构造和赋值运算符依赖于shared_ptr,当发生shared_ptr互相赋值相关联时使用weak_ptr,即将shared_ptr赋值给weak_ptr,没有发生引用计数*/
weak_ptr(const shared_ptr<T>& sp)
{
_ptr = sp.get();
}weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
演示实例
struct ListNode
{
int _val;
//shared_ptr对象内部要使用weak_ptr,避免循环引用
bit::weak_ptr<ListNode> _next;
bit::weak_ptr<ListNode> _prev;ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
bit::shared_ptr<ListNode> n1(new ListNode(10));
bit::shared_ptr<ListNode> n2(new ListNode(20));cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
cout << endl;n1->_next = n2;
n2->_prev = n1;cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
cout << endl;
return 0;
}
输入输出样例一:自己设计命名空间bit下的版本
输入输出样例一:标准库命名空间std下的版本
这里的原理就是,当发生n1->_next = n2和n2->_prev = n1时,weak_ptr的_next和 _prev不会增加n1和n2的引用计数。
2-6,shared_ptr的删除器
shared_ptr智能指针删除器提供了一种灵活的方式来控制如何释放它所指向的对象。因为平常我们释放空间都是依靠关键字 delete释放,若不是new出来的对象强行使用delete可能会导致一系列问题。删除器能够使其使用各种不同的使用场景,从而杜绝此问题。
删除器是一个可调用对象(如函数、函数对象、Lambda 表达式等),它会在 shared_ptr 的引用计数变为零时被调用,以删除或释放它所指向的对象。通过提供自定义的删除器,你可以控制如何删除或释放 shared_ptr 所指向的对象。
我们先观看以下代码问题。
int main()
{
std::shared_ptr<ListNode> p1(new ListNode(10)); //正确应使用delete p1析构
std::shared_ptr<ListNode> p2(new ListNode[10]); //正确应使用delete[] p2析构return 0;
}
shared_ptr默认情况下的释放空间方式是delete _ptr,要想释放不同场景下的空间必须要使用删除器,下面我们模拟实现一种delete[] _ptr 释放空间的删除器。
template <class T>
class Shared_ptr
{
T* _ptr;
int* _count;
function<void(T*)> _del;
public:
Shared_ptr(T* ptr = nullptr, function<void(T*)> del = [](T* ptr){ delete ptr; })
: _ptr(ptr), _del(del)
{
if (_ptr != nullptr) _count = new int(1);
else _count = new int(0);
}
Shared_ptr(Shared_ptr<T>& sptr)
{
if (_ptr != sptr._ptr) *this = sptr;
}
Shared_ptr<T>& operator=(Shared_ptr<T>& sptr)
{
if (_ptr != nullptr)
{
(*_count)--;
if ((*_count) == 0) { _del(_ptr); }
}
_count = sptr._count;
(*_count)++;
_del = sptr._del;
_ptr = sptr._ptr;
return *this;
}
~Shared_ptr()
{
(*_count)--;
if ((*_count) == 0 && _ptr != nullptr) { _del(_ptr); }
}
T* operator->() { return _ptr; }
T& operator*() { return *_ptr; }
};struct ListNode
{
int _val;bit::weak_ptr<ListNode> _next;
bit::weak_ptr<ListNode> _prev;ListNode(int val = 0)
:_val(val)
{}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};//删除器
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};int main()
{
Shared_ptr<ListNode> p1(new ListNode(10));
Shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());
//下面是标准库std作用域下的样例
/*std::shared_ptr<ListNode> p1(new ListNode(10));
std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArray<ListNode>());*/return 0;
}
2-7,内存泄漏
最后我们来说明一下有关内存泄漏的问题。其实,平常在练习代码或部分小型项目,即便存在内存泄漏的问题其实影响也不大,因为一旦当进程结束时,系统会回收该进程下的全部内部资源,包括我们开辟的空间。内存泄漏的危害主要在于长期运行的程序,比如QQ、微信、操作系统、后台服务等等,这些程序一般会长时间执行,一旦发生内存泄漏将会导致响应越来越慢,最终卡死。
手动管理内存较为麻烦,还会可能会引起一系列问题。智能指针的使用可以自动管理内存,减少手动管理的复杂性和出错的可能性,它提供了异常安全性,即使发生异常的情况下也能确保资源得到正确释放,但是智能指针本身也不是万能的,也会有出错的时候,比如 shared_ptr的循环引用,若正确使用智能,基本可以保证程序是正常的。