智能指针的使用场景

  • new 了之后,也 delete 了,但是因为抛出异常导致后面的 delete 没有执行,所以就导致内存泄露了,我们需要 new 以后捕获异常,捕获到异常后 delete 内存,再把异常抛出,但是因为 new 本身也能抛出异常,连续的两个 new 和下面的Divide 都会抛出异常,处理起来很麻烦
double Divide(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        throw "Division by zero condition!";
    }

    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 << "Unkown Exception" << endl;
    }
    return 0;
}

RAII和智能指针的设计思路

  • RAII 是一种管理资源的类的设计思想
  • RAII 在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题
  • 智能指针除了满足 RAII 的设计思路,还要方便资源的访问,所以还要重载 operator*/operator->/operator[] 运算符
  • 普通的指针 int* p1 = new int(10); int* p2 = p1; p1和p2指向同一块内存,是浅拷贝,会导致重复析构;智能指针就是解决这个问题的

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

  • 在==<memory>==这个头文件下
  • C++98的 auto_ptr 强烈不推荐使用,就不写了
  • C++11:
    1. unique_ptr,唯一指针,特点是不支持拷贝,只支持移动,如果不需要拷贝的场景就建议使用它
    2. shared_ptr,共享指针,支持拷贝,也支持移动,如果需要拷贝的场景就需要使用它。底层是用引用技术的方式实现的
    3. weak_ptr,弱指针,不同于上面的智能指针,它不支持 RAII。也就是说不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的循环引用导致的内存泄漏问题
    4. unique_ptr 和 shared_ptr 移动后会悬空
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()
{
    unique_ptr<Date> up1(new Date);
    //不支持拷贝
    //unique_ptr<Date> up2(up1);    //错误写法
    //支持移动,但是移动后 up1 为空,所以使用移动要谨慎
    unique_ptr<Date> up3(move(up1));

    shared_ptr<Date> sp1(new Date);
    //支持拷贝
    shared_ptr<Date> sp2(sp1);
    shared_ptr<Date> sp3(sp1);
    cout << sp1.use_count() << endl;    //获取当前指向同一对象的 shared_ptr 数量(引用计数)
    //sp1\sp2\sp3指向同一对象
    sp1->_year++;
    cout << sp1->_year << endl; //2
    cout << sp2->_year << endl; //2
    cout << sp3->_year << endl; //2

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

    return 0;
}
  • 智能指针析构时默认是进行 delete 释放资源。如果不是 new 出来的资源,交给智能指针管理,析构时就回崩溃。
  • 智能指针支持在构造时给一个删除器,实现想要的释放资源的方式
  • 因为new[]经常使⽤,所以 unique_ptr 和 shared_ptr 都特化了⼀份[]的版本,使用时 unique_ptr<Date[]> up1(new Date[5]); shared_ptr<Date[]> 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和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>());

    //解决方案3
    //函数指针做删除器
    unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);   //在类的声明传
    shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);   //在构造函数传
    
    //解决方案4,最推荐
    //lambda表达式做删除器
    //unique_ptr定制删除器推荐用仿函数.shared_per都可以,相对建议lambda
    auto delArrOBJ = [](Date* ptr) {delete[] ptr; };    
    unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);  //decltype推导()中表达式类型
    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;
}
  • shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造
  • shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回 false,否则返回true可以直接把智能指针对象给 if 判断是否为空
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)
        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;
}

智能指针原理

  • unique_ptr 的思路不支持拷贝
  • shared_ptr 采用引用计数的设计,因为这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要 new 一个引用计数出来
  • 多个 shared_ptr 指向资源时就 ++引用计数,shared_ptr 对象析构时就 --引用计数,引用计数减到0时代表当前析构的 shared_ptr时最后一个管理资源的对象,则析构资源
namespace ssp
{
	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;
			return *this;
		}
	private:
		T* _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;
		function<void(T*)> _del = [](T* ptr) {delete ptr; }	//定制删除器
	};
}

shared_ptr和weak_ptr

shared_ptr循环引用问题

  • 在循环引用的场景下会导致资源没得到释放,导致内存泄漏,使用weak_ptr解决这种问题
  • 引用计数的值是有多少个指针指向它,prev指向它也会使计数+1
    请添加图片描述
  • 他们两个互相指向对方,想要析构必须等对方先析构,导致了循环引用问题
  • 把_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用
    请添加图片描述

weak_ptr

  • weak_ptr不支持RAII,也不支持访问资源,只支持绑定到shared_ptr,且不增加shared_ptr的引用计数
  • weak_ptr支持expired检查指向的资源是否过期use_count也可获取shared_ptr的引用计数
  • weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的
int main()
{
	std::shared_ptr<string> sp1(new string("11111"));
	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>("22222");
	cout << wp.expired() << endl;
	cout << wp.use_count() << endl;

	sp2 = make_shared<string>("22222");
	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;
}

shared_ptr的线程安全问题

  • shared_ptr本身是线程安全的,但是指向的对象有线程安全的问题
  • 如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引⽤计数,就会存在线程安全问题,所以shared_ptr引⽤计数是需要加锁或者 原⼦操作保证线程安全的
  • bit::shared_ptr引⽤计数从int*\改成==atomic<int>*==就可以保证引⽤计数的线程安全问题,或者使⽤互斥锁加锁也可以

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

  • Boost库是为C++语言标准库提供扩展的⼀些C++程序库的总称
  • C++boost给出了更实用的scoped_ptr/scoped_array和shared_ptr/shared_array和weak_ptr等

内存泄漏

什么是内存泄漏

  • 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,⼀般是忘记释放或者发生异常释放程序未能执行导致的

内存泄露的危害

  • 普通程序运行⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,页表的映射关系解除,物理内存也可以释放。
  • 长期运行的程序出现内存泄漏,影响很大。不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越 慢,最终卡死

如何避免内存泄漏

  • 1、事前预防型。如智能指针等。
  • 2、事后查错型。如泄漏检测⼯具
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐