有效的使用和设计COM智能指针——条款8和条款9
条款8:对智能指针的使用规则烂熟于心
我们在第一章的时候接触到了普通接口引用计数的规则(条款2)。那么在开始这一章节之前,我们来看一下使用智能指针要遵循哪些规则。智能指针的使用规则相对于接口指针会更加复杂,但考虑到他所提供的便利以及安全性。或许谨记这些规则是值得的:
1.资源申请之时将其立即易做图到一个智能指针之上(RAII)。详见条款1。
2.函数返回一个接口指针之前,调用Dettach()及时将智能指针与接口指针解绑定。详见条款12。
3.智能指针需使用Attach()函数接收来自某一函数返回值中的接口指针。详见条款12。
4.利用传出参数传出接口指针时,需要保证传入的智能指针为空。详见条款25。
条款9:尽可能不将智能指针放置于堆上
先来看一段哭笑不得的代码:
view plaincopy to clipboardprint?void SomeApp()
{
CComPtr<ICalculator>* pspMyCalculator = new CComPtr<ICalculator>;
(*pspMyCalculator).CoCreateInstance(CLSID_CALCULATOR);
pspMyCalculator->DoSomething();
delete pspMyCalculator;
}
void SomeApp()
{
CComPtr<ICalculator>* pspMyCalculator = new CComPtr<ICalculator>;
(*pspMyCalculator).CoCreateInstance(CLSID_CALCULATOR);
pspMyCalculator->DoSomething();
delete pspMyCalculator;
}
我担保你不会写出像上面那样的代码,原因是他将“智能”这顶桂冠从智能指针头上又摘了下来。当然,赋值运算符重载中仍将调用AddRef,智能指针也仍旧给我们提供类型安全的特性。但异常安全,自动释放引用计数,已经离我们远去。想想DoSomething()抛出一个异常,会发生什么?资源泄漏了……
或许你说你永远不会傻到写出上面这样的代码来。然后,类似的悲剧还是发生了,只不过它更加隐晦:
view plaincopy to clipboardprint?class MyClass
{
public:
MyClass();
~MyClass();
DoSomething();
private:
CComPtr<ICalculator> m_spCalculator;
};
class MyClass
{
public:
MyClass();
~MyClass();
DoSomething();
private:
CComPtr<ICalculator> m_spCalculator;
};
这个声明没有问题,但是下面的使用方式却带来了一个潜在问题。
view plaincopy to clipboardprint?void SomeApp()
{
MyClass* pMyClass = new MyClass(); // 哦~ 果然还是出问题了。
...
delete pMyClass;
}
void SomeApp()
{
MyClass* pMyClass = new MyClass(); // 哦~ 果然还是出问题了。
...
delete pMyClass;
}
试想一下上面代码会发生什么,情况不容乐观。智能指针出栈的条件是delete pMyClass。但如果他永远无法执行到呢?内存泄漏又发生了。
或许你会说这种问题还是太过于明显,但我的举例只是让你明白问题的所在。在正真的代码中MyClass可能有是另外一个堆上对象的成员属性,而这个堆上对象的申请过程可能在一个Init()函数中,释放过程却在UnInit()函数中,这中间相差十万八千里。要发现他是多么的困难。
因此忠告是“尽可能不将智能指针放置于堆上”,对于堆上出现的智能指针一定要严格提防。但是你可能还有最后一个疑惑:为什么是“尽可能”而不是“切勿”?难道某些时候我们还是需要堆上的智能指针吗?
答案是肯定的,我们需要堆上的智能指针。你可能会问,如果我们确实需要一个在堆上的资源,那应该如何做呢?
一种特殊情况是资源的生命周期与程序进程相同,因此而不存在提前释放资源的难题,如下这种做法。那它是安全的:
view plaincopy to clipboardprint?class TheApp
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //这个接口在整个程序生命周期中使用
};
TheApp g_theApp; //全局对象会在堆上开辟空间。
TheApp *g_pTheApp = NULL; //或者采用这种方式,在另一个地方new出来。
class TheApp
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //这个接口在整个程序生命周期中使用
};
TheApp g_theApp; //全局对象会在堆上开辟空间。
TheApp *g_pTheApp = NULL; //或者采用这种方式,在另一个地方new出来。
若的生命周期是全局的,那就将其放置到堆上不会出现太多问题。毕竟他可能需要在程序结束之后才释放。但若他的生命周期并非全局,而又需要动态申请的情况呢?
正确的做法是再用一个资源管理对象或智能指针对象负责此堆上的资源。他大概会像如像这样:
view plaincopy to clipboardprint?class TheApp //若TheApp需要最堆上申请
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //一个会出现在堆中的智能指针
};
auto_ptr<TheApp> func()
{
auto_ptr<TheApp> spTheApp(new TheApp); //用另一个智能指针负责堆上资源。
...
return spTheApp; //将其传递出去,以延续堆上对象的生命周期
}
class TheApp //若TheApp需要最堆上申请
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //一个会出现在堆中的智能指针
};
auto_ptr<TheApp> func()
{
auto_ptr<TheApp> spTheApp(new TheApp); //用另一个智能指针负责堆上资源。
...
return spTheApp; //将其传递出去,以延续堆上对象的生命周期
}
请记住“尽可能不将智能指针放置于堆上”。OK,结束了~
作者“liuchang5的专栏”
补充:软件开发 , C语言 ,