一.智能指针及RAII

问题:
  C++中最令人头疼的问题是强迫程序员对申请的资源(文件,内存等)进行管理,一不小心就会出现泄露(忘记对申请的资源进行释放)的问题。

C++的解决办法:RAII
  在传统 C++ 里我们只好使用 newdelete 去『记得』对资源进行释放。而 C++11 引入了智能指针的概念,使用了引用计数的想法,让程序员不再需要关心手动释放内存。

解决思路:
  利用C++中一个对象出了其作用域会被自动析构,因此我们只需要在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间,这样,就减轻了程序员在编码过程中,考虑资源释放的问题,这就是RAII
  RAII,完整的英文是 Resource Acquisition Is Initialization,是 C++ 所特有的资源管理方式。

  • 有少量其他语言,如 D、Ada 和 Rust 也采纳了 RAII,但主流的编程语言中, C++是唯一一个依赖RAII来做资源管理的。
  • RAII依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理。对 RAII的使用,使得C++不需要类似于Java 那样的垃圾收集方法,也能有效地对内存进行管理。

  具体而言,C11的stl中为大家带来了3种智能指针,正确合理的使用可以有效的帮助大家管理资源,当然,在C++的使用智能指针没有像Java,python这种具备垃圾回收机制的语言那么舒适,毕竟,程序员还需要做一些额外的事情,但是,这远比传统的C或者C++更加优雅。

3种智能指针分别是:

  • std::shared_ptr 强指针
  • std::unique_ptr
  • std::weak_ptr 弱指针

在早期有一个auto_ptr,这四种指针在使用上有区别:

  • auto_ptr有缺陷是过时的产物。

  • unique_ptr对auto_ptr的问题进行了修正。

  • shared_ptr使用了引用计数,但是会出现循环引用的问题需要配合后面的weak_ptr一起使用。

二.引用计数

引用计数

  要正确的理解智能指针,首先必须理解引用计数技术。

深拷贝、浅拷贝的概念

  • 深拷贝优缺点:

    • 优点:每一个的对象(哪怕是通过拷贝构造函数实例化的对象)的指针都有指向的内存空间,而不是共享,所以在对象析构的时候就不存在重复释放或内存泄露的问题了。
    • 缺点:内存开销大
  • 浅拷贝优缺点:

    • 优点:通过拷贝构造函数实例化的对象的指针数据变量指向的共享的内存空间,因此内存开销较小。
    • 缺点:对象析构的时候就可能会重复释放或造成内存泄露。

  鉴于深拷贝和浅拷贝的优缺点,可采用引用计数技术,既减小了内存开销,又避免了堆的重复释放或内存泄露问题。

例1:

例1可能在VS2015编译不过,需要在属性管理器页面进行修改

重复释放:一旦其中一个对象释放了资源,那么所有的其他对象的资源也被释放了。


附例1代码:

#include <iostream>
#include <cstring>
using namespace std;class CStudent
{public:CStudent(const char* pszName);CStudent(CStudent& obj);//拷贝构造CStudent& operator=(CStudent& obj);//拷贝赋值void release();void Show()//打印出变量以及地址{cout << hex << (int&)m_pszName << m_pszName <<endl;}
private:char* m_pszName;
};CStudent::CStudent(const char* pszName)
{m_pszName = new char[256];strcpy(m_pszName, pszName);
}CStudent::CStudent(CStudent& obj)//浅拷贝
{m_pszName = obj.m_pszName;//strcpy(m_pszName, obj.m_pszName);
}CStudent& CStudent::operator=(CStudent& obj)//浅拷贝
{m_pszName = obj.m_pszName;return *this;
}void CStudent::release()
{if (m_pszName != NULL){delete m_pszName;m_pszName = NULL;}
}int main(int argc, char* argv[])
{CStudent stu1("zhang san");CStudent stu2("li si");CStudent stu3 = stu2;//拷贝构造,stu2和stu3使用的是同一个资源stu1.Show();stu2.Show();stu3.Show();stu2.release();//在释放stu2的时候把stu3也给释放了,造成了重复释放的问题stu3.Show();return 0;
}

解决方法:增加一个变量,记录资源使用的次数


例2:添加引用计数器

附上例代码:

#include <iostream>
#include <cstring>
using namespace std;class CStudent
{public:CStudent(const char* pszName);CStudent(CStudent& obj);~CStudent();CStudent& operator=(CStudent& obj);void release();void Show(){if (*m_pCount > 0){cout << hex << (int&)m_pszName << m_pszName <<endl;}}
private:char* m_pszName;int* m_pCount;//资源计数器,当资源计数器减为0时,那么表示该资源可以被释放,从而避免重复释放的问题。
};CStudent::CStudent(const char* pszName)
{m_pszName = new char[256];m_pCount  = new int;strcpy(m_pszName, pszName);*m_pCount = 1;//创建资源的时候同时创建了资源计数器,初始为1
}CStudent::CStudent(CStudent& obj)//浅拷贝
{m_pszName = obj.m_pszName;m_pCount = obj.m_pCount;(*m_pCount)++;}CStudent::~CStudent()
{release();
}CStudent& CStudent::operator=(CStudent& obj)
{if (obj.m_pszName == m_pszName){return *this;}if (--(*m_pCount) == 0){delete m_pszName;m_pszName = NULL;delete m_pCount;}m_pszName = obj.m_pszName;m_pCount = obj.m_pCount;(*m_pCount)++;return *this;
}void CStudent::release()
{if (m_pszName != NULL && --*m_pCount == 0){//通过计数器来避免资源的重复释放的问题delete m_pszName;m_pszName = NULL;delete m_pCount;}
}int main(int argc, char* argv[])
{CStudent stu1("zhang san");CStudent stu2("li si");CStudent stu3 = stu2;stu1.Show();stu2.Show();stu3.Show();stu2.release();stu3.release();stu3.Show();return 0;
}

  但上述写法的缺点是:资源计数器和类绑在一起了,如果还有一个新的类要使用引用计数器不方便。
  最后,我们将该引用计数做一个简易的封装,也就是把引用计数作为一个新的类来使用:

例3:引用计数器封装

#include <iostream>
#include <cstring>
using namespace std;struct RefValue
{RefValue(const char* pszName);~RefValue();void AddRef();void Release();char* m_pszName;int   m_nCount;
};RefValue::RefValue(const char* pszName)
{m_pszName = new char[strlen(pszName)+1];m_nCount = 1;}RefValue::~RefValue()
{if (m_pszName != NULL){delete m_pszName;m_pszName = NULL;}
}void RefValue::AddRef()
{m_nCount++;
}void RefValue::Release()
{if (--m_nCount == 0){delete this;}
}class CStudent
{public:CStudent(const char* pszName);CStudent(CStudent& obj);~CStudent();CStudent& operator=(CStudent& obj);void release();void Show(){if (m_pValue->m_nCount > 0){cout << hex << (int&)m_pValue->m_pszName << m_pValue->m_nCount <<endl;}}private:RefValue* m_pValue;
};CStudent::CStudent(const char* pszName)
{m_pValue = new RefValue(pszName);
}CStudent::CStudent(CStudent& obj)
{m_pValue = obj.m_pValue;m_pValue->AddRef();
}CStudent::~CStudent()
{release();
}CStudent& CStudent::operator=(CStudent& obj)
{if (obj.m_pValue == m_pValue){return *this;}m_pValue->Release();m_pValue = obj.m_pValue;m_pValue->AddRef();return *this;
}void CStudent::release()
{m_pValue->Release();
}int main(int argc, char* argv[])
{CStudent stu1("zhang san");CStudent stu2("li si");CStudent stu3 = stu2;stu1.Show();stu2.Show();stu3.Show();stu2.release();//stu3.release();stu3.Show();stu3.release();return 0;
}

上面的做法能在一定程度上解决资源多次重复申请的浪费,但是仍然存在两个核心的问题:

  1.   如果对其中某一个类对象中的资源进行了修改,那么所有引用该资源的对象全部会被修改,这显然是错误的。
  2.   当前的计数器作用于Student类,在使用时候,需要强行加上引用计数类,这样复用性不好,使用不方便。

三.写时拷贝

问题:如果共享资源中的值发生了变化,那么其他使用该共享资源的值如何保持不变?

解决思路:使用引用计数时,当发生共享资源值改变的时候,需要对其资源进行重新的拷贝,这样改变的时拷贝的值,而不影响原有的对象中的共享资源。

写时拷贝(COW copy on write),在所有需要改变值的地方,重新分配内存。

例4:

#include <iostream>
#include <cstring>struct RefValue
{RefValue(const char* pszName);~RefValue();void AddRef();void Release();char* m_pszName;int   m_nCount;
};RefValue::RefValue(const char* pszName)
{m_pszName = new char[256];    strcpy(m_pszName, pszName);m_nCount = 1;
}RefValue::~RefValue()
{if (m_pszName != NULL){delete m_pszName;m_pszName = NULL;}
}void RefValue::AddRef()
{m_nCount++;
}void RefValue::Release()
{if (--m_nCount == 0){delete this;}
}class CStudent
{public:CStudent(const char* pszName);CStudent(CStudent& obj);void SetName(const char* pszName);~CStudent();CStudent& operator=(CStudent& obj);void release();void Show(){if (m_pValue->m_nCount > 0){cout << hex << (int&)m_pValue->m_pszName << m_pValue->m_pszName <<endl;}}
private:RefValue* m_pValue;
};void CStudent::SetName(const char* pszName)
{m_pValue->Release();m_pValue = new RefValue(pszName);
}CStudent::CStudent(const char* pszName)
{m_pValue = new RefValue(pszName);
}CStudent::CStudent(CStudent& obj)
{m_pValue = obj.m_pValue;m_pValue->AddRef();
}CStudent::~CStudent()
{release();
}CStudent& CStudent::operator=(CStudent& obj)
{if (obj.m_pValue == m_pValue){return *this;}m_pValue->Release();m_pValue = obj.m_pValue;m_pValue->AddRef();return *this;
}void CStudent::release()
{m_pValue->Release();
}int main(int argc, char* argv[])
{CStudent stu1("zhang san");CStudent stu2("li si");CStudent stu3 = stu2;stu2.Show();stu3.Show();stu2.SetName("li si2");stu2.Show();stu3.Show();return 0;
}

四.智能指针的原理

  前面,我们学会了如何使用引用计数及写时拷贝,这是理解智能指针必不可少的方法。但是,在实际写代码中,我们其实更倾向于让程序员对于资源的管理没有任何的感知,也就是说,最好让程序员只需要考虑资源的何时申请,对于何时释放以及资源内部如何计数等问题,统统交给编译器内部自己处理。

  智能指针另外一点就是在使用上要像真正的指针一样可以支持取内容, 指针访问成员->等操作,因此,就需要对这些运算符进行重载。

例5:

一.最原始
#include<iostream>
#include<cstring>
using namespace std;//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放class CStudent
{public:CStudent(){}void test(){cout<<"CStudent"<<endl;}
private:char* m_pszBuf;int m_nSex;
};int main()
{CStudent* pStu=new CStudent();if(pStu!=nullptr){delete pStu;pStu=nullptr;}return 0;
}二.//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制来解决资源自动释放的问题
#include<iostream>
#include<cstring>
using namespace std;//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放class CStudent
{public:CStudent(){}void test(){cout<<"CStudent"<<endl;}
private:char* m_pszBuf;int m_nSex;
};//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制
//来解决资源自动释放的问题
//智能指针雏形,需要管理资源
class CSmartPtr
{public://一定要是一个堆对象CSmartPtr(CStudent* pObj){m_pObj=pObj;}~CSmartPtr(){if(m_pObj!=nullptr){delete m_pObj;}}
private:CStudent* m_pObj;//将资源放入智能指针类中,管理起来
};
int main()
{//这里可以完成资源的自动释放CSmartPtr sp(new CStudent());//但是,用起来不像是一个指针return 0;
}三.
#include<iostream>
#include<cstring>
using namespace std;//智能指针:
//1.用起来像指针
//2.会自己对资源进行释放class CStudent
{public:CStudent(){}void test(){cout<<"CStudent"<<endl;}
private:char* m_pszBuf;int m_nSex;
};//创建一个类,利用该类的构造和析构(进出作用域自动被编译器调用)的机制
//来解决资源自动释放的问题
//智能指针雏形,需要管理资源
class CSmartPtr
{public://一定要是一个堆对象CSmartPtr(CStudent* pObj){m_pObj=pObj;}~CSmartPtr(){if(m_pObj!=nullptr){delete m_pObj;}}//需要想办法让其对象用起来像是一个指针//像一个指针的写法CStudent* operator->(){return m_pObj;}CStudent& operator*(){return *m_pObj;}//booloperator bool(){return m_pObj!=nullptr;}
private:CStudent* m_pObj;//将资源放入智能指针类中,管理起来
};
int main()
{//这里可以完成资源的自动释放CSmartPtr sp(new CStudent());//但是,用起来不像是一个指针sp->test();(*sp).test();return 0;
}

五.智能指针的简易实现及添加模板

C++新特性探究(十八):智能指针相关推荐

  1. C++新特性探究(十六):move constructor移动构造

    相关博文: C++新特性探究(十三):右值引用(r-value ref)&&探究 C++新特性探究(十六):move constructor移动构造 C++新特性探究(13.5):右值 ...

  2. C++新特性探究(十五):bind

    相关博文:C++新特性探究(十四):function 相关博文:C++头文件<functional>和bind.placeholders占位符使用简单例子 相关博文:<Essenti ...

  3. C++新特性探究(十四):function

    相关博文:C++头文件<functional>和bind.placeholders占位符使用简单例子 相关博文:<Essential C++>笔记之设计一个泛型算法(二) 相关 ...

  4. 【C++11新特性】 C++11智能指针之weak_ptr

    http://blog.csdn.net/xiejingfa/article/details/50772571 原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/ar ...

  5. c++11新特性_【C++11新特性】 C++11智能指针之weak_ptr

    如题,我们今天要讲的是 C++11 引入的三种智能指针中的:weak_ptr. 在学习 weak_ptr 之前最好对 shared_ptr 有所了解.如果你还不知道 shared_ptr 是何物,可以 ...

  6. C++新特性探究(十):Lambda

    一. 匿名函数   简短函数,就地书写,调用.即Lambda存在的意义,常用于取代作回调用的简短函数指针与仿函数.   就地书写,因只有函数体,即无函数名,也称匿名函数. 格式 最小的 Lambda: ...

  7. 【C++11新特性】 C++11智能指针之shared_ptr

    http://blog.csdn.net/Xiejingfa/article/details/50750037 原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/ar ...

  8. 【C++11新特性】 C++11智能指针之unique_ptr

    原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50759210 在前面一篇文章中,我们了解了C++11中引入的智能指针之一shar ...

  9. C++新特性探究(十二):static_assert(提前判误)

    相关博文:C++之异常处理探究 相关博文:assert.if else.try catch三者的区别 相关博文:C++之assert.NDEBUG探究 相关博文:static_assert和asser ...

最新文章

  1. go语言支持继承吗_Go语言的结构体嵌入为什么不算继承?
  2. 再见,CentOS!
  3. 记录爬取信用中国,里面的行政许可内容,行政处罚,守信激励的内容,并以excel形式显示
  4. 动态系统建模与仿真 基本知识笔记(源自DR_CAN)
  5. 商住楼和住宅楼的区别
  6. 可以做mysql题的网站_牛客网MySQL练习题
  7. 到底什么是移动边缘计算?
  8. HTML深海骑兵制作,深海迷航代码独眼巨人号护盾发生器 | 手游网游页游攻略大全...
  9. 如何为长期旅行做准备
  10. socks5 python_用Python写socks5服务器端
  11. jpg转bmp c语言 linux,C语言实现BMP转换JPG的方法
  12. 玩吧公司,入职后的真实感受
  13. 2022年漏洞事件盘点
  14. 163邮箱注册,163邮箱申请方法
  15. 【电源专题】开关电源的控制器和稳压器的区别
  16. 数论[计算机数学专题(5)]
  17. KAIST : Multispectral Pedestrian Detection: Benchmark Dataset and Baseline
  18. android 8.1 开机壁纸下半部分黑屏2-3秒的问题解决
  19. Android必会的自动化测试
  20. 【论文阅读】SML:标准最大logits

热门文章

  1. Oracle(四)多表查询
  2. 基于JAVA+Servlet+JSP+MYSQL的企业员工投票系统
  3. auth java_java – 使用auth的httpget请求
  4. 更新cocoapods相关问题
  5. 关键字this、new;apply、call、bind方法的用法
  6. ASP.NET Core学习——6
  7. 应用层协议:HTTPS
  8. django系列5.2--ORM数据库的单表操作
  9. 4.kibana部署
  10. Too many open files 问题的解决