1. 为什么需要智能指针?

我们先来分析下面这段程序有没有什么内存方面的问题?

提示一下:注意分析 MergeSort 函数中的问题。

代码如下所示:

#include <iostream>

using namespace std;

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;
}

这段代码在异常发生时会发生内存泄漏。因为:new 分配的内存没有被 delete,而异常会导致函数提前退出。

但是明明代码里写了 delete,为什么还会泄漏?因为异常会 “跳过后面的代码”!

先看这段:

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

你看到的是:后面不是有 delete p1; delete p2; 吗?但真实执行是:一旦异常发生,这两行根本不会执行!

如果 div() 抛异常了,也就是说假设 b == 0,执行:

throw invalid_argument("除0错误");

程序实际执行顺序如下:

1. p1 = new int        ✅ 成功
2. p2 = new int        ✅ 成功
3. 调用 div()          ❌ 抛异常
4. 立刻离开 Func()     ⛔(后面的代码全部跳过)
5. 进入 main 的 catch

而关键来了:

delete p1;
delete p2;

根本没执行到!!!

为什么会这样?这是 C++ 的异常机制:栈展开。

当异常发生时:当前函数立即停止执行,开始 “往外层函数返回”,沿途会调用栈上对象的析构函数,而不会执行普通语句(比如 delete)。

这句代码 int* p1 = new int; 中的 p1 是一个普通指针变量(栈上),但它指向的内存在堆上,异常时 p1 这个变量没了(栈清理),但它指向的堆内存没人管理,那么就会发生内存泄漏。

2. 内存泄漏

内存泄漏: 是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害: 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

2.1 内存泄漏分类

C/C++ 程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak): 堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。

系统资源泄漏: 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.3 如何避免内存泄漏

第一,工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。

第二,采用 RAII 思想或者智能指针来管理资源。

第三,有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

第四,出问题了使用内存泄漏工具检测。(ps:不过很多工具都不够靠谱,或者收费昂贵。)

内存泄漏非常常见,解决方案分为两种:

  • 事前预防型:如智能指针等。
  • 事后查错型:如泄漏检测工具。

3. 智能指针的使用及原理

3.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

大白话:资源获取以后马上进行初始化。就是说:你获取到了一个资源,但是你别自己去管理它,而是把它交给一个对象的生命周期来管。

这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

如下所示,我们可以使用 RAII 思想设计一个 SmartPtr 类

template<class T>
class SmartPtr 
{
public:
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete: " << _ptr << endl;
		if (_ptr)
			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()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	cout << div() << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

这段代码通过自定义 SmartPtr,实现了一个 基于 RAII 思想的智能指针,用于管理动态内存,避免异常导致的内存泄漏。

RAII(资源获取即初始化)

SmartPtr<int> sp1(new int);

它会在构造函数中获取资源(new),在析构函数中释放资源(delete)。

当 div() 抛异常时:Func() 会提前退出,然后触发 栈展开,最后自动调用析构来释放 sp1 和 sp2 的内存。

但是目前的 SmartPtr 是简化版,存在一些隐患,如果在工程中的话是有很大问题的。

问题一:会发生重复释放

SmartPtr<int> sp1(new int);
SmartPtr<int> sp2 = sp1;  // 默认拷贝!

此时:

sp1._ptr ─┐
          ├──> 同一块内存
sp2._ptr ─┘

两个对象析构时:delete 同一块内存两次,从而导致崩溃。如下所示:

在这里插入图片描述

问题二:没有移动语义

现代 C++ 必须支持:移动构造 和 移动赋值,否则性能和安全都有问题。

下面是更安全的版本:

template<class T>
class SmartPtr 
{
public:
    // 构造
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}

    // 析构
    ~SmartPtr()
    {
        cout << "delete: " << _ptr << endl;
        if (_ptr)
            delete _ptr;
    }

    // 禁止拷贝和赋值
    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;

    // 移动构造
    SmartPtr(SmartPtr&& other)
        : _ptr(other._ptr)
    {
        cout << "SmartPtr(SmartPtr&& other) --- 移动构造" << endl;
        other._ptr = nullptr;
    }

    // 移动赋值
    SmartPtr& operator=(SmartPtr&& other)
    {
        cout << "SmartPtr& operator=(SmartPtr&& other) --- 移动赋值" << endl;
        if (this != &other)
        {
            delete _ptr;
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }

private:
    T* _ptr;
};

此时我们再进行调试:

在这里插入图片描述

3.2 智能指针的原理

上述的 SmartPtr 还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过 -> 去访问所指空间中的内容,因此:SmartPtr 模板类中还得需要将 *-> 重载一下,才可让其像指针一样去使用。

代码如下所示:

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{
public:
    // 构造
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}


    // 析构
    ~SmartPtr()
    {
        cout << "delete: " << _ptr << endl;
        if (_ptr)
            delete _ptr;
    }

    // 禁止拷贝和赋值
    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;

    // 移动构造
    SmartPtr(SmartPtr&& other)
        : _ptr(other._ptr)
    {
        cout << "SmartPtr(SmartPtr&& other) --- 移动构造" << endl;
        other._ptr = nullptr;
    }

    // 移动赋值
    SmartPtr& operator=(SmartPtr&& other)
    {
        cout << "SmartPtr& operator=(SmartPtr&& other) --- 移动赋值" << endl;
        if (this != &other)
        {
            delete _ptr;
            _ptr = other._ptr;
            other._ptr = nullptr;
        }
        return *this;
    }

    // 重载 * 和 ->
    T& operator*() {
        return *_ptr;
    }

    T* operator->() {
        return _ptr;
    }

private:
    T* _ptr;
};

// 日期类
struct Date
{
    int _year;
    int _month;
    int _day;
};

int main()
{
    SmartPtr<int> sp1(new int);
    *sp1 = 10;
    cout << "sp1=" << *sp1 << endl;

    SmartPtr<Date> sp2(new Date);
    // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    sp2->_year = 2026;
    sp2->_month = 10;
    sp2->_day = 5;
    cout << sp2->_year << "/" << sp2->_month << "/" << sp2->_day << endl;

    return 0;
}

运行结果如下:

在这里插入图片描述

总结一下智能指针的原理:

  • RAII 特性
  • 重载 operator*operator->,使其具有像指针一样的行为。

4. auto_ptr

具体详情可以访问:auto_ptr 文档

C++98 版本的库中就提供了 auto_ptr 的智能指针,下面演示的 auto_ptr 的使用及问题。

auto_ptr 的实现原理:管理权转移的思想。

4.1 使用

代码如下所示:

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) -- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() -- 析构" << endl;
    }
private:
    int _a;
};

int main()
{
    auto_ptr<A> ap1(new A(1));
    return 0;
}

运行结果如下:可以看到此时 ap1 是能正常调用构造和析构的。

在这里插入图片描述

同样,如果要对 ap1 进行拷贝呢?

int main()
{
    auto_ptr<A> ap1(new A(1));

    // 拷贝
    auto_ptr<A> ap2(ap1);

    return 0;
}

运行结果如下:

在这里插入图片描述

可以看到 auto_ptr 很好的解决了拷贝的问题,但是它是怎么解决的呢?我们来调试看一下

在这里插入图片描述

我们发现 new A(1) 出来的资源最开始是由 ap1 进行管理的,现在用 ap1 去拷贝到 ap2 里面了,此时 ap1 已经悬空了,相当于把资源的管理权给了 ap2 了。这就是所谓的 管理权转移

那么如果一个新手不懂这个,然后对 ap1 和 ap2 分别操作,就会发生崩溃。如下所示:

在这里插入图片描述

总结:所谓管理权转移,就是拷贝时会把拷贝对象的资源管理权转移给拷贝对象,那么就会导致拷贝对象悬空,此时访问就会出现问题。

4.2 模拟实现

下面我们简化模拟实现一份 auto_ptr 来了解它的原理。

代码如下所示:

// 自定义 auto_ptr
namespace edison
{
    template<class T>
    class auto_ptr
    {
    public:
        // 构造函数
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 拷贝构造
        auto_ptr(auto_ptr<T>& sp)
            :_ptr(sp._ptr)
        {
            // 管理权转移
            sp._ptr = nullptr;
        }

        // 赋值重载
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            // 检测是否为自己给自己赋值
            if (this != &ap)
            {
                // 释放当前对象中资源
                if (_ptr)
                    delete _ptr;

                // 转移ap中资源到当前对象中
                _ptr = ap._ptr;
                ap._ptr = NULL;
            }
        }

        // 析构
        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }

        // 重载 * 和 ->
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

    private:
        T* _ptr;
    };
}

这段代码实现的是 “所有权转移”,也就是:**拷贝不是共享资源,而是把资源 “搬走” **

auto_ptr(auto_ptr<T>& sp)
    : _ptr(sp._ptr)
{
    sp._ptr = nullptr;
}

假设有:

auto_ptr<int> ap1(new int(10));
auto_ptr<int> ap2(ap1);

进入拷贝构造

: _ptr(sp._ptr)

此时:ap2._ptr = ap1._ptr,也就是说 ap1 和 ap2 指向同一块内存

接着,转移所有权

sp._ptr = nullptr;

这段代码会把 ap1 的指针置空,然后变成下面这样:

ap2._ptr -> 原来的内存
ap1._ptr -> nullptr

最终效果:ap2 接管资源,ap1 不再拥有资源。也就是说:

原来的拥有者放弃资源,新对象成为唯一拥有者

调试结果如下:

在这里插入图片描述

结论:auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr。

5. unique_ptr

C++11 中开始提供更靠谱的 unique_ptr,具体详情可以访问:unique_ptr 文档

unique_ptr 的实现原理:简单粗暴的防拷贝。

5.1 使用

代码如下所示:

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) --- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() --- 析构" << endl;
    }
    //private:
    int _a;
};

int main()
{
    unique_ptr<A> up1(new A(10));
    unique_ptr<A> up2(new A(20));

    return 0;
}

运行结果如下:可以看到能够让对象正常的进行构造和析构

在这里插入图片描述

那么我们再进行拷贝一下呢?可以看到 unique_ptr 的解决方法非常简单粗暴,就是不允许你进行拷贝。

在这里插入图片描述

5.2 模拟实现

下面我们简化模拟实现一份 unique_ptr 来了解它的原理。

代码如下所示:

namespace edison
{
    template<class T>
    class unique_ptr
    {
    public:
        // 构造函数
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 析构
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 禁止拷贝构造和赋值重载
        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
    
    private:
        T* _ptr;
    };
}

这两行代码在做什么?

unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

作用只有一个:彻底禁止拷贝。也就是说:

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2(up1);   // 编译错误
up2 = up1;                  // 编译错误

为什么要禁止拷贝?先看如果 “允许拷贝” 会发生什么:

unique_ptr<int> up1(new int(10));
unique_ptr<int> up2 = up1;

如果是普通拷贝:

up1._ptr ─┐
          ├──> 同一块内存
up2._ptr ─┘

执行结果:

up1 析构 → delete
up2 析构 → 再 delete

问题:同一块内存被释放两次!

那么它和 auto_ptr 的核心区别是什么呢?

auto_ptr 的做法:

auto_ptr(auto_ptr<T>& sp)
{
    _ptr = sp._ptr;
    sp._ptr = nullptr;
}

它的特点是:允许拷贝,但拷贝时偷偷 “转移所有权”,会导致原对象被清空。

而 unique_ptr 的做法是直接禁止 = delete,意思是:这种操作从语法层面就不允许存在。

unique_ptr 的核心原则是:同一时刻只能有一个指针拥有资源。那怎么保证?

答案就是:

  • 不允许拷贝(避免多个指针指向同一资源)
  • 只允许 “转移所有权”(通过移动语义实现)

运行结果如下所示:

在这里插入图片描述

所以 unique_ptr 是禁止拷贝,保证资源永远只有一个拥有者。

6. shared_ptr

C++11 中开始提供更靠谱的、并且支持拷贝的智能指针,叫做 shared_ptr,具体详情可以访问:shared_ptr 文档

shared_ptr的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源。

例如:公司的行政人员晚上在下班之前都会通知,让最后走的同事记得把门锁下。

也就是说:

  • shared_ptr 在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是 0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是 0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

6.1 使用

代码如下所示:

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) --- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() --- 析构" << endl;
    }
    //private:
    int _a;
};

int main()
{
    // C++11
    shared_ptr<A> sp1(new A(10));
    shared_ptr<A> sp2(new A(20));

    // 拷贝
    shared_ptr<A> sp3(sp1);

    sp1->_a++;
    sp3->_a++;

    cout << "a = " << sp1->_a << endl;
    cout << "a = " << sp3->_a << endl;

    return 0;
}

运行结果如下所示:可以看到此时 pa1和 sp3 共同管理这份资源。

在这里插入图片描述

6.2 模拟实现

下面我们简化模拟实现一份 shared_ptr 来了解它的原理。

shared_ptr 是使用【引用计数】来支持多个拷贝管理同一个资源,最后一个析构对象释放资源。

namespace edison
{
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        shared_ptr(T* ptr)
            :_ptr(ptr)
            ,_pConut(new int(1)) // 当有资源来的时候,就创建一个计数
        {}

        // 析构
        ~shared_ptr()
        {
            // 当计数为0的时候,就进行释放
            if (--(*_pConut) == 0)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
                delete _pConut;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 拷贝构造
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            ,_pConut(sp._pConut)
        {
            ++(*_pConut); // 对计数进行++操作
        }

    private:
        T* _ptr;
        int* _pConut;  // 引用计数
    };
}

如下所示:sp1 和 sp3 的引用计数为 2,sp2、sp4、sp5 的引用计数为 3。

在这里插入图片描述

那么画图演示如下:

在这里插入图片描述

其实对于自定义的 shared_ptr 最复杂的其实是赋值重载函数,也就是如果遇到 sp1 = sp5sp3 = sp5 这种情况。因为它涉及 “旧资源释放 + 新资源共享 + 引用计数维护”。

参考代码如下所示:

// 赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
    // _ptr != sp._ptr 防止自己给自己赋值
    if (_ptr == sp._ptr)
    {
        return *this;
    }

    if (--(*_pConut) == 0)
    {
        delete _ptr;
        delete _pConut;
    }

    _ptr = sp._ptr;
    _pConut = sp._pConut;
    ++(*_pConut);

    return *this;
}

我们一步一步解释这段代码到底在干什么。

shared_ptr<T>& operator=(const shared_ptr<T>& sp)

它要完成三件事:

  • 处理自己原来管理的资源
  • 接管 sp 的资源
  • 正确维护引用计数

第一步:防止自己给自己赋值

if (_ptr == sp._ptr)
{
    return *this;
}

例如:

sp1 = sp1;

如果不判断:计数会先减 1,可能直接把资源释放掉,然后再使用已经被释放的指针,所以这里直接返回,避免灾难。

第二步:处理当前对象原来管理的资源

if (--(*_pConut) == 0)
{
    delete _ptr;
    delete _pConut;
}

假设当前对象是:

shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2 = sp1;

此时:

sp1, sp2 -> 同一块内存
引用计数 = 2

如果执行:

sp1 = sp5;

那么 sp1 必须先:放弃原来的资源

步骤:

  • 引用计数减一
  • 如果变成 0,说明没人用了,那么释放资源即可。

第三步:接管新资源

_ptr = sp._ptr;
_pConut = sp._pConut;

现在:

sp1 和 sp5 指向同一块内存

第四步:更新新资源的引用计数

++(*_pConut);

因为:

sp1 = sp5;

意味着:原本 sp5 的资源被 1 个指针使用,现在变成 2 个指针使用,所以计数要 +1

假设:

shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp3(new int(30));
shared_ptr<int> sp5(new int(50));

执行:

sp1 = sp5;
sp3 = sp5;

第一次赋值:sp1 = sp5。

原状态:

sp1 -> 1  count=1
sp5 -> 5  count=1

如下所示:

在这里插入图片描述

执行后:

sp1 -> 5
sp5 -> 5
count = 2

如下所示:

在这里插入图片描述

第二次赋值:sp3 = sp5

原状态:

sp3 -> 3 count=1
sp1 -> 5
sp5 -> 5 count=2

如下图所示:

在这里插入图片描述

执行后:

sp3 -> 5
sp1 -> 5
sp5 -> 5
count = 3

如下图所示:

在这里插入图片描述

为什么顺序不能错?这段代码的顺序非常关键:

if (--(*_pConut) == 0)
    delete _ptr;

必须先释放旧资源,再接管新资源。如果顺序反过来:

_ptr = sp._ptr;
--(*_pConut);

那么,你已经失去了原来资源的指针,再也无法 delete,会造成内存泄漏。

shared_ptr 的赋值运算符本质是:先减少旧资源的引用计数并在必要时释放,再共享新资源并增加其引用计数。

6.3 循环引用分析

shared_ptr 一般情况下是没有问题的,但是在一些极端场景下,会有一些缺点。

假设有下面这样一个场景:

struct Node
{
    A _val;
    Node* _next;
    Node* _prev;
};

int main()
{
    Node* n1 = new Node;
    Node* n2 = new Node;

    delete n1;
    delete n2;

    return 0;
}

目前这段代码是没有任何问题的

在这里插入图片描述

同样选用 shared_ptr 来管理对象,也是没有任何问题的

在这里插入图片描述

但是,如果我们想让 sp1 和 sp2 两个结点互相链接起来呢?同样还是会报错,这是一个经典的类型不匹配问题

在这里插入图片描述

很简单,我们 _next 和 _prev 用智能指针来定义

struct Node
{
    A _val;
    shared_ptr<Node> _next;
    shared_ptr<Node> _prev;
};

int main()
{
    // 智能指针
    shared_ptr<Node> sp1(new Node);
    shared_ptr<Node> sp2(new Node);

    //
    sp1->_next = sp2;
    sp2->_prev = sp1;

    return 0;
}

运行以后发现,程序倒是没有崩溃,但是 sp1 和 sp2 没有调用析构来进行释放!

在这里插入图片描述

初始状态:sp1 管理一个节点,sp2 管理一个节点

shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);

此时:不需要手动 delete,因为有 shared_ptr 管理。

sp1 所指节点:引用计数 = 1
sp2 所指节点:引用计数 = 1

接着建立双向关系

sp1->_next = sp2;
sp2->_prev = sp1;

但是发生了两件事:

  • sp1 的 _next 持有 sp2 → sp2 引用计数 +1
  • sp2 的 _prev 持有 sp1 → sp1 引用计数 +1

此时:

sp1 所指节点:引用计数 = 2(sp1 + sp2->_prev)
sp2 所指节点:引用计数 = 2(sp2 + sp1->_next)

那么 main 函数结束以后,sp1 和 sp2 析构

sp1 析构 → sp1 所指节点引用计数 -1 → 变成 1
sp2 析构 → sp2 所指节点引用计数 -1 → 变成 1

注意:此时两个节点都没有被释放!为什么没有释放?

因为:

sp1 节点 仍被 sp2->_prev 持有
sp2 节点 仍被 sp1->_next 持有

也就是说:每个节点的引用计数都还剩 1,所以析构函数不会触发 delete。

此时就形成 循环引用,关系如下:

Node1 (sp1)  → _next → Node2
Node2 (sp2)  → _prev → Node1

本质是:

Node1 持有 Node2
Node2 持有 Node1

为什么永远不会释放?关键点在这里:

  • Node1 想释放 → 必须引用计数为 0,但它被 Node2 持有(_prev)。
  • Node2 想释放 → 也必须引用计数为 0,但它被 Node1 持有(_next)。

结果 两者互相 “拉住对方”,谁也到不了 0。这不是普通的内存泄漏,而是 循环引用(cycle reference)

核心问题是:shared_ptr 是 “强引用”,会增加引用计数。当形成环时,引用计数永远无法归零,那么资源永远不会释放。

两个 shared_ptr 相互持有对方,会导致引用计数无法归零,从而产生循环引用,最终内存无法释放。

这里对应的标准解法就是:用 weak_ptr 来打破环,把节点中的 _prev 和 _next 改成 weak_ptr 就可以了。

如下所示:

在这里插入图片描述

6.3 定制删除器

所谓定制删除器,就是我们自己来定义控制删除资源的方式。

如果不是 new 出来的对象如何通过智能指针管理呢?比如 malloc 出来的对象。来看看下面这种情况。

代码如下所示:

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) --- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() --- 析构" << endl;
    }
    //private:
    int _a;
};

int main()
{
    shared_ptr<A> sp1((A*)malloc(10));
    
    return 0;
}

运行结果如下:对于 malloc 申请的对象并没有进行 free。

在这里插入图片描述

并且除了 malloc,如果我们使用了 new [] 那么也会出现这种问题。

代码如下所示:

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) --- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() --- 析构" << endl;
    }
    //private:
    int _a;
};

int main()
{
    shared_ptr<A> sp1(new A[10]);
    
    return 0;
}

运行结果如下:可以看到构造了 10 次,但是却析构了一次就崩溃了

在这里插入图片描述

其实 shared_ptr 设计了一个删除器来解决这个问题,我们可以先定制一个专门输出数组的删除器。

struct DeleteArray
{
    void operator()(A* ptr)
    {
        delete[] ptr;
    }
};

可以看到,此时就能成功的释放了

在这里插入图片描述

同样的,我们还可以把这个数组删除器写成一个类模板,这样所有类型都可以进行使用。

template<class T>
struct DeleteArray
{
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

运行结果如下:

在这里插入图片描述

同时,对于 malloc 申请的资源,我们可以写一个仿函数的删除器。

// 仿函数的删除器
template<class T>
struct FreeFunc 
{
    void operator()(T* ptr)
    {
        cout << "free:" << ptr << endl;
        free(ptr);
    }
};

运行结果如下所示:可以看到此时已经成功的释放掉了

在这里插入图片描述

同样,我们还可以写成 lambda 表达式的方式,这样更简洁

int main()
{
    shared_ptr<A> sp1(new A[10], [](A* ptr) {delete[] ptr; });
    shared_ptr<A> sp2((A*)malloc(10), [](A* ptr) {free(ptr); });

    return 0;
}

同样,我们可以采用 function 函数包装器的方式,来为我们自己实现的 shared_ptr 增加删除器。

namespace edison
{
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        ......

        // 析构
        ~shared_ptr()
        {
            // 当计数为0的时候,就进行释放
            if (--(*_pcount) == 0)
            {
                cout << "delete: " << _ptr << endl;
                //delete _ptr;
                _del(_ptr);
                delete _pcount;
            }
        }

        // 定制删除器
        template<class D>
        shared_ptr(T* ptr, D del)
            :_ptr(ptr)
            , _pConut(new int(1))
            , _del(del)
        {}

    private:
        T* _ptr;
        int* _pConut;  // 引用计数

        function<void(T*)> _del; // 删除器
    };
}

此时我们再换成自定义的 shared_ptr 来看看

// 自定义类
class A
{
public:
    // 构造
    A(int a = 0)
        :_a(a)
    {
        cout << "A(int a = 0) --- 构造" << endl;
    }

    // 析构
    ~A()
    {
        cout << "~A() --- 析构" << endl;
    }
    //private:
    int _a;
};

template<class T>
struct DeleteArray
{
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};

int main()
{
    edison::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
    
    return 0;
}

运行结果如下:

在这里插入图片描述

7. weak_ptr

具体详情可以访问:weak_ptr 文档

weak_ptr 原理:不拥有资源,只是 “观察者”,不会增加引用计数。

7.1 使用

weak_ptr 不是 RAII 智能指针,而是专门用来解决 shared_ptr 循环引用的问题。

它有一个函数叫做 use_count ,它可以用来查看引用计数。

struct Node
{
    A _val;
    weak_ptr<Node> _next;
    weak_ptr<Node> _prev;
};

int main()
{
    // 智能指针
    shared_ptr<Node> sp1(new Node);
    shared_ptr<Node> sp2(new Node);

    cout << "链接前: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;

    //
    sp1->_next = sp2;
    sp2->_prev = sp1;

    cout << "链接后: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;

    return 0;
}

运行结果如下:

在这里插入图片描述

本质机制:weak_ptr 指向 shared_ptr 的资源,但不参与 “所有权”,不参与资源管理,只做弱引用,用来打破 shared_ptr 的循环引用。

7.2 模拟实现

既然它是用来解决 shared_ptr 循环引用的问题,那么我们需要在自定义的 shared_ptr 中进行添加 use_count() 函数。

如下所示:

namespace edison
{
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pConut(new int(1)) // 当有资源来的时候,就创建一个计数
        {}

        // 析构
        ~shared_ptr()
        {
            // 当计数为0的时候,就进行释放
            if (--(*_pConut) == 0)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
                delete _pConut;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 拷贝构造
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pConut(sp._pConut)
        {
            ++(*_pConut); // 对计数进行++操作
        }

        // 赋值重载
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            // _ptr != sp._ptr 防止自己给自己赋值
            if (_ptr == sp._ptr)
            {
                return *this;
            }

            if (--(*_pConut) == 0)
            {
                delete _ptr;
                delete _pConut;
            }

            _ptr = sp._ptr;
            _pConut = sp._pConut;
            ++(*_pConut);

            return *this;
        }

        // 统计引用计数的个数
        int use_count()
        {
            return *_pConut;
        }
        
        // 获取原生指针
        T* get()
        {
            return _ptr;
        }

    private:
        T* _ptr;
        int* _pConut;  // 引用计数
    };
}

struct Node
{
    A _val;
    edison::shared_ptr<Node> _next;
    edison::shared_ptr<Node> _prev;
};

int main()
{
    // 智能指针
    edison::shared_ptr<Node> sp1(new Node);
    edison::shared_ptr<Node> sp2(new Node);

    cout << "链接前: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp1.use_count() << endl;

    //
    sp1->_next = sp2;
    sp2->_prev = sp1;

    cout << "链接后: " << "sp1.use_count=" << sp1.use_count() << " sp2.use_count= " << sp2.use_count() << endl;

    return 0;
}

运行结果如下:可以看到此时已经产生循环引用了。

在这里插入图片描述

那么接下来模拟实现一个 weak_ptr。

namespace edison
{
    template<class T>
    class weak_ptr
    {
    public:
        // 无参构造
        weak_ptr()
            :_ptr(nullptr)
        {}

        // 用 shared_ptr 来构造
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())
        {}

        // 用 shared_ptr 来赋值
        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;
    };
}

此时再用我们自己实现的 weak_ptr 来管理结点的 _next 和 _prev 指针

struct Node
{
    A _val;
    edison::weak_ptr<Node> _next;
    edison::weak_ptr<Node> _prev;
};

运行结果如下:

在这里插入图片描述

可以看到我们自己实现的 weak_ptr 也能解决循环引用的问题!

8. 总结

我们来把智能指针总结一下:

auto_ptr:允许拷贝,但用 “置空源对象” 来转移所有权(隐式转移)

管理权转移,并且会导致被拷贝对象悬空,建议不要使用它。

unique_ptr:直接禁止拷贝,从语法层面杜绝多所有权(只允许唯一拥有)

禁止拷贝,简单粗暴,如果有不需要拷贝的场景,建议使用它。

shared_ptr:允许拷贝,通过 “引用计数” 实现多对象共享所有权

引用计数支持拷贝,如果碰到需要拷贝的场景,就使用它。但是要小心构成循环引用,因为循环引用会导致内存泄漏。

weak_ptr:不拥有资源,不增加引用计数,用于辅助 shared_ptr(解决循环引用)

专门解决 shared_ptr 的循环引用问题。

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

C++ 98 中产生了第一个智能指针 auto_ptr,而 C++ 的 boost 库给出了更实用的 scoped_ptr 和 shared_ptr 和 weak_ptr。

接着 C++ 的 TR1 引入了 shared_ptr 等等,不过需要注意的是 TR1 并不是标准版。

最后在 C++ 11 中引入了 unique_ptr、shared_ptr 和 weak_ptr,不过需要注意的是 unique_ptr 对应的是 boost 库中的 scoped_ptr,并且这些智能指针的实现原理都是参考 boost 库来实现的。

智能指针完整代码

#include <iostream>
using namespace std;

namespace edison
{
    template<class T>
    class auto_ptr
    {
    public:
        // 构造函数
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 赋值重载
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            // 检测是否为自己给自己赋值
            if (this != &ap)
            {
                // 释放当前对象中资源
                if (_ptr)
                    delete _ptr;

                // 转移ap中资源到当前对象中
                _ptr = ap._ptr;
                ap._ptr = NULL;
            }
        }

        // 析构
        ~auto_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 拷贝构造
        // ap2(ap1)
        auto_ptr(auto_ptr<T>& sp)
            :_ptr(sp._ptr)
        {
            // 管理权转移
            sp._ptr = nullptr; // 把 ap1 的内容给置空
        }

    private:
        T* _ptr;
    };

    template<class T>
    class unique_ptr
    {
    public:
        // 构造函数
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}

        // 析构
        ~unique_ptr()
        {
            if (_ptr)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 禁止拷贝构造和赋值重载
        unique_ptr(const unique_ptr<T>& sp) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;

    private:
        T* _ptr;
    };

    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pcount(new int(1)) // 当有资源来的时候,就创建一个计数
        {}

        // 析构
        ~shared_ptr()
        {
            // 当计数为0的时候,就进行释放
            if (--(*_pcount) == 0)
            {
                cout << "delete: " << _ptr << endl;
                delete _ptr;
                delete _pcount;
            }
        }

        // 重载 * 和 ->,能够像指针一样使用
        T& operator*() {
            return *_ptr;
        }

        T* operator->() {
            return _ptr;
        }

        // 拷贝构造
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pcount(sp._pcount)
        {
            ++(*_pcount); // 对计数进行++操作
        }

        // 赋值重载
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            // _ptr != sp._ptr 防止自己给自己赋值
            if (_ptr == sp._ptr)
            {
                return *this;
            }

            if (--(*_pcount) == 0)
            {
                delete _ptr;
                delete _pcount;
            }

            _ptr = sp._ptr;
            _pcount = sp._pcount;
            ++(*_pcount);

            return *this;
        }

        // 统计引用计数的个数
        int use_count() const
        {
            return *_pcount;
        }

        // 获取原生指针
        T* get() const
        {
            return _ptr;
        }

    private:
        T* _ptr;
        int* _pcount;  // 引用计数   
    };

    template<class T>
    class weak_ptr
    {
    public:
        // 无参构造
        weak_ptr()
            :_ptr(nullptr)
        {}

        // 用 shared_ptr 来构造
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())
        {}

        // 用 shared_ptr 来赋值
        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;
    };
}

以上就是 C++11 中智能指针的全部内容。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐