1、永恒的话题
- 内存泄漏(臭名昭著的Bug)
— 动态申请堆空间,用完不还
— C++语言中没有垃圾回收的机制
— 指针无法控制所指堆空间的生命周期
#include <iostream>
#include <string>
using namespace std;
class Test
{
private:
int i;
public:
Test(int i)
{
this->i = i;
}
int value()
{
return i;
}
~Test()
{
}
};
int main()
{
for (int i = 0; i < 5; i++)
{
Test* p = new Test(i);
cout << p->value() << endl;
}
return 0;
}
这个程序的问题在于在代码的27行,每for循环一次,就new一个对象,但是只用一个指针去指向这个对象,没释放内存,但是指针马上又指向了另一段内存,这就造成了内存泄漏。
2、深度的思考
- 我们需要什么
— 需要一个特殊的指针
— 指针生命周期结束时主动释放堆空间
— 一片堆空间最多只能由一个指针标识 //避免多次释放
— 杜绝指针运算和指针比较
3、智能指针分析(使用对象代替指针)
- 解决方案
— 重载指针特征操作符(->
和*
)
— 只能通过类的成员函数重载
— 重载函数不能使用参数
—只能定义一个重载函数
#include <iostream>
#include <string>
using namespace std;
class Test
{
private:
int i;
public:
Test(int i = 0)
{
cout << "Test(int i = 0)" << endl;
this->i = i;
}
int value()
{
return i;
}
~Test()
{
cout << "~Test()" << endl;
}
};
class Pointer
{
private:
Test* mp;
public:
Pointer(Test* p = NULL)
{
mp = p;
}
Test* operator->()
{
return mp;
}
Test& operator*()
{
return *mp;
}
~Pointer()
{
delete mp;
}
};
int main()
{
for (int i = 0; i < 5; i++)
{
Pointer p = new Test(i); //其实赋值符号的左右两边都是定义对象,左边是对成员变量进行初始化,右边是new 一段堆上的空间,两个直接对等,相当于用mp就指向堆空间。
cout << p->value() << endl;
}
return 0;
}
- 重点
1、->
操作符的重载 返回值类型 为 类指针
2、*
操作符的重载 返回值类型 为 类引用
从结果我们可以发现,指针指向的内存被释放了,不会造成内存泄漏。智能指针真的强。
- 分析
我们分析一下智能指针的原理:因为析构发生在对象生命周期结束的时候,我们的Pointer p是一个栈对象,栈上的对象会生命周期结束的时候自动析构。每一次循环结束后,p都会被析构,(p的作用域就是那个for循环的花括号{ },每次花括号循环一次结束p的生命周期就结束了)从而调用~Pointer()函数里面的delete mp;堆空间被delete,最后调用堆空间的析构函数。总结来说就是:栈的析构使得指针指向的堆空间被delete。
分享一个顺序的程序:
#include <iostream>
#include <string>
using namespace std;
class Test
{
private:
int i;
public:
Test(int i = 0)
{
cout << "Test(int i = 0)" << endl;
this->i = i;
}
int value()
{
return i;
}
~Test()
{
cout << "~Test()" << endl;
}
};
class Pointer
{
private:
Test* mp;
public:
Pointer(Test* p = NULL)
{
cout << "Pointer(Test* p = NULL)" << endl;
mp = p;
}
Test* operator->()
{
return mp;
}
Test& operator*()
{
return *mp;
}
~Pointer()
{
cout << "~Pointer()" << endl;
delete mp;
}
};
int main()
{
for (int i = 0; i < 5; i++)
{
Pointer p = new Test(i);
cout << p->value() << endl;
}
return 0;
}
4、智能指针的再分析
- 在前面第二点,我们想要智能指针满足以下几个需求
1、 需要一个特殊的指针
2、 指针生命周期结束时主动释放堆空间
3、 一片堆空间最多只能由一个指针标识 //避免多次释放
4、 杜绝指针运算和指针比较
经过上面的一系列研究,我们解决了上面的第一条和第二条。那如何解决第三条和第四条的需求呢?
答案是:拷贝构造函数 和 赋值操作符重载
#include <iostream>
#include <string>
using namespace std;
class Test
{
private:
int i;
public:
Test(int i = 0)
{
this->i = i;
cout << "Test(int i = 0)" << endl;
}
int getI()
{
return i;
}
~Test()
{
cout << "~Test()" << endl;
}
};
class Pointer
{
private:
Test* mp;
public:
Pointer(Test* p = NULL)
{
mp = p;
}
Pointer(const Pointer& obj)
{
mp = obj.mp;
const_cast<Pointer&>(obj).mp = NULL;
}
Pointer& operator=(Pointer& obj)
{
if (this != &obj)
{
delete mp;
mp = obj.mp;
const_cast<Pointer&>(obj).mp = NULL;
}
return *this;
}
Test* operator->()
{
return mp;
}
Test& operator*()
{
return *mp;
}
bool isNULL()
{
return (mp == NULL); //判断是否为空指针
}
~Pointer()
{
delete mp;
}
};
int main()
{
Pointer p1 = new Test(0);
cout << p1->getI() << endl;
Pointer p2 = p1;
cout << p1.isNULL() << endl;
cout << p2->getI() << endl;
return 0;
}
- 分析
我们先分析一下第3点需求:一片堆空间最多只能由一个指针标识,我们是如何实现的呢?
我们借助 拷贝构造函数 和 赋值操作符重载。拷贝构造函数为Pointer(const Pointer& obj)
,在里面我们先进行指针地址的赋值,然后把被赋值对象 obj 的指针给置为空。因为 obj 是 const 类型,不能作为左值使用,所以需要一个 const_cast 的强制类型转换。
对于 赋值操作符重载 要注意四点内容:
— 返回值类型为类引用
— 参数类型为 const 类引用
— 加 if 语句防止自赋值
— 返回类本身
所以返回值类型为 Pointer 引用,参数类型为 const Pointer&
。第一句语句防止自赋值, if (this != &obj)
。然后delete mp
是为了防止 new
第二个对象,把初始化的给delete
了。mp = obj.mp; const_cast<Pointer&>(obj).mp = NULL;
这两句语句和拷贝构造函数里面是一样的。
综上,我们完成了第3点需求:一片堆空间最多只能由一个指针标识。主要原理就在于一旦出现多个我们就让上一个指向的指针指向 NULL
。
第四点需求只要我们不重载运算符,那么它就没有与这些操作符相匹配的运算符。
综上所诉,需求全部完成。唯一的缺陷在于上面的智能指针只能指向Test这个固定的类型,没办法指向其他的类型。后续学模板技术就可以指向其他的类型。
5、军规
智能指针的使用军规:只能用来指向堆空间中的对象或者变量,不能使用智能指针来指向栈空间中的对象或者变量。
- 小结
— 指针特征操作符(->
和*
)可以被重载
— 重载指针特征符能够使用对象代替指针
— 智能指针只能用于指向堆空间中的内存
— 智能指针的意义在于最大程度的避免内存问题
▲▲▲一直以来的疑惑(已解决)
Pointer p = new Test(3);
把它改成 Pointer p(new Test(3))
方便更好的理解,先调用成员函数的构造函数,把这个当做Pointer 对象构造函数里面的参数,可以达到一一对应的情况。