当前位置: 首页 > article >正文

(学习总结21)C++11 异常与智能指针

C++11 异常与智能指针

  • 异常
    • 异常的概念
    • 异常的抛出和捕获
    • 栈展开
    • 查找匹配的处理代码
    • 异常重新抛出
    • 异常安全问题
    • 异常规范
    • 标准库的异常
  • 智能指针
    • RAII 和智能指针的设计思路
    • 智能指针的使用场景分析
    • C++标准库智能指针的使用
    • weak_ptr 和 shared_ptr循环引用
      • weak_ptr
      • shared_ptr 循环引用问题
    • 智能指针的原理
      • auto_ptr 与 unique_ptr 简单实现
      • shared_ptr 与 weak_ptr 简单实现
    • shared_ptr 的线程安全问题
    • C++11 和 Boost 库中智能指针的关系
    • 内存泄漏
      • 内存泄漏概念与其危害
      • 如何检测内存泄漏
      • 如何避免内存泄漏

以下代码环境为 VS2022 C++

异常

异常的概念

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

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

异常的抛出和捕获

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

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

当 throw 执行时,throw 后面的语句将不再被执行。程序的执行从 throw 位置跳到与之匹配的 catch 模块,catch 可能是同一函数中的一个局部的 catch,也可能是调用链中另一个函数中的 catch,控制权从 throw 位置转移到了 catch 位置。这里还有两个重要的含义:

  1. 沿着调用链的函数可能提早退出

  2. 一旦程序开始执行异常处理程序,沿着调用链创建的对象都将销毁。

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

#include <iostream>
using namespace std;

class temp
{
public:

	temp()
	{
		cout << "temp 构造函数执行" << endl;
	}

	~temp()
	{
		cout << "temp 析构函数执行" << endl;
	}
};

void test()
{
	temp one;		// 只要函数栈帧销毁就会执行析构函数
					// 不用担心对象的资源泄漏
	try
	{
		throw string("func1 出现异常");
		//throw 1.5;
		//throw 5;

		cout << "调用 throw 语句后以下的程序不执行" << endl;
	}
	catch (int abnormal)
	{
		cout << "在 func1 捕获 int 类型异常" << endl;
	}

	cout << "func1 的 catch 类型不匹配 string,不会捕获异常,则接下来的这部分也不会执行" << endl;
}

int main()
{
	try
	{
		test();
	}
	catch (double abnormal)				// 同一个函数中 catch 允许出现多个,变量名可以相同
	{
		cout << "在 main 捕获 double 类型异常" << endl;
	}
	catch (const string& abnormal)
	{
		cout << abnormal << endl;
		cout << "在 main 第二个 catch 分支中捕获 string 类型异常" << endl;
	}
	//catch (const string& abnormal)	// 但是同一个函数中不允许出现相同的类型,会报错
	//{
	//	cout << abnormal << endl;
	//	cout << "在 main 第三个 catch 分支中捕获 string 类型异常" << endl;
	//}
	//catch (string abnormal)			// 报错
	//{
	//	cout << abnormal << endl;
	//	cout << "在 main 第四个 catch 分支中捕获 string 类型异常" << endl;
	//}

	cout << "main 的 catch 类型匹配,会捕获异常,则接下来的这部分会执行" << endl;

	return 0;
}

栈展开

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

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

如果到达 main 函数,依旧没有找到匹配的 catch 子句,程序会调用标准库的 terminate 函数终止程序。

如果找到匹配的 catch 子句处理后,catch 子句代码会继续执行。

在这里插入图片描述

#include <iostream>
using namespace std;

int divide(int left, int right)
{
	if (right == 0)	// 当前函数没有 try / catch 语句时,执行 throw 会直接退出当前函数
	{
		throw string("程序错误,除数为零,请重新输入");
	}
	else
	{
		return left / right;
	}
}

void func()
{
	int left = 0;
	int right = 0;
	cin >> left >> right;

	try
	{
		cout << divide(left, right) << endl;
	}
	catch (double abnormal)
	{
		cout << abnormal << endl;
	}
}

int main()
{
	while (true)
	{
		try
		{
			func();
		}
		catch (const string& abnormal)
		{
			cout << abnormal << endl;
		}
	}

	return 0;
}

查找匹配的处理代码

一般情况下抛出对象和 catch 是类型完全匹配的,如果有多个类型匹配的,就选择离它位置更近的那个。

但是也有一些例外,允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换,这个点非常实用,实际中继承体系基本都是用这个方式设计的。

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

#include <iostream>
#include <Windows.h>
using namespace std;

class Exception
{
protected:

	string _errmsg;

public:

	Exception(const string& errmsg)
		:_errmsg(errmsg)
	{
		;
	}

	virtual string what() const
	{
		return _errmsg;
	}
};

class clothesException : public Exception
{
public:

	clothesException(const string& errmsg)
		:Exception(errmsg)
	{
		;
	}

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

class pantsException : public Exception
{
public:

	pantsException(const string& errmsg)
		:Exception(errmsg)
	{
		;
	}

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

class shoesException : public Exception
{
public:

	shoesException(const string& errmsg)
		:Exception(errmsg)
	{
		;
	}

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

void operate()
{
	int randNum = rand() % 3;

	if (randNum == 0)
	{
		throw clothesException("衣服");
	}
	else if (randNum == 1)
	{
		throw pantsException("裤子");
	}
	else
	{
		throw shoesException("鞋子");
	}
}

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

	while (true)
	{
		try
		{
			operate();
		}
		catch (const Exception& abnormal)	// 这里不仅会捕获基类,还会捕获派生类
		{
			cout << abnormal.what() << endl << endl;
		}
		catch (...)							// 捕获任意类型的异常
		{
			cout << "未知错误" << endl;
		}

		Sleep(1000);
	}

	return 0;
}

异常重新抛出

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

#include <iostream>
#include <Windows.h>
using namespace std;

// 下面程序模拟聊天发送信息情况,网络差时进行重试,重试后无果则再次抛出异常
// 若不是网络问题,也需要抛出异常
class Exception
{
	string _errmsg;
	int _id;

public:

	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		,_id(id)
	{
		;
	}

	string what() const
	{
		return _errmsg;
	}

	int getID() const
	{
		return _id;
	}
};

void send(const string& info)
{
	if (rand() % 3 == 0)
	{
		throw Exception("网络不稳定,发送失败", 101);
	}
	else if (rand() % 3 == 1)
	{
		throw Exception("你已不是对方好友,发送失败", 102);
	}
	else
	{
		cout << "发送成功" << endl;
	}
}

void operate()
{
	string str;
	cin >> str;

	for (int i = 1; i <= 3; ++i)
	{
		try
		{
			send(str);
			break;							// 若无异常,则发送成功,直接跳出循环
		}
		catch (const Exception& abnormal)	// 捕获异常
		{
			if (abnormal.getID() == 101)	// 这里规定 101 为网络不稳定,需重试
			{
				if (i == 3)					// 重试 2 次后无效则抛出异常
				{
					throw;					// 将上次的异常抛出
				}

				cout << "开始第 " << i << " 次尝试" << endl;
				Sleep(500);
			}
			else							// 这里规定 102 为两者关系非好友,抛出 
			{
				throw;						// 将上次的异常抛出
			}
		}
	}
}

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

	while (true)
	{
		try
		{
			operate();
		}
		catch (const Exception& abnormal)	// 再次捕获异常并打印信息
		{
			cout << abnormal.what() << endl;
		}
		catch (...)
		{
			cout << "未知异常" << endl;
		}
	}

	return 0;
}

异常安全问题

异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。中间我们需要捕获异常,释放资源后面再重新抛出,一般释放的方法很繁琐也不易维护,下面的智能指针部分讲的 RAII 方式能更好的解决这种问题。

其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放 10 个资源,释放到第 5 个时抛出异常,则也需要捕获处理,否则后面的 5 个资源就没释放,也资源泄漏了。《Effctive C++》第8个条款也专门讲了这个问题,别让异常逃离析构函数。

#include <iostream>
using namespace std;

void func2()
{
	if (rand() % 3 == 0)
	{
		;
	}
	else if (rand() % 3 == 1)
	{
		throw 1;
	}
	else
	{
		throw string("1111");
	}
}

void func1()
{
	// 可以看到,异常的出现
	// 使得一般释放资源的方法太麻烦了

	int* p1 = new int[10];			// 如果 int* p2 = new int[10]; 的 new 申请时发生异常
	int* p2 = new int[10];			// 程序就不会经过下面的资源释放
									// 使得 int* p1 的资源泄漏;

	// 则需要这样处理
	//int* p1 = new int[10];
	//try
	//{
	//	int* p2 = new int[10];		// 当 new int[10] 发生异常
	//}
	//catch (...)
	//{
	//	cout << "delete[] p1" << endl;
	//	delete[] p1;				// 将 p1 的资源释放

	//	throw;
	//}

	try
	{
		func2();
	}
	catch (int a)					// 若程序成功匹配这里,再次抛出需要释放	
	{							
		cout << "delete[] p1" << endl;
		cout << "delete[] p2" << endl;
		delete[] p1;
		delete[] p2;

		throw;						// 继续抛出
	}
	catch (string& b)				// 若程序成功匹配这里,再次抛出需要释放
	{
		cout << "delete[] p1" << endl;
		cout << "delete[] p2" << endl;
		delete[] p1;
		delete[] p2;

		throw;						// 继续抛出
	}
	// ...
									// 若程序无异常需要正常释放
	cout << "delete[] p1" << endl;
	cout << "delete[] p2" << endl;
	delete[] p1;
	delete[] p2;
}

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

	try
	{
		func1();
	}
	catch (...)
	{
		cout << "出现异常" << endl;
	}

	return 0;
}

异常规范

对于用户和编译器而言,预先知道某个程序是否会抛出异常很有帮助,若某个函数不会抛出异常编译器就可以简化调用该函数的代码。

C++98 中函数参数列表的后面接 throw(),表示函数不抛异常,函数参数列表的后面接 throw(类型1, 类型2…) 表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

C++98 的方式这种方式过于复杂,实践中并不好用,C++11 中进行了简化,函数参数列表后面加 noexcept 表示不会抛出异常,啥都不加表示可能会抛出异常

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

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

#include <iostream>
using namespace std;

void func1() throw()			// C++98 无异常表示
{
	;
}

void func2() noexcept			// C++11 无异常表示
{								// 也可写为 noexcept(true)
	;
}

void func3() throw(int, string)	// C++98 可能有异常表示
{
	;
}

void func4() noexcept(false)	// C++11 也可以显示表示有异常
{
	;
}

void func()						// C++11 不写 noexcept 表示可能有异常
{
	func1();
	func2();

	throw string("1111111");
}

int main()
{
	try
	{
		func();
	}
	catch (...)
	{
		cout << "发生异常" << endl;
	}

	int num = 0;
	cout << "++num   是否无异常: " << noexcept(++num) << endl;
	cout << "func()  是否无异常: " << noexcept(func()) << endl;
	cout << "func1() 是否无异常: " << noexcept(func1()) << endl;
	cout << "func2() 是否无异常: " << noexcept(func2()) << endl;
	cout << "func3() 是否无异常: " << noexcept(func3()) << endl;
	cout << "func4() 是否无异常: " << noexcept(func4()) << endl;
	cout << "main()  是否无异常: " << noexcept(main()) << endl;

	return 0;
}

标准库的异常

C++标准库也定义了一套自己的一套异常继承体系库,基类是 exception,所以我们日常写程序,需要就在主函数捕获 exception 即可,要获取异常信息,调用 what 函数,what 是一个虚函数,派生类可以重写。
可以参考:std::exception

智能指针

RAII 和智能指针的设计思路

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

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

智能指针的使用场景分析

在异常的异常安全问题部分讨论的一般释放资源的方法过于复杂,我们可以使用智能指针来简化这个问题。下面程序中我们可以看到,new 以后,也 delete 了。不管异常发生在何处,即便 new 申请空间也出现抛异常,销毁当前函数时都能保证正常的释放资源,防止了内存泄漏,让资源管理变得更轻松。

#include <iostream>
using namespace std;

template<class T>
class SmartPtr
{
public:

	SmartPtr(T* ptr_arr)
		:_ptr_arr(ptr_arr)
	{
		;
	}

	~SmartPtr()					// 析构时释放资源
	{
		if (_ptr_arr != nullptr)
		{
			cout << "delete[] _ptr_arr" << endl;
			delete[] _ptr_arr;
			_ptr_arr = nullptr;
		}
	}

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

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

	T& operator[](size_t index)
	{
		return _ptr_arr[index];
	}

private:

	T* _ptr_arr = nullptr;			// 指向数组的指针
};

void func2()
{
	if (rand() % 3 == 0)
	{
		;
	}
	else if (rand() % 3 == 1)
	{
		throw 1;
	}
	else
	{
		throw string("1111");
	}
}

void func1()
{
	SmartPtr<int> p1 = new int[10];	// 当函数销毁时自动释放资源
	SmartPtr<int> p2 = new int[10];

	try
	{
		func2();
	}
	catch (int a)
	{
		throw;						// 继续抛出
	}
	catch (string& b)
	{
		throw;						// 继续抛出
	}
	// ...
}

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

	try
	{
		func1();
	}
	catch (...)
	{
		cout << "出现异常" << endl;
	}

	return 0;
}

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

C++ 标准库中的智能指针都在 <memory> 这个头文件中,包含头文件 <memory> 就可以是使用。智能指针有好几种,除了 weak_ptr 它们都符合 RAII 和像指针一样访问的行为,不同之处在于解决智能指针拷贝时的思路:

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

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

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

  4. weak_ptr 是 C++11 设计出来的智能指针,它的名字翻译出来是弱指针,其完全不同于上面的智能指针,它不支持 RAII,也就意味着不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。具体细节下面再展开。

智能指针析构时默认是进行 delete 释放资源,这也就意味着如果不是 new 出来的资源交给智能指针管理,析构时就会崩溃

智能指针支持在构造时给一个删除器,所谓删除器本质就是一个可调用对象,在这个可调用对象中实现自己想要释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为 new[] 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份 delete[] 的版本

shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值
直接构造。

shared_ptr 和 unique_ptr 都支持 operator bool() 的类型转换,如果智能指针对象是一个
空对象没有管理资源,则返回 false,否则返回 true,意味着我们可以直接把智能指针对象给 if 判断是否为空。

shared_ptr 和 unique_ptr 的构造函数都使用 explicit 修饰,防止普通指针隐式类型转换
成智能指针对象。

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

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

void print_ptr_arr(shared_ptr<int[]>& arr, int size)
{
	for (int i = 0; i < size; ++i)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}

int main()
{
	auto_ptr<int> p1(new int(5));
	// auto_ptr 在 p2 拷贝时导致 p1 悬空,使用 p1 访问资源时为非法访问
	auto_ptr<int> p2(p1);
	//cout << *p1 << endl;	// 报错


	unique_ptr<int[]> p3(new int[10]);		// unique_ptr<int[]> 可以让智能指针指向 int 型数组并正确释放

	// unique_ptr 不允许拷贝,可以移动,移动后 p3 悬空 
	//unique_ptr<int> p4(p3);
	unique_ptr<int[]> p4(move(p3));


	shared_ptr<int[]> p5(new int[10]{0});
	//shared_ptr<int[]> p5 = new int[10]{0};// shared_ptr 和 unique_ptr 不支持普通指针隐式类型转换

	// shared_ptr 可以拷贝,可以移动,
	// 多个智能指针可以共享一个资源
	shared_ptr<int[]> p6 = p5;
	shared_ptr<int[]> p7 = move(p6);		// p6 移动后悬空

	cout << "p5[] = ";
	print_ptr_arr(p5, 10);

	cout << "p7[] = ";
	print_ptr_arr(p7, 10);

	p5[1] = 100;	// p5 修改资源,p7 使用时也会受到影响
	p5[7] = 33;

	cout << "p5[] = ";
	print_ptr_arr(p5, 10);

	cout << "p7[] = ";
	print_ptr_arr(p7, 10);
	cout << endl;

	cout << "多少个 shared_ptr 指针指向当前资源:" << p7.use_count() << endl << endl;

	if (p6)
	{
		cout << "p6 指向了资源" << endl;
	}
	else
	{
		cout << "p6 指针悬空" << endl;
	}
	cout << endl;

	shared_ptr<int> p8 = make_shared<int>(5);
	cout << *p8 << endl << endl;

	return 0;
}
#define _CRT_SECURE_NO_WARNINGS 1

#include <iostream>
#include <stdio.h>
#include <memory>
using namespace std;

class number
{
public:

	number(int num1, float num2)
		:_one(num1)
		, _two(num2)
	{
		;
	}

	number() = default;

private:

	int _one = 0;
	float _two = 1.0f;
};

void delete_number_arr(number* p)
{
	delete[] p;
}

int main()
{
	unique_ptr<number[]> u1(new number[5]);
	shared_ptr<number[]> p1(new number[5]);

	// 除了特化 [] 方式还可以使用定制删除器
	// unique_ptr 的删除器要求传在模版类参数中,但函数指针与lambda还要传构造参数,
	// shared_ptr 的删除器要求传在构造参数中

	// 函数指针方式做删除器
	unique_ptr<number[], void(*)(number*)> u2(new number[5], &delete_number_arr);
	shared_ptr<number[]> p2(new number[5], &delete_number_arr);

	// lambda表达式做删除器
	// decltype 关键字可以识别类型
	auto func_delete = [](number* p) { delete[] p; };
	unique_ptr<number[], decltype(func_delete)> u3(new number[5], func_delete);
	shared_ptr<number[]> p3(new number[5], [](number* p) {delete[] p; });

	// 其他资源管理的删除器
	unique_ptr<FILE, int(*)(FILE*)> u4(fopen("test.cpp", "r"), &fclose);
	shared_ptr<FILE> p4(fopen("test.cpp", "r"), [](FILE* fp) { 
		cout << "fclose:" << fp << endl;
		fclose(fp); });

	return 0;
}

weak_ptr 和 shared_ptr循环引用

weak_ptr

weak_ptr 不支持 RAII,也不支持访问资源,在文档 weak_ptr 可以注意到它构造时不支持绑定到资源,只支持绑定到 shared_ptr。绑定到 shared_ptr 时,不增加 shared_ptr 的引用计数,则可以解决 shared_ptr 的循环引用问题

weak_ptr 也没有重载 operator* 、operator-> 等,因为它不参与资源管理,若它绑定的 shared_ptr 已经释放资源,它再去访问资源就是很危险的。

weak_ptr 支持 expired 检查指向的资源是否过期,过期为 true,反之为 false,use_count 也可获取 shared_ptr 的引用计数。weak_ptr 想访问资源时,可以调用 lock 返回一个管理资源的 shared_ptr,如果资源已经被释放,返回的 shared_ptr 是一个空对象,如果资源没有释放,则通过返回的 shared_ptr 访问资源是安全的。

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

int main()
{
	shared_ptr<int> p1(new int(100));
	shared_ptr<int> p2 = p1;
	cout << "p1 == " << *p1 << endl << endl;

	weak_ptr<int> w1 = p1;

	cout << w1.expired() << endl;
	cout << w1.use_count() << endl << endl;

	p1 = make_shared<int>(200);
	cout << w1.expired() << endl;
	cout << w1.use_count() << endl << endl;

	p2 = p1;			// 当 p1, p2 指向其他资源时,则 w1 过期了
	cout << w1.expired() << endl;
	cout << w1.use_count() << endl << endl;

	shared_ptr<int> p3 = w1.lock();

	if (p3)
	{
		cout << "p3 有资源" << endl;
	}
	else
	{
		cout << "p3 没有资源" << endl;
	}

	w1 = p2;
	shared_ptr<int> p4 = w1.lock();

	cout << "p4 == " << *p4 << endl << endl;
	cout << w1.expired() << endl;
	cout << w1.use_count() << endl;

	return 0;
}

shared_ptr 循环引用问题

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

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

struct ListNode
{
	int _val = -1;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;

	ListNode() = default;

	ListNode(int value)
		:_val(value)
	{
		;
	}

	~ListNode()
	{
		cout << "ListNode 析构函数" << endl;
	}
};

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

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

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

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

	return 0;
}

如下图所述场景,p1 和 p2 析构后,管理两个节点的引用计数减到 1:

在这里插入图片描述

  1. 右边的节点什么时候释放呢?左边节点中的 shared_ptr智能指针 _next 正指向它,要等 _next 析构后,右边的节点才可以释放。

  2. 那左边节点的 _next 什么时候析构呢?它是左边节点的成员,要等左边节点释放,_next 才能析构。

  3. 那左边节点什么时候释放呢?左边节点由右边节点中的 shared_ptr智能指针 _prev 管理,_prev 析构后,左边的节点才能释放。

  4. 最后右边节点的 _prev 什么时候析构呢?_prev 是右边节点的成员,右边节点释放,_prev才能析构。

至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成循环引用,导致内存泄漏。

把 ListNode 结构体中的 _next 和 _prev 改成 weak_ptr,weak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next 和 _prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。

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

struct ListNode
{
	int _val = -1;
	weak_ptr<ListNode> _next;	// 将这部分的 shared_ptr 改为 weak_ptr
	weak_ptr<ListNode> _prev;

	ListNode() = default;

	ListNode(int value)
		:_val(value)
	{
		;
	}

	~ListNode()
	{
		cout << "ListNode 析构函数" << endl;
	}
};

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

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

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

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

	return 0;
}

智能指针的原理

auto_ptr 与 unique_ptr 简单实现

下面我们简单模拟实现 auto_ptr 和 unique_ptr 的核心功能,这两个智能指针的实现本身也比较简单。auto_ptr 的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr 的思路是不支持拷贝。

#include <iostream>
using namespace std;

namespace my
{
	template<class T>
	class auto_ptr
	{

		typedef auto_ptr<T> self;

	public:

		auto_ptr(T* ptr)
			:_ptr(ptr)
		{
			;
		}

		auto_ptr() = default;

		auto_ptr(self& one)
			:_ptr(one._ptr)
		{
			one._ptr = nullptr;		// 管理权转移
		}

		~auto_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete _ptr" << endl;
				delete _ptr;
				_ptr = nullptr;
			}
		}

		self& operator=(self& one)
		{
			if (&one != this)
			{
				if (_ptr != nullptr)
				{
					delete _ptr;
				}

				_ptr = one._ptr;
				one._ptr = nullptr;
			}
		}

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

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

	private:

		T* _ptr = nullptr;
	};

	template<class T>
	struct default_delete
	{
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};

	template<class T, class D = default_delete<T>>
	class unique_ptr
	{
		typedef unique_ptr<T, D> self;

	public:

		unique_ptr() = default;
		unique_ptr(self& one) = delete;
		self& operator=(self& one) = delete;

		unique_ptr(self&& one)
			:_ptr(one._ptr)
		{
			one._ptr = nullptr;
		}

		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
			;
		}

		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete _ptr" << endl;
				_delete(_ptr);
				_ptr = nullptr;
			}
		}

		self& operator=(self&& one)
		{
			if (&one != this)
			{
				if (_ptr != nullptr)
				{
					_delete(_ptr);
				}

				_ptr = one._ptr;
				one._ptr = nullptr;
			}
		}

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

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

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

	private:

		T* _ptr = nullptr;
		D _delete;
	};

	template<class T>
	class unique_ptr<T[]>				// 偏特化
	{

		typedef unique_ptr<T[]> self;

	public:

		unique_ptr() = default;
		unique_ptr(self& one) = delete;
		self& operator=(self& one) = delete;

		unique_ptr(self&& one)
			:_ptr(one._ptr)
		{
			one._ptr = nullptr;
		}

		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
			;
		}

		~unique_ptr()
		{
			if (_ptr != nullptr)
			{
				cout << "delete[] _ptr" << endl;
				delete[] _ptr;
				_ptr = nullptr;
			}
		}

		self& operator=(self&& one)
		{
			if (&one != this)
			{
				if (_ptr != nullptr)
				{
					delete[] _ptr;
				}

				_ptr = one._ptr;
				one._ptr = nullptr;
			}
		}

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

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

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

	private:

		T* _ptr = nullptr;
	};
}

int main()
{
	my::auto_ptr<int> p1(new int(5));
	// auto_ptr 在 p2 拷贝时导致 p1 悬空,使用 p1 访问资源时为非法访问
	my::auto_ptr<int> p2(p1);
	//cout << *p1 << endl;	// 报错


	my::unique_ptr<int[]> p3(new int[10]{0});		// unique_ptr<int[]> 可以让智能指针指向 int 型数组并正确释放

	// unique_ptr 不允许拷贝,可以移动,移动后 p3 悬空 
	//unique_ptr<int> p4(p3);
	my::unique_ptr<int[]> p4(move(p3)); 

	return 0;
}

shared_ptr 与 weak_ptr 简单实现

难点在于 shared_ptr 的设计,尤其是引用计数,这里一份资源就需要一个引用计数,所以引用计数不能使用静态成员,要使用堆上动态开辟的方式。构造智能指针对象时开辟一份资源,就要 new 一个引用计数出来记录。当前 shared_ptr 指向资源时就 引用计数 + 1,shared_ptr 对象析构、拷贝、移动时就 引用计数 - 1。引用计数减到 0 时代表当前析构的 shared_ptr 是最后一个管理资源的对象,则析构资源。

#define _CRT_SECURE_NO_WARNINGS 1

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

namespace my
{
	template<class T>
	class shared_ptr_base
	{
		typedef shared_ptr_base<T> self;

	public:

		shared_ptr_base() = default;
		shared_ptr_base(T* ptr)
			:_ptr(ptr)
			, _count(new int(1))
		{
			;
		}

		template<class D>
		shared_ptr_base(T* ptr, D delete_way)
			:_ptr(ptr)
			, _count(new int(1))
			, _delete(delete_way)
		{
			;
		}

		shared_ptr_base(self& one)
			:_ptr(one._ptr)
			, _count(one._count)
			, _delete(one._delete)
		{
			++(*_count);
		}

		shared_ptr_base(self&& one)
			:_ptr(one._ptr)
			, _count(one._count)
			, _delete(one._delete)
		{
			one._ptr = nullptr;
			one._count = nullptr;
		}

		~shared_ptr_base()
		{
			release();
		}

		void release()
		{
			if (_ptr != nullptr)
			{
				if (--(*_count) == 0)
				{
					_delete(_ptr);
					delete _count;
				}
			}
			_count = nullptr;
			_ptr = nullptr;
		}

		self& operator=(self& one)
		{
			if (&one != this)
			{
				release();

				_ptr = one._ptr;
				_count = one._count;
				++(*_count);
			}
			return *this;
		}

		self& operator=(self&& one)
		{
			if (&one != this)
			{
				release();

				_ptr = one._ptr;
				_count = one._count;
				one._ptr = nullptr;
				one._count = nullptr;
			}
			return *this;
		}

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_count;
		}

		operator bool()
		{
			return _ptr != nullptr;
		}

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

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

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

	protected:

		T* _ptr = nullptr;
		int* _count = nullptr;
		function<void(T*)> _delete;
	};

	template<class T>
	class shared_ptr : public shared_ptr_base<T>
	{
		typedef shared_ptr<T> self;
		typedef shared_ptr_base<T> base;

	public:

		shared_ptr() = default;

		shared_ptr(T* ptr, const function<void(T*)>& delete_way = [](T* ptr) { cout << "delete _ptr" << endl; delete ptr; })
			:base(ptr, delete_way)
		{
			;
		}
	};

	template<class T>
	class shared_ptr<T[]> : public shared_ptr_base<T>	// 偏特化(支持 [] 销毁) + 继承(省略偏特化重复函数)
	{
		typedef shared_ptr<T[]> self;
		typedef shared_ptr_base<T> base;

	public:

		shared_ptr() = default;

		shared_ptr(T* ptr, const function<void(T*)>& delete_way = [](T* ptr) { cout << "delete[] _ptr" << endl; delete[] ptr; })
			: base(ptr, delete_way)
		{
			;
		}
	};
}

void print_ptr_arr(my::shared_ptr<int[]>& arr, int size)
{
	for (int i = 0; i < size; ++i)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
}

class number
{
public:

	number(int num1, float num2)
		:_one(num1)
		, _two(num2)
	{
		;
	}

	number() = default;

private:

	int _one = 0;
	float _two = 1.0f;
};

void delete_number_arr(number* p)
{
	delete[] p;
	cout << "delete[] _ptr" << endl;
}

int main()
{
	my::shared_ptr<int[]> p5(new int[10] {0});
	//shared_ptr<int[]> p5 = new int[10]{0};// shared_ptr 和 unique_ptr 不支持普通指针隐式类型转换

	// shared_ptr 可以拷贝,可以移动,
	// 多个智能指针可以共享一个资源
	my::shared_ptr<int[]> p6 = p5;
	my::shared_ptr<int[]> p7 = move(p6);		// p6 移动后悬空

	cout << "p5[] = ";
	print_ptr_arr(p5, 10);

	cout << "p7[] = ";
	print_ptr_arr(p7, 10);

	p5[1] = 100;	// p5 修改资源,p7 使用时也会受到影响
	p5[7] = 33;

	cout << "p5[] = ";
	print_ptr_arr(p5, 10);

	cout << "p7[] = ";
	print_ptr_arr(p7, 10);
	cout << endl;

	cout << "多少个 shared_ptr 指针指向当前资源:" << p7.use_count() << endl << endl;

	if (p6)
	{
		cout << "p6 指向了资源" << endl;
	}
	else
	{
		cout << "p6 指针悬空" << endl;
	}
	cout << endl;

	my::shared_ptr<number[]> p1(new number[5]);

	// 除了特化 [] 方式还可以使用定制删除器
	// shared_ptr 的删除器要求传在构造参数中

	// 函数指针方式做删除器
	my::shared_ptr<number[]> p2(new number[5], &delete_number_arr);

	// lambda表达式做删除器
	my::shared_ptr<number[]> p3(new number[5], [](number* p) { cout << "delete[] _ptr" << endl; delete[] p; });

	// 其他资源管理的删除器
	my::shared_ptr<FILE> p4(fopen("test.cpp", "r"), [](FILE* fp) {
		cout << "fclose:" << fp << endl;
		cout << "delete _ptr" << endl;
		fclose(fp); });

	return 0;
}
// 上面 my::shared_ptr 实现的源码这里省略

// 这里的 weak_ptr 只是简单解决了 shared_ptr循环引用的场景
// 完整的 shared_ptr 与 weak_ptr 实现很复杂,感兴趣的朋友
// 可以查看官方文档与其源码
namespace my
{
	template<class T>
	class weak_ptr
	{
		typedef weak_ptr<T> self;

	public:

		weak_ptr() = default;

		weak_ptr(const my::shared_ptr<T>& one)
			:_ptr(one.get())
		{
			;
		}

		self& operator=(const my::shared_ptr<T>& one)
		{
			_ptr = one.get();
			return *this;
		}

	private:

		T* _ptr = nullptr;
	};
}

struct ListNode
{
	int _val = -1;
	my::weak_ptr<ListNode> _next;	// 将这部分的 shared_ptr 改为 weak_ptr
	my::weak_ptr<ListNode> _prev;

	ListNode() = default;

	ListNode(int value)
		:_val(value)
	{
		;
	}

	~ListNode()
	{
		cout << "ListNode 析构函数" << endl;
	}
};

int main()
{
	my::shared_ptr<ListNode> p1(new ListNode);
	my::shared_ptr<ListNode> p2(new ListNode);

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

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

	cout << "p1 计数:" << p1.use_count() << endl;
	cout << "p2 计数:" << p2.use_count() << endl;

	return 0;
}

shared_ptr 的线程安全问题

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

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

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

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

C++ Boost 有 scoped_ptr / scoped_array 和 shared_ptr / shared_array 和 weak_ptr 等智能指针。
C++ 11 在 C++ Boost 基础上引入了 unique_ptr 和 shared_ptr 和 weak_ptr。需要注意的是 unique_ptr 对应的是 boost 的 scoped_ptr。并且这些智能指针的实现原理是参考 Boost 中的实现的。
C++ TR1,引入了 shared_ptr 等,不过注意的是 TR1 并不是标准版。

内存泄漏

内存泄漏概念与其危害

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

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

如何检测内存泄漏

这部分大家可以参考下面文章:

linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具
windows下使用第三方工具:windows下的内存泄露检测工具VLD使用

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。

  2. 尽量使用智能指针来管理资源,如果代码场景比较特殊,采用 RAII 思想自己造个轮子管理。

  3. 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。

总结:内存泄漏非常常见,解决方案分为两种:1、事前预防型,如智能指针等。2、事后查错型,如泄漏检测工具。


http://www.kler.cn/a/523847.html

相关文章:

  • C++并发编程指南02
  • 批量卸载fnm中已经安装的所有版本
  • QT 通过ODBC连接数据库的好方法:
  • 单路由及双路由端口映射指南
  • Web 代理、爬行器和爬虫
  • macbook安装go语言
  • 第24章 质量培训与探啥未来
  • deepseek-r1 本地部署
  • 【SH】Windows禁用Alt+F4关机、重启、注销等功能,只保留关闭应用的功能
  • 利用 PyTorch 动态计算图和自动求导机制实现自适应神经网络
  • 炒股-技术面分析(技术指标)
  • JJJ:linux时间子系统相关术语
  • 【MySQL-7】事务
  • 【WebGL】纹理
  • 【某大厂一面】java深拷贝和浅拷贝的区别
  • 顶刊JFR|ROLO-SLAM:首个针对不平坦路面的车载Lidar SLAM系统
  • 基于Python的智慧物业管理系统
  • aws sagemaker api 获取/删除 endpoints
  • ResNeSt: Split-Attention Networks论文学习笔记
  • MATLAB基础应用精讲-【数模应用】DBSCAN算法(附MATLAB、R语言和python代码实现)(二)
  • 54.数字翻译成字符串的可能性|Marscode AI刷题
  • Next.js 14 TS 中使用jwt 和 App Router 进行管理
  • 基于 NodeJs 一个后端接口的创建过程及其规范 -- 【elpis全栈项目】
  • oracle比较一下统计信息差异吧
  • Vue 响应式渲染 - 列表布局和v-html
  • 【2024年华为OD机试】(C卷,200分)- 推荐多样性 (JavaScriptJava PythonC/C++)