文章目录
一、智能指针的概念
用关键字new开辟堆内存,需要delete手动释放。这一特性可以会出现:
- 忘记释放
- 代码调整
等问题导致堆内存出现内存泄露的问题。为了解决内存泄露的问题。
最开始想效仿Java中的垃圾回收机制:在后台启动一个守护进程,检测对象的生存周期是否结束,结束则系统释放内存,未结束不释放。
但是 存在缺点:开启守护进程,影响程序执行效率,C/C++主要做服务器,要求高效率,所以不采用Java的垃圾回收机制。
最后 结合堆,栈,对象的特性想出了自主内存回收机制:智能指针。
- 栈:系统开辟,系统释放
- 堆:人为开辟,人为释放
- 对象:生成对象时开辟内存,调用构造;销毁对象时调用析构函数,释放内存。
现在想要实现人为开辟,系统释放,可以让对象中的指针指向开辟的内存,当对象销毁时,系统自动调用析构,析构中释放内存。实现了人为开辟,系统释放。
【1. 智能指针主要思想:】
- 对象有一个指针,指向这块内存。
- 析构函数中实现释放堆内存,对象销毁时,系统自动调用析构函数,释放开辟的内存。
实现了人为开辟,系统释放,即只需要new开辟,不需要自己调用delete。
【2. 智能指针的分类:】
使用系统的智能指针,需要添加头文件:
# include<memory>
根据智能指针对内存的特性,将智能指针进行分类:
【C++98标准库:】
- auto_ptr:内存的所有权唯一,最后只有一个指针指向此块内存。存在所有权失效问题。
【 带标志位的智能指针:】
不属于任何库,保证内存的释放权唯一,最后只有一个指针拥有释放权。存在释放权失效问题。
【 C++11标准库:】
- unique_ptr:只允许一个指针指向该内存,至始至终。存在从代码层面让多个指针指向一块内存,内存被多次释放的问题。
- shared_ptr:通过引用计数的方式实现了多个指针指向同一块内存,强指针。存在强指针互相引用,导致内存泄露的问题。
- weak_ptr:为了解决强指针互相引用的问题,不能单独使用,需要配合强指针一起使用。
C++11标准后已经将auto_ptr摒弃了,即存在但是不建议使用。
下面我们讲解所有智能指针的具体实现原理,因为使用智能指针指向的类型不确定,所以使用模板实现代码。
二、auto_ptr
(一)基本概念
auto_ptr智能指针是C++98标准库中的智能指针。它要求内存的所有权唯一,即当前内存块只允许有一个指针指向,当新指针指向时将旧指针释放对内存块的指向,新指针指向该内存块,获得所有权,如下图:
所以 需要在有新指针指向内存时,进行所有权转移,故实现构造,赋值重载函数需要进行转移处理。
(二)实现原理
使用模板实现auto_ptr模板类,私有成员变量为指针,主要成员函数如下:
【1. 所有权转移函数:】
使用此函数实现内存块的所有权唯一,写在类私有访问限定符下。实现当新指针指向时,旧指针释放对内存块的指向,新指针指向该内存块。
函数流程:
- 定义变量保存旧指针,将旧指针断开。
- 返回变量
代码如下:
T* Release()//新指针获得所有权函数
{
T* temp = mptr;//保存旧指针的指向
mptr = NULL;//断开旧指针指向内存的连接
return temp;//将旧指针原来指向的内存地址返回,让新指针指向
}
当发生拷贝构造或赋值操作时,需要调用此函数。
【2. 拷贝构造函数:】
用旧对象拷贝出一个新对象,故旧指针失效,新指针指向,调用所有权转移函数即可,代码如下:
Auto_ptr(Auto_ptr<T>& rhs)//rhs为已经存在的对象,让新对象指针指向它指向的的内存,它断开指向,保证所有权唯一
{
std::cout<<"Auto_ptr(&rhs)"<<std::endl;
mptr = rhs.Release();
}
【3. 赋值运算符重载函数:】
旧对象将值赋给已经存在的对象:
- 那么已经存在的对象需要先释放原来指向的内存,原来指针指向的内存有多个指针或一个指针指向,多个此时mptr=NULL,一个mptr=地址;delete NULL是正确的,所以可以写为delete mptr
- 调用所有权转移函数
代码如下:
Auto_ptr<T> operator=(Auto_ptr<T>& rhs)//赋值运算符重载
{
if(this != &rhs)
{
std::cout<<"Auto_ptr(operator=)"<<std::endl;
delete mptr;
mptr=rhs.Release();
}
return *this;
}
【4. 重载、*,->运算符:】
智能指针称为称为面向对象的指针,也是指针,应该具备指针的基本功能,所以需要重载这两个运算符:
T* operator->()
{
return mptr;
}
T& operator*()
{
return *mptr;
}
【5. 构造,析构函数:】
构造函数对成员变量指针初始化;析构函数中delete释放指针指向的内存。
Auto_ptr(T* ptr)
:mptr(ptr)
{
std::cout<<"Auto_ptr()"<<std::endl;
}
~Auto_ptr()
{
delete mptr;
}
auto_ptr指针模板类:Auto_ptr源码实现完成,给出主函数和测试类,进行测试:
class Test
{
public:
void show()
{
std::cout<<"Hello"<<std::endl;
}
};
int main()
{
//智能指针应该可以和普通指针的使用一样
//1. 开辟内存int *p = new int(10);
Auto_ptr<int> pa1 = new int(10);
//2. 解引用赋值*p=20
*pa1=20;
std::cout<<"*pa1="<<*pa1<<std::endl;
//3. 指向对象,调用函数如:Test* ptest = new Test();ptest->show();
Auto_ptr<Test>pa2=new Test();
pa2->show();
}
运行结果如下:
实现的Auto_ptr基本普通指针的功能,也实现了人为开辟,系统释放的自主内存回收功能,
(三)缺陷
auto_ptr智能指针需要保证所有权唯一,这种处理方式,会带来问题,如:
//4.缺陷,智能指针提前失效问题
Auto_ptr<int> pa3 = new int(10);
Auto_ptr<int> pa4 = pa3;//此时pa3指针已经失效为NULL
*pa4=30;//成功,因为此时它已经获得了内存的所有权
*pa3=40;//失败,此时pa3为NULL,对保留区操作,崩溃
return 0;
只允许有一个指针指向内存,那么此时pa3的指针指向空,对其操作,是对保留区操作,程序崩溃。
所以缺陷就是:因为内存所有权的转移,导致智能指针提前失效,即指针提前被置为NULL,导致无法操作。
所以C++11标准后,auto_ptr指针不建议使用,被摒弃。
三、带标志位的智能指针
(一)基本概念
这个智能指针不是C++库中的指针,称为带标志位的智能指针。它的思想是: 所有权不唯一, 释放权唯一。
允许多个指针指向同一个内存,但是只有一个指针拥有对内存的释放权,所以我们可以使用一个标识flag标志这个指针是否具有对内存的释放权:
- flag标识释放权,true有释放权。
- false没有。只有为true才可以进行内存释放。
那么当有新指针指向内存时,即存在拷贝构造,赋值运算符重载函数被调用时,它们都是用自身的值来创建或赋值一个相同值的对象,所以:
- 如果旧指针有释放权,则转移的新指针有。
- 如果没有,那么转移的新指针也没有。
总结来说,当新指针被指向内存时:
- 旧指针将其释放权标识(不管旧指针有没有转移权),直接赋给新指针即可。
- 将旧智能指针的释放权标识置为false。
如下图:
代码表示:
new.flag=old.flag;
old.flag=false;
这样解决了auto_ptr指针所有权失效的问题。
(二)实现原理
设计Smart_ptr智能指针类时,私有成员有指针,标志位flag,主要成员函数为:
【1. 构造,析构:】
- 构造函数进行指针和标志位的初始化。
- 析构函数:只有拥有释放权的对象结束时才释放内存,即flag为true时,其他对象,将指针置为空即可。
代码:
Smart_ptr(T* ptr)
:mptr(ptr),flag(true)
{
std::cout<<"Smart_ptr()"<<std::endl;
}
~Smart_ptr()
{
if(flag)//判断是否是最后一个指针
{
delete mptr;
std::cout<<"~Smart_ptr()"<<std::endl;
}
mptr = NULL;
}
【2. 拷贝构造:】
旧对象生成新对象:
- 将指针赋给新对象的指针。
- 旧对象进行释放权转移,转移给新对象的指针上,自己为false。
Smart_ptr(Smart_ptr<T>& rhs)
{
mptr = rhs.mptr;
flag = rhs.flag;//旧指针将自己的释放权给新指针。
rhs.flag = false;//取消旧指针释放权
}
【3. 赋值运算符重载:】
用旧对象对另一个旧对象的指针进行赋值:
- 被赋值的对象对原来指向的内存取消释放权,调用析构。
- 指向新内存块。
- 旧指针将自己的释放权给新指针。
- 取消旧指针释放权。
代码:
Smart_ptr<T>& operator=(Smart_ptr<T>& rhs)
{
if(this != &rhs)
{
this->~Smart_ptr();//对原来指向的内存取消释放权,调用析构
mptr = rhs.mptr;//指向新内存块
flag = rhs.flag;//旧指针将自己的释放权给新指针。
rhs.flag = false;//取消旧指针释放权
}
return *this;
}
带有标志位的Smart_ptr模板类Smart_ptr源码设计完成,给出主函数测试:
int main()
{
Smart_ptr<int> s1 = new int(10);
*s1=20;
std::cout<<*s1<<std::endl;
Smart_ptr<int> s2 = s1;
*s2=30;
*s1=40;//解决了auto_ptr所有权失效的问题
}
我们可以看调试代码查看逻辑是否正确:
运行结果:
(三)缺陷
带标识的智能指针需要保证释放权唯一,这种处理机制,如果使用不当,会出现缺陷。如下面这种情况,程序就会出现问题:
所以缺陷就是:因为内存释放权的转移,导致智能指针提前失效,即指针指向的空间被提前释放,无法对指针操作。
四、unique_ptr
(一)基本概念
前面两个指针都会存在因为权限转移导致程序出错的情况,为了解决这种问题,设计不允许权限转移的指针,这就是unique_ptr指针。
unique_ptr: 要求至始至终内存所有权唯一,不允许权限转移(禁止)。只有一个对象指向一块内存,在设计点就限制了。和auto_ptr是有区别的,auto_ptr允许权限转移,转移后只有一个对象指向内存。
在拷贝构造,赋值运算符重载时会产生权限转移,将这两个函数写到私有下,就不会出现权限转移的情况。
(二)实现原理
unique_ptr智能指针的实现,相对比较简单,只需要将拷贝构造,赋值运算符重载函数的声明写到私有下即可,那么代码如下:
# include<iostream>
//3.前两个智能指针都是因为权限转移出现的问题,现在设计一个不允许转移权限的智能指针,禁止多个指针指向同一块内存
//只有一个指针指向这块内存,将拷贝构造函数赋值运算符写为私有即可,这就是unique_ptr
template<typename T>
class Unique_ptr
{
public:
Unique_ptr(T* ptr)
:mptr(ptr)
{
std::cout<<"Unique_ptr()"<<std::endl;
}
T* operator->()
{
return mptr;
}
T& operator*()
{
return *mptr;
}
~Unique_ptr()
{
delete mptr;
std::cout<<"~Unique_ptr()"<<std::endl;
}
private:
Unique_ptr(Unique_ptr<T>& rhs);//私有拷贝构造
Unique_ptr<T>& operator=(Unique_ptr<T>& rhs);//私有赋值
T* mptr;
};
int main()
{
Unique_ptr<int> u1=new int(10);
*u1=20;
//Unique_ptr<int> u2=u1;//禁止
std::cout<<*u1<<std::endl;
}
运行结果:
(三)缺陷
unique_ptr虽然在设计层面,不允许多个智能指针指向同一块内存,但是可以通过代码,让多个智能指针指向同一个内存,那么就会出现一块内存被释放多次的问题。 如:
*int* a=new int;
Unique_ptr<int> u2(a);
Unique_ptr<int> u3(a);//程序崩溃,a内存被释放多次
此时u2和u3都指向a内存块,a内存块被释放多次,程序崩溃。
所以可见:让一个指针指向一个内存块的设计是不现实的,需要设计出允许多个指针指向同一块内存,且不出现权限转移问题的智能指针。
五、shared_ptr
(一)基本概念
需要实现:
- 多个指针指向同一块内存。
- 不采取权限转移的方法。
我们可以采取写时拷贝中使用的引用计数的办法实现,即记录每一块内存被指向的次数。
这就是我们最常用的指针,shared_ptr,称为带引用计数的智能指针,或称为强智能指针。思想:允许多个智能指针对象指向一块内存,最后一个对象释放该堆内存,采取引用计数实现。
(二)实现原理
概念,分析:
- 需要用一个数据结构保存内存地址和计数信息,所以设计一个管理数据结构的类。
- 数据结构中的每一行记录一个内存块的地址和指向内存的指针个数,可以用结点类表示。
- 智能指针类,用来实现智能指针的操作。
【1. 数据结构的设计】
采取引用计数办法记录内存块被指向的次数,那么就需要使用数据结构来保存:
- 每一块内存的地址
- 每一个内存地址的指针计数,即当前有几个指针指向这块内存。
我们可以使用数组,map等结构实现,采取数组思想实现,如:
那么该结构体的每一行就是结点,提供管理类对该结构体进行操作,智能指针每指向或释放一块内存,在此结构体上进行信息的更新,所以需要和管理类进行通信。
【2. 实现智能指针的类间关系:】
C++类间通信,提供接口实现即可。实现shared_ptr智能指针,需要设计三个类,三个类提供数据结构,接口实现通信。那么三个类的作用是:
- 结点类:负责初始化结构体每一行的信息,将地址置为NULL,计数初始化为0。
- 管理结构体类:负责开辟Node[x]大小的结构体,对结构体进行地址,计数添加,计数减少,查找,获得计数等管理操作。
- 智能指针类:实现指针的基本操作,当指针开辟内存,释放内存时需要调用管理结构体类的函数。
那么三个类的关系为:
从上图可以看出,核心是实现结构体管理类,因为智能指针的函数基本都需要调用管理类的方法。
【3. 结点类】
将结点类写为结构体管理类的私有成员,结点类中:
- 成员变量:内存地址,计数。
- 成员函数:构造函数:对两个成员变量进行初始化。
结点类代码如下:
class Node//一条记录
{
public:
Node(void* a = NULL,int c = 0)
:addr(a),count(c)
{}
void* addr;
int count;
};
【4. 结构体管理类:】
结构体管理类进行结构体大小的申请,对其进行操作,因为操作的结构体类型是固定的,地址是void*,计数为int,所以不需要写为模板类:
成员变量:
- 开辟一定数量的结点,如开辟10个结点大小的结构体,Node total[10];表示可以记录10个内存块的信息。
- 结构体可以提供下标来访问,所以定义变量current,代表现在里面的有效元素个数,如current=1,表示记录了一个内存信息,因为数组从0开始,所以下一个内存信息放入current下标1中。
成员函数:
-
查找地址函数,写为私有的,不允许外界使用,遍历查找结构体,判断此内存块地址是否存在。
//查找当前数据结构中是否存在该地址 int find(void* ptr) { for(int i = 0;i<10;i++) { if(ptr == total[i].addr) return i; } return -1; }
-
获取内存块的计数情况:先查找,有返回对应结点的count即可,没有返回-1。
//3.获得内存块的引用计数 int getc(void* ptr) { int i = find(ptr); if(i < 0) { return -1; } else { return total[i].count; } }
-
增加函数。
-
删除函数。
【4-1 成员函数增加函数的实现:】
当智能指针开辟内存块或拷贝构造,赋值操作,就要调用此函数,将内存块的地址信息更新到结构体中。
增加函数主要实现流程:
- 查找地址内存块是否存在,存在表示指针不是第一次指向,直接将对应的**内存块计数++**即可。
- 不存在,将地址放入current下标的结构体,计数置为1,current++。
那么代码如下:
//1.添加一个内存块信息或计数
void add(void* ptr)
{
int i = find(ptr);//查找该内存块是否存在
if(i != -1)//表示存在
{
total[i].count++;//对应内存块计数++即可
}
else//不存在
{
total[current].addr = ptr;//将地址放入结构体
total[current].count = 1;//计数为1
current++;
}
}
【4-3 成员函数删除函数的实现:】
当智能指针对象结束时,调用析构函数,需要释放内存,或将引用计数- -,调用此函数,将计数- -,此函数实现流程:
- 判断地址内存块是否存在,不存在抛出异常
- 存在,如果此时计数大于0,计数- -
注意: 当计数为0时,不允许进行- -操作,不清空那一行的信息,所以结构体可以保存的内存块地址信息,如果Node[10],那么就只能记录10块内存的地址信息。
代码如下:
//2. 减少计数,当计数变为0,表示最后一个指针对此内存块指向结束,我们并不清空地址记录,只是不允许减了
//所以结构体只能保存10块内存地址信息
void del(void* ptr)
{
int i = find(ptr);
if(i < 0)//表示没找到,抛出异常
{
throw std::exception("ptr error");
}
else
{
if(total[i].count>0)
{
total[i].count--;
}
}
}
【5. 智能指针Shared_ptr类:】
因为不知道指向什么类型的变量,所以写为模板类。实现指针的基本操作,调用结构体管理类函数:
成员变量:
- 静态结构体管理类对象,所有对象都可以看见,方便进行管理类函数的调用,需要在类外初始化。
- 指针
private:
T* mptr;
static Manage manage;
};
template<typename T>
Manage Shared_ptr<T>::manage;
主要成员函数:
【1. 构造函数:】
- 得到内存地址。
- 指向一块内存,调用管理类添加函数将内存地址添加到管理结构体中。
代码为:
Shared_ptr(T* ptr = NULL)
:mptr(ptr)
{
manage.add(mptr);
}
【2. 拷贝构造函数:】
- 获取旧对象的指针
- 调用管理类添加函数将内存地址添加到管理结构体中。
Shared_ptr(Shared_ptr<T>& rhs)
{
mptr = rhs.mptr;
manage.add(mptr);
}
【 3. 赋值重载函数:】
- 调用析构函数处理指针原来指向的内存块;
- 得到新指针的地址。
- 调用管理类添加函数将内存地址添加到管理结构体中。
Shared_ptr<T> operator=(Shared_ptr<T>& rhs)//赋值,先析构旧内存块,再指向新的
{
if(this != &rhs)
{
this->~Shared_ptr();//处理旧指针
mptr=rhs.mptr;
manage.add(mptr);
}
return *this;
}
【 4. 析构函数:】
- 调用管理类删除函数,减少内存地址计数;
- 调用管理类获取计数函数,得到当前内存块的计数,如果此时内存计数为0,表示当前指针为最后一个指针,delete释放内存块。
- 如果不为0,表示还有其他指针指向该内存,置空当前指针即可。
~Shared_ptr()
{
manage.del(mptr);//减少内存地址计数
if(manage.getc(mptr) == 0)//如果此时内存计数为0,表示当前指针为最后一个指针,释放内存块
{
delete mptr;
}
else//否则,置为NULL即可。
{
mptr=NULL;
}
}
整合代码Shared_ptr源码,主函数测试:
int main()
{
int* a = new int(10);
Shared_ptr<int> s1(a);
Shared_ptr<int> s2(a);
Shared_ptr<int> s3(a);
Shared_ptr<int> s4 = new int(20);
Shared_ptr<int> s5=s4;
s1=s5;
s5.showma();
}
主函数操作:s1,s2,s3指向内存块a,s4申请新内存块,通过拷贝构造s5指向新内存块,通过赋值运算符重载s1指向新内存块,所以第一块内存现在有2个指针指向,第二块3个指针。
运行结果验证如下:
和我们分析的一样。
(三)缺陷
shared_ptr是强指针,和引用计数具有强关联性,只要存在指针指向,引用计数就++。这就会导致程序出现问题, 如下面一段代码:
class B;
class A
{
public:
A()
{
std::cout<<"A()"<<std::endl;
};
~A()
{
std::cout<<"~A()"<<std::endl;
}
Shared_ptr<B> spa;//自己实现的shared_ptr
};
class B
{
public:
B()
{
std::cout<<"B()"<<std::endl;
};
~B()
{
std::cout<<"~B()"<<std::endl;
}
Shared_ptr<A> spb;
};
int main()
{
//缺陷,强指针内部的互相引用
Shared_ptr<A> pa = new A();//A内部存在强指针
Shared_ptr<B> pb = new B();//B也存在
pa->spa=pb;//互相指向
pb->spb=pa;//析构只会析构pa,此时A生成的堆内存有2个指针指向,但是只会析构pa,内存块不能被释放,因为计数为1
}
按照代码设计,应该输出A,B构造,B,A析构。运行程序:
和我们想的不一样,它并没有释放申请的内存,原因就是发生了强指针相互引用,我们分析上面的代码:
释放时,销毁了pb和pa的指向,此时没有释放内存,因为还有别的指针指向,此时就形成了内部指针的相互指向,无法释放内存块。
核心原因:强指针的互相指向,导致内存无法释放
六、weak_ptr
(一)基本概念
为了解决强智能指针相互引用的问题。如果在互相指向时引用计数不加1,那么就不会出现互相指向无法释放的问题了。需要使用到弱指针weak_ptr:
- 不能单独使用,必须结合强智能指针一起使用。因为弱智能指针的析构不释放内存,导致内存泄露,故需要结合强指针一起使用。
- 在拷贝构造函数,赋值运算符重载函数的实现中不改变计数值,不使用计数计数,只是让指针指向即可。
(二)实现原理
不使用引用计数技术,那么:
【成员变量:】
只需要一个指针即可。
【成员函数:】
-
拷贝构造函数:浅拷贝,直接指向
Weak_ptr(Weak_ptr<T>& rhs) { mptr=rhs.mptr; }
-
弱指针给弱指针赋值的赋值运算符重载:浅拷贝:
Weak_ptr<T> operator= (Weak_ptr<T>& rhs)//弱指针=弱指针赋值 { if(this != &rhs) { mptr=rhs.mptr; } return *this; }
-
强指针给弱指针赋值的运算符重载:不能直接访问Shared_ptr强指针的的私有成员,所以从公有接口获取,强指针提供一个常方法getptr返回自己的地址指向,弱指针直接接收即可。
//Shared_ptr类中使用 T* getptr()const//给弱指针用 { return mptr; } Weak_ptr<T> operator= (const Shared_ptr<T>& rhs)//弱指针=强指针赋值,不能直接访问Shared_ptr的私有成员,所以从公有接口获取 { mptr=rhs.getptr();//常对象只能调用常方法,所以getptr需要用const修饰 return *this; }
-
析构函数:里面什么也不写,故不能单独使用,因为它不释放内存。
~Weak_ptr() {}
将上面强指针缺陷代码中A,B类的成员变量改为弱指针,那么此时代码分析为:
整合代码 Weak_ptr源码 程序运行如下:
可以成功释放A,B内存,弱指针解决了强指针相互指向的问题。
(三)缺陷
必须和强指针配合使用,不能单独使用,因为只开辟了内存,析构中没有释放。
加油哦!🥘。