【C++】智能指针

目录

一.智能指针的背景概念与发展历史

1.为何需要有智能指针

2.RAII思想

3.智能指针实现框架

4.智能指针的发展历史

二.智能指针的拷贝/赋值问题

三.定制删除器(仿函数)

1.new/new[]与delete/delete[]匹配问题

2.定制删除器的简单实现

四.auto_ptr/unique_ptr/shared_ptr的区别与简单实现

0.前言

1.C++98: auto_ptr

2.C++11: unique_ptr

3.C++11: shared_ptr

五.循环引用问题,weak_ptr登场

1.循环引用问题

2.如何解决(使用weak_ptr)


一.智能指针的背景概念与发展历史

1.为何需要有智能指针

回顾异常安全问题: 如果申请了一个需要手动释放的资源, 而在手动释放代码执行之前, 有异常被抛出, 很可能因为抛出异常而导致进程执行流的跳跃, 导致资源没有被释放, 引发内存泄漏问题

例如以下伪代码

还没有释放pa, 就因为抛出异常跳跃执行流, 内存泄漏, 更严重的情况new本身抛出异常, 如果外面有对该代码段捕获异常的行为, 再加上多次new堆区空间, 也是很麻烦的内存泄漏问题

int* pa = new int;
throw ...;
delete pa;

 以下为实际情况中的代码

#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;

//以下程序有很多种情况会发生内存泄漏问题
//1.两次new成功了, 而在delete之前抛出异常, 执行流直接跳到catch, pa与pb内存泄漏
//2.第二次new失败, new本身抛出异常, 被外部捕获, pa内存泄漏

void func()
{
	int* pa = new int;
	int* pb = new int;

	int x = rand() % 5 + 1;
	if (x == 1 || x == 5)
	{
		throw x;
	}

	delete pa;
	delete pb;
}

int main()
{
	srand((unsigned int)time(nullptr));
	try
	{
		func();
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

基于以上问题, 如果new出的空间不再需要我们手动释放, 而是出函数栈帧就自动释放, 就不再会发生内存泄漏问题了

那么如何才能"自动化"管理堆区资源呢

智能指针很好的解决了"自动化管理资源"这一问题, 智能指针主要采用的RAII思想

2.RAII思想

RAII - Resource Acquistion Is Initialization

利用对象生命周期控制进程资源

1.在对象构造时获取资源

2.在对象析构时释放资源

本质上是一种将资源托管给一个对象来管理的行为

由于将资源托管给了对象, 自然地, 在对象创建时就要调用构造函数, 在对象销毁时就要调用析构函数, 完成了所谓的自动释放资源这一过程

3.智能指针实现框架

这里只是一般智能指针的简单框架的模拟实现, 真正的各种智能指针的实现在第三大点详细介绍

//智能指针框架
//主要实现3点
//1.资源的初始化与释放
//2.像指针一样的行为
//3.拷贝与赋值(且必须是浅拷贝,采用计数法调用析构)
template<class T>
class MySmartPtr
{
public:
	MySmartPtr(const T* ptr)
		:_ptr(ptr)
		,_pCount(new int(1))
	{}

	MySmartPtr(MySmartPtr& sp)
	{
		//...
	}
	
	MySmartPtr& operator=(MySmartPtr& sp)
	{
		//...
	}

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

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

	~MySmartPtr()
	{
		//...
	}

private:
	T* _ptr;
	int* _pCount;
};

4.智能指针的发展历史

--->C++98 auto_ptr, 拷贝有很严重的问题, 基本没人用

在C++98支持了auto_ptr智能指针, 但是auto_ptr在拷贝时相当于一种资源控制权的转移

如果std::auto_ptr p1= new int;

std::auto_ptr p2(p1);

这种行为会使得之前由p1管理的资源转交到了p2手上, p1中指针置为空

当在对p1进行访问的时候, 就会对空指针访问导致进程崩溃

--->后来boost库, 为C++标准库探索, 发表scoped_ptr/shared_ptr/weak_ptr三种智能指针

三种智能指针各有用途, 并且解决了auto_ptr拷贝的缺陷

scoped_ptr禁止拷贝或赋值

shared_ptr通过计数的方式控制资源释放, 从根源上解决智能指针拷贝与赋值的问题

weak_ptr专门为了解决shared_ptr中的循环引用问题

--->再后来C++ TR1, 引入了shared_ptr等。不过注意的是TR1并不是标准版

--->C++11用了boost库中的智能指针, 并将scoped_ptr重命名为unique_ptr

现在我们的C++程序在使用智能指针时, 通常是使用unique_ptr/shared_ptr/weak_ptr

三者的使用需要包头文件memory

二.智能指针的拷贝/赋值问题

以拷贝举例, 使用编译器默认生成的拷贝构造, 只是值拷贝, 属于浅拷贝, 所以在对象销毁调用析构时, 由于多个对象同时释放同一块资源, 就导致一块资源被多次释放, 而delete释放资源后又不会将指针置为nullptr, 对一个非空指针delete, 进程就会报错

通常情况下我们使用深拷贝来解决这样的问题, 所谓深拷贝, 就是不仅仅进行值拷贝, 而是连资源也一起拷贝过来, 但是这与智能指针的用途相违背, 意思就是, 智能指针的RAII思想, 是一种资源托管给对象的行为, 如果拷贝了对象的同时也对资源进行了拷贝, 那么这两个智能指针就分别管控两个值相同但空间不同的资源, 拷贝者的愿意应该是用两个智能指针管理同一个资源才发生的拷贝, 所以, 既然不可以用深拷贝来解决这种问题, 我们就要另辟蹊径. 采用计数的方式来对资源进行释放

计数法释放资源的原理:

每拷贝一个智能指针, 就让计数++一次, 当计数减为0时, 说明不再有指针管理这个资源, 就可以delete掉了

但是, 这个计数应该如何定义呢

思考1: 不可以使用普通成员变量来定义, 因为每个类对象都有属于自己的成员变量, 无法与其他对象产生关联

思考2: 不可以使用静态成员变量来定义, 因为所有对象都靠一个静态成员变量来管理, 这是不合理的, 也无法管理, 因为智能指针是类模板, 可能会有多种类型

综合以上思考, 得出结论

让这个计数变量也变为一种资源, 既然浅拷贝是值拷贝, 让多个智能指针管理同一块资源, 那么自然也就能让管理该资源的所有智能指针都管理计数资源, 这样就将管理同一块资源的所有指针都与计数资源产生关联, 就能进行加减操作了

三.定制删除器(仿函数)

1.new/new[]与delete/delete[]匹配问题

new与delete[]不匹配, new[]与delete不匹配

其报错的本质原因是:

所以为了避免程序崩溃, 避免内存泄漏等严重问题, 在实际使用中必须将new delete与new[] delete[]严格匹配

所以在实现智能指针析构函数时, 需要用到定制删除器, 对应制定的new, 调用指定的删除器进行析构

2.定制删除器的简单实现

定制删除器本质就是由仿函数实现的

在使用时如果需要传入的是模板参数则必须用仿函数实现(标准库unique_ptr就是这样)

如果有支持构造的重载, 可以将仿函数匿名对象传入构造函数参数中(标准库shared_ptr就是这样), 可以使用lambda表达式

template<class T>
struct Delete
{
public:
	void operator()(T* ptr)
	{
		cout << "call: delete ptr\n";
		delete ptr;
	}
};

template<class T>
struct DeleteArray
{
public:
	void operator()(T* ptr)
	{
		cout << "call: delete[] ptr\n";
		delete[] ptr;
	}
};

template<class T>
struct Free
{
public:
	void operator()(T* ptr)
	{
		cout << "call: free(ptr)\n";
		free(ptr);
	}
};

template<class T>
struct DocumentClose
{
public:
	void operator()(T* ptr)
	{
		cout << "call: fclose(ptr)\n";
		fclose(ptr);
	}
};

四.auto_ptr/unique_ptr/shared_ptr的区别与简单实现

0.前言

auto_ptr如今已没人使用, 所以学习auto_ptr的意义就是认识到它的缺点, 不要犯相同的错误

在C++标准库中, unique_ptr的定制删除器采用模板参数, 在实例化模板显式写入仿函数类型来控制

shared_ptr的定制删除器采用构造重载, 在调用构造函数时, 以传入仿函数对象来控制

  

template<class T>
struct DeleteArray
{
public:
	void operator()(T* ptr)
	{
		cout << "call: delete[] ptr\n";
		delete[] ptr;
	}
};

int main()
{
    std::unique_ptr<int, DeleteArray<int>> up(new int[5]);
    std::shared_ptr<int> sp(new int[5], DeleteArray<int>());
    return 0;
}

1.C++98: auto_ptr

对于auto_ptr只用程序验证它的缺陷, 不做实现, 因为实际中没人用这个, 实现起来也没有意义

2.C++11: unique_ptr

unique_ptr禁止拷贝与赋值, 其余与shared_ptr相同

//C++98实现方式
private:
    MyUniquePtr(MyUniquePtr& sp);
    MyUnipuePtr& operator=(MyUniquePtr& sp);

//C++11实现方式
MyUniquePtr(MyUniquePtr& sp) = delete;
MyUnipuePtr& operator=(MyUniquePtr& sp) = delete

3.C++11: shared_ptr

template<class T>
struct Delete
{
public:
	void operator()(T* ptr)
	{
		cout << "call: delete ptr\n";
		delete ptr;
	}
};

template<class T>
struct DeleteArray
{
public:
	void operator()(T* ptr)
	{
		cout << "call: delete[] ptr\n";
		delete[] ptr;
	}
};

template<class T>
struct Free
{
public:
	void operator()(T* ptr)
	{
		cout << "call: free(ptr)\n";
		free(ptr);
	}
};

template<class T>
struct DocumentClose
{
public:
	void operator()(T* ptr)
	{
		cout << "call: fclose(ptr)\n";
		fclose(ptr);
	}
};


template<class T, class D = Delete<T>>
class MySharedPtr
{
public:
	MySharedPtr(T* ptr)
		:_ptr(ptr)
		, _pCount(new int(1))
	{}

	MySharedPtr(MySharedPtr& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		(*_pCount)++;
	}

	MySharedPtr& operator=(MySharedPtr& sp)
	{
		if (_ptr == sp._ptr)
		{
			return *this;
		}
		//1.检查原计数
		if (--(*_pCount) == 0)
		{
			DeleteSource();
		}
		//2.赋值
		_ptr = sp._ptr;
		_pCount = sp._pCount;
		//3.++新计数
		(*_pCount)++;
		return *this;
	}

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

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

	T& operator[](int pos)
	{
		return *(_ptr + pos);
	}

	~MySharedPtr()
	{
		if (--(*_pCount) == 0)
		{
			DeleteSource();
		}
	}

	void DeleteSource()
	{
		D()(_ptr);
		delete _pCount;
	}
	
private:
	T* _ptr;
	int* _pCount;
};

void test1()
{
	cout << "-------------test1-------------" << endl;
	MySharedPtr<int> sp1(new int(5));
	MySharedPtr<int> sp2(new int(10));
	MySharedPtr<int> sp3(sp1);

	cout << *sp1 << endl;
	cout << *sp2 << endl;
	cout << *sp3 << endl;

	++(*sp1);
	++(*sp2);
	sp3 = sp2;

	cout << *sp1 << endl;
	cout << *sp2 << endl;
	cout << *sp3 << endl;
}

void test2()
{
	cout << "-------------test2-------------" << endl;
	MySharedPtr<int, DeleteArray<int>> sp4(new int[10]);
	for (int i = 0; i < 10; ++i)
	{
		//*(&(*sp4) + i) = i;
		sp4[i] = i;
	}
	for (int i = 0; i < 10; ++i)
	{
		cout << ++sp4[i] << ' ';
	}
	cout << endl;
}

void test3()
{
	cout << "-------------test3-------------" << endl;
	MySharedPtr<FILE, DocumentClose<FILE>> sp5(fopen("log.txt", "w"));
	MySharedPtr<int> sp6(new int(1));
	MySharedPtr<int, DeleteArray<int>> sp7(new int[15]);
	MySharedPtr<int, Free<int>> sp8((int*)malloc(sizeof(8)));
}

int main()
{
	test1();
	test2();
	test3();
	return 0;
}

五.循环引用问题,weak_ptr登场

1.循环引用问题

#include<memory>

struct A
{
	int _val = 5;
	std::shared_ptr<A> _prev;
	std::shared_ptr<A> _next;
};

int main()
{
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(new A);
	sp1->_next = sp2;
	sp2->_prev = sp1;

	return 0;
}

2.如何解决(使用weak_ptr)

weak_ptr抛弃了RAII思想, 主要为了解决shared_ptr的循环引用的问题

weak_ptr就像是shared_ptr的跟班, 独立使用没有价值, 要与shared_ptr配合使用

weak_ptr用在从shared_ptr的拷贝与赋值的情况, shared_ptr对象是可以拷贝或者赋值过来的

拷贝/赋值期间只将指针成员指向资源, 不参与资源控制计数, 但是也会将计数记录下来, 以防指向对象被析构之后的野指针问题

使用weak_ptr解决shared_ptr循环引用问题:

#include<memory>

struct A
{
	int _val;
	std::weak_ptr<A> _prev;
	std::weak_ptr<A> _next;
};

int main()
{
	std::shared_ptr<A> sp1(new A);
	std::shared_ptr<A> sp2(new A);
	sp1->_next = sp2;
	sp2->_prev = sp1;
	sp1.~shared_ptr(); 
	sp2.~shared_ptr();
	return 0;
}

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值