C++学习笔记——智能指针

c++里不像java等语言设置了垃圾回收的功能,但是c++通过它的王牌指针实现了智能指针这一解决办法。

目录

异常

1、异常

2、异常的使用

3、异常的规则

4、异常安全

智能指针

概念

原理

auto_ptr

unique_ptr

shared_ptr


异常

再说智能指针之前,先用异常来引出智能指针。

  • 1、异常

先看一段代码

void test()
{
	int x = 1024;
	int y;
	y = x / 0;
}

 执行后,编译器会报一个警告,并且程序崩溃。这就产生了一个异常,或者说是一个错误。

以前我们解决异常的方式常用方式是:

1)assert断言,这个在前面数据结构学习的时候,写代码经常用到,但是它的缺陷就是,如果发生错误直接终止程序,太粗鲁了。

2)返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误

3)安全性判定,这个就是程序员自己写出错误的情况,触发则程序运行相应代码,但是可能考虑的不是很全面,会出现未知的错误

  • 2、异常的使用

1)抛出异常:用throw:关键字。当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成

2)捕获异常:catch和try关键字。try关键字是标识异常,也就是把可能出异常的地方圈起来,称为块。catch 关键字用于捕获异常,可以有多 个catch进行捕获。 try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块来进行捕获。

3)处理异常:异常的处理就是在catch的代码块中,当catch捕获到了异常,就在相应的catch块中处理。

关键字理解:

throw:如果程序没有异常,可以throw(),标识不会抛出异常,(当然你也可以不用多次一举的写出来,毕竟以前写代码也没有用过)。如果有异常,通过该关键字抛出,其实可以理解为发送一则错误原因。它可以是一个整型的数字,也可以是字符串,这就是它的类型,它是一个对象,后面处理的时候,就会根据它的类型进行处理。下面的例子,异常的类型就是字符串类型

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "除数是 0 !";
	else
		return ((double)a / (double)b);
}

// 这里表示这个函数不会抛出异常
void* operator new (std::size_t size, void* ptr) throw();

try和catch:在try块的代码里,我们成为保护代码。如果throw抛出了异常对象,就会触发try。将异常扔给catch,catch通过异常的类型进行处理。处理常字符串类型的异常对象。

void Func()
{
	int Dividend;
	int Divisor;
	cin >> Dividend >> Divisor;
	try 
	{
		cout << Division(Dividend, Divisor) << endl;
	}
        catch (const char* str)
	{
		cout << str << endl;
	}
        catch (...)//当异常都不是前面的类型时,他会在这里被捕获,这个会在规则中细说
	{
		cout << "不知名错误" << endl;
	}
}

演示结果:

  • 3、异常的规则

  1. 前面说抛出的异常是一个对象,那么它就是有类型的,该类型决定了应该激活哪个catch的处理代码。
  2. 被触发的try是离抛出异常位置最近的那一个。
  3.  抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成 一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
  4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
  5. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。这个就要说catch的匹配了。

在函数调用链中异常栈展开匹配原则

1、如果在当前try中没有找到合适的catch,则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。 

2、如果到达main函数的栈,依旧没有匹配的,则终止程序。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕 获,程序就会直接终止。

函数调用栈:

 对上面的一段话用一个例子来进行说明:

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "除数是 0 !";
	else
		return ((double)a / (double)b);
}

void Func()
{
	int Dividend;
	int Divisor;
	cin >> Dividend >> Divisor;
	try 
	{
		cout << Division(Dividend, Divisor) << endl;
	}
        //catch (const char* e)//如果在这里进行进行处理,就会直接捕获,不会在main函数哪里捕
                                //获,因为这里里异常抛出的地方最近,它是调用者
	//{
	//	cout << e << endl;
	//}
        catch (int  n)//这里不匹配,到main函数中去寻找匹配
	{
		cout << n << endl;
	}
        cout << "会不会执行这儿呢?" << endl;//答案是没有执行
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* e)//将前面的能匹配的catch注释掉,这个时候,异常就会返回到这里被处理
	{
		cout << e << endl;
	}
	catch (...)//如果前面的都没有被匹配到,则会在这里处理
	{
		cout << "不知名错误" << endl;
	}
	cout << "异常被处理" << endl;//会执行匹配到的catch后面的代码
	return 0;
}

  • 4、异常安全

在前面我们可以看到,异常抛出后,它便会会匹配catch,它只会执行匹配成功的catch后面的代码,那么着就会造成一系列的安全问题。

  1. 构造函数是完成对象的构造和初始化的,如果构造函数中抛出异常,可能导致对象不完整或没有 完全初始化
  2. 析构函数主要完成资源的清理,如果析构函数内抛出异常,可能导致资源泄漏(内存泄漏、句 柄未关闭等)

5、异常的优缺点

说了这么多,到底异常好不好?是return干脆好?还是assert的粗暴好?

优点:

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
  2. 效率好,他会匹配最近的try,如果没有合适的在hi层层往上找。
  3. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如T&operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误

缺点:

  1. 前面我们看到寻找合适的catch时,层层跳转会让我们调试和分析程序时眼花缭乱。
  2. 异常安全问题
  3. 异常实则是一个对象,类型就是它的类,如果异常这个对象不规范的,匹配起来那叫一个龙飞凤舞。

智能指针

  • 概念

在前面学习继承和多态的时候,就说过c++有时候就是拆东墙补西墙,为了解决异常安全等造成资源泄露的问题,就出现了智能指针。将资源给托管给一个对象,让完成功能后,释放掉资源,这个对象就是智能指针。没错就是托管用的,

  • 原理

智能指针是基于RAII思想设计的。

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在我们构造对象时,平常都是用类的构造函数。有智能指针了,我们就把对象的资源托付给智能指针,我们直接操作智能指针就行了,也就是说智能指针现在就是我们的对象。所以在对象构造时就让智能指针获取资源,接着让智能指针控制对资源的访问权限在对象的生命周期内始终保持有效,这样就成了我们的对象。最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效。

画个图理解一下。在后面实现智能指针的阶段,再用代码来说明。

这里提供一个Student类。

class Student
{
public:
	Student(const string& name = "张三", const string& sex = "男", const int& stunum = 12345)
		:name_(name)
		,stu_num_(stunum)
		,sex_(sex)
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
	void Print()
	{
		cout << "name: " << name_ << "   sex: " << sex_ << "   stunum: " << stu_num_ << endl;
	}
private:
	string name_;
	string sex_;
	int stu_num_;
};
  • auto_ptr

年代久远的C++98版本的库中提供了auto_ptr的智能指针,看到auto就联想到c++的关键字——类型占位符。下面我们就先用一下。接用上面的Student类。

void AutoPtrTest()
{
	auto_ptr<Student> ptr(new Student);

	ptr->Print();//通过指针来操作
}
int main()
{
	AutoPtrTest();
	system("pause");
	return 0;
}

看这一节第一个词说到了年代久远,就说明这个智能指针不太靠谱,为什么这么说?如果我两个智能指针同时指向这个类的对象会怎么样?

void AutoPtrTest()
{
	auto_ptr<Student> ptr(new Student);

	auto_ptr<Student> ptr2 = ptr;//这里其实相当于拷贝构造
	ptr->Print();
}
int main()
{
	AutoPtrTest();

	system("pause");
	return 0;
}

这个界面很常见吧?当auto_ptr时,如果两个智能指针同时指向一个对象,也就是说同一份资源有两个操作者,会把前一个智能指针置空,也就是说一份资源只能有一个智能指针来操作,这叫一个操作权的转移。这样做是为了解决了一块空间被多个对象使用而造成程序奔溃,比如一个智能指针对象把资源释放了,那另外的智能指针只能喝西北风了(野指针)。但是这样的话,会因为不当操作造成程序崩溃,因为auto_ptr把上一个只能指针置空了,如果继续使用上一个指针就非法了。

下面来简单实现一下auto_ptr。

template <class T>
class Auto_Ptr
{
public:
	Auto_Ptr(T* ptr = nullptr)
		:autoptr_(ptr)
	{
		cout << "Auto_Ptr()" << endl;
	}
	Auto_Ptr(Auto_Ptr<T>& ptr)//这里是要修改不能要const属性
		:autoptr_(ptr)
	{
		ptr = nullptr;//当对象拷贝或者赋值后,前面的智能指针就悬空
		cout << "Auto_Ptr(&)" << endl;
	}
	~Auto_Ptr()
	{
		if (autoptr_)
		{
			delete autoptr_;//会自动调用对象的析构函数
			cout << "~Auto_Ptr()" << endl;
		}
	}
	Auto_Ptr<T>& operator=(Auto_Ptr<T>& ptr)
	{
		if (this != &ptr)//防止自己给自己赋值
		{
			if (autoptr_)
			{
				delete autoptr_;//如果原来的智能指针是指向某一个对象时要释放原来的资源
			}
			autoptr_(ptr);
		}
	}
	//既然是指针就会有++,->,*等操作
	T& operator*()
	{
		return *autoptr_;
	}
	T* operator->()
	{
		return autoptr_;
	}
private:
	T* autoptr_;
};

来用一下

void AutoPtrTest()
{
	Auto_Ptr<Student> ptr(new Student);
	ptr->Print();
}
int main()
{
	AutoPtrTest();
	system("pause");
	return 0;
}

  • unique_ptr

C++11中开始提供比较靠谱的unique_ptr,为什么说是比较靠谱,因为它解决了前面的拷贝问题。为什么是“比较”,来看看。(使用方式和auto_ptr方式相同,这里不进行示例)

为了防止拷贝带来的问题,把拷贝构造和赋值运算符重载直接给删除了。。。。

简单粗暴。不过也来实现一下。

template <class T>
class UniquePtr
{
public:
	UniquePtr(T* ptr = nullptr)
		:uniqueptr_(ptr)
	{
		cout << "UniquePtr()" << endl;
	}
	~UniquePtr()
	{
		if (uniqueptr_)
		{
			delete uniqueptr_;
		}
	}
	T& operator*()
	{
		return *uniqueptr_;
	}
	T* operator->()
	{
		return uniqueptr_;
	}
private:
	T* uniqueptr_;
	UniquePtr(UniquePtr<T>& ptr) = delete;
	UniquePtr<T>& operator=(UniquePtr<T>& ptr) = delete;
};

简单的测试一下;

	UniquePtr<Student> ptr(new Student);
	ptr->Print();

  • shared_ptr

C++11中又开始提供更靠谱的并且支持拷贝的shared_ptr。在前面已经使用了两种指针了,所以现在主要看看怎么解决多个指向同一个资源的指针能和谐的使用资源,不用担心资源被释放的问题。

shared_ptr是什么原理呢?

在shared_ptr的内部有一个计数器,用来记录有多少个指针对象在使用这个资源

  1. 当某一个对象不适用资源了,就把计数器减一
  2. 如果计数器变成0了,说明是最后一个指针对象,就释放该资源
  3. 如果计数器大于0,说明还有其他的指针对象在使用,则什么都不用做。

简单实现一下shared_ptr。

#include<mutex>//需要用到互斥锁,线程安全
template <class T>
class SharePtr
{
public:
	SharePtr(T* ptr = nullptr)
		:shareptr_(ptr)
		,pcount_(new int(1))
		,pmutex_(new mutex)
	{
		cout << "SharePtr() " << endl;
	}
	SharePtr(SharePtr<T>& ptr)
		:shareptr_(ptr.shareptr_)
		, pcount_(ptr.pcount_)
		, pmutex_(ptr.pmutex_)
	{
		AddCount();
		cout << "SharePtr(&)" << endl;
	}
	~SharePtr()
	{
		cout << "~SharePtr()" << endl;
		Release();
	}
	void AddCount()
	{
		pmutex_->lock();
		++(*pcount_);
		pmutex_->unlock();
	}
	SharePtr<T> operator=(SharePtr<T>& ptr)
	{
		if (this != &ptr)
		{
			Release();//删除旧的资源,由于count初始化时就是1,所以不会是-1
			shareptr_ = ptr.shareptr_;
			pmutex_ = ptr.pmutex_;
			AddCount();
		}
		return *this;
	}
	T& operator*()
	{
		return *shareptr_;
	}
	T* operator->()
	{
		return shareptr_;
	}
	int UseCount()
	{
		return *pcount_;
	}
private:
	void Release()
	{
		bool mutexflag = false;
		pmutex_->lock();
		if (--(*pcount_) == 0)
		{
			delete shareptr_;
			delete pcount_;
			mutexflag = true;
		}
		pmutex_->unlock();
		if (mutexflag == true)
		{
			delete pmutex_;
		}
	}
private:
	T* shareptr_;
	int* pcount_;//引用计数是共享的,所以需要考虑线程安全问题
	mutex* pmutex_;
};

上面的share_ptr是否一定是完美的呢?坑总是无处不在的。看下面的代码。(用库中的智能指针)

struct ListNode
{
	int data = 0;
	shared_ptr<ListNode> prev = nullptr;
	shared_ptr<ListNode> next = nullptr;

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


void ProblemTest()
{
	shared_ptr<ListNode> ptr1(new ListNode);
	shared_ptr<ListNode> ptr2(new ListNode);
	cout << ptr1.use_count() << endl;
	cout << ptr2.use_count() << endl;
	ptr1->next = ptr2;
	ptr2->prev = ptr1;
	cout << ptr1.use_count() << endl;
	cout << ptr2.use_count() << endl;
	ptr1 = ptr2;
}

乍一看,是没啥问题,但是没有调用析构函数啊!!但是如果在上面的代码中把下面的语句注销掉。

	ptr1->next = ptr2;
	ptr2->prev = ptr1;

这个是就是share_ptr带来的循环引用带来的问题。

解决方案:在引用计数的场景下,把节点中的prev和next改成weak_ptr就可以了。原理就是,ptr1->_next = node2;和ptr2->prev = node1;时weak_ptr的next和prev不会增加 ptr1和ptr2的引用计数。 (字面意思就是弱指针,它是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少)

总结上一篇的结尾问题

1、静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。

2、构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

3、析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实是编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明基类的析构函数最好写成虚函数

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值