<C++11> 智能指针

news2025/2/27 14:15:18

目录

前言

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

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

1. 智能指针介绍

2. 智能指针的使用

3. 智能指针的原理

二、C++中的智能指针

1. auto_ptr

2. unique_ptr

3. shared_ptr 

std::shared_ptr的基本设计

shared_ptr的线程安全问题

定制删除器

4. weak_ptr

shared_ptr的循环引用问题

weak_ptr解决循环引用问题


前言

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

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

说明一下:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方。

Boost库是一个广泛的、同行评审的、免费的C++库集合,它提供了许多在标准C++库中没有的工具和实用程序。 Boost库的质量很高,许多Boost库的组件最终都被纳入到标准C++库中(例如,部分Boost库的组件已经包含在C++11, C++14, C++17, C++20等标准中)。
 


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

1. 智能指针介绍

智能指针是C++中一种重要的内存管理工具,它能够自动管理动态分配对象的内存,有效地防止内存泄漏和悬空指针的问题。 智能指针并非直接管理内存,而是通过RAII (Resource Acquisition Is Initialization) 机制,将对象的生存期与智能指针的生存期绑定在一起。 当智能指针对象超出作用域时,它会自动释放所管理的对象的内存。

C++标准库主要提供了三种智能指针:unique_ptrshared_ptr 和 weak_ptr。 它们各有特点,适用于不同的场景。

2. 智能指针的使用

内存泄露问题

内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。比如:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	//...
	cout << div() << endl;
	//...
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

执行上述代码时,如果用户输入的除数为0,那么div函数中就会抛出异常,这时程序的执行流会直接跳转到主函数中的catch块中执行,最终导致func函数中申请的内存资源没有得到释放。

利用异常的重新捕获解决

对于这种情况,我们可以在func函数中先对div函数中抛出的异常进行捕获,捕获后先将之前申请的内存资源释放,然后再将异常重新抛出。比如:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void func()
{
	int* ptr = new int;
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete ptr;
		throw;
	}
	delete ptr;
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

情景:因为每个函数都可能抛出异常,异常多了之后,new出来的空间就非常难管理,代码也十分丑陋,所以这是就引入了指针指针,来解决new出的空间释放问题。

智能指针解决

将指针交给一个类的对象,对象出了作用域自动调用析构函数,所以不用担心内存泄露,这就是智能指针,十分简单。

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;
	}
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> sp(new int);
	//...
	cout << div() << endl;
	//...
}
int main()
{
	try
	{
		func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。

  • 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
  • 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
  • 此外,为了让SmartPtr对象能够像原生指针一样使用(类似迭代器的实现),还需要对*和->运算符进行重载。

这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放

3. 智能指针的原理

智能指针的原理

实现智能指针时需要考虑以下三个方面的问题:

  1. 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
  2. 对 * 和 -> 运算符进行重载,使得该对象具有像指针一样的行为。
  3. 智能指针对象的拷贝问题。

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

为什么要解决智能指针对象的拷贝问题

对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃。比如:

int main()
{
    SmartPtr<int> sp1(new int);
    SmartPtr<int> sp2(sp1); //拷贝构造

    SmartPtr<int> sp3(new int);
    SmartPtr<int> sp4(new int);
    sp3 = sp4; //拷贝赋值
    
    return 0;
}

原因如下:

  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
  • 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。

需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。

二、C++中的智能指针

1. auto_ptr

管理权转移

auto_ptr是C++98中引入的智能指针,auto_ptr通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了。比如:

int main()
{
	std::auto_ptr<int> ap1(new int(1));
	std::auto_ptr<int> ap2(ap1);
	*ap2 = 10;
	//*ap1 = 20; //error

	std::auto_ptr<int> ap3(new int(1));
	std::auto_ptr<int> ap4(new int(2));
	ap3 = ap4;
	return 0;
}

但一个对象的管理权转移后也就意味着,该对象不能再用对原来管理的资源进行访问了,否则程序就会崩溃,因此使用auto_ptr之前必须先了解它的机制,否则程序很容易出问题,很多公司也都明确规定了禁止使用auto_ptr。 

标准库

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << " ~A()  " << this << endl;
	}
public:
	int _a;
};

 

auto_ptr 模拟实现

四个关键点:

  • RAII(构造函数获取资源、析构函数释放资源,利用对象的生命周期来控制资源)
  • 像指针一样使用(重载operator*、operator->,使auto_ptr对象具有指针一样的行为,类似迭代器) 
  • 拷贝构造函数中,将传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
  • 赋值运算符重载中,现将自身资源释放,然后再对传入对象管理资源的指针拷贝到自己的ptr,最后将传入对象管理资源的指针置空。
	// auto_ptr
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete " << _ptr << endl;
				delete _ptr;
			}
		}

		// 拷贝构造
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

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

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

2. unique_ptr

unique_ptr (独占指针):

  • 特性: unique_ptr 指向一个对象,并且在任意时刻只有一个 unique_ptr 指向该对象。 当 unique_ptr 超出作用域或被销毁时,它所指向的对象也会被自动销毁。 unique_ptr 不能被复制,只能被移动。 这保证了对象的所有权的唯一性。
  • 使用场景: 当一个对象只有一个拥有者时,unique_ptr 是最佳选择。 它能清晰地表达所有权,并避免了多重释放的问题。

防拷贝

unique_ptr是C++11中引入的智能指针,unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,这样也能保证资源不会被多次释放。

但防拷贝其实也不是一个很好的办法,因为总有一些场景需要进行拷贝。

标准库

unique_ptr 模拟实现

三个关键点: 

  • RAII(构造函数获取资源、析构函数释放资源,利用对象的生命周期来控制资源)
  • 像指针一样使用(重载operator*、operator->,使auto_ptr对象具有指针一样的行为,类似迭代器) 
  • 拷贝构造、赋值运算符重载都禁止使用,防止拷贝。可以用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,也可以用C++11的方式在这两个函数后面加上 =delete,防止外部调用。
// unique_ptr
template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~unique_ptr()
	{
		cout << "delete " << _ptr << endl;
		delete _ptr;
	}

	// 拷贝构造
	unique_ptr(unique_ptr<T>& up) = delete;
	// 赋值运算符重载
	unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

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

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

3. shared_ptr 

shared_ptr (共享指针):

  • 特性: 多个 shared_ptr 可以同时指向同一个对象。 shared_ptr 内部维护了一个引用计数器,跟踪有多少个 shared_ptr 指向同一个对象。 当最后一个 shared_ptr 超出作用域或被销毁时,对象会被自动销毁。 shared_ptr 可以被复制,这使得共享所有权成为可能。
  • 使用场景: 当多个对象需要共享同一个资源时,shared_ptr 是一个很好的选择。 它能够自动管理引用计数,避免了手动管理内存的复杂性和错误。
std::shared_ptr的基本设计

引用计数

shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。

  • 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
  • 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
  • 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。

通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:

int main()
{
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl; //2

	shared_ptr<int> sp3(new int(1));
	shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl; //2
	return 0;
}

说明一下: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。

标准库

模拟实现

几个关键点: 

  • RAII:构造函数获取资源、析构函数释放资源,利用对象的生命周期来控制资源。
  • 在share_ptr类中增加一个int*类型的成员变量count,用来引用计数。
  • 像指针一样使用,重载operator*、operator->,使auto_ptr对象具有指针一样的行为,类似迭代器。
  • 拷贝构造函数中,与传入对象一起管理它的资源,同时将该资源的引用计数++。
  • 赋值运算符重载中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。在函数最开始需要先判断是否是对象自身赋值给自身,如果该对象的引用计数为1,那么这种情况可能会导致该对象资源被释放。
  • 析构函数中先对 --(*count),判断是否为0,不为0则什么也不做,如果为0则 delete 掉 ptr。
// shared_ptr
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete " << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

	// 拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		++(*_pcount);
	}

	// 赋值运算符重载
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		// 也可以判断pcount,这两个成员都是指针,可以比较对象是否“相等”
		if (_ptr == sp._ptr)
			return *this;
			
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		++(*_pcount);
			
		return *this;
	}

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

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

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
	//static size_t _count;	// 静态的变量(错误),因为所有实例化的对象都共享一个_count,即使所掌握的_ptr资源不同!!!
};

// 静态变量
//template<class T>
//size_t shared_ptr<T>::_count = 0;

细节: 

  • 当拷贝构造时,将被拷贝的对象的ptr、pcount资源都浅拷贝一份,再将 ++(*pcount)即可。
  • 赋值运算符重载有几点细节:首先不能自己赋值给自己,因为会导致自己的资源被释放(自己资源唯一的话,引用计数为1),并且如果两个智能指针的 ptr 指向相同(这表明ptr的引用计数至少为 2,不用担心 delete,所以即使不判断也没有问题,因为 pcount -- 之后又 ++ 了),这也算自己赋值给自己,如果不做这个判断,代码也是没错的,所以我们可以在判断时判断成员 ptr 或者 pcount 资源地址,从而避免这个冗余操作。
  • 其次在赋值之前,要判断自己的资源是否要释放

为什么引用计数需要存放在堆区?

首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象用到的不是同一个引用计数。

其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。

既然普通成员变量、静态成员变量都行不通,那么我们就动态开辟空间,每一个对象在构造函数的初始化列表处都 new 一个 pcount ,作为各自资源的引用计数。

将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间(new int(1) )用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。

这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。

但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。

operator= 测试

赋值前

赋值后 

shared_ptr的线程安全问题

shared_ptr的线程安全问题

当前模拟实现的shared_ptr还存在线程安全的问题,由于管理同一个资源的多个对象的引用计数是共享的,因此多个线程可能会同时对同一个引用计数进行自增或自减操作,而自增和自减操作都不是原子操作,因此需要通过加锁来对引用计数进行保护,否则就会导致线程安全问题,即多线程并发访问导致数据不一致问题,解决方案就是引入互斥量。

比如下面代码中用一个shared_ptr管理一个整型变量,然后用两个线程分别对这个shared_ptr对象进行1000次拷贝操作,这些对象被拷贝出来后又会立即被销毁。比如:

void func(cl::shared_ptr<int>& sp, size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cl::shared_ptr<int> copy(sp);
	}
}
int main()
{
	cl::shared_ptr<int> p(new int(0));

	const size_t n = 1000;
	thread t1(func, p, n);
	thread t2(func, p, n);

	t1.join();
	t2.join();

	cout << p.use_count() << endl; //预期:1

	return 0;
}

在这个过程中两个线程会不断对引用计数进行自增和自减操作,理论上最终两个线程执行完毕后引用计数的值应该是1,因为拷贝出来的对象都被销毁了,只剩下最初的shared_ptr对象还在管理这个整型变量,但每次运行程序得到引用计数的值可能都是不一样的,根本原因就是因为对引用计数的自增和自减不是原子操作。

加锁解决线程安全问题

要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例。

  • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建。
  • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
  • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
  • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成ReleaseRef函数,这样就只需要对AddRef和ReleaseRef函数进行加锁保护即可。
// 线程安全版shared_ptr
template<class T>
class shared_ptr
{
private:
	// ++引用计数
	void AddRef()
	{
		_pmutex->lock();
		++(*_pcount);
		_pmutex->unlock();
	}

	// --引用计数
	void ReleaseRef()
	{
		_pmutex->lock();
		bool flag = false;
		if (--(*_pcount) == 0)
		{
			cout << "delete " << _ptr << endl;
			delete _ptr;
			delete _pcount;

			flag = true;
		}
		_pmutex->unlock();
		if (flag) 
			delete _pmutex;
	}

public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		,_pmutex(new mutex)
	{}

	~shared_ptr()
	{
		ReleaseRef();
	}

	// 拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		,_pmutex(sp._pmutex)
	{
		AddRef();
	}

	// 赋值运算符重载
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		// 也可以判断pcount,这两个成员都是指针,可以比较对象是否“相等”
		if (_ptr == sp._ptr)
			return *this;

		ReleaseRef();
		_ptr = sp._ptr;
		_pcount = sp._pcount;
		_pmutex = sp._pmutex;
		AddRef();

		return *this;
	}

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

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

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmutex;
};

 说明一下:

  • 在ReleaseRef函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源。
  • shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证。
定制删除器

智能指针的析构函数内默认就是delete。

但是对于有些情况,不能统一使用delete:

  • 如果 ptr 接收的是 new [10],那么析构函数内就需要使用 delete [] 来释放资源。
  • 如果 ptr 接收的是 malloc的,那么析构函数内就需要使用 free 释放堆空间。
  • 如果 ptr 接收的是 fopen 返回值,那么析构函数内就需要使用 fclose 关闭文件。

所以C++对此提供了重载函数,额外加了一个模板参数,用来接收用户传递的定制删除器,也就是三种可调用对象,函数指针、仿函数、lambda表达式。

// 定制删除器
template<class T>
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	// 定制删除器
	std::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	std::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	std::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* fp) {fclose(fp); });

	return 0;
}

注意:在memory库中,只有shared_ptr、unique_ptr实现了这个定制删除器功能,但是unique_ptr是将模板放在了类模板处,所以在使用时应该传类型,例如 unique_ptr<A, DeleteArray> up(new A[10]),如果是在构造函数需要传对象,注意区分。

模拟实现

查看手册可以发现,shared_ptr是在构造函数重载了带有定制删除器的模板参数,但是问题是我们需要在析构函数使用该模板参数,因为只有类的模板参数才可以使3所有成员函数共享,此时只有构造函数有D模板,我们该如何解决?

在类的成员变量中用包装器声明一个变量_del,并给一个默认值,

// shared_ptr
template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	// 定制删除器的构造函数
	template<class D>
	shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		,_del(del)
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete " << _ptr << endl;
			//delete _ptr;
			_del(_ptr);
			delete _pcount;
		}
	}

	// 拷贝构造
	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		++(*_pcount);
	}

	// 赋值运算符重载
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		// 也可以判断pcount,这两个成员都是指针,可以比较对象是否“相等”
		if (_ptr == sp._ptr)
			return *this;

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

		return *this;
	}

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

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

	int use_count() const
	{
		return *_pcount;
	}

	T* get() const
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* _pcount;
	function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
};

// 定制删除器
template<class T>
class DeleteArray
{
public:
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};

int main()
{
	// 定制删除器
	my_ptr::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	my_ptr::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	my_ptr::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* fp) {fclose(fp); });

	return 0;
}

 

4. weak_ptr

shared_ptr的循环引用问题

循环引用问题

shared_ptr已经很完美了,但是却隐含了一个bug:shared_ptr的循环引用问题。

在一些特定的场景下会产生该问题。比如定义如下的结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << " ~A()  " << this << endl;
	}
public:
	int _a;
};

class Node
{
public:
	Node()
		:_val(A())
		, _prev(nullptr)
		, _next(nullptr)
	{}
public:
	A _val;

	// 1.0 编译不通过,因为赋值时类型不一致
	//Node* _prev;
	//Node* _next;
	
	// 2.0 使用相同类型
	my_ptr::shared_ptr<Node> _prev;
	my_ptr::shared_ptr<Node> _next;
};

现在以new的方式在堆上构建两个结点,并将这两个结点连接起来

int main()
{
	// 循环引用
	my_ptr::shared_ptr<Node> sp1(new Node);
	my_ptr::shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

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

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

    return 0;
}

这时程序运行结束后两个结点都没有被释放,但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用。

当以new的方式申请到两个Node结点并交给两个智能指针管理后,这两个资源对应的引用计数都被加到了1。如下图:

将这两个结点连接起来后,资源1当中的next成员与node2一同管理资源2,资源2中的prev成员与node1一同管理资源1,此时这两个资源对应的引用计数都被加到了2。如下图:

当出了main函数的作用域后,node1和node2的生命周期也就结束了,因此这两个资源对应的引用计数最终都减到了1。如下图:

循环引用导致资源未被释放的原因:

  • 当资源对应的引用计数减为0时对应的资源才会被释放,因此资源1的释放取决于资源2当中的prev成员,而资源2的释放取决于资源1当中的next成员。
  • 而资源1当中的next成员的释放又取决于资源1,资源2当中的prev成员的释放又取决于资源2,于是这就变成了一个死锁状态,最终导致资源无法释放。

而如果连接结点时只进行一个连接操作,那么当node1和node2的生命周期结束时,就会有一个资源对应的引用计数被减为0,此时这个资源就会被释放,这个释放后另一个资源的引用计数也会被减为0,最终两个资源就都被释放了,这就是为什么只进行一个连接操作时这两个结点就都能够正确释放的原因。

weak_ptr解决循环引用问题

解决循环引用问题

weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。

  • weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数
     

weak_ptr不是RAII的智能指针(因为他不能用指针进行初始化,看一看接口),它是专门用来解决shared_ptr的循环引用问题 .

原理:不增加引用计数,不参与资源释放管理,但是可以访问资源。

所以将Node中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。

class Node
{
public:
	A _val;

	// 1.0 编译不通过,因为赋值时类型不一致
	//Node* _prev;
	//Node* _next;

	// 2.0 使用相同类型
	/*my_ptr::shared_ptr<Node> _prev;
	my_ptr::shared_ptr<Node> _next;*/

	// 3.0 采用不会增加引用计数的weak_ptr
	my_ptr::weak_ptr<Node> _prev;
	my_ptr::weak_ptr<Node> _next;
};

int main()
{
	// 循环引用
	my_ptr::shared_ptr<Node> sp1(new Node);
	my_ptr::shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

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

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

    return 0;
}

通过use_count获取这两个资源对应的引用计数就会发现,在结点连接前后这两个资源对应的引用计数就是1,根本原因就是weak_ptr不会增加管理的资源对应的引用计数。

模拟实现weak_ptr

几个关键点:

  • 提供一个无参的构造函数,比如刚才new Node时就会调用weak_ptr的无参的构造函数。
  • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
  • 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
  • 对*和->运算符进行重载,使weak_ptr对象具有指针一样的行为。

 代码:

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

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}

	~A()
	{
		cout << " ~A()  " << this << endl;
	}
public:
	int _a;
};

class Node
{
public:
	A _val;

	// 1.0 编译不通过,因为赋值时类型不一致
	//Node* _prev;
	//Node* _next;
	
	// 2.0 使用相同类型
	/*my_ptr::shared_ptr<Node> _prev;
	my_ptr::shared_ptr<Node> _next;*/

	// 3.0 采用不会增加引用计数的weak_ptr
	my_ptr::weak_ptr<Node> _prev;
	my_ptr::weak_ptr<Node> _next;
};

int main()
{
	// 循环引用
	my_ptr::shared_ptr<Node> sp1(new Node);
	my_ptr::shared_ptr<Node> sp2(new Node);

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

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

	cout << sp1.use_count() << endl;
	cout << sp2.use_count() << endl;

	return 0;
}

 

此时观察输出可以发现,即使相互引用,shared_ptr 的引用计数也不增加,最终出了作用域直接调用析构函数,析构Node,Node的析构函数默认生成,对自定义类型默认调用他们的析构函数,所以调用了val(A类型)的析构函数、_prev、_next(weak_ptr的析构函数)

四个智能指针代码+shared_ptr的多线程安全版

#pragma once
#include <iostream>
#include <mutex>
#include <functional>
using namespace std;

namespace my_ptr
{
	// auto_ptr
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete " << _ptr << endl;
				delete _ptr;
			}
		}

		// 拷贝构造
		auto_ptr(auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

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

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

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

		~unique_ptr()
		{
			cout << "delete " << _ptr << endl;
			delete _ptr;
		}

		// 拷贝构造
		unique_ptr(unique_ptr<T>& up) = delete;
		// 赋值运算符重载
		unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;

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

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

	// shared_ptr
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))
		{}

		// 定制删除器的构造函数
		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			,_del(del)
		{}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				cout << "delete " << _ptr << endl;
				//delete _ptr;
				_del(_ptr);
				delete _pcount;
			}
		}

		// 拷贝构造
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);
		}

		// 赋值运算符重载
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			// 也可以判断pcount,这两个成员都是指针,可以比较对象是否“相等”
			if (_ptr == sp._ptr)
				return *this;

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

			return *this;
		}

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

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

		int use_count() const
		{
			return *_pcount;
		}

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
		function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
		//static size_t _count;	// 静态的变量(错误),因为所有实例化的对象都共享一个_count,即使所掌握的_ptr资源不同!!!
	};
	// 静态变量
	//template<class T>
	//size_t shared_ptr<T>::_count = 0;

	// 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
	//template<class T>
	//class shared_ptr
	//{
	//private:
	//	// ++引用计数
	//	void AddRef()
	//	{
	//		_pmutex->lock();
	//		++(*_pcount);
	//		_pmutex->unlock();
	//	}

	//	// --引用计数
	//	void ReleaseRef()
	//	{
	//		_pmutex->lock();
	//		bool flag = false;
	//		if (--(*_pcount) == 0)
	//		{
	//			cout << "delete " << _ptr << endl;
	//			//delete _ptr;
	//			_del(_ptr);
	//			delete _pcount;

	//			flag = true;
	//		}
	//		_pmutex->unlock();
	//		if (flag) 
	//			delete _pmutex;
	//	}

	//public:
	//	shared_ptr(T* ptr)
	//		:_ptr(ptr)
	//		,_pcount(new int(1))
	//		,_pmutex(new mutex)
	//	{}

	//	// 定制删除器的构造函数
	//	template<class D>
	//	shared_ptr(T* ptr, D del)
	//		:_ptr(ptr)
	//		,_pcount(new int(1))
	//		,_pmutex(new mutex)
	//		,_del(del)
	//	{}

	//	~shared_ptr()
	//	{
	//		ReleaseRef();
	//	}

	//	// 拷贝构造
	//	shared_ptr(const shared_ptr<T>& sp)
	//		:_ptr(sp._ptr)
	//		, _pcount(sp._pcount)
	//		,_pmutex(sp._pmutex)
	//	{
	//		AddRef();
	//	}

	//	// 赋值运算符重载
	//	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	//	{
	//		// 也可以判断pcount,这两个成员都是指针,可以比较对象是否“相等”
	//		if (_ptr == sp._ptr)
	//			return *this;

	//		ReleaseRef();
	//		_ptr = sp._ptr;
	//		_pcount = sp._pcount;
	//		_pmutex = sp._pmutex;
	//		AddRef();

	//		return *this;
	//	}

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

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

	//	int use_count() const
	//	{
	//		return *_pcount;
	//	}

	//	T* get() const
	//	{
	//		return _ptr;
	//	}
	//private:
	//	T* _ptr;
	//	int* _pcount;
	//	mutex* _pmutex;
	//	function<void(T* ptr)> _del = [](T* ptr) {delete ptr; };
	//};
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2258023.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

书生实战营第四期-进阶岛第六关-MindSearch 快速部署

一、开发环境配置 1、打开codespace主页&#xff0c;选择Blank模板进行创建 Codespaces 2、创建conda环境隔离并安装依赖 conda create -n mindsearch python3.10 -y conda init 因为是新建的codespace&#xff0c;在第一次创建conda环境时&#xff0c;需要conda init 然后再…

ViT学习笔记(二) Patch+Position Embedding阶段的详细推演与理解

我认为讲得最好的一个文章&#xff1a;Vision Transformer详解-CSDN博客 有很多文章&#xff0c;自己并没有完全正确理解。 我的笔记&#xff0c;以ViT的标准应用为例&#xff1a; • 输入图像&#xff1a;输入图像的尺寸是224x224&#xff0c;且是RGB图像&#xff0c;因此输…

JS听到了强运的回响

正则表达式 介绍 正则表达式是用于匹配字符串中字符组合的模式&#xff0c;在JS中&#xff0c;正则表达式也是对象 通常用来查找&#xff0c;替换那些符合正则表达式的文本 就是筛选出符合条件的一类人 比如说 有人喜欢玩艾斯爱慕&#xff0c;那他喜欢的就是这一类人&…

工业4.0下的IT网络与OT网络

https://zhuanlan.zhihu.com/p/498984722 随着“中国制造2025”的深入推进&#xff0c;制药行业以手工为主的传统生产方式正在被以“工业4.0 ”为核心的自动化生产方式逐步替代。 为了实现生产自动化&#xff0c;很多制药企业都引入了由PLC&#xff08;可编程逻辑控制器 &am…

C# MVVM 牛牛的实现依赖注入和MVVM绑定(DependencyInjection+CommunityToolkit)

这段时间在网上发现搜索MVVM数据绑定时&#xff0c;发现很多都是最基本的数据绑定&#xff0c;完全没有考虑依赖注入的问题&#xff0c;这里实现一下我们的方法&#xff0c;让我们的数据绑定和依赖注入都变得简单起来。 安装资源包 首先我们要下载一下资源包DependencyInject…

gitee常见命令

目录 1.本地分支重命名 2.更新远程仓库分支 3.为当前分支设置远程跟踪分支 4.撤销已经push远程的代码 5.idea->gitee的‘还原提交’ 需要和本地当前的代码解决冲突 解决冲突 本地工作区的差异代码显示 本地commit和push远程 6.idea->gitee的‘将当前分支重置到此…

【JAVA高级篇教学】第二篇:使用 Redisson 实现高效限流机制

在高并发系统中&#xff0c;限流是一项非常重要的技术手段&#xff0c;用于保护后端服务&#xff0c;防止因流量过大导致系统崩溃。本文将详细介绍如何使用 Redisson 提供的 RRateLimiter 实现分布式限流&#xff0c;以及其原理、使用场景和完整代码示例。 目录 一、什么是限流…

Python画泰勒图

1. 安装画泰勒图的库 pip install SkillMetricsSkillMetrics库在图的设置细节&#xff08;模型标记符号、colorbar&#xff09;有很多不足&#xff0c;比如无法按颜色区分每个散点。 注意&#xff01;&#xff01;&#xff01; 提前算好数据的标准差、相关系数和中心化均方根…

可视化建模以及UML期末复习篇----UML图

这是一篇相对较长的文章&#xff0c;如你们所见&#xff0c;比较详细&#xff0c;全长两万字。我不建议你们一次性看完&#xff0c;直接跳目录找你需要的知识点即可。 --------欢迎各位来到我UML国&#xff01; 一、UML图 总共有如下几种&#xff1a; 用例图&#xff08;Use Ca…

【MySQL】——​​用一文领悟表的增删查改

目录 前言 &#x1f343;1.表的增加 &#x1f359;1.1增——insert &#x1f359;1.2插入否则更新 &#x1f364;1.2.1影响行说明 &#x1f342;2.表的查询 &#x1f358;2.1查询——select &#x1f358;2.2特殊表查询 &#x1f365;2.2.1添加表达式 &#x1f365;…

福昕PDF低代码平台

福昕PDF低代码平台简介 福昕PDF 低代码平台是一款创新的工具&#xff0c;旨在简化PDF处理和管理的流程。通过这个平台&#xff0c;用户可以通过简单的拖拽界面上的按钮&#xff0c;轻松完成对Cloud API的调用工作流&#xff0c;而无需编写复杂的代码。这使得即使没有编程经验的…

【数据结构实战】二叉树——从根节点到叶节点

&#x1f3dd;️专栏&#xff1a; 【数据结构实战篇】 &#x1f305;主页&#xff1a; f狐o狸x 目录 一、堆的应用 1.1 堆排序 1.2 topk问题 二、二叉树 2.1 二叉树链式结构的实现 2.2 二叉树的遍历 2.2.1 前序、中序以及后序遍历 2.2.2 层序遍历 2.3 节点个数以及高度等 2.3.…

命令模式的理解和实践

在软件开发中&#xff0c;设计模式是开发者们经过长期实践总结出来的、可复用的解决方案&#xff0c;用于解决常见的设计问题。命令模式&#xff08;Command Pattern&#xff09;是行为型设计模式之一&#xff0c;它通过将一个请求封装成一个对象&#xff0c;从而允许用户用不同…

vue-router查漏补缺

一、动态路由匹配 1.带参数的动态路由匹配 import User from ./User.vue// 这些都会传递给 createRouter const routes [// 动态字段以冒号开始{ path: /users/:efg, component: User }, ]这种方式的路由会匹配到/users/abc或者/users/123,路径参数用冒号:表示&#xff0c;并…

Ubuntu22.04深度学习环境安装【Anaconda+Pycharm】

anaconda可以提供多个独立的虚拟环境&#xff0c;方便我们学习深度学习&#xff08;比如复现论文&#xff09;&#xff1b; Pycharm编辑器可以高效的编写python代码&#xff0c;也是一个很不错的工具。 下面就记录下Ubuntu22.04的安装流程&#xff1a; 1.Anaconda安装 下载Ana…

三维测量与建模笔记 - 6.2 结构光三维成像简介

简介 双目视觉系统中&#xff0c;找到左右图像中的同名点是很困难的。 即便是经过了极线校正&#xff0c;也存在很多问题比如高光等造成无匹配。对于结构光方案来说&#xff0c;找到同名点的过程会相对简单些。 结构光方案中&#xff0c;会增加一个投射器&#xff0c;将编码后的…

this version of the Java Runtime only recognizes class file versions up to 52.0

问题描述 Exception in thread "main" java.lang.UnsupportedClassVersionError: com/xxx/Main has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versi…

【开源免费】基于SpringBoot+Vue.JS在线办公系统(JAVA毕业设计)

本文项目编号 T 001 &#xff0c;文末自助获取源码 \color{red}{T001&#xff0c;文末自助获取源码} T001&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

【WRF理论第十三期】详细介绍 Registry 的作用、结构和内容

目录 1. Introduction&#xff1a;介绍 Registry 的作用和功能。2. Registry Contents&#xff1a;详细描述 Registry 的结构和内容&#xff0c;包括各个部分的条目类型。2.1. DIMSPEC ENTRIES&#xff08;维度规格条目&#xff09;2.2. STATE ENTRIES&#xff08;状态变量条目…

Redis篇-4--原理篇3--Redis发布/订阅(Pub/Sub)

1、概述 Redis 发布/订阅&#xff08;Publish/Subscribe&#xff0c;简称 Pub/Sub&#xff09;是一种消息传递模式&#xff0c;允许客户端订阅一个或多个通道&#xff08;channel&#xff09;&#xff0c;并接收其他客户端发布到这些通道的消息。 2、Redis 发布/订阅的主要概…