C++智能指针


一、 为什么需要智能指针?

C++引入智能指针的主要目的是为了解决手动管理内存的问题,提高程序的健壮性和可维护性。在C++中,内存管理由程序员手动完成,包括内存的分配和释放。手动管理内存可能导致一些常见的问题,如内存泄漏、释放已经释放的内存(二次释放)、野指针等、悬挂指针等等。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>

class Person 
{
public:
	Person(const char* n) 
	{
		name = new char[strlen(n) + 1];
		strcpy(name, n);
	}

	~Person() 
	{
		delete[] name;
	}

	void print() const
	{
		cout << name << std::endl;
	}

private:
	char* name;
};

int main() 
{
	Person* personPtr = new Person("JJJ");
	personPtr->print();
	delete personPtr; // 忘记释放内存
	return 0;
}

我们通过 new 在堆上创建了一个 Person 对象,但在程序结束前忘记了调用 delete 来释放内存。这会导致内存泄漏,因为自定义对象的析构函数不会被调用,从而无法释放 name 的内存,从而导致内存泄漏的问题。
代码比较短的情况下,这种用了new,忘记delete的情况可能出现的概率比较小,但是如果代码行数一多,好几个文件互相包含那可就容易出问题了。
忘记delete这种情况可能还比较容易排查,那我们来看看下面这种情况:这段程序有没有什么内存方面的问题?

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、如果p1这里new抛异常会如何?

如果p1这里new int抛出了异常(比如因为内存不足),那么这个异常会立即传播到main函数中的catch块。Func函数中的后续代码(包括p2的初始化、div函数的调用、delete p1delete p2)将不会被执行。因为异常被捕获了,程序不会立即终止,而是会输出异常信息(如果有的话)并继续执行catch块之后的代码。

2、如果p2这里new抛异常会如何?

如果p2这里new int抛出了异常,那么和p1的情况类似,这个异常也会立即传播到main函数中的catch块。div函数的调用、delete p1delete p2将不会被执行。

3、如果div调用这里又会抛异常会如何?

如果div函数调用时抛出了异常(比如因为b为0),这个异常也会立即传播到main函数中的catch块。任何在div调用之后的代码(包括delete p1delete p2)将不会被执行。

总结

在C++中,一旦一个异常被抛出并且没有被函数内的catch块捕获,它会一直传播到调用该函数的更高层级,直到被某个catch块捕获,或者到达main函数的末尾(此时程序会终止)。在这个例子中,任何newdiv调用中的异常都会被main函数中的catch块捕获,并且程序会输出异常信息而不会立即终止。

另外,注意一点,在捕获异常后,如果程序继续执行,你应该确保那些因为异常而未被执行的delete操作在后续被正确处理,否则可能会导致内存泄漏。在这个例子中,如果newdiv调用抛出异常,那么相应的delete操作就不会被执行,所以你需要设计一种策略来处理这种情况。例如,你可以在捕获异常后手动释放已经分配的内存,或者使用智能指针来自动管理内存。

  1. 利用异常的重新捕获解决:
#include <iostream>  
#include <stdexcept> // For std::invalid_argument  
using namespace std;  
  
int div1(int a, int b)  
{  
    if (b == 0)  
        throw invalid_argument("除0错误");  
    return a / b;  
}  
  
void Func()  
{  
    int* p1 = nullptr;  
    int* p2 = nullptr;  
      
    try {  
        // 分配内存  
        p1 = new int;  
        p2 = new int;  
          
        // 在这里调用可能抛出异常的函数  
        cout << div1(5, 2) << endl; // 假设这里的参数是用户输入的  
    } catch (const exception& e) {  
        // 捕获异常,此时需要手动释放已经分配的内存  
        cout << "Exception caught: " << e.what() << endl;  
        delete p2; // 先释放p2,因为p1可能还在使用  
        delete p1; // 然后释放p1  
        p1 = nullptr; // 将指针设置为nullptr以避免悬挂指针  
        p2 = nullptr;  
    }  
      
    // 无需在这里显式释放内存,因为如果在try块中没有抛出异常,那么指针将在try块的末尾之外自动成为悬挂指针  
    // 如果在catch块中释放了内存,那么这些指针在catch块之外也应该是nullptr  
}  
  
int main()  
{  
    try {  
        Func();  
    } catch (const exception& e) {  
        // 这里可以捕获Func中未处理的异常,但在这个例子中Func已经处理了所有异常  
        cout << "Main catch block: " << e.what() << endl;  
    }  
    return 0;  
}
  1. 利用智能指针来解决:
#include <iostream>  
#include <memory> // For std::unique_ptr  
#include <stdexcept> // For std::invalid_argument  
using namespace std;  
  
int div1(int a, int b)  
{  
    if (b == 0)  
        throw invalid_argument("除0错误");  
    return a / b;  
}  
  
void Func()  
{  
    // 使用unique_ptr来自动管理内存  
    unique_ptr<int> p1(new int);  
    unique_ptr<int> p2(new int);  
      
    try {  
        // 在这里调用可能抛出异常的函数  
        cout << div1(5, 2) << endl; // 假设这里的参数是用户输入的  
    } catch (const exception& e) {  
        // 捕获异常,此时p1和p2会在离开作用域时自动删除  
        cout << "Exception caught: " << e.what() << endl;  
    }  
      
    // 无需显式调用delete,因为unique_ptr会在离开作用域时自动处理  
}  
  
int main()  
{  
    try {  
        Func();  
    } catch (const exception& e) {  
        // 这里可以捕获Func中未处理的异常,但在这个例子中Func已经处理了所有异常  
        cout << "Main catch block: " << e.what() << endl;  
    }  
    return 0;  
}

C++11引入智能指针的主要原因是为了解决手动管理内存时可能出现的各种问题,特别是内存泄漏和悬挂指针的问题。以下是智能指针的几个关键优势:

  1. 自动内存管理:智能指针可以在不再需要内存时自动释放它。这通过引用计数(如std::shared_ptr)或作用域管理(如std::unique_ptrstd::weak_ptr)来实现。这大大减少了手动删除对象时可能出现的错误,从而降低了内存泄漏的风险。
  2. 安全性:智能指针可以帮助防止悬挂指针问题。悬挂指针是指向已被释放内存的指针。当试图通过悬挂指针访问内存时,可能会导致未定义行为,包括程序崩溃。智能指针通过确保在对象生命周期结束时自动删除指针来避免这种情况。
  3. 简化代码:使用智能指针可以减少显式管理内存所需的代码量。例如,不需要再显式调用delete来释放通过new分配的内存。这不仅减少了出错的可能性,还使代码更易于阅读和维护。
  4. 支持自定义删除器:智能指针允许你指定一个自定义的删除器,用于在适当的时候释放资源。这使得智能指针能够处理不仅仅是动态分配的内存,还可以处理其他类型的资源,如文件句柄、网络连接等。
  5. 更好的异常安全性:在手动管理内存时,如果在分配内存后但在使用它之前抛出异常,可能会导致内存泄漏。使用智能指针可以确保即使在这种情况下,内存也会被正确释放。

C++11提供了三种主要的智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr。每种智能指针都有其特定的用途和适用场景,使得开发者可以根据需要选择最适合的智能指针来管理资源。

二、 内存泄漏

1. 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

2. 内存泄漏分类(了解)

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

  • 堆内存泄漏(Heap leak)
    堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏
    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

3. 如何检测内存泄漏(了解)

4. 如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。

总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

三、 智能指针的使用及原理

1. RAII

RAII,全称Resource Acquisition Is Initialization,即资源获取即初始化,是C++语言的一种管理资源、避免泄漏的惯用法。它的核心思想是资源的获取和释放应与对象的生命周期绑定在一起。通过定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。

RAII的典型应用包括文件句柄、内存分配、数据库连接、线程锁等各种资源管理。例如,可以使用一个包含文件句柄的类,在构造函数中打开文件,并在析构函数中关闭文件,确保无论发生什么情况,都能正确关闭文件。

通过使用RAII,可以更容易地管理资源,提高代码的可维护性和健壮性。这种做法有两大好处:

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

比如这里我们简单的写一个只能指针出来,类中只需要一个指针变量,在构造函数中通过new将对象创建出来,在析构函数中通过delete释放资源就好。

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
   SmartPtr(T* ptr = nullptr)
      : _ptr(ptr)
   {}
   ~SmartPtr()
   {
       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()
{
 	ShardPtr<int> sp1(new int);
    ShardPtr<int> sp2(new int);
    cout << div() << endl;
}
int main()
{
   try {
	 Func();
   }
   catch(const exception& e)
   {
        cout<<e.what()<<endl;
   }
 return 0;
}

2. 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。当然库中提供的指针指针不止重载了*和->,还重载了其他的运算符,下面是与之相关的一些简单介绍:

  1. 解引用运算符 (*):
    允许通过智能指针访问其指向的对象。
   std::unique_ptr<int> ptr(new int(5));
   *ptr = 10; // 使用解引用运算符访问并修改指向的值
  1. 箭头运算符 (->):
    允许通过智能指针访问其指向对象的成员。
struct MyStruct 
	{
       int value;
    };
   
std::unique_ptr<MyStruct> ptr(new MyStruct{5});
ptr->value = 10; // 使用箭头运算符访问并修改成员变量
  1. 赋值运算符 (=):
    允许将另一个智能指针或原始指针赋值给智能指针。
std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = ptr1; // 错误,unique_ptr不支持拷贝赋值
std::unique_ptr<int> ptr3 = std::move(ptr1); // 正确,使用移动赋值
  1. 比较运算符 (==, !=):
    用于比较两个智能指针是否相等或不等。
std::shared_ptr<int> ptr1(new int(5));
std::shared_ptr<int> ptr2(new int(5));
if (ptr1 == ptr2) {
   // 这不会执行,因为ptr1和ptr2指向不同的对象
}
if (ptr1 != ptr2) {
   // 这会执行
}
  1. 布尔转换运算符:
    智能指针通常定义了到bool的转换,以允许在条件语句中检查智能指针是否为空。
std::unique_ptr<int> ptr(new int(5));
if (ptr) {
    // 这会执行,因为ptr不为空
}

请注意,std::unique_ptrstd::shared_ptr的行为和接口有所不同。例如,std::unique_ptr是独占所有权的智能指针,因此不支持拷贝构造函数和拷贝赋值运算符,但支持移动构造函数和移动赋值运算符。而std::shared_ptr支持共享所有权的智能指针,它使用引用计数来管理对象的生命周期,并允许拷贝和移动操作。

这里我们重载运算符之后可以设计出一个简单的智能指针:

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

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<<endl;
 	SmartPtr<int> sparray(new Date);
 // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
 // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
 	sparray->_year = 2018;
 	sparray->_month = 1;
 	sparray->_day = 1;
}

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

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

3. std::auto_ptr

std::auto_ptr文档
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份hao::auto_ptr来了解它的原理

// C++98 管理权转移 auto_ptr
namespace hao
{
	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;
			}
			return *this;
		}
		~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
//int main()
//{
// std::auto_ptr<int> sp1(new int);
// std::auto_ptr<int> sp2(sp1); // 管理权转移
//
// // sp1悬空
// *sp2 = 10;
// cout << *sp2 << endl;
// cout << *sp1 << endl;
// return 0;
//}

4. std::unique_ptr

C++11中开始提供更靠谱的unique_ptr
std::unique_ptr文档
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原理

// C++11库才更新智能指针实现
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace hao
{
	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;
	};
}
//int main()
//{
// /*hao::unique_ptr<int> sp1(new int);
// hao::unique_ptr<int> sp2(sp1);*/
//
// std::unique_ptr<int> sp1(new int);
// //std::unique_ptr<int> sp2(sp1);
//
// return 0;
//}

5. std::shared_ptr

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
std::shared_ptr文档
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
// 引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源
namespace hao
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pRefCount(new int(1))
            , _pmtx(new mutex)
        {}
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pRefCount(sp._pRefCount)
            , _pmtx(sp._pmtx)
        {
            AddRef();
        }
        void Release()
        {
            _pmtx->lock();
            bool flag = false;
            if (--(*_pRefCount) == 0 && _ptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                delete _pRefCount;
                flag = true;
            }
            _pmtx->unlock();
            if (flag == true)
            {
                delete _pmtx;
            }
        }
        void AddRef()
        {
            _pmtx->lock();
            ++(*_pRefCount);
            _pmtx->unlock();
        }
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            //if (this != &sp)
            if (_ptr != sp._ptr)
            {
                Release();
                _ptr = sp._ptr;
                _pRefCount = sp._pRefCount;
                _pmtx = sp._pmtx;
                AddRef();
            }
            return *this;
        }
        int use_count()
        {
            return *_pRefCount;
        }
        ~shared_ptr()
        {
            Release();
        }
        // 像指针一样使用
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        T* get() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pRefCount;
        mutex* _pmtx;
    };

    // 简化版本的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;
    };
}
// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
//int main()
//{
// hao::shared_ptr<int> sp1(new int);
// hao::shared_ptr<int> sp2(sp1);
// hao::shared_ptr<int> sp3(sp1);
//
// hao::shared_ptr<int> sp4(new int);
// hao::shared_ptr<int> sp5(sp4);
//
// //sp1 = sp1;
// //sp1 = sp2;
//
// //sp1 = sp4;
// //sp2 = sp4;
// //sp3 = sp4;
//
// *sp1 = 2;
// *sp2 = 3;
//
// return 0;
//}

std::shared_ptr的线程安全问题
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
// 1.演示引用计数线程安全问题,就把AddRefCount和SubRefCount中的锁去掉
// 2.演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,main函数的n改大一些概率就
变大了,就容易出现了。
// 3.下面代码我们使用SharedPtr演示,是为了方便演示引用计数的线程安全问题,将代码中的
//SharedPtr换成shared_ptr进行测试,可以验证库的shared_ptr,发现结论是一样的。
struct Date
{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void SharePtrFunc(hao::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		hao::shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
		{
		unique_lock<mutex> lk(mtx);
		copy->_year++;
		copy->_month++;
		copy->_day++;
		}
	}
}
int main()
{
	hao::shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 100000;
	mutex mtx;
	thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	cout << p.use_count() << endl;
	return 0;
}

std::shared_ptr的循环引用

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

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

在这里插入图片描述

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

如果不是new出来的对象如何通过智能指针管理呢?
比如说如果我们new的时候是一个数组,那delete的时候是不是就得delete[]啊?那对于文件来说,删除器是不是就是关闭文件啊?所以我们可以通过不同的删除器来控制不同的释放资源的方法,删除器可以充当参数直接传进去。
在C++智能指针中,删除器(deleter)是一个用于定制对象删除行为的机制。默认情况下,智能指针使用delete运算符来释放它们所指向的对象。但是,通过提供一个自定义的删除器,你可以改变这种行为,以满足特定的需求。

删除器是一个可调用对象(函数、函数指针、函数对象或lambda表达式),它接受智能指针所管理的对象的指针作为参数,并执行适当的删除逻辑。

// 仿函数的删除器
template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteArrayFunc {
	void operator()(T* ptr)
	{
		cout << "delete[]" << ptr << endl;
		delete[] ptr;
	}
};
int main()
{
	FreeFunc<int> freeFunc;
	std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
	DeleteArrayFunc<int> deleteArrayFunc;
	std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
	std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });
	std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
		{fclose(p); });

	return 0;
}

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

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

争不过朝夕,又念着往昔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值