C++智能指针(3.30)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++智能指针(3.30)相关的知识,希望对你有一定的参考价值。

一、问题引入

关于C++中的new和delete操作符,

我们知道这两个操作符必须成对存在,才能避免内存泄漏。

这一点在学习的时候被认为是常识,然而,在实际编写代码的过程中,却常常很难做到。

下面有3种情况:


1、代码很长。

当需要用到delete的地方离使用与之对应的new操作符距离非常远时,我们很容易忘记delete。当然,这种情况是完全可以避免的。


2、如下面代码:

void Test()
{
	int *pi = new int(1);
	if(1)
	{
		return;
	}
	delete pi;     //程序并没有执行到这一步
}
int main()
{
	void Test();
	return 0;
}

这里我们在Test函数中,开辟了一段长度为 1个int类型大小 的动态内存,

但接下来,进入if语句,直接return掉了,因此之前开辟的那段内存没有得到回收,导致内存泄漏。

对于这种情况,我们可以这样修改:

void Test()
{
	int *pi = new int(1);
	if(1)
	{
		delete pi;
		return;
	}
	delete pi;    //程序并没有执行到这一步
}
int main()
{
	void Test();
	return 0;
}


3、让情况再复杂一些,看看这段代码:

void DoSomeThing()
{
	if(1)
	{
		throw 1;
	}
}
void Test()
{
	int *pi = new int(1);
	DoSomeThing();
	delete pi;
}
int main()
{
	try
	{
		Test1();
	}
	catch(...)
	{
		;
	}
	return 0;
}

在Test()函数中,看似new和delete是成对存在的,中间也只有一行代码,

然而当中间这个DoSomeThing()抛出异常导致这个程序结构不是按部就班地进行时,

仍然没有执行delete pi 这一步。

这种情况我们仍然可以做如下修正:

void DoSomeThing()
{
	if(1)
	{
		throw 1;
	}
}
void Test()
{
	int *pi = new int(1);
	try
	{
		DoSomeThing();
	}
	catch(...)
	{
		delete pi;
		throw;
	}
	delete pi;
}
int main()
{
	try
	{
		Test1();
	}
	catch(...)
	{
		;
	}
	return 0;
}

在Test中加上一个try catch语句,作为DoSomeThing()函数抛出异常的“中介”,处理动态内存。


以上3种情况都是完全可以解决的。

但特别是当程序是类似情况3的结构,甚至更复杂的时候,我们不得不加上一大堆的代码,却仅仅是为了处理动态内存的回收。

这是十分影响开发效率的,同时程序也变得难以阅读。


二、简单的智能指针

我们知道,类的成员函数中,析构函数的存在似乎能解决动态内存回收的问题:当程序出了类的作用域,会自动调用该类的析构函数。

带着这个思想,我们定义一个名为AutoPtr的类模板

template<typename T>
class AutoPtr
{
public:
	AutoPtr(T *ptr = NULL)
		:_ptr(ptr)
	{}
	AutoPtr(AutoPtr<T> &ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = NULL;
	}
	~AutoPtr()
	{
		if (_ptr != NULL)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			_ptr = NULL;
		}
	}
	AutoPtr<T> operator=(AutoPtr<T> &ap)
	{
		if(this != &ap)		
		{
			if (_ptr != ap._ptr)	
			{
				delete _ptr;
			}
			_ptr = ap._ptr;
			ap._ptr = NULL;
		}
		return *this;
	}
	T &operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
protected:
    T* _ptr;

值得注意的是,当这个模板类的模板类型为结构体或类时,我们需要访问该类的成员,因此需要一个

"->"操作符的重载,举个例子:

已知结构体stru的定义如下:

struct stru
{
	void PrintTest()
	{
		std::cout << "hi" << std::endl;
	}
};

对于这样的类,当我们执行如下代码

	stru st1;
	stru *ps1 = &st1;
	AutoPtr<stru> aps1(ps1);
	aps1->PrintTest();//操作符的重载

最后一行发生 "->"操作符的重载,但根据之前关于 "->"操作符的定义严格意义上,访问到的是

_ps1; 这行代码实际上经过重载后,应该为:_ps1PrintTest()

其实这里是编译器为了保证代码的可读性,把_ps1PrintTest优化为 _ps1->PrintTest()。

(这个优化在g++和VS2015中都是存在的)


当然,这个智能指针并不完美,仔细观察我的拷贝构造函数和赋值操作符的重载,你会发现,每当我们进行拷贝或者赋值的时候,永远都是让源智能指针置空,也就是说,同一时间一段动态内存只能由一个智能指针来维护。

这样做是有原因的:如果有若干个个智能指针指向同一块动态空间,那么在析构的时候,将会对这块空间析构若干次,程序必然崩溃。所以我只能让这个类具有“同一时间一段动态内存只能由一个智能指针来维护”的特性。


总之,尽管不完美,但我们还是实现类一个简单的智能指针AutoPtr。有了它,我们可以不用手动delete,将释放内存的工作全部交给析构函数来处理。


上面说的“不完美”主要体现为以下几点,也是我接下来要解决的问题:


1、“同一时间一段动态内存只能由一个智能指针来维护”的特性:这个特性让它和普通的指针“不太像”,不符合普遍的编程习惯,

此外如果手动构造多个指向同一内存的智能指针,仍会导致析构函数的时候对同一内存析构多次,仍会令程序崩溃,因此这样的做法并没有根本上解决问题;

2、智能指针只能指向动态内存,如果指向静态内存,在析构的过程中必然崩溃。



三、改进

我之前写的AutoPtr有种种不完美之处,需要对其进行一些改进,其中由于拷贝和赋值的不合理导致

同一时间在特定的一段动态内存,只能存在一个AutoPtr维护,针对这个问题,ScopedPtr 和 SharedPtr都是对AutoPtr的改进

ScopedPtr类模板的定义如下:

template<typename T>
class ScopedPtr
{
public:
	ScopedPtr(T* ptr = NULL)
		:_ptr(ptr)
	{}

	~ScopedPtr()
	{
		if (_ptr)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			_ptr = NULL;
		}
	}
	T &operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
protected:
	ScopedPtr(ScopedPtr<T> &sp);
	ScopedPtr<T> &operator=(ScopedPtr<T> &sp);
protected:
	T* _ptr;
};

这里跟AutoPtr的区别在于

1:拷贝构造函数和赋值操作符重载函数只声明,没有定义

2:并且以上两个函数的声明放在了protected限定符内


采用这样的做法,当我们想对ScopedPtr类型的变量进行拷贝构造或者赋值时,由于并没有定义相应的函数,程序是无法编译通过的,因此根本上就防止了赋值行为的发生;

此外,将这两个函数放在protected限定符内也是有意义的:假如我们声明这两个函数为public,那么的确可以起到同样防止调用的效果。

但是一旦这样做,造成的后果就是,其他人在读这样的代码时,有可能会误解为我们没来得及定义这两个函数,然后“画蛇添足”地加上相应的定义,如此一来,这个ScopedPrt类就与之前我们定义的AutoPtr类没两样了。

此外,心怀恶意的“捣乱者”一旦看到这样的漏洞,破坏掉你的程序也将会轻而易举——只需要给这两个函数写上定义就可以了。

总之,将这两个函数声明为protected是有意义的,它可以防止其他人对程序的破坏行为。

                                                                             (待续)

以上是关于C++智能指针(3.30)的主要内容,如果未能解决你的问题,请参考以下文章

C++ 智能指针最佳实践&源码分析

C++ 智能指针最佳实践&源码分析

C++ 智能指针最佳实践&源码分析

C++ --- C++智能指针

c++智能指针的不断演化

C++中智能指针的原理使用实现