网站首页 全球最实用的IT互联网站!

人工智能P2P分享Wind搜索发布信息网站地图标签大全

当前位置:诺佳网 > 人工智能 > AI通用技术 >

C++进阶9:异常和智能指针

时间:2026-03-18 19:36

人气:

作者:admin

标签:

导读:本文摘要主要分为异常处理和智能指针两部分内容。异常处理部分详细介绍了异常的概念、捕获与抛出机制、栈展开过程、匹配处理代码查找、异常重新抛出、异常安全问题及异常规范...

目录

一、异常的概念及使用

1.1异常的概念

1.2异常的捕获和抛出

1.3栈展开

1.4查找匹配的处理代码

1.5异常重新抛出

1.6异常安全问题

1.7异常规范

二、智能指针的使用及其原理

2.1智能指针使用场景分析

2.2RAII和智能指针的设计思路

2.3C++标准库智能指针的使用

2.4删除器

2.5智能指针的原理

2.6shared_ptr的线程安全问题

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

2.8内存泄漏

2.8.1什么是内存泄漏,内存泄漏的危害

2.8.2如何检测内存泄漏

2.8.3如何避免内存泄漏


一、异常的概念及使用

1.1异常的概念

异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并做出相应的处理, 异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后 解决问题的任务传递给程序的另⼀部分,检测环节无须知道问题的处理模块的所有细节。

C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以 后还要去查询错误信息,比较麻烦。异常时抛出一个对象,这个对象可以函数更全面的各种信息。

1.2异常的捕获和抛出

程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常。

被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那⼀个。根据抛出对象的 类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误。

当throw执行时,throw后面的语句将不再被执行。程序的执行从throw位置跳到与之匹配的catch模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调用链中另⼀个函数中的catch,控制权从throw位置转移到了catch位置。这里还有两个重要的含义:1、沿着调用链的函数可能提早退出。2、⼀旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。

抛出异常对象后,会生成⼀个异常对象的拷贝,因为抛出的异常对象可能是⼀个局部对象,所以会生成⼀个拷贝对象,这个拷贝的对象会在catch子句后销毁。(这里的处理类似于函数的传值返 回)

1.3栈展开

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否 在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。

如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续 在外层调用函数链中查找,上述查找的catch过程被称为栈展开。

如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的 terminate 函数终止程序。 如果找到匹配的catch子句处理后,catch子句代码会继续执行。

Divide 函数中,当 b == 0 时,抛出的是 std::string 类型 异常,但 Divide 内部的 catch 块只捕获 int 类型Func 函数的 catch 块只捕获 const char* 类型只有 main 函数的 catch (...) 能捕获所有未被处理的异常。当除以 0 时,string 异常会穿透 DivideFunc,最终被 maincatch (...) 捕获,输出 “未知异常”。

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

			cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
		}
		else
		{
			return ((double)a / (double)b);
		}
	}
	catch (int errid)
	{
		cout << errid << endl;
	}

	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;

	return 0;
}

void Func()
{
	int len, time;
	cin >> len >> time;

	try
	{
		cout << Divide(len, time) << endl;
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}

int main()
{
	while (1)
	{
		try
		{
			Func();
		}
		catch (const char* errmsg)
		{
			cout << errmsg << endl;
		}
		catch (...) //  任意类型的异常
		{
			cout << "未知异常" << endl;
		}
	}

	return 0;
}

1.4查找匹配的处理代码

⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的 那个。 但是也有⼀些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点⾮常实用,实际中继承体系基本都是用这个方式设计的。

如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以⼀般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是是 不知道异常错误是什么。

#include<thread>

// 一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块
// 每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据
// 最后捕获时,我们捕获基类就可以
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}

	virtual string what() const
	{
		return _errmsg;
	}

	int getid() const
	{
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};

class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}

	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};

class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}

	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};

class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}

	virtual string what() const
	{
		string str = "HttpException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}

private:
	const string _type;
};

void SQLMgr()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	else
	{
		cout << "SQLMgr 调用成功" << endl;
	}
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	else
	{
		cout << "CacheMgr 调用成功" << endl;
	}

	SQLMgr();
}

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		throw HttpException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 101, "post");
	}
	else
	{
		cout << "HttpServer调用成功" << endl;
	}

	CacheMgr();
}

int main()
{
	srand(time(0));

	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));

		try
		{
			HttpServer();
		}
		//无论抛出 SqlException/CacheException/HttpException,都会被这个 catch 块捕获
		catch (const Exception& e) 
		{
			// 多态调用
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

1.5异常重新抛出

有时catch到⼀个异常对象后,需要对错误进行分类,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。

// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在
// 电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发
// 送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的
// 错误,捕获后也要重新抛出。
void _SendMsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw HttpException("网络不稳定,发送失败", 102, "put");
	}
	else if (rand() % 7 == 0)
	{
		throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

void SendMsg(const string& s)
{
	// 发送消息失败,则再重试3次
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_SendMsg(s);
			break;
		}
		catch (const Exception& e)
		{
			// 捕获异常,if中是102号错误,网络不稳定,则重新发送
			// 捕获异常,else中不是102号错误,则将异常重新抛出
			if (e.getid() == 102)
			{
				// 重试三次以后否失败了,则说明网络太差了,重新抛出异常
				if (i == 3)
					throw;

				cout << "开始第" << i + 1 << "重试" << endl;
			}
			else
			{
				// 重新抛出
				throw;
			}
		}
	}
}

int main()
{
	srand(time(0));
	string str;
	while (cin >> str)
	{
		try
		{
			SendMsg(str);
		}
		catch (const Exception& e)
		{
			cout << e.what() << endl << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}

	return 0;
}

1.6异常安全问题

异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可 能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我 们需要捕获异常,释放资源后面再重新抛出。

其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛 出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。

double Divide(int a, int b)
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。 
	// 所以这里捕获异常后并不处理异常,异常还是交给外层处理,这里捕获了再重新抛出去。 
	int* array = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	}
	catch (...)
	{
		// 捕获异常释放内存 
		cout << "delete []" << array << endl;
		delete[] array;
		throw; // 异常重新抛出,捕获到什么抛出什么 
	}
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception" << endl;
	}
	return 0;
}

1.7异常规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异 常有助于简化调用函数的代码。

C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1, 类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。C++98的方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加 noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。

编译器并不会在编译时检查noexcept,也就是说如果⼀个函数用noexcept修饰了,但是同时又包 含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会 报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调用 terminate 终止程序。

noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回 false,不会就返回true。

二、智能指针的使用及其原理

2.1智能指针使用场景分析

下⾯程序中我们可以看到,new了以后,我们也delete了,但是因为抛异常导,后⾯的delete没有得到执行,所以就内存泄漏了,所以我们需要new以后捕获异常,捕获到异常后delete内存,再把异常抛出,但是因为new本身也可能抛异常,连续的两个new和下面的Divide都可能会抛异常,让我们处理起来很麻烦。智能指针放到这样的场景里面就让问题简单多了。

这里如果Divide发生除0错误抛出异常,下面的array1和array2会得不到释放,所以这里捕获异常后不处理,重新抛出,但是如果array2 new的时候抛异常就还需要套⼀层捕获释放逻辑。所以我们就应该使用智能指针。

double Divide(int a, int b)
{
	// 当b == 0时抛出异常 
	if (b == 0)
	{
		throw "Divide by zero condition!";
	}
	else
	{
		return (double)a / (double)b;
	}
}
void Func()
{
	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;
}

2.2RAII和智能指针的设计思路

RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是 ⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指 针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问, 资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常 释放,避免资源泄漏问题。

智能指针类除了满足RAII的设计思路,还要方便资源的访问,所以智能指针类还会想迭代器类⼀ 样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

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()
{
	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;
}

2.3C++标准库智能指针的使用

C++标准库中的智能指针都在这个头文件下面,我们包含就可以是使用了, 智能指针有好几种,除了weak_ptr他们都符合RAII和像指针⼀样访问的行为,原理上而言主要是解解决智能指针拷贝时的思路不同。

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给 拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计 出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁⽌使用 这个智能指针的。

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;
}

unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯⼀指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。

int main()
{
	unique_ptr<Date> up1(new Date);
	// 不支持拷贝
	//unique_ptr<Date> up2(up1);
	// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎 
	//与右值比较相似
	unique_ptr<Date> up3(move(up1));

    return 0;
}

shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝, 也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

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;
}

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上⾯的智能指 针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr 的⼀个循环引用导致内存泄漏的问题


shared_ptr循环引用问题:

shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会 导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使 用weak_ptr解决这种问题。

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;
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	return 0;
}

n1和n2析构后,管理两个节点的引用计数减到1,右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。 _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。


2.4删除器

智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指 针管理,析构时就会崩溃。智能指针支持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调用对象,这个可调用对象中实现你想要的释放资源的⽅式,当构造智能指针时,给了定制的删除器, 在智能指针析构时就会调用删除器去释放资源。因为new[]经常使用,所以为了简洁⼀点, unique_ptr和shared_ptr都特化了⼀份[]的版本,使用时 unique_ptr up1(new Date[5]);shared_ptr sp1(new Date[5]); 就可以管理new[]的资源。

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);
	// 实现其他资源管理的删除器 
    //析构的时候del(_ptr)
	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;
}

shared_ptr 除了支持用指向资源的指针构造,还支持make_shared用初始化资源对象的值直接构造。shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。shared_ptr 和 unique_ptr 都得构造函数都使用explicit修饰,防止普通指针隐式类型转换成智能指针对象。

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;
}

2.5智能指针的原理

auto_ptr的思路是拷贝时转移资源管理权给被拷贝对象。

	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;
	};

unique_ptr的思路是不支持拷贝。

	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;
	};

shared_ptr重点在于引用计数的设计,主要这里⼀份资源就需要⼀个引用计数,所以引用计数才用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来⼀份资源,就要new⼀个引用计数出来。多个shared_ptr指向资源时就++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时代表当前析构的shared_ptr是最后一个管理资源的对象,则析构资源。

	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;
		//atomic<int>* _pcount;

		function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资 源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数。weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的 shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

	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;
	};

 注意:

我们这里实现的shared_ptr和weak_ptr都是以最简洁的方式实现的,只能满足基本的功能,这里的weak_ptr lock等功能是无法实现的,想要实现就要把shared_ptr和weak_ptr⼀起改了,把引用计数拿出来放到⼀个单独类型,shared_ptr和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码 。

int main()
{
	cqw::auto_ptr<Date> ap1(new Date);           
	cqw::auto_ptr<Date> ap2(ap1); 
	//ap1->_year++;

	cqw::unique_ptr<Date> up1(new Date);
	//unique_ptr<Date> up2(up1);
	cqw::unique_ptr<Date> up3(move(up1));

	cqw::shared_ptr<Date> sp1(new Date);
	cqw::shared_ptr<Date> sp2(sp1);
	cqw::shared_ptr<Date> sp3(sp2);
	cout << sp1.use_count() << endl;
	sp1->_year++;
	cout << sp1->_year << endl;
	cout << sp2->_year << endl;
	cout << sp3->_year << endl;

	return 0;
}
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;
}

2.6shared_ptr的线程安全问题

shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者 原子操作保证线程安全的。

shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr 管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。

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

Boost库是为C++语⾔标准库提供扩展的⼀些C++程序库的总称,Boost社区建⽴的初衷之⼀就是为 C++的标准化⼯作提供可供参考的实现,Boost社区的发起⼈Dawes本⼈就是C++标准委员会的成员之⼀。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。

2.8内存泄漏

2.8.1什么是内存泄漏,内存泄漏的危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,⼀般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:普通程序运行⼀会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

2.8.2如何检测内存泄漏

使用一些第三方工具

2.8.3如何避免内存泄漏

1、事前预防型,如智能指针等。

2、事后查错型,如泄漏检测工具。

温馨提示:以上内容整理于网络,仅供参考,如果对您有帮助,留下您的阅读感言吧!
相关阅读
本类排行
相关标签
本类推荐

CPU | 内存 | 硬盘 | 显卡 | 显示器 | 主板 | 电源 | 键鼠 | 网站地图

Copyright © 2025-2035 诺佳网 版权所有 备案号:赣ICP备2025066733号
本站资料均来源互联网收集整理,作品版权归作者所有,如果侵犯了您的版权,请跟我们联系。

关注微信