目录
一个指针还能智能??? 在没学习智能指针之前, 听着还挺有噱头..
智能指针智能在哪儿?
用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有了更多的功能, 比如[]的重载, 判断运算符的重载等, 更加灵活方便
shared_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