7026 字
35 分钟
[C++11] C++智能指针: RAII思想、unique_ptr、shared_ptr、weak_ptr分析及模拟
2023-07-09

一直以来, C++一直最令人诟病的问题之一是什么?

没错, 就是内存泄漏

C语言还好一些, 但在C++中, 异常的相关处理出现之后, 内存泄漏的问题简直是防不胜防

而且, C++也不像Java那样带有垃圾自动回收机制, 所以在C++11之前, C++的内存管理一直是一件非常麻烦的事情

虽然现在也麻烦, 不过已经好了很多

博主 C++异常相关文章:

[C++] C++异常处理分析介绍: 异常概念、异常抛出与捕获匹配原则、重新抛出、异常安全、异常体系…

智能指针#

C++在引入了异常的概念之后, 内存泄漏的问题就非常有可能出现

比如这段代码:

#include <iostream>
#include <exception>
using std::cin;
using std::cout;
using std::endl;
using std::exception;
using std::invalid_argument;
int div() {
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func() {
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main() {
try {
Func();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}

注释的三个问题, 各自都会产生什么结果?

  1. 如果 int* p1 = new int; 时抛异常, 其实是空间开辟失败了, 所以不会发生内存泄漏

  2. 如果 int* p2 = new int; 时抛异常, 那就说明p1空间是开辟成功了的, 所以如果这里抛异常, 会导致p1指向的空间无法被delete, 会发生内存泄漏

  3. 而, 如果div()执行时抛异常, 那么 就会导致p1p2所指向的空间泄露

不过, 如果是第3种情况, 可以直接这样解决:

void Func() {
int* p1 = new int;
int* p2 = new int;
try {
cout << div() << endl;
}
catch (...) {
delete p1;
delete p2;
throw;
}
delete p1;
delete p2;
}

但是, 如果类似第2种情况, 就无法相对方便的解决

虽然看似可以模仿上面的解决方法尝试解决问题, 但实际上是无法解决的

为什么呢?

int* p2 = new int;时抛异常, 我们捕捉到异常之后, 就需要将p1的空间释放掉

但如果是这种情况呢?

void Func() {
int* p1 = new int;
int* p2 = new int;
int* p3 = new int;
int* p4 = new int;
int* p5 = new int;
try {
cout << div() << endl;
}
catch (...) {
delete p1;
delete p2;
delete p3;
delete p4;
delete p5;
throw;
}
delete p1;
delete p2;
delete p3;
delete p4;
delete p5;
}

如果是在p3p4p5任意一个位置抛异常了呢?

如果是p3, 那就需要释放 p1 p2; 如果是p4, 那就需要释放 p1 p2 p3; 如果是p5, 那就需要释放 p1 p2 p3 p4

如果 开辟的操作更多, 那么不同位置抛异常, 要处理的情况就不同

这种情况, 就会非常的麻烦.

所以, C++11 正式引入了智能指针

RAII 思想#

RAII (Resource Acquisition Is Initialization) 资源获取即初始化 是一种 利用对象生命周期来控制程序资源 的技术

什么意思呢?

C++中, 一个对象在生命周期即将结束时 是会自动调用析构函数释放资源的

进而可以利用这一点, 将我们所需要获取的资源 来托管给对象, 通过对象来进行管理、访问、使用资源

即, 实现一个类, 其成员变量包括所需资源, 在实例化对象时 通过构造函数开辟并初始化资源, 在对象生命周期快要结束时 对象会自动调用析构函数, 将开辟出来的空间释放

这样做有什么好处呢?

  1. 不需要显式地释放资源

  2. 所需的资源在对象生命周期内始终保持有效

比如, 这段代码:

#include <iostream>
#include <exception>
using std::cin;
using std::cout;
using std::endl;
using std::exception;
using std::invalid_argument;
template <class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr) {}
~SmartPtr() {
if (_ptr) {
cout << "析构 释放资源" << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
int div() {
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func() {
// int* p1 = new int;
// int* p2 = new int;
SmartPtr<int> p1(new int);
SmartPtr<int> p2(new int);
cout << div() << endl;
}
int main() {
try {
Func();
}
catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}

这段代码, 如果发生异常 会是什么结果?

通过传入new int调用构造函数, 实例化一个 SmartPtr对象

SmartPtr对象的成员函数是一个指针, 用来指向开辟资源的空间

构造函数将_ptr指向new int出来的空间, 析构函数delete_ptr指向的空间

这就实现了, 不用手动的delete new出来的资源, 对象生命周期结束, 析构函数会自动释放.

这就是 智能指针的实现思想, 也是RAII思想

智能指针原理#

对于上面的SmartPtr, 还不能将其称之为智能指针

因为, 它还不具有指针的行为: *解引用->

所以, 智能指针至少还需要 重载*->, 不过这两个操作符的重载很简单:

template <class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr) {}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~SmartPtr() {
if (_ptr) {
cout << "析构 释放资源" << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};

这样就可以实现, 直接通过对象进行*解引用->相关操作:

struct Date {
int _year;
int _month;
int _day;
};
int main() {
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date);
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
cout << sparray->_year << "-" << sparray->_month << "-" << sparray->_day << endl;
return 0;
}

这段代码执行结果为:

可以直接通过对象, 来实现指针相关的操作

智能指针的问题#

从上面来看, 智能指针的原理是不是非常的简单?

但智能指针的实现, 其实还存在着另外的问题: 对象的拷贝构造和赋值怎么实现?

在普通指针的使用中, 经常会有这样的操作:

在指针定义时, 直接用其他指针初始化 或 用指针给指针赋值

转换为SmartPtr, 就大概是这样:

对于类来说 SmartPtr<int> sp2 = sp1就是拷贝构造

所以 智能指针是需要实现拷贝构造和赋值重载

但问题是, 该如何实现呢? 需要使用 深拷贝还是浅拷贝?

**一定是需要实现浅拷贝 **

因为无论是 普通指针给普通指针初始化 还是普通指针给普通指针赋值

这两个操作完成之后, 这两个普通指针一定是指向同一块空间的, 即两个指针维护同一块空间

那么, 智能指针也需要实现相同的逻辑, 所以 无论是拷贝构造还是赋值重载都不能实现深拷贝

浅拷贝的实现旧很简单了:

template <class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr) {}
// 拷贝构造
SmartPtr(const SmartPtr<T>& sp)
: _ptr(sp._ptr) {}
// 赋值重载
SmartPtr<T>& operator=(const SmartPtr<T>& sp){
// 防止自我赋值
if(this != &sp) {
// 先清除被赋值对象中的资源
if(_ptr)
delete _ptr;
_ptr = sp._ptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~SmartPtr() {
if (_ptr) {
cout << "析构 释放资源" << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};

都浅拷贝实现, 只需要将传入的SmartPtr对象维护的资源 赋值给当前对象需要维护的资源变量就可以了

但是, 如果只是这样处理会出大问题

int main() {
SmartPtr<int> sp1(new int);
// 用 sp1 拷贝构造 sp2
SmartPtr<int> sp2 = sp1;
return 0;
}

这样的代码执行之后:

会发现, 同一块空间被释放了两次, 直接报错

出现这种情况的原因是, 浅拷贝让两个对象的成员变量指向了同一块空间, 在两个对象生命周期结束时, 都会调用析构函数 但会尝试delete同一块空间

这就是 智能指针的实现存在的最大的问题, 需要实现浅拷贝, 但是浅拷贝就会出现重复delete

为了解决这个问题, C++诞生了许多版本的智能指针

auto_ptr#

为了解决 同一块空间会被delete两次的问题, C++98 实现的智能指针auto_ptr用了一个”神”操作

auto_ptr在执行拷贝构造或者赋值重载时, 会转移资源的管理权

什么意思呢?

就是, 会 先把传入的对象的资源给新对象, 然后再把传入对象的指向资源的指针置空

auto_ptr的模拟代码就是这样的:

namespace July {
template <class T>
class auto_ptr {
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr) {}
auto_ptr(auto_ptr<T>& sp)
: _ptr(sp._ptr) {
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& sp) {
// 防止自我赋值
if (this != &sp) {
// 先清除被赋值对象中的资源
if (_ptr)
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~auto_ptr() {
if (_ptr) {
cout << "析构 释放资源" << _ptr << endl;
delete _ptr;
}
}
private:
T* _ptr;
};
} // namespace July

这种方法 确实解决了 同一块空间会被delete两次的问题(因为其中一个对象的资源变成了 nullptr, delete nullptr 什么都不做)

但是, 却又出现了一个更离谱的问题:

auto_ptr执行过拷贝构造或赋值重载之后, 等号右边的对象, 就不能再使用了

int main() {
July::auto_ptr<int> ap1(new int);
*ap1 = 10;
cout << "ap1:: " << *ap1 << endl;
July::auto_ptr<int> ap2(ap1);
cout << "ap1:: " << *ap1 << endl;
cout << "ap2:: " << *ap2 << endl;
return 0;
}

使用上面的类, 执行这段代码:

会报出段错误. 原因是: 通过ap1拷贝构造ap2, ap1的资源会被置空, 所以无法再被访问

使用库中的auto_ptr也是相同的效果:

#include <memory>
int main() {
std::auto_ptr<int> ap1(new int);
*ap1 = 10;
cout << "ap1:: " << *ap1 << endl;
std::auto_ptr<int> ap2(ap1);
cout << "ap1:: " << *ap1 << endl;
cout << "ap2:: " << *ap2 << endl;
return 0;
}

使用库中的auto_ptr, 编译时 编译器甚至会报出警告, 提示此类已弃用

把后面的 cout << "ap1:: " << *ap1 << endl; 注释掉, 就可以执行了:

成功的用ap1拷贝构造了ap2, 然后成功的解决了同一块空间释放两次的问题, 但是 也成功的添加了一个更离谱的问题

由于 auto_ptr 执行拷贝构造或赋值重载之后, 旧对象就无法正常使用了, 所以这是一个失败的设计

auto_ptr解决 同一块空间释放两次 的方法是 转移对资源的管理权

这会导致, 旧对象无法再访问资源, 所以挺没用的

这个智能指针是 C++98 就存在的, 但是太难用

所以 C++11又设计了三个相对好用一些的智能指针

unique_ptr#

unique_ptr 是 C++11 设计的智能指针之一

基本的用法不必多说

unique_ptr解决 同一块空间会被delete两次的问题 的方法, 更加简单粗暴: 禁止拷贝构造和赋值

没错, unique_ptr直接禁止了拷贝构造和赋值的行为

namespace July {
template <class T>
class unique_ptr {
public:
// RAII思想
unique_ptr(T* ptr = nullptr)
: _ptr(ptr) {}
~unique_ptr() {
if (_ptr) {
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// C++11 之后
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
// C++98 可以这样实现
// 1. 只声明不实现, 不过如果是public, 这种方法可以在其他地方实现功能
// 2. 声明成私有
// unique_ptr(const unique_ptr<T>& sp);
// unique_ptr<T>& operator=(const unique_ptr<T>& sp);
T* _ptr;
};
} // namespace July

使用此类, 如果尝试拷贝构造或者赋值, 是无法编译通过的:

int main(){
July::unique_ptr<int> up1(new int);
*up1 = 20;
cout << "up1:: " << *up1 << endl;
// 尝试拷贝构造
July::unique_ptr<int> up2(up1);
return 0;
}

编译时, 编译器会直接报错:

使用库中的, 也会有相同的效果:

不过 库中的实现要复杂的多

shared_ptr **#

上面介绍的两个智能指针, 都没有办法相对完美的解决 同一块资源被delete两次的问题(一个转移资源、一个禁止拷贝)

不过, C++11 还有一个智能指针 可以很好的解决这个问题

那就是 shared_ptr

这个智能指针的解决方法是:

在类中添加一个成员变量 用来引用计数, 每有一个对象指向同一个资源空间, 这个计数就增加 1, 否则则减少 1; 只有当计数为 0 是, 才会释放资源空间

但是, 这个用以引用计数的成员变量, 不能直接用static修饰的变量

如果使用 static修饰的变量作为引用计数变量, 那么这个变量会被所有shared_ptr对象共享, 无论是否维护同一块资源空间

但是, 我们要实现的是 维护同一块资源空间的对象之间共享一个引用计数

这要怎么实现呢? 其实很简单, 如何实现多个对象维护同一块资源空间 就如何实现多个对象共享引用计数

shared_ptr模拟:

namespace July {
template <class T>
class shared_ptr {
public:
// RAII思想
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1)) {}
~shared_ptr() {
release();
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) {
(*_pCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
// 这里需要用 维护资源是否相同 来判断 是否需要执行赋值操作
// 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同
if (_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 释放资源函数
// 当前对象的引用计数为1(再--为0)时, 才真正进行资源释放操作
void release() {
if (--(*_pCount) == 0 && _ptr) {
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
int getCount() {
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
};
} // namespace July

其中, 最重要的四个部分:

  1. 成员变量int* _pCount

    要实现多个对象之间共享一个引用计数, 就需要让多个维护同一资源的对象能够同时访问这个变量

    那其实 资源共享是怎么是实现的, 引用计数一样的方法实现 就可以了

    所以, 使用一个int*指针变量, 在实例化对象时初始化为new int(1), 就可以让 此指针指向一块堆空间

    这样, 就可以让其他对象的int*指针变量指向同一块空间

  2. 析构函数

    由于可能存在多个对象正在共同维护同一块资源的情况

    所以 需要在当前引用计数为1(再减就为0)时 调用析构函数, 才进行资源的释放

    否则就只需要将引用计数--, 就可以了

    所以实现了一个, release()函数:

    // 释放资源函数
    // 当前对象的引用计数为1(再--为0)时, 才真正进行资源释放操作
    void release() {
    // 先 --(*_pCount), 然后判断结果是否为0
    // 如果为0, 且维护资源不为空, 则进行资源释放
    // 需要释放 资源空间 和 引用计数空间
    if (--(*_pCount) == 0 && _ptr) {
    cout << "delete" << _ptr << endl;
    delete _ptr;
    _ptr = nullptr;
    delete _pCount;
    _pCount = nullptr;
    }
    }

    在析构函数内, 直接调用此函数就可以了

  3. 拷贝构造

    此类的拷贝构造的实现 非常简单

    只需要根据传入的对象所 维护的资源和引用计数, 初始化新对象

    然后 引用计数++就可以了

    shared_ptr(const shared_ptr<T>& sp)
    : _ptr(sp._ptr)
    , _pCount(sp._pCount) {
    (*_pCount)++;
    }
  4. 赋值重载

    赋值重载的实现 是最复杂的

    首先, 需要判断 两个对象维护的资源是否为同一资源:

    如果是, 则不执行赋值操作; 如果不是, 再执行

    然后要执行赋值操作, 就需要知道两个对象相互赋值会发生什么: 比如sp1 = sp2可能会发生什么呢?

    1. sp1将会失去原先维护的资源

      如果sp1原来的引用计数为 1, 那么就需要将sp1维护的资源空间释放掉

      所以, 需要先执行 release(), 判断是否需要释放资源

    2. sp2维护的资源和引用计数, 将会多一个维护者sp1

      所以 需要将sp2维护的资源 和 引用计数, 赋值给sp1

    shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
    // 这里需要用 维护资源是否相同来判断 是否需要执行赋值操作
    // 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同
    if (_ptr != sp._ptr) {
    release();
    _ptr = sp._ptr;
    _pCount = sp._pCount;
    ++(*_pCount);
    }
    return *this;
    }

此时模拟的shared_ptr, 就是一个好用的智能指针:

int main() {
July::shared_ptr<int> sp1(new int(10));
cout << "构造 sp1, sp1:: " << *sp1 << ", pCount:: " << sp1.getCount()
<< endl;
July::shared_ptr<int> sp2(sp1);
cout << "由sp1拷贝构造 sp2, sp2:: " << *sp2
<< ", pCount:: " << sp2.getCount() << endl;
July::shared_ptr<int> sp3(sp1);
cout << "由sp1拷贝构造 sp3, sp3:: " << *sp3
<< ", pCount:: " << sp3.getCount() << endl << endl;
cout << "更改 *sp2 = 20" << endl;
*sp2 = 20;
cout << "sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl;
cout << "sp2:: " << *sp2 << ", pCount:: " << sp2.getCount() << endl;
cout << "sp3:: " << *sp3 << ", pCount:: " << sp3.getCount() << endl << endl;
July::shared_ptr<int> sp4(new int(44));
cout << "构造 sp4, sp4:: " << *sp4 << ", pCount:: " << sp4.getCount()
<< endl << endl;
sp2 = sp4;
cout << "sp4赋值给sp2, sp2:: " << *sp2 << ", pCount:: " << sp2.getCount()
<< endl << endl;
cout << "sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl << endl;
sp4 = sp2;
cout << "sp2赋值给sp4, sp4:: " << *sp4 << ", pCount:: " << sp4.getCount()
<< endl << endl;
return 0;
}

这段代码的执行结果是:

shared_ptr换成标准库中的(还需要将 调用的getCount()改为use_count()), 依旧是这样:

shared_ptr的循环引用 与 weak_ptr#

虽然shared_ptr已经很好用了

但是, 使用不当的话还会出现一种后果很严重的错误:

struct ListNode {
int _data;
July::shared_ptr<ListNode> _prev = nullptr;
July::shared_ptr<ListNode> _next = nullptr;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
July::shared_ptr<ListNode> node1(new ListNode);
July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
return 0;
}

使用shared_ptr管理ListNode结构体对象

ListNode内部 指向前节点和后节点的成员变量, 同样是shared_ptr<ListNode>

那么, 上面的这段代码 执行结果是什么?

首先创建了两个节点. 然后分别指向对方, 就像这样:

node1维护自己的空间资源, 然后node2->_prev也维护同一空间, 所以node1.getCount()变为2

node2维护自己的空间资源, 然后node1->_next也维护同一空间, 所以node2.getCount()变为2

看起来很正常, 但是 空间释放正常吗?

是实现的shared_ptr的析构函数中, 如果释放了资源 是会输出delete+地址

再加一句话演示一下:

struct ListNode {
int _data;
July::shared_ptr<ListNode> _prev = nullptr;
July::shared_ptr<ListNode> _next = nullptr;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
July::shared_ptr<ListNode> node1(new ListNode);
July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
July::shared_ptr<int> sp1(new int(10));
cout << "构造 sp1, sp1:: " << *sp1 << ", pCount:: " << sp1.getCount() << endl;
return 0;
}

sp1的空间释放了, 但是**node1node2的空间并没有释放!**

用 VS 调试看看:

可以看到, main 函数执行完return 0;之后, sp1的引用计数从1->0, 资源被销毁

但是, node1node2的引用计数 却是从2->1, 而且没有销毁资源

这是为什么?

来分析一下:

  1. 首先 用shared_ptr构造维护了两个节点: node1node2

    此时, 两块资源空间, 只有各自维护, _pCount 为 1

  2. node1node2 被构造出来的时候, 其各自有两个成员变量_next_prev也被构造了出来

  3. 然后, 将node2赋值给了node1->_next

    此时, node2的资源空间, 就有两个shared_ptr对象在维护: node2node1->_next

    _pCount变为2

  4. node1赋值给了node2->_prev

    此时, node1的资源空间, 也就有两个shared_ptr对象在维护: node1node2->_next

    _pCount变为2

  5. 然后, 直到执行 return 0;

    按期望来说, main函数结束node1node2对象生命周期结束, 应该调用析构函数 将维护的资源释放掉

    很明显node1node2析构函数是调用了, 但是资源并没有释放掉

    因为 在node1node2调用析构函数时, 两块资源空间的引用计数都是2, 是不会释放资源的, 只会将对应的引用计数--, 然后将对象摧毁掉

    也就是说, node1node2对象被摧毁, 但由于原node2的_prev还在维护原node1的资源空间, 原node1的_next还在维护原node2的资源空间, 导致两块资源空间没有释放

    变化如下:

    如果将原node1标号为1号空间, 原node2标号为2号空间

    node1node2对象被销毁之后, 资源的维护状态就变成了: 1号空间内有对象在维护 2号空间, 2号空间内有对象在维护 1号空间

    那么 这两块空间就没办法释放掉了

    因为, 如果要释放1号空间资源, 就需要调用维护着它的2号空间里的_prev对象 的析构函数

    析构函数是在对象生命周期结束时自动调用的, 想要2号空间里的_prev对象生命周期结束 就需要 释放2号空间

    而 如果要 释放2号空间资源, 就需要调用维护着它的1号空间里的_next对象 的析构函数

    这就成了一个 循环, 然后 释放空间的条件永远无法满足, 空间就无法释放, 也无法访问, 这又是一种内存泄漏

    这种情况被称为, shared_ptr的循环引用, 即 两块空间内都存在shared_ptr对象 维护着对方空间, 导致释放空间的条件无法满足, 出现内存泄漏

上面的这种情况, 单使用 shared_ptr是无法解决的

shared_ptr的循环引用问题, 需要通过weak_ptr这个智能指针来解决

weak_ptr#

weak_ptr是一个特殊的智能指针, 它是**辅助shared_ptr**使用的

它可以指向shared_ptr指向的对象. 但是, 它只能访问、管理数据, 不能管理这块资源空间

即 不参与资源的释放, 也不会使资源空间的引用计数增加

它就像一个指向shared_ptr指向的对象的普通指针

并且, weak_ptr只能访问已经存在的对象, 实例化新对象需要使用shared_ptr

所以, 在上述例子中, 我们只需要将 ListNode 节点结构体中的_prev_next改用weak_ptr管理数据就可以了:

namespace July {
template <class T>
class shared_ptr {
public:
// RAII思想
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1)) {}
~shared_ptr() {
release();
}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount) {
(*_pCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
// 这里需要用 维护资源是否相同来判断 是否需要执行赋值操作
// 不能只判断对象是否相同, 因为不同对象维护的资源也可能相同
if (_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
else {
// 为了演示
cout << "自我赋值, 跳过执行" << endl;
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
// 释放资源函数
// 当前对象的引用计数为1时, 进行资源释放操作
void release() {
if (--(*_pCount) == 0 && _ptr) {
cout << "delete" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
delete _pCount;
_pCount = nullptr;
}
}
T* get() const {
return _ptr;
}
int getCount() {
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
};
// 简化版本的weak_ptr实现
template <class T>
class weak_ptr {
public:
weak_ptr()
: _ptr(nullptr) {}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get()) {}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) {
_ptr = sp.get();
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
} // namespace July
struct ListNode {
int _data;
July::weak_ptr<ListNode> _prev;
July::weak_ptr<ListNode> _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
int main() {
July::shared_ptr<ListNode> node1(new ListNode);
July::shared_ptr<ListNode> node2(new ListNode);
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.getCount() << endl;
cout << node2.getCount() << endl;
return 0;
}

修改了, ListNode 之后, 这段代码的运行结果是:

即使 执行了node1->_next = node2; node2->_prev = node1; node1node2的引用计数也不会发生变化

也就不会影响资源的释放

但, 使用weak_ptr需要注意, 资源的声明周期, 因为weak_ptr并不管理资源, 所以在使用上更贴近原始指针一些, 因为不能保证所指向资源是否有效

智能指针的定制删除器#

本文前面介绍智能指针时, 智能指针维护的资源 全部都是new出来的

但是, C++除了new之外 还有new[]

new[]开辟的资源 需要使用delete[]释放

C++是兼容C语言的, 还可以malloc空间

这就导致了, 使用智能指针释放资源的方式并不是固定的, 并且开辟空间存储的资源也是不固定的

智能指针 针对标准内置类型数据, 有一个很好的释放支持

但如果是自定义类型呢?

class Date {
public:
~Date() {
cout << "~Date" << endl;
}
private:
int _year = 1;
int _month = 1;
int _day = 1;
};
int main() {
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date> sp2(new Date[10]);
return 0;
}

执行后, 输出的第一行是 ~Date 说明 new 出来的正常释放了

但是, new[]出来的数据, 没法释放 进程直接崩溃了

那么, 怎么才能让智能指针 支持对各种类型的数据, 各种开辟的方法实现正常的释放呢?

这就需要用到 定制删除器的概念了:

查看文档中 unique_ptr的模板 和 shared_ptr的构造函数

unique_ptr的模板中, 实际有两个模板参数, 一个是资源类型, 另一个 则是仿函数-定制删除器 用来指定资源的释放方式

shared_ptr的构造函数中, 也有一个可以传入del的重载, 这也是需要传入定制删除器的

定制删除器实际 就是释放资源方式的仿函数.

还是以上面的代码为例, 我们在 构造智能指针时, 传入一个lambda表达式:

int main() {
std::shared_ptr<Date> sp1(new Date);
std::shared_ptr<Date> sp2(new Date[10], [](Date* d){ delete [] d; });
return 0;
}

这段代码的执行结果:

很顺利的释放了所有资源

C++11 智能指针 和 boost 智能指针的关系#

其实这两个东西之间的关系非常容易解释

我们每个人肯定或多或少都玩过网络游戏

他们之间的关系 就好像 游戏的体验服 和 正式服之间的关系

Boost库是一个非常强大且广泛使用的C++库集合, 它包含了一系列为C++开发提供便利的库, 旨在扩展 C++标准库的功能

Boost 库的开发遵循严格的编程规范和高质量标准, 因此其代码质量和性能得到了广泛认可

许多Boost库的作者本身就是C++标准委员会成员, 因此, Boost”天然”成了标准库的后备, 负责向新标准输送组件, 这也使得 Boost获得了”准”标准库的美誉

而 C++11 中的三个智能指针 unique_ptr shared_ptr weak_ptr 就是从 Boost 库中优化过来的

  1. C++ 98 中产生了第一个智能指针auto_ptr, 这个智能指针太离谱

  2. C++ Boost 给出了更实用的scoped_ptrshared_ptrweak_ptr

  3. C++ TR1, 引入了shared_ptr等, 不过注意的是 TR1 并不是标准版

  4. C++ 11, 引入了unique_ptrshared_ptrweak_ptr

需要注意的是unique_ptr对应的就是 Boost 库中的scoped_ptr, 改了个名字

并且这些智能指针的实现原理是参考 Boost 中的实现的