入门向? 一个容易被忽视的内存泄露角度(STL容器中为何放入智能指针会比较好?)

一、问题由来(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;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值