IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++新特性(五)智能指针 -> 正文阅读

[C++知识库]C++新特性(五)智能指针

不同于java的垃圾回收机制,C++需要程序员自己申请资源,使用完资源以后还要记得释放资源,例如new和delete要配套使用;文件打开以后使用完要关闭;数据库连接以后使用完要断开…这都是程序员需要额外注意的点。忘记了资源释放,会造成资源泄露。为了做到智能释放使用完的资源,C++有了智能指针的技术。结合类使用了引用计数的想法,使程序员不用再担心资源释放的问题。(RAII思想)
下面先介绍一下RAII思想

1,RAII思想

众所周知,一个类对象创建会自动调用构造函数,析构会自动调用析构函数。因此我们可以利用此机制自动解决资源的申请和释放问题,例如:
在这里插入图片描述
RAII,完整的英文是Resource Acquisition Is Initialization,是C++特有的资源管理方式。在主流的语言中,C++是唯一一个依赖RAII来做资源管理的。RAII依托栈和析构函数,来对所有的资源(包括堆内存在内)进行管理,对RAII的使用,使得C++不需要类似于java那样的垃圾收集方法,也能有效地对内存进行管理。具体而言,C++11的stl中带来了3中智能指针,正确合理的使用可以帮助我们管理资源,当然,在C++中使用智能指针不像在java,python中使用垃圾回收机制那么舒适,毕竟程序员还是需要做一些额外的事情,但这远比原来的C++更加方便。这三种智能指针分别是

  • unique_ptr
  • shared_ptr(强指针)
  • weak_ptr(弱指针)
    而在早期有一个auto_ptr,这四种指针在使用上有区别:auto_ptr是有缺陷过时的产物;unique_ptr对auto_ptr的问题做了修正;shared_ptr使用了引用计数,但是会出现循环引用的问题,需要配合后面的weak_ptr一起使用。
    ——————————————————————
    接下来先来介绍智能指针背后的原理,最后再介绍一下这四个智能指针,相信看到最后的读者一定会有醍醐灌顶的感觉。

2,模拟实现智能指针

2.1 实现指针的自动释放

首先,如果将指针要申请的资源放在一个类当中的话,在析构的时候可以自动释放资源,我们的简单目的已经达成。相当于这个类管理了一个指针,可以让他自动释放,相当于模拟了char* p=new char[255];同时也可以自动释放。

#define _crt_secure_no_warnings
#include<iostream>
#include <cstring>
using namespace std;
class SmartPtr {
private:
	char* m_Pname;
public:
	SmartPtr(const char*a){
		m_Pname = new char[255];
		strcpy(m_Pname, a);
	}
	~SmartPtr()
	{
		if (m_Pname != nullptr)
		{
			delete m_Pname;
		}
	}
};

2.2 实现指针用另一指针赋值

上文仅仅模拟了一个指针的动态申请内存,并且可以做到对该指针做自动释放而已,如果要模拟char* m_Pname2=m_Pname呢?,这条语句m_pname2指向m_Pname指向的一块内存。用类来模拟的话其实就是浅拷贝。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <cstring>
using namespace std;
class SmartPtr {
private:
	char* m_Pname;
public:
	SmartPtr(const char* a) {//在构造函数中动态申请m_Pname资源
		m_Pname = new char[255];
		strcpy(m_Pname, a);
	}
	SmartPtr(SmartPtr& obj) {//拷贝构造函数
		m_Pname = obj.m_Pname;
	}
	SmartPtr& operator=(SmartPtr& obj) {
		m_Pname = obj.m_Pname;
		return *this;
	}
	~SmartPtr()//析构函数
	{
		if (m_Pname != nullptr)
		{
			delete m_Pname;
		}
	}
};

int main()
{
	{
		SmartPtr obj1("张三");//读者可将断点打在此处进行调试进行观察
		SmartPtr obj2 = obj1;
	}//约束在一个块作用域中查看析构
	return 0;
}

现在obj2对象管理的obj2.m_Pname和obj1.m_Pname就指向了同一块堆内存

2.3 解决在实现指针用另一指针赋值过程中的重复释放过程

但是我们又知道浅拷贝在析构时有重复释放的bug,于是就有了引用计数的想法——每拷贝一个对象,引用计数器加一;每个对象析构的时候要查看引用计数器的值,只有引用计数器的值为1时才释放资源。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <cstring>
using namespace std;
class SmartPtr {
private:
	char* m_Pname;
	int* m_pCount;//引用计数器,注意引用计数器设置为int*变量,这样的话每个对象的都操作的自己的指针变量指向
	//的都是同一片内存*m_pCount。不能直接用int类型,而且也不能用static,因为这样的话就相当于只能有一个计数器
	//而指针的话我们以后写时拷贝的话可以再申请计数器,这一点介绍到写时拷贝再看就理解了
public:
	SmartPtr(const char* a) {//在构造函数中动态申请m_Pname资源
		m_Pname = new char[255];
		m_pCount = new int;
		strcpy(m_Pname, a);
		*m_pCount = 1;
	}
	SmartPtr(SmartPtr& obj) {//拷贝构造函数
		m_Pname = obj.m_Pname;
		m_pCount = obj.m_pCount;
		(*m_pCount)++;
	}
	SmartPtr& operator=(SmartPtr& obj) {
		m_Pname = obj.m_Pname;
		m_pCount = obj.m_pCount;
		(*m_pCount)++;
		return *this;
	}
	void release() {
		(*m_pCount)--;
		if (*m_pCount == 0)
		{
			delete m_Pname;
		}
	}
	~SmartPtr()//析构函数
	{
		release();
	}
};

int main()
{
	{
		SmartPtr obj1("张三");//读者可将断点打在此处进行调试进行观察
		SmartPtr obj2 = obj1;
	}//约束在一个块作用域中查看析构
	return 0;
}

可以再将这个引用计数封装成一个类

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <cstring>
using namespace std;

struct RefValue
{
	char* m_Pname;
	int   m_nCount;
	RefValue(const char* pszName) {
		m_Pname = new char[strlen(pszName) + 1];
		strcpy(m_Pname, pszName);
		m_nCount = 1;
	}
	~RefValue() {
		if (m_Pname != nullptr)
		{
			delete m_Pname;
			m_Pname = nullptr;
		}
	}
	void AddRef() { m_nCount++; }
	void Release() {
		if (--m_nCount == 0)
		{
			delete this;//对象必须new创建
		}
	}
};


class SmartPtr {
private:
	RefValue* m_p;
public:
	SmartPtr(const char* a) {//在构造函数中动态申请m_Pname资源
		m_p = new RefValue(a);
	}
	SmartPtr(SmartPtr& obj) {//拷贝构造函数
		m_p = obj.m_p;
		m_p->AddRef();
	}
	SmartPtr& operator=(SmartPtr& obj) {
		m_p = obj.m_p;
		m_p->AddRef();
		return *this;
	}
	void release() {
		m_p->Release();
	}
	~SmartPtr()//析构函数
	{
		release();
	}
};

int main()
{
	{
		SmartPtr obj1("张三");//读者可将断点打在此处进行调试进行观察
		SmartPtr obj2 = obj1;
	}//约束在一个块作用域中查看析构
	return 0;
}

封装一个引用计数器RefValue之后就是用引用计数器去管理m_Pname指针,该引用计数器内部实现引用计数,而用SmartPtr类管理RefValue类。

2.4 实现指针用另一指针赋值过后,另一指针重新申请内存问题

现在解决了浅拷贝重复释放的问题,新的问题是对象拷贝以后可能会有给新对象修改m_Pname的操作,但是拷贝的新对象的m_Pname和被拷贝的旧对象的m_Pname使用的是同一块堆内存,修改了新的拷贝对象的m_Pname指向的值以后,旧对象的m_Pname指向的值也会改变。因此就有了写时拷贝的想法,如果要改变新对象m_Pname指向的值,则为该对象的m_Pname指针重新申请堆内存。
只需加一个函数setNmae即可:

void setName(const char*a) {
		m_p->Release();
		m_p = new RefValue(a);
	}

完整代码如下

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <cstring>
using namespace std;

struct RefValue
{
	char* m_Pname;
	int   m_nCount;
	RefValue(const char* pszName) {
		m_Pname = new char[strlen(pszName) + 1];
		strcpy(m_Pname, pszName);
		m_nCount = 1;
	}
	~RefValue() {
		if (m_Pname != nullptr){
			delete m_Pname;
			m_Pname = nullptr;
		}
	}
	void AddRef() { m_nCount++; }
	void Release() {
		if (--m_nCount == 0){
			delete this;//对象必须new创建
		}
	}
};


class SmartPtr {
private:
	RefValue* m_p;
public:
	SmartPtr(const char* a) {//在构造函数中动态申请m_Pname资源
		m_p = new RefValue(a);
	}
	SmartPtr(SmartPtr& obj) {//拷贝构造函数
		m_p = obj.m_p;
		m_p->AddRef();
	}
	SmartPtr& operator=(SmartPtr& obj) {
		m_p = obj.m_p;
		m_p->AddRef();
		return *this;
	}
	void setName(const char* a) {
		m_p->Release();
		m_p = new RefValue(a);
	}
	void release() {
		m_p->Release();
	}
	~SmartPtr()//析构函数
	{
		release();
	}
};

int main()
{
	{
		SmartPtr obj1("张三");//读者可将断点打在此处进行调试进行观察
		SmartPtr obj2 = obj1;
		SmartPtr obj3 = obj2;//张三的引用计数为3


		obj3.setName("Lisi");//张三的引用计数变为2,李四也有引用计数了,为1
		SmartPtr obj4 = obj3;//张三引用计数还是2,李四引用计数变为2

	}//约束在一个块作用域中查看析构
	return 0;
}

调试过程中观察引用计数器发现的确如此:
在这里插入图片描述

2.5 模拟指针的其他用法

现在我们已经给SmartPtr加了写时拷贝功能,Smartptr最终管理的m_Pname指针不仅可以自动释放;又可以在拷贝时自动引用计数避免最后多次释放;又可以在重写时避免和原来被拷贝对象产生联系;这相当于我们模仿了下面的功能并且还能自动释放指针。

char *m_Pname=new char[255];
char *p=m_Pname;
p=new char[255];

也就是说SmartPtr作为一个智能指针类,这个智能指针类管理了m_Pname这个真正的指针,让它可以自动释放,同时SmartPtr类对象(即SmartPtr智能指针)内部管理的m_Pname指针实现了上面的赋值和重新开辟堆空间的功能。
现在的问题就是智能指针另外一点就是在使用上要像真正的指针一样可以支持取内容*, 指针访问成员->等操作,因此,就需要对这些运算符进行重载。
注意:下面的例子不像前面一样模拟char*类型的指针,而是模拟Cstudent类型的指针。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include <cstring>
using namespace std;
class CStudent
{
public:
    CStudent(){ }
    void test(){cout << "CStudent" << endl;}
private:
    char* m_pszBuf;
    int   m_nSex;
};

class CRefCount
{
    friend class CSmartPtr;
private:
    CStudent* m_pObj;
    int       m_nCount;
    CRefCount(CStudent* pStu){
        m_pObj = pStu;
        m_nCount = 1;
    }

    ~CRefCount(){
        delete m_pObj;
        m_pObj = NULL;
    }
    void AddRef(){
        m_nCount++;
    }
    void Release(){
        if (--m_nCount == 0)
        {
            delete this;
        }
    }
};


class CSmartPtr
{
private:
    CRefCount* m_pRef;
public:

    CSmartPtr(){
        m_pRef = NULL;
    }

    CSmartPtr(CStudent* pStu){
        m_pRef = new CRefCount(pStu);
    }

    ~CSmartPtr(){
        m_pRef->Release();
    }

    CSmartPtr(CSmartPtr& obj){
        m_pRef = obj.m_pRef;
        m_pRef->AddRef();
    }

    CSmartPtr& operator=(CSmartPtr& obj){
        if (m_pRef == obj.m_pRef){
            return *this;
        }
        if (m_pRef != NULL){
            m_pRef->Release();
        }
        m_pRef = obj.m_pRef;
        m_pRef->AddRef();
        return *this;
    }

    void test2(){
        cout << "test2" << endl;
    }
    CStudent* operator->(){//注意是类成员函数,所以内部传入了this指针
        return m_pRef->m_pObj;//返回
    }
    CStudent** operator&(){//
        return &m_pRef->m_pObj;//返回真正管理的指针的地址,就是一个Cstudent**类型的二重指针
    }
    CStudent& operator*(){
        return *m_pRef->m_pObj;
    }
    operator CStudent* (){
        return m_pRef->m_pObj;
    }
};

int main(int argc, char* argv[])
{
    
    CSmartPtr obj = new CStudent;//相当于CSmartPtr objs(new CStudent);有些同学可能对这种语法有疑问,具体看我前面的博客
    //相当于CStudent* obj=new CStudent; 但是不用担心释放的问题
    CSmartPtr obj4 = new CStudent;
    //相当于CStudent* obj4=new CStudent; 但是不用担心释放的问题

    obj4->test();//相当于obj4.m_pRef->m_pObj->test();这样调用,不过SmartPtr类中重载了->运算符
                  //注意按照运算符重载的过程,return m_pRef->m_pObj;所以obj4->test()就好像变成了
                 //obj4.m_pRef->m_pObj test(),还缺一个->运算符,所以貌似应该重载->->运算符,不过C++没有这
                 //种用法,所以编译器能够识别这种重载
    CSmartPtr obj2 = obj;
    //相当于CStudent* obj2=obj;但是不用担心释放的问题
    return 0;
}

到这里我们就实现了


Cstudent *obj=new Cstudent;
Cstudent *obj1=obj;
obj1=new Cstudent;
cout<<obj.m_name;
cout<<*obj<<endl;
cout<<&obj<<endl;
顺便还可以访问p内部的成员

2.6 为智能指针类添加模板,使其可以作为任意类型的智能指针。

也就是说,通过CSmartPtr类,模拟了Cstudent类的指针,并且还可以自动释放,也就是CSmartPtr是一个Cstudent类的智能指针。现在我们只需要将CSmartPtr类设置成模板类,那我们就可以模拟任意类型的智能指针了。

3,四种智能指针的使用(明天再写完,应该没人再我发布第一天就看到这里吧)

有了前面的内容铺垫,下面的智能指针的使用就非常容易理解了。

3.1 auto_ptr

3.2 unique_ptr

3.3 shared_ptr(强指针)

3.4 weak_ptr(弱指针)

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-02-04 10:51:31  更:2022-02-04 10:53:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 8:56:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码