C++ 11 -- 初步认识智能指针

2023-12-13 13:06:14

一.RAII

1.1 RAII的概念

? 一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。

? RAII是一种利用对象的生命周期来控制资源释放的技术。比如一个局部对象,出了作用域就被销毁,RAII利用这一特性将资源与对象绑定在一起,当局部对象释放时,绑定在其身上的资源也要被释放。


1.2 内存泄漏的场景


int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void func()
{
	int* p1 = new int[10]; 

	int* p2 = new int[10]; 
	int* p3 = new int[10]; 
	int* p4 = new int[10]; 

	try
	{
		div();
	}
	catch (...)
	{
		delete[] p1;
		delete[] p2;
		delete[] p3;
		delete[] p4;

		throw;
	}

	delete[] p1;
	delete[] p2;
	delete[] p3;
	delete[] p4;
}

int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
		// ...
	}

	return 0;
}

? main函数调用func函数并捕获其中的异常,func函数申请资源后调用div函数并捕获其中的异常,div函数可能抛出异常。当资源被申请后,调用div函数时发生了异常,func函数就能捕获其异常,并释放之前申请的资源。

? 但程序还有问题,如果func函数只申请一次资源,申请失败时,main函数捕获申请失败的异常,这没有问题,但上面的func函数申请了四次资源,假如前三次申请资源成功,第四次申请资源失败,异常被抛出并被main函数捕获,但前三次申请的资源没有被释放,造成了资源泄漏。总不能在main函数中释放资源,p1到p4都是局部对象,出了作用域就销毁了,main函数想释放资源也不知道资源在哪。而使用RAII的思想就能解决这样的问题。

? 当申请资源时,就构造一个对象,在对象的声明周期内,资源能被正常使用,对象析构时,资源也随之释放。

? 智能指针就是RAII思想的一种具体实现。

二.智能指针的概念

??智能指针的本质上就是通过一个类把指针封装起来,利用RAII 思想,使这个类代替指针的同时,避免内存泄漏的情况。

? 简单的说:智能指针就是对普通的裸指针进行了一层包装,包装之后,就使得这个指针更加智能,能够自动在合适时间帮你去释放内存。

??C++标准库提供了四种智能指针的使用:?

? ? ?std::auto_ptr; c++98就有的一种智能指针,但是现在被遗弃,完全被std::unique_ptr取代。

??????? ??下面三种都是C++11提供的新智能指针;

  1. std::unique_ptr:一种独占式智能指针,同一个时间内只能有一个指针指向该对象。
  2. std::shared_ptr:多个指针可以指向同一个对象的指针(最像指针的智能指针)。
  3. std::weak_ptr:一种辅助std::shared_ptr指针而存在的指针。

? 使用智能指针的时候,记得包含头文件#include<memory>?。

? 智能指针的使用方法和其它容器差不多,只是多了一些指针的特性,最主要的还是出现了-> 和* 操作符,但智能指针是不支持隐式构造的

三.智能指针的最简易实现

#include<iostream>
using namespace std;

template <class T>
class My_Ptr
{
public:
	My_Ptr(T* ptr)
		:_ptr(ptr)
	{}
	~My_Ptr() {
		cout << "delete:" << _ptr << endl; 
		delete _ptr;
		_ptr = nullptr;
	}

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

private:
	T* _ptr;
};

int main() {
	int* a=new int(10);
	My_Ptr<int> ptr(a);
	cout << *ptr << endl;
	return 0;
}

可以看出我们的指针可以有指针的特性,也可以在程序结束时自动销毁(这个其实很好理解吧,毕竟利用了类自动调用析构函数的特性)。


int main() {
	int* a=new int(10);
	My_Ptr<int> ptr(a);
	cout << *ptr << endl;
	My_Ptr<int> ptr2(a);
	return 0;
}
int main() {
	int* a=new int(10);
	My_Ptr<int> ptr(a);
	cout << *ptr << endl;
	My_Ptr<int> ptr2(a);
	return 0;
}

总结一下智能指针的特点:

1.不用显式地写出析构函数。

2.资源在智能指针的生命周期中始终有效。

3.可以像指针一样的使用。

4.最重要的是:具有RAII特性?

上面的SmartPtr还存在着问题:拷贝构造对象和赋值时,多个SmartPtr指向同一块空间,当SmartPtr析构时便会造成资源的多次释放,导致程序崩溃。?

比如我们将main函数中的内容改成这样:


int main() {
	int* a=new int(10);
	My_Ptr<int> ptr(a);
	cout << *ptr << endl;
	My_Ptr<int> ptr2(a);
	return 0;
}

直接报错:

我们库中有四个智能指针,那么他们是怎么解决这个?这个问题的呢?我们一个个来看一下。

四.auto_ptr -- C++98最失败的设计

? 这个我都不想讲,谁用谁是大傻春。

? auto_ptr对于拷贝问题的解决方案是:管理权的转移,比如将p1拷贝给p2,也就是将p1对于资源的管理权转移给了p2,即将p1的指针置空,带来的问题是:后面的代码不能使用p1对象。由于这个问题的存在auto_ptr指针被很多人诟病,并且许多公司明确要求不能使用auto_ptr指针,因为管理权转移导致了原指针不能使用,相比后续的智能指针,auto_ptr确实是个失败的设计。

#include<iostream>
using namespace std;

int main() {
	auto_ptr<int> a1(new int(10));
	auto_ptr<int> a2(a1);
	cout << *a1 << endl;
	return 0;
}

这串代码直接报错:

五.unique_ptr?

??oost作为C++的一个开源库,承担着C++新功能的开发,如果boost库中有好用的设计出现,C++的标准库便会将好用的设计引入,出现在下一次的更新中。

? unique_ptr最初就是boost库中的scoped_ptr,C++的标准库汲取scoped_ptr中的精华并设计出了unique_ptr。unique_ptr解决智能指针拷贝问题的方案是:禁止拷贝.

? 可以看出,库中使用delete 禁止了unique的拷贝函数。

unique_ptr的模拟实现:
?

#pragma once
template <class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	unique_ptr(const unique_ptr<T>& p) = delete; //直接用delete禁止生成就行了
	unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;

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


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

private:
	T* _ptr;
};

? 在拷贝构造函数和赋值重载函数后加上delete,表示不能调用该函数,并且不能定义该函数。unique用这种简单粗暴的方式解决智能指针的拷贝问题,带来的缺陷是:unique独自占有资源,即不能使用拷贝构造和赋值,每一个unique都指向不同的资源。

六.shared_ptr

6.1 shared_ptr的基本概念和思想

? shared_ptr允许拷贝智能指针,其采用了计数的方式进行拷贝,只有一个指针指向资源时,计数为1,两个指针指向,计数为2,以此类推。对象析构时,只要将计数减一,如果减一后的计数为0,说明该对象是指向资源的最后一个对象,需要完成资源的释放。

?使用示例:

#include<iostream>
using namespace std;

int main() {

	shared_ptr<int> s1(new int(10));
	shared_ptr<int> s2(s1);
	cout << *s1 << endl;
	cout << *s2 << endl;
	return 0;
}

结果为:
?

? 我们都知道 shared_ptr 的底层使用了引用计数,那么它是怎么实现的呢? 实际上它用了一个int指针来进行引用计数,底层原理是这样的。

? 设_count为引用技术的int指针,_ptrx为每一个shared_ptr指针。

6.2 shared_ptr的底层实现

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		: _ptr(ptr)
		, _pCount(new int(1))
	{}
	shared_ptr(const shared_ptr<T>& p)
	{
		_ptr = p._ptr;
		_pCount = p._pCount;
		(*_pCount)++;
	}
	//写一个函数判断是否应该释放空间
	void Release()
	{
		if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
		{
			if (_ptr) // 如果_ptr为空,只要释放pCount
			{
				delete _ptr;
				_ptr = nullptr;
			}
			cout << "~shared_ptr()" << endl;
			delete _pCount;
			_pCount = nullptr;
		}
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& p)
	{
		// 指向资源不同时,使两指针指向同一资源,并且计数增加
		if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
		{
			Release(); // 先释放该指针之前指向的空间
			_ptr = p._ptr;
			_pCount = p._pCount;
			*_pCount++;
		}
	}

	~shared_ptr(){
		Release();
	}

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

private:
	T* _ptr;  //实际上只是用了两个指针实现
	int* _pCount;
};

?6.3 shared_ptr的用法详解

6.3.1 use_count成员函数

use_count成员函数使用来统计有多少个shared_ptr指针指向同一份内存空间对象的.

#include<iostream>

using namespace std;

int main() {
	shared_ptr<int> s1(new int(10));
	shared_ptr<int> s2(s1);
	shared_ptr<int> s3(s1);
	shared_ptr<int> s4(s1);

	cout << s1.use_count() << endl;
	cout << s4.use_count() << endl;
	s1.reset();//删除函数
	cout << s4.use_count() << endl;
	return 0;
}

代码结果为:
?

6.3.2 unique成员函数?

这个成员函数主要是判断:shared_ptr指针是否只有一个智能指针指向该对象,如果是:返回true,如果不是:返回false;

#include<iostream>

using namespace std;

int main() {
	shared_ptr<int> s1(new int(10));
	shared_ptr<int> s2(s1);
	shared_ptr<int> s3(new int (1));

	cout << s1.unique() << endl;
	cout << s3.unique() << endl;
	return 0;
}

代码结果为:
?

6.6.3 reset成员函数

'reset成员函数就是重置shared_ptr指针的的意思。

reset成员有两个重载版本: 第一个无参数的版本:重置该shared_ptr为空,同时引用计数减一,如果减到0就释放指针指向的内存空间.
第二个有参数的版本:重置shared_ptr指向为该参数的内存空间对象中,并且原来的内存空间对象的引用计数减一,如果减到0那么就释放该内存空间.

#include<iostream>

using namespace std;

int main() {
	shared_ptr<int> s1(new int(10));
	shared_ptr<int> s2(s1);
	shared_ptr<int> s3(s1);
	cout << s2.use_count() << endl;
	s1.reset();//删除函数
	cout << s2.use_count() << endl;

	cout << *s3 << endl;
	s3.reset(new int(20));
	cout << *s3 << endl;
	return 0;
}

?

七.指定删除器和指向数组的问题?

? C++的智能指针初始化的第二个参数,可以指定自定义的删除器,其实这个删除器就是一个函数指针,并且是单参数的函数指针,当然,你也可以传lambda表达式。


? 如果不指定第二个初始化的参数,那么就是使用默认的删除器,也就是直接delete的版本。

为什么要指定自己的删除器呢?
因为智能指针在管理数组指针时候,需要释放数组的内存,假如使用默认的删除器,也就是直接delete,那么就会导致内存泄漏了,所以需要自己指定自己删除器,去释放数组内存。

例如:

#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		cout << "A()构造函数执行" << endl;
	}
	~A()
	{
		cout << "A()析构函数执行" << endl;
	}
};

int main()
{
	shared_ptr<A> p(new A[10]); //试图开辟10个A类的数组空间,用智能指针P去指向它;
								//但是这会报错,报错原因就是默认删除器使用的delete p,
								//这样只能析构一个数组元素,剩下的9个没有析构成功
								//而我们需要的是delete[]p的方式释放内存,所以要自己指定删除器

	return 0;
}

?代码结果为:

可以看出析构函数只执行了一次。

加入删除器后:

#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		cout << "A()构造函数执行" << endl;
	}
	~A()
	{
		cout << "A()析构函数执行" << endl;
	}
};

int main()
{
	shared_ptr<A> p2(new A[10], [](A* p2) {
		delete[] p2; 
	}); //用lambda表达式指定删除器
	//这样就可以释放干净数组的内存了

	//其实,删除器还有一种是C++ 标准库提供的类模板std::default_delete
	//这种方式也可以用来删除数组
	shared_ptr<A> p3(new A[10], std::default_delete<A[]>());
	return 0;
}

?

? 在C++17提供了一种更加方便的方式来管理数组的,但是这种在C++11 和14都是不支持的,所以可能老的编译器会报错.

? 只在<>尖括号 和()小括号里面的类型都加上[ ]中括号即可,如:

#include<iostream>
using namespace std;

class A
{
public:
	A()
	{
		cout << "A()构造函数执行" << endl;
	}
	~A()
	{
		cout << "A()析构函数执行" << endl;
	}
};

int main()
{

	shared_ptr<A[]> p(new A[10]); //c++17就开始支持这种写法来管理数组
	return 0;
}

八.weak_ptr?

? 8.1 weak_ptr的基本概念

  1. 首先我们得知道weak_ptr:是一种辅助shared_ptr的智能指针.(也就是说,weak_ptr本身是不可以被单独使用的,?不可以被单独使用的意思:weak_ptr<int> p(new int(10)) 这种方式是不可以创建weak_ptr对象的,这是错误的用法).
  2. weak_ptr的对象只能指向一个由shared_ptr创建的对象,但是weak_ptr是不管理shared_ptr指针指向的对象内存的空间生存周期的.这个weak_ptr是不会增加shared_ptr的引用计数的。
  3. 也就是说shared_ptr所指向的对象该释放空间就释放空间,和weak_ptr没有关系,尽管weak_ptr还是指向该对象的内存空间,只要shared_ptr的引用计数为0,那么就会释放该对象内存空间.

?

? 我们知道weak_ptr就是用来辅助shared_ptr使用的,那么是如何辅助呢?
? 首先我们得认识什么是循环引用得问题。

8.2 循环引用问题


? 我们先来假装创建连个list节点,让他俩互相指向。

#include<iostream>
#include<memory>
using namespace std;

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode() { 
		cout << "~ListNode()" << endl;
	}

	ListNode() {
		cout << "ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;//这步会导致智能指针引用计数+1 
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

代码会正常调用两次构造和两次析构吗?

?很明显,当我执行这行代码的时候,并没有显示正确的两次析构函数,也就是说,这段代码出现了一个很严重的问题,那就是内存泄漏了。

? 这个就是循环引用带来的问题,导致内存泄漏,那到底什么是循环引用呢?


? ?也就是shared_ptr管理资源内存时候,互相指向的问题,你的shared_ptr指向我的sahred_ptr,我的shared_ptr又指向你的shared_ptr。

循环引用分析:

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. ?node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
  7. 属于node2成员,所以这就叫循环引用,谁也不会释放。


画个图更好理解上面的代码:

?

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
_prev不会增加node1和node2的引用计数。?

?

#include<iostream>
#include<memory>
using namespace std;

struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() {
		cout << "~ListNode()" << endl;
	}

	ListNode() {
		cout << "ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;//这步会导致智能指针引用计数+1 
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}#include<iostream>
#include<memory>
using namespace std;

struct ListNode
{
	int _data;
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode() {
		cout << "~ListNode()" << endl;
	}

	ListNode() {
		cout << "ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;//这步会导致智能指针引用计数+1 
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

此时代码结果为:

那 么原理是什么呢? 原理很简单,画个图就明白了。

? ?实际上,weak_ptr 只是指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,你也可以相当于weak_ptr不是一个智能指针,只是一个普通指针,因此解决了循环引用的问题,weak_ptr更像是shared_ptr的一个助手而不是智能指针

?

文章来源:https://blog.csdn.net/sushhsishdgsusk/article/details/134942760
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。