前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >智能指针:作用 | 使用 | 原理 | 内存泄漏

智能指针:作用 | 使用 | 原理 | 内存泄漏

作者头像
南桥
发布2024-09-25 08:59:44
1220
发布2024-09-25 08:59:44
举报
文章被收录于专栏:南桥谈编程

智能指针的作用

由于异常的存在,反复横跳可能会导致内存泄漏,不同的异常处理逻辑没有妥善管理内存分配和释放,可能会在某些路径中遗漏delete 操作,从而导致内存泄漏。

代码语言:javascript
复制
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如果 p1new int 抛出异常:这通常是因为内存分配失败。在这种情况下,程序会终止到这一行,控制权会转移到 main 函数中的 catch 块,因为 Func 中没有处理这个异常。

如果 p2new int 抛出异常:同样的道理,如果 new intp2 的分配中抛出异常,它也会传播到 main,并在 catch 块中处理。

如果 div 抛出异常(例如除以零):同样,这个异常会被 main 中的 catch 块捕获,输出 “除0错误”。

在所有情况下,如果在 delete p1delete p2 之前抛出了异常,程序将不会执行到这两行。因此,内存泄漏的风险在这种情况下是存在的,因为如果 new 语句抛出异常,就不会有对应的 delete 调用。

智能指针的使用与原理

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做法有两大好处:

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效

使用RAII思想设计一个Smartptr类:

代码语言:javascript
复制
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete: " << _ptr << endl;
	}

private:
	T* _ptr;
};

double Division(int a, int b)
{
	if (b == 0)
		throw invalid_argument("除0错误");

	return a / b;
}

void Func()
{
	SmartPtr<int> sp1(new int[10]);
	SmartPtr<int> sp2(new int[20]);

	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}

int main()
{
	try
	{
		Func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}

	return 0;
}

智能指针的基本框架

智能指针也是一个指针,因此也需要运算符重载

代码语言:javascript
复制
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr)
		: _ptr(ptr)
	{}

	~SmartPtr()
	{
		delete[] _ptr;
		cout << "delete: " << _ptr << endl;
	}

	T* get()
	{
		return _ptr;
	}

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

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

	T& operator[](size_t i)
		{
		return _ptr[i];
	}


private:
	T* _ptr;
};

std::auto_ptr

上述基本框架解决了指针问题,但是没有解决拷贝的问题。 智能指针有自己的发展历史,拷贝的思想是不一样的。

std::auto_ptr文档 C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。 auto_ptr的实现原理:管理权转移的思想

特点:拷贝构造时转移管理权,sp1是一个将亡值,进行资源转移,sp1是一个左值,资源转移导致sp1对象悬空,因此无法再安全访问 sp1。任何尝试解引用或访问sp1都会导致未定义行为。 不推荐使用这一做法

内部核心:

代码语言:javascript
复制
auto_ptr(auto_ptr<T>& sp)
	:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}

std::unique_ptr

unique_ptr阅读文档

在这里插入图片描述
在这里插入图片描述

std::unique_ptr使用:

代码语言:javascript
复制
class A
{
public:
	~A()
	{
		cout << "~A()" << endl;
	}
//private:
	int _a1 = 1;
	int _a2 = 1;
};

int main()
{
	unique_ptr<A> sp1(new A);
	//获取原生指针
	A* p = sp1.get();
	cout << p << endl;
	//访问元素
	sp1->_a1++;
	sp1->_a2++;

	return 0;
}

特点:std::unique_ptr不支持拷贝,建议使用。

std::shared_ptr

使用

std::shared_ptr文档

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共 享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了

std::shared_ptr的使用:

代码语言:javascript
复制
int main()
{
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2(sp1);

	return 0;
}
模拟实现

在模拟实现shared_ptr中,引用计数器需要使用指针来实现;

赋值的原理: 在执行赋值的操作时,需要先对sp1pcount进行--操作,因为此时的pcount==2,如果直接修改sp1pcount,导致数据不匹配,因此需要先将pcount的值减为1,再去赋值,这样才能保证引用计数器的值是正确的。这样可以避免内存泄漏。

自己给自己赋值原理:只要资源一样就是自己给自己赋值,直接返回对应指针就行。

完整代码:

代码语言:javascript
复制
#pragma once

namespace gwj
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			,_pcount(new int(1))
		{}

		//sp2(sp1)
		shared_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
		{
			(*_pcount)++;
		}

		//sp1==sp3
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr)
			{
				this->release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理的对象释放资源
				delete _ptr;
				delete _pcount;
			}
		}

		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				//最后一个管理的对象释放资源
				delete _ptr;
				delete _pcount;
			}
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

	private:
		T* _ptr;
		int* _pcount;  //引用计数器,用指针实现
	};
}
线程安全问题

智能指针对象本身拷贝析构是线程安全的,底层引用计数加减是安全的,指向的资源访问不是线程安全的(该加锁需要加锁)

智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++--,这个操作不是原子的,引用计数原来是1++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++--是需要加锁的,也就是说引用计数的操作是线程安全的。

循环引用
代码语言:javascript
复制
struct Node
{
	shared_ptr<Node> _next;
	shared_ptr<Node> _prev;
	int _val;

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

int main()
{
	shared_ptr<Node> p1(new Node);
	shared_ptr<Node> p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	return 0;
}

std::weak_ptr

不支持RAII,不单独管理资源,辅助解决shared_ptr的循环引用。本质是赋值或拷贝时,只指向资源,但是不增加shared_ptr的引用计数。

代码语言:javascript
复制
struct Node
{
	//shared_ptr<Node> _next;
	//shared_ptr<Node> _prev;
	weak_ptr<Node> _next;
	weak_ptr<Node> _prev;
	int _val;

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

int main()
{
	shared_ptr<Node> p1(new Node);
	shared_ptr<Node> p2(new Node);

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	p1->_next = p2;
	p2->_prev = p1;

	cout << p1.use_count() << endl;
	cout << p2.use_count() << endl;

	return 0;
}

定制删除器

定制删除器可以通过在 std::shared_ptr 中使用自定义的删除函数来管理资源。

代码语言:javascript
复制
class A
{
public:
	A()
	{}

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

private:
	int _a1 = 1;
	int _a2 = 1;
};

// 定制删除器
template<class T>
struct FreeFunc {
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};

int main()
{
	//std::shared_ptr<A[]> sp1(new A[10]);
	std::shared_ptr<A[]> sp1(new A[2], [](A* ptr) {delete[] ptr; });

	std::shared_ptr<int> sp2((int*)malloc(4), FreeFunc<int>());

	std::shared_ptr<FILE> sp3(fopen("test.txt", "w"), [](FILE* ptr) {fclose(ptr); });

	return 0;
}

内存泄漏问题

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

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

C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 智能指针的作用
  • 智能指针的使用与原理
    • RAII
      • 智能指针的基本框架
        • std::auto_ptr
          • std::unique_ptr
            • std::shared_ptr
              • 使用
              • 模拟实现
              • 线程安全问题
              • 循环引用
            • std::weak_ptr
              • 定制删除器
              • 内存泄漏问题
              • C++11和boost中智能指针的关系
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档