C++ 智能指针(auto_ptr/unique_ptr/shared_ptr)

目录

为什么需要智能指针?

智能指针的原理

1. RAII特性

2. 重载operator* 和 opertaor->

auto_ptr(已废弃)

常用接口

auto_ptr总结

unique_ptr

常用接口

unique_ptr总结

hared_ptr

常用接口

shared_ptr原理

shared_ptr线程安全的问题

shared_ptr循环引用问题

循环引用解决方法 

shared_ptr删除器


 一个指针还能智能???  在没学习智能指针之前, 听着还挺有噱头..

智能指针智能在哪儿?

用C/C++的都知道, 堆区申请的内存, 需要我们手动释放, 否则会造成资源泄露(内存泄漏) .

C中动态内存分配,   戳链接  ( ̄︶ ̄)↗ https://blog.csdn.net/qq_41071068/article/details/90741413
C++中动态内存分配, 戳链接( ̄︶ ̄)↗ https://blog.csdn.net/qq_41071068/article/details/102791293

其实, 智能指针就智能在, 如果用智能指针管理指向堆区内存的指针 那就不需要我们手动释放在堆区申请的内存了. 伴随着

智能指针生命周期的结束而自动释放. 至于怎么自动释放的, 具体改如何使用, 下面慢慢来看.

为什么需要智能指针?

                                                        

: 普通指针我用的好好的又给我                                       大佬                                                                 
搞出来一个智能指针, 闹哪样 ?

我为什么会心虚(手动捂脸)?  首先, 我是个小菜鸡, 其次, 还有一个困扰众多C/C++程序员的问题 .

这个问题就是在堆区申请内存的问题, 这能有什么问题呢? malloc/calloc/realloc申请的用free释放, new 申请的用delete释放不就

完了? 额, 确实是这样的..... 不过总会有一些特殊情况. 比如, 忘了释放了(手动滑稽), 或者在释放之前存在有抛异常的情况.

什么?  忘了也是理由? 内心: 我可不会犯这样的低级错误. 不过说实在的, "忘了"这种错误虽然低级, 但却没几个人敢保证永不会犯.

1. 当一个函数中有多处返回时(或一个进程中有多处退出时), 就很有可能忘掉了释放. 例如下面代码.

int func() {
    int* p = (int*)malloc(1000000);
    int* p2 = (int*)malloc(1000000);
    if(xx){
        free(p);
        free(p2);
        return 0;
    }
    /*code*/
    if(xx){
        return 1;
    }
    /*code*/
    if(xx){
        return 2;
    }
    /*code*/
    if(xx){
        return 3;
    }
    /*code*/
    if(xx){
        return 4;
    }
    /*code*/
    //....
    free(p);
    free(p2);
    return 1000;
}

上面的代码显然是不合适的,  应该要在每个return 之前都free()一下, 但实际开发中, 代码可不止这几行, 成千上万行代码, 函数返

回可能会有多处,  堆区申请的内存也不止一块, 当敲上一天的代码后, 浑浑噩噩的程序员忘掉free/delete, 也是有可能的, 但这样的

错误并不会编译报错, 不会影响程序逻辑, 很难察觉, 但当程序一直运行, 就可能会导致堆区碎片过多, 机器越来越卡, 直到无法再

申请到堆区内存. 导致程序奔溃, 甚至导致宕机, 只能让机器重启.  尤其是服务器端的程序, 因为服务器生来就是为了长期运行的,

再小的内存碎片, 就算是1字节, 时间长了也会造成一样严重的后果. 内存泄漏简直就是C/C++程序员永远的敌人. 好在C++有智能

指针,可以帮助程序猿解决内存泄漏的问题, 写C++的就偷着乐吧, 毕竟C可没有这玩意 .

2. 还有就是抛异常的情况, 如下面代码 :

int func(){
    int* p = (int*)malloc(10000000);
    //code
    //抛异常
    //code
    free(p);
    return 0;
}

代码没有运行到释放内存的操作(free)时就已经抛出异常, 进行异常处理了, 这样, 在堆区申请的空间也就没有释放 .

为了避免可能会发生的内存泄漏问题,  智能指针应运而生 .

智能指针的原理

智能指针的原理有两个方面, 这两个方面都依赖与C++的语法特性.

1. RAII特性

前面说到智能指就针智能在不需要我们手动释放在堆区申请的内存, 而会伴随着智能指针的生命周期的结束而自动释放 .这正是利

用了RAII 特性. RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网

络连接、互斥量等等)的简单技术 .

我们知道, 局部变量(注意,不包括静态变量)是存在于函数栈中的, 当函数执行完毕, 伴随着整个函数栈的释放, 局部变量也

都会释放, 当局部变量为对象时,  同样在函数执行完毕, 这个对象也会释放内存空间, 但对象在释放内存时系统会自动调

用类的析构函数来清理一些资源. 所以说, RAII 特性就是 利用释放对象时系统会自动调用析构函数的特点 .

具体怎样操作呢, 简单来说, 就是用一个类, 简单的把原来的普通指针封装起来, 用对象来代替原来的指针. 在析构函数中释放堆区

内存, 当对象生命周期结束时, 系统调用析构函数时, 堆区内存也就被释放掉. 代码如下:

template<class T>
class Smart_Ptr {
    T* m_ptr;
public:
    Smart_Ptr(T* ptr = nullptr)
        :m_ptr(ptr) 
    {}
    ~Smart_Ptr() {
        if (m_ptr) {
            delete m_ptr;
        }
    }
};

2. 重载operator* 和 opertaor->

我们利用对象的特性, 用类把原来的指针封装起来了, 但这个对象却并没有原来指针的特性. 什么特性呢?

指针最基本的就是解引用 * 了, 任何类型的指针都可以进行* 解引用.  所以需要重载解引用运算符 *  

但有时候我们还需要用到普通指针本身的值进行一些其他操作, 比如 自定义类型的指针可以用 -> 访问其成员, 但封装后的类并没

有这个功能, 所以还需要重载-> , 重载的 operator->() 返回的是原生指针的值, 那么访问一个自定义类型的成员变量时就应该如下:

class A{
public:
    int m_data;
}:
Smart_Ptr<A> p = new A;
p.operator->()->m_data = 10;

这样写也没错, 但麻烦不直观, 所以C++语法将其优化成了 p->m_data = 10; 就可以方便直观的使用了

Smart_Ptr 具体实现如下 :

template<class T>
class Smart_Ptr {
    T* m_ptr;
public:
    Smart_Ptr(T* ptr = nullptr)
        :m_ptr(ptr) 
    {}
    ~Smart_Ptr() {
        if (m_ptr) {
            delete m_ptr;
        }
    }
    T& operator*() { return *m_ptr; }
    T* operator->() { return m_ptr; }
    T* get() { return m_ptr; }
};

这样, 就完成了一个乞丐版的智能指针 .

但这还没完,  智能指针起初也并不太智能. 像人工智能一样, 发展初期, 人们会亲切的叫人工智能为人工智障 .

我们先来试一下这个简易版的智能指针, 再来看看智能指针如何一步步更加智能.

用上面的代码作为SmartPtr.h头文件

#include<iostream>
#include"Smart_Ptr.h"
using namespace std;
class A {
public:
	int m_data;
	~A() {
		cout << "析构\n";
	}
};
A* test() {
	A* p = new A;
	Smart_Ptr<A> sp(p);
	(*sp).m_data = 10;
	cout << (*sp).m_data << endl;
	cout << sp.operator->()->m_data << endl;
	cout << sp->m_data << endl;
	sp->m_data = 20;
	cout << (*sp).m_data << endl;
	cout << sp.operator->()->m_data << endl;
	cout << sp->m_data << endl;
	return sp.get();
}
int main() {
	A* p = test();
	cout << p->m_data << endl;
	system("pause");
	return 0;
}

可以看到, 重载后的 operator*() 和operator->()都可以像原生指针一样正常使用, 当test()函数调用结束后, 智能指针对象被析构时,

在堆上申请的空间被delete,  delete将这块堆区内存置为0xffffdddd(也就是-572662307)(这个值取决于编译器), 这是为了数据安全

性, 释放了之后就不能再再被访问到,  虽然此时这块内存的状态是没有被申请的, 对这块内存的访问是未定义行为, 可能会导致

程序奔溃, 但也有很大可能会向上面程序一样不会奔溃.  不过可以确定的是这块堆区内存已经被释

放. 我们再来看看C++库中初代的智能指针 auto_ptr.


auto_ptr(已废弃)

C++98版本的库中就提供了auto_ptr智能指针. 现已经被废弃了.

C++库中的智能指针都定义在memory这个头文件中

常用接口

T* get();
T& operator*();
T* operator->();
T& operator=(const T& val);
T* release();
void reset (T* ptr = nullptr);
  • T 是模板参数, 也就是传入的类型
  • get() 用来获取auto_ptr封装在内部的指针, 也就是获取原生指针
  • operator*() 重载* , operator->() 重载了->,  operator=()重载了=
  • realease() 将auto_ptr封装在内部的指针置为nullptr, 但并不会破坏指针所指向的内容, 函数返回的是内部指针置空之前的值
  • 直接释放封装的内部指针所指向的内存, 如果指定了ptr的值, 则将内部指针初始化为该值 (否则将其设置为nullptr)

我们先来用一下

#include<iostream>
#include<memory>
using namespace std;
class A {
public:
	int m_data;
};
int main() {
	auto_ptr<A> ap(new A);
	ap->m_data = 10;
	cout << ap->m_data << endl;
	auto_ptr<A> ap2(ap);
	cout << ap2->m_data << endl;
	cout << ap->m_data << endl;
	return 0;
}

一用就出错 ?? 

像上面的代码, 当一个auto_ptr的智能指针对象拷贝其他指针的值后, 之前的的auto_ptr就失效了, 这里的拷贝指的是拷贝构造

和对象之间的赋值(即赋值运算符重载) . auto_ptr为什么要这样做呢? 事出反常必有妖, 原因就是堆区内存不能重复释放, 但当多个

个auto_ptr智能指针都指向同一片堆区内存时, 每一个auto_ptr智能指针最终都会释放, 这就会导致重复释放的问题. 所以为了避

免这种bug产生, auto_ptr索性采取一种托管的思想, 指针只有一份, 给你我就没有了, 即在拷贝之后, 直接让旧的失效. 这样就避

免的重复释放的问题. 但也导致了之前的智能指针不能用.

再来看下面代码 

#include<iostream>
#include<memory>
using namespace std;
class A {
public:
	int m_data;
	~A() {
		cout << "析构\n";
	}
};
int main() {
	auto_ptr<A> ap(new A[100]);
	return 0;
}

又出错? 这么简单的代码还出错? 这时因为auto_ptr析构函数是用的delete , 当用new [] 申请自定义类型的空间时, 用delete释放就

会出错. 那为什么auto_ptr析构中不用delete[] 呢? 同样, new申请单个自定义类型, 用delete [] 释放也会出错 .(注意, 只有自定义类

型时new/new[]和delete/delet[]混用会造成程序奔溃, 而预定义类型如int, char, float等就不会) .

用delete释放new[ ]以及delete[ ]释放new的问题
写在另一篇博客中, 戳链接( ̄︶ ̄):https://blog.csdn.net/qq_41071068/article/details/103149856

auto_ptr总结

1. auto_ptr存在拷贝(包括赋值)之后, 之前的auto_ptr就不能再使用了

2. auto_ptr在释放堆区内存时, 统一用的delete, 当指针是自定义类型并用new []申请时, 就会出错

由于auto_ptr打着智能的旗号, 干着不靠谱的事 , 所以很多公司都规定不能使用auto_ptr .

所以 C++11中开始提供更靠谱的 unique_ptr.


unique_ptr

unique_ptr的设计思路就非常的清奇了, 你们嫌弃auto_ptr的"拷贝" 问题是吧, 所谓眼不见心不烦, 那我不让你们 "拷贝" ,也就是不

让拷贝和赋值, 处理非常简单粗暴.  具体到unique_ptr中, 是怎样不让"拷贝"的呢?  C++98是将unique_ptr的拷贝构造函数和赋值运

算符重载函数私有化(因为自己不定义, 编译器也会自动生成默认的, 所以要先显式定义,不让编译器默认生成, 再将其私有, 用户就

调用不到了), 而在C++11之后, 是将拷贝构造和赋值运算符重载删除, 怎样删除呢? 如下 :

unique_ptr(const unique_ptr&) = delete;  unique_ptr& operator=(const unique_ptr&) = delete;

C++11之后支持用 函数声明 = delete关键字的形式表示删除默认成员函数 .

为了解决auto_ptr释放内存时的问题, unique_ptr还引入了删除器, 用户可以自己传入释放的方法, 就不会出现像auto_ptr的错误了.

常用接口

说明: T是类型, D是删除器类型(也就是函数指针类型)

成员函数
T* get();
获取unique_ptr封装的原生指针

deleter_type& get_deleter();
返回删除器

operator bool();
判断封装的指针是否为空, 也就是判断unique_ptr有没有管理一个指针

T& operator*();
重载*

T* operator->();	
重载->

unique_ptr& operator= (unique_ptr&& x);
将右操作数x内部封装的指针交给做操作数(this)管理, 右操作数x内部封装的指针置空

unique_ptr& operator= (nullptr_t);
将这个unique_ptr内部封装的指针置空, 再置空前释放掉其所指的空间

T& operator[](size_t i);
重载[]

bool operator==/!=/>/>=/</<= ();
重载>, >=, <, <=, ==, != 号

T* release();
将unique_ptr封装在内部的指针置为nullptr, 但并不会破坏指针所指向的内容, 函数返回的
是内部指针置空之前的值, 也就是取消对原生指针的管理

void reset(T* ptr = nullptr);
直接释放封装的内部指针所指向的内存, 如果指定了ptr的值, 则将内部指针初始化为该值(否则将其设置为nullptr), 作用相当于提前析构

void swap(unique_ptr& x);
交换两个unipue_ptr

非成员函数(友元函数)
void swap (unique_ptr<T,D>& x, unique_ptr<T,D>& y);
交换两个unipue_ptr

值得注意的是unique_ptr重载了[] , 重载了>, >=, <, <=, ==, != . 更加方便了我们的使用, 但我们发现其中有两个=重载函数

unique_ptr& operator= (unique_ptr&& x); 和 unique_ptr& operator= (nullptr_t); 这两个=运算符重载函数并不会造成前面所说

的"拷贝"问题, 将一个unique_ptr对象赋值给另一个unique_ptr对象时并不会调用这两个函数 .

还有就是引入了删除器, 所谓删除器就是自己定义的内存释放方法, 在unique_ptr中的删除器需要传入函数指针和函数指针类型.

在模板参数的第二个参数是函数指针的类型, 构造函数的第二个参数是函数指针. 

#include<iostream>
#include<memory>
using namespace std;
class A {
public:
	int m_data;
	~A() {
		cout << "析构\n";
	}
};
void deleter(A* p) {
	cout << "deletet调用";
	delete[] p;
}
void test() {
	A* p = new A;
	unique_ptr<A> up1(p);
	unique_ptr<A> up2;
	unique_ptr<A> up3;
	up1->m_data = 10;

	//调用unique_ptr& operator=(unique_ptr&& x);
	cout << "调用unique_ptr& operator=(unique_ptr&& x)\n";
	up2 = unique_ptr<A>(new A);//情形1

	cout << up1->m_data << endl;
	cout << up1.get() << endl;
	up3 = move(up1);//情形2
	cout << up3->m_data << endl;
	cout << up1.get() << endl;

	//调用unique_ptr& operator=(nullptr_t);
	cout << "调用unique_ptr& operator=(nullptr_t)\n";
	up2 = nullptr; //只能=nullptr, NULL, 0等值为0的常量
	cout << up1.get() << endl;

	//删除器的使用
	cout << "删除器的使用\n";
	unique_ptr<A, void(*)(A*)> up4(new A[5], deleter);
}
int main() {
	test();
	system("pause");
 	return 0;
}

unique_ptr总结

1. unique_ptr防止 "拷贝" , 所以避免的"拷贝"带来的问题, 但只是剑走偏锋, 并没有真正意义上的解决"拷贝问题"

2. unique_ptr引入了删除器, 就不会出现auto_ptr那样可能会释放出错的问题了

3. unique_ptr有了更多的功能, 比如[]的重载,  判断运算符的重载等, 更加灵活方便


hared_ptr

但C++11并没有止步于unique_ptr, C++11中又新增了比unique_ptr更加靠谱的hared_ptr

更靠谱在它真正意义上的解决了"拷贝问题" , 也就是说, 一个原生指针可以被多个hared_ptr智能指针对象管理了, 也不会造

成重复的释放, 同样也引入了删除器, 增加了其他接口. 可以说是把auto_ptr和unique_ptr踩过的坑全跳过了, hared_ptr站在

了巨人的肩膀上. 

常用接口

成员函数:
T* get();
返回shared_ptr存储(封装)的指针

bool operator bool();
返回shared_ptr存储(封装)的指针是否为空

T& operator*();
重载*

T* operator->();
重载->

shared_ptr& operator= (const shared_ptr& x);
shared_ptr& operator= (shared_ptr&& x);
template <class U> shared_ptr& operator= (const shared_ptr<U>& x);
template <class U> shared_ptr& operator= (shared_ptr<U>&& x);
重载 =

template <class U> shared_ptr& operator= (auto_ptr<U>&& x);
template <class U, class D> shared_ptr& operator= (unique_ptr<U, D>&& x);
重载=, 向后兼容auto_ptr和unique_ptr

void reset();
直接释放封装的内部指针所指向的内存, 如果指定了ptr的值, 则将内部指针初始化为该值
(否则将其设置为nullptr), 作用相当于提前析构

void swap(shared_ptr& x);
与x交换shared_ptr对象的内容, 在它们之间转移各自所托管对象的所有权,而不会破坏或改变两者的使用计数

bool unique();
检查引用计数是否为1, 是则返回true, 反之返回false

long int use_count();
返回该对象的引用计数
非成员函数(友元函数):
ostream operator<< (shared_ptr&);
重载<<, x为shared_ptr对象, cout<<x; 等同于 cout<<x.get(); 

bool operators >/>=/</<=/==/!=();
重载了>, >= ,<, <=, ==, !=等比较运算符

void swap (shared_ptr)
交换两个shared_ptr对象的内容, 在它们之间转移各自所托管对象的所有权,而不会破坏或改变两者的使用计数

还是先来看一下shared_ptr中 "拷贝" 问题是否存在 

#include<iostream>
#include<memory>
using namespace std;
class A {
public:
	int m_data;
	~A() {
		cout << "析构\n";
	}
};
void test2() {
	shared_ptr<A> hp1(new A);
	shared_ptr<A> hp2(hp1);
	shared_ptr<A> hp3 = hp2;
	hp3->m_data = 10;
	cout << hp1->m_data << endl;
	cout << hp2->m_data << endl;
	cout << hp3->m_data << endl;
}
int main() {
	test2();
	system("pause");
	return 0;
}

可以看到, 最后只调用了一次析构函数.  并没有出现auto_ptr和unique_ptr中出现的问题 .

shared_ptr原理

shared_ptr 通过引用计数的方式来实现多个shared_ptr对象之间共享资源 . 举个例子, 在学生寝室里, 大家共用一台空调, 但

当宿舍没人时肯定要关掉空调, 那么谁关呢? 谁最后离开宿舍谁关 .(对于shared_ptr来说, "拷贝" 之后, 大家共用同管理一个

原生指针, 当其中一个shared_ptr对象释放销毁时, 只有还有其他shared_ptr对象管理着原生指针, 那么就不会释放这个原生

指针所指向的这片堆区内存), 具体如下 :

1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享 .

2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了, 对象的引用计数减 1.

3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,  此时在自身释放时, 必须释放该资源;

4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了.

下面简单模拟实现一个没有互斥锁的SharedPtr  ,来具体看看到底是怎样引用计数的.

Shared_ptr.h

#pragma once
template<class T> 
class Shared_ptr {
	T* m_ptr;
	int* m_pRefCount;//引用计数
public:
	Shared_ptr(T* ptr = nullptr)
		:m_ptr(ptr),
		m_pRefCount(new int(1))
	{}
	~Shared_ptr() { Release(); }
	Shared_ptr(const Shared_ptr<T>& sp)
		: m_ptr(sp.m_ptr)
		, m_pRefCount(sp.m_pRefCount) 
	{
		AddRefCount();
	}
	Shared_ptr<T>& operator=(const Shared_ptr<T>& sp){
		if (m_ptr != sp.m_ptr){
			Release();// 释放管理的旧资源
			// 共享管理新对象的资源,并增加引用计数
			m_ptr = sp.m_ptr;
			m_pRefCount = sp.m_pRefCount;
			AddRefCount();
		}
		return *this;
	}
	void Release() {
		if (--(*m_pRefCount) == 0) {
			delete m_ptr;
			delete m_pRefCount;
		}
	}
	void AddRefCount() { ++(*m_pRefCount); }
	int UseCount() { return *m_pRefCount; }
	T& operator*() { return *m_ptr; }
	T* operator->() { return m_ptr; }
	T* get() { return m_ptr; }
};

shared_ptr.cpp 

#include<iostream>
#include"Shared_ptr.h"
using namespace std;
class A {
public:
	int m_data;
	~A() {
		cout << "析构\n";
	}
};
void test() {
	Shared_ptr<A> hp1(new A);
	Shared_ptr<A> hp2(hp1);
	Shared_ptr<A> hp3 = hp2;
	hp3->m_data = 10;
	cout << hp1->m_data << endl;
	cout << hp2->m_data << endl;
	cout << hp3->m_data << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

可以看到, 结果和之前用库中的shared_ptr的结果一样 .

shared_ptr线程安全的问题

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

1. 智能指针对象中引用计数是多个智能指针对象共享的, 多个线程中智能指针的引用计数同时++或--, 这个操作不是原子的. (操作

原子性, 如果一个操作不可被打断, 则说此操作具有原子性)这样引用计数就错乱了. 会导致资源未释放或者程序崩溃的问题. 所以

shared_ptr智能指针中引用计数++和--时是需要加锁的, 而shared_ptr确实也这样做了, 所以说引用计数的操作是线程安全的。

2. 智能指针管理的对象存放在堆上,多个线程同时去访问,会导致线程安全问题.所以此时线程不是安全的.

shared_ptr循环引用问题

#include<iostream>
#include<memory>
using namespace std;
class ListNode {
	int m_data;
public:
	shared_ptr<ListNode> m_prev;
	shared_ptr<ListNode> m_next;
	~ListNode() {
		cout << "~ListNode()" << endl; 
	}
};
void test() {
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	/****构建循环链表****/
	node1->m_next = node2;
	node2->m_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
}
int main() {
	test();
	system("pause");
	return 0;
}

可以看到, 结果不是我们所预期的, node1和node2并没有析构, 也就是说, 发生了内存泄漏.  这..emm, 怎么shared_ptr会有这种问

题? 导致这种问题的原因在于, shar_ptr采用的是引用计数的原理, 让多个shared_ptr同时管理一份原生指针. 但当出现类似于上面

代码中的, 自定义类型中又包含了指向这个自定义类型的指针, 当出现这种情况也就是循环引用问题时, 智能指针就会出现无法释

放资源的问题 . 原理如下图 :

就比如上面代码中构建循环链表, 两个节点形成链表时(前一个next指向后一个, 后一个prev指向前一个), 两个节点的引用计数

都变成了2, 此时test()调用结束后shared_ptr对象node1和node2 要释放, 当调用其析构函数后, 其所管理资源并没有被释放, 只是

引用计数变为1.此时node1的资源由node2的prev管理, node2的资源由node1的next管理. 要释放所管理资源就要释放 node2的

prev和node1的next, 而prev和next是成员变量, 他们的析构要各自对象先析构释放, 但 node1和node2并没有释放. 

就这样, 说完了一段绕口令 , 总结一下, 就是下面的绕口令,  emmm...

node1不能释放: 原因: node2的prev没有释放

node2不能释放: 原因: node1的next没有释放

node1的next不能释放: 原因: node1没有释放

node2的prev不能释放: 原因: node2没有释放

                      

循环引用解决方法 

首先, 循环引用出现自定义类型中有指向自身类型的shared_ptr智能指针时, 最典型的例子就是双向循环链表, 如上面的例子

方法一: 不用

这个办法, 简单粗暴, 但却异常好用, 本来, 在一般情况下, 循环链表也没必要使用智能指针, 还不如不用.

方法二: weak_ptr

但若是执意要用的话, 也是有办法的, 这时候就需要weak_ptr这个弱指针类型, 具体怎么操作呢?

以上面的例子为例, 将ListNode类中的next成员和prev成员定义成weak_ptr类型, 就可以了. 如下:

class ListNode {
	int m_data;
public:
	weak_ptr<ListNode> m_prev;
	weak_ptr<ListNode> m_next;
	~ListNode() {
		cout << "~ListNode()" << endl; 
	}
};

修改后再运行上面的代码, 可以看到结果如下, 符合预期

weak_ptr原理

weak_ptr只是封装了原生指针, 并没有其他功能, 并且它还提供了与shared_ptr之间的类型转换, 使得其可以直接等于shared_ptr类

型, 当weak_ptr对象 = shared_ptr对象时, shared_ptr对象的引用计数并不会增加, weak_ptr也只是获取到了shared_ptr对象所所管

理的指针而已.

shared_ptr删除器

在unique_ptr中就已经引入了删除器, 已解决释放内时可能会出现的错误. 具体是, 我们手动提供给智能指针一个释放方法, 让其根

据我们自己定义的方法释放.  但shared_ptr删除器的使用更加方便了, 不像unique_ptr一样还需要在模板参数位置传入函数指针类

型, shared_ptr只需要在构造函数第二个参数位置传入仿函数 (仿函数, 重载了小括号运算符的类)

那么在什么时候我们需要自定义删除器呢?

1. 用malloc/calloc/realloc分配内存空间时, 需要自定义删除器, 用free()释放 (注意, 自定义类型要用new)

2. 用new给自定义类型申请连续空间时, 必须自定义删除器, 删除器中用delete []来释放, 否则程序会出错奔溃

3. 用new申请单个自定义类型空间时, 传入用delete[] 释放的删除器会出错, 一般不传入.

关于new和delete的问题, 详解戳链接( ̄︶ ̄):https://blog.csdn.net/qq_41071068/article/details/103149856

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103339986