C++ 智能指针的魅力你都了解吗

前情提要

我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。

在C++中,动态内存的管理是用一对运算符完成的:new和delete,ne:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete指向一个动态独享的指针,销毁对象,并释放与之关联的内存。

动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。

为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象

智能指针的原理

RAII:利用对象生命周期来控制程序资源。主要是通过对象的构造函数来获得资源的管理权,然后再通过析构函数来释放所管理的资源。其原理就是把管理一份资源的责任托管给了一个对象

//RAII
template<class T>
class SmartPtr
{
public:
	//构造函数获取资源管理权
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	//通过析构函数释放资源
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
class A
{
private:
	int _a = 10;
};

void test()
{
	//错误写法
	int* ptr = new int[10];
	SmartPtr<int> sp(ptr);
	//立即初始化--申请资源时就立即绑定资源
	SmartPtr<int> sp2(new int);
	SmartPtr<A> sp3(new A);
}

这并不是一个智能指针对象,智能指针应该要满足以下条件

  • 实现RAII思想
  • 使用方式和指针一样,例如需要支持*解引用和->操作

应在类中添加以下操作的重载

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

智能指针和普通的指针的区别之一是智能指针不需要手动释放空间

void test()
{
	//智能指针--编译器调用析构自动释放资源--不存在内存泄漏
	SmartPtr<A> sp(new A);
	(*sp)._a = 10;
	sp->_a = 100;

	//普通指针--手动释放内存
	int* p = new int;
	A* pa = new A;
	*p = 1;
	pa->_a = 10;
	//return  //提前结束普通指针就会导致内存泄漏
	delete p;
	delete pa;
}

C++标准库中的智能指针的使用

库中的智能指针分为 auto_ptrunique_ptrshare_ptr
他们都需要引入头文件#include <memory>才能使用

auto_ptr

auto_ptr是一种存在缺陷的智能指针(禁用

#include <memory>
using namespace std;
void test()
{
	auto_ptr<int> ap(new int);
	auto_ptr<int> ap2(new int(2));
	*ap = 10;
	*ap2 = 20;
}

auto_ptr指针进行赋值操作时会将资源进行转移,目的是为了防止多个智能指针指向同一块内存资源。但是这种设计显然不符合我们的需求

我们来简单模拟实现auto_ptr,看他底层是如何进行资源权的转移的

//实现auto_ptr
template<class T>
class Auto_ptr
{
public:
	Auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~Auto_ptr()
	{
		if (_ptr)
			delete _ptr;
	}

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

	Auto_ptr(Auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		//资源管理权转移
		ap._ptr = nullptr;
	}

	Auto_ptr<T>& operator=(Auto_ptr<T>& ap)
	{
		if (this != &ap)
		{
			if (_ptr)
				delete _ptr;
			//资源管理权转移
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
private:
	T* _ptr;
};

unique_ptr

unique_ptr智能指针是通过防拷贝来解决资源管理权限转移问题----将unique_ptr赋值运算符函数和拷贝构造函数设置为删除函数

void test()
{
	unique_ptr<int> up(new int(10));
	unique_ptr<int> up2(up);//error
	unique_ptr<int> up3(new int(20));
	up = up3; //error
}

报错原因:拷贝构造和赋值重载函数都是已经删除的函数

底层实现:

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

	Unique_ptr(const Unique_ptr<T>& up) = delete;
	Unique_ptr<T>& operator=(const Unique_ptr<T>& up) = delete;

	~Unique_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

shared_ptr

shared_ptr是C++11中新提供的一种智能指针,不仅解决了资源管理权限转移问题,还提供靠谱的拷贝功能

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

void test()
{
	shared_ptr<A> sp(new A);
	shared_ptr<A> sp2(new A);
	shared_ptr<A> sp3(sp2);//ok
	sp3 = sp;//ok
	sp->_a = 100;
	sp2->_a = 1000;
	sp3->_a = 10000;
	cout << sp->_a << endl;
	cout << sp2->_a << endl;
	cout << sp3->_a << endl;
}

运行结果:

我们发现申请多少资源就会释放多少资源,此时的sp和sp3共享一份资源,修改sp3也就相等于修改了sp。所以最终都会打印10000。那共享了一份资源,是如何实现资源只释放一次呢?----引用计数

我们可以通过shared_ptr提供的接口use_count()来查看,当前有多少个智能指针来管理同一份资源

void test()
{
	shared_ptr<A> sp(new A);
	cout << sp.use_count() << endl;//1
	shared_ptr<A> sp2(sp);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	shared_ptr<A> sp3(new A);
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//1
	sp3 = sp;
	sp3 = sp2;
	cout << sp.use_count() << endl;//2
	cout << sp2.use_count() << endl;//2
	cout << sp3.use_count() << endl;//2
}

运行截图:之所以中间会有调析构函数,是因为当sp3指向sp时,sp3的引用计数为0,则会调用析构函数来释放资源。此时sp创建的资源就有3个指智能指针来管理

图解

在实现时,我们应该确保一个资源只对应一个计数器,而不是每个智能指针都有各自的计数器。所以我们可以将资源和计数器绑定在一起,此时指向同一份资源的智能指针,访问的也都是同一个计数器
成员变量:成员变量应该有两个变量,分别是资源指针的变量_ptr和计数器变量_countPtr,他们都是一个指针类型的变量
拷贝构造函数:在拷贝构造函数中,应该将当前对象的指针指向要拷贝的对象的资源,而且还要拷贝它的计数器,最后还需要将计数器进行++
赋值运算符重载:我们不能判断两个对象是否相等,而应该为只要两个对象的资源不同,那么才需要进行赋值。在赋值中,先让当前对象的计数器进行–,如果为0则表示当前对象的资源是只被当前对象管理,则需要释放资源。然后再将当前对象指针修改为要拷贝的对象的资源,并且拷贝它的计数器。最后还要对计数器进行++操作
析构函数:判断当前对象的资源的计数器,先进行–操作,在判断计数器是否为0,如果为0才会将资源释放,不为0则什么都不做

template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
	{}
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
	{
		//计数器累加
		++(*_countPtr);
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			if (--(*_countPtr) == 0)
			{
				delete _ptr;
				delete _countPtr;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			++(*_countPtr);
		}
		return *this;
	}

	~Shared_ptr()
	{
		//计数器自减
		if (--(*_countPtr) == 0)
		{
			delete _ptr;
			delete _countPtr;
			_ptr = nullptr;
			_countPtr = nullptr;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
};

我们实现的shared_ptr智能指针在多线程的场景下其实是不存在线程安全问题的----引用计数器指针是一个共享变量,多个线程进行修改时会导致计数器混乱。导致资源提前被释放或者会产生内存泄漏问题
我们来看看一下代码, 如果是安全的,那么最后析构函数应该只被调用一次

void fun(const Shared_ptr<A>& sp, int n)
{
	for (int i = 0; i < n; ++i)
		Shared_ptr<A> copy(sp);//创建copy智能指针
}

void test()
{
	Shared_ptr<A> sp(new A);
	int n = 100000;
	thread t1(fun, ref(sp), n);
	thread t2(fun, ref(sp), n);
	t1.join();
	t2.join();
}

运行结果1:我们发现并没有调用对象的析构函数,说明此时产生了内存泄漏的问题

运行结果2:调用两次析构函数,也就说明一份资源被释放两次。

我们可以在类中提供获取计数器的值的接口

size_t getCount()
	{
		return *_countPtr;
	}

然后再代码中运行并获取计数器的值,发现计数器的值并没有为0,所以就不会调用调用析构函数

所以我们可以在修改计数器的地方进行加锁保护。而这个锁不能为全局变量的锁,资源之间不能被有影响,否则当一个资源进行加锁修改时,另一个资源会被收到影响,此时会影响代码的执行效率。应当给每一个计数器提供单独的锁
这里++操作和–操作都进行封装

template<class T>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{}
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
				delete _ptr;
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
		//计数器自减
		if (subCount() == 0)
		{
			delete _ptr;
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}

	size_t getCount()
	{
		return *_countPtr;
	}

	size_t addCount()
	{
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
};

运行结果:我们发现多线程场景下,也是都可以正常释放的

循环引用问题

shared_ptr其实也存在一些小问题,也就是循环引用问题
我们先来看看以下代码

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

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

void test()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

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

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

运行结果:我们发现并没有释放资源,计数器也在自增

图解:


在C++11中,专门为了解决这个问题,又引入了一种新的智能指针waek_ptr,这种指针称为弱指针。在赋值或者拷贝的时候,计数器并不会进行++。析构时也并不会进行真正资源的释放。waek_ptr不能单独使用,其最大的作用就是解决shared_ptr循环引用的问题。

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

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

void test()
{
	shared_ptr<ListNode> n1(new ListNode);
	shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

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

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
}

运行结果:

在我们自己实现的shared_ptr中,我们在释放资源时都单单以delete来释放,而在我们申请空间方式中并非就只用new来申请空间们也有可能是用malloc来申请,则此时就应该要用free来释放。所以我们还要给智能指针添加一个删除器

void test()
{
	Shared_ptr<A> sp(new A[100]);//调用析构会报错
}

删除器主要可以通过仿函数来实现

template<class T>
struct DeleteDel
{
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template<class T>
struct FreeDel
{
	void operator()(T* ptr)
	{
		free(ptr);
	}
};

template<class T>
struct DeleteArrDel
{
	void operator()(T* ptr)
	{
		delete[] ptr;
	}
};
template<class T, class Del = DeleteDel<T>>
class Shared_ptr
{
public:
	Shared_ptr(T* ptr)
		:_ptr(ptr)
		, _countPtr(new size_t(1))//初始化为1
		, _mtx(new mutex)
	{}
	Shared_ptr(const Shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _countPtr(sp._countPtr)
		,_mtx(sp._mtx)
	{
		//计数器累加
		//++(*_countPtr);
		addCount();
	}
	Shared_ptr<T> operator=(const Shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//本身计数器自减
			//计数器为0,则当前对象需要释放资源
			//if (--(*_countPtr) == 0)
			if (subCount() == 0)
			{
				//delete _ptr;
				//通过删除器来释放空间
				_del(_ptr);
				delete _countPtr;
				delete _mtx;
			}

			_ptr = sp._ptr;
			_countPtr = sp._countPtr;

			addCount();
		}
		return *this;
	}

	~Shared_ptr()
	{
		//计数器自减
		if (subCount() == 0)
		{
			//delete _ptr;
			//通过删除器来释放空间
			_del(_ptr);
			delete _countPtr;
			delete _mtx;
			_ptr = nullptr;
			_countPtr = nullptr;
			_mtx = nullptr;
		}
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}

	size_t getCount()
	{
		return *_countPtr;
	}

	size_t addCount()
	{
		_mtx->lock();
		++(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}

	size_t subCount()
	{
		_mtx->lock();
		--(*_countPtr);
		_mtx->unlock();
		return *_countPtr;
	}
private:
	T* _ptr;
	size_t* _countPtr;//计数器指针
	mutex* _mtx;
	Del _del;
};

以上就是C++ 一篇文章让你知道智能指针的魅力的详细内容,更多关于C++智能指针的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++11新特性之智能指针(shared_ptr/unique_ptr/weak_ptr)

    shared_ptr基本用法 shared_ptr采用引用计数的方式管理所指向的对象.当有一个新的shared_ptr指向同一个对象时(复制shared_ptr等),引用计数加1.当shared_ptr离开作用域时,引用计数减1.当引用计数为0时,释放所管理的内存. 这样做的好处在于解放了程序员手动释放内存的压力.之前,为了处理程序中的异常情况,往往需要将指针手动封装到类中,通过析构函数来释放动态分配的内存:现在这一过程就可以交给shared_ptr去做了. 一般我们使用make_shared来

  • C++中智能指针如何设计和使用

    智能指针(smart pointer)是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露.它的一种通用实现技术是使用引用计数(reference count).智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针.每次创建类的新对象时,初始化指针并将引用计数置为1:当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数:对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果

  • C++ 中boost::share_ptr智能指针的使用方法

    C++ 中boost::share_ptr智能指针的使用方法 最近项目中使用boost库的智能指针,感觉智能指针还是蛮强大的,在此贴出自己学习过程中编写的测试代码,以供其他想了解boost智能指针的朋友参考,有讲得不正确之处欢迎指出讨论.当然,使用boost智能指针首先要编译boost库,具体方法可以网上查询,在此不再赘述. 智能指针能够使C++的开发简单化,主要是它能够自动管理内存的释放,而且能够做更多的事情,即使用智能指针,则可以再代码中new了之后不用delete,智能指针自己会帮助你管理

  • C++ 智能指针深入解析

    1. 为什么需要智能指针?简单的说,智能指针是为了实现类似于Java中的垃圾回收机制.Java的垃圾回收机制使程序员从繁杂的内存管理任务中彻底的解脱出来,在申请使用一块内存区域之后,无需去关注应该何时何地释放内存,Java将会自动帮助回收.但是出于效率和其他原因(可能C++设计者不屑于这种傻瓜氏的编程方式),C++本身并没有这样的功能,其繁杂且易出错的内存管理也一直为广大程序员所诟病. 更进一步地说,智能指针的出现是为了满足管理类中指针成员的需要.包含指针成员的类需要特别注意复制控制和赋值操作,

  • C++中auto_ptr智能指针的用法详解

    智能指针(auto_ptr) 这个名字听起来很酷是不是?其实auto_ptr 只是C++标准库提供的一个类模板,它与传统的new/delete控制内存相比有一定优势,但也有其局限.本文总结的8个问题足以涵盖auto_ptr的大部分内容. auto_ptr是什么? auto_ptr 是C++标准库提供的类模板,auto_ptr对象通过初始化指向由new创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者.当auto_ptr对象生命周期结束时,其析构函数会将auto_ptr对象拥有

  • C++ 智能指针的魅力你都了解吗

    前情提要 我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆.程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们. 在C++中,动态内存的管理是用一对运算符完成的:new和delete,ne:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete指向一个动态独享的指针,销毁对象,并释放与之关联的内存. 动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏:一种是尚有指针引用

  • C++学习之移动语义与智能指针详解

    移动语义 1.几个基本概念的理解 (1)可以取地址的是左值,不能取地址的就是右值,右值可能存在寄存器,也可能存在于栈上(短暂存在栈)上 (2)右值包括:临时对象.匿名对象.字面值常量 (3)const 左值引用可以绑定到左值与右值上面,称为万能引用.正因如此,也就无法区分传进来的参数是左值还是右值. const int &ref = a;//const左值引用可以绑定到左值 const int &ref1 = 10;//const左值引用可以绑定到右值 (4)右值引用:只能绑定到右值不能绑

  • 关于c++ 智能指针及 循环引用的问题

    c++智能指针介绍 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete,比如流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见,并造成内存泄露.如此c++引入 智能指针 ,智能指针即是C++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略. 智能指针在 <memory>标头文件的 std 命名空间中定义. 它们对 RAII 或 获取资源即初始化 编程惯用法至关重要. RAII 的主要原则是

  • 浅析Boost智能指针:scoped_ptr shared_ptr weak_ptr

    一. scoped_ptrboost::scoped_ptr和std::auto_ptr非常类似,是一个简单的智能指针,它能够保证在离开作用域后对象被自动释放.下列代码演示了该指针的基本应用: 复制代码 代码如下: #include <string>#include <iostream>#include <boost/scoped_ptr.hpp> class implementation{public:    ~implementation() { std::cout

  • C++智能指针读书笔记

    最近在补看<C++ Primer Plus>第六版,这的确是本好书,其中关于智能指针的章节解析的非常清晰,一解我以前的多处困惑.C++面试过程中,很多面试官都喜欢问智能指针相关的问题,比如你知道哪些智能指针?shared_ptr的设计原理是什么?如果让你自己设计一个智能指针,你如何完成?等等--.而且在看开源的C++项目时,也能随处看到智能指针的影子.这说明智能指针不仅是面试官爱问的题材,更是非常有实用价值. C++通过一对运算符 new 和 delete 进行动态内存管理,new在动态内存中

  • 智能指针与弱引用详解

    在android 中可以广泛看到的template<typename T> class Sp 句柄类实际上是android 为实现垃圾回收机制的智能指针.智能指针是c++ 中的一个概念,因为c++ 本身不具备垃圾回收机制,而且指针也不具备构造函数和析构函数,所以为了实现内存( 动态存储区) 的安全回收,必须对指针进行一层封装,而这个封装就是智能指针,其实说白了,智能指针就是具备指针功能同时提供安全内存回收的一个类.当然,智能指针的功能还不只这些,想了解更多大家可以去研究下- 智能指针有很多实现

  • C++智能指针实例详解

    本文通过实例详细阐述了C++关于智能指针的概念及用法,有助于读者加深对智能指针的理解.详情如下: 一.简介 由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete.程序员忘记 delete,流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见. 用智能指针便可以有效缓解这类问题,本文主要讲解参见的智能指针的用法.包括:std::auto_ptr.boost::scoped_ptr.boost::shared_p

  • 详解C++-(=)赋值操作符、智能指针编写

    (=)赋值操作符 编译器为每个类默认重载了(=)赋值操作符 默认的(=)赋值操作符仅完成浅拷贝 默认的赋值操作符和默认的拷贝构造函数有相同的存在意义 (=)赋值操作符注意事项 首先要判断两个操作数是否相等 返回值一定是 return *this; 返回类型是Type&型,避免连续使用=后,出现bug 比如: class Test{ int *p; Test(int i) { p=new int(i); } Test& operator = (const Test& obj) { i

随机推荐