文章目录
- 一、什么是智能指针
- RAII思想
- std::auto_ptr
- 二、智能指针的拷贝问题(C++98)
- 1.unique_ptr
- 2.shared_ptr
- shared_ptr的问题
- 循环引用的问题
- 3.weak_ptr
- 内存泄漏的危害
一、什么是智能指针
#include<iostream>
using namespace std;
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
// 如果出了异常,会直接跳转到捕获的地方去了。
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
//如果是p1 抛异常需要释放p2和div
//如果是p2 抛异常需要释放p1和div
//如果是div抛异常,需要释放p1和p2
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
RAII思想
RAII(Resource Acquisition Is Initialization)(资源请求即初始化,也就是获取到资源,马上就初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
①不需要显式地释放资源。
②采用这种方式,对象所需的资源在其生命期内始终保持有效
#include<iostream>
using namespace std;
//利用RAII思想设计的delete资源的类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr(){
cout<<"delete"<<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()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
cout<<"释放资源"<<endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
不管是正方func结束,还是抛异常,sp1和sp2都会调用析构函数,释放资源。
但是我们这里就不能通过解引用的方式获取到资源了,这里我们就需要通过运算符重载来获取到我们的资源。
#include<iostream>
using namespace std;
//利用RAII思想设计的delete资源的类
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr(){
cout<<"delete"<<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<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
*sp1=1;
*sp2=2;
cout<<*sp1<<endl;
cout<<*sp2<<endl;
cout<<"释放资源"<<endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
1.利用RAII思想设计delete类
2.重载*和->运算符
std::auto_ptr
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
#include<iostream>
#include <memory>
using namespace std;
class A
{
public:
~A(){
cout<<"~A()"<<endl;
}
private:
int _a1;
int _a2;
};
int main()
{
auto_ptr<A>ap1(new A);
return 0;
}
#include<iostream>
#include <memory>
using namespace std;
class A
{
public:
~A(){
cout<<"~A()"<<endl;
}
//private:
int _a1=0;
int _a2=0;
};
int main()
{
auto_ptr<A>ap1(new A);
ap1->_a1++;
ap1->_a2++;
return 0;
}
二、智能指针的拷贝问题(C++98)
因为智能指针在拷贝的时候,只有一个内置类型的拷贝,所以发生的是浅拷贝,会析构同一块空间,而二次析构内存就会报错,也就是我们的浅拷贝问题
#include<iostream>
#include <memory>
using namespace std;
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
private:
T* _ptr;
};
class A
{
public:
~A(){
cout<<"~A()"<<endl;
}
//private:
int _a1=0;
int _a2=0;
};
int main()
{
SmartPtr<A>sp1(new A);
sp1->_a1++;
sp1->_a2++;
SmartPtr<A>sp2(sp1);
return 0;
}
解决方案:
1.深拷贝?不能,因为违背了功能需求。智能指针只是像指针一样,帮你托管空间。
但是像下面这种情况我们就需要的是浅拷贝。
list<int> lt;
auto it=lt.begin();
为什么迭代器浅拷贝没有问题呢?
因为迭代器并不负责资源的管理!迭代器只是为了封装底层的细节,以统一的方式来遍历资源。它不管迭代器中资源的释放。
#include<iostream>
#include <memory>
using namespace std;
class A
{
public:
~A(){
cout<<"~A()"<<endl;
}
//private:
int _a1=0;
int _a2=0;
};
int main()
{
auto_ptr<A>sp1(new A);
sp1->_a1++;
sp1->_a2++;
auto_ptr<A>sp2(sp1);
return 0;
}
库里面的是没有问题的。
因为库里面的行为就是将sp1的资源转移给了sp2。
是资源管理权转移,不负责任地拷贝,会导致被拷贝对象的悬空问题
手动实现一个auto_ptr,了解上述的C++98的智能指针的底层
#include <iostream>
#include <string>
using namespace std;
namespace zhuyuan
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr(){
cout<<"delete"<<endl;
delete _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;
}
T&operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
class A
{
public:
~A()
{
cout<<"~A()"<<endl;
}
//private:
int _a1=0;
int _a2=0;
};
int main()
{
zhuyuan::auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
cout<<ap1->_a1<<" "<<ap1->_a2<<endl;
zhuyuan::auto_ptr<A> ap2(ap1);
return 0;
}
这个时候如果我们再去调用sp1指针,我们的程序就会发生崩溃。
很多公司明确要求不能使用C++98的智能指针!
boost库
智能指针首先从boost社区中发展起来的。
scoped_ptr
shared_ptr
weak_ptr
C++11
unique_ptr
shared_ptr
weak_ptr
1.unique_ptr
不允许拷贝,只要有拷贝就会报错
void test_unique_ptr()
{
std::unique_ptr<A> up2(new A);
std::unique_ptr<A> up1(up2);
}
手动实现
namespace zhuyuan
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~ unique_ptr(){
cout<<"delete"<<endl;
delete _ptr;
}
//防止拷贝C++11
unique_ptr( unique_ptr<T>&ap)=delete;
unique_ptr<T>& operator=( unique_ptr<T>& ap)=delete;
//防止拷贝C++98
//只声明,不实现
T&operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
void test_unique_ptr()
{
zhuyuan::unique_ptr<A> up2(new A);
zhuyuan::unique_ptr<A> up1(up2);
up1->_a1++;
up1->_a2++;
zhuyuan::unique_ptr<A>up3=up2;
}
unique_ptr:简单粗暴,不让拷贝,只适用于不需要拷贝的一些场景
那如果我们就是需要拷贝呢?
2.shared_ptr
void test_shared_ptr()
{
std::shared_ptr<A> up2(new A);
std::shared_ptr<A> up1(up2);
up1->_a1++;
up1->_a2++;
cout<<up1->_a1<<" "<<up1->_a2<<endl;
shared_ptr<A>up3=up2;
}
也就是说,它们共同管理了同一份数据。也就是三个指针指向的都是up2中的资源。
想要实现的话,我们就需要引用计数。
因为多个对象管理同一份资源,析构的时候就会出问题。但是,如果我们这时引入一个计数,也就是表示当前有多少个对象正在管理这份资源。当对象被析构的时候,我们就将这个计数–,当有新的对象引用这份资源的时候,我们就将这个计数++,当最后一个析构的对象释放时,释放这份资源。也就是说只需要析构一次就可以了。
这里使用静态计数对象是不可以滴。
因为一个资源就需要配一个计数,多个智能指针对象共管。
如果是静态对象的话,是所有资源都是有一个计数,因为静态成员属于整个类,类的所有对象。
每个资源需要管理时,会给构造函数,构造new一个计数。
namespace zhuyuan
{
template<class T>
class shared_ptr
{
public:
void Release()
{
//如果这里的引用计数先--,然后等于0了之后,也就是说没有对象使用这份资源了
//并且我们的要析构的资源的指针不为nullptr的时候
//我们就进行析构。
//同时清空我们这里的计数和我们的指向的资源。
if (--(*_pCount) == 0 && _ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
// RAII思想
shared_ptr(T* ptr)
//使用初始化列表进行初始化
//初始化我们这里的资源
:_ptr(ptr)
//初始化我们的计数
//创建一个类对象的时候,默认计数就是1
, _pCount(new int(1))
{}
~shared_ptr()
{
//调用release进行析构
Release();
}
//拷贝构造
//sp1(sp2)
shared_ptr(const shared_ptr<T>& sp)
//将指针和计数都进行拷贝
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
//将计数++,表示又有一个对象对其进行了引用
(*_pCount)++;
}
//赋值
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
//防止自己给自己赋值
if (_ptr != sp._ptr)
{
//这里我们需要先把我们sp1的对象的资源的计数给释放掉
//然后再将sp1拷贝给sp3
//否则我们sp1所指向的资源的计数就会比真实的多出一份
//就会导致内存泄漏的问题!!
Release();
//将指针赋值
_ptr = sp._ptr;
//将计数也进行赋值
_pCount = sp._pCount;
//将计数++
++(*_pCount);
}
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
//指向的资源的地址
T* _ptr;
//引用计数
int* _pCount;
};
}
class A
{
public:
~A()
{
cout<<"~A()"<<endl;
}
//private:
int _a1=0;
int _a2=0;
};
void test_shared_ptr()
{
zhuyuan::shared_ptr<A> up2(new A);
zhuyuan::shared_ptr<A> up1(up2);
up1->_a1++;
up1->_a2++;
cout<<up1->_a1<<" "<<up1->_a2<<endl;
zhuyuan::shared_ptr<A>up3=up2;
zhuyuan::shared_ptr<int>up4(new int);
zhuyuan::shared_ptr<A> sp5(new A);
zhuyuan::shared_ptr<A> sp6(sp5);
}
重点注意
这里的赋值问题
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止自己给自己赋值
//如果自己给自己赋值,首先就会释放自己的资源,然后再进行赋值
//就会崩溃。
//if (this != &sp)
//采用下面的方法更好,这样我们知道如果我们拷贝的对象和我们被拷贝的对象
//所持有的是同一块空间的话,我们就不会进行拷贝。
//比方说我们的sp1和sp2指向的是同一块资源
//然后我们这里运行的时候,知道了sp1的ptr和sp2的ptr指向的是同一块空间
//就不再会进行拷贝。
if (_ptr != sp._ptr)
{
// 我们这里拿sp1 = sp3距离
//因为我们的sp1之后不再持有原来的资源了,而是指向sp3的资源
//这里我们需要先把我们sp1的对象的资源的计数给释放掉
//然后再将sp3拷贝给sp1
//否则我们原先sp1所指向的资源的计数就会比真实的多出一份
//就会导致内存泄漏的问题!!
Release();
//共同管理新的资源,++计数
//将指针赋值
_ptr = sp._ptr;
//将计数也进行赋值
_pCount = sp._pCount;
//将计数++
++(*_pCount);
}
return *this;
}
void Release()
{
//--被赋值对象的计数,如果是最后一个对象,需要释放资源
if (--(*_pCount) == 0 && _ptr)
{
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr的问题
如果两个智能指针指向同一块资源给多线程用的时候,里面可能会存在线程安全的风险。
循环引用的问题
struct Node
{
int _val;
//自定义类型的对象不能赋值给原生指针,
//我们需要将其转换成智能指针
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
~Node()
{
cout<<"~Node"<<endl;
}
};
void test_shared_ptr2()
{
//不支持隐式类型转换,所以我们需要使用()来进行赋值
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
//循环引用问题
n1->_next=n2;
n2->_prev=n1;
}
int main()
{
test_shared_ptr2();
return 0;
}
没有打印析构的信息,我们的资源没有被正确释放,这就出现了内存泄漏
如何理解这个问题?
当这个函数(test_shared_ptr2)结束了之后n2先析构,n1再析构,因为n2后定义,先析构
然后就结束了。
我们的两个结点都没有正常释放!!
进一步分析
n1的_next管着右边节点的内存块,n2的_prev管着左边结点的内存块。
所以n1的_next析构,右边的结点就释放了(delete),
n2的_next析构,左边的结点就释放了(delete)。
那么_next什么时候释放呢?_prev什么时候释放呢?
右边结点什么时候delete呢?
左边的结点被delete,调用析构函数,_next作为成员才会析构
左边结点什么时候delete呢?
右边的结点被delete,调用析构函数,_prev作为成员才会析构
这就是一个循环等待对方释放的过程!
share_ptr内部无法解决这个问题!所以我们需要使用weak_ptr!
3.weak_ptr
weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源
weak_ptr主要用shared_ptr构造,用来解决shared_ptr的循环引用的问题
可以使用shared_ptr进行构造,为了不增加引用计数,也就是不参与资源管理
struct Node
{
int _val;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
~Node()
{
cout<<"~Node"<<endl;
}
};
void test_shared_ptr2()
{
//不支持隐式类型转换,所以我们需要使用()来进行赋值
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
n1->_next=n2;
n2->_prev=n1;
}
int main()
{
test_shared_ptr2();
return 0;
}
这里当_next和_prev是weak_ptr的时候,它不参与资源的释放管理,但是可以访问和修改资源,不增加计数,不存在循环引用的问题。
可以使用use_count来看一下这里使用weak_ptr前后的计数是多少
void test_shared_ptr2()
{
//不支持隐式类型转换,所以我们需要使用()来进行赋值
std::shared_ptr<Node> n1(new Node);
std::shared_ptr<Node> n2(new Node);
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;
}
内存泄漏的危害
内存泄漏,在进程结束的时候,操作系统都是会回收的,我们为什么要解决内存泄漏?
因为我们很多服务器中的进程都是不会关掉的!除非停服维修!并且有一些进程不能够正常释放