深入了解C++智能指针的使用
作者:卖寂寞的小男孩 发布时间:2023-04-07 01:41:46
一、C++11智能指针概述
在C++中,动态内存的使用时有一定的风险的,因为它没有垃圾回收机制,很容易导致忘记释放内存的问题,具体体现在异常的处理上。想要释放掉抛异常的程序的一些内存,往往需要多次抛异常,这种处理方式是十分麻烦的。
智能指针的本质就是使用一个对象来接管一段开辟的空间,在该对象在销毁的时候,自动调用析构函数来释放这段内存。
因此智能指针的本质是一个类,类中最主要的对象是一个指针,该类的析构函数就是销毁该指针指向的空间,使用智能指针的本质就是将一个指向动态开辟空间的指针赋给该类中的指针。不过这样的处理过程会有一定的问题,比如浅拷贝等。
C++标准库提供了两种智能指针类型来管理动态对象,由于该对象的行为酷似指针,所以称为智能指针。它们分别是shared_ptr以及unique_ptr。还提供了一个weak_ptr它主要是为了解决shared_ptr的循环引用问题。
shared_ptr允许多个指针指向同一个对象,unique_ptr则独占所指向的对象。
二、C++98中的智能指针
在很早以前,大佬们就已经认识到了内存释放的问题,因此为标准库中增加了一个类:auto_str。它有着和unique_str智能指针类似的功能,它虽然成功的将一个开辟的资源塞给了一个类,不过存在很严重的问题,一些公司已经明令禁止使用它了:
auto_ptr<int> sptr1(new int);
auto_ptr<int> sptr2(sptr1);
*sptr1;
此时如果对sptr1进行解引用操作,会发生报错。要了解报错的原因,我们需要了解它的大致底层原理,作为第一个出现的智能指针,它只是简单执行了将资源转移,以及在析构中加入资源释放,还有一些解引用的运算符重载函数:
template<class T>
class MyAuto
{
private:
T* _ptr;
public:
MyAuto(T* ptr)
:_ptr(ptr)
{}
~MyAuto()
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
MyAuto(MyAuto<T>& Ptr)
{
_ptr = Ptr._ptr;
Ptr._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
可以发现,最终是浅拷贝的锅。因为在进行资源转移的时候,必须将原来的指针置为nullptr,否则析构的时候会析构两次。而将其置为nullptr之后再要使用该指针对其进行解引用就会发生崩溃。
三、C++11中的智能指针
1.unique_ptr
unique_ptr处理上述问题简单而粗暴,即不让进行拷贝操作:
unique_ptr<int> sptr1(new int);
unique_ptr<int> sptr2(sptr1);
直接进行报错处理。
我们也可以猜测出它的实现方式,那就是在拷贝构造和赋值构造的后面加上delete关键字。
template<class T>
class MyUnique
{
private:
T* _ptr;
public:
MyUnique(T* ptr)
:_ptr(ptr)
{}
~MyUnique()
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
MyUnique(MyUnique<T>& Ptr) = delete;
MyUnique& operator=(MyUnique<T>& Ptr) = delete;
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
2.shared_ptr
(1)引用计数器
shared_ptr是使用最多的智能指针,即它可以进行拷贝构造。
每一个智能指针类都有一个专门用于记录该智能指针指向的资源的指针个数的计数器。
当多了一个智能指针指向该资源,则对所有指向该资源的智能指针的计数器进行++操作,当一个智能指针不再指向该资源的时候·,所有指向该资源的智能指针的计数器进行–操作。
当某一个智能指针将其–到0的时候由该智能指针释放该资源。从而解决了不让拷贝的根本问题:防止资源释放多次。
同时智能指针有一个use_count函数来返回计数器的值。
shared_ptr<int> sptr1(new int(1));
shared_ptr<int> sptr2(sptr1);
shared_ptr<int> sptr3(sptr2);
cout << sptr1.use_count() << endl;
cout << sptr2.use_count() << endl;
cout << sptr2.use_count() << endl;
cout << "资源释放成功" << endl;
(2)线程安全
涉及到共享,我们不得不将线程安全问题考虑进来,很显然shared_ptr无论是要管理的资源的使用,还是要指向的该资源对应的计数器的加减操作,都不是线程安全的。
对于要管理的资源来说,如果多个线程不去使用该资源,是不会产生问题的。因此如果需要使用该资源由于代码量的不同位置,C++为了保证性能,希望用户来自己保证它的线程安全,即由用户自己来加锁解锁。
而对于资源计数器来说,只要增加一个智能指针就会++,减少一个就会–,其逻辑明确简单,因此shared_ptr为其加了锁。
template<class T>
class MyShared
{
private:
T* _ptr;
mutex* _pmtx;
int* _pcount;
public:
MyShared(T* ptr)
:_ptr(ptr),
_pmtx(new mutex),
_pcount(new int(1))
{}
void AddCount()
{
_pmtx->lock();
(*_pcount)++;
_pmtx->unlock();
}
void DelCount()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;//当为0的时候删除计数器
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
_pmtx = nullptr;
}
}
MyShared(MyShared<T>& sp)
:_ptr(sp._ptr),
_pcount(sp._pcount),
_pmtx(sp._pmtx)
{
AddCount();
}
MyShared& operator=(MyShared<T>& sp)
{
if (_ptr != sp._ptr)
{
DelCount();//释放管理的旧资源
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddCount();//对管理的新资源的计数器进行++
}
return *this;
}
//获取引用计数
int use_count()
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
(3)删除器
如果不是new出来的对象如何通过智能指针进行管理呢?其实shared_ptr设计了一个删除器来解决这一问题。
template<class T>
struct FreeFunc
{
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc
{
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
???????};
此时使用malloc进行初始化的时候就也可以进行清理空间了:
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
3.weak_ptr
(1)shared_ptr中的循环调用问题
循环调用问题在一些特殊的情况下会产生:
1.node1和node2两个智能指针指向两个节点,引用计数变成1,我们不需要手动delete。
2.node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3.node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
4.也就是说_next析构了,node2就释放了。
5.也就是说_prev析构了,node1就释放了。
6.但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所
以这就叫循环引用,谁也不会释放。
struct ListNode
{
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
};
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
node1 ->_next = node2;
node2 -> _prev = node1;
通俗来讲,就是此时如果想释放node2,那么就需要delete(n1->next),但是如果要释放n1->next就必须delete(n1),而要deleten1又需要delete(node2->prev)因此如果不让prev指向n就没有问题。
(2)weak_ptr
struct ListNode
{
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
//...
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
来源:https://blog.csdn.net/qq_51492202/article/details/127111768
猜你喜欢
- 文章描述可能我标题描述不太准确,所以还是要稍微解释下:横线样式就是将TextBox以一条底横线的形式展示在页面,占位提示就是Web的Plac
- synchronized关键字synchronized,我们谓之锁,主要用来给方法、代码块加锁。当某个方法或者代码块使用synchroniz
- 字符流是针对字符数据的特点进行过优化的,因而提供一些面向字符的有用特性,字符流的源或目标通常是文本文件。 Reader和Writer是jav
- springboot和springmvc的区别spring boot 内嵌tomcat,Jetty和Undertow容器,可以直接运行起来,
- 本文实例讲述了Java中计算时间差的方法。分享给大家供大家参考。具体如下:假设现在是2004-03-26 13:31:40过去是:2004-
- 本文实例为大家分享了Qt实现计算器功能的具体代码,供大家参考,具体内容如下该计算器主要通过lineEdit获取和显示数字,通过tablevi
- 先来看一个很简单的核心图片缩放方法:public static Bitmap scale(Bitmap bitmap, float scal
- maven配置项目的jdk版本无效排查最近在配置项目的jdk的时候发现在pom.xml中配置的1.8版本无效,maven更新后就变成了1.7
- Process#waitFor()阻塞问题有时需要在程序中调用可执行程序或脚本命令:Process process = Runtime.ge
- public class MainActivity extends Activity { @Override protected void
- 本文实例为大家分享了Java实现考试系统的具体代码,供大家参考,具体内容如下说明这里的考试系统是指由学生,老师以及考试机构成的,学生通过用户
- 1安装eclipse插件步骤,点击help,选择Eclipse Marketplace2.输入Scala,点击go3.选择搜索到的Scala
- 修订功能可以跟踪文档所有的修改,了解修改的过程,这对于团队协同文档编辑、审阅是非常有用的一个功能。将工作簿发送给他人审阅时,我们可以开启修订
- 一、ThreadPoolThreadPool是.Net Framework 2.0版本中出现的。ThreadPool出现的背景:Thread
- Java 读取外部资源的方法详解在Java代码中经常有读取外部资源的要求:如配置文件等等,通常会把配置文件放在classpath下或者在we
- 日期格式化标准 DateTime 格式字符串如果格式字符串只包含下表列出的某个单个格式说明符,则它们被解释为标准格式说明符。如果指定的格式字
- 首先是创建redis-cluster文件夹:因为redis最少需要6个节点(三主三从),为了更好的理解,我这里创建了两台虚拟机(192.16
- 序言for循环语句是java循环语句中最常用的循环语句,一般用在循环次数已知的情况下使用。for循环语句的语法格式如下:java语言 for
- 概念逃逸分析一种数据分析算法,基于此算法可以有效减少 Java 对象在堆内存中的分配。 Hotspot 虚拟机的编译器能够分析出一个新对象的
- 一、OutputStreamWriter流 API说明:OutputStreamWriter是从字符流到