C++智能指针
前言
一、智能指针的使用场景
二、RAII和智能指针的设计思路
三、C++标准库智能指针的使用
四、智能指针的原理
4.1 auto_ptr模拟实现
4.2 unique_ptr模拟实现
4.4 定制删除器
4.6 weak_ptr介绍
4.7 weak_ptr模拟实现
五、C++11和boost中智能指针的关系
六、内存泄漏
6.1 什么是内存泄漏,内存泄漏的危害
6.2 如何检测内存泄漏
6.3 如何避免内存泄漏
总结
前言
本篇文章来讲智能指针,智能指针的使用在实际中还是非常多的,也是避免内存泄漏一个非常有用的手段,那我们就正式开始
一、智能指针的使用场景
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}void Func()
{int* array1 = new int[10];int* array2 = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}delete[] array1;delete[] array2;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}
虽然异常的重新抛出可以解决这里的问题,但是现在只有两个资源,第一个new抛异常不需要释放,第二个new抛异常要释放第一个new,Divide抛异常要释放两个new,那如果还有更多申请的资源呢?就要一直套try/catch,代码非常难看,在这种场景下,就可以使用智能指针来解决。
二、RAII和智能指针的设计思路
template<class T>
class SmartPtr
{
public:// RAIISmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针的⾏为,⽅便访问资源T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";}else{return (double)a / (double)b;}
}void Func()
{// 这⾥使⽤RAII的智能指针类管理new出来的数组以后,程序简单多了SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i;}int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (const exception& e){cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}
根据栈展开查找catch子句的规则,当前throw不在try/catch语句内,就到上一层去查找,在这个过程中,沿着调用链创建的对象都将销毁,所以到了main函数中匹配catch子句时,func函数结束时,sp1和sp2对象都会调用析构函数释放资源,我们就不需要再担心这种问题了。
智能指针的拷贝构造我们没有写,如果现在要拷贝的话就是浅拷贝,一块空间析构两次会出问题,那我们要手动深拷贝吗?其实并不是,智能指针的拷贝本意上就是要让它们指向同一块空间,那析构的问题怎么解决?我们来看看标准库的智能指针是如何实现的
三、C++标准库智能指针的使用
struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给 拷贝对象,这是一个非常糟糕的设计,因为这会导致被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。
int main()
{auto_ptr<Date> ap1(new Date);auto_ptr<Date> ap2(ap1);// 不能访问ap1->_month++;return 0;
}
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用它。
int main()
{unique_ptr<Date> up1(new Date);// 拷贝构造和拷贝赋值都被禁了//unique_ptr<Date> up2(up1);// 移动构造可以unique_ptr<Date> up3(move(up1));return 0;
}
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的,use_count这个函数还可以看到当前的引用计数是多少,shared_ptr非常全能。但是shared_ptr在一个场景下是会有问题的,就是循环应用,这种情况就需要用weak_ptr来解决,在模拟实现的时候再讲这个场景以及weak_ptr这个智能指针。
int main()
{shared_ptr<Date> sp1(new Date);shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl; // 3shared_ptr<Date> sp4(move(sp1));return 0;
}
四、智能指针的原理
4.1 auto_ptr模拟实现
auto_ptr是需要把被拷贝对象的资源转移给拷贝对象,然后把被拷贝对象置空即可,赋值就先要把自己管理的资源释放了,再去指向其他的资源,如果不先释放再指向就内存泄漏了。
namespace hx
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){if (_ptr)delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
4.2 unique_ptr模拟实现
拷贝构造和拷贝赋值直接禁掉,然后实现一个移动构造和移动赋值,也是直接转移资源即可。
namespace hx
{template<class T>class unique_ptr{public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}unique_ptr(unique_ptr<T>&& up):_ptr(up._ptr){up._ptr = nullptr;}unique_ptr<T>& operator=(unique_ptr<T>&& up){delete _ptr;_ptr = up._ptr;up._ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}
4.3 shared_ptr模拟实现
关于shared_ptr我们要先理解引用计数的设计,每一份资源都有一个配套的引用计数,只要有一个shared_ptr指管着这块资源,引用计数就++,shared_ptr出了作用域要去调用析构,引用计数先--,然后看引用计数是否是0,如果引用计数是0了,那就说明没有其它个shared_ptr指管着这块资源了,那我析构的时候就可以把这块空间释放了,但是如果引用计数不是0,就说明还有其它的shared_ptr指管着这块资源,我析构的时候就不能释放。
那我们来思考一下,我们自己设计的时候引用计数应该怎样设计呢?直接写成成员变量吗?显然不是,这样设计就是每个对象都有一个引用计数,就比如上面的sp1和sp2,我们想让它们共用一个引用计数,引用计数是2,而不是sp1和sp2都有一个引用计数。那设计成静态的?也不行,如果设计成静态的那就是所有对象都用这一个引用计数,显然也是不合理的,那上面的sp1和sp3肯定不能用同一个引用计数。所以引用计数真正实现的方法是要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。
在赋值时判断是否是自己给自己赋值和之前不太一样,例如上面的例子,sp1 = sp1,sp1 = sp2,都叫自己给自己赋值,那在这里判断的方式就是看资源的地址相不相同,相同就是自己给自己赋值。而且也需要把删除的逻辑再判断一遍,如果当前指向的资源引用计数--完不是0,那我不需要释放,直接指向其它资源即可,如果当前指向的资源引用计数--完是0,那我就需要先释放,再指向其它资源。
同时我们也提供一个可以拿到指向的资源和引用计数的函数。
namespace hx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;};
}
4.4 定制删除器
到这里大家会发现,我们的智能指针析构函数中都是对应的new单个的情况,如果是new[]那析构时就会崩溃。所以智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本,可以直接用。
int main()
{unique_ptr<Date[]> up(new Date[10]);shared_ptr<Date[]> sp(new Date[10]);return 0;
}
如果要传可调用对象的话,要注意的是:unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的。unique_ptr要显示实例化传类型给模版,shared_ptr只需要传对象过去让编译器自动推演类型。
下面来看一个示例:
template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{
public:void operator()(T* ptr){delete[] ptr;}
};class Fclose
{
public:void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};
int main()
{unique_ptr<Date, DeleteArray<Date>> up1(new Date[5]);shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());unique_ptr<Date, void(*)(Date*)> up2(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);auto delArrOBJ = [](Date* ptr) {delete[] ptr; };unique_ptr<Date, decltype(delArrOBJ)> up3(new Date[5], delArrOBJ);shared_ptr<Date> sp3(new Date[5], delArrOBJ);shared_ptr<FILE> sp4(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;fclose(ptr);});return 0;
}
template<class D>
shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1))
{}
这个del是要在析构函数中用的,但是析构函数没有模版参数D,用不了,那就只能再来一个成员变量,先把del给保存下来,但是问题是,D不是类的模版参数,成员变量照样用不了D,那怎么办呢?D虽然用不了,但是我们知道del是一个可调用对象,那用包装器包装一下不就好了吗。
private:T* _ptr;int* _pcount;function<void(T*)> _del;
但是如果没有传定制删除器,那就匹配只有一个参数的构造函数,这个构造函数没有对_del初始化,那析构就出问题,所以我们可以给上一个缺省值,直接用一个lambda,在初始化列表阶段用,这样无论是new还是new[]就都解决了。
private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
如果当我们写出shared_ptr<Date> sp = new Date(2024, 9, 11);这样的代码时,是不可以的,为了防止普通指针隐式类型转换成智能指针对象,所以我们还可以在构造函数前加上explicit来限制这种隐式类型转换。
namespace hx
{template<class T>class shared_ptr{public:explicit shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _del(sp.del){(*_pcount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){if (--(*_pcount) == 0){//delete _ptr;_del(_ptr);delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}~shared_ptr(){if (--(*_pcount) == 0){//delete _ptr;_del(_ptr);delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };};
int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp1 is nullptr" << endl;return 0;
}
4.5 shared_ptr循环引用问题
struct ListNode
{int _data;ListNode* _next;ListNode* _prev~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
这段代码会编译报错,原因就是ListNode*类型的指针和shared_ptr类型之间无法进行互相赋值,把ListNode*改成shared_ptr类型就可以通过编译
struct ListNode
{int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
现在就构成了循环引用问题,我们画图来分析一下循环引用问题
当n1和n2析构后,左边节点的_next管着右边节点,右边节点的_prev管着左边节点,管理两个节点的引用计数是1,紧接着,右边节点什么时候析构呢?右边节点由左边节点的_next管着,左边节点的_next析构,右边节点就析构,那左边节点的_next什么时候析构呢?_next是左边节点的成员,左边节点析构,_next就析构,左边节点什么时候析构呢?左边节点由右边节点的_prev管着,右边节点的_prev析构,左边节点就析构,右边节点的_prev什么时候析构呢?_prev是右边节点的成员,右边节点析构,_prev就析构,右边节点什么时候析构呢?...大家可以看到,这就是一个死循环,引用计数永远无法减到0,节点永远无法析构,那总结一下,当有两个对象,他们的shared_ptr智能指针成员互相指向对方的时候,就构成循环引用,解决方法是:把ListNode结构体中的_next和_prev的类型改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。
struct ListNode
{int _data;weak_ptr<ListNode> _next;weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};int main()
{shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}
4.6 weak_ptr介绍
weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,它不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的一个循环引用导致内存泄漏的问题。weak_ptr只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{shared_ptr<string> sp1(new string("111111"));shared_ptr<string> sp2(sp1);weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;wp = sp1;shared_ptr<string> sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}
最开始的时候wp绑定sp1,绑定的资源没有过期,引用计数就返回绑定的shared_ptr的引用计数2。sp1指向其它资源,wp没有过期,引用计数是1。sp2指向其它资源,wp指向的资源过期了,并且引用计数是0。随后wp绑定到sp1,又通过lock返回了一个shared_ptr,sp1和sp3指向的资源是相同的,wp没有过期,引用计数是2。
4.7 weak_ptr模拟实现
weak_ptr实现无参的构造,以及用shared_ptr拷贝构造weak_ptr,用shared_ptr给weak_ptr赋值
namespace hx
{template<class T>class weak_ptr{public:weak_ptr(){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr;};
}
五、C++11和boost中智能指针的关系
六、内存泄漏
6.1 什么是内存泄漏,内存泄漏的危害
6.2 如何检测内存泄漏
Linux下内存泄漏检测工具:Linux 下几款程序内存泄漏检查工具-CSDN博客
windows下内存泄漏检测工具:针对windows平台上C++内存泄漏检测的软件和方法 - he伟_li - 博客园
6.3 如何避免内存泄漏
总结
智能指针在实际中是用的非常多的,用它来解决问题可以非常有效的避免资源泄漏,也非常方便,不用一直中间层try/catch,利用RAII,出了作用域自动调用析构,那本篇文章到这里就结束了,如果觉得小编写的还不错的,可以给一个三连支持,感谢大家。