C++11标准引入了boost库中的智能指针,给C++开发时的内存管理提供了极大的方便。接下来这篇文件介绍shared_ptr/weak_ptr内部实现原理及使用细节。

C++不像java有内存回收机制,每次程序员new出来的对象需要手动delete,流程复杂时可能会漏掉delete,导致内存泄漏。于是C++引入智能指针,可用于动态资源管理,资源即对象的管理策略。

C++中的shared_ptr/weak_ptr和Android的sp/wp功能类似,都为解决多线程编程中heap内存管理问题而产生的。当程序规模较小时,我们可以手动管理new分配出来的裸指针,什么时候delete释放我们自己手动控制。

但是,当程序规模变大时,并且该heap内存会在各个线程/模块中进行传递和互相引用,当各个模块退出时,谁去释放?由此引入了智能指针的概念。

其原理可以概况为,内部通过引用计数方式,指示heap内存对象的生存周期,而智能指针变量作为一个stack变量,利用栈区变量由操作系统维护(程序员无法控制)的特点进行管理。

实现细节:

这样的东东(我们姑且称其为一个“类”,就像int/char/string为程序语言的内建类,我们也可以定义自己的类来使用)需要有什么特点?

1.这个类内部需要有个指针,就是保护那个经常犯错的裸指针:heap内存对象指针

2.这个类能够代表所有类型的指针,因此必须是模板类。

3.根据需要自动释放其指向的heap内存对象,也即当这个“智能指针类对象”释放时,其内部所包含的heap内存对象根据需要进行释放,因此这个类对象只能是一个stack区的对象(如果是heap区的,我们还需要手动delete,而我们希望有个系统能帮我们去做的东西),另外一点,这个类内部还需要有个变量,用于指示内部的heap内存对象引用数量,以便决定是否释放该heap内存对象

智能指针shared_ptr对象跟其它stack区对象一样有共同的特点——每次离开作用域时会自动调用析构函数进行内存回收。利用该特点,析构时检查其内部所引用的heap内存对象的引用数量进行操作:1.引用计数减一变为0时,则必须释放;2.减一后仍不为0,那么其内部的heap内存对象同时被别的智能指针引用,因此不能释放。

使用示例:

 #include <iostream>
#include <memory>
#include <string> using namespace std; class Person {
public:
Person() {
cout<<"Person ctor"<<endl;
} Person(const string &alias): name(alias) {
cout<<"Person ctor for "<<name.c_str()<<endl;
} ~Person() {
cout<<"Person dtor for "<<name.c_str()<<endl;
} void setFather(shared_ptr<Person> &p) {
father = p;
} void setSon(shared_ptr<Person> &p) {
son = p;
} void printName() {
cout<<"name: "<<name.c_str()<<endl;
} private:
const string name;
shared_ptr<Person> father;
shared_ptr<Person> son;
}; void test0()
{
cout<<"---------test0 normal release begin---------"<<endl; shared_ptr<Person> sp_pf(new Person("zjz"));
shared_ptr<Person> sp_ps(new Person("zcx")); cout<<"---------test0 normal release end---------\n"<<endl;
} void test1()
{
cout<<"\n---------test1 no release begin---------"<<endl; shared_ptr<Person> sp_pf(new Person("zjz"));
shared_ptr<Person> sp_ps(new Person("zcx")); cout<<"addr: "<<&sp_pf<<endl;
cout<<"addr: "<<&sp_ps<<endl; cout<<"111 father use_count: "<<sp_pf.use_count()<<endl;
cout<<"111 son use_count: "<<sp_ps.use_count()<<endl; sp_pf->setSon(sp_ps);
sp_ps->setFather(sp_pf); cout<<"222 father use_count: "<<sp_pf.use_count()<<endl;
cout<<"222 son use_count: "<<sp_ps.use_count()<<endl; cout<<"---------test1 no release end---------\n"<<endl;
} void test2()
{
cout<<"---------test2 release sequence begin---------"<<endl; shared_ptr<Person> sp_pf(new Person("zjz"));
shared_ptr<Person> sp_ps(new Person("zcx")); cout<<"addr: "<<&sp_pf<<endl;
cout<<"addr: "<<&sp_ps<<endl; cout<<"111 father use_count: "<<sp_pf.use_count()<<endl;
cout<<"111 son use_count: "<<sp_ps.use_count()<<endl; sp_pf->setSon(sp_ps);
//sp_ps->setFather(sp_pf); cout<<"222 father use_count: "<<sp_pf.use_count()<<endl;
cout<<"222 son use_count: "<<sp_ps.use_count()<<endl; cout<<"---------test2 release sequence end---------"<<endl;
} int main(void)
{
test0();
test1();
test2(); return ;
}

其执行结果如下:

几点解释说明:

1.test0和test1中为什么有dtor的打印?

sp_pf和sp_ps作为stack对象,当其离开作用域时自动由系统释放,因此在输出“test0 end”后test0()退出时,才会真正释放stack object。
当其释放时,检查内部引用计数为1,则可以释放引用的真正的heap内存——new Person("zjz")和new Person("zcx"),调用其析构函数。

2.释放顺序是什么?为什么test0和test2中不一样?

sp_pf和sp_ps作为stack对象,我们可以回想stack对象内存管理方式——“先进后出,后进先出”,且地址变化由高地址向低地址过渡,由test1和test2中对sp_pf和sp_ps对象地址的打印信息可以验证。
那么当退出时,肯定先释放sp_ps对象,再释放sp_pf对象。test1可以确认——先释放zcx,再释放zjz。
然而test2中好像正好颠倒,怎么回事呢???
答案是,仍然成立!退出test2()时,先释放sp_ps,再释放sp_pf。
在释放sp_ps时,发现其引用的这个内存对象new Person("zcx"),还同时被别人(sp_pf)引用,只能将son的引用计数减一,而不能释放,因此什么打印都没有。
在释放sp_pf时,先进入构造函数~Person(),再释放其member var。因此先有打印信息:“dtor zjz”,再释放内部的另外成员变量(对象)——son,因此后有信息“dtor zcx”。
这里面涉及到C++中另外一个知识点:构造/析构先后顺序。——读者可以回顾类对象构造时,是先进入构造函数,还是先构造内部的member。析构和构造正好颠倒。写一个demo进行了折叠,读者自己去验证。
 #include <iostream>
#include <memory>
#include <string> using namespace std; class tmp {
public:
tmp() {
cout<<"tmp ctor"<<endl;
}
~tmp() {
cout<<"tmp dtor"<<endl;
}
}; class Person {
public:
Person() {
cout<<"Person ctor"<<endl;
} Person(const string &alias): name(alias) {
cout<<"Person ctor for "<<name.c_str()<<endl;
} ~Person() {
cout<<"Person dtor for "<<name.c_str()<<endl;
} void setFather(shared_ptr<Person> &p) {
father = p;
} void setSon(shared_ptr<Person> &p) {
son = p;
} void printName() {
cout<<"name: "<<name.c_str()<<endl;
} private:
const string name;
shared_ptr<Person> father;
shared_ptr<Person> son;
tmp mtmp;
}; int main(void)
{
Person *pp = new Person("sequence");
delete pp; return ;
}

3.test1中的好像没有释放?

是的。在setFather/Son前,refCnt=1,之后变成了2。当退出test1()时,两块heap内存new Person("zjz")和new Person("zcx")的ref减一变成1,但都因互相引用对方而无法释放。这时需要引入另外一种智能指针——weak_ptr。

weak_ptr的引入:

weak_ptr是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。没有重载*和->但可以使用lock获得一个可用的shared_ptr对象。

为什么要引入“弱引用”指针呢?

weak_ptr和shared_ptr是为解决heap对象的“所有权”而来。弱引用指针就是没有“所有权”的指针。有时候我只是想找个指向这块内存的指针,但我不想把这块内存的生命周期与这个指针关联。这种情况下,弱引用指针就代表“我指向这东西,但这东西什么时候释放不关我事儿……”

使用区别

首先,不要把智能指针和祼指针的区别看得那么大,它们都是指针。因此,我们可以把智能指针和祼指针都统称为指针,它们共同的目标是通过地址去代表资源。既然指针能代表资源,那么不可避免地会涉及资源的所有权问题。在选择具体指针类型的时候,通过问以下几个问题就能知道使用哪种指针了。

1.指针是否需要拥有资源的所有权?

如果指针变量需要绑定资源的所有权,那么会选择unique_ptr或shared_ptr。它们可以通过RAII完成对资源生命期的自动管理。如果不需要拥有资源的所有权,那么会选择weak_ptr和raw pointer,这两种指针变量在离开作用域时不会对其所指向的资源产生任何影响。

2.如果指针拥有资源的所有权(owning pointer),那么该指针是否需要独占所有权?

独占则使用unique_ptr(人无我有,人有我丢),否则使用shared_ptr(你有我有全都有)。这一点很好理解。

3.如果不拥有资源的所有权(non-owning pointer),那么指针变量是否需要在适当的时候感知到资源的有效性?

如果需要则使用weak_ptr,它可以在适当的时候通过weak_ptr::lock()获得所有权,当拥有所有权后便可以得知资源的有效性。

c++智能指针介绍的更多相关文章

  1. c++智能指针介绍_补充

    不明白我做错了什么,这几天老婆给我冷战了起来,也不给我开视频让我看娃了..哎,心累!趁着今晚的一些空闲时间来对智能指针做个补充吧. 写完上篇“智能指针介绍”后,第二天上班途中时,突然一个疑问盘踞在心头 ...

  2. auto_ptr,shared_ptr 智能指针的使用

    Q: 那个auto_ptr是什么东东啊?为什么没有auto_array?A: 哦,auto_ptr是一个很简单的资源封装类,是在<memory>头文件中定义的.它使用“资源分配即初始化”技 ...

  3. 19.C++-(=)赋值操作符、智能指针编写(详解)

    (=)赋值操作符 编译器为每个类默认重载了(=)赋值操作符 默认的(=)赋值操作符仅完成浅拷贝 默认的赋值操作符和默认的拷贝构造函数有相同的存在意义 (=)赋值操作符注意事项 首先要判断两个操作数是否 ...

  4. C++智能指针shared_ptr

    shared_ptr 这里有一个你在标准库中找不到的—引用数智能指针.大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章.最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引 ...

  5. 详解C++11智能指针

    前言 C++里面的四个智能指针: auto_ptr, unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持,并且第一个已经被C++11弃用. C++11智能指针介 ...

  6. 【原/转】【boost】智能指针使用规则以及介绍

    智能指针机制跟Objective-C里面的retainCount引用计数有着相同的原理,当某个对象的引用计数为0是执行delete操作,类似于autorelease 初学者在使用智能指针时,很多情况下 ...

  7. React Native 4 for Android源码分析 一《JNI智能指针之介绍篇》

    文/ Tamic: http://blog.csdn.net/sk719887916/article/details/53455441 原文:http://blog.csdn.net/eewolf/a ...

  8. Android智能指针SP WP使用方法介绍

    Android手机操作系统既然是开源的操作系统.那么在具体的文件夹中就会存放着各种相关功能的开源代码.我们在使用的时候可以根据这些源代码进行相应的修改就能轻松的完成我们所需的功能.在这里大家就一起来看 ...

  9. 基于C/S架构的3D对战网络游戏C++框架 _05搭建系统开发环境与Boost智能指针、内存池初步了解

    本系列博客主要是以对战游戏为背景介绍3D对战网络游戏常用的开发技术以及C++高级编程技巧,有了这些知识,就可以开发出中小型游戏项目或3D工业仿真项目. 笔者将分为以下三个部分向大家介绍(每日更新): ...

随机推荐

  1. 【干货干货】hyperledger fabric 之动态添加组织/修改配置 (Fabric-java-sdk) 下

    我们接着上一节来讲: 在熟悉动态增加组织或修改配置的步骤后,我们就可以使用java的api来完成动态增加组织或修改配置了: 废话不多说,直接上干货: 1,预制条件 org3的证书以及组织3的MSP详情 ...

  2. SpringMvc最全的约束——你的感冒清个人总结

    SpringMvc最全的约束--你的感冒清个人总结 <?xml version="1.0" encoding="UTF-8"?> <beans ...

  3. [leetcode] 96 Unique Binary Search Trees (Medium)

    原题 字母题 思路: 一开始妹有一点思路,去查了二叉查找树,发现有个叫做卡特兰数的东西. 1.求可行的二叉查找树的数量,只要满足中序遍历有序. 2.以一个结点为根的可行二叉树数量就是左右子树可行二叉树 ...

  4. python购物车升级版

    各文件内容 前言 功能架构等请参考前一篇博客,此篇博客为进阶版的存代码展示. 详细文件内容 启动文件 starts.py启动文件 import os import sys BASE_DIR = os. ...

  5. Vue 报错 listen EADDRINUSE :::8080

    今天在重启vue项目的时候,发现报了错, listen EADDRINUSE :::8080错误提示 原因:因为另一个项目占用了8080端口,我直接在命令行npm run dev第二个项目,就给出了这 ...

  6. 章节十五、5-记录日志---Log4j

    一.为什么要用Log4j记录日志? 日志记录对于任何应用程序都非常重要. 它可以帮助我们快速调试代码,通过收集代码执行的信息让代码容易维护. 二.Log4j 是什么? Apache为Java提供的日志 ...

  7. ProcessBuilder waitFor 调用外部应用

    小程序项目最初使用ffmpeg转换微信录音文件为wav格式,再交给阿里云asr识别成文字.视频音频转换最常用是ffmpeg. 1 ffmpeg -i a.mp3 b.wav 相关文章: 小程序实现语音 ...

  8. 夯实Java基础(六)——包装类

    1.包装类简介 我们都知道Java是面向对象编程语言,包含了8种基本数据类型,但是这8种基本数据类型并不支持面向对象的特征,它们既不是类,也不能调用方法.这在实际使用时存在很多的不便,比如int类型需 ...

  9. 微信公众号接入服务器验证(Go实现)

    1 基本流程 将token.timestamp.nonce三个参数进行字典序排序 将三个参数字符串拼接成一个字符串进行sha1加密 开发者获得加密后的字符串可与signature对比,标识该请求来源于 ...

  10. c++随笔之编译器编译原理

    /* C++编译器原理:1)首先明白声明与定义是两个不同的概念 extern int i;是声明,int i;是定义 函数就更简单了2)编译分为: 预编译:将宏替换,include等代码拷贝过来 编译 ...