内存泄漏始终是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
void vector_test()
{
std::vector 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
对此,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> vc; //智能指针作为模板参数, 限定了只能放入堆内存
for(int i = 0; i < 5; ++i)
{
vc.emplace_back(new int(i));
}
return 0; //执行到此处时, 根据智能指针的RAII手法, 并不会造成内存泄漏
}
五、收尾了,收尾了(再不懂就看看这里吧)
或许还是会有不少读者困惑上一节提到的大盒子、小盒子,栈内存、堆内存之类的关系,对此我们不妨自己设计一个类,并将其放到STL容器中来验证前面那些说法吧!!
#include#include #include 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 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 > 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
至此,相信各位读者已经比较清楚:除了存放基本数据类型以外,为何STL容器中一般以智能指针来作为实际存储的元素。
换句话说,在以后的coding生涯中,以下的三种书写形式(特别是针对自定义类类型),大家知道最好应该用哪一种写法了吧!
vectorvc; vector vc; vector > vc;



