四种常见智能指针的介绍

一、介绍

当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针(smart pointer),从而实现指针指向的对象的共享。

智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。

一句话简单概括:智能指针是帮助我们管理动态内存分配的,会自动帮忙释放new出来的内存,从而避免内存泄漏的问题。

二、为什么要使用智能指针

答:使用智能指针可以有效的避免内存泄漏。

什么是内存泄漏?

答:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

如下例子:人为原因导致忘记释放;代码逻辑原因抛出异常使内存释放执行不了。

void MemoryLeaks()
{// 1.内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.delete[] p3;}

常见的内存泄漏

堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 释放。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

三、智能指针的使用及原理

RAII思想

当我们使用一块资源时,一般会经历三个步骤:a.获取资源(创建对象);b.使用资源;c.销毁资源(析构对象)。但我们通常会忘记c销毁资源,从而导致一些内存问题的产生,而RAII的思想,就可以巧妙的解决c步骤的发生。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术 。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:不需要显式地释放资源;采用这种方式,对象所需的资源在其生命期内始终保持有效 。

原理

智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。

即具有RAII思想,且重载有operator*和operator->像指针一样的行为的一种指针类型。

一个智能指针模板类,至少具有如下格式:

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;
};

四、常见的智能指针类型

头文件 #include <memory>

1.auto_ptr

C++98版本的库中就提供了auto_ptr的智能指针,其实现原理是管理权转移的思想。

这就导致了很不好的情况产生如下代码,将对象p1转移给p2时,不会发生任何问题,但是当访问到被转移后的p1时,就会报错。

auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错
cout << *p2 << endl;  // 访问没有问题
cout << *p1 << endl;  // 会报错

在p1赋值给p2后,首先p2会先将自己原先托管的指针释放掉,然后接收托管p1所托管的指针,然后p1所托管的指针置NULL,也就是p2托管了p1的指针,而p1放弃了托管,所以再次访问到p1时,就会发生错误。

即auto_ptr:存在潜在的内存崩溃问题。所以在C++11中,auto_ptr已经被弃用了。并且企业中也会禁止使用auto_ptr。

模拟实现:

template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr = nullptr): _ptr(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)  // 先释放自己的空间,再获取别的对象{std::cout << "Delete:" << _ptr << std::endl;delete _ptr;}// 转移ap中资源到当前对象中_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){std::cout << "Delete:" << _ptr << std::endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

2.unique_ptr

unique_ptr避免了auto_ptr中出现的问题,而它的解决方法也更简单粗暴,即禁用拷贝赋值。

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。

unique_ptr特性

  1. 基于排他所有权模式:两个指针不能指向同一个资源。
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值。
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的。
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 不允许
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 允许

当程序试图将一个 unique_ptr 赋值给另一个时,如果源unique_ptr 是个临时右值(#2),编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做(#1)。

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

另外:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++11标准中的库函数std::move(),能够允许将一个unique_ptr赋给另一个。

std::unique_ptr<std::string> p1(new std::string("test move copy"));
std::unique_ptr<std::string> p2 = std::move(p1);
std::cout << p1.get() << std::endl;
std::cout << p2.get() << std::endl;

但是,通过运行结果,可以看出,赋值后,p1还是被置空了,结果与auto_ptr一致了。所以,unique_ptr会禁止拷贝复制。

与auto_ptr区别:禁用了拷贝赋值操作。

模拟实现:

template<class T>
class unique_ptr
{
private:
public:unique_ptr(T* ptr = nullptr): _ptr(ptr){}// 防拷贝 C++11, 禁用拷贝赋值unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;~unique_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

3.shared_ptr

原理:

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

  1. shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享,发生拷贝或赋值时+1。可以通过成员函数use_count()来查看资源的所有者个数 。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

成员函数:

  • use_count 返回引用计数的个数。
  • unique 返回是否是独占所有权( use_count 为 1)。
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的。

use_count 使用:

int main()
{std::shared_ptr<std::string> sp1;std::shared_ptr<std::string> sp2(new std::string("test"));std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;    // 未发生拷贝或复制 0std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 拷贝1次 +1// 共享sp1 = sp2;  // 赋值一次,sp2 +1,sp1内部变为sp2,所以结果应该与sp2一致std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 2std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 打印 2std::shared_ptr<std::string> sp3(sp1);  // sp3拷贝sp1; sp1 次数+1,sp3与sp1 一致std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 3std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl;  // 打印 3std::cout << "sp2	use_count() = " << sp3.use_count() << std::endl << std::endl;  // 打印 3return 0;
}

运行结果:

可以发现,三次发生计数+1操作的位置在sp2(new std::string("test"))sp1 = sp2; sp3(sp1)三个拷贝、赋值的地方。

reset使用:

int main()
{std::shared_ptr<std::string> sp1;std::shared_ptr<std::string> sp2(new std::string("test"));std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;    // 未发生拷贝或复制 0std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 拷贝1次 +1// 共享sp1 = sp2;  // 赋值一次,sp2 +1,sp1内部变为sp2,所以结果应该与sp2一致std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 2std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl << std::endl;  // 打印 2std::shared_ptr<std::string> sp3(sp1);  // sp3拷贝sp1; sp1 次数+1,sp3与sp1 一致std::cout << "sp1	use_count() = " << sp1.use_count() << std::endl;  // 打印 3std::cout << "sp2	use_count() = " << sp2.use_count() << std::endl;  // 打印 3std::cout << "sp2	use_count() = " << sp3.use_count() << std::endl << std::endl;  // 打印 3// reset使用sp1.reset();std::cout << "sp1	use_count() = " << sp1.use_count() << " " << sp1 << std::endl;std::cout << "sp2	use_count() = " << sp2.use_count() << " " << sp2 << std::endl;std::cout << "sp3	use_count() = " << sp3.use_count() << " " << sp3 << std::endl << std::endl;return 0;
}

由运行结果可见,sp1调用reset后,sp1本身被置空,它对应的计数也变为0,而sp2和sp3,会随着sp1调用reset导致计数会减1。

unique使用:

std::shared_ptr<int> p1(new int(10));
std::shared_ptr<int> p2 = p1;// #1
std::cout << "p1 is unique: " << std::boolalpha << p1.unique() << std::endl; // 输出:p1 is unique: false
std::cout << "p2 is unique: " << std::boolalpha << p2.unique() << std::endl; // 输出:p2 is unique: false// #2
std::shared_ptr<int> p3(new int(20));
std::cout << "p3 is unique: " << std::boolalpha << p3.unique() << std::endl; // 输出:p3 is unique: true// #3
p2.reset();
std::cout << "p1 is unique: " << std::boolalpha << p1.unique() << std::endl; // 输出:p1 is unique: true
std::cout << "p2 is unique: " << std::boolalpha << p2.unique() << std::endl; // 输出:p2 is unique: false

#1 p1进行了一次赋值给p2,所以发生了一次共享,所以不是独占所有权false。

#2 p3,它指向一个新的int类型的对象,此时p3是该对象的唯一所有者。

#3 使用reset函数将p2重置为空,此时p1是该对象的唯一所有者,unique函数返回true。由于p2已经被置空,它不再指向任何资源,输出为false。

swap使用

    // 创建两个 shared_ptr 对象std::shared_ptr<int> ptr1 = std::make_shared<int>(42);std::shared_ptr<int> ptr2 = std::make_shared<int>(100);std::shared_ptr<int> ptr3 = ptr1;std::cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << std::endl;std::cout << "ptr2: " << *ptr2 << ", use_count: " << ptr2.use_count() << std::endl;// 使用 swap 交换资源ptr1.swap(ptr2);std::cout << "ptr1: " << *ptr1 << ", use_count: " << ptr1.use_count() << std::endl;std::cout << "ptr2: " << *ptr2 << ", use_count: " << ptr2.use_count() << std::endl;

由运行结果可以看出,ptr1.swap(ptr2);执行后,ptr1与ptr2对应的值以及计数发生了交换。

shared_ptr的线程安全问题

shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

模拟实现:

template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) == 0){cout << "Delete:" << _ptr << endl;delete _ptr;delete _pCount;}}~shared_ptr(){Release();}// sp1(sp2)shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this == &sp)if (_ptr == sp._ptr){return *this;}// 减减被赋值对象的计数,如果是最后一个对象,要释放资源/*if (--(*_pCount) == 0){delete _ptr;delete _pCount;}*/Release();// 共管新资源,++计数_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;// 引用计数int* _pCount;
};

4.weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{public :std::shared_ptr<B> pb_;~A(){std::cout << "A delete\n";}
};
class B
{public :std::shared_ptr<A> pa_;~B(){std::cout << "B delete\n";}
};
void fun()
{std::shared_ptr<B> pb(new B());std::shared_ptr<A> pa(new A());pb->pa_ = pa;pa->pb_ = pb;std::cout << pb.use_count() << std::endl;std::cout << pa.use_count() << std::endl;
} int main()
{fun();return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用):

(没有调用析构)

如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb; 运行结果如下:

这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb->print(); 英文pb是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/311457.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯C/C++程序设计——特别数的和

题目描述 小明对数位中含有 2、0、1、9 的数字很感兴趣&#xff08;不包括前导 0&#xff09;&#xff0c;在 1 到 40 中这样的数包括 1、2、9、10 至 32、39 和 40&#xff0c;共 28 个&#xff0c;他们的和是 574。 请问&#xff0c;在 1 到 n 中&#xff0c;所有这样的数的…

GitHub 一周热点汇总 第3期 (2023/12/24-12/30)

GitHub一周热点汇总第三期 (2023/12/24-12/30)&#xff0c;梳理每周热门的GitHub项目&#xff0c;了解热点技术趋势&#xff0c;掌握前沿科技方向&#xff0c;发掘更多商机。元旦就要到了&#xff0c;提前祝大家新年快乐。 #1 StreamDiffusion 项目名称&#xff1a;StreamDiff…

vue项目表单使用正则过滤ip、手机号

import useFormValidate from /hooks/useFormValidatesetup(props, { emit }) {const { validateName, validateIPAndPort } useFormValidate()const state reactive({workFaceInfo: props.info?.id ? props.info : {},sysTypeData: props.sysType,formRules: {name: [{req…

wsl中的Ubuntu安装远程桌面

wsl Ubuntu默认只能打开命令行&#xff0c;看不到图形化界面&#xff0c;有些操作不方便。这里介绍两种方法来远程连接到wsl里 VNC 因为win10的wsl不支持systemd&#xff0c; 所以这种方式只能是Windows11的系统&#xff0c;Window10只能用xrdp 1、禁用WSLg 在c:\users\用户…

12.31_黑马数据结构与算法笔记Java

目录 345 设计跳表 Leetcode 1206 346 设计最小栈 Leetcode 155 347 设计端网址 Leetcode 355 348 设计推特 Leetcode 355 349 股票系列问题 Leetcode 121 350 股票系列问题 Leetcode 122 351 股票系列问题 Leetcode 714 352 股票系列问题 Leetcode 309 353 股票系列问…

【Redis-10】Redis集群的实现原理和实践

Redis集群是Redis提供的分布式数据库方案&#xff0c;通过分片来进行数据共享&#xff0c;实现复制和故障转移的功能。 1. Redis集群节点 一个Redis集群由多个节点组成&#xff0c;多个节点可以通过命令实现连接&#xff0c;由独立状态转为集群状态&#xff0c;命令是cluster …

LabVIEW开发智能火灾自动报警系统

LabVIEW开发智能火灾自动报警系统 系统基于LabVIEW虚拟仪器开发&#xff0c;由火灾报警控制器、感温感烟探测器、手动报警器、声光报警器、ZigBee无线通讯节点以及上位机电脑等组成&#xff0c;展示了LabVIEW在智能化火灾预警与控制方面的应用。该系统通过结合二总线协议和Zig…

动态规划10-多重背包

题目描述 有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用&#xff0c;每件耗费的空间是Ci &#xff0c;价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量&#xff0c;且价值总和最大。 思路分析 区别于完全背包和简单的01背包问题&…

天擎离线升级工具使用说明

免责声明 本文旨在提供信息和解决问题的建议&#xff0c;观点和建议可能不适用于个人情况&#xff0c;仅供参考&#xff01;&#xff01;&#xff01; 文章中所有敏感信息已经修改&#xff0c;对于因本文中提供的信息而导致的任何直接或间接损失或损害不承担责任。 使用本文中的…

【C++】STL 容器 - multiset 容器 ( std::multiset 容器简介 | std::multiset 容器 常用操作 api 简介 )

文章目录 一、mulset 容器1、std::multiset 容器简介2、代码示例 - multiset 容器 二、std::multiset 容器 常用操作 api 简介1、常用 api 简介2、代码示例 - multiset 容器常用操作 一、mulset 容器 1、std::multiset 容器简介 在 C 语言 的 标准模板库 ( STL , Standard Temp…

街道洗扫车VR虚拟仿真展示创新了培训方式

吸污车用于收集处理城市中的污水、污泥&#xff0c;起到疏通管道的作用&#xff0c;特别是洪涝灾害时是重要的清理工具。吸污车由于内部结构复杂、工艺原理繁琐且造价成本高&#xff0c;因此传统的吸污车作业培训难以达到满意效果。VR虚拟仿真技术的出现&#xff0c;给企业提供…

多线程编程设计模式(单例,阻塞队列,定时器,线程池)

&#x1f495;"只有首先看到事情的可能性&#xff0c;才会有发生的机会。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;多线程编程设计模式(单例,阻塞队列,定时器,线程池) 本文主要讲解多线程编程中常用到的设计模式,包括单例模式,阻塞队列,定时…