一、问题引入

关于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还有另外一种实现方式,

代码如下:

template <class T>
class OldAutoPtr
{
public:
	OldAutoPtr(T *ptr)
		:_ptr(ptr)
		,_IsHost(true)	
	{
		std::cout << "构造" << std::endl;
	}

	OldAutoPtr(OldAutoPtr<T> &oap)
		:_ptr(oap._ptr)
		,_IsHost(true)
	{
		oap._IsHost = false;
	}

	~OldAutoPtr()
	{
		if (_IsHost)
		{
			std::cout << "释放" << std::endl;
			delete _ptr;
		}
	}

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

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

	OldAutoPtr<T>& operator=(OldAutoPtr<T> &oap)
	{
		if (_IsHost)
		{
			cout << "释放" << endl;
			delete _ptr;
		}
		_ptr = oap._ptr;
		_IsHost = true;
		oap._IsHost = false;
		return *this;
	}
	void Test()
	{
		cout << "test" << endl;
	}

protected:
	T* _ptr;
	bool _IsHost;
};

成员变量_IdHost需要保证对于多个指向同一个动态内存的指针,只能有一个指针为ture。

这样,在析构时,只对_IsHost值为ture的指针进行内存释放。

在这种实现方式下,允许多个指针指向同一块动态内存。

但这种方式存在隐患,如下面这种情况:

int a = 0;
int b = 1;
auto_ptr<int> ap1(new int(1));
if (a < b)
{
    auto_ptr<int> ap2(ap1);
}

这时候,ap1指向的空间已经释放,但仍然可以访问ap1指向的空间,这样就成为了野指针。

因此这种实现方式存在严重漏洞。

三、防拷贝措施(ScopedPtr)

我之前写的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是有意义的,它可以防止其他人对程序的破坏行为。



四、进一步改进(SharedPtr)

到这里,简单的ScopedPtr类已经实现了,它可以防止让多个指针对同一片动态内存的行为。

但前提是你必须按照规范,一旦像下面这段代码的方式使用ScopedPtr,那么我们刚才做的一系列保护就失去作用了:

	int *pi = new int(1);
	ScopedPtr<int> sp1(pi);
	ScopedPtr<int> sp2(pi);

执行上面这段代码,仍然可以让两个ScopedPtr类型的智能指针指针指向同一片动态内存,因此这两个

ScopedPtr指针在退出作用域自动调用析构函数时,必然会对同一块内存释放2次,程序崩溃。


看来,ScopedPtr仍不是最佳的解决方式。我们希望实现一种新的智能指针,它能够允许多个该智能指针指向同一块内存。


于是,我们试着实现一个名为 SharedPtr 的类,要想实现允许多个智能指针指向同一块内存,需要修改一下我们之前定义的析构函数中的那个delete语句的条件。

举个例子,有2个智能指针sp1 sp2指向同一块动态内存,那么在这两个指针的析构函数调用时,delete的行为应该只发生一次。

也就是说 假设当析构sp1时,我们应该在delete之前检测sp1所指向的内存是否还有被其他指针维护,

若有,则不delete,

若无,才发生delete。


因此,我们还需要一个计数器变量_pCount,能够告诉我们指定的内存有几个智能指针在维护。

我们可以将_pCount定义在SharedPtr类内,SharedPtr类的定义如下:

template<typename T>
class SharedPtr
{
public:
	SharedPtr(T *ptr = NULL)
		:_ptr(ptr)
		,_pCount(new long long(1))
	{}
	SharedPtr(const SharedPtr<T> &sp)
		:_ptr(sp._ptr)
		,_pCount(sp._pCount)
	{
		++*(_pCount);
	}
	~SharedPtr()
	{
		if (_ptr)
		{
			if (--(*_pCount) == 0)
			{
				delete _ptr;
				delete _pCount;
			}
		}
	}
	SharedPtr<T> &operator=(SharedPtr<T> &sp)
	{
		if (this != &sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pCount) == 0)
				{
					delete _ptr;
					delete _pCount;
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	long long UseCount()
	{
		return *(_pCount);
	}
protected:
	T *_ptr;
	long long* _pCount;
};

对于类内的这个_ptr指针成员,,每个我们在拷贝构造和赋值的时候,需要不断维护它,以保证每个指向相同内存的智能指针也同时指向相同的计数器。

有了这个计数器,在析构的时候,就可以通过计数器的值判断是否应该delete掉相应的内存了:

如果只有析构的这一个智能指针在维护这段内存,则将这段内存delete掉,同时别忘了将计数器_pCount指向的内存也delete掉。

如果判断出除了当前析构的智能指针,还有其他智能指针也在维护相应的内存,则不delete,但计数器的值要减1。


当然,在该类的赋值操作符重载函数中,为了减少程序执行的步骤,我们也可以用如下的现代式写法的方式进行定义,具体实现如下:

SharedPtr<T> &operator=(SharedPtr<T> &sp)
{
    --(*_pCount);
    SharedPtr<T> tmp(sp);
    swap(_ptr, tmp._ptr);
    _pCount = sp._pCount;
    ++(*_pCount);
    return *this;
}


五、智能数组

我们还可以参照这些智能指针类的设计思路,设计出智能数组。

智能数组用来存放动态开辟的数组,因此析构函数释放内存采用delete[]操作符。

此外,智能数组不需要* 和 -> 操作符,但需要重载[]操作符,具体的实现如下:

template<typename T>
class SharedArr
{
public:
	SharedArr(T *ptr = NULL)
		:_ptr(ptr)
		, _pCount(new long long(1))
	{}
	SharedArr(const SharedPtr<T> &sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		++*(_pCount);
	}
	~SharedArr()
	{
		if (_ptr)
		{
			if (--(*_pCount) == 0)
			{
				delete[] _ptr;
				delete _pCount;
			}
		}
	}
	SharedArr<T> &operator=(SharedArr<T> &sp)
	{
		if (this != &sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pCount) == 0)
				{
					delete _ptr;
					delete _pCount;
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}
		}
		return *this;
	}
	T &operator[](size_t index)
	{
		return _ptr[index];
	}
	long long UseCount()
	{
		return *(_pCount);
	}
protected:
	T *_ptr;
	long long* _pCount;
};


可以增加一个指针成员变量保存数组的长度,修改相应的构造、拷贝构造、赋值操作符等,从而实现数组的打印等操作。

但缺点是在调用这个类的构造函数时,必须传入准确的数组长度,否则程序会出错

实现代码如下:

template<typename T>
class SharedArr
{
public:
	SharedArr(T *ptr = NULL, long long size = 0)
		:_ptr(ptr)
		, _pSize(new long long(size))
		, _pCount(new long long(1))
	{}
	SharedArr(const SharedPtr<T> &sp)
		:_ptr(sp._ptr)
		,_pSize(sp._pSize)
		, _pCount(sp._pCount)
	{
		++*(_pCount);
	}
	~SharedArr()
	{
		if (_ptr)
		{
			if (--(*_pCount) == 0)
			{
				delete[] _ptr;
				delete _pCount;
				delete _pSize;
			}
		}
	}
	SharedArr<T> &operator=(SharedArr<T> &sp)
	{
		if (this != &sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pCount) == 0)
				{
					delete _ptr;
					delete _pCount;
					delete _pSize;
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_size = sp._pSize;
				++(*_pCount);
			}
		}
		return *this;
	}
	T &operator[](size_t index)
	{
		return _ptr[index];
	}
	long long UseCount()
	{
		return *(_pCount);
	}
	void Print()
	{
		for (int i = 0; i < *(_pSize); i++)
		{
			cout << _ptr[i] << " ";
		}
		cout << endl;
	}
protected:
	T *_ptr;
	long long* _pSize;
	long long* _pCount;
};


六、定置删除器

在我之前实现的SharedPtr中,在一些需要释放内存的地方(如析构函数、赋值操作符重载),采用了delete操作符,然而,对于非new操作符开辟的内存,这会导致程序崩溃。

比如:

int a = 10;
int *p = &a;
SharedPtr<int> sp1(p);

此外,当智能指针用于文件操作时,我们希望成员方法中用到delete的地方能产生类似fopen的效果,因此需要用到定置删除器。实现方式如下:

struct DefaultDel
{
	void operator() (void *ptr)
	{
		std::cout << "DefaultDel::operator()" << std::endl;
		delete ptr;
	}
};
struct Free
{
	void operator() (void *ptr)
	{
		std::cout << "Free::operator()" << std::endl;
		free(ptr);
	}
};

struct Fclose
{
	void operator() (void *ptr)
	{
		std::cout << "Fclose::operator()" << std::endl;
		fclose((FILE*)ptr);
	}
};

struct Stack
{
	void operator() (void *ptr)
	{
		std::cout << "Stack::operator()" << std::endl;
	}
};


//基于定置删除器的类模板


template<class T, class D = DefaultDel>
class SharedPtr
{
public:
	SharedPtr(T *ptr)
		:_ptr(ptr)
		, _pCount(new long long(1))
	{}

	SharedPtr(T *ptr, D del)
		:_ptr(ptr)
		,_pCount(new long long(1))
		,_del(del)
	{}

	SharedPtr(const SharedPtr<T, D> &sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
	{
		++*(_pCount);
	}

	void _Release()
	{
		_del(_ptr);
		delete _pCount;
	}
	~SharedPtr()
	{
		if (_ptr)
		{
			if (--(*_pCount) == 0)
			{
				_Release();
			}
		}
	}
	SharedPtr<T, D> &operator=(SharedPtr<T, D> &sp)
	{
		if (this != &sp)
		{
			if (_ptr != sp._ptr)
			{
				if (--(*_pCount) == 0)
				{
					_Release();
				}
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				++(*_pCount);
			}
		}
		return *this;
	}
	T &operator[](size_t index)
	{
		return _ptr[index];
	}
	long long UseCount()
	{
		return *(_pCount);
	}
protected:
	T *_ptr;
	long long* _pCount;
	D _del;
};

在这种实现方式中,我定义了四种类:DefaultDel、Free、Fclose、Stack,它们分别可以对应new、malloc开辟的动态内存,以及用于文件操作(指向fopen类型数据)的指针、指向栈空间(静态内存)的指针。

然后在每个类中实现()操作符的重载,重载函数实现了它们对于指定内存类型的处理方式。这样就可以用类似函数的方式调用这些重载函数。

为了方便起见,在SharedPtr类中,我还声明了D类型的成员变量_del,这样调用_del()就可以实现相应类型的结构体中的()重载函数,也就是定置删除的功能。

使用方式如下:

int a = 10;
int *p = &a;
SharedPtr<int, Stack> sp1(p);

由于p指向的内存不是new操作符开辟的,因此模板参数不能用缺省值,

Stack是针对栈内存的定置删除器,因此采用Stack



七、梳理


智能指针的实现,是利用了类的析构函数“已构造的对象最终会销毁,即它的析构函数最终会被调用”的特性,即RAII。

智能指针的意义在于让我们不用关心指针指向对象的回收、处理。

因此对于特定的内存,我们需要写特定的定置删除器进行处理。

以上是我实现的AutoPtr ScopedPtr SharedPtr,

实际上在boost库中,已经实现同名的智能指针,只不过命名方式为下划线法(如shared_ptr)

此外,在C++新标准中,这些智能指针也是标准库中的成员,我们只需要引头文件 memory即可使用这些智能指针(其中,scoped_ptr被“改名”为 unique_ptr)。


//关于boost库的编译,这是我的步骤

http://zhweizhi.blog.51cto.com/10800691/1760312