【C++】智能指针

本篇文章主要讲解 C++ 中的智能指针的使用及其原理


目录

[1 什么是智能指针](#1 什么是智能指针)

[2 智能指针的优点](#2 智能指针的优点)

[3 智能指针的使用](#3 智能指针的使用)

[3.1 库中提供的智能指针](#3.1 库中提供的智能指针)

[3.2 定制删除器](#3.2 定制删除器)

[3.3 智能指针的其他特性](#3.3 智能指针的其他特性)

[4 智能指针的原理](#4 智能指针的原理)

[5 weak_ptr](#5 weak_ptr)

[5.1 shared_ptr 循环引用问题](#5.1 shared_ptr 循环引用问题)

[5.2 weak_ptr 解决循环引用问题](#5.2 weak_ptr 解决循环引用问题)

总结


1 什么是智能指针

智能指针RAII设计思想的一种具体实现。RAII 是 Resource Acquisition Is Initialization 的缩写,是一种管理资源的类的设计思想,本质上就是将动态获取到的资源交给类对象来进行管理,避免资源泄露。这里的资源可以是内存指针、网络连接、文件指针、互斥锁等等。将资源交给一个对象管理之后,在对象的生命周期内部我们可以正常使用动态开辟的资源,出了对象的生命周期,对象就会调用析构函数释放资源,这样就避免了内存泄露问题。

智能指针就是借用了 RAII 的设计思想,实现了一个具体的类来管理动态开辟的内存资源。但是只管理资源是不够的,用户还是想要通过智能指针来访问资源,所以在智能指针中还必须实现 operator*、operator->、operator\[\] 等接口。

以下是一个智能指针的简单实现:

cpp 复制代码
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		//析构时自动释放资源
		cout << "delete[] ptr" << endl;
		delete[] _ptr;
	}

	//重载对应过的访问运算符
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

2 智能指针的优点

看下面这个动态申请资源的场景:

cpp 复制代码
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <cstring>

using namespace std;

class Logger
{
public:
    Logger(const char* filename)
    {
        //C++ 打开文件
        fout.open(filename);

        if (!fout.is_open())
        {
            throw runtime_error("日志文件打开失败");
        }

        cout << "Logger 构造:日志文件打开成功" << endl;
    }

    void Write(const char* msg)
    {
        fout << msg << endl;
    }

    ~Logger()
    {
        cout << "Logger 析构:关闭日志文件" << endl;

        if (fout.is_open())
        {
            fout.close();
        }
    }

private:
    ofstream fout;
};

void ProcessData()
{
    int* data = new int[100];
    cout << "data 数组申请成功" << endl;

    char* buffer = new char[256];
    cout << "buffer 缓冲区申请成功" << endl;

    Logger* logger = new Logger("log.txt");
    cout << "logger 对象创建成功" << endl;

    strcpy(buffer, "开始处理数据");
    logger->Write(buffer);

    //假设这里中间发生了异常
    throw runtime_error("处理中途发生异常");

    //后面的代码都不会执行,资源不释放,造成了资源泄露
    delete[] data;
    delete[] buffer;
    delete logger;

    cout << "资源释放完成" << endl;
}

int main()
{
    try
    {
        ProcessData();
    }
    catch (const exception& e)
    {
        cout << "捕获异常:" << e.what() << endl;
    }

    return 0;
}

可以看到上面的代码,在 ProcessData 函数中申请了资源,但是由于中间执行过程中出错,抛出了异常,所以后续释放资源的代码没有执行,而且该资源是在函数内部申请的,后续再也无法拿到指向这些资源的指针,这些资源就永久资源泄露了。

那么如果我们将这些资源交给智能指针管理,就不会发生上述资源泄露的问题:

cpp 复制代码
#include <iostream>
#include <string>
#include <fstream>
#include <stdexcept>
#include <cstring>
#include <functional>

using namespace std;

template<class T>
class SmartPtr
{
    using del_t = function<void(T* ptr)>;
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

    SmartPtr(T* ptr, del_t del)
        :_ptr(ptr)
        ,_del(del)
    {}

	~SmartPtr()
	{
		//析构时自动释放资源
		cout << "delete[] ptr" << endl;
		_del(_ptr);
	}

	//重载对应过的访问运算符
	T& operator*()
	{
		return *_ptr;
	}

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

	T& operator[](size_t i)
	{
		return _ptr[i];
	}

    T* Ptr()
    {
        return _ptr;
    }

private:
	T* _ptr;
    del_t _del = [](T* ptr) { delete ptr; };
};

class Logger
{
public:
    Logger(const char* filename)
    {
        //C++ 打开文件
        fout.open(filename);

        if (!fout.is_open())
        {
            throw runtime_error("日志文件打开失败");
        }

        cout << "Logger 构造:日志文件打开成功" << endl;
    }

    void Write(const char* msg)
    {
        fout << msg << endl;
    }

    ~Logger()
    {
        cout << "Logger 析构:关闭日志文件" << endl;

        if (fout.is_open())
        {
            fout.close();
        }
    }

private:
    ofstream fout;
};

void ProcessData()
{
    
    SmartPtr<int> sp1 = { new int[100], [](int* ptr) { delete[] ptr; } };
    cout << "data 数组申请成功" << endl;

    SmartPtr<char> sp2 = { new char[256], [](char* ptr) { delete[] ptr; } };
    cout << "buffer 缓冲区申请成功" << endl;

    SmartPtr<Logger> logger = new Logger("log.txt");
    cout << "logger 对象创建成功" << endl;

    strcpy(sp2.Ptr(), "开始处理数据");
    logger->Write(sp2.Ptr());

    //假设这里中间发生了异常
    throw runtime_error("处理中途发生异常");

    //不再需要资源释放代码了

    cout << "资源释放完成" << endl;
}

int main()
{
    try
    {
        ProcessData();
    }
    catch (const exception& e)
    {
        cout << "捕获异常:" << e.what() << endl;
    }

    return 0;
}

在智能指针中,我新添加了一个定制删除器的功能,就是可以在智能指针构造时传入一个 function<void (T* ptr)> 可以封装的可调用对象,这样就可以实现传入 delete 与 delete\[\] 了,而不是限制写死 delete 了。这里我们也可以看到包装器和 lambda 匿名函数的价值。

所以比起手动 delete 或者 delete\[\] 释放资源,智能指针会更加安全,即使出现一些不可预知的情况,智能指针的使用也能保证资源的正确释放,不至于造成内存泄露情况。


3 智能指针的使用

3.1 库中提供的智能指针

在 C++98 及 C++11 中一共支持 4 种常用的智能指针:

这几个智能指针都在 memory头文件下,之所以有这么多智能指针,还是为了解决各自的问题而产生的。

auto_ptr

auto_ptr 是 C++98 就存在的智能指针,auto_ptr 的特点就是将被拷贝对象的资源管理权转移给拷贝对象,这样就会导致被拷贝对象被悬空,此时再访问被拷贝对象就会报错:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

int main()
{
	auto_ptr<int> ap1(new int(10));
	cout << *ap1 << endl;

	auto_ptr<int> ap2 = ap1;
	cout << *ap2 << endl;
	//ap1 被悬空,再次访问会报错
	//cout << *ap1 << endl;

	return 0;
}

我们可以调试一下看看:

可以看到将 ap1 拷贝给 ap2 之后,ap1 的指针就为空了,所以说 auto_ptr 拷贝之后会将被拷贝对象置空,再访问就会出错。

正是由于 auto_ptr 会出现上述的拷贝问题,所以不建议使用 auto_ptr 智能指针

unique_ptr

unique_ptr 是 C++11 新添加的智能指针,从其名字看来,该智能指针为唯一指针。unique_ptr 的特性就是其支持移动,但是不支持拷贝,其将拷贝构造函数设为了 delete 函数。如果智能指针不需要拷贝,那就推荐使用 unique_ptr。

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

int main()
{
	unique_ptr<int> up1(new int(10));
	cout << *up1 << endl;

	//无法进行拷贝,拷贝构造函数直接被 delete 掉了
	//unique_ptr<int> up2 = up1;

	//但是可以被移动
	unique_ptr<int> up2 = move(up1);
	cout << *up2 << endl;

	return 0;
}

移动之后,原对象就会被置空。

shared_ptr

shared_ptr 也是 C++11 新添加的智能指针,其中文名称为共享指针。其特点就是既支持拷贝,又支持移动,其拷贝是使用引用计数的方式实现的。如果智能指针对象需要被拷贝,那就推荐使用 shared_ptr。

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

int main()
{
	shared_ptr<int> sp1(new int(10));
	cout << *sp1 << endl;

	// shared_ptr 支持拷贝
	shared_ptr<int> sp2 = sp1;

	//也支持移动
	shared_ptr<int> sp3 = move(sp1);
	cout << *sp3 << endl;

	return 0;
}

可以看到 shared_ptr 移动并不是将所有拷贝与被拷贝对象都置空,而只是将被移动对象的指针置空。

weak_ptr

weak_ptr 是 C++11 提供的另一种指针类型,但是其不是智能指针,不是按照 RAII 设计思想设计出来的。所以并不能直接用 weak_ptr 来管理动态资源,其就是为了解决 shared_ptr 循环引用问题而存在的,至于 shared_ptr 的循环引用问题会在 weak_ptr 中进行讲解。

3.2 定制删除器

  • unique_ptr 与 shared_ptr 析构函数默认都是使用 delete 释放资源,所以如果你开辟资源时,如果不是使用的 new,而是 new\[\],那就可能会出错。所以 unique_ptr 与 shared_ptr 支持我们传入定制删除器,实现定制化的析构逻辑。
  • 定制删除器就是一个具体的可调用对象 。shared_ptr 与 unique_ptr 传入定制删除器的位置不同,shared_ptr 是在构造时传入具体的对象,unique_ptr 是在模板参数中传入具体的类型
  • 由于 new\[\] 特别常用,所以 unique_ptr 与 shared_ptr 都支持了一份特化的版本,只要使用 T\[\] 作为模板参数,就可以管理 new\[\] new 出来的资源,比如:
cpp 复制代码
std::unique_ptr<std::string[]> up(new std::string[10]);
std::shared_ptr<std::string[]> sp(new std::string[10]);
cpp 复制代码
#include <iostream>
#include <memory>
#include <string>

struct Date 
{
    int day;

    Date(int d) : day(d) { std::cout << "Date(" << day << ") 构造\n"; }
    ~Date() { std::cout << "Date(" << day << ") 析构\n"; }

};

// 自定义删除器示例:适用于单个对象
auto singleDateDeleter = [](Date* p) {
    std::cout << "自定义删除单个Date\n";
    delete p;  // 必须对应 new
    };

// 自定义删除器示例:适用于数组
auto arrayDateDeleter = [](Date* p) {
    std::cout << "自定义删除 Date 数组\n";
    delete[] p;  // 必须对应 new[]
    };

int main() 
{
    std::cout << "--- unique_ptr 单个对象 ---\n";
    {
        //可以使用 decltype 推导出匿名函数的类型
        std::unique_ptr<Date, decltype(singleDateDeleter)> up1(new Date(1), singleDateDeleter);
        std::cout << "unique_ptr up1 使用中\n";
    }

    std::cout << "\n--- unique_ptr 数组 ---\n";
    {
        std::unique_ptr<Date[], decltype(arrayDateDeleter)> up2(new Date[3]{ Date(2), Date(3), Date(4) }, arrayDateDeleter);
        std::cout << "unique_ptr 数组 up2 使用中\n";
    }


    std::cout << "\n--- shared_ptr 单个对象 ---\n";
    {
        std::shared_ptr<Date> sp1(new Date(5), singleDateDeleter);
        std::cout << "shared_ptr sp1 使用中\n";
    }

    std::cout << "\n--- shared_ptr 数组 ---\n";
    {
        std::shared_ptr<Date> sp2(new Date[2]{ Date(6), Date(7) }, arrayDateDeleter);
        std::cout << "shared_ptr 数组 sp2 使用中\n";
    }

    return 0;
}

3.3 智能指针的其他特性

  • 我们不仅可以使用 new 或者 new\[\] 来构造 shared_ptr 或者 unique_ptr 对象,C++ 也支持使用 make_shared(C++11) 与 make_unique(C++14) 来构造一个 shared_ptr 或者 unique_ptr 对象:
cpp 复制代码
#include <iostream>
#include <memory>
#include <string>

using namespace std;

int main()
{
	//使用 make_shared 创建 sp 对象
	shared_ptr<string> sp1 = make_shared<string>("11111");

	//make_shared 无法创建 new[] 对象
	//shared_ptr<string[]> sp2 = make_shared<string[]>(10);

	//使用 make_uniqe 创建 up1 对象
	unique_ptr<string> up1 = make_unique<string>("222222");

	//使用 make_unique 创建 new[] 对象
	unique_ptr<string[]> up2 = make_unique<string[]>(10);

	return 0;
}
  • shared_ptr 与 unique_ptr 都实现了 operator bool,也就是可以支持逻辑判断,当 shared_ptr 与 unique_ptr 为空时,都返回的是 false,不为空就返回 true。
  • shared_ptr 与 unique_ptr 中构造函数都添加了 explicit 关键字,不允许普通指针隐式类型转换为智能指针对象。

4 智能指针的原理

这里我们主要实现 shared_ptr,至于 auto_ptr 和 unique_ptr 在实现了 shared_ptr 之后就很好实现了。shared_ptr 的特点就是既支持拷贝也支持移动,那么底层是如何实现的呢?其实底层是通过一个引用计数实现的,拷贝改变的引用计数,而移动则是转移资源管理权。开始状态为 sp1 与 sp2 管理同一块资源,sp3 自己管理一块资源,此时 sp1 与 sp2 的引用计数为 2,sp3 引用计数为1:

然后将 sp3 拷贝赋值给 sp1,首先 sp1 会先将引用计数--,如果引用计数减到0,那么就是释放 sp1 管理的资源;然后将 sp3 的指针以及引用计数赋值给 sp1,并将 sp1 与 sp3 的引用计数++:

之后将 sp1 移动赋值给 sp3,sp3 会先先处理自己的资源,将引用计数--,如果引用计数变为0,那么就将资源释放;然后直接将 sp1 的资源移动给 sp3,再将 sp1 置空:

所以 shared_ptr 的核心实现就在于引用计数如何实现,要想实现引用计数,我们就要想想引用计数有哪些特点。引用计数最核心的特点就是指向一块资源的对象之间共享,那么要使用静态成员变量吗?但是静态成员变量是所有成员之间共享,引用计数需要一份资源就有一个引用计数,所有成员共享就意味着所有成员的引用计数都是相同的,所以这里是不行的。这里我们选择使用 int* 指针变量来作为引用计数,第一次申请时就开辟一块 int 的堆空间;析构时也别忘记释放这块堆空间就可以了。

这里我们主要讲解拷贝构造、拷贝复制、移动构造、移动赋值、定制删除器的实现,其他函数比较简单,就不讲解了,可以看下面的具体实现代码。

拷贝构造

既然有了引用计数的具体实现方案,那么拷贝构造其实就很简单了。假设是 sp1 = sp3,只需要将 sp3 内部的 _ptr、_pcount(开辟引用计数空间的指针)赋值给 sp1 就可以了。最后也是最重要的一步,别忘记让引用计数++。

拷贝赋值

拷贝构造的时候我们首先要避免自己给自己赋值,那么这里要采用 this != &sp 吗?在智能指针这里是不行的,因为虽然两个智能指针对象的地址不同,但是其管理的资源却是相同的,此时也认为他们是同一个对象,所以这里的核心是判断管理的资源是否是同一个,所以应该采用 _ptr != sp._ptr

这里我们假设是 sp1 = sp3,这个语句的意思是让 sp1 与 sp3 共同管理一块资源,所以我们需要先对 sp1 管理的资源进行处理,也就是让 sp1 的引用计数 --,如果为0,那就释放资源;之后再将 sp3 的 _ptr、_pcount 赋值给 sp1,再将引用计数++即可。

移动构造

移动构造也很简单,只需要转移资源管理权,也就是 swap 底层资源的 _ptr 与 _pcount 就可以。

移动赋值

对于移动赋值,我们不需要判断是否指向了同一块资源,因为不管是否指向同一块资源,最后都会将被移动对象的资源管理权交给移动对象,将被移动对象的 _ptr 与 _pcount 置空。假设是 sp3 = move(sp1),只需要先对 sp3 的资源进行管理,就是引用计数--,如果减到了0就释放资源;然后交换 sp3 与 sp1 的资源管理权即可。

定制删除器

这里的定制删除器主要是指我们在构造 shared_ptr 时可以传入一个可调用对象完成一些自定义类型的资源清理工作,定制删除器我们可以使用 C++11 中的包装器实现。在 shared_ptr 类中我们添加一个成员变量,该成员变量为 function<void (T*)> 类型的可调用对象,默认为 delete 版本,然后再构造时就可以传入该类型的可调用对象,析构时调用定制删除器析构 _ptr 即可。

代码实现

cpp 复制代码
//SmartPtr.hpp
#pragma once
#include <iostream>
#include <functional>

using namespace std;

namespace LTL
{
	template<class T>
	class shared_ptr
	{
		using del_t = function<void(T*)>;

		void release()
		{
			if (_pcount && --(*_pcount) == 0)
			{
				//释放资源
				_del(_ptr);
				delete _pcount;
				_ptr = nullptr;
				_del = nullptr;
			}
		}

	public:
		//默认构造
		shared_ptr()
		{}

		//避免进行隐式类型转换
		explicit shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1)) //第一次构造时开辟引用计数的空间
		{}

		//支持定制删除器版本
		explicit shared_ptr(T* ptr, del_t del)
			:_ptr(ptr)
			, _pcount(new int(1)) //第一次构造时开辟引用计数的空间
			,_del(del)
		{}

		//析构函数
		~shared_ptr()
		{
			release();
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_del(sp._del)
		{
			//别忘记让引用计数++
			if (_pcount) 
				++(*_pcount);
		}

		//拷贝赋值
		//sp1 = sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//一定要判断资源的相同,_pcount 也可以
			if (_ptr != sp._ptr)
			{
				//先对 sp1 的资源进行管理
				release();

				//进行资源的拷贝
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_del = sp._del;

				//别忘记进行引用计数的 ++
				if (_pcount)
					++(*_pcount);
			}

			return *this;
		}

		void swap(shared_ptr<T>& sp)
		{
			std::swap(_ptr, sp._ptr);
			std::swap(_pcount, sp._pcount);
			std::swap(_del, sp._del);
		}

		//移动构造
		shared_ptr(shared_ptr<T>&& sp)
		{
			//直接转移资源的管理权
			swap(sp);
		}

		//移动赋值
		//sp3 = move(sp1)
		shared_ptr<T>& operator=(shared_ptr<T>&& sp)
		{
			//先对 sp3 的资源进行管理
			release();

			//转移资源管理权
			swap(sp);

			//将 sp 的指针置空
			sp._ptr = nullptr;
			sp._pcount = nullptr;

			return *this;
		}

		T* get() const
		{
			return _ptr;
		}

		int use_count() const
		{
			return _pcount ? *_pcount : 0;
		}

		T& operator*()
		{
			return *_ptr;
		}

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

	private:
		T* _ptr = nullptr;
		int* _pcount = nullptr; // 引用计数
		del_t _del = [](T* ptr) { delete ptr; }; // 定制删除器
	};
}



//Main.cc -- 测试用例
#include "SmartPtr.hpp"

using namespace std;
using namespace LTL;

// 简单类用于测试
class Test
{
public:
    Test(int v = 0) : val(v) { cout << "Test构造: " << val << endl; }
    ~Test() { cout << "Test析构: " << val << endl; }

    void show() { cout << "Test值: " << val << endl; }

private:
    int val;
};

// 测试默认构造和空指针
void test_default()
{
    cout << "\n=== test_default ===" << endl;
    LTL::shared_ptr<int> sp;
    cout << "use_count: " << sp.use_count() << endl;
}

// 测试拷贝构造
void test_copy()
{
    cout << "\n=== test_copy ===" << endl;
    LTL::shared_ptr<Test> sp1(new Test(10));
    cout << "sp1 use_count: " << sp1.use_count() << endl;

    LTL::shared_ptr<Test> sp2(sp1);
    cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;

    sp2->show();
}

// 测试拷贝赋值
void test_copy_assign()
{
    cout << "\n=== test_copy_assign ===" << endl;
    LTL::shared_ptr<Test> sp1(new Test(20));
    LTL::shared_ptr<Test> sp2;
    sp2 = sp1;
    cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
    sp2->show();
}

// 测试移动构造
void test_move()
{
    cout << "\n=== test_move ===" << endl;
    LTL::shared_ptr<Test> sp1(new Test(30));
    LTL::shared_ptr<Test> sp2(std::move(sp1));

    cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
    if (sp2.get()) sp2->show();
}

// 测试移动赋值
void test_move_assign()
{
    cout << "\n=== test_move_assign ===" << endl;
    LTL::shared_ptr<Test> sp1(new Test(40));
    LTL::shared_ptr<Test> sp2;
    sp2 = std::move(sp1);

    cout << "sp1 use_count: " << sp1.use_count() << ", sp2 use_count: " << sp2.use_count() << endl;
    if (sp2.get()) sp2->show();
}

// 测试自定义删除器
void test_custom_deleter()
{
    cout << "\n=== test_custom_deleter ===" << endl;
    auto del = [](Test* t) {
        cout << "自定义删除器释放对象" << endl;
        delete t;
        };

    LTL::shared_ptr<Test> sp(new Test(50), del);
    cout << "sp use_count: " << sp.use_count() << endl;
}

// 测试异常安全
void test_exception()
{
    cout << "\n=== test_exception ===" << endl;
    try
    {
        LTL::shared_ptr<Test> sp(new Test(60));
        cout << "sp use_count: " << sp.use_count() << endl;
        throw runtime_error("模拟异常");
    }
    catch (const exception& e)
    {
        cout << "捕获异常: " << e.what() << endl;
    }
    // 此时 shared_ptr 自动析构,不会泄漏资源
}

int main()
{
    test_default();
    test_copy();
    test_copy_assign();
    test_move();
    test_move_assign();
    test_custom_deleter();
    test_exception();

    return 0;
}

5 weak_ptr

5.1 shared_ptr 循环引用问题

shared_ptr 在一般情况下是没有什么问题的,适合管理资源,也支持拷贝与移动,但是 shared_ptr 一旦发生了循环引用,就会产生资源泄露问题。

那么什么是循环引用问题呢?简单来说,就是 两个 shared_ptr 对象内部又各自有 shared_ptr 指向对方,导致引用计数永远减不到0,就会导致内存泄露。比如以下的这个场景,就会发生循环引用问题:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;

	ListNode(int data = 0)
		:_data(data)
		,_next(nullptr)
		,_prev(nullptr)
	{}

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> n1(new ListNode(1));
	shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	n2->_prev = n1;

	return 0;
}

在 ListNode 结构体中,_next 与 _prev 对象分别都是 shared_ptr 类型;然后在 main 函数中开辟了 n1 与 n2 两个节点,然后 n1 的 _next 指向了 n2,n2 的 _prev 指向了 n1,这样就造成了 shared_ptr 的循环引用:

以上的循环引用问题其实就是一个逻辑上的死循环:n1 中的 _next 释放之后,n2 节点的引用计数减为 0,n2 节点就释放,那么 n1 的 _next 什么时候释放呢?n1 节点释放时,_next 就释放了,n1 节点什么时候释放呢?n1 的引用计数减为 0 时,那么 n1 的引用计数什么时候减为 0 呢?n2 的 _prev 释放时,n1 的引用减到 0,n2 的 _prev 什么时候释放呢?n2 节点释放时,也就是 n2 节点的引用计数减为 0,n2 的引用计数什么时候减到 0 呢?n1 的 next 释放之后,至此就完成了逻辑上的死循环,也就是循环引用问题。

5.2 weak_ptr 解决循环引用问题

weak_ptr 并不是像 unique_ptr 与 shared_ptr 一样,他不符合 RAII 设计思想,也不能管理资源,所以就不能用一个具体的指针来构造 weak_ptr,即使是空指针也不可以,只支持使用 shared_ptr 对象来构造 weak_ptr 对象:

weak_ptr 在用 shared_ptr 构造之后,并不会增加 shared_ptr 的引用计数,所以就可以解决循环引用的问题:

cpp 复制代码
#include <iostream>
#include <memory>

using namespace std;

struct ListNode
{
	int _data;
	weak_ptr<ListNode> _next;
	weak_ptr<ListNode> _prev;

	ListNode(int data = 0)
		:_data(data)
	{}

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> n1(new ListNode(1));
	shared_ptr<ListNode> n2(new ListNode(2));

	n1->_next = n2;
	n2->_prev = n1;

	return 0;
}

由于 weak_ptr 不参与资源管理,所以 weak_ptr 中也没有对应的 operator*,operator-> 等访问接口,但是可以通过其他接口来间接实现资源管理:

其中可以使用 expired 函数来查看绑定的 shared_ptr 有没有过期,也就是有没有释放资源;可以使用 use_count 来查看绑定的 shared_ptr 的引用计数;也可以使用 lock 函数返回一个 shared_ptr 对象,但是如果 weak_ptr 绑定的 shared_ptr 已经过期了,那么 lock 函数返回的是一个空指针


总结

在现代 C++ 编程中,智能指针是一种用于自动管理动态分配内存的工具,它能显著降低内存泄漏和野指针的风险。unique_ptr 提供对象的独占所有权,生命周期结束时自动释放资源;shared_ptr 支持多个指针共享同一对象,并通过引用计数在最后一个拥有者销毁时释放资源,但是存在循环引用问题;weak_ptr 则用作观察者,避免循环引用问题。通过 make_unique 和 make_shared 工厂函数,可以安全、高效地创建智能指针实例,同时保证异常安全性。智能指针的使用不仅提高了代码的安全性,也让资源管理更直观、简洁,是现代 C++ 程序设计的推荐实践。

但是在 shared_ptr 中容易发生线程安全问题,引用计数作为共享资源,需要加锁或者保证其原子性来保证是线程安全的。

相关推荐
程序猿乐锅1 小时前
【苍穹外卖|Day02】后台接口自测闭环:Token、DTO 与 yml 配置
java·开发语言
春栀怡铃声1 小时前
【C++修仙录02】筑基篇:适配器
c++
冰暮流星1 小时前
javascript之对象的建立-使用Object
开发语言·javascript·ecmascript
qq_2518364571 小时前
基于java 税务管理系统设计与实现
java·开发语言
LuminousCPP1 小时前
从零开始学 C++|系列开篇:从 C 到 C++ 的衔接之路
开发语言·c++·笔记
超梦dasgg1 小时前
Java 生产环境分布式定时任务全解(实战落地版)
java·开发语言·分布式
Legendary_0081 小时前
18-30W 便携照明设备 USB-C PD 升级:选型与设计要点
c语言·开发语言
破土士V1 小时前
Java基础知识集合
java·开发语言
keykey6.1 小时前
从感知机到神经网络:深度学习的起源
开发语言·人工智能·深度学习·机器学习