【C++】智能指针

目录

异常处理

智能指针

C++98 auto_ptr

C++11 unique_ptr

C++11 shared_ptr

C++11 weak_ptr


异常处理

C语言异常处理机制:

  1. 终止程序
  2. 返回错误码

C++异常处理机制:

  1. try:测试异常
  2. throw:抛出异常
  3. catch:捕获异常

throw抛出异常时,先看当前函数栈帧是否有try-catch,若有就在当前栈帧捕获,若无就去上一层栈帧检查try-catch

#include <iostream>
using namespace std;

double div(double a, double b)
{
	if (b == 0)
	{
		throw "除0错误";
	}

	return a / b;
}

void func()
{
	double a, b;
	cin >> a >> b;
	try
	{
		div(a, b);
	}
	catch (const char* err)		// catch 捕捉异常需要类型匹配
	{
		cout << err << endl;
	}
	catch(...)					// 捕获任意类型异常
	{
		cout << "未知错误" << endl;
	}
}

int main()
{
	func();

	return 0;
}

异常安全:最好不要在构造函数和析构函数抛出异常,可能造成对象构造不完整或资源泄露

C++异常的缺陷

  • 异常会导致执行流乱跳,提高我们跟踪调试和分析程序的难度
  • C++没有垃圾回收机制,资源需要自己管理,容易造成内存泄漏、死锁等问题

针对异常处理容易造成的内存泄漏问题,C++设计了智能指针。

智能指针

智能指针设计意义:协助管理动态分配的内存,帮助我们自动释放new出来的内存,从而避免内存泄漏。

内存泄漏示例:

#include <iostream>
using namespace std;

void memory_leak_test1()
{
	string* str = new string("hello world");

	return;
	// 没有释放new出来的内存,内存泄漏
}

void memory_leak_test2()
{
	string* str = new string("hello world");
	if (1)
	{
		return;
		// 因某种情况或发生异常退出函数,delete没有正常释放内存,内存泄漏
	}

	delete str;
}

int main()
{
	memory_leak_test1();
	memory_leak_test2();

	return 0;
}

智能指针设计原理:将我们分配的动态内存都交由有生命周期的对象来处理,在对象过期时,让它自动调用析构函数释放内存。

C++98 auto_ptr

auto_ptr:auto_ptr是C++98提供的智能指针模板,其定义了管理指针的对象,可以将new获得的空间地址赋给这个对象,当对象过期时调用析构函数来delete内存。

auto_ptr源码设计:

#pragma once
#include <iostream>

template<class T>
class AutoPtr
{
public:
	AutoPtr()
		: _ptr(nullptr)
	{}

	AutoPtr(T* ptr)
		: _ptr(ptr)
	{}

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

	AutoPtr(AutoPtr<T>& ap)
		: _ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}

	AutoPtr<T>& operator=(AutoPtr<T>& ap)
	{
		_ptr = ap._ptr;
		ap._ptr = nullptr;
		return *this;
	}

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

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

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

private:
	T* _ptr;
};

void auto_ptr_test()
{
	AutoPtr<int> ap1(new int(1));
	std::cout << *ap1 << std::endl;
	*ap1 += 1;

	AutoPtr<int> ap2 = ap1;
	std::cout << *ap2 << std::endl;
	++(*ap2);

	AutoPtr<int> ap3(ap2);
	std::cout << *ap3 << std::endl;

	// auto_ptr缺陷
	// ap1 和 ap2 在赋值和复制的时候被置空,但是程序员有可能再次调用ap1和ap2造成访问越界
}

// 输出结果
// 1
// 2
// 3
// auto_ptr delete: 00A17030
// auto_ptr delete: 00000000
// auto_ptr delete: 00000000

auto_ptr的重大缺陷:

  1. 复制或者赋值都会改变资源的所有权,导致原智能指针失效,但是程序员有可能继续访问原来的智能指针,引发访问越界
  2. 在STL容器中使用auto_ptr存在风险,因为容器内元素必须支持可复制、可赋值,这会引发第一个缺陷
  3. 不支持对象数组的内存管理

于是C++11之后不再使用auto_ptr,取而代之的是unique_ptr、shared_ptr/weak_ptr,这三种智能指针有各自的应用场景,都是在弥补auto_ptr的重大缺陷之后各自也存在缺陷,互相弥补以适应不同的应用场景。

C++11 unique_ptr

unique_ptr:unique_ptr为了弥补auto_ptr的缺陷,在auto_ptr的设计基础上进行了升级。

unique_ptr特性:

  1. 排他性:两个指针不能指向同一个资源
  2. 无法进行unique_ptr左值的赋值和构造,但支持unique_ptr右值的赋值和构造
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象
  4. 在STL容器中保存指针是安全的,支持对象数组的内存管理

unique_ptr源码设计:

#include <iostream>

template<class T>
class UniquePtr
{
public:
	UniquePtr(T* ptr)
		: _ptr(ptr)
	{}

	~UniquePtr()
	{
		delete _ptr;
		std::cout << "unique_ptr delete: " << _ptr << std::endl;
	}

	UniquePtr(const UniquePtr<T>& up) = delete;
	UniquePtr<T>& operator=(const UniquePtr<T>& up) = delete;

	UniquePtr(UniquePtr<T>&& up)
	{
		_ptr = up._ptr;
		up._ptr = nullptr;
	}

	UniquePtr<T>& operator=(UniquePtr<T>&& up)
	{
		_ptr = up._ptr;
		up._ptr = nullptr;
	}

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

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

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

private:
	T* _ptr;
};

void unique_ptr_test()
{
	UniquePtr<std::string> up1(new std::string("hello world"));
	std::cout << *up1 << std::endl;
	std::cout << up1[0] << std::endl;

	UniquePtr<int> up2(new int[10]);
	for (int i = 0; i < 10; ++i)
	{
		up2[i] = i;
	}
	for (int i = 0; i < 10; ++i)
	{
		std::cout << up2[i] << " ";
	}
	std::cout << std::endl;

	UniquePtr<int> up3 = std::move(up2);
	for (int i = 0; i < 10; ++i)
	{
		std::cout << up3[i] << " ";
	}
	std::cout << std::endl;

	UniquePtr<int> up4(std::move(up3));
	for (int i = 0; i < 10; ++i)
	{
		std::cout << up4[i] << " ";
	}
	std::cout << std::endl;
}

unique_ptr的排他性虽然解决了auto_ptr的复制赋值的风险,但是其本是也是一个缺陷,因为我们很多应用场景是需要让智能指针进行复制和赋值操作的,于是C++有了shared_ptr。

C++11 shared_ptr

shared_ptr:在unique_ptr的设计基础上进行升级,支持多个智能指针变量指向同一个资源,支持智能指针进行左值复制赋值。

shared_ptr设计原理:设计引用计数,记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数+1,当智能指针析构时,引用计数-1,当某个资源的引用计数为0时,代表已经没有指针指向这块资源,则释放这个内存资源。

为防止引用计数在多线程中容易因为资源共享而导致报错,引用计数在修改智能指针数量时加上了互斥锁。

shared_ptr源码设计:

#include <iostream>
#include <thread>
#include <mutex>

// 默认删除器,用于内置变量,对于自定义变量的可定制删除器
template<class T>
class DefaultDelete
{
public:
	void operator()(T* ptr)
	{
		delete ptr;
	}
};

template<class T, class D = DefaultDelete<T>>
class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		: _ptr(ptr), _pCount(new int(1)), _pMtx(new std::mutex)
	{}

	~SharedPtr()
	{
		release();
	}

	void release()
	{
		bool flag = false;
		_pMtx->lock();
		if (--(*_pCount) == 0)
		{
			_del(_ptr);
			delete _pCount;
			flag = true;
		}
		_pMtx->unlock();

		if (flag)
		{
			delete _pMtx;
		}
	}

	SharedPtr(const SharedPtr<T>& sp)
		: _ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx), _del(sp._del)
	{
		_pMtx->lock();
		++(*_pCount);
		_pMtx->unlock();
	}

	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			release();

			_ptr = sp._ptr;
			_pCount = sp._pCount;
			_pMtx = sp._pMtx;
			_del = sp._del;

			_pMtx->lock();
			++(*_pCount);
			_pMtx->unlock();
		}

		return *this;
	}

	int use_count()const
	{
		return *_pCount;
	}
	
	T* get()const
	{
		return _ptr;
	}

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

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

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

private:
	T* _ptr;
	int* _pCount;
	std::mutex* _pMtx;
	D _del;
};

struct Date
{
	int _year;
	int _month;
	int _day;

	Date(int y, int m, int d)
		: _year(y), _month(m), _day(d)
	{}

	friend std::ostream& operator<<(std::ostream& os, const Date& d);
};

std::ostream& operator<<(std::ostream& os, const Date& d)
{
	os << d._year << "/" << d._month << "/" << d._day << std::endl;
	return os;
}

void shared_ptr_test()
{
	SharedPtr<Date> sp1(new Date(2023, 5, 20));

	int n = 10000;
	std::mutex mtx;

	std::thread t1([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				SharedPtr<Date> sp2(sp1);
				mtx.lock();
				sp2->_year++;
				sp2->_month++;
				sp2->_day++;
				mtx.unlock();
			}
		});

	std::thread t2([&]()
		{
			for (int i = 0; i < n; ++i)
			{
				SharedPtr<Date> sp3(sp1);
				mtx.lock();
				sp3->_year++;
				sp3->_month++;
				sp3->_day++;
				mtx.unlock();
			}
		});

	t1.join();
	t2.join();

	std::cout << sp1.use_count() << std::endl;
	std::cout << sp1.get() << std::endl;
	std::cout << sp1[0] << std::endl;

}


// 输出
// 1
// 006514D0
// 22023 / 20005 / 20020

shared_ptr的缺陷:当shared_ptr作为被管控对象的成员时,可能因为循环引用而造成资源泄露。即A类中有B类的智能指针,B类中有A类的智能指针,当它们交叉互相持有对方的管理对象时,就形成了循环引用。

class Girl;

class Boy
{
public:
	Boy()
	{
		std::cout << "Boy 构造" << std::endl;
	}

	~Boy()
	{
		std::cout << "Boy 析构" << std::endl;
	}

	void set_girlfriend(SharedPtr<Girl> girlFriend)
	{
		_girlFriend = girlFriend;
	}

private:
	SharedPtr<Girl> _girlFriend;
};

class Girl
{
public:
	Girl()
	{
		std::cout << "Girl 构造" << std::endl;
	}

	~Girl()
	{
		std::cout << "Girl 析构" << std::endl;
	}

	void set_boyfriend(SharedPtr<Boy> boyFriend)
	{
		_boyFriend = boyFriend;
	}

private:
	SharedPtr<Boy> _boyFriend;
};

void shared_ptr_test1()
{
	SharedPtr<Boy> spBoy(new Boy);
	SharedPtr<Girl> spGirl(new Girl);

	// 循环引用
	spBoy->set_girlfriend(spGirl);
	spGirl->set_boyfriend(spBoy);

	std::cout << spBoy.use_count() << std::endl;
	std::cout << spGirl.use_count() << std::endl;
}

// 输出
// Boy 构造
// Girl 构造
// 2
// 2

// 因为循环引用,无法进行析构,造成内存泄漏

所以在使用shared_ptr的时候,要避免对象的交叉引用情况,而weak_ptr就是为了弥补shared_ptr的这个缺陷而出现。

C++11 weak_ptr

weak_ptr:weak_ptr是为了配合shared_ptr而设计的,目的是协助shared_ptr工作,它只可以从一个shared_ptr和另一个weak_ptr构造,它的构造和析构不会引起引用计数的增减,同时weak_ptr没有重载 * 和 -> ,但可以使用 lock 获得一个可用的shared_ptr对象。

weak_ptr源码设计:

#pragma once
#include "shared_ptr.h"

template<class T>
class WeakPtr
{
public:
	WeakPtr()
		: _ptr(nullptr)
	{}

	WeakPtr(const SharedPtr<T>& sp)
		: _ptr(sp.get())
	{}

	WeakPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		_ptr = sp.get();
		return *this;
	}

private:
	T* _ptr;
};

class Girl;

class Boy
{
public:
	Boy()
	{
		std::cout << "Boy 构造" << std::endl;
	}

	~Boy()
	{
		std::cout << "Boy 析构" << std::endl;
	}

	void set_girlfriend(SharedPtr<Girl> girlFriend)
	{
		_girlFriend = girlFriend;
	}

private:
	WeakPtr<Girl> _girlFriend;
};

class Girl
{
public:
	Girl()
	{
		std::cout << "Girl 构造" << std::endl;
	}

	~Girl()
	{
		std::cout << "Girl 析构" << std::endl;
	}

	void set_boyfriend(SharedPtr<Boy> boyFriend)
	{
		_boyFriend = boyFriend;
	}

private:
	WeakPtr<Boy> _boyFriend;
};

void weak_ptr_test()
{
	SharedPtr<Boy> spBoy(new Boy);
	SharedPtr<Girl> spGirl(new Girl);

	WeakPtr<Girl> wpGirl_1;				// 定义空的弱指针
	WeakPtr<Girl> wpGirl_2(spGirl);		// 使用共享指针构造弱指针
	wpGirl_1 = spGirl;					// 允许共享指针赋值弱指针

	std::cout << spBoy.use_count() << std::endl;		// 1
	std::cout << spGirl.use_count() << std::endl;		// 1

	spBoy->set_girlfriend(spGirl);
	spGirl->set_boyfriend(spBoy);

	std::cout << spBoy.use_count() << std::endl;		// 1
	std::cout << spGirl.use_count() << std::endl;		// 1
}

// 修改Boy类和Girl类后的输出:
// Boy 构造
// Girl 构造
// 1
// 1
// 1
// 1
// Girl 析构
// Boy 析构

注:文中的源码设计相比与标准库进行了精简,保留了主要特征的功能,还有部分功能函数没有实现。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AllinTome

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值