C++进阶(9)——智能指针的使用及其原理
目录
智能指针的使用场景分析
重新抛出异常
RAII和智能指针的设计
基本概念
具体使用
智能指针的原理
C++标准库中的各种智能指针
auto_ptr
模拟实现
unique_ptr
模拟实现
引用计数
模拟实现
weak_ptr
模拟实现
其他的使用
在智能指针中的模拟实现
C++11与Boost智能指针的关系
智能指针的使用场景分析
我们在异常这一节中就提到了一个需要重新抛异常的一个示例,这个示例说的是我们在处理代码的过程中除零抛异常导致之前申请的资源没有来得及释放,示例如下。
示例代码:
#include <exception>
#include <iostream>
using namespace std;
double Divide(int a, int b) {if(b == 0) {throw"除零异常!";} else {return (double)a / (double)b;}
}
void func() {int* array1 = new int[10];int* array2 = new int[10];int len, time;cin >> len >> time;cout << Divide(len, time) << endl;cout << "delete[]" << array1 << endl;delete[] array1;cout << "delete[]" << array2 << endl;delete[] array2;
}
int main() {try {func();} catch(exception& e) {cout << e.what() << endl;} catch(...) {cout << "未知异常" << endl;}return 0;
}
在这个代码中,如果用户输入的除数是0的话,那么我们的Divide函数就会抛出除零异常,然后根据我们的调用栈,我们的程序会跳到我们的main函数中,这个时候我们的第一个catch就会捕获异常,但是我们在Func函数中申请的两个数组空间就没有被释放掉,最终导致了内存泄露。
重新抛出异常
这是我们之前章节中的解决方案,也就是在Func函数中捕获来自Divide函数的异常,在catch块中释放申请的资源,然后再将我们原来的异常重新抛出即可。
示例代码:
#include <exception>
#include <iostream>
using namespace std;
double Divide(int a, int b) {if(b == 0) {throw("除零异常!");} 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;delete[] array1;cout << "delete[]" << array2 << endl;delete[] array2;}cout << "delete[]" << array1 << endl;delete[] array1;cout << "delete[]" << array2 << endl;delete[] array2;
}
int main() {try {func();} catch(exception& e) {cout << e.what() << endl;} catch(...) {cout << "未知异常" << endl;}return 0;
}
敲黑板:
这里其实并没有完全解决我们的问题,或者说我们的这个代码有Bug,就是如果我们的在给两个数组申请资源的时候抛异常了呢,这个时候我们就需要套一层捕获释放的逻辑,非常的麻烦,所以这里更好的方式就是使用智能指针。
RAII和智能指针的设计
基本概念
RAII是Resource Acquisition Is Initialization的缩写,他是一种管理资源的类的设计思路,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源的泄露,这里的资源有很多种,比如我们的内存、文件指针、网络连接、互斥锁等等。RAII在获取资源的时候将资源委托给了一个对象,接着控制了对资源的访问,资源在对象的生命周期内始终是保持有效的,最后我们在对象析构的时候就可以释放资源了,这样保障了我们资源的正常释放,避免了资源泄露的问题。
同时我们的智能指针类除了要满足我们的RAII设计原则,我们还要方便我们的资源访问,所以一般的我们的智能指针类还会想和迭代器类一样,重载operator*、operator->和operator[ ]运算符,这样可以方便我们访问我们的资源。
具体使用
这里我们给出我们上面问题的智能指针解决方案:
示例代码:
#include <cstddef>
#include <exception>
#include <iostream>
using namespace std;
template <class T>
class SmartPtr {public:SmartPtr(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) {if(b == 0) {throw "除零异常!";}else {return (double)a / (double)b;}
}
void Func() {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 exception& e) {cout << e.what() << endl;} catch (...) {cout << "未知异常" << endl;}return 0;
}
测试效果如图:
智能指针的原理
我们在实现我们的智能指针的时候,首要考虑的问题是智能指针对象的拷贝问题,其次才是对于运算符的重载等等。
那么我们为什么要解决智能指针的拷贝问题呢?
我们这里还是拿上面的实现智能指针类来举例子,如果我们使用一个对象去拷贝构造另一个对象或是对一个已有的对象进行拷贝赋值操作,那么这个时候我们的程序就会崩溃了。
示例代码:
#include <cstddef>
#include <exception>
#include <iostream>
using namespace std;
template <class T>
class SmartPtr {public:SmartPtr(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;
};int main() {// 拷贝构造SmartPtr<int> sp1(new int[10]);SmartPtr<int> sp2(sp1);// 拷贝赋值SmartPtr<int> sp3(new int[10]);SmartPtr<int> sp4(new int[10]);sp3 = sp4;return 0;
}
测试效果:
敲黑板:
1、我们这里编译器默认生成的构造函数实现的是值拷贝(浅拷贝),这样就会导致我们的sp1和sp2实际上管理的都是同一块空间,也就是最会析构的时候实际上是会析构两次的。
2、我们这里编译器默认生成的拷贝赋值函数实际上实现的也是值拷贝(浅拷贝),这样就会导致我们的sp3和sp4指向的都是sp3管理的空间,最后析构的时候也是会析构两次,并且还会导致sp4管理的空间没有释放。
C++标准库中的各种智能指针
auto_ptr
这是C++98时候设计出来的智能指针,它实现的原理就是在我们拷贝的时候将我们的被拷贝对象的资源的管理权转移给我们的拷贝对象,这实际上是一个十分糟糕的设计,因为这样会导致我们的被拷贝对象悬空,访问报错的问题。在C++设计处理新的智能指针之后,强烈不建议使用auto_ptr。很多公司都是明令禁止使用这个智能指针。
示例代码:
#include <iostream>
#include <memory>
using namespace std;
int main() {auto_ptr<int> sp1(new int(1));auto_ptr<int> sp2(sp1);*sp2 = 2;*sp1 = 2; // 错误return 0;
}
我们这里面由于我们的空间管理权发生了转移,所以我们这里访问之前的空间就会报错了,但是我们想要的并不是这样的效果,而是可以访问。
模拟实现
我们模拟实现auto_ptr的步骤:
1、在实现拷贝构造函数的时候,我们需要用传入对象管理的资源来构造我们的当前对象,然后将我们的当前对象管理的资源的指针置空即可。
2、在实现拷贝赋值函数的时候,我们需要将当前对象管理的资源释放,然后用传入对象的资源填补空缺,最后将我们的传入对象的指针置空即可。
3、实现对于*和->运算符的冲在操作。
实现代码:
#include <iostream>
#include <memory>
using namespace std;
namespace xywl {template <class T>class myauto_ptr {public:myauto_ptr(T* ptr = nullptr) : _ptr(ptr){}~myauto_ptr() {cout << "delete: " << _ptr << endl;delete _ptr;}myauto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) {ap._ptr = nullptr; // 这里是将传入的对象的资源置空,防止多次析构}myauto_ptr& operator=(myauto_ptr<T>& 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设计出来的智能指针,特点就是不支持拷贝,只支持移动。如果是不需要拷贝的场景还是很推荐使用的。
模拟实现
实现步骤:
1、我们这里要做的就是将我们的拷贝构造函数和拷贝赋值函数禁用,实现的方式可以是将这两个函数私有,另一种通用的方式是在这两个函数的后面加上关键是=delete。
2、实现对于*和->的运算符重载操作。
实现代码:
#include <iostream>
#include <memory>
using namespace std;
namespace xywl { template <class T>class myunique_ptr {public:myunique_ptr(T* ptr = nullptr) : _ptr(ptr) {}~myunique_ptr() {cout << "delete: " << _ptr << endl;delete _ptr;}myunique_ptr(const myunique_ptr<T>& up) = delete;myunique_ptr<T>& operator=(const myunique_ptr<T>& up) = delete;myunique_ptr(unique_ptr<T>&& up) : _ptr(up._ptr) {up._ptr = nullptr;}myunique_ptr<T>& operator=(myunique_ptr<T>&& up) {delete _ptr;_ptr = up._ptr;up._ptr = nullptr;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}private:T* _ptr;};
}
敲黑板:
我们这里的实现本质上就是限制了左值的传入,而只能传入右值进行拷贝构造和拷贝赋值操作。
shared_ptr
这个智能指针是C++11设计出来的智能指针,正如它的名字共享指针一样,这个智能指针是支持拷贝也支持移动的,使用的次数也相对多一点,在面试中也是可能作为手撕的结构的。
我们这里实现的时候,引入了一个非常重要的概念,那就是使用引用计数来实现。
引用计数
1、每一个被管理的资源都有一个对应的引用计数,这个引用计数表示的是有多少个对象在管理这个资源。
2、多一个对象来管理这个资源,引用计数就++,当一个对象不管理了或是这个对象析构了那么引用计数就--,当我们的引用计数减到了0之后,我们的资源就可以进行释放了。
示例代码:
#include <algorithm>
#include <iostream>
#include <memory>
using namespace std;
int main() {shared_ptr<int> sp1(new int(1));shared_ptr<int> sp2(sp1);*sp1 = 2;*sp2 = 3;cout << *sp1 << " " << *sp2 << endl;cout << "count: " << sp1.use_count() << endl; // 引用计数shared_ptr<int> sp3(new int(1));shared_ptr<int> sp4(new int(2));sp3 = sp4;cout << *sp3 << " " << *sp4 << endl;cout << "count: " << sp3.use_count() << endl; // 引用计数return 0;
}
测试效果:
模拟实现
我们这里模拟实现的步骤如下:
1、就是在myshared_ptr类中新增加一个int类型的指针来存储count,也就是我们的引用计数了。
2、在实现构造函数的时候我们默认将我们的count设置成1,因为这个时候已经有一个对象在管理这个资源了。
3、拷贝构造函数和之前的实现差不多,将我们的指针赋值之后,还需要将我们的引用计数++。
4、拷贝赋值函数因为需要将当前对象的释放,所以需要将引用计数--,当我们真正见到了0的时候,我们就需要将我们的当前资源释放了,如果没有减到0,我们就要将我们的指针和引用计数一起赋值给当前对象,同时当前对象管理了一个管理了一个资源引用计数++。
5、析构的时候就是将我们的引用计数--,减到0之后就正真释放掉当前资源,没有减到0就只是引用计数--。
6、将我们的*和->运算符进行重载操作。
实现代码:
#include <iostream>
#include <memory>
using namespace std;
namespace xywl { template <class T>class myshared_ptr {public:myshared_ptr(T* ptr = nullptr) : _ptr(ptr), _count(new int(1)) {}~myshared_ptr() {if(--(*_count) == 0) // 减到了0就要释放了{cout << "delete: " << _ptr << endl;delete _ptr; // 释放资源 delete _count; // 释放引用计数}}myshared_ptr(myshared_ptr<T>& sp) :_count(sp._count),_ptr(sp._ptr){*(_count) ++; // 计数++} myshared_ptr<T>& operator=(myshared_ptr<T>& sp) {if(_ptr != sp._ptr) // 这里不能是管理同一个资源,因为没有操作的必要{if(--(*_count) == 0) // 减到0之后就要释放了{cout << "delete: " << _ptr << endl;delete _ptr; // 释放资源delete _count; // 释放引用计数}_ptr = sp._ptr; // 赋值_count = sp._count; // 赋值*(_count)++; // 计数++}return *this;}int use_count() {return *_count;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}private:int* _count; // 引用计数T* _ptr; // 指向要管理的资源};
}
说明一下这里为什么使用的引用计数为什么使用的是指针
我们这里其实是要分情况讨论的,也就是说我们这里要指出其他用法为什么不行。
第一种情况:设置成int类型
我们这里讲成员变量设置成了int类型之后我们的每一个myshared_ptr对象都会有一个自己的引用计数,这个时候我们有多个对象管理一个资源的时候,就会用到不同的引用计数了。
图示:
第二种情况:设置成静态成员变量
这个时候我们的所有的对象共享了一个引用计数,也就导致了管理不同资源的对象也是使用的同一个引用计数。
图示:
第三种情况:设置成int类型的地址
这个就是我们最终选定的方案了,也就是把这个也当成一个资源,和我们的资源是一起开辟一起释放的,这样就可以很好的解决问题了。
图示:
shared_ptr循环引用问题
我们的shared_ptr在绝大多数的情况下都是很适合管理资源的,因为它支持RAII,同时也支持了拷贝。但是在我们的循环引用的情况下就会导致资源没得到释放而产生内存泄露问题的出现,这样的场景是不多见的,一般的场景就是出现在我们的链表节点中了。
示例代码:
#include <iostream>
using namespace std;
struct ListNode {int _data;ListNode* _next;ListNode* _prev;~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来实现。
示例代码:
#include <iostream>
#include <memory>
using namespace std;
struct ListNode {int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;~ListNode() {cout << "~ListNode" << endl;}
};
int main() {shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);cout << "node1_count: " << node1.use_count() << endl;cout << "node2_count: " << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << "node1_count: " << node1.use_count() << endl;cout << "node2_count: " << node2.use_count() << endl;return 0;
}
虽然这个代码是可以编译通过的,但是这个代码会有内存泄露的问题,这里也体现了编译器并不是万能的,这个内存泄露问题实际上是由于循环引用导致的,具体逻辑如下:
1、我们一开始创建两个节点的时候,它们各自的引用计数就是1。
2、node1->_next = node2;==>node2的引用计数增加到了2(被main里面的node2和node1->_next持有(也就是说谁指向这个资源))。
3、node2->_prev = node1;==>node1的引用计数增加到了2(被main里面的node1和node2->_prev持有)。
4、main函数结束之后,我们的局部变量node1和node2出了作用域,它们持有的shared_ptr被销毁。
node1的引用计数从2减到了1。
node2的引用计数从2减到了1。
因为我们的这两个节点最终的计数都是1,没有到0,所以它们的析构函数永远不会被调用,堆上的内存也就不会被释放了,这也就导致了我们的内存泄露。
我们这里再解释一下什么是循环:
1、右边的节点(node2)什么时候释放,左边的节点(node1)的_next管着呢,_next析构了之后,右边的节点就释放了。
2、_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
3、左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
4、_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
至此我们就形成了逻辑循环了,最终导致内存泄露,我们这里还是给出图示方便理解。
图示:
创建两个节点的时候:
互相指向的时候:
node1和node2析构之后:
那么我们应该如何解决这个问题呢?这个时候就要用到我们的下一个需要介绍的智能指针了叫weak_ptr指针。
weak_ptr
我们的这个智能指针不支持我们的RAII,也不支持访问资源,所以我们可以看文档的时候发现我们的weak_ptr构造的时候是不支持绑定到资源的,只支持绑定到shared_ptr,而绑定我们的shared_ptr的时候不增加我们的引用计数,这也就解决了我们的循环引用问题了。
示例代码:
#include <iostream>
#include <memory>
using namespace std;
struct ListNode {int _data;weak_ptr<ListNode> _next; // 这里改成weak_ptrweak_ptr<ListNode> _prev; // 这里也改成weak_ptr~ListNode() {cout << "~ListNode" << endl;}
};
int main() {shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);cout << "node1_count: " << node1.use_count() << endl;cout << "node2_count: " << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << "node1_count: " << node1.use_count() << endl;cout << "node2_count: " << node2.use_count() << endl;return 0;
}
测试效果:
这里符合了我们的预期,指向并没有改变我们的引用计数。
模拟实现
我们这里模拟实现也是非常简单的,具体步骤如下:
1、我们要有一个无参的构造函数。
2、支持实现我们用shared_ptr对象拷贝构造我们的weak_ptr对象,也支持用shared_ptr对象来拷贝赋值我们的weak_ptr对象。
实现代码:
namespace xywl {template <class T>class myweak_ptr {public:myweak_ptr() {}myweak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}myweak_ptr<T>& operator=(const shared_ptr<T>& sp) {_ptr = sp.get();return *this;}private:T* _ptr = nullptr;};
}
敲黑板:
我们这里要注意的是我们这里实现的shared_ptr和weak_ptr都只是以最简洁的方式实现的,我们这里还有一些其他的很多功能没有实现(比如我们的shar_ptr中的lock()函数,这个函数在实际使用的时候还是很重要的),我们接下来就简单的使用一下。
其他的使用
lock()函数:获取管理资源的shared_ptr,存在就返回不存在就返回空对象。
expired()函数:检查指向的资源是不是过期了。
use_count()函数:获取sared_ptr的引用计数。
使用示例:
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
using namespace std;int main() {shared_ptr<string> sp1(new string("hello"));shared_ptr<string> sp2(sp1);weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;cout << "******************" << endl;// 这里sp1和sp2都指向了其他的资源,也就表示我们的weak_ptr过期了sp1 = make_shared<string>("world");cout << wp.expired() << endl;cout << wp.use_count() << endl;cout << "******************" << endl;sp2 = make_shared<string>("!");cout << wp.expired() << endl;cout << wp.use_count() << endl;cout << "******************" << endl;wp = sp1;cout << wp.use_count() << endl;auto sp3 = wp.lock();cout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "xywl";cout << *sp1 << endl;cout << *sp2 << endl;cout << "******************" << endl;return 0;
}
测试效果:
shared_ptr的线程安全的问题
我们的shared_ptr的引用计数对象是在堆上面的,如果多个shared_ptr对象在多个线程中,进行shared_ptr拷贝和析构访问我们的引用计数的时候就会有线程安全的问题了,所以我们这里需要对shared_ptr的引用计数进行加锁或是原子操作来保证我们的线程安全。
下面我们给出一个具体的线程安全问题的示例:
示例代码:
#include <iostream>
#include <thread>
using namespace std;struct AA {int _a1 = 0;int _a2 = 0;~AA() {cout << "~AA()" << endl;}
};int main() {xywl::myshared_ptr<AA> p(new AA);const size_t N = 100000;auto func = [&]() {for (int i = 0; i < N; i++) {xywl::myshared_ptr<AA> copy(p);copy->_a1++;copy->_a2++;}};thread t1(func);thread t2(func);t1.join();t2.join();cout << p->_a1 << endl;cout << p->_a2 << endl;cout << p.use_count() << endl;return 0;
}
测试效果:
我们这里可以看到我们的这个代码直接就崩溃了,这就是上面提到的引用计数的线程安全问题。
我这里的解决方案可以是设置引用计数为atomic<int>*或是使用互斥锁也是可以的,其实这里使用前者更加方便,这里我们为了完整性我们都是实现一下:
第一种方式:使用atomic<int>*
实现步骤:
我们这里就是将引用计数改成这个类型就行了,同时要注意的是初始化的时候也要改变:
实现代码:
namespace xywl {template <class T>
class myshared_ptr {
public:myshared_ptr(T* ptr = nullptr) : _ptr(ptr),_count(new std::atomic<int>(1)) {} // 这里需要改变~myshared_ptr() {if (--(*_count) == 0) {cout << "delete: " << _ptr << endl;delete _ptr;delete _count;}}myshared_ptr(myshared_ptr<T>& sp):_count(sp._count), _ptr(sp._ptr) {(*_count)++;}myshared_ptr<T>& operator=(myshared_ptr<T>& sp) {if (_ptr != sp._ptr) {if (--(*_count) == 0) {cout << "delete: " << _ptr << endl;delete _ptr;delete _count;}_ptr = sp._ptr;_count = sp._count;(*_count)++;}return *this;}int use_count() {return *_count;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}
private:// int* _count;atomic<int>* _count; // 修改T* _ptr;
};
}
测试效果:
第二种实现方式:使用互斥锁
实现步骤:
1、我们这里主要是对我们的新增和删除进行临界处理,同时我们这里的互斥锁也是要在堆上的,管理同一个资源访问同一把锁,不同资源访问不同的锁。
2、我们这里为了代码的简洁和易读性,可以将自增(Add())和自减(Release)操作单独出来进行锁处理。
实现代码:
template <class T>class myshared_ptr {public:myshared_ptr(T* ptr = nullptr) : _ptr(ptr), _count(new int(1)) {}~myshared_ptr() {if (--(*_count) == 0) {Release();}}myshared_ptr(myshared_ptr<T>& sp):_count(sp._count), _ptr(sp._ptr) , _mut(sp._mut){Add();}myshared_ptr<T>& operator=(myshared_ptr<T>& sp) {if (_ptr != sp._ptr) {Release();_ptr = sp._ptr;_count = sp._count;Add();}return *this;}int use_count() {return *_count;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}private:void Add() {_mut->lock();(*_count)++;_mut->unlock();}void Release() {_mut->lock();bool flag = false;if(--(*_count) == 0) {delete _ptr;delete _count;flag = true;}_mut->unlock();if(flag) {delete _mut;}}private:int* _count;T* _ptr;mutex* _mut;};
这个代码仅供参考,实际上使用方法一更好。
shared_ptr的定制删除器
我们的智能指针默认使用的是delete来释放资源,这也就意味着如果不是new出来的资源,交给智能指针来管理的话,析构就会出现问题,最终程序就会出现崩溃的情况。所以我们的实现中往往会支持在构造的时候给一个删除器,删除器本质上就是一个可以调用的对象,这个可调用对象中可以实现你想要的释放资源的方式。
比如下面的这个代码就不能使用我们的默认方法:
示例代码:
#include <iostream>
#include <memory>
using namespace std;
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;}
};
int main() {// unique_ptr<Date> up1(new Date[10]); // 报错// shared_ptr<Date> sp1(new Date[10]); // 报错return 0;
}
报错之后其实我们有两种解决方案:
第一种方案:使用指针指针提供好的
因为我们经常会用到new[ ],所以我们的unique_ptr和shared_ptr实现了一个特化版本。
示例代码:
#include <iostream>
using namespace std;
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;}
};
int main() {// unique_ptr<Date> up1(new Date[10]);// shared_ptr<Date> sp1(new Date[10]);unique_ptr<Date[]> up1(new Date[10]);shared_ptr<Date[]> sp1(new Date[10]);return 0;
}
测试效果:
第二种方案:自己实现
我们的C++标准库中提供了shared_ptr中提供了下面的模板:
template<class U, class D>
shared_ptr(U* p, D del);
参数说明:
p:智能指针管理的资源
del:删除器,这里是可调用对象,可以是函数指针、仿函数、lambda表达式以及包装器包装后的可调用对象。
示例代码:
#include <iostream>
#include <cstdio>
using namespace std;
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;}
};
template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{
public:void operator()(T* ptr) const{delete[] ptr;}
};class Fclose
{
public:void operator()(FILE* ptr) const{std::cout << "Fclose:" << ptr << std::endl;if (ptr != nullptr) {fclose(ptr);}}
};int main() {// ----------------------------------------------------// 1. 函数对象 (Functor) 作为删除器,仿函数// ----------------------------------------------------std::unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());std::shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());// ----------------------------------------------------// 2. 函数指针 (Function Pointer) 作为删除器// ----------------------------------------------------std::unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);std::shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// ----------------------------------------------------// 3. Lambda 表达式 作为删除器// ----------------------------------------------------auto delArrOBJ = [](Date* ptr) { delete[] ptr; };std::unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);std::shared_ptr<Date> sp4(new Date[5], delArrOBJ);// ----------------------------------------------------// 4. 实现其他资源管理的删除器// ----------------------------------------------------FILE* file_a = fopen("Test.cpp", "r");if (file_a) {std::shared_ptr<FILE> sp5(file_a, Fclose());}FILE* file_b = fopen("Test.cpp", "r");if (file_b) {std::shared_ptr<FILE> sp6(file_b, [](FILE* ptr) {std::cout << "fclose: " << ptr << std::endl;if (ptr) fclose(ptr);});}return 0;
}
测试效果:
在智能指针中的模拟实现
我们这里可以在我们的shared_ptr中模拟实现一个定制删除器,其实就是给原来的类增加了一个模板参数,然后设置好我们的默认删除器即可。
实现代码:
namespace xywl {// 默认的删除器 (Default Deleter)
template<class T>
struct DefaultDelete {void operator()(T* ptr) const {// 默认使用 delete 释放单个对象delete ptr;}
};template<class T, class D = DefaultDelete<T>>
class shared_ptr {
private:// 释放资源和引用计数的原子操作void ReleaseRef() {// 使用 fetch_sub(1) 实现原子递减,并获取递减前的旧值// 如果旧值是 1,说明递减后新值是 0,这是最后一个引用if (_pcount->fetch_sub(1) == 1) {// 资源释放if (_ptr != nullptr) {std::cout << "delete: " << _ptr << std::endl;// 使用自定义删除器释放资源_del(_ptr); _ptr = nullptr;}// 释放控制块(引用计数器)delete _pcount;_pcount = nullptr;}}public:// 构造函数// 注意:不再需要分配 mutexshared_ptr(T* ptr, D del = D()): _ptr(ptr),// 使用 std::atomic<int> 初始化,初始计数为 1_pcount(ptr ? new std::atomic<int>(1) : new std::atomic<int>(0)),_del(del){// 构造完成后,引用计数是线程安全的}// 析构函数~shared_ptr() {ReleaseRef();}// 拷贝构造函数shared_ptr(const shared_ptr& sp): _ptr(sp._ptr),_pcount(sp._pcount),_del(sp._del) // shared_ptr 拷贝删除器对象{if (_ptr) {// 原子递增引用计数_pcount->fetch_add(1);}}// (为简洁起见,省略赋值运算符和访问运算符,它们会使用相同的原子操作)private:T* _ptr; // 管理的资源std::atomic<int>* _pcount; // 管理资源的引用计数 (原子操作)D _del; // 管理资源的删除器
};}
C++11与Boost智能指针的关系
Boost 库是 C++ 标准化进程的重要参考来源,它提供的大量高质量库,为 C++ 委员会制定新的标准特性提供了可行性验证和实现基础。智能指针就是其中最著名的例子之一。
标准/阶段 | 智能指针 | 关键点 |
C++98 | auto_ptr | C++ 标准中引入的第一个智能指针,但存在严重的所有权转移缺陷(非复制语义)。 |
Boost 库 | scoped_ptr、shared_ptr、weak_ptr 等 | 提供了 C++98 时代实用的、无缺陷的智能指针实现,成为后续标准的基础。 |
C++ TR1 | shared_ptr 等 | 作为技术报告 (TR1) 引入了 shared_ptr 等 Boost 库中的特性,但 TR1 不是正式标准。 |
C++11 | unique_ptr、shared_ptr、weak_ptr | 正式纳入 C++ 标准。这些智能指针的实现原理和接口设计主要参考和借鉴了 Boost 库中的对应实现。 |
如何检测内存泄漏(了解)
linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具
windows下使用第三方工具:windows下的内存泄露检测工具VLD使用