C++基础总结系列之【智能指针】

目录

智能指针

what

why

how

auto_ptr

unique_ptr

shared_ptr

weak_ptr


智能指针

what

在C++中,对动态内存的管理是通过new和delete完成的,在使用时极其容易出现问题,因为确保在正确的时间释放内存是很难的,有时忘记释放内存,就会产生内存泄漏,有时在尚有指针引用内存的情况下就释放了内存,就会产生引用非法内存的指针的情况。为了更安全地使用动态内存,C++新标准库提供了智能指针来管理动态对象,它的行为类似于常规指针,重要的区别是它能够自动释放所指向对象的空间,并且它是封装好的模板类(引用自C++ Primer Page400)。


why

为了安全方便地使用动态内存,管理动态内存的开辟与释放,避免内存泄露。


how

智能指针分为四种:auto_ptr、shared_ptr、unique_ptr、weak_ptr


auto_ptr

这是一个C++98中的方案,由于具有设计缺陷,在C++11中已经被舍弃,但具有学习的价值。

初始化方法

  • 方法一:

std::auto_ptr<类型> p1(new 类型(值));

  • 方法二:

std::auto_ptr<类型> p1

p1.reset(new 类型(值));

  • auto_ptr设计的缺陷:

auto_ptr采用copy语义来转移指针资源,采用所有权模型,转移指针资源的所有权的同时将原指针置为NULL。

这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。

下面为示例代码:

auto_ptr<int> p1(new int(100));
auto_ptr<int> p2;
p2.reset(new int(200));
p2 = p1;
cout<<p1.get()<<endl;
cout<<*p2<<endl;
cout<<*p1<<endl;

下面代码能通过编译,可以输出,输出的结果为:100,和00000000。即修改了p2的指向,指向p1原来指向的空间。

但随后会崩溃,原因是p1原来指向的空间,在通过p1给p2赋值时,已经被置为空,再次访问p1,操作了野指针,因此造成崩溃。

即:多个auto_ptr不能指向同一块内存。


unique_ptr

初始化方法

  • 方法一

std::unique_ptr<类型> p1(new 类型(值));

  • 方法二:

std::unique_ptr<类型> p1

p1.reset(new 类型(值));

  • 对于auto_ptr缺陷的解决方式
unique_ptr<int> p1(new int(100));
unique_ptr<int> p2;
p2.reset(new int(200));
p2 = p1;

上述代码在编译时候即已无法通过,unique_ptr,顾名思义,采用的是所有权模型,但多个unique_ptr不能指向同一块内存,通过这种方式避免了产生auto_ptr的缺陷问题。

unique_ptr<int> p1(new int(100));
unique_ptr<int> p2;
p2.reset(new int(200));
p2 = move(p1);
	
if(nullptr != p1.get())
	cout<<"p1:"<<*p1<<endl;
else
	cout<<"p2:"<<*p2<<endl;

为了能够转移所有权,提供了move()方法,称为移动语义,可以将p1的所有权转移给p2,此段代码输出为p2:100。

此时p1指向的空间已不能再操作,原因是p1已经是野指针。


shared_ptr

初始化方法

  • 方法一

std::shared_ptr<类型> p1(new 类型(值));

  • 方法二:

std::shared_ptr<类型> p1

p1.reset(new 类型(值));

  • 方法三

std::shared_ptr<类型> p1

p1 = make_shared<类型>(值);

  • 对于auto_ptr缺陷的解决方式
shared_ptr<int> p1;
p1.reset(new int(200));
shared_ptr<int> p2;
p2 = make_shared<int>(300);
p2 = p1;

cout<<"p1:"<<*p1<<endl;
cout<<"p2:"<<*p2<<endl;
cout<<"use_count:"<<p2.use_count()<<endl;

输出为

 

编译通过,正常输出,shared_ptr,顾名思义,多个shared_ptr可以共享同一块内存空间,通过引用计数来控制空间的回收,调用use_count()方法可以查看引用计数。

当有一个指针指向该空间时,引用计数+1,当引用计数归零时候,才回收内存空间。

  • 循环引用问题
#include<iostream>
#include<memory>
#include<string>
using namespace std;

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

class B
{
public:
	B()
	{
		cout<<"B()"<<endl;
	}
	~B()
	{
		cout<<"~B()"<<endl;
	}
public:
	shared_ptr<A> p2;
};
int main()
{
	{
		shared_ptr<A> pA(new A);
		shared_ptr<B> pB(new B);
		pA->p1 = pB;
		pB->p2 = pA;
		cout<<"pA use_count:"<<pA.use_count()<<endl;
		cout<<"pB use_count:"<<pB.use_count()<<endl;
	}
	system("pause");
	return 0;
}

运行输出为

正常情况下,{}内定义的指针pA和pB在{}结束后将会被回收,但是由于产生了循环引用的问题,导致上述运行的意外情况。

图中,类A中定义的p1指向pB,类B中定义的p2指向pA,两块内存空间的引用计数都为2,在走出{}范围后,pA、pB的生命周期结束,各自的引用计数-1,但shared_ptr要在引用计数归零时才调用析构函数回收空间,而p1和p2间仍存在指向关系,所以引用计数各自为1,这就是循环引用的问题。

自己实现shared_ptr

提供一种基于map记录引用计数的shared_ptr的实现

需要特别注意的几点:

  1. 普通的含参构造函数参数默认值需要为nullptr,才能正确调用=赋值运算符
  2. 拷贝构造函数实现的是浅拷贝,这是由于shared_ptr本身的特性决定的,多个指针指向同一块内存空间,使用引用计数避免重复释放空间
  3. 重载的*解引用运算符,返回值类型是模板类型的引用,由于智能指针就是包裹这个模板类型指针的类,所以最后返回的也就是这个指针的解引用
  4. 在析构函数和重载的=赋值运算符中,需要先判断当前操作的指针是否为空指针,对空指针进行的操作会产生错误,然后每次调用析构对map中的引用计数-1,直到引用计数归0释放空间
#include<iostream>
#include<map>
using namespace std;

template <class T>
class mshared_ptr
{
public:
	mshared_ptr(T* src = nullptr);
	~mshared_ptr();
	mshared_ptr(const mshared_ptr<T>& src_ptr);
	mshared_ptr<T>& operator = (const mshared_ptr<T>& src_ptr);
	T& operator *();
	int use_count();
private:
	T* ptr;
	static map<T*,int> use_count_map;
};

//静态成员类外初始化
template <class T>
map<T*,int> mshared_ptr<T>::use_count_map;

//普通含参构造
template <class T>
mshared_ptr<T>::mshared_ptr(T* src)
{
	cout<<"正确调用普通含参构造函数"<<endl;
	ptr = src;
	use_count_map.insert(make_pair(ptr,1));
}

//析构
template <class T>
mshared_ptr<T>::~mshared_ptr()
{
	if(nullptr != ptr && --use_count_map[ptr] == 0)
	{
		cout<<"正确调用析构函数释放空间"<<endl;
		delete ptr;
		ptr = nullptr;
		use_count_map.erase(ptr);
	}
}

//拷贝构造函数(浅拷贝)
template <class T>
mshared_ptr<T>::mshared_ptr(const mshared_ptr<T>& src_ptr)
{
	cout<<"正确调用拷贝构造函数"<<endl;
	ptr = src_ptr.ptr;
	++use_count_map[ptr];
}

//重载=赋值运算符 返回值是一个智能指针对象的引用 
template <class T>
mshared_ptr<T>& mshared_ptr<T>::operator=(const mshared_ptr<T>& src_ptr)
{
	if(ptr == src_ptr.ptr)
	{
		cout<<"指针已存在,返回本身"<<endl;
		return *this;
	}
	if(nullptr != ptr && --use_count_map[ptr] == 0)
	{
		cout<<"引用计数归0 清除空间"<<endl;
		delete ptr;
		ptr = nullptr;
		use_count_map.erase(ptr);
	}
	cout<<"正确调用重载=赋值运算符"<<endl;
	ptr = src_ptr.ptr;
	++use_count_map[ptr];
	return *this;
}
template <class T>
T& mshared_ptr<T>::operator *()
{
	return *ptr;
}
template <class T>
int mshared_ptr<T>::use_count()
{
	return use_count_map[ptr];
}
int main()
{
	{
		int *p = new int(10);
		mshared_ptr<int> p1(p);
		mshared_ptr<int> p2(p1);
		mshared_ptr<int> p3;
		p3 = p2;
		cout<<*p1<<endl;
		cout<<*p2<<endl;
		cout<<*p3<<endl;
		cout<<p2.use_count()<<endl;
	}
	system("pause");
	return 0;
}

更多智能指针的实现,请参考:

https://blog.csdn.net/caoshangpa/article/details/79221544


weak_ptr

初始化方法

  • 使用注意

weak_ptr只能通过已经初始化的shared_ptr进行初始化,且该模板类中没有重载*和->操作符,因此不能通过*解引用和通过->调用,只能在调用lock()方法获取使用权限后,再给shared_ptr或weak_ptr赋值。

std::shared_ptr<类型> p1(new 类型(值));

std::weak_ptr<类型> p2(p1); (其类型要与shared_ptr相同

shared_ptr<int> p1(new int(100));
weak_ptr<int> p2(p1);
p2.lock();
shared_ptr<int> p3(p2);
weak_ptr<int> p4(p2);
cout<<*p3<<endl;
shared_ptr<int> p5 = p4.lock();
cout<<*p5<<endl;

输出结果


通过上述代码可以看出:

weak_ptr在调用lock()方法后,获得一个所指向的shared_ptr的实例,可以给其他shared_ptr或weak_ptr进行初始化和赋值。 

shared_ptr<int> p1(new int(100));
weak_ptr<int> p2(p1);
p2.lock();
p1.reset();
if(p2.expired())
	cout<<"p2 is delete"<<endl;
else
	shared_ptr<int> p3(p2);

在p1调用reset()方法,weak_ptr指向的shared_ptr的空间被回收后, p2也被回收,通过调用expired()方法可以判断p2是否指向空,上述代码执行结果为输出p2 is delete。

weak_ptr<int> p1;
shared_ptr<int> p2(p1);

在vs2012下,这2行代码可以通过编译,但会崩溃,切忌出现这种操作。

  • 解决循环引用的问题

引入weak_ptr解决shared_ptr循环引用的问题,shared_ptr与weak_ptr在指向同一块空间时,各自维护自身的引用计数。

在释放时,pA生命周期先结束,它的引用计数pA use_count从1变为0,调用析构函数回收该空间,因此图中4代表的指针也被回收;

此时pB use_count在指向该空间的指针4被回收后,由2变为1,pB的生命周期随后结束,释放自身空间,引用计数从1变为0,pB指向的空间也调用析构函数回收,进而解决了循环引用的问题。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值