标签:类;c++
一、问题引入
关于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++
原文地址:http://zhweizhi.blog.51cto.com/10800691/1758585