C++智能指针

目录

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

2. 内存泄漏

2.1. 什么叫内存泄漏呢?

2.2 内存泄漏分类

2.3. 异常安全问题

2.4. 如何避免内存泄漏

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

3.1. RAII

3.2. 智能指针的原理

3.3. 标准库的智能指针

3.4. 智能指针的模拟实现

3.4.1. auto_ptr

1. std::auto_ptr原理

2. std::auto_ptr使用

3. auto_ptr的模拟实现

拷贝构造和赋值的原理

auto_ptr的完整实现:

3.4.2. unique_ptr

1. std::unique_ptr的原理

2. std::unique_ptr的使用

3. unique_ptr的模拟实现

3.4.3. shared_ptr

1. std::shared_ptr的原理

2. std::shared_ptr的使用

3. shared_ptr的模拟实现

3.1. 用一个非静态的int成员属性

3.2. 用一个静态int的成员属性

3.3. 用new构造一个计数器

3.4.4. weak_ptr

1. std::weak_ptr的原理

2. std::weak_ptr的使用

 1. 情况一:

 2. 情况二:

 3. 情况三:

3. weak_ptr的模拟实现

3.4.5. 定制删除器

1. 当自定义类型实现了析构函数:​编辑

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

4.1. 拓展了解C++标准库和boost标准库的联系

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


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

请看下面的代码:

double Division(double left, double right)
{
	if (right == 0)
		throw "this is a error for except 0";
	else
		return left / right;
}

void why_auto_ptr_1(void)
{
	while (1)
	{
		int* ptr = new int;
		double x, y;
		std::cin >> x >> y;
		std::cout << Division(x, y) << std::endl;
		std::cout << "delete " << ptr << std::endl;
		delete ptr;
	}
}

上面的代码存在隐藏的问题,可能会带来内存泄漏。

当y != 0 时,此时上面的代码没有任何的问题。

当 y== 0 时,由于Division()内部实现,会抛异常,而我们知道,当抛了异常以后,执行流会直接跳到与之匹配且最近的catch的处理块中。那么此时上面申请的空间,就没有释放,导致内存泄漏。

例如:

2. 内存泄漏

2.1. 什么叫内存泄漏呢?

内存泄漏是指在进程中存在着一些被分配的内存空间,但在进程不再需要使用这些空间时,却没有被正确释放的问题。这种情况可能会导致进程占用的内存逐渐增加,最终导致系统性能下降甚至崩溃。

内存泄漏通常发生在程序员未能正确管理动态分配的内存的情况下。例如,当程序中分配了一块内存空间来存储数据,但在使用完毕后未释放该空间,就会引起内存泄漏问题。

内存泄漏可以是小规模的,只占用少量内存,或者更严重的,持续占用大量内存。对于较严重的内存泄漏,长时间运行的程序可能会耗尽可用的内存资源,并导致系统的不稳定性。

为了避免内存泄漏,程序员需要确保在动态分配内存后及时释放它们,通常通过调用特定的释放内存操作(如free或delete等)实现。此外,使用合理的编码规范和调试工具,进行内存管理和泄漏检测也是很重要的。

2.2 内存泄漏分类

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

2.3. 异常安全问题

由于抛出异常引发的内存泄漏通常被称为异常安全性问题。在进程发生异常时,如果未正确处理和释放相关资源,就会导致内存泄漏。

异常安全性是指即使进程发生异常(即当抛出异常后),也能够保证资源的正确释放和回收。在异常安全的代码中,无论发生何种异常,资源都会被正确地释放,从而避免了内存泄漏和其他资源泄漏的问题。

为了提高代码的异常安全性,可以采取以下几种方法:

  1. 使用智能指针:使用智能指针而不是裸指针,可以自动管理资源的释放,并确保在发生异常时也能正确释放。
  2. 使用RAII(资源获取即初始化):通过在对象的构造函数中获取资源,在析构函数中释放资源,可以确保资源在对象的生命周期结束时得以正确释放。
  3. 使用异常捕获和处理机制:在发生异常时及时捕获并处理异常,以避免资源无法正确释放的情况。

通过遵循良好的异常处理和资源管理原则,可以有效地避免由于抛出异常引发的内存泄漏和其他资源泄漏问题。

补充:

对于内存泄漏,我们可以简单理解为是指针丢了而不是内存丢了,内存不会丢失。 

如下面的情况:

void memory_leakage_test1(void)
{
	char* ptr = new char[1024 * 1024 * 1024];
}

申请空间前: 

申请空间后:

进程结束:

哎,不是说会内存泄露吗?注意,申请内存是谁去申请的?是进程,当进程的生命周期结束(即进程正常结束),那么会释放掉之前得到的所有资源(包括PCB,页表,地址空间等等资源)。而对于我们上面实现的进程,其内存泄漏造成的影响微乎其微(因为进程正常结束也会释放掉这些资源)。

对于内存泄漏真正怕的场景是这种:

1、僵尸进程有内存泄漏。

2、对于一些长期运行的进程,每次泄露一点点。每天泄露1MB(最害怕这种)。

2.4. 如何避免内存泄漏

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

内存泄漏工具检测原理:将申请的内存用一个容器记录下来。

释放内存时,从容器中删除掉。进程结束前或没有任务跑时最后在容器中还存在的,可能就是未释放的内存。

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

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

3.1. RAII

RAII是资源获取即初始化(Resource Acquisition Is Initialization)的缩写。

RAII是一种编程技术或者称之为一种编程思想,它通过在对象的构造函数中获取资源,并在析构函数中释放资源,以确保资源的正确管理。这种技术基于C++中对象的生命周期和作用域规则

使用RAII的主要目的是确保资源在使用完毕后能够被正确释放,以避免资源泄漏和其他错误。通过将资源分配和释放的逻辑封装在对象中的构造函数和析构函数中,可以利用C++对象的自动调用构造和析构的机制来管理资源。

具体步骤是:
1. 创建资源管理类:编写一个类,将要分配和释放的资源包装在其中。
2. 在构造函数中获取资源:在资源管理类的构造函数中进行资源的分配和初始化。
3. 在析构函数中释放资源:在资源管理类的析构函数中进行资源的释放和清理。

通过这种方式,当对象超出其作用域,或者发生异常导致进程的执行流离开对象所在的作用域时,资源管理类的析构函数会被自动调用,从而确保资源的正确释放。

RAII是C++中一种非常常用和高效的资源管理技术,它使得资源管理更为安全、简洁和易于维护。常见的使用RAII的场景包括动态内存分配、文件操作、线程同步等。

3.2. 智能指针的原理

智能指针是C++中的一个特殊类型,它是用于管理动态分配的内存的对象。智能指针的原理是利用对象的析构函数,在对象销毁时自动释放所持有的资源。

智能指针实质上是一个类模板,包含一个指针成员变量,用于保存动态分配的内存地址。它重载了指针相关的操作符(例如 operator*(),以及operator->()),使其具备指针的行为,例如通过解引用操作符(*)访问所指向的对象。

智能指针的主要特点是:
1. 自动内存管理:智能指针在析构函数中负责释放所持有的内存资源,避免了手动调用delete操作符的繁琐和容易出错的问题,从而避免了内存泄漏。
2. 引用计数:智能指针通常会维护一个引用计数(reference count),用于记录有多少个智能指针对象指向同一块内存。当引用计数变为0时,即没有智能指针对象再引用这块内存时,才会真正释放该内存。
3. 自动判空:智能指针能够自动检测指针是否为空指针,避免了对空指针的操作和访问,提高代码的健壮性。

3.3. 标准库的智能指针

常用的智能指针类有std::shared_ptr、std::unique_ptr和std::weak_ptr。其中:

  1. std::shared_ptr采用引用计数的方式进行内存管理,可以被多个智能指针共享资源;
  2. std::unique_ptr则采用独占所有权的方式,只能有一个智能指针对象拥有资源;
  3. std::weak_ptr是一种弱引用,不会增加引用计数,通常用于解决循环引用的问题。
  4. std::auto_ptr是早期C++标准中提供的智能指针类。它采用了独占所有权的策略,即同一时间只能有一个std::auto_ptr对象拥有所管理的资源。

使用智能指针可以方便地管理动态分配的内存,避免内存泄漏和悬空指针等问题,提高代码的可靠性和可维护性。

3.4. 智能指针的模拟实现

3.4.1. auto_ptr

1. std::auto_ptr原理

首先,需要指出的是,std::auto_ptr是C++98标准中引入的智能指针,但其设计不敢让人恭维。

std::auto_ptr是一种独占所有权的智能指针,它采用了简单的所有权转移语义。其内部实现主要基于指针的生命周期管理。

std::auto_ptr通过将资源的所有权从一个std::auto_ptr转移到另一个std::auto_ptr来实现简单的资源管理。当一个std::auto_ptr对象拥有资源时,其他std::auto_ptr对象将无法访问该资源,直到第一个std::auto_ptr对象释放或转移资源。

然而,由于std::auto_ptr具有无法进行安全的复制操作的限制,这导致了一系列的问题。例如,当使用std::auto_ptr进行赋值操作时,所有权会被转移,导致之前的std::auto_ptr对象将无法再使用该资源。这可能会导致潜在的内存泄漏或资源错误使用。

由于这些问题,C++11引入了更安全和功能更强大的智能指针std::unique_ptr来替代std::auto_ptr。std::unique_ptr提供了更好的语义和性能,并且支持移动语义,使资源管理更加灵活和安全。

综上所述,std::auto_ptr是一种简单的独占所有权智能指针,通过所有权转移来管理资源的生命周期。然而,由于其存在安全性和功能上的问题,C++11不推荐使用auto_ptr,并推荐使用更现代的std::unique_ptr来进行资源管理。

2. std::auto_ptr使用
void auto_ptr_test1(void)
{
	std::auto_ptr<int> ap1(new int);

	std::auto_ptr<int> ap2(ap1);
}

通过上面的代码和运行图我们发现,auto_ptr实现的方案是资源管理权的转移,不分责任的拷贝,会导致被拷贝对象悬空,非常不负责任的设计。但由于C++标准必须要遵守向前兼容的原则,导致这个不好的设计至今存在。

3. auto_ptr的模拟实现
namespace Xq
{
template<class T>
	class auto_ptr
	{
	public:
		// 一般是将动态资源的地址传过来构造这个智能指针对象
		auto_ptr<T>(T* ptr)
			: _ptr(ptr)
		{}
		// 利用对象的生命周期结束会自动调用析构函数
		// 确保了此时无论如何(不管你是忘记释放了,亦或者是抛异常等等)都会释放这份动态资源
		~auto_ptr<T>()
		{
			std::cout << "delete: " << _ptr << std::endl;
			delete _ptr;
		}
		// 智能指针的一大特点:像指针一样使用
		// 因此要实现operator*() 返回数据的引用 
		T& operator*()
		{
			return *_ptr;
		}
		// operator->() 返回数据的地址
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
};
拷贝构造和赋值的原理
void auto_ptr_test1(void)
{
	Xq::auto_ptr<int> ap1(new int);
	Xq::auto_ptr<int> ap2(ap1);
}

上面的代码跑起来成为一个进程的时候,会导致进程崩溃。为什么呢?老生常谈的问题,一个类如果我们没有显示实现copy constructor,那么编译器会自动生成一份,而生成的这一份copy constructor会对内置类型按字节序的方式进行拷贝,对自定义类型会去调用它的拷贝构造函数,而上面的auto_ptr只有一个内置类型,因此ap1和ap2会指向同一段空间,例如:

当这两个对象生命周期结束,回去调用析构函数,同一段空间被析构两次,导致进程crash,那难道说,在这里实现深拷贝?抱歉,不可以,因为这份资源不是你这个智能指针的啊,你只是托管这份资源。你怎么实现深拷贝啊? 这份资源不是你的。其实,我们发现,智能指针某种程度上和迭代器十分类似。它们都要达到一个目的:像指针一样使用。但不同的是,迭代器不会去管理那份资源,它只是提供了一种访问这份数据的方式。而智能指针却需要释放这份资源(当自己的生命周期结束时)。

好,你说了,既不能去实现深拷贝,而浅拷贝又会导致进程crash,那么怎么办?OK,在这里,对于auto_ptr来说,它对于拷贝和赋值的处理方案:对这份资源的管理权进行转移,使这份资源的管理者唯一。也就是说,当发生拷贝或者赋值时,会导致另一个对象悬空。

auto_ptr的完整实现:
namespace Xq
{
    template<class T>
	class auto_ptr
	{
	public:
		// 一般是将动态资源的地址传过来构造这个智能指针对象
		auto_ptr<T>(T* ptr)
			: _ptr(ptr)
		{}

		// 模拟实现 --- 资源管理权的转移
		// 导致被拷贝对象悬空,对于一些不了解底层的人,很容易犯错
		auto_ptr<T>(auto_ptr<T>& copy)
			: _ptr(copy._ptr)
		{
			copy._ptr = nullptr;
		}
		// 与拷贝构造类似,资源管理权的转移的这种无脑设计
		// 会导致被赋值的对象悬空
		auto_ptr<T>& operator=(auto_ptr<T>& copy)
		{
            // 跟以前一样,判断是否给自己赋值
			if (this != &copy)
			{
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;
				_ptr = copy._ptr;
				copy._ptr = nullptr;
			}
			return *this;
		}

		// 利用对象的生命周期结束会自动调用析构函数
		// 确保了此时无论如何(不管你是忘记释放了,亦或者是抛异常等等)都会释放这份动态资源
		~auto_ptr<T>()
		{
			std::cout << "delete: " << _ptr << std::endl;
			delete _ptr;
		}
		// 智能指针的一大特点:像指针一样使用
		// 因此要实现operator*() 返回数据的引用 
		T& operator*()
		{
			return *_ptr;
		}
		// operator->() 返回数据的地址
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

3.4.2. unique_ptr

1. std::unique_ptr的原理

std::unique_ptr是C++11引入的智能指针,用于管理动态分配的资源,它提供了独占所有权的语义,并且支持移动语义。

std::unique_ptr的原理可以简单描述如下:

1. 所有权管理:std::unique_ptr通过使用独占所有权的方式来管理资源。这意味着一旦一个std::unique_ptr拥有一个资源,其他的std::unique_ptr对象就无法访问该资源,直到拥有资源的std::unique_ptr对象释放或转移资源。

2. 指针语义:std::unique_ptr内部实际上是一个普通指针,它通过自定义的析构函数和删除器(deleter)来管理资源。可以将std::unique_ptr与裸指针进行互操作(通过运算符重载),从而使得代码与普通指针的使用类似。

3. 移动语义:std::unique_ptr支持移动语义,这意味着可以通过移动操作将资源所有权从一个std::unique_ptr转移到另一个std::unique_ptr,而不需要进行资源的复制和释放。这大大提高了资源管理的效率。

4. 模板参数:std::unique_ptr是一个类模板,其模板参数用于指定所管理资源的类型。可以在创建std::unique_ptr对象时指定资源类型,并在之后进行资源的动态分配和释放。

5. 释放资源:当一个std::unique_ptr对象被销毁或者显式调用release()函数时,它会释放所拥有的资源。资源的释放由自定义的析构函数和删除器来完成,可以根据需要指定自定义的删除器。

总的来说,std::unique_ptr通过独占所有权、指针语义和移动语义来管理动态分配的资源。它提供了更安全、更高效的资源管理方式,是C++中推荐使用的智能指针之一。

2. std::unique_ptr的使用
void unique_ptr_test1(void)
{
	std::unique_ptr<int> up1(new int);
	//copy constructor
	std::unique_ptr<int> up1(up2);
	//----------------------------------
	std::unique_ptr<int> up3(new int);
	std::unique_ptr<int> up4(new int);
	// operator=
	up3 = up4;
}

可以发现,对于std::unique_ptr来说,它的拷贝构造和赋值处理的相当粗暴简单,从错误列表我们可以看出,std::unique_ptr会将拷贝构造和赋值都设置为delete,从而间接的保证了每一个unique_ptr对象只能拥有一份唯一的资源。

3. unique_ptr的模拟实现
namespace Xq
{
    template<class T>
	class unique_ptr
	{
		// C++98的做法:只声明不实现,并设为私有
        // 之所以设置为私有,防止老六,在类外定义
		//private:
		//  unique_ptr(const unique_ptr<T>& copy);
		//  unique_ptr<T>& operator=(const unique_ptr<T>& copy);

		// C++11的做法:用delete修饰
	public:
		unique_ptr<T>(const unique_ptr<T>& copy) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& copy) = delete;

	public:
		unique_ptr<T>(T* ptr)
			:_ptr(ptr)
		{}
        // 利用对象的生命周期结束会自动调用析构函数
		// 确保了此时无论如何(不管你是忘记释放了,亦或者是抛异常等等)都会释放这份动态资源
		~unique_ptr<T>()
		{
			std::cout << "delete: " << _ptr << std::endl;
			delete _ptr;
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return &(operator*());
		}
	private:
		T* _ptr;
	};
}

3.4.3. shared_ptr

1. std::shared_ptr的原理

std::shared_ptr是C++11引入的智能指针,用于管理动态分配的资源,它提供了共享所有权的语义。

std::shared_ptr的原理可以简单描述如下:

1. 所有权管理:std::shared_ptr采用引用计数的方式来管理资源的生命周期。当一个std::shared_ptr拥有一个资源时,它会维护一个引用计数,记录共有多少个std::shared_ptr对象共享该资源。只有当最后一个std::shared_ptr对象释放资源时,资源才会被销毁。

2. 指针语义:std::shared_ptr内部实际上是一个普通指针,它通过自定义的析构函数和删除器(deleter)来管理资源。可以将std::shared_ptr与裸指针进行互操作,从而使得代码与普通指针的使用类似。

3. 引用计数:std::shared_ptr使用一个计数器来跟踪资源的引用次数。每次创建一个新的std::shared_ptr对象共享资源时,引用计数会增加。每次销毁或者转移一个std::shared_ptr对象时,引用计数会减少。当引用计数为零时,表示没有任何std::shared_ptr对象共享资源,此时资源会被销毁。

4. 循环引用问题:std::shared_ptr容易产生循环引用的问题,即两个或多个std::shared_ptr对象互相引用,导致资源无法被释放。为了解决这个问题,可以使用弱引用(std::weak_ptr)来打破循环引用,弱引用不会增加资源的引用计数,从而避免资源无法释放的情况。

5. 定制删除器:std::shared_ptr允许指定自定义的删除器来管理资源的释放,删除器是一个函数对象,用于执行资源的释放操作。可以自定义删除器来处理资源非常有用,例如在资源被释放时执行特定的清理操作。

总的来说,std::shared_ptr通过引用计数的方式来管理资源的共享所有权,它提供了更灵活、更安全的资源管理方式,适用于多个对象共享同一资源的场景。然而,需要小心处理循环引用问题,以避免资源泄漏。

2. std::shared_ptr的使用
void shared_ptr_test1(void)
{
    // sp1 引用计数 = 1
	std::shared_ptr<int> sp1(new int);  
	//copy constructor
    // sp1和sp2共同管理同一份资源,其内部引用计数 = 2
	std::shared_ptr<int> sp2(sp1);     

	// operator=
	std::shared_ptr<int> sp3(new int);
    // sp1引用计数 = 1 
    // sp2和sp3共同管理同一份资源,其内部引用计数 = 2
	sp2 = sp3;  
}
3. shared_ptr的模拟实现

通过上面的示例,我们应该知道shared_ptr是通过一个引用计数实现多个智能指针对象共同管理同一份资源。

当引用计数不为0时,--引用计数;当引用计数为0时,调用析构,释放这份资源。这样就可以达到多个对象共同管理同一份资源且不会多次释放这份资源。

那么在这里第一个问题就是:这个引用计数如何设计???

3.1. 用一个非静态的int成员属性
namespace Xq
{
    template<class T>
	class shared_ptr
	{
    public:
    //...
    private:
        T* _ptr;    // 要管理的资源
		int _count;   // 引用计数
    }
}

首先说答案,这种引用计数的设计不可取。为什么?我们所要的引用计数是不是应该要保证管理同一份资源的智能指针对象的引用计数是同一个啊?但是你这种设计,能完成上面的要求吗?显然不可以,因为每个对象都有一个_count,且不相同。

啊,听你这么一说,难道说,用一个静态成员属性?

3.2. 用一个静态int的成员属性
namespace Xq
{
    template<class T>
	class static_shared_ptr
	{
	public:
		static_shared_ptr<T>(T* ptr)
			: _ptr(ptr)
		{}
		~static_shared_ptr<T>()
		{
			if (--(_count) == 0)
			{
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;
			}
		}
		static_shared_ptr<T>(const static_shared_ptr<T>& copy)
			: _ptr(copy._ptr)
		{
			++_count;
		}

	private:
		T* _ptr;
		static int _count;
	};
    template<class T>
    int shared_ptr<T>::_count = 1;
}

首先说答案,这种设计方式同样不可取。哎,那是为什么?请看下面代码:

void shared_ptr_test3(void)
{
	Xq::shared_ptr<int> sp1(new int);
	Xq::shared_ptr<int> sp2(sp1);
	Xq::shared_ptr<int> sp3(new int);
}

当我们调用后,发现只调了一次析构,可是,这里是有两份资源啊,你却只给我释放了一次,你搞什么呢?那为什么会出现这种现象呢? 我们首先应该知道,作为静态成员属性,其不属于某一个具体的对象,而是属于整个类,也就是说,上面的引用计数为所有对象共有。那你这不是扯的吗?对于不同的资源,我们本身就要求需要不同的引用计数。

而上面代码当三个对象的生命周期结束时,会去调用析构函数,而此时发现三个对象的引用计数都是3,只有当sp1去调用析构的时候(此时引用计数 == 0),才会释放sp1的那份资源,而sp3的资源就无人释放,也就造成了内存泄漏。

因此我们推出了第三种设计方案:

3.3. 用new构造一个计数器
namespace Xq
{
    template<class T>
	class shared_ptr
	{
	public:
        shared_ptr<T>()
			:_ptr(nullptr)
			, _Pcount(nullptr)
		{}

		shared_ptr<T>(T* ptr)
			:_ptr(ptr)
			, _Pcount(new int(1))
		{}
        // 当引用计数 == 0,才释放这份资源
        // 否则,只减当前计数即可
		void _delete_func()
		{
			if (_Pcount && --(*_Pcount) == 0)
			{
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;
				delete _Pcount;
			}
		}
		~shared_ptr<T>()
		{
			_delete_func();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return &(operator*());
		}
		// 重头戏: copy constructor and operator=:

        // 将被拷贝的对象的引用计数给我,我们两个对象共有
        // 此时这两个对象共同管理同一份资源,引用计数是同一份
        // 但是只有管理这份资源的引用计数才是同一份,不与静态一样
		shared_ptr<T>(const shared_ptr<T>& copy)
			:_ptr(copy._ptr)
			, _Pcount(copy._Pcount)
		{
			++(*_Pcount);
		}
        // 对于赋值,要考虑:
        // 1. 避免自己给自己赋值(利用资源的地址或者引用计数的地址判断)
        // 2. 赋值前要将被赋值对象的引用计数--,当其 == 0时,释放该资源
        // 3. 赋值完后,此时的计数++
		shared_ptr<T>& operator=(const shared_ptr<T>& copy)
		{
			if (_ptr != copy._ptr)
			{
				_delete_func();
				_ptr = copy._ptr;
				_Pcount = copy._Pcount;
				++(*_Pcount);
			}
			return *this;
		}
        T* get() const 
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _Pcount;
	};
}

3.4.4. weak_ptr

1. std::weak_ptr的原理

std::weak_ptr是C++11引入的智能指针,用于解决shared_ptr循环引用问题,并与std::shared_ptr一起使用。它提供了对std::shared_ptr所管理的资源的非拥有性访问。

std::weak_ptr的原理可以简单描述如下:

1. 非拥有性访问:std::weak_ptr提供了对std::shared_ptr所管理的资源的非拥有性访问。它不会增加资源的引用计数,也不对资源的生命周期产生影响。通过std::weak_ptr,可以检查资源是否存在,并且可以获取对资源的std::shared_ptr的临时拷贝。

2. 引用计数:std::weak_ptr内部也维护了一个引用计数。与std::shared_ptr不同的是,std::weak_ptr对象的引用计数不会导致资源的引用计数增加。引用计数主要用于判断资源是否仍然存在。

3. 锁定资源:要访问由std::weak_ptr所引用的资源,可以使用lock()函数。lock()函数返回一个std::shared_ptr对象,如果资源仍然存在,则返回对资源的std::shared_ptr的临时拷贝;如果资源已经被销毁或者不存在,则返回一个空的std::shared_ptr。

4. 解决循环引用:std::weak_ptr主要用于解决循环引用问题。当多个std::shared_ptr对象相互引用时,可能会导致资源无法释放。通过使用std::weak_ptr引用其中一个std::shared_ptr对象,可以打破循环引用关系,防止资源的泄漏。

5. 用于观察者模式:std::weak_ptr还可以用于观察者模式,其中观察者对象通过std::weak_ptr观察被观察者对象。这样可以在被观察者对象被销毁后,观察者对象可以安全地知道被观察者对象已经不存在。

总的来说,weak_ptr是一个弱指针,不是常规智能指针,而是一个辅助性的智能指针(专门辅助shared_ptr解决循环引用),它没有RAII,不支持直接管理资源(即不参与资源的释放),weak_ptr主要用shared_ptr构造,用来解决shared_ptr的循环引用的问题,其核心原理就是:不会增加资源的引用计数。 

2. std::weak_ptr的使用

std::weak_ptr的一般使用场景都是帮助shared_ptr解决循环引用的问题。

那什么是循环引用呢?请看下面的几个例子:

 1. 情况一:
namespace Xq
{
    template<typename T>
	struct Node
	{
		T _val;
		Node* _prev;
		Node* _next;
	};
}

void test_circulate_reference_1(void)
{
	Xq::shared_ptr<Xq::Node<int>> sp1(new Xq::Node<int>);
	Xq::shared_ptr<Xq::Node<int>> sp2(new Xq::Node<int>);
}

当这两个对象生命周期结束后,--引用计数 == 0,正常释放这两份资源,没有问题。 

 2. 情况二:
namespace Xq
{
    template<typename T>
    struct Node
    {
	    int _val;
	    shared_ptr<Node<int>> _prev;
	    shared_ptr<Node<int>> _next;
    };
}
void test_circulate_reference_1(void)
{
	Xq::shared_ptr<Xq::Node<int>> sp1(new Xq::Node<int>);
	Xq::shared_ptr<Xq::Node<int>> sp2(new Xq::Node<int>);
	sp1->_prev = sp2;
}

当sp1和sp2的生命周期结束时,sp2调用析构,sp2管理的这份资源引用计数--,等于1,暂时不会调用析构,然后sp1调用析构,当sp1的_prev调用析构时,此时sp2管理的这份资源引用计数--,等于0,因此sp2的资源被释放,最后sp1的管理资源被释放。 

 3. 情况三:
namespace Xq
{
    template<typename T>
	struct Node
	{
		int _val;
		shared_ptr<Node<int>> _prev;
		shared_ptr<Node<int>> _next;
	};
}
void test_circulate_reference_1(void)
{
	Xq::shared_ptr<Xq::Node<int>> sp1(new Xq::Node<int>);
	Xq::shared_ptr<Xq::Node<int>> sp2(new Xq::Node<int>);
	sp1->_prev = sp2;
	sp2->_next = sp1;
}

此时就会带来问题,循环引用。当sp2和sp1生命周期结束时,需要调用析构,sp2和sp1先后调用析构,引用计数--,但是此时左边的_prev依旧指向右边的资源,其引用计数 == 1,右边的_next依旧指向左边的资源,其引用计数也为1,而此时要想释放左边的资源,先要释放右边的_next,而_next是右边资源成员,也就是要先释放右边的资源。同理,要想释放右边的资源,需要先释放左边的资源。循环往复,最后谁都无法释放。

解决方案:当发生循环引用时,把节点中的_prev和_next由shared_ptr改成weak_ptr就可以了。
原理就是,sp1->_prev = sp2;sp2->_next = sp1;如果此时_prev和_next的类型是weak_ptr,那么不会增加sp1和sp2的引用计数。
namespace Xq
{
    template<typename T>
	struct Node
	{
		int _val;
		std::weak_ptr<Node<int>> _prev;
		std::weak_ptr<Node<int>> _next;
	};
}

void test_circulate_reference_1(void)
{
	std::shared_ptr<Xq::Node<int>> sp1(new Xq::Node<int>);
	std::shared_ptr<Xq::Node<int>> sp2(new Xq::Node<int>);
	sp1->_prev = sp2;
	sp2->_next = sp1;
}

由于此时_prev和_next是一个weak_ptr,而weak_ptr其最大特点就是不会增加资源的引用计数,因此当sp1和sp2发生析构时,引用计数--,等于0,直接释放掉资源,由此解决了循环引用的问题。也就是说,weak_ptr就是为了解决shared_ptr循环引用的问题。

3. weak_ptr的模拟实现
namespace Xq
{
    template<class T>
	class weak_ptr
	{
	public:
		weak_ptr<T>()
			: _ptr(nullptr)
		{}
		weak_ptr<T>(const weak_ptr<T>& copy)
			: _ptr(copy._ptr)
		{}
		weak_ptr<T>(const shared_ptr<T>& copy)
			: _ptr(copy._ptr)
		{}
		weak_ptr<T>& operator=(const weak_ptr<T>& copy)
		{
			_ptr = copy.get();
			return *this;
		}
		weak_ptr<T>& operator=(const shared_ptr<T>& copy)
		{
			_ptr = copy.get();
			return *this;
		}
		T* get()
		{
			return _ptr;
		}
        T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return &(operator*());
		}
		// weak_ptr 不用实现析构函数,因为它不参与管理资源
	private:
		T* _ptr;
	};
}

3.4.5. 定制删除器

在这里,还有一个问题尚未解决。注意,我们上面的空间都是new出来的,释放也都是delete,那如果是new[] 呢,如果此时还是delete,可能导致进程崩溃(内置类型不会崩,自定义类型可能会崩),请看下面的例子:

class A
{
public:
	~A()
	{
		std::cout << "~A()" << std::endl;
	}
};
class B
{
public:
};
void shared_ptr_test1(void)
{
    std::shared_ptr<int> sp1(new int[5]);  // 内置类型	
}

void shared_ptr_test2(void)
{
    std::shared_ptr<B> sp2(new B[5]);      // B类没有实现析构函数
}

void shared_ptr_test3(void)
{
    std::shared_ptr<A> sp3(new A[5]);      // A类实现了析构函数
}

int main()
{
    std::cout << "shared_ptr_test4:> ";
	shared_ptr_test4();
	std::cout << "\n" << "shared_ptr_test5:> ";
	shared_ptr_test5();
	std::cout << "\n" << "shared_ptr_test6:> ";
	shared_ptr_test6();
}

结果:shared_ptr_test1()和shared_ptr_test2()正常结束,但是shared_ptr_test3()进程崩溃

为什么呢?

原因是因为:这些资源都是new[]出来的,但是最后却是由delete释放的,那为什么shared_ptr_test1()和shared_ptr_test2()不崩溃呢?

首先我们要知道,delete和delete[]的区别是什么?

我们之前学过,delete可以分为 析构函数 + operator delete()

delete[] 可以分为  N次析构函数(N由new T[N]决定) + operator delete[](实际中operator delete[] 中调用 operator delete来释放空间)

而 operator delete其底层调的是free();

operator new 其底层调的是 malloc();

为了更好地理解这个问题:

1. 当自定义类型实现了析构函数:

对于new A[5]来说,编译器是知道需要调用5次构造函数 + malloc的,而对于delete来说,也是确定的,一次析构 + 一次free,但是delete[] 不知道要调用多少次析构啊,因为你也没告诉编译器要析构多少次,因此,事实上,编译器在这里有特殊处理:

当你进行new A[5]时,编译器会进行特殊处理,它会多开一个指针,这个指针是一个int*,存储一个整数,这个整数就是new A[5]的这个5,即存储的这个数字代表了调用构造函数的次数也就是delete[]所需要的数字,它通过这个数字确定要调用多少次析构。

当调用5次析构以后,会调用operator delete[],实际在operator delete[]中会调用operator delete,而operator delete其底层调的是free;而free释放资源需要一个正确的起始位置,那么上面的正确的起始位置是哪呢?

如何得到这个位置呢?假设new A[5]的返回值是pos,那么这个位置可以表示为: (char*)pos-4,因此free释放:free((char*)pos-4);

但如果你是delete释放这段 new A[5]的资源,那么最后free的时候,不会处理指针偏移的问题(虽然此时会产生这个int*,但不会指针偏移),因此会free(错误位置),因此它会调用一次析构,但是free的时候,编译器会强制检查释放的位置是否正确,此时发现位置不正确,因此进程crash。这也就是上面我们打印的结果只调了一次析构,进程就崩溃的原因。

2. 当自定义类型没实现析构函数:

如果此时自定义类型没有实现析构函数,那么编译器在new B[5]的时候,不会生成前面这个int* ptr这四个字节,因为没有显式实现析构函数,编译器会忽略到底调用几次析构的问题。

因此,此时编译器会忽略调用几次析构的问题,直接从起始位置free()这段空间,因此不会报错。而对于内置类型同样如此,对于内置类型我们可以认为没有析构函数,直接free()这段空间,最后,这也印证了我们之前打印的结果。对于内置类型和没有实现析构函数的自定义类型,delete释放new[]的空间,不会导致进程崩溃。

虽然某些情况下可能不会致使进程crash,但是这种不规范的方式始终是存在着安全隐患的,其代码的健壮性有待商榷。因此为了更严谨且提高代码的健壮性,我们还是建议采用规范的方式进行处理问题,意思就是说:new的空间,你就用delete释放;new[]的空间,你就用delete[]释放;malloc的空间,你就用free()等等;当我们做到这些,才可能在未来的工作中避免一些本就不应该发生的问题。

回归主题,我们上面实现的shared_ptr对于释放资源已经是写死的了,就是用delete释放其资源。这很有问题。因此人们提出了定制删除器。

定制删除器的原理很简单,其是一个仿函数,通过你申请资源的方式,确定其释放资源的方式。

namespace Xq
{
    // 你是new[]出来的资源,就通过delete[]释放该资源
    template<class D>
	class delete_array
	{
	public:
		void operator()(const D* del)
		{
			delete[] del; 
		}
	};
    // 你是new出来的资源,就通过delete释放资源
	template <class D>
	class delete_one
	{
	public:
		void operator()(const D* del)
		{
			delete del;
		}
	};
	template<class T, class D = delete_one<T>>
	class shared_ptr
	{
	public:
		shared_ptr<T,D>()
			:_ptr(nullptr)
			, _Pcount(nullptr)
		{}

		shared_ptr<T,D>(T* ptr)
			:_ptr(ptr)
			, _Pcount(new int(1))
		{}
		void _delete_func()
		{

			if (_Pcount && --(*_Pcount) == 0)
			{
				std::cout << "delete: " << _ptr << std::endl;
				D()(_ptr); 
				delete _Pcount;
			}
		}
		~shared_ptr<T,D>()
		{
			_delete_func();
		}
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return &(operator*());
		}
		// 重头戏: copy constructor and operator=
		shared_ptr<T,D>(const shared_ptr<T,D>& copy)
			:_ptr(copy._ptr)
			, _Pcount(copy._Pcount)
		{
			++(*_Pcount);
		}
		shared_ptr<T,D>& operator=(const shared_ptr<T,D>& copy)
		{
			if (_ptr != copy._ptr)
			{
				_delete_func();
				_ptr = copy._ptr;
				_Pcount = copy._Pcount;
				++(*_Pcount);
			}
			return *this;
		}
		T* get() const 
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _Pcount;
	};
}
void shared_ptr_test4(void)
{
	Xq::shared_ptr<int,Xq::delete_array<int>> sp1(new int[5]);  // 内置类型	
}

void shared_ptr_test5(void)
{
	Xq::shared_ptr<B, Xq::delete_array<B>> sp2(new B[5]);      // B类没有实现析构函数
}

void shared_ptr_test6(void)
{
	Xq::shared_ptr<A, Xq::delete_array<A>> sp3(new A[5]);      // A类实现了析构函数
}
int main()
{
    std::cout << "shared_ptr_test4:> ";
	shared_ptr_test4();
	std::cout << "\n" << "shared_ptr_test5:> ";
	shared_ptr_test5();
	std::cout << "\n" << "shared_ptr_test6:> ";
	shared_ptr_test6();
}

这样我们就可以通过仿函数确定释放资源的方式,已达到规范处理。

而库里面的实现是通过函数模板处理的,我们在这里不好实现。但我们可以演示一下库里面如何运用得。其实,在这里主要目的是:通过lambda表达式控制释放资源逻辑。

void std_shared_ptr_test1(void)
{
	// 通过仿函数的匿名对象传参
	std::shared_ptr<A> sp1(new A[5], Xq::delete_array<A>());
	// 通过lambda表达是传参
	std::shared_ptr<A> sp2(new A[5], [](A* ptr){delete[] ptr; });
	//  你是malloc申请资源,我就用free释放资源
	std::shared_ptr<A> sp3((A*)malloc(sizeof(A)* 5), [](A* ptr){free(ptr); });
	// 你是fopen申请资源,我就用fclose释放资源
	std::shared_ptr<FILE> sp4((FILE*)fopen("log.txt", "w"), [](FILE* fp){fclose(fp); });
}

4.C++11boost中智能指针的关系

4.1. 拓展了解C++标准库和boost标准库的联系

Boost 库是一个流行且广泛使用的开源C++库集合,提供了许多高质量、跨平台的组件和工具,涵盖了从基本的数据结构和算法到网络编程、并发编程、图形学等众多领域。

与此同时,C++标准库是由C++语言标准所定义的一组标准化的库,它包含了许多用于常见任务的类、函数和算法,比如容器、字符串处理、输入输出操作、并发编程等。

Boost 库和 C++ 标准库之间有许多联系和交互点:
1. 扩展功能:Boost 库提供了大量的扩展功能,填补了 C++ 标准库中的一些空白,或者提供了对某些特定领域的更强大支持。这些功能在 C++ 标准库中可能不存在,或者在某个版本的标准库中才被加入。一些优秀的 Boost 组件后来甚至成为了 C++ 标准库的一部分,例如智能指针(std::shared_ptr, std::unique_ptr)以及正则表达式库(std::regex)等。
2. 实验性特性:Boost 库通常作为实验和原型平台,用于尝试可能在未来被纳入 C++ 标准库的新特性。这些特性经过广泛的使用和测试后,有望成为新的 C++ 标准的一部分。C++11标准中的一些特性就是在 Boost 库中得到验证和推动的,包括Lambda表达式、智能指针(std::unique_ptr)等。
3. 互补功能:Boost 库中的一些组件和C++标准库中的功能类似,但在具体实现上可能提供了不同的接口和特性。这使得开发人员可以根据具体需求选择更适合的工具。例如,在多线程编程方面,Boost.Thread 和 C++ 标准库中的 std::thread 提供了类似的功能,但具体的使用方式和特性可能有所不同。

可以说 Boost 库和 C++ 标准库是紧密相关的,它们共同构成了 C++ 开发中重要的资源和工具集。Boost 库中的许多组件都具有高质量和广泛的实践应用,这些经验反哺到了 C++ 标准库的发展和演进中。同时,C++ 标准库为 Boost 库提供了一个统一的基准和共享的接口规范,使得开发人员更易于迁移和适配代码。

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

C++11的智能指针与Boost库中的智能指针有一定的关联和互操作性。C++11引入了std::shared_ptr和std::unique_ptr作为标准库的一部分,而Boost库也提供了类似的智能指针实现。

1. std::shared_ptr与boost::shared_ptr:

  1. std::shared_ptr是C++11引入的共享所有权智能指针,而boost::shared_ptr是Boost库中的实现。
  2. 二者都采用引用计数的方式来管理资源的生命周期,可以在多个智能指针间共享相同的资源。它们具有相同的使用接口和语义,可以在一定程度上互相替换使用。

2. std::unique_ptr与boost::scoped_ptr:

  1. std::unique_ptr是C++11引入的独占所有权智能指针,而boost::scoped_ptr是Boost库中的实现。
  2. 二者都提供了独占所有权的语义,用于管理独占资源。然而,std::unique_ptr拥有更强大的功能,支持移动语义,可以进行转移和赋值操作,而boost::scoped_ptr则不支持。

3. 互操作性:

  1. C++11的智能指针可以与Boost库中的智能指针进行一定程度的互操作。例如,std::shared_ptr可以与boost::shared_ptr之间进行转换,因为它们使用相同的引用计数实现。类似地,std::unique_ptr也可以与boost::scoped_ptr进行互操作,但需要进行显式的转换。
  2. 但是,需要注意的是,C++11的std::shared_ptr和std::unique_ptr具有更强大和更安全的语义,因此在新的C++项目中,推荐使用C++11标准库中的智能指针,而不是Boost库中的原始实现。

总结来说,C++11的智能指针与Boost库中的相似智能指针有一定的关联和互操作性。C++11的std::shared_ptr和std::unique_ptr提供了更强大和更安全的功能,因此在新的C++项目中推荐使用C++11标准库中的智能指针。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值