首页 > 编程语言 >【C++11】智能指针

【C++11】智能指针

时间:2024-11-07 21:46:44浏览次数:3  
标签:11 auto C++ 智能 unique pointer ptr 指针

一.为什么需要智能指针

        学习C++的人,一直在接触裸指针,一边感受着它的强大,一边感受着它的坑爹。当然,坑不坑爹在于开发者,指针本身近乎完美,但奈何用的人比较猥琐,给自己埋下无数的坑,还哭喊着指针不好用,那么今天要介绍的智能指针可以释放大家在使用裸指针时的一些压力,当然智能指针无法替代裸指针的全部功能。

        那么,我们在使用裸指针的时候会遇到哪些问题呢??

  1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题)
  2. 同一资源释放多次,导致释放野指针,程序崩溃
  3. 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间return掉了,导致释放资源的代码未被执行到,懵
  4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到

        总之,智能指针的智能二字,主要体现在用户可以不关注资源的释放,因为智能指针会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异常,资源在到期的情况下,一定会进行释放。

二.内存泄漏

2.1.什么是内存泄漏

        内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

2.2.内存泄漏的危害

        长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

2.3.内存泄漏的分类

堆内存泄漏 (Heap leak)         堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new 等从堆中分配的一 块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分 内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak 。 系统资源泄漏         指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放 掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

2.4.如何避免内存泄漏

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。 ps : 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。 2. 采用 RAII 思想或者智能指针来管理资源。 3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。 4. 出问题了使用内存泄漏工具检测。 ps :不过很多工具都不够靠谱,或者收费昂贵。 总结一下 :         内存泄漏非常常见,解决方案分为两种:1 、事前预防型。如智能指针等。 2 、事后查错型。如泄漏检测工具。

        C++11库里面,提供了带引用计数的智能指针和不带引用计数的智能指针,这篇文章主要介绍它们的原理和应用场景,包括auto_ptr,scoped_ptr,unique_ptr,shared_ptr,weak_ptr。

三.智能指针

        为了更好的理解C++库中智能指针的原理,我们首先需要自己实现一个简单的智能指针,窥探一下智能指针的基本原理,就是利用栈上的对象出作用域会自动析构这么一个特点,把资源释放的代码全部放在这个析构函数中执行,就达到了所谓的智能指针。对比下面的两块代码:

int main()
{
	int *p = new int;
	/*其它的代码...*/
	/*
	如果这里忘记写delete,或者上面的代码段中程序return掉了,
	没有执行到这里,都会导致这里没有释放内存,内存泄漏
	*/
	delete p;

	return 0;
}

 

template<typename T>
class CSmartPtr
{
public:
	CSmartPtr(T *ptr = nullptr) :mptr(ptr) {}
	~CSmartPtr() { delete mptr; }

	T& operator*() { return *mptr; }
	const T& operator*()const { return *mptr; }

	T* operator->() { return mptr; }
	const T* operator->()const { return mptr; }
private:
	T *mptr;
};
int main()
{
	CSmartPtr<int> ptr(new int);
	*ptr = 20;
	cout << *ptr << endl;
	return 0;
}

        上面的这个智能指针,使用起来就和普通的裸指针非常相似了,但是它还存在很大的问题,看下面的代码:

int main()
{
	CSmartPtr<int> ptr1(new int);
	CSmartPtr<int> ptr2(ptr1);
	return 0;
}

        这个main函数运行,代码直接崩溃,问题出在默认的拷贝构造函数做的是浅拷贝,两个智能指针都持有一个new int资源,ptr2先析构释放了资源,到ptr1析构的时候,就成了delete野指针了,造成程序崩溃。所以这里引出来智能指针需要解决的两件事情:

怎么解决智能指针的浅拷贝问题
        多个智能指针指向同一个资源的时候,怎么保证资源只释放一次,而不是每个智能指针都释放一次,造成代码运行不可预期的严重后果
        我们一起看看C++库中提供的智能指针是怎么解决上面提到的问题的。

3.1.不带引用计数的智能指针std::auto_ptr、std::unique_ptr

std::auto_ptr

先来看一下auto_ptr的部分源码:

template<class _Ty>
	class auto_ptr
	{	// wrap an object pointer to ensure destruction
public:
	typedef _Ty element_type;

	explicit auto_ptr(_Ty * _Ptr = nullptr) noexcept
		: _Myptr(_Ptr)
		{	// construct from object pointer
		}

	/*这里是auto_ptr的拷贝构造函数,
	_Right.release()函数中,把_Right的_Myptr
	赋为nullptr,也就是换成当前auto_ptr持有资源地址
	*/
	auto_ptr(auto_ptr& _Right) noexcept
		: _Myptr(_Right.release())
		{	// construct by assuming pointer from _Right auto_ptr
		}
		
	_Ty * release() noexcept
		{	// return wrapped pointer and give up ownership
		_Ty * _Tmp = _Myptr;
		_Myptr = nullptr;
		return (_Tmp);
		}
private:
	_Ty * _Myptr;	// the wrapped object pointer
};

        auto_ptr的构造函数是将原来的地址赋给新的指针,然后将原来的指针置为nullptr。

int main()
{
	vector<auto_ptr<int>> vec;
	vec.push_back(auto_ptr<int>(new int(10)));
	vec.push_back(auto_ptr<int>(new int(20)));
	vec.push_back(auto_ptr<int>(new int(30)));
	// 这里可以打印出10
	cout << *vec[0] << endl;
	vector<auto_ptr<int>> vec2 = vec;
	/* 这里由于上面做了vector容器的拷贝,相当于容器中
	的每一个元素都进行了拷贝构造,原来vec中的智能指针
	全部为nullptr了,再次访问就成访问空指针了,程序崩溃
	*/
	cout << *vec[0] << endl;
	return 0;
}

         上面的程序,如果我们不了解auto_ptr的实现,代码就会出现严重的问题。

        auto_ptr智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的auto_ptr都置为nullptr,只让最后一个auto_ptr持有资源。

std::unique_ptr

我们还是先来看看源码:

template<class _Ty,
	class _Dx>	// = default_delete<_Ty>
	class unique_ptr
		: public _Unique_ptr_base<_Ty, _Dx>
	{	// non-copyable pointer to an object
public:
	typedef _Unique_ptr_base<_Ty, _Dx> _Mybase;
	typedef typename _Mybase::pointer pointer;
	typedef _Ty element_type;
	typedef _Dx deleter_type;

	/*提供了右值引用的拷贝构造函数*/
	unique_ptr(unique_ptr&& _Right) noexcept
		: _Mybase(_Right.release(),
			_STD forward<_Dx>(_Right.get_deleter()))
		{	// construct by moving _Right
		}
	
	/*提供了右值引用的operator=赋值重载函数*/
	unique_ptr& operator=(unique_ptr&& _Right) noexcept
		{	// assign by moving _Right
		if (this != _STD addressof(_Right))
			{	// different, do the move
			reset(_Right.release());
			this->get_deleter() = _STD forward<_Dx>(_Right.get_deleter());
			}
		return (*this);
		}

	/*
	交换两个unique_ptr智能指针对象的底层指针
	和删除器
	*/
	void swap(unique_ptr& _Right) noexcept
		{	// swap elements
		_Swap_adl(this->_Myptr(), _Right._Myptr());
		_Swap_adl(this->get_deleter(), _Right.get_deleter());
		}

	/*通过自定义删除器释放资源*/
	~unique_ptr() noexcept
		{	// destroy the object
		if (get() != pointer())
			{
			this->get_deleter()(get());
			}
		}
	
	/*unique_ptr提供->运算符的重载函数*/
	_NODISCARD pointer operator->() const noexcept
		{	// return pointer to class object
		return (this->_Myptr());
		}

	/*返回智能指针对象底层管理的指针*/
	_NODISCARD pointer get() const noexcept
		{	// return pointer to object
		return (this->_Myptr());
		}

	/*提供bool类型的重载,使unique_ptr对象可以
	直接使用在逻辑语句当中,比如if,for,while等*/
	explicit operator bool() const noexcept
		{	// test for non-null pointer
		return (get() != pointer());
		}
    
    /*功能和auto_ptr的release函数功能相同,最终也是只有一个unique_ptr指针指向资源*/
	pointer release() noexcept
		{	// yield ownership of pointer
		pointer _Ans = get();
		this->_Myptr() = pointer();
		return (_Ans);
		}

	/*把unique_ptr原来的旧资源释放,重置新的资源_Ptr*/
	void reset(pointer _Ptr = pointer()) noexcept
		{	// establish new pointer
		pointer _Old = get();
		this->_Myptr() = _Ptr;
		if (_Old != pointer())
			{
			this->get_deleter()(_Old);
			}
		}
	/*
	删除了unique_ptr的拷贝构造和operator=赋值函数,
	因此不能做unique_ptr智能指针对象的拷贝构造和
	赋值,防止浅拷贝的发生
	*/
	unique_ptr(const unique_ptr&) = delete;
	unique_ptr& operator=(const unique_ptr&) = delete;
	};

        unique_ptr去掉了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生。

        但是unique_ptr提供了带右值引用参数的拷贝构造和赋值,也就是说,unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时,示例代码如下:

// 示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); // 使用了右值引用的拷贝构造
ptr2 = std::move(ptr); // 使用了右值引用的operator=赋值重载函数

        使用std::move移动语义,将ptr转化为了右值引用,传入,调用了它的带右值引用的拷贝构造函数,也就是移动构造函数。

        unique_ptr还提供了reset重置资源,swap交换资源等函数,也经常会使用到。可以看到,unique_ptr从名字就可以看出来,最终也是只能有一个该智能指针引用资源,因此建议在使用不带引用计数的智能指针时,可以优先选择unique_ptr智能指针

3.2.带引用计数的智能指针std::shared_ptr和weak_ptr

shared_ptr:是一个强智能指针,可以改变资源的引用计数。

weak_ptr:是弱智能指针,不会改变资源的引用计数。

强智能指针循环引用是什么问题?什么结果?怎么解决?

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> _ptrb;
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> _ptra;
};
int main()
{
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());

	pa->_ptrb = pb;
	pb->_ptra = pa;

	cout << pa.use_count() << endl;
	cout << pb.use_count() << endl;

	return 0;
}

        出了main函数后,pa和pb指针会析构,但是我们类的引用计数从2变成了1,没有办法进行析构,从而导致了资源泄漏。

解决: 

        定义对象的时候,用强智能指针,引用对象的时候使用弱智能指针。即定义A,B类的时候使用弱智能指针。

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb;
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_ptr<A> _ptra;
};

我们再看一段代码:

class B;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	void testA() { cout << "非常好用的方法" << endl; }
	weak_ptr<B> _ptrb;
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	void func() { _ptra->testA(); }
	weak_ptr<A> _ptra;
};

         B类有个函数,想要去调用A类中的那个非常好用的方法,能否调用成功?

        答案是不能的,weak_ptr没有重载operaot=,operator->,它相当于是一个观察者,不能去访问资源。但是我们想要调用这个方法也是可以的,我们需要对weak_ptr提升等级,提升后看那部分资源是否还在,不在的话返回的是nullptr。

void func() 
	{ 
		shared_ptr<A> ps = _ptra.lock(); // 提升方法
		if (ps != nullptr)
		{
			ps->testA();
		}
	}

        这样我们就能访问到A类中那个非常好用的方法啦! 

四.多线程访问共享对象的线程安全问题

        C++ 非常著名的开源网络库muduo库

        多线程访问共享对象的线程安全问题:

#include <thread>
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	void testA() { cout << "非常好用的方法" << endl; }
	weak_ptr<B> _ptrb;
};
void handler01(weak_ptr<A> pw)
{
	
	// q访问A对象的时候,需要侦测一下A对象是否存活
	shared_ptr<A> ps = pw.lock();
	if( ps != nullptr)
	{ 
		ps->testA();
	}
	else
	{
		cout << "A对象已经析构,无法访问" << endl;
	}
		
}
// main线程
int main()
{
	{
		shared_ptr<A> p(new A());
		thread t1(handler01, weak_ptr<A>(p));
		std::this_thread::sleep_for(std::chrono::seconds(2));
		t1.detach();
	}
	std::this_thread::sleep_for(std::chrono::seconds(20));
	// 阻塞等待子线程结束
	// t1.join();
	return 0;
}

五.智能指针删除器

deletor

// 仿函数的删除器
template<class T>
struct FreeFunc {
 void operator()(T* ptr)
 {
 cout << "free:" << ptr << endl;
 free(ptr);
 }
};
template<class T>
struct DeleteArrayFunc {
 void operator()(T* ptr)
 { 
 cout << "delete[]" << ptr << endl;
 delete[] ptr; 
 }
};
int main()
{
 FreeFunc<int> freeFunc;
 std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
 DeleteArrayFunc<int> deleteArrayFunc;
 std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
    
    
 std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
{fclose(p); });
 
 return 0;
}

标签:11,auto,C++,智能,unique,pointer,ptr,指针
From: https://blog.csdn.net/m0_73839343/article/details/143590718

相关文章

  • 【C++】C++11之函数对象,Lambda表达式和functional函数对象类型
    知识的学习在于点滴记录,坚持不懈函数对象        重载了函数调用运算符()的类的对象,即为函数对象。        std::function由上文可以看出:由于可调用对象的定义方式比较多,但是函数的调用方式较为类似,因此需要使用一个统一的方式保存可调用对象或者传递可......
  • 2024.11.7随笔
    前言觉得就两三个人在机房安静自习真的好,有很多事情要做,规划好后按计划走不会感到迷茫而无所适从,头脑中也有时间的意识。只能说我个人比较喜欢对时间的掌控感,也喜欢安静的环境。明天大家就都要归队了,不知道下一次这么安静又要等到多久?写题今天水了个三倍经验所以就过了六道题,然......
  • YOLOv11 正式发布!你需要知道什么? 另附:YOLOv8 与YOLOv11 各模型性能比较
    YOLOv11目标检测创新改进与实战案例专栏点击查看文章目录:YOLOv11创新改进系列及项目实战目录包含卷积,主干注意力,检测头等创新机制以及各种目标检测分割项目实战案例点击查看专栏链接:YOLOv11目标检测创新改进与实战案例2024年9月30日,Ultralytics在他们的YOLOVision活动......
  • C++循环引用指的是什么,在使用过程当中需要注意什么问题
    C++中的循环引用是指两个或多个对象相互持有对方的引用,导致这些对象无法被自动释放,从而造成内存泄漏。循环引用主要发生在使用智能指针(如 std::shared_ptr)管理对象生命周期时。以下是循环引用的具体解释及其使用中需要注意的问题:循环引用的形成当两个对象A和B互相持......
  • cpp_9【用指针在更改主函数中的变量值】
    5.编写并测试一个函数larger_of(),该函数把两个double类型变量的值替换为较大的值。例如,larger_of(x,y)会把x和y中较大的值重新赋给两个变量。#include<stdio.h>voidlarger_of(double*x,double*y){ if(*x>*y){ *y=*x; } elseif(*y>*x){......
  • 11月7日 NOIP模拟(难题math、矩阵游戏matrix、括号序列seq、道路road) - 模拟赛记录
    PrefaceT1试图找规律失败,正经推反而几分钟就出来了。以后应该少想这些歪门邪道(除非实在闲的蛋疼或者没有一点头绪,且必须要打完所有能打的子任务比如暴力或特殊性质;而且必须在用常规方法思考过后,才能够用一些稍微不那么常规的方法)至于T2、T3、T4,因为知道T1浪费了太多时间,都是......
  • 2024/11/5日工作总结
    学习JS基础知识:1.引入方式:点击查看代码<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>Title</title></head><body><!--内部脚本--><!--<script>alert(......
  • 11.7 HTML
    Html一、基本介绍1、定义:html是一种超文本标记语言,也是一种标识性语言(不是编程语言)标记:记号(绰号)超文本:就是页面内容可以包含图片、链接,音乐,视频等素材。2、为什么学习html?(1)测试页面元素,了解页面页面元素(页面是html语言编写的)(2)进行ui自动化需用到元素定位3、html有哪些特点......
  • 2024年11月随便做做
    十月太摆了没有随便做做环节。测试题目选集20241106-D.盼君勿忘题解等会写qwq。Miscellaneous[AGC022D]Shopping神秘题目,比较酷。首先发现对于\(t_i\ge2L\)的\(t_i\)可以直接将\(t_i\lfloor\frac{t_i}{2L}\rfloor\)加入答案并将\(t_i\)对\(2L\)取模。然后只......
  • 11.7日
    创建一个新的DynamicWebProject打开Eclipse。选择File->New->DynamicWebProject。在弹出的对话框中,输入项目名称,例如MyWebApp。确保Targetruntime设置为ApacheTomcatv9.0(或其他你安装的Tomcat版本)。如果没有配置,点击NewRuntime按钮进行配置。点击Finish......