【智能指针】智能指针的使用及其原理

智能指针的使用及其原理

RAll

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。具体来说,RAll的核心是:对象构造时获取资源,对象析构的时候释放资源,又由于对象析构会在退出函数栈帧时自动释放,所以我们不需要显式的释放资源
智能指针就是使用这种机制来实现对资源的控制。

智能指针的原理

来看下面一段代码:

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

上述代码就是智能指针的基本原理。构造时传递给SmartPtr一个指向资源的指针,之后我们就不用再显式的释放资源了,因为SmartPtr会自动帮我们释放。通过重载*->,我们可以把SmartPtr对象当指针从而来使用资源,就像指向资源的指针一样。比如下面代码

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

综上总结智能指针的主要基本原理:

  • 具有RAll特性,即是用对象控制资源的生命周期
  • 重载operator*和opertaor->,具有像指针一样的行为,类似迭代器

四种智能智能

C++标准库中给我们提供了一些智能指针类模板且包含在memory头文件中,下面介绍一些常见的智能指针

auto_ptr

C++98其实就已经提供了auto_ptr的智能指针,我们可以通过手册来查看:std::auto_ptr文档

auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份bit::auto_ptr来了解它的原理:

namespace bit
{
	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对象。表面上看似乎没什么问题,可是对于原来的auto_ptr对象来说,它的内置资源指针就悬空了。比如:
在这里插入图片描述
上述代码中,p1将资源交给p2后自己就悬空了,此时再想通过p1去访问资源就会出现报错。

auto_ptr这种设计模式会导致悬空指针,所以比较少见。

unique_ptr

C++11中提供了更完善的智能指针unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_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;
};

出了不能拷贝构造和拷贝赋值,unique_ptr的其余实现和auto_ptr还是一样的。

shared_ptr

前两种智能指针实际上没有很好的解决智能指针之间的拷贝问题,于是C++11又设计了另一种可以支持拷贝且不会造成指针悬空的智能指针,即shared_ptr。简单来说,shared_ptr通过引用计数的方式支持对象之间共享资源。具体地:

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

下面看看具体代码:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr=nullptr)
		:_ptr(ptr)
		,_count(new int(1))
	{
		
	}
	shared_ptr(const shared_ptr<T>& sp)
	{
		_ptr = sp._ptr;
		_count = sp._count;
		*_count++;
	}
	~shared_ptr()
	{
		if (*_count == 1)
		{
			delete _ptr;
			delete _count;
		}
		else {
			*_count--;
		}
	}
	shared_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			if (*_count == 1)
			{
				delete _ptr;
				delete _count;
			}
			else {
				*_count--;
			}
			_ptr = sp._ptr;
			_count = sp._count;
			*_count++;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}

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

private:
	T* _ptr;
	int* _count;

};

我这里是简单实现了一下,其实还可以使用锁保证多线程下线程同步访问计数器。标准库里面的shared_ptr也是使用了锁来保证线程安全的。

shared_ptr的缺陷

尽管 std::shared_ptr 提供了许多优势,它也有一些缺陷和局限性。主要缺陷是循环引用造成内存泄漏
如果两个或多个 std::shared_ptr 之间形成循环引用(即,A 引用 B,B 又引用 A),则引用计数永远不会变为 0,从而导致内存泄漏。
观察下面代码:

#include <iostream>
#include <memory>

class A;
class B;

class A {
public:
    std::shared_ptr<B> b;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a;
    ~B() { std::cout << "B destroyed" << std::endl; }
};

void example() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b = b;
    b->a = a; // 形成循环引用
    
    // A 和 B 对象不会被销毁,因为它们互相引用
}
int main()
{
	example();
	return 0;
}

上面代码运行结果是什么都没有打印,也就意味着a和b都没有析构。为什么呢?
分析:

  1. 对象创建,创建 std::shared_ptr 对象 a 和 std::shared_ptr 对象 b。a 持有 B 的共享指针,b 持有 A 的共享指针
  2. 设置引用:a 的 b_ptr 指向 b。b 的 a_ptr 指向 a。此时a的引用计数为2,b的引用计数也是2。
  3. 析构a对象,引用计数减一,但是不为0,于是不释放资源。析构b对象也是如此。所以出现了内存泄漏。

wead_ptr

为了专门解决shared_ptr的循环引用问题,C++11又提供了一种weak_ptr的智能指针。用于观察由shared_ptr 管理的对象,而不影响对象的引用计数
weak_ptr的构造可以传入一个shared_ptr对象
在这里插入图片描述
再来看一段代码:

#include <iostream>
#include <memory>

class A;

class B {
public:
    std::weak_ptr<A> a_ptr;  // 使用 weak_ptr 观察 A
    ~B() { std::cout << "B destroyed" << std::endl; }
};

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};

void example() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    
    a->b_ptr = b;  // A 持有对 B 的共享指针
    b->a_ptr = a;  // B 观察 A,使用 weak_ptr

    // 当 a 和 b 超出作用域时,它们的引用计数会归零
}

int main() {
    example(); // A 和 B 对象会被正确销毁
    return 0;
}

上面代码中的a、b对象出作用域后可以正确销毁。这是因为b->a_ptr = a 表示 B 持有 A 的一个 std::weak_ptr,这个 std::weak_ptr 不增加 A 的引用计数。所以a的引用计数还是1,也就能正确销毁。

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++智能指针是一种用于管理动态分配的内存资源的工具。C++中提供了多种类型的智能指针,其中包括shared_ptr、unique_ptr和weak_ptr。这些智能指针都位于头文件<memory>中。 其中,shared_ptr是一种引用计数智能指针,它允许多个智能指针共享同一个对象。shared_ptr通过对对象的引用计数来管理内存的释放,当引用计数为0时,即没有智能指针指向该对象时,对象会被自动释放。使用shared_ptr需要包含头文件<memory>,并通过new关键字创建动态分配的对象并将其交给shared_ptr进行管理。 另一种智能指针是unique_ptr,它是一种独占型智能指针,只能有一个智能指针拥有对对象的所有权。当unique_ptr对象被销毁时,它所管理的对象也会被自动释放。unique_ptr提供了更高效的内存管理方式,因为它不需要进行引用计数。使用unique_ptr也需要包含头文件<memory>,并使用new关键字创建动态分配的对象并将其交给unique_ptr进行管理。 除了shared_ptr和unique_ptr,还有其他类型的智能指针,如weak_ptr和scoped_ptr。weak_ptr是一种弱引用智能指针,它用于解决shared_ptr可能出现的循环引用问题。scoped_ptr是一种简单的智能指针,它只能在创建时初始化,并且不能进行复制和赋值操作。scoped_ptr在其所属的作用域结束时自动释放所管理的对象。 总结起来,C++智能指针是一种用于管理动态分配的内存资源的工具,包括shared_ptr、unique_ptr、weak_ptr和scoped_ptr等类型。它们提供了一种更安全、更高效的内存管理方式,避免了手动释放内存的麻烦。要使用这些智能指针,需要包含头文件<memory>,并通过new关键字创建动态分配的对象并将其交给相应的智能指针进行管理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [C++ -- 智能指针C++11与boost库的智能指针及其使用)](https://blog.csdn.net/xu1105775448/article/details/80625936)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [C++智能指针的底层实现原理](https://blog.csdn.net/ArtAndLife/article/details/120793343)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值