一、内存泄露
C++没有内存管理机制,对于建立在堆上的内存需要手动释放。比如我们new了一个对象,会占用堆上的一块内存,返回指针,如果不手动delete这个指针,那么这块内存就会一直被占用,直到进程结束后被操作系统回收,这就是所谓的内存泄露。
class Test {
public:
Test() { cout << "Test start..." << endl; }
~Test() { cout << "Test end..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
int main ( )
{
Test * test = new Test();
}
结果仅仅输出了:
Test start…
说明没有调用析构函数,这块内存一直被这个已经不需要的对象占用着。
二、智能指针的基本思想
首先很容易想到一个事情:对于建立在栈上的对象,我们不必考虑对象本身内存泄露的问题。因为当函数结束后,编译器会自动对栈上的对象调用析构函数。所以,我们可以把堆上对象的指针,托管给栈上的对象。当栈上对象调用析构函数的时候,连带delete堆上对象的指针。
于是,我们可以很好的总结智能指针的基本要素。
(1)智能指针本质上是一个模板类,模板参数表示实际上要托管的指针指向的对象的类型。
(2)这个模板类有一个成员变量,类型是:指向模板参数类型的指针,由这个成员变量存储被托管的指针。
(3)这个模板类的构造函数应该将模板参数类型的指针作为参数,以便于创建对象时候传入被托管指针。
(4)这个模板类的析构函数中,应该delete被托管指针。
(5)这个模板类应该自定义拷贝构造函数,实现被托管指针控制权的转移。但是注意,在通常的拷贝构造函数中,参数类型为const引用,但是这里应该是非const的引用,因为要保证被拷贝的智能指针,解除对于堆对象指针的托管。
(6)这个模板类应该有自定义的赋值构造函数,原理同上。
(7)通过这个模板类的对象,可以拿到被托管的指针。
(8)既然名字叫智能指针,那么这个模板类一定要用起来像指针才对。所以它要重载->运算符,重载*运算符。
上述逻辑可以说是足够简单了,这就是大名鼎鼎的智能指针的最初设计思想。在2003年版本的C++中将其付诸实践,这就是第一代智能指针:auto_ptr。
三、auto_ptr
基本思想在上面了,我们大可以自己实现一个智能指针。本人没有看过03版本的auto_ptr源码,但是基本思想相信大差不差。
以下是自己实现的auto_ptr代码:
template<typename Ty> //Ty是被托管的指针类型
class my_ptr{
private:
Ty * ty; //被托管的指针
public:
explicit my_ptr(Ty * ty1=0):ty(ty1){}
~my_ptr(){delete ty;} //析构函数中释放资源
Ty * get(){ //返回被托管的指针
return ty;
}
Ty * release(){ //转移被托管的指针
Ty * temp = ty;
ty = 0;
return temp;
}
void reset(Ty * ty2){ //重置被托管的指针
delete ty;
ty = ty2;
}
Ty * operator ->(){ //重载->和*运算符,让智能指针用起来像指针
return ty;
}
Ty & operator * (){
return *(ty);
}
my_ptr(my_ptr & ty1){ //拷贝构造函数
this->ty = ty1.release();
}
my_ptr& operator = (my_ptr && ty1 ){ //赋值运算符重载
reset(ty1.release());
return *this;
}
};
我们可以测试使用一下:
(1)测试自动内存管理:
class Test {
public:
Test() { cout << "Test start..." << endl; }
~Test() { cout << "Test end..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
int main ( )
{
my_ptr<Test> ptr1(new Test());
}
输出:
Test start...
Test end...
自动释放堆内存。
(2)测试运算符重载:
class Test {
public:
Test() { cout << "Test start..." << endl; }
~Test() { cout << "Test end..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
int main ( )
{
my_ptr<Test> ptr1(new Test());
cout<<ptr1->getDebug()<<endl;
cout<<(*ptr1).getDebug()<<endl;
}
输出:
Test start...
20
20
Test end...
用起来像指针。
(3)测试拷贝构造函数与移动构造函数的资源转移:
class Test {
public:
Test() { cout << "Test start..." << endl; }
~Test() { cout << "Test end..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
int main ( )
{
my_ptr<Test> ptr1(new Test());
cout<<ptr1.get()<<endl;
my_ptr<Test> ptr2(ptr1);
cout<<ptr2.get()<<endl;
cout<<ptr1.get()<<endl;
}
输出:
Test start...
0x23270d418e0
0x23270d418e0
0
Test end...
拷贝构造函数,资源转移成功。
赋值运算符重载也一样,不演示了。
四、auto_ptr的弊端
如果使用Clion等高级IDE,你会发现auto_ptr会被标记中间划线,系统提醒你,这个只能指针尽可能不要用。
这个设计看起来非常棒,但是实际上,03版的智能指针,是一个非常草率,甚至可以说是一个非常失败的设计。auto_ptr问世以后,不久就被程序员集体抛弃,以至于现在提智能指针,大家都认为是C++11中才提出来的。
这个设计究竟有什么致命缺陷呢?看代码:
class Test {
public:
Test() { cout << "Test start..." << endl; }
~Test() { cout << "Test end..." << endl; }
int getDebug() { return this->debug; }
private:
int debug = 20;
};
void f(auto_ptr<Test> ptr){}
int main ( )
{
auto_ptr<Test> ptr(new Test());
f(ptr);
cout<<ptr->getDebug();
}
其中f不过是一个空函数,也会导致程序崩溃退出,为什么呢?因为将ptr传过去以后,会在栈上建立临时对象,调用运算符重载函数对其赋值,相当于将ptr持有的指针转移给临时对象!这个时候ptr中托管的就是一个空指针,你在不知不觉的情况下还想用就会导致程序崩溃!
简单地说,就是auto_ptr不带限制的资源转移,可能会产生比内存泄漏更恶心的事情。这个问题直到C++11才解决,至于怎么解决的,敬请期待下一篇~