什么是智能指针
来看这段代码:
#include <iostream> using namespace std; int div() { int a, b; cin >> a >> b; if (b == 0) throw "除零错误"; return a / b; } void func() { int* p1 = new int[10]; int* p2 = new int[10]; try { cout << div() << endl; } catch (...) { delete[] p1; delete[] p2; throw; } delete[] p1; delete[] p2; } int main() { try { func(); } catch(const char* str) { cout << str << endl; } return 0; }
func函数中在堆中申请了资源,在func函数结束前也要释放资源,又因为异常的原因,所以抛异常的前面还需要再加一次资源释放,这非常的不方便,代码看起来也很差劲。
并且new本身也会抛异常,如果都抛异常了怎么办,也很容易导致内存泄漏。
所以这里就有了智能指针,这是大体思路:
#include <iostream> using namespace std; template<class T> class SmartPtr { public: //RALL SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { delete[] _ptr; } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T& operator[](size_t pos) { return _ptr[pos]; } private: T* _ptr; }; int div() { int a, b; cin >> a >> b; if (b == 0) throw "除零错误"; return a / b; } void func() { SmartPtr<int> p1(new int[10]); SmartPtr<int> p2(new int[10]); try { cout << div() << endl; } catch (...) { throw; } } int main() { try { func(); } catch(const char* str) { cout << str << endl; } return 0; }
这里的指针出了作用域就会自动释放申请的资源。
智能指针结构
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
像指针一样的结构是第二部分结构,主要重载实现*,->,[],让用起来像指针一样。
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
https://legacy.cplusplus.com/reference/memory/auto_ptr/
#include <iostream> #include <memory> using namespace std; int main() { auto_ptr<int> p1(new int(0)); auto_ptr<int> p2 = p1;//本来应该是两个指针指向同一处空间 return 0; }
通过调试看到,本来应该只想同一处空间的指针因为深浅拷贝析构两次的问题,让第一个原本指向该位置的指针变了方向,直接不管了原本的地址。
这样会导致空指针的问题。
内部差不多是这样的:
auto_ptr(auto_ptr<T>& sp) :_ptr(sp._ptr) { // 管理权转移 sp._ptr = nullptr; }
这也导致很多公司禁止使用auto_ptr。
boost与C++
然后就出现了boost,是C++的一个扩展库,是为C++探路用的,也是准标准库。
boost中产生的最号的两个智能指针是scoped_ptr与shared_ptr/weak_ptr。
C++11中也就添加了unique_ptr与shared_ptr/weak_ptr。
unique_ptr
https://legacy.cplusplus.com/reference/memory/unique_ptr/
这里直接删除了拷贝构造了函数,不让拷贝了。
unique_ptr(auto_ptr<T>& sp)=delete;
主要就是防拷贝。
shared_ptr
这个智能指针可以拷贝,用的是引用计数的方式。
https://legacy.cplusplus.com/reference/memory/shared_ptr/
那么这里的引用计数是如何实现的呢?难道是类的成员当中有一个计数器的吗?
这样是行不通的,假设如果每次多一个指针指向这个空间,那么在此之前的对象都需要++,如果- -就需要让之前的对象都- -,这样是非常麻烦的。
如果让这个计数器变成静态的呢?
这也是行不通的,因为静态成员是属于整个类的所有对象的,假设一个对象释放掉了,那么这个静态成员要不要被释放呢?并且,如果前三个指针指向了一处资源,第四个指针指向的是另一处资源呢?
其实最佳的方式是分配资源的时候多开辟一个计数的地方就可以了。
#pragma once #include<iostream> using namespace std; template<class T> class Shared { public: //RALL Shared(T* ptr) :_ptr(ptr) ,pcount(new int(1)) {} Shared(const Shared<T>& ptr) :_ptr(ptr._ptr) ,pcount(ptr.pcount) { (*pcount)++; } ~Shared() { if (--(*pcount) == 0) { delete _ptr; delete pcount; } } //像指针一样 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } T& operator[](size_t pos) { return _ptr[pos]; } private: T* _ptr; int* pcount; };
这里最麻烦的还是赋值重载,因为如果是两个不相同的对象,但是指向同一处资源,那么这里赋值也不应该成立。
Shared<T>& operator=(const Shared<T>& ptr) { if (this->_ptr != ptr._ptr) { if (--(*pcount) == 0)//如果被赋值的对象是指向的资源是最后一个对象记得释放原来的空间 { delete _ptr; delete pcount; } _ptr = ptr._ptr; pcount = ptr.pcount; (*pcount)++; } return *this; }
智能指针与互斥锁
void test() { int n = 10000; Shared<int> sp1(new int(0)); thread t1([&]() { for (int i = 0; i < n; i++) { Shared<int> sp2(sp1); } }); thread t2([&]() { for (int i = 0; i < n; i++) { Shared<int> sp3(sp1); } }); t1.join(); t2.join(); cout << sp1.ppcount() << endl;//正常来说,结果应该是1 }
这是我运行了四次的结果。
这是因为对于引用计数的地方是进行++,- -,这两个操作不是原子的,有可能进行到一半CPU切换,操作终止,这也导致,了这四种结果:
结果大于一就是- -的时候第一个- - ,第二个也进行了- - ,然后依次将数据放回去,这就导致了少减少一次。
结果小于一就是相反,++的时候有一个线程被屏蔽了数据。
什么是智能指针
来看这段代码:
#include <iostream> using namespace std; int div() { int a, b; cin >> a >> b; if (b == 0) throw "除零错误"; return a / b; } void func() { int* p1 = new int[10]; int* p2 = new int[10]; try { cout << div() << endl; } catch (...) { delete[] p1; delete[] p2; throw; } delete[] p1; delete[] p2; } int main() { try { func(); } catch(const char* str) { cout << str << endl; } return 0; }
func函数中在堆中申请了资源,在func函数结束前也要释放资源,又因为异常的原因,所以抛异常的前面还需要再加一次资源释放,这非常的不方便,代码看起来也很差劲。
并且new本身也会抛异常,如果都抛异常了怎么办,也很容易导致内存泄漏。
所以这里就有了智能指针,这是大体思路:
#include <iostream> using namespace std; template<class T> class SmartPtr { public: //RALL SmartPtr(T* ptr) :_ptr(ptr) {} ~SmartPtr() { delete[] _ptr; } //像指针一样 T& operator*() { return *_ptr; } T* operator->() { return _ptr; } T& operator[](size_t pos) { return _ptr[pos]; } private: T* _ptr; }; int div() { int a, b; cin >> a >> b; if (b == 0) throw "除零错误"; return a / b; } void func() { SmartPtr<int> p1(new int[10]); SmartPtr<int> p2(new int[10]); try { cout << div() << endl; } catch (...) { throw; } } int main() { try { func(); } catch(const char* str) { cout << str << endl; } return 0; }
这里的指针出了作用域就会自动释放申请的资源。
智能指针结构
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
像指针一样的结构是第二部分结构,主要重载实现*,->,[],让用起来像指针一样。
auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。
https://legacy.cplusplus.com/reference/memory/auto_ptr/
#include <iostream> #include <memory> using namespace std; int main() { auto_ptr<int> p1(new int(0)); auto_ptr<int> p2 = p1;//本来应该是两个指针指向同一处空间 return 0; }
通过调试看到,本来应该只想同一处空间的指针因为深浅拷贝析构两次的问题,让第一个原本指向该位置的指针变了方向,直接不管了原本的地址。
这样会导致空指针的问题。
内部差不多是这样的:
auto_ptr(auto_ptr<T>& sp) :_ptr(sp._ptr) { // 管理权转移 sp._ptr = nullptr; }
这也导致很多公司禁止使用auto_ptr。
boost与C++
然后就出现了boost,是C++的一个扩展库,是为C++探路用的,也是准标准库。
boost中产生的最号的两个智能指针是scoped_ptr与shared_ptr/weak_ptr。
C++11中也就添加了unique_ptr与shared_ptr/weak_ptr。
unique_ptr
https://legacy.cplusplus.com/reference/memory/unique_ptr/
这里直接删除了拷贝构造了函数,不让拷贝了。
unique_ptr(auto_ptr<T>& sp)=delete;
主要就是防拷贝。
shared_ptr
这个智能指针可以拷贝,用的是引用计数的方式。
https://legacy.cplusplus.com/reference/memory/shared_ptr/
那么这里的引用计数是如何实现的呢?难道是类的成员当中有一个计数器的吗?
这样是行不通的,假设如果每次多一个指针指向这个空间,那么在此之前的对象都需要++,如果- -就需要让之前的对象都- -,这样是非常麻烦的。
如果让这个计数器变成静态的呢?
这也是行不通的,因为静态成员是属于整个类的所有对象的,假设一个对象释放掉了,那么这个静态成员要不要被释放呢?并且,如果前三个指针指向了一处资源,第四个指针指向的是另一处资源呢?
其实最佳的方式是分配资源的时候多开辟一个计数的地方就可以了。
#pragma once #include<iostream> using namespace std; template<class T> class Shared { public: //RALL Shared(T* ptr) :_ptr(ptr) ,pcount(new int(1)) {} Shared(const Shared<T>& ptr) :_ptr(ptr._ptr) ,pcount(ptr.pcount) { (*pcount)++; } ~Shared() { if (--(*pcount) == 0) { delete _ptr; delete pcount; } } //像指针一样 T* operator->() { return _ptr; } T& operator*() { return *_ptr; } T& operator[](size_t pos) { return _ptr[pos]; } private: T* _ptr; int* pcount; };
这里最麻烦的还是赋值重载,因为如果是两个不相同的对象,但是指向同一处资源,那么这里赋值也不应该成立。
Shared<T>& operator=(const Shared<T>& ptr) { if (this->_ptr != ptr._ptr) { if (--(*pcount) == 0)//如果被赋值的对象是指向的资源是最后一个对象记得释放原来的空间 { delete _ptr; delete pcount; } _ptr = ptr._ptr; pcount = ptr.pcount; (*pcount)++; } return *this; }
智能指针与互斥锁
void test() { int n = 10000; Shared<int> sp1(new int(0)); thread t1([&]() { for (int i = 0; i < n; i++) { Shared<int> sp2(sp1); } }); thread t2([&]() { for (int i = 0; i < n; i++) { Shared<int> sp3(sp1); } }); t1.join(); t2.join(); cout << sp1.ppcount() << endl;//正常来说,结果应该是1 }
这是我运行了四次的结果。
这是因为对于引用计数的地方是进行++,- -,这两个操作不是原子的,有可能进行到一半CPU切换,操作终止,这也导致,了这四种结果:
结果大于一就是- -的时候第一个- - ,第二个也进行了- - ,然后依次将数据放回去,这就导致了少减少一次。
结果小于一就是相反,++的时候有一个线程被屏蔽了数据。