首页 > 程序开发 > 软件开发 > C语言 >

有效的使用和设计COM智能指针——条款1:智能指针之前世今生

2011-09-15

条款1:智能指针之前世今生我最初研究COM引用计数和智能指针时候,是先从编写_com_ptr_t和CComPtr的API文档开始的。那时,我的项目经理曾多次问我_com_ptr_t和CComPtr的一些的背景。诸如:它所属的库,产生的历史...


条款1:智能指针之前世今生
我最初研究COM引用计数和智能指针时候,是先从编写_com_ptr_t和CComPtr的API文档开始的。那时,我的项目经理曾多次问我_com_ptr_t和CComPtr的一些的背景。诸如:它所属的库,产生的历史原因。并提议我将这些内容融入到文档中去。

这是个很好的建议,我这么做了。取得的效果也觉得十分只令人满意。最明显之处在于,原本需要大篇幅解释的问题会因为前面一个背景简介,而缩小很多篇幅。读者也便于理解。当然如果我坚持这么继续做下去的话可能会带来更多的附加好处:干巴巴的“纪传体”API文档宛若变成了“编年体”小说。确实能够让每天趴在电脑前,文档看到昏昏欲睡的程序员提起不少精神。

因此也不妨让我们了解更多关于COM接口的智能指针的其他内容前,回顾一下智能指针产生的历史原因。这可能会帮你回答更多的“为什么?”。

在只有C的时代,对于资源释放的问题,向来是留给程序员手动完成的。如:有molloc应当存在与之对应的free。使用fopen打开文件就应当有fclose将文件关闭。这些程序员来完成,都是天经地义的。若是使用完资源而没有将其返还给系统,那么我们更多的情况下认为这个程序存在着某种资源泄漏,需要对其进行修改。

貌似一个malloc与free成对出现并不难,只要一个程序员不疏忽而认真编码这类问题都可以解决。资源释放与回收只是个态度问题,而并非一个技术性难题。当时很多公司要求函数单入单出,以保证所有资源在函数出口处被释放。

但是随着语言的进一步发展,一些新的C++的语言特性引入到我们的程序之中,这使得我们难以保证资源被释放的过程必定发生。最明显的便是异常:

view plaincopy to clipboardprint?void f()
{
Investment* pInv = new Investment(123);
...
delete pInv;
}
void f()
{
Investment* pInv = new Investment(123);
...
delete pInv;
}


经过前面的介绍,应当会发现“...”可能才在一个return语句,不小心跳过了后面的delete。这或许只是某个程序员的态度问题。也或许是在其他程序员修改编码的时候,疏忽所造成的。但如果这个地方抛出了一个异常呢?程序轨迹被打乱了,你可能不得不下大精力来写一些烦人的try{}catch{}语句,然后针对不同情况做出处理并释放相应的资源。若可能抛出异常的情况很多呢?额~稍微有经验的程序员用大腿想想都能知道try{}catch{}层层嵌套带来的逻辑有多么复杂!

于是乎资源释放从态度问题升级为一个技术性难题。怎么有效管理资源?以不至于泄漏?

于是最初想到的办法是通过一个栈上(stack-based)的资源管理一个堆上(heap-based)资源。如果利用栈上对象出栈过程所调用的析构函数来释放掉所持有的堆上资源,那无论何种情况下资源都不会泄漏了。这确实是个绝妙的注意,也奠定了智能指针的一个核心思想:“用栈中对象管理堆上或外部资源”这个在栈上创建的对象名曰“资源管理对象”,确实很形象。

view plaincopy to clipboardprint?void f()
{
Investment* pInv = new Investment(123);
InvestmentManager investmentManager(pInv); //将资源交给一个资源管理对象使用
//不管这后面出现什么问题,只要函数出栈了,资源都能被自动释放
investmentManager.getInvestment().DoSomeThing();
...
}
void f()
{
Investment* pInv = new Investment(123);
InvestmentManager investmentManager(pInv); //将资源交给一个资源管理对象使用
//不管这后面出现什么问题,只要函数出栈了,资源都能被自动释放
investmentManager.getInvestment().DoSomeThing();
...
}

之后种种思想进一步升级成了C++中的一个惯用法(idiom)名曰“RAII:Resource Acquisition Is Initialization”貌似理解起来有点困难。Bjarne Stroustrup在的《C++程序设计语言(第3版)》【14】一书中是这样解释的:使用局部对象管理资源的技术通常称为“资源获取就是初始化RAII”。这种通用技术依赖于构造函数和析构函数的性质以及它们与异常处理的交互作用。

这又与之前的资源管理思想有些什么不同之处呢?RAII更加强调的是将资源获取与一个对象的初始化过程紧密的绑定在一起,在看RAII做法之前我们看一下如果没有这种强调,程序会出现什么问题。

view plaincopy to clipboardprint?void f()
{
Investment* pInv = new Investment(123);
Profit* pPro = new Profit(123); //关注这里,如果抛出异常会怎样?
InvestmentManager investmentManager(pInv);
ProfitManager profitManager(pPro);
}
void f()
{
Investment* pInv = new Investment(123);
Profit* pPro = new Profit(123); //关注这里,如果抛出异常会怎样?
InvestmentManager investmentManager(pInv);
ProfitManager profitManager(pPro);
}

啊~ 虽然有了资源管理对象,但是如果在资源申请后没有立即将其与对象绑定起来。那资源泄漏还是发生了。

因此RAII的做法如下:


view plaincopy to clipboardprint?void f()
{
InvestmentManager investmentManager(new Investment(123););
ProfitManager profitManager(new Profit(123); //再怎么出错也不会产生问题了。
investmentManager.getInvestment().DoSomeThing();
...
}
void f()
{
InvestmentManager investmentManager(new Investment(123););
ProfitManager profitManager(new Profit(123); //再怎么出错也不会产生问题了。
investmentManager.getInvestment().DoSomeThing();
...
}

观察上面的代码,你会发现我们仍然有使这个问题处理得更好的空间。在最原始的程序中,外部资源的访问都是通过指针来实现的,如文件指针、指向内存数组的指针。那么我们是否可以将资源管理类抽象成一个类似指针的类呢?在C++为我们提供的重载那些运算符能实现这一功能。具体讨论细节可以参见条款19“巧妙的将对象伪装成指针”于是他之后呈现出的形式更加的简洁,更加类似于我们惯用的普通指针。这样做的好处是异常情况下资源不会泄漏,但值得注意的是他确实没有将资源管理的问题从程序员的编码问题中解决掉。如果你对此疑惑不解,请回顾一下条款5所讲述的内容“必须手动释放COM组件式,别妄想智能指针帮你完成”。用完后将资源及时返回给系统,仍然是程序员需要自己解决的问题。

view plaincopy to clipboardprint?void f()
{
InvestmentPtr pInvestment = new Investment(123);//重载构造函数实现RIIA
ProfitManagerPtr pProfit = new Profit(123);
pInvestment ->DoSomeThing();//这个动作太酷了,它俨然就是一个指针!
...
}
void f()
{
InvestmentPtr pInvestment = new Investment(123);//重载构造函数实现RIIA
ProfitManagerPtr pProfit = new Profit(123);
pInvestment ->DoSomeThing();//这个动作太酷了,它俨然就是一个指针!
...
}

OK智能指针诞生了~ 或许可以给他套上template这顶小帽子,他将呈现出更加的通用的形式。

view plaincopy to clipboardprint?void f()
{
MySmartPtr<Investment> pInvestment = new Investment(123);
MySmartPtr<Profit> pProfit = new Profit(123); //智能指针可以复用了
pInvestment ->DoSomeThing();//这个动作太酷了,它俨然就是一个指针!
...
}
void f()
{
MySmartPtr<Investment> pInvestment = new Investment(123);
MySmartPtr<Profit> pProfit = new Profit(123); //智能指针可以复用了
pInvestment ->DoSomeThing();//这个动作太酷了,它俨然就是一个指针!
...
}

你可能会询问智能指针的更多细节,但让我们就此打住一下。让我们先看看引用计数以及如何用智能指针简化COM开发的问题。


作者“liuchang5的专栏”

相关文章
最新文章
热点推荐