目录
一、智能指针的初步认识
1.1 使用场景
1.2 原理
二、std::auto_ptr
2.1 管理权转移
2.2 auto_ptr的模拟实现
三、std::unique_ptr
4.1 基础设计
4.2 线程安全问题
4.3 定制删除器
五、std::weak_ptr
六、C++11与Boost中智能指针的关系
使用智能指针是解决内存泄露问题的良好手段
int Div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void func()
{int* ptr = new int;//...cout << Div() << endl;//...delete ptr;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
执行上述代码时,若用户输入的除数为0,那么Div()函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func()函数中申请的内存资源没有得到释放
利用异常的重新捕获解决
对于这种情况,可以在func()函数中先对Div()函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出
int Div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void func()
{int* ptr = new int;try{cout << Div() << endl;}catch (...){delete ptr;throw;}delete ptr;
}
int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
但这种方式并完全不可靠,有时可能会疏忽一些异常情况
利用智能指针解决
#include
using namespace std;template
class SmartPtr
{
public:SmartPtr(T* ptr) :_ptr(ptr) {}~SmartPtr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }
private:T* _ptr;
};int Div(){int a, b;cin >> a >> b;if (b == 0) throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPtr sp1(new int);//是否抛异常都会释放SmartPtr sp2(new int);*sp1 = 0;*sp2 = 2;cout << Div() << endl;
}int main()
{try {Func();}catch (exception& e) {cout << e.what() << endl;}return 0;
}
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理
无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放
实现智能指针时需要考虑以下三个方面的问题:
概念说明: RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、互斥量等等)的简单技术
智能指针对象的拷贝问题
对于当前实现的SmartPtr类,若用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃
int main()
{SmartPtr sp1(new int);SmartPtr sp2(sp1); //拷贝构造SmartPtr sp3(new int);SmartPtr sp4(new int);sp3 = sp4; //拷贝赋值return 0;
}
原因如下:
智能指针就是要模拟原生指针的行为,当将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里就应该进行浅拷贝。但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针
auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源只有一个智能指针对象在对其进行管理,同一个资源就不会被多次释放了
#include
using namespace std;
int main()
{std::auto_ptr ap1(new int(1));std::auto_ptr ap2(ap1);*ap2 = 10;//*ap1 = 20; //errorstd::auto_ptr ap3(new int(1));std::auto_ptr ap4(new int(2));ap3 = ap4;cout << *ap3 << endl;//2//cout << *ap4 << endl;//errorreturn 0;
}
但使用管理权转移的方式来解决问题并不优秀。对象的管理权转移后也就意味着,不能再用该对象对原来管理的资源进行访问了,否则程序就会崩溃
使用auto_ptr之前必须先了解其机制,否则程序极易出问题,很多公司也规定禁止使用auto_ptr
namespace bjy
{templateclass auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr) {}~auto_ptr() {if (_ptr != nullptr) {delete _ptr;_ptr = nullptr;}}auto_ptr(auto_ptr& ap) {_ptr = ap._ptr;ap._ptr = nullptr;}auto_ptr& operator=(auto_ptr& ap) {if (this != &ap) {delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }private:T* _ptr;//指向所管理的资源};
}
unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,ji即简单粗暴的防止对智能指针对象进行拷贝,保证资源不会被多次释放
int main()
{std::unique_ptr up1(new int(10));//std::unique_ptr up2(up1); //errorreturn 0;
}
但防拷贝其实也不是一个很好的办法,总有一些场景需要进行拷贝
unique_ptr的模拟实现
namespace bjy
{templateclass unique_ptr{public:unique_ptr(T* ptr = nullptr) :_ptr(ptr) {}~unique_ptr() {if (_ptr != nullptr) {delete _ptr;_ptr = nullptr;}}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }//防拷贝unique_ptr(unique_ptr& ap) = delete;unique_ptr& operator=(unique_ptr& ap) = delete;private:T* _ptr;//指向所管理的资源};
}
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题
通过引用计数的方式就能支持多个对象一起管理某一个资源,即支持了智能指针的拷贝,并且只有当资源对应的引用计数减为0时才会释放资源,保证了同一个资源不会被释放多次
#include
int main()
{std::shared_ptr sp1(new int(1));std::shared_ptr sp2(sp1);*sp1 = 10;*sp2 = 20;std::cout << sp1.use_count() << std::endl; //2std::shared_ptr sp3(new int(1));std::shared_ptr sp4(new int(2));sp3 = sp4;std::cout << *sp3 << std::endl;//2std::cout << sp3.use_count() << std::endl; //2return 0;
}
注意: use_count()成员函数,用于获取当前对象管理的资源对应的引用计数
shared_ptr的模拟实现
namespace bjy
{templateclass shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new size_t(1)) {}~shared_ptr() {if (--(*_pCount) == 0) {if (_ptr != nullptr) {//shared_ptr可能管理的是0地址处的空间delete _ptr;_ptr = nullptr;}delete _pCount;_pCount = nullptr;}}shared_ptr(shared_ptr& sp):_ptr(sp._ptr),_pCount(sp._pCount) {++(*_pCount);}shared_ptr& operator=(shared_ptr& sp) {if (_ptr != sp._ptr){if (--(*_pCount) == 0) {//若引用计数为0,则释放该对象delete _ptr;delete _pCount;}_ptr = sp._ptr;_pCount = sp._pCount;++(*_pCount);}return *this;}size_t GetCount() { return *_pCount; }T& operator*() { return *_ptr; }T* operator->() { return _ptr; };private:T* _ptr;size_t* _pCount;};
}
为什么引用计数需要存放在堆区?
shared_ptr中的引用计数不能单纯的定义成一个整型类型的成员变量,否则每个shared_ptr对象都有各自的引用计数,而当多个对象要管理同一个资源时,这些对象应该用的是同一个引用计数
shared_ptr中的引用计数也不能定义成静态成员变量,因为静态成员变量是所有类型对象共享的,会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数
若将shared_ptr中的引用计数定义成一个指针,当资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,若有其他对象也想要管理这个资源,那么除了需要这个资源的地址之外,还需要引用计数的地址 。此时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数,相当于将各个资源与其对应的引用计数进行了绑定
注意:由于引用计数的内存空间也是在堆上开辟的,因此当资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放
存在问题
当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题
如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁
在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作
加锁解决问题
引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护
#include
using std::mutex;
using std::unique_lock;
namespace bjy
{templateclass shared_ptr{public:shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pCount(new size_t(1)), _pMtx(new mutex) {}~shared_ptr() { ReleaseRef(); }shared_ptr(shared_ptr& sp) :_ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx) {AddRef();}shared_ptr& operator=(shared_ptr& sp) {if (_ptr != sp._ptr) {//管理同一块空间的对象之间不需进行赋值操作ReleaseRef();_ptr = sp._ptr;_pCount = sp._pCount;_pMtx = sp._pMtx;AddRef();}return *this;}size_t GetCount() { return *_pCount; }T& operator*() { return *_ptr; }T* operator->() { return _ptr; };private:void AddRef() {unique_lock lock(*_pMtx);++(*_pCount);}void ReleaseRef()//flag为true表示引用计数已为0,需要删除锁{bool flag = false;{unique_lock lock(*_pMtx);if (--(*_pCount) == 0) //引用计数完成--{if (nullptr != _ptr) {delete _ptr;_ptr = nullptr;}delete _pCount;_pCount = nullptr;flag = true;}}if (flag == true) delete _pMtx;}private:T* _ptr;size_t* _pCount;mutex* _pMtx;};
}
定制删除器的用法
当智能指针对象的生命周期结束时,所有的智能指针默认都是以 delete 的方式将资源释放,但是智能指针并不是只管理以 new 方式申请到的内存空间,智能指针管理的也可能是以 new[] 的方式申请到的空间,或管理的是一个文件指针
#include
struct ListNode
{ListNode* _prev;ListNode* _next;size_t _value;~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main()
{std::shared_ptr sp1(new ListNode[10]);//errstd::shared_ptr sp2(std::fopen("test.cpp","w"));//errreturn 0;
}
以 new[] 的方式申请到的内存空间必须以 delete[] 的方式进行释放,而文件指针必须通过调用 fclose 函数进行释放
这时需要定制删除器来控制释放资源的方式,C++标准库中的shared_ptr提供了如下构造函数
template
shared_ptr (U* p, D del);
#include
struct ListNode
{ListNode* _prev;ListNode* _next;size_t _value;~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
template
struct DelArrary
{void operator()(const T* ptr) {std::cout << "delete[]: " << ptr << std::endl;delete[] ptr;}
};
int main()
{std::shared_ptr sp1(new ListNode[10], DelArrary());std::shared_ptr sp2(fopen("test.cpp", "w"), [](FILE* ptr) {std::cout << "fclose: " << ptr << std::endl;fclose(ptr);});return 0;
}
定制删除器的模拟实现
namespace bjy
{//默认的删除器templatestruct Delete{void operator()(const T* ptr) {delete ptr;}};template>class shared_ptr{private:void ReleaseRef(){_pmutex->lock();bool flag = false;if (--(*_pcount) == 0) //将管理的资源对应的引用计数--{if (_ptr != nullptr){cout << "delete: " << _ptr << endl;_del(_ptr); //使用定制删除器释放资源_ptr = nullptr;}delete _pcount;_pcount = nullptr;flag = true;}_pmutex->unlock();if (flag == true){delete _pmutex;}}//...public:shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)), _pmutex(new mutex), _del(del){}//...private:T* _ptr; //管理的资源int* _pcount; //管理的资源对应的引用计数mutex* _pmutex; //管理的资源对应的互斥锁D _del; //管理的资源对应的删除器};
}
template
struct DelArr
{void operator()(const T* ptr) {cout << "delete[]: " << ptr << endl;delete[] ptr;}
};
int main()
{//仿函数示例cl::shared_ptr> sp1(new ListNode[10], DelArr());//lambda示例1cl::shared_ptr> sp2(fopen("test.cpp", "r"), [](FILE* ptr) {cout << "fclose: " << ptr << endl;fclose(ptr);});//lambda示例2auto f = [](FILE* ptr) {cout << "fclose: " << ptr << endl;fclose(ptr);};cl::shared_ptr sp3(fopen("test.cpp", "w"), f);return 0;
}
shared_ptr的循环引用问题
shared_ptr的循环引用问题在一些特定的场景下才会产生。如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。以 new 的方式在堆上构建两个结点,并将这两个结点连接起来,在程序的最后以 delete 的方式释放这两个结点
struct ListNode
{ListNode* _next;ListNode* _prev;int _val;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{ListNode* node1 = new ListNode;ListNode* node2 = new ListNode;node1->_next = node2;node2->_prev = node1;//...delete node1;delete node2;return 0;
}
上述程序没有问题,两个结点都能够正确释放。为了防止程序中途返回或抛异常等原因导致结点未被释放,将这两个结点分别交给两个shared_ptr对象进行管理,这时为了让连接节点的赋值操作能够执行,就需要把ListNode类中的_next和_prev成员变量的类型也改为shared_ptr类型
struct ListNode
{std::shared_ptr _next;std::shared_ptr _prev;size_t _val;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr node1(new ListNode);std::shared_ptr node2(new ListNode);node1->_next = node2;node2->_prev = node1;//...return 0;
}
这时程序运行结束后两个结点都没有被释放,但若是去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用
当以new的方式申请到两个ListNode结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1
将这两个结点连接起来后,资源1当中的_next成员与node2一同管理资源2,资源2中的_prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2
当出了main()函数的作用域后,node1和node2的生命周期都结束了,因此这两个资源对应的引用计数都减到了1
而若连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因
weak_ptr解决循环引用问题
weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,主要是用来解决shared_ptr的循环引用问题的。weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数
将ListNode中的_next和_prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源
struct ListNode
{std::weak_ptr _next;std::weak_ptr _prev;size_t _val = 10;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr node1(new ListNode);std::shared_ptr node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;//...cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}
weak_ptr的模拟实现
namespace bjy
{templateclass weak_ptr{public:weak_ptr(const weak_ptr& wp)noexcept :_ptr(ptr) {}weak_ptr(const shared_ptr& sp)noexcept :_ptr(sp.get()) {}weak_ptr& operator=(const shared_ptr& sp) {_ptr = sp.get();return *this;}T& operator*() { return *_ptr; };T* operator->() { return _ptr; };private:T* _ptr;};
}
利用shared_ptr的成员函数get()获取裸指针
注意:
Boost是为C++标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
Boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库
Boost由于其对跨平台的强调,对标准C++的强调,与编写平台无关。但Boost中也有很多是实验性质的东西,在实际的开发中使用需要谨慎