c 四种智能指针
1、裸指针中可能存在的问题
裸指针是指未经类封装的原生指针。在工程项目中,如果使用裸指针不规范或者书写代码逻辑时候不仔细,那么就有可能产生各种错误、异常现象。
(1)malloc出来的空间,如果没有及时释放,就会造成内存泄漏
(2)异常安全问题:如果malloc和free之间存在异常,那么发生异常时,就有可能无法释放空间,造成内存泄漏。这种问题称为“异常安全问题”。
(3)当要delete一个指针指向的空间时,不方便判断该指针指向的是一个数组还是一个单独的对象,所以使用“delete”还是"delete[]"不方便确定。
(4)无法判断一个不为nullptr的指针是否为悬挂指针。
(5)还有可能出现多次释放空一块空间的情况
面对这些问题,gc(垃圾回收机制)是可以有效解决的,但是c 并没有垃圾回收机制,而是提出了一种思想raii。
2、raii思想
rall (resource acquisition ls initialization)是c 之父bjarne stroustrup提出的,翻译为资源获取即初始化。使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字,互斥量,文件句柄等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
(1)raii原理
符合raii的资源一般要经历三个步骤:
获取资源——使用资源——销毁资源
其实在c 中,类对象就很好体现了raii这一原理。具体如下
获取资源(构造)——使用资源——销毁资源(析构)
我们都知道,当我们创建一个类对象的时候,编译器就会自动调用构造函数,当对象出了所在作用域,编译器就会自动调用该类的析构函数。无论是构造函数还是析构函数,都不需要我们自己手动调用,这样就避免了我们忘记初始化,忘记销毁对象的不好的事情发生。
这样看来,避免忘记析构是不是也对应着我们想要避免忘记free/delete指针。
类对象符合raii的举例
class a
{
public:
a()
{
cout << "construct the object" << endl;
}
~a()
{
cout << "destroy the object" << endl;
}
};
void fun()
{
//创建一个对象
a a = a();
//fun调用结束,a出了作用域,自动调用其析构函数
}
int main()
{
fun();
return 0;
}
代码运行结果
(2)整个rali过程总结为四个步骤:
a.设计一个类封装资源
b.在构造函数中初始化
c.在析构函数中执行销毁操作
d.使用时定义一个该类的对象
3、c 内存泄漏
(1)堆内存泄露
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生heap leak(堆内存泄露)。
(2)系统资源泄露
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
从比较简单的层面来看,智能指针是raii(resource acquisition is initialization,资源获取即初始化)机制对普通指针进行的一层封装。这样使得智能指针的行为动作像一个指针,本质上却是一个对象,这样可以方便管理一个对象的生命周期。
c 中总共有四种智能指针:
auto_ptr
unique_ptr
shared_ptr
weak_ptr
其中,auto_ptr 在 c 11已被摒弃,在c 17中已经移除不可用。
总结:智能指针是将原生指针(裸指针)封装成类,通过构造、析构函数来实现资源的即使释放,避免内存泄漏和多次释放空间等非法行为发生(raii特性)。同时还需要重载operator*和operator->来使该类有指针的行为,能够像指针一样使用。
1、auto_ptr的使用
在c98中,auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。
std::auto_ptr ap1(new int);
std::auto_ptr ap2(ap1);
2、auto_ptr的问题
(1)拷贝问题:编译器对auto_ptr默认生成的拷贝构造和复制重载会对指针进行”浅拷贝“,这个时候对指针进行delete时候就会对同一块空间delete多次,就会发生错误。
这个时候就有人说,那我就深拷贝。但事实是,我们需要的就是浅拷贝,想一下指针赋值的场景下,我们的目的就是让多个指针指向相同的空间,即指针内容相同。所以只能指针去赋值或者拷贝也要进行浅拷贝。
3、问题的解决
(1)c 98中对于auto_ptr的拷贝问题,ag真人游戏的解决方案是管理权转移
管理权转移:指针浅拷贝以后,将被赋值指针指向nullptr。
auto_ptr sp1 = new auto_ptr(new int);//管理权在sp1手中
auto_ptr sp2(sp1);//sp1拷贝构造sp2后,sp1中管理的指针指向nullptr了,此时sp2中指针指向sp1创建时开辟的空间。
<注>:这个时候,sp1已经悬空了,sp1指向空,如果这个时候再去对其中的指针解引用,就会触发空指针异常。
4、auto_ptr的模拟实现
template
class my_auto_ptr
{
private:
t* _ptr;
public:
//构造和析构实现raii特性
my_auto_ptr(t* ptr)
:_ptr(ptr)
{
}
~my_auto_ptr()
{
if(_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
//*和->的重载实现指针行为的模拟
t& operator*()
{
return *_ptr;
}
t* operator()
{
return _ptr;
}
//赋值 拷贝实现管理权转移
my_auto_ptr(my_auto_ptr& my_ap)
{
_ptr = my_ap._ptr;
my_ap._ptr = nullptr;
}
my_auto_ptr& operator=(my_auto_ptr& my_ap)
{
if(this != &my_ap)
{
delete _ptr;
_ptr = my_ap._ptr;
my_ap._ptr = nullptr;
}
return *this;
}
};
1、unique_ptr的使用
禁止拷贝(复制重载 拷贝构造),这样就不存在auto_ptr中析构两次的问题。也就不需要管理权转移的方法。
std::unqiue_ptr up1(new int);
<注>:不能进行拷贝构造和赋值操作
2、unique_ptr的模拟实现
tempalte
class my_unique_ptr
{
public:
my_unique_ptr(t* ptr)
:_ptr(ptr)
{
}
~my_unique_ptr()
{
if(_ptr != nullptr)
{
delete _ptr;
_ptr = nullptr;
}
}
//禁用拷贝
my_unique_ptr(my_unique_ptr& my_up) = delete;
my_unique_ptr& operator=(my_unique_ptr& my_up) = delete;
t& operator*()
{
return *_ptr;
}
t* operator()
{
return _ptr;
}
private:
t* _ptr;
};
3、unique_ptr的问题
unique_ptr是一种十分暴力的方式,我直接禁用operator=和拷贝构造,这样就根本无法使用,从根本上解决了拷贝带来的问题。但是太暴力,毕竟有些情况下确实需要拷贝。所以c 库又采用了另一种机制引用计数。
使用引用计数的只能指针叫做shared_ptr,将在后续继续介绍。
1、shared_ptr的使用
使用引用计数机制,统计指向同一块空间的shared_ptr的个数。增加一个,计数器就 1,减少一个计数器就-1。shared_ptr出作用域调用析构函数时,只有当计数器数值为0时,才回去调用delete去释放空间;否则只对计数器进行–操作。
#include
#include
using namespace std;
int main()
{
shared_ptr sp1(new int);
shared_ptr sp2(sp1);
//use_count()可以统计出管理同一块空间的shared_ptr个数
cout << sp2.use_count() << endl;
return 0;
}
2、shared_ptr的模拟实现
template
class my_shared_ptr
{
void addref()
{
(*_count);
}
void releaseref()
{
if(--(*_count) == 0)
{
if(_ptr)
{
delete _ptr;
}
delete _count;
}
}
public:
my_shared_ptr(t* ptr)
:_ptr(ptr)
,_count(new int(1))
{
}
~my_shared_ptr()
{
releaseref()
}
my_shared_ptr(const my_shared_ptr& my_sp)
:_ptr(my_sp._ptr)
,_count(my_sp._count)
{
addref()
}
my_shared_ptr& operator=(const my_shared_ptr& my_sp)
{
//防止自己给自己赋值时候,不能使用
//if(this != & my_sp)这种方式赋值
if(_ptr != my_sp._ptr)
{
releaseref()
_ptr = my_sp._ptr;
_count = my_sp._count;
addref()
}
return *this;
}
t& operator*()
{
return *_ptr;
}
t* operator->()
{
return _ptr;
}
size_t use_count() const
{
return *_count;
}'
const t* get() const
{
return _ptr;
}
private:
t* _ptr;
int* _count;
};
引用计数需要注意的问题:
(1)int _count:
(2)static int _count;
(3)int* _count;
3、shared_ptr的问题
shared_ptr也是解决了指针使用的绝大部分问题,但是还是在有些场景下会出现问题。shared_ptr出现的问题时:循环引用,接下来具体解释一下什么叫循环引用。
(1)循环引用举例1
示例1
struct b;
struct a
{
shared_ptr _b;
};
struct b
{
shared_ptr
死锁的循环等待问题)。
(2)循环引用举例2
在双向链表中,定义_next和_prev两个指针,指向前后节点
struct listnode
{
shared_ptr _next;
shared_ptr _prev;
int _val;
};
int main()
{
shared_ptr node1(new listnode);
shared_ptr node2(new listnode);
node1->_next = node2;
node2->_prev = node1;
}
和情况1相同。node1空间的释放依赖于node2空间释放,node2空间释放依赖于node1空间释放。形成循环引用现象,导致两部分空间都不释放。
1、weak_ptr的使用
weak_ptr并不像前三种智能指针一样单独使用。weak_ptr是为了解决循环引用问题而产生的,所以weak_ptr是配合shared_ptr来使用的。不参与资源的管理,只是来访问内容。
weak_ptr可以使用shared_ptr来构造或者将shared_ptr赋值给weak_ptr
weak_ptr不会改变引用计数
上述两种示例的修改
struct b;
struct a
{
weak_ptr _b;
};
struct b
{
weak_ptr