智能指针循环引用——你真的懂了吗?

相信不少同学都在面试中都被问到过c++智能指针的问题,接踵而至的必定是循环引用了,而我每次的答案都是一招鲜:因为它们都在互相等待对方先释放,所以造成内存泄漏。面试官很满意,我也很满意。

但是为啥要等到对方先释放?在我内心也曾有个问号。。

智能指针起源

所谓人类进步的阶梯就是懒,曾几何时,就有人想,我把new 运算符返回的指针p交给一个对象“托管”,而不用操心在哪里去释放这个指针p,由这个托管者自动的在合适的时机进行指针p的释放,这样也不怕自己忘记释放指针p了。而且,我这边操作托管者,要跟操作原来的指针一毛一样,这样才方便。即假设托管指针p的对象叫做shared_ptr,那么

*shared_ptr 所指,便是 *p 所向!

第一章 auto_ptr

最早的智能指针类,实现了*运算符。你用它实例化出来一个类对象,用起来就好像指针一样。对象的特性自然是在作用域结束时,自动调用析构函数实现自我销毁。如此一来,攻城狮们便可不再操心何时释放指针,而可以随心所欲的摸鱼啦。

但其有个致命缺陷:因为其缺省的复制和赋值构造函数,带来的同一个对象被多个智能指针托管,释放的时候便混乱异常,一不小心就重复释放了,使得用户使用起来战战兢兢,老早就被淘汰了。

第二章 unique_ptr

为了规避auto_ptr可以随意复制和赋值构造的缺陷,就推出了unique_ptr, 其实就是给auto_ptr简单换了个名字,并禁用掉其默认的复制和赋值构造函数,好嘛,大家都别用了<(`^′)>,不写代码,就不会有bug!

第三章 shared_ptr

只有一个unique_ptr,对于省吃俭用的高级攻城狮们来说自然是不够的,于是就有人站了出来喊了一声:我们要搞共享经济——砰!基于引用计数的shared_ptr横空出世了,每多一个人持有这个对象,引用计数就加一;每少一个人持有这个对象,引用计数就减一。引用计数为零时,才做真正的销毁操作。

很快,梦魇悄然而至,“我的shared_ptr怎么没有释放?”——一位焦头烂额的格子少年路过。。

起初,没有人在意这场灾难,这不过是一个指针的丢失、一个bug,一个服务器的宕机,直到这场灾难和每个人息息相关......

"你看这个人写的代码,它好像一坨狗屎"

#include <iostream>

class B;
class A {
public:
    A() {
        std::cout << "A" << std::endl;
    }
    ~A() {
        std::cout << "~A" << std::endl;
    }
public:
	std::shared_ptr<B> ptr;
};

class B {
public:
    B() {
        std::cout << "B" << std::endl;
    }
    ~B() {
        std::cout << "~B" << std::endl;
    }
public:
	std::shared_ptr<A> ptr;
};

void fun() {
	std::shared_ptr<A> pa(new A());
	std::shared_ptr<B> pb(new B());
	pa->ptr = pb;
	pb->ptr = pa;
}

int main()
{
	fun();
	return 0;
}

运行结果我不敢看: (*/ω\*)

纳尼?A和B的析构函数都没有被调用,妥妥的内存泄漏了! 

事后,据某位亲身经历这次事件的大牛回忆说:“喔,当时的内存布局是这个样子的!”

“对象A同时被pa和对象B中的ptr两个智能指针托管,所以引用计数为2;对象B同时被pb和对象
A中的ptr两个智能指针托管,所以引用计数也为2。那么当fun函数执行完,栈对象pb、pa依次开始执行这样的析构函数:”

// 大牛随手写的析构函数伪代码
// Copyright 2022 DaNiu. All rights reserved.

if (--ref_cnt == 0) delete obj;

“紧接着内存布局就变成了这样:”

“pa和pb已经销毁了,然而对象A和B,已经迷失在这浩瀚内存中,亘古难灭。。。吾称之为循环引用!”

路人震惊:“嘶!随着这样的迷失越来越多,这片天地再也无人可搞对象!!!”

゜゜(´O`) ゜゜。:“不要啊!我还没有搞过对象呢。”

......

庆幸的是,不就之后,有人就在shared_ptr出世的那方世界的一个不起眼的角落里,发现了一个小家伙,伴shared_ptr而生。

终章 weak_ptr

为了解决shared_ptr在在循环引用中存在的资源泄漏问题,weak_ptr在这种场景下应用而生,weak_ptr指向的智能指针对象,其引用计数不会加一,也就不会存在无法释放的问题了。

解决的方法就是,把A和B其中的一个ptr改成weak_ptr。

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++智能指针循环引⽤问题分析 C++11中引⼊了三种智能指针,分别是shared_ptr、weak_ptr和unique_ptr 智能指针的作⽤ 智能指针可以帮助我们管理动态分配的堆内存,减少内存泄漏的可能性 ⼿动管理堆内存有引起内存泄漏的可能,⽐如这段代码 try { int* p = new int; // Do something delete p; } catch(...) { // Catch exception } 如果在执⾏Do something的时候发⽣了异常,那么程序就会直接跳到catch语句捕获异常,delete p这句代码不会被执⾏,发⽣了内存泄 漏 我们把上⾯的程序改成 try { shared_ptr<int> p(new int); // Do something } catch(...) { // Catch exception } 当执⾏Do something的时候发⽣了异常,那么try块中的栈对象都会被析构。因此代码中p的析构函数会被调⽤,引⽤计数从1变成0,通过 new分配的堆内存被释放,这样就避免了内存泄漏的问题 循环引⽤问题 虽然智能指针会减少内存泄漏的可能性,但是如果使⽤智能指针的⽅式不对,⼀样会造成内存泄漏。⽐较典型的情况是循环引⽤问题,⽐如 这段代码 class B; // 前置声明 class A { public: shared_ptr<B> ptr; }; class B { public: shared_ptr<A> ptr; }; int main() { while(true) { shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa -> ptr = pb; pb -> ptr = pa; } return 0; } 这个程序中智能指针的引⽤情况如下图 上图中,class A和class B的对象各⾃被两个智能指针管理,也就是A object和B object引⽤计数都为2,为什么是2? 分析class A对象的引⽤情况,该对象被main函数中的pa和class B对象中的ptr管理,因此A object引⽤计数是2,B object同理。 在这种情况下,在main函数中⼀个while循环结束的时候,pa和pb的析构函数被调⽤,但是class A对象和class B对象仍然被⼀个智能指 针管理,A object和B object引⽤计数变成1,于是这两个对象的内存⽆法被释放,造成内存泄漏,如下图所⽰ 解决⽅法 解决⽅法很简单,把class A或者class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会增加shared_ptr的引⽤计数,所以A object和B object中有⼀个的引⽤计数为1,在pa和pb析构时,会正确地释放掉内存 ————————————————
CruiseYoung提供的带有详细书签的电子书籍目录 http://blog.csdn.net/fksec/article/details/7888251 该资料是《COM技术内幕——微软组件对象模型》一书的随书源代码 COM技术内幕——微软组件对象模型 基本信息 原书名: Inside COM: Microsoft's Component Object Model with Cdrom 原出版社: Microsoft Press 作者: (美)Dale Rogerson 译者: 杨秀章 丛书名: 微软版权图书 出版社:清华大学出版社 ISBN:730203320X 上架时间:2001-10-11 出版日期:1999 年3月 页码:293 版次:1-1 所属分类:计算机 > 软件与程序设计 > COM/DCOM/ATL/COM+ 内容简介    微软公司的组件对象模型(COM)作为一种重要的工具已崭露头角,它是微软迈向分布式计算的基础。不论现在还是将来,它都是定制应用程序的一种强大的方法。并且它是OLE和ActiveX 的基础。COM帮助你理解未来的程序开发技术,而这本书帮助你理解COM。在本书中你将发现:构建优美的COM组件的清晰、简单、实用的规则;COM是如何易学易用,特虽是对那些熟练掌握C++ 的人;循序渐进地介绍COM设计;以代码形式给出的大量实例。    《COM技术内幕》适合于中、高级C++程序员;COM、ActiveX和OLE程序员;对组件设计感兴趣的研究人员;以及那些当COM移植到UNIX、MVS和其他环境时想要使用到COM的程序员。 编辑推荐    微软公司的组件对象模型(COM)作为一种重要的工具已崭露头角,它是微软迈向分布式计算的基础。不论现在还是将来,它都是定制应用程序的一种强大的方法。并且它是OLE和ActiveX 的基础。COM帮助你理解未来的程序开发技术,而这本书帮助你理解COM。在本书中你将发现:构建优美的COM组件的清晰、简单、实用的规则;COM是如何易学易用,特虽是对那些熟练掌握C++ 的人;循序渐进地介绍COM设计;以代码形式给出的大量实例。 目录 封面 -17 扉页 -16 版权 -15 译者前言 -14 目录 -13 引言 -6 第1章 组件 1 1.1 使用组件的优点 2 1.1.1 应用程序的定制 2 1.1.2 组件库 3 1.1.3 分布式组件 3 1.2 对组件的需求 4 1.2.1 动态链接 4 1.2.2 信息封装 5 1.3 COM 6 1.3.1 COM组件是…… 7 1.3.2 COM不是…… 7 1.3.3 COM库 8 1.3.4 COM方法 8 1.3.5 COM超越了用户的需要 8 1.4 本章小结 9 第2章 接口 11 2.1 接口的作用 11 2.1.1 可复用应用程序架构 12 2.1.2 COM接口的其他优点 13 2.2 COM接口的实现 13 2.2.1 编码约定 14 2.2.2 一个完整的例子 15 2.2.3 非接口通信 18 2.2.4 实现细节 18 2.3 接口理论:第二部分 20 2.3.1 接口的不变性 20 2.3.2 多态 20 2.4 接口的背后 21 2.4.1 虚拟函数表 21 2.4.2 vtbl指针及实例数据 23 2.4.3 多重实例 24 2.4.4 不同的类,相同的vtbl 24 2.5 本章小结 26 第3章 QueryInterface函数 27 3.1 接口查询 28 3.1.1 关于IUnknown 28 3.1.2 IUnknown指针的获取 29 3.1.3 关于QueryInterface 29 3.1.4 QueryInterface的使用 30 3.1.5 QueryInterface的实现 31 3.1.6 关于类型转换 32 3.1.7 一个完整的例子 35 3.2 关于QueryInterface的实现规则 40 3.2.1 同一IUnknown 40 3.3.2 客户可以获取曾经得到过的接口 41 3.2.3 可以再次获取已经拥有的接口 41 3.2.4 客户可以从任何接口返回到起始接口 42 3.2.5 若能够从某接口获取某特定接口,则从任意接口都将能够获取此接口 42 3.3 QueryInterface定义了组件 43 3.3.1 接口集 44 3.4 新版本组件的处理 44 3.4.1 何时需要建立一个新版本 46 3.4.2 不同版本接口的命名 46 3.4.3 隐含
组织和策略问题 1 第0条 不要拘泥于小节(又名:了解哪些东西不应该标准化) 2 第1条 在高警告级别干净利落地进行编译 4 第2条 使用自动构建系统 7 第3条 使用版本控制系统 8 第4条 做代码审查 9设计风格 11 第5条 一个实体应该只有一个紧凑的职责 12 第6条 正确、简单和清晰第一 13 第7条 编程中应知道何时和如何考虑可伸缩性 14 第8条 不要进行不成熟的优化 16 第9条 不要进行不成熟的劣化 18 第10条 尽量减少全局和共享数据 19 第11条 隐藏信息 20 第12条 得何时和如何进行并发性编程 21 第13条 确保资源为对象所拥有。使用显式的RAII和智能指针 24 编程风格 27 第14条 宁要编译时和连接时错误,也不要运行时错误 28 第15条 积极使用const 30 第16条 避免使用宏 32 第17条 避免使用“魔数” 34 第18条 尽可能局部地声明变量 35 第19条 总是初始化变量 36 第20条 避免函数过长,避免嵌套过深 38 第21条 避免跨编译单元的初始化依赖 39 第22条 尽量减少定义性依赖。避免循环依赖 40 第23条 头文件应该自给自足 42 第24条 总是编写内部#include保护符,决不要编写外部#include保护符 43 函数与操作符 45 第25条 正确地选择通过值、(智能)指针或者引用传递参数 46 第26条 保持重载操作符的自然语义 47 第27条 优先使用算术操作符和赋值操作符的标准形式 48 第28条 优先使用++和--的标准形式。优先调用前缀形式 50 第29条 考虑重载以避免隐含类型转换 51 第30条 避免重载&&、||或 ,(逗号) 52 第31条 不要编写依赖于函数参数求值顺序的代码 54 类的设计与继承 55 第32条 弄清所要编写的是哪种类 56 第33条 用小类代替巨类 57 第34条 用组合代替继承 58 第35条 避免从并非要设计成基类的类中继承 60 第36条 优先提供抽象接口 62 第37条 公用继承即可替换性。继承,不是为了重用,而是为了被重用 64 第38条 实施安全的覆盖 66 第39条 考虑将虚拟函数声明为非公用的,将公用函数声明为非虚拟的 68
visualC++2010入门经典源代码 第1章 使用visual c++ 2010编程 1.1 .net framework 1 1.2 clr 2 1.3 编写c++应用程序 3 1.4 学习windows编程 4 1.4.1 学习c++ 4 1.4.2 c++标准 5 1.4.3 属性 5 1.4.4 控制台应用程序 5 1.4.5 windows编程概念 6 1.5 集成开发环境简介 7 1.5.1 编辑器 8 1.5.2 编译器 8 1.5.3 链接器 8 1.5.4 库 8 1.6 使用ide 8 1.6.1 工具栏选项 9 1.6.2 可停靠的工具栏 10 1.6.3 文档 11 1.6.4 项目和解决方案 11 1.6.5 设置visual c++ 2010的选项 23 1.6.6 创建和执行windows应用程序 23 1.6.7 创建windows forms应用程序 26 1.7 小结 27 1.8 本章主要内容 28 第2章 数据、变量和计算 29 2.1 c++程序结构 29 2.1.1 main()函数 36 2.1.2 程序语句 36 2.1.3 空白 38 2.1.4 语句块 38 2.1.5 自动生成的控制台程序 39 2.2 定义变量 40 2.2.1 命名变量 40 2.2.2 声明变量 41 2.2.3 变量的初始值 42 2.3 基本数据类型 42 2.3.1 整型变量 43 2.3.2 字符数据类型 44 2.3.3 整型修饰符 45 2.3.4 布尔类型 46 2.3.5 浮点类型 46 2.3.6 字面值 47 2.3.7 定义数据类型的同义词 48 2.3.8 具有特定值集的变量 49 2.4 基本的输入/输出操作 50 2.4.1 从键盘输入 50 2.4.2 到命令行的输出 50 2.4.3 格式化输出 51 2.4.4 转义序列 52 2.5 c++中的计算 54 2.5.1 赋值语句 54 2.5.2 算术运算 55 2.5.3 计算余数 59 2.5.4 修改变量 60 2.5.5 增量和减量运算符 60 2.5.6 计算的顺序 63 2.6 类型转换和类型强制转换 64 2.6.1 赋值语句中的类型转换 65 2.6.2 显式类型转换 65 2.6.3 老式的类型强制转换 66 2.7 auto关键字 66 2.8 查看类型 67 2.9 按位运算符 67 2.9.1 按位and运算符 68 2.9.2 按位or运算符 69 2.9.3 按位eor运算符 71 2.9.4 按位not运算符 71 2.9.5 移位运算符 71 2.10 lvalue和rvalue 73 2.11 了解存储时间和作用域 74 2.11.1 自动变量 74 2.11.2 决定变量声明的位置 76 2.11.3 全局变量 77 2.11.4 静态变量 80 2.12 名称空间 80 2.12.1 声明名称空间 81 2.12.2 多个名称空间 82 2.13 c++/cli编程 84 2.13.1 c++/cli特有的基本数据类型 84 2.13.2 命令行上的c++/cli输出 87 2.13.3 c++/cli特有的功能—— 格式化输出 88 2.13.4 c++/cli的键盘输入 91 2.13.5 使用safe_cast 92 2.13.6 c++/cli枚举 92 2.14 查看c++/cli类型 96 2.15 小结 97 2.16 练习 97 2.17 本章主要内容 98 第3章 判断和循环 101 3.1 比较数据值 101 3.1.1 if语句 102 3.1.2 嵌套的if语句 104 3.1.3 嵌套的if-else语句 107 3.1.4 逻辑运算符和表达式 109 3.1.5 条件运算符 112 3.1.6 switch语句 113 3.1.7 无条件转移 116 3.2 重复执行语句块 117 3.2.1 循环的概念 117 3.2.2 for循环的变体 119 3.2.3 while循环 126 3.2.4 do-while循环 128 3.2.5 嵌套的循环 129 3.3 c++/cli编程 132 3.4 小结 137 3.5 练习 138 3.6 本章主要内容 138 第4章 数组、字符串和指针 139 4.1 处理多个相同类型的数据值 139 4.1.1 数组 140 4.1.2 声明数组 140 4.1.3 初始化数组 143 4.1.4 字符数组和字符串处理 144 4.1.5 多维数组 147 4.2 间接数据访问 150 4.2.1 指针的概念 150 4.2.2 声明指针 150 4.2.3 使用指针 152 4.2.4 初始化指针 152 4.2.5 sizeof操作符 158 4.2.6 常量指针和指向常量的指针 159 4.2.7 指针和数组 161 4.3 动态内存分配 168 4.3.1 堆的别名—— 空闲存储器 168 4.3.2 new和delete操作符 168 4.3.3 为数组动态分配内存 169 4.3.4 多维数组的动态分配 171 4.4 使用引用 172 4.4.1 引用的概念 172 4.4.2 声明并初始化lvalue引用 172 4.4.3 声明并初始化rvalue引用 173 4.5 字符串的本地c++库函数 174 4.5.1 查找以空字符结尾的字符串的长度 174 4.5.2 连接以空字符结尾的字符串 174 4.5.3 复制以空字符结尾的字符串 176 4.5.4 比较以空字符结尾的字符串 177 4.5.5 搜索以空字符结尾的字符串 177 4.6 c++/cli编程 179 4.6.1 跟踪句柄 180 4.6.2 clr数组 181 4.6.3 字符串 195 4.6.4 跟踪引用 203 4.6.5 内部指针 204 4.7 小结 206 4.8 练习 206 4.9 本章主要内容 207 第5章 程序结构(1) 209 5.1 理解函数 209 5.1.1 需要函数的原因 210 5.1.2 函数的结构 210 5.1.3 使用函数 213 5.2 给函数传递实参 216 5.2.1 按值传递机制 216 5.2.2 给函数传递指针实参 217 5.2.3 给函数传递数组 219 5.2.4 给函数传递引用实参 222 5.2.5 使用const修饰符 224 5.2.6 rvalue引用形参 225 5.2.7 main()函数的实参 227 5.2.8 接受数量不定的函数实参 229 5.3 从函数返回值 231 5.3.1 返回指针 231 5.3.2 返回引用 233 5.3.3 函数中的静态变量 236 5.4 递归函数调用 238 5.5 c++/cli编程 240 5.5.1 接受数量可变实参的函数 241 5.5.2 main( )的实参 242 5.6 小结 243 5.7 练习 243 5.8 本章主要内容 244 第6章 程序结构(2) 245 6.1 函数指针 245 6.1.1 声明函数指针 246 6.1.2 函数指针作为实参 249 6.1.3 函数指针的数组 250 6.2 初始化函数形参 250 6.3 异常 252 6.3.1 抛出异常 253 6.3.2 捕获异常 254 6.3.3 mfc中的异常处理 255 6.4 处理内存分配错误 256 6.5 函数重载 257 6.5.1 函数重载的概念 258 6.5.2 引用类型和重载选择 260 6.5.3 何时重载函数 260 6.6 函数模板 261 6.7 使用decltype操作符 263 6.8 使用函数的示例 265 6.8.1 实现计算器 265 6.8.2 从字符串中删除空格 268 6.8.3 计算表达式的值 268 6.8.4 获得项值 270 6.8.5 分析数 271 6.8.6 整合程序 274 6.8.7 扩展程序 275 6.8.8 提取子字符串 277 6.8.9 运行修改过的程序 279 6.9 c++/cli编程 279 6.9.1 理解泛型函数 280 6.9.2 clr版本的计算器程序 285 6.10 小结 290 6.11 练习 291 6.12 本章主要内容 292 第7章 自定义数据类型 293 7.1 c++中的结构 293 7.1.1 结构的概念 294 7.1.2 定义结构 294 7.1.3 初始化结构 294 7.1.4 访问结构的成员 295 7.1.5 伴随结构的智能感知帮助 298 7.1.6 rect结构 299 7.1.7 使用指针处理结构 300 7.2 数据类型、对象、类和实例 301 7.2.1 类的起源 303 7.2.2 类的操作 303 7.2.3 术语 303 7.3 理解类 304 7.3.1 定义类 304 7.3.2 声明类的对象 305 7.3.3 访问类的数据成员 305 7.3.4 类的成员函数 307 7.3.5 成员函数定义的位置 309 7.3.6 内联函数 309 7.4 类构造函数 310 7.4.1 构造函数的概念 311 7.4.2 默认的构造函数 312 7.4.3 在类定义中指定默认的形参值 314 7.4.4 在构造函数中使用初始化列表 316 7.4.5 声明显式的构造函数 317 7.5 类的私有成员 318 7.5.1 访问私有类成员 320 7.5.2 类的友元函数 321 7.5.3 默认复制构造函数 323 7.6 this指针 325 7.7 类的const对象 327 7.7.1 类的const成员函数 327 7.7.2 类外部的成员函数定义 328 7.8 类对象的数组 329 7.9 类的静态成员 331 7.9.1 类的静态数据成员 331 7.9.2 类的静态函数成员 334 7.10 类对象的指针和引用 334 7.10.1 类对象的指针 334 7.10.2 类对象的引用 337 7.11 c++/cli编程 338 7.11.1 定义值类类型 339 7.11.2 定义引用类类型 344 7.11.3 定义引用类类型的复制构造函数 346 7.11.4 类属性 346 7.11.5 initonly字段 358 7.11.6 静态构造函数 360 7.12 小结 360 7.13 练习 360 7.14 本章主要内容 361 第8章 深入理解类 363 8.1 类析构函数 363 8.1.1 析构函数的概念 363 8.1.2 默认的析构函数 364 8.1.3 析构函数与动态内存分配 366 8.2 实现复制构造函数 369 8.3 在变量之间共享内存 370 8.3.1 定义联合 371 8.3.2 匿名联合 372 8.3.3 类和结构中的联合 372 8.4 运算符重载 373 …… 第9章 类继承和虚函数 第10章 标准模板库 第11章 调试技术 第12章 windows编程的概念 第13章 多核编程 第14章 使用mfc编写windows程序 第15章 处理菜单和工具栏 第16章 在窗口中绘图 第17章 创建文档和改进视图 第18章 使用对话框和控件 第19章 存储和打印文档 第20章 编写自己的dll

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fireplusplus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值