C++笔记归纳20:智能指针

智能指针

目录

智能指针

一、智能指针的使用场景

二、RAII和智能指针

三、智能指针的问题

四、C++标准库的智能指针

4.1.auto_ptr

4.2.唯一指针(unique_ptr)

4.3.共享指针(shared_ptr)

4.4.弱指针(weak_ptr)

4.5.删除器

4.6.make_share

五、智能指针的原理

5.1.auto_ptr原理

5.2.unique_ptr原理

5.3.share_ptr原理

5.4.weak_ptr原理

六、share_ptr循环引用问题

七、share_ptr的线程安全问题

八、内存泄漏

8.1.内存泄漏的概念

8.2.内存泄漏的危害

8.3.内存泄漏的避免


一、智能指针的使用场景

new完对象后,也使用了delete

但是因为抛异常,后面的delete没有执行,导致内存泄漏

需要new完对象后捕获异常,捕获到异常后delete内存,再把异常抛出

但因为new本身也可能抛异常

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
#include <exception>

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}

void Func()
{
	//如果发生除0错误抛出异常,另外下面的array和array2没有得到释放
	//这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去
	//但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案是智能指针
	int* array1 = new int[10];
	int* array2 = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array1 << endl;
		cout << "delete []" << array2 << endl;
		
		delete[] array1;
		delete[] array2;
		throw; //异常重新抛出,捕获到什么抛出什么
	}
	//...
	cout << "delete []" << array1 << endl;
	delete[] array1;
	cout << "delete []" << array2 << endl;
	delete[] array2;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
	return 0;
}

二、RAII和智能指针

**RAII(Resource Acquisition Is Initialization):**资源获取立即初始化

一种管理资源的类的设计思想

**本质:**利用对象的生命周期来管理获取到的动态资源,避免资源泄露

(注:这里的资源指的是内存、文件指针、网络链接、互斥锁...)

RAII在获取资源是把资源委托给一个对象,控制对资源的访问

资源在对象的生命周期内始终保持有效,在对象析构时被释放

既保障了资源的正常释放、又可以避免资源泄露问题

**智能指针:**一个类模板,用来实现RAII的设计思路,方便资源访问

(注:智能指针类像迭代器一样,重载operator*/operator->/operator[]等运算符)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <string>
#include <exception>

template<class T>
class SmartPtr
{
public:
	//RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{
	}
	~SmartPtr()
	{
		cout << "delete[] " << _ptr << endl;
		delete[] _ptr;
	}
	//重载运算符,模拟指针的行为,方便访问资源
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator[](size_t i)
	{
		return _ptr[i];
	}
private:
	T* _ptr;
};

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
	
}

void Func()
{
	//这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了
	SmartPtr<int> sp1 = new int[10];//隐式类型转换
	SmartPtr<int> sp2 = new int[10];
	
	for (size_t i = 0; i < 10; i++)
	{
		sp1[i] = sp2[i] = i;
	}
	int len, time;
	cin >> len >> time;
	cout << Divide(len, time) << endl;
}

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

三、智能指针的问题

cpp 复制代码
int main()
{
	//需要sp1和sp2共同管理资源,浅拷贝
	//但是析构多次的问题需要被解决
	SmartPtr<int> sp1 = new int[10];
	SmartPtr<int> sp2(sp1);

	return 0;
}

四、C++标准库的智能指针

C++标准库中的智能指针都在<memory>头文件

智能指针有好多种类型,除了weak_ptr,他们都符合RAII

原理上而言主要是解决智能指针拷贝时的思路不同

4.1.auto_ptr

C++98时设计出来的智能指针

拷贝时把被拷贝对象资源的管理权转移给拷贝对象

导致被拷贝的对象悬空,发生访问报错的问题,不建议使用

cpp 复制代码
struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	
	~Date()
	{
		cout << "~Date()" << endl;
	}
};

int main()
{
	auto_ptr<Date> ap1(new Date);
	//拷贝时,管理权限转移,被拷贝对象ap1悬空
	auto_ptr<Date> ap2(ap1);

	//空指针访问:ap1对象已经悬空
	//ap1->_year++;
	return 0;
}

4.2.唯一指针(unique_ptr)

C++11时设计出来的智能指针

不支持拷贝,只支持移动

建议在不需要拷贝的场景使用

cpp 复制代码
int main()
{
	unique_ptr<Date> up1(new Date);

	//不支持拷⻉
	//unique_ptr<Date> up2(up1);
	
	//支持移动,但是移动后up1也悬空,所以使用移动要谨慎
	unique_ptr<Date> up3(move(up1));

	return 0;
}

4.3.共享指针(shared_ptr)

C++11时设计出来的智能指针

支持拷贝,也支持移动

可以在需要拷贝的场景时使用,底层用引用计数实现

cpp 复制代码
int main()
{
	shared_ptr<Date> sp1(new Date);

	//支持拷贝
	shared_ptr<Date> sp2(sp1);
	shared_ptr<Date> sp3(sp2);

	cout << sp1.use_count() << endl;
	sp1->_year++;
	cout << sp1->_year << endl;
	cout << sp2->_year << endl;
	cout << sp3->_year << endl;

    //支持移动
    //但是移动后sp1也悬空,所以使用移动要谨慎
    shared_ptr<Date> sp4(move(sp1));

	return 0;
}

4.4.弱指针(weak_ptr)

C++11时设计出来的智能指针

不支持RAII ,无法用它直接管理资源,解决share_ptr的循环引用缺陷

4.5.删除器

智能指针在析构时默认进行delete释放资源

如果不是new出来的资源,交给智能指针管理,析构时会崩溃

智能指针在构造时给一个删除器

**删除器:**一个可调用的对象,在对象中实现你想要的释放资源方式

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

struct Date
{
	int _year;
	int _month;
	int _day;
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

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

template < class T>
void DeleteArrayFunc(T * ptr)
{
	delete[] ptr;
}

template < class T>
class DeleteArray
{
public:
	void operator()(T * ptr)
	{
		delete[] ptr;
	}
};

class Fclose
{
public:
	void operator()(FILE * ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	}
};

int main()
{
	//这样实现程序会崩溃
	unique_ptr<Date> up1(new Date[10]);
	shared_ptr<Date> sp1(new Date[10]);
	
	//解决方案1:特化
	
	//因为new[]经常使用,所以unique_ptr和shared_ptr
	//实现了一个特化版本,这个特化版本析构时用的delete[]
	unique_ptr<Date[]> up1(new Date[5]);
	shared_ptr<Date[]> sp1(new Date[5]);
	
	//解决方案2:删除器
	
	//仿函数对象做删除器
	//unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());
	//unique_ptr和shared_ptr支持删除器的方有所不同
	//unique_ptr是在类模板参数支持的
	//shared_ptr是构造函数参数支持的
	//使用仿函数unique_ptr可以不在构造函数传递
	//因为仿函数类型构造的对象直接就可以调用
	//但是下面的函数指针和lambda的类型不可以
	unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
	shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
	
	//函数指针做删除器
	unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
	shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
	
	//lambda表达式做删除器
	auto delArrOBJ = [](Date* ptr) {delete[] ptr;};
	unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
	shared_ptr<Date> sp4(new Date[5], delArrOBJ);
	
	//实现其他资源管理的删除器
	shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
	shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
	cout << "fclose:" << ptr << endl;
	fclose(ptr);});
	
	return 0;
}

4.6.make_share

cpp 复制代码
int main()
{
	shared_ptr<Date> sp1(new Date(2024, 9, 11));
	shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
	auto sp3 = make_shared<Date>(2024, 9, 11);
	shared_ptr<Date> sp4;
	
	// if (sp1.operator bool())
	if (sp1)
	cout << "sp1 is not nullptr" << endl;
	
	if (!sp4)
	cout << "sp1 is nullptr" << endl;
	
	// 报错
	shared_ptr<Date> sp5 = new Date(2024, 9, 11);
	unique_ptr<Date> sp6 = new Date(2024, 9, 11);
	
	return 0;
}

五、智能指针的原理

5.1.auto_ptr原理

拷贝时转移资源管理权给被拷贝的对象

cpp 复制代码
template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		//管理权转移
		sp._ptr = nullptr;
	}

	auto_ptr<T>& operator=(auto_ptr<T>& ap)
	{
		//检测是否为自己给自己赋值
		if (this != &ap)
		{
			//释放当前对象中资源
			if (_ptr)
			{
				delete _ptr;
			}

			//转移ap中资源到当前对象中
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}

		return *this;
	}

	~auto_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}

	//像指针一样使用
	T& operator*()
	{
		return *_ptr;
	}

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

5.2.unique_ptr原理

不支持拷贝,只支持移动

cpp 复制代码
template <class T>
class unique_ptr
{
public:
	explicit unique_ptr(T * ptr)
		:_ptr(ptr)
	{}
	
	~unique_ptr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
	}
	
	// 像指针一样使用
	T & operator*()
	{
		return *_ptr;
	}
	
	T * operator->()
	{
		return _ptr;
	}
	
	unique_ptr(const unique_ptr<T>&sp) = delete;
	unique_ptr<T>&operator=(const unique_ptr<T>&sp) = delete;
	unique_ptr(unique_ptr<T> && sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}

	unique_ptr<T>&operator=(unique_ptr<T> && sp)
	{
		delete _ptr;
		_ptr = sp._ptr;
		sp._ptr = nullptr;
	}
private:
	T * _ptr;
};

5.3.share_ptr原理

设计引用计数,一份资源就需要一个引用计数

所以引用计数用静态成员的方式是无法实现的

要使用堆上动态开辟的方式

构造智能指针对象时来一份资源,就要new一个引用计数

增加share_ptr指向资源就++引用计数

当shared_ptr对象析构时就--引用计数

引用计数减到0时代表当前析构的share_ptr是最后一个管理资源的对象,则析构资源

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

template < class T>
class shared_ptr
{
public:
	explicit shared_ptr(T * ptr = nullptr)
		: _ptr(ptr)
		,_pcount(new int(1))
	{}
	
	template<class D>
	shared_ptr(T * ptr, D del)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_del(del)
	{}
	
	shared_ptr(const shared_ptr<T>&sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		,_del(sp._del)
	{
		++(*_pcount);
	}
	
	void release()
	{
		if (--(*_pcount) == 0)
		{
			// 最后一个管理的对象,释放资源
			_del(_ptr);
			delete _pcount;
			_ptr = nullptr;
			_pcount = nullptr;
		}
	}
	
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();
			
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
			_del = sp._del;
		}
		
		return *this;
	}
	
	~shared_ptr()
	{
		release();
	}
	
	T * get() const
	{
		return _ptr;
	}
	
	int use_count() const
	{
		return *_pcount;
	}
	
	T & operator*()
	{
		return *_ptr;
	}

	T * operator->()
	{
		return _ptr;
	}
private:
	T * _ptr;
	int* _pcount;
	function<void(T*)> _del = [](T* ptr) {delete ptr;};
};

5.4.weak_ptr原理

weak_ptr不支持RAII,也不支持访问资源

绑定到share_ptr时,不增加share_ptr的引用计数,可以解决循环引用问题

cpp 复制代码
template < class T>
class weak_ptr
{
public:
	weak_ptr()
	{}
	
	weak_ptr(const shared_ptr<T>&sp)
		: _ptr(sp.get())
	{}
	
	weak_ptr<T>&operator=(const shared_ptr<T>&sp)
	{
		_ptr = sp.get();
		
		return *this;
	}
private:
	T* _ptr = nullptr;
};

int main()
{
	std::shared_ptr<string> sp1(new string("111111"));
	std::shared_ptr<string> sp2(sp1);

	std::weak_ptr<string> wp = sp1;
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	// sp1和sp2都指向了其他资源,则weak_ptr就过期了
	sp1 = make_shared<string>("222222");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	sp2 = make_shared<string>("333333");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	wp = sp1;
	//std::shared_ptr<string> sp3 = wp.lock();
	auto sp3 = wp.lock();
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	*sp3 += "###";
	cout << *sp1 << endl;

	return 0;
}

六、share_ptr循环引用问题

cpp 复制代码
struct ListNode
{
	int _data;
	
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;
		
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	// 循环引⽤ -- 内存泄露
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	
	n1->_next = n2;
	n2->_prev = n1;
	
	return 0;
}

右边的节点什么时候释放

左边节点中的_next管理右边节点,_next析构后,右边的节点释放

_next什么时候析构

_next是左边节点的成员,左边节点释放后,_next析构

左边的节点什么时候释放

右边节点中的_prev管理左边节点,_prev析构后,左边的节点释放

_prev什么时候析构

_prev是右边节点的成员,右边节点释放后,_prev析构

以上逻辑形成了循环引用,导致内存泄漏问题

weak_ptr可以解决这个问题

cpp 复制代码
struct ListNode
{
	int _data;

	//这里改成weak_ptr,当n1->_next = n2,绑定shared_ptr时
    //不增加n2的引⽤计数,不参与资源释放的管理,就不会形成循环引用
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
		
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode);
	std::shared_ptr<ListNode> n2(new ListNode);

	n1->_next = n2;
	n2->_prev = n1;
	
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	//weak_ptr不支持管理资源,不支持RAII
	//weak_ptr是专⻔绑定shared_ptr,不增加引用计数,作为⼀些场景的辅助管理
	std::weak_ptr<ListNode> wp(new ListNode);

	return 0;
}

七、share_ptr的线程安全问题

share_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中

进行share_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题

所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全

share_ptr指向的对象也是有线程安全问题的,但这个对象的线程安全不归share_ptr管,也管不了

下面的程序会崩溃,或者A资源没释放

bit::share_ptr引用计数从int*改成atomic<int>*就可以保证引用计数的线程安全问题

cpp 复制代码
struct AA
{
	int _a1 = 0;
	int _a2 = 0;
	~AA()
	{
		cout << "~AA()" << endl;
	}
};

int main()
{
	bit::shared_ptr<AA> p(new AA);
	const size_t n = 100000;
	mutex mtx;
	auto func = [&]()
	{
		for (size_t i = 0; i < n; ++i)
		{
			//这里智能指针拷贝会++计数
			bit::shared_ptr<AA> copy(p);
			{
				unique_lock<mutex> lk(mtx);
				copy->_a1++;
				copy->_a2++;
			}
		}
	};
	thread t1(func);
	thread t2(func);
	t1.join();
	t2.join();
	cout << p->_a1 << endl;
	cout << p->_a2 << endl;
	cout << p.use_count() << endl;
	return 0;
}

八、内存泄漏

8.1.内存泄漏的概念

因为疏忽或错误造成程序未能释放已经不再使用的内存

一般是忘记释放或者发生异常释放程序未能执行导致

并不是指内存在物理上的消失

而是应用程序分配某段内存后

因为设计错误,失去对该段内存的控制,造成内存浪费

8.2.内存泄漏的危害

普通程序运行一会就结束,出现内存泄漏问题不大

进程正常结束,页表的映射关系解除,物理内存也可以释放

长期运行的程序出现内存泄漏,影响很大

如操作系统、后台服务、长期运行的客户端等

不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死

cpp 复制代码
int main()
{
    //申请⼀个1G未释放,这个程序多次运行也没啥危害
    //因为程序马上就结束,进程结束各种资源也就回收了
    char* ptr = new char[1024 * 1024 * 1024];
    cout << (void*)ptr << endl;
    return 0;
}

8.3.内存泄漏的避免

申请的内存空间记得匹配的去释放

用智能指针管理资源,采用RAII思想自己造轮子处理

定期使用内存泄漏工具检测

相关推荐
jinanwuhuaguo2 小时前
OpenClaw 2026.4.5 深度解读
android·开发语言·人工智能·kotlin·openclaw
小小马喽_Thendras2 小时前
ScheduledExecutorService 和Timer的区别
java·开发语言
后藤十八里2 小时前
极验4滑动拼图验证码逆向笔记
笔记·爬虫·python
报错小能手2 小时前
ios开发方向——swift内存基础
开发语言·ios·swift
minji...2 小时前
Linux 多线程(四)线程等待,线程分离,线程管理,C++多线程,pthread库
linux·运维·开发语言·网络·c++·算法
麦德泽特2 小时前
基于 Go 语言的 Modbus 项目实战:构建高性能、可扩展的工业通信服务器
服务器·开发语言·golang·modbus·rtu
Keep Running *2 小时前
Angular_学习笔记
笔记·学习·angular.js
H_BB2 小时前
DFS实现回溯算法
数据结构·c++·算法·深度优先
还是大剑师兰特2 小时前
pnpm format 什么作用
开发语言·javascript·ecmascript