一、问题由来(delete这样用?)
内存泄漏始终是C/C++开发中令人头疼的一个问题,大家也都知道free和malloc、new和delete要匹配使用,且数量不能多也不能少。
可用到后面的时候,会有部分开发者一看到指针出现,就想着将其delete掉,以至于出现下面这样尴尬的情形:
void func(int* p)
{
do_something();
delete p;
}
int main()
{
func(new int(10));
int x = 10;
func(&x);
return 0;
}
对于上面的代码片段,相信各位读者都能看出问题所在。造成这样尴尬局面的原因在于:对于func函数的设计者而言,是没法判断调用者传递进来的形参指针p的归属(来自于栈还是堆呢)。同时,在内存管理问题上若我们遵循“在哪申请,就在哪释放”的原则,自然也就不会写出上面这样令人尴尬的代码。
二、我就是要这样(引入工厂模式后的解决方案)
一些读者看完上面的代码片段后,对此提出了质疑:假如上面的func函数是Free函数呢?那不就有在Free函数中使用delete的理由了么!
这样一说貌似又挺有道理的,可问题也就随之出现了:形参指针p指向的内存可能来源于不同的存储空间,我们如何判断p指向的是堆空间的内存并进行释放呢?栈空间的内存不受Coder的管理,而且我们只关心堆内存的释放,自然可以想到从重载new这一途径入手。
下面是关于这一手法的演示性代码,此处用到一个常用的设计模式“工厂模式”:
class Test
{
private:
int m_i;
bool m_flag; //标记是否为堆内存
void* operator new(size_t size) noexcept
{
return malloc(size);
}
Test(const Test&);
Test& operator = (const Test&);
public:
Test() :
m_flag(false),
m_i(0)
{
}
~Test()
{
}
bool flag()
{
return m_flag;
}
static Test* NewInstance() //工厂方法
{
Test* ret = new Test();
if(ret)
{
ret->m_flag = true;
}
return ret;
}
};
void Free(Test* pt)
{
if(pt->flag())
{
std::cout << "pt is heap memory." << std::endl;
delete pt;
}
else
{
std::cout << "pt is other memory." << std::endl;
}
}
int main()
{
Test t;
Test *pt = &t;
Test *pt1 = Test::NewInstance(); //通过工厂方法,获得带有标记的堆内存
Free(pt);
Free(pt1);
return 0;
}
(程序结果及说明)
三、前面的只是开胃小菜,这才是正戏
阅读到此处的读者,可能已经慢慢开始感觉到,笔者似乎在有意地去引导大家区分栈内存和堆内存的行为,这样做的目的其实也是为了引出一种内存泄漏的角度。
先以STL中的vector容器来说明问题:vector<int*> vc,若该容器中存放的都是指向int的指针,且往其中放入了若干元素,则当vc析构或手动调用clear方法时,容器中的每个元素都会被进行“销毁移除”?。
void vector_test()
{
std::vector<int*> vc;
for(int i = 0; i < 5; ++i)
{
vc.emplace_back(new int(i));
}
std::cout << "before: vc_size = " << vc.size() << std::endl;
vc.clear();
std::cout << "after: vc_size = " << vc.size() << std::endl;
}
int main()
{
vector_test();
return 0;
}
(程序结果及分析)
这样一看好像还挺对的?似乎for循环中new出来的5份堆空间都被delete掉了(并未发生内存泄漏),事实真是如此吗?为了验证这一想法,可以用内存泄漏检查工具来判断。笔者使用Linux环境下的valgrind工具来完成这一任务:
可以看到,即使是手动调用了vector的clear方法,vector中保存的5个指针没有一个被delete掉。原因是什么?难不成是系统出现了Bug?
让我们再次聚焦于vector<int*> vc这行代码,int*表示指向的数据是int,且数据的来源有可能是栈空间或堆空间,但vector的设计者并不能确定填入的模板参数具体是什么,以及当模板参数为指针时,每个指针元素指向的是堆内存还是栈内存呢?
对此,vector并不会对放入的指针元素进行delete操作,因为它不能保证delete的就是堆内存,万一放入的是栈内存呢?那岂不是就程序崩溃咯!鉴于此,vector根本不会去delete其中的元素。那么,vecotr提供的clear方法到底干了什么?它销毁的空间又是什么?
四、拨开迷雾,引出智能指针
从官方文档中可知:vector容器本身也需要自己的内存空间,这样它才能存放一定数量的元素(类型为T),而这段内存空间是靠allocator分配器所分配的堆内存(通常不需要用户显示指定)。
你可以将allocator分配的这段内存空间视为一个大的外壳盒子,而模板参数T不论是普通类型还是类类型,其对应的对象实例都有自己的内存空间。如T=int*,则意味着vector容器中的每个元素都为int*指针(可将这每个指针指向的空间都视为一个小盒子,而每个小盒子的内存空间既可以是栈内存也可以是堆内存)。
当调用vector的clear方法时,销毁的只是大盒子(释放的只是大盒子的空间),其中的元素(小盒子)则交给编译器或者调用者自己管理(堆空间就得手动delete,栈空间则就由编译器自己回收了)。
相信读者或多或少的都会感觉这种方式的不便之处,即很容易就会产生内存泄漏。既然T=int*这样的裸指针不行,那么将T换为智能指针不就行了!换为智能指针后,不仅排除了容器中可能存在栈内存的干扰,而且还解决了堆内容自动回收的问题。
int main()
{
std::vector<std::unique_ptr<int>> vc; //智能指针作为模板参数, 限定了只能放入堆内存
for(int i = 0; i < 5; ++i)
{
vc.emplace_back(new int(i));
}
return 0; //执行到此处时, 根据智能指针的RAII手法, 并不会造成内存泄漏
}
五、收尾了,收尾了(再不懂就看看这里吧)
或许还是会有不少读者困惑上一节提到的大盒子、小盒子,栈内存、堆内存之类的关系,对此我们不妨自己设计一个类,并将其放到STL容器中来验证前面那些说法吧!!
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Test
{
private:
int *mP;
int mi;
public:
Test(int i) : mi(i), mP(new int(i))
{
cout << "Test(int i) is called." << endl;
}
~Test()
{
cout << "~Test() is called." << endl;
delete mP;
}
};
int main()
{
vector<Test*> vc;
Test t(0);
vc.emplace_back(&t);
vc.emplace_back(new Test(1));
vc.clear(); //此处会触发Test析构吗?
cout << "vc clear OK..." << endl;
cout << endl;
vector<unique_ptr<Test>> arr;
arr.emplace_back(new Test(0));
arr.emplace_back(new Test(1));
arr.clear(); //此处呢?会不会触发Test析构?
cout << "arr clear OK..." << endl;
return 0;
}
(程序结果及分析)
当vc容器中存放的是裸指针Test*时,我们往其中分别放入了一个栈实例和堆实例。随后手动调用vc.clear()时,发现并没有出现Test析构函数的打印(实际运行结果末行的输出,就是栈对象t的析构,只不过t的析构是return 0时的结果,而不是clear调用的结果)。
当arr容器中存放的是智能指针unique_ptr<Test>时,就已经严格限定该容器中只能放入unique_ptr类对象(由它来负责帮我们管理堆内存)。当调用arr.clear()时,除了容器本身的内存被释放掉,其中的元素(因为是类对象实例,而不是裸指针)也会被触发自身的类析构函数:先调用unique_ptr的析构函数,接着又会调用到Test的析构函数(很容易通过断点调试来观察,这样的调用顺序)。
六、总结
至此,相信各位读者已经比较清楚:除了存放基本数据类型以外,为何STL容器中一般以智能指针来作为实际存储的元素。
换句话说,在以后的coding生涯中,以下的三种书写形式(特别是针对自定义类类型),大家知道最好应该用哪一种写法了吧!
vector<Test> vc;
vector<Test*> vc;
vector<unique_ptr<Test>> vc;