关键字:C++、静态对象、临时对象
本文将以一个简单的例子逐步地讲解静态对象和临时对象在C++程序设计中的使用技巧。
故事的开始
一天,我在写一个有复杂内存结构的MFC程序,为了看到我关心的内存数据结构内容,我要频繁地将大量的数据TRACE到屏幕上来。当我这么做时,天哪,我的CPU占用率达到了100%!在悲叹VC.NET的效率和我的老爷机的同时,我请出了我的珍藏法宝“DBWIN”,可是没想到,效率并没有太多提高,还使我的老爷机上所有运行着的DEBUG版程序把信息全都打印在“DBWIN”的窗口上,于是,我痛下决心,自己写一个TRACE。我的想法很简单,申请一个Console,将信息写上去。
第一版:丑小鸭的诞生
我用最简单的技术,大体上需要在程序运行开始时加入一个Console,在结束时自动释放掉。所以我利用了一下C++对象特性。写了一个类:
class MyConsole
{
public:
MyConsole()
{
m_bHaveConsole=AllocConsole();
}
~MyConsole()
{
if(m_bHaveConsole)
FreeConsole();
}
BOOL m_bHaveConsole;
};
有了它之后,只要定义一个全局类对象,就拥有了程序开始得到控制台,结束自动释放的好处。然后添加了如下的代码:
#include <conio.h>
#undef TRACE
#define TRACE _cprintf
在XApp.cpp定义全局的theApp前写上:
MyConsole theConsole;//这样我们拥有了一个全局可用的控制台!!!
XApp theApp;
好了,编译、运行,现在,所有的TRACE都打印在Console上了。
可是,第一只丑小鸭有很大缺点,首先,每次使用之前打扰了用户的代码,我们都想写用户仅需#include头文件就能运行的代码,而这里必须在CPP文件中加入MyConsole theConsole,这让我心如油煎。可是,该对象实体不能加在.H文件中。我开始想到的是静态类成员,可是这样的.H不能包含在两个以上的.CPP中,否则链接会有错误。于是,我想到了静态类对象。
第二版:成为天鹅前的涅槃
我把我的MyTrace.H改为如下内容
#ifndef MYTRACE_HPP
#define MYTRACE_HPP
#include <conio.h>
class MyConsole
{
public:
MyConsole()
{
m_bHaveConsole=AllocConsole();
}
~MyConsole()
{
if(m_bHaveConsole)
FreeConsole();
}
BOOL m_bHaveConsole;
};
class MyTraceCondition
{
public:
static Fun()
{
static MyConsole theConsole;
}
};
#undef TRACE
#define TRACE MyTraceCondition::Fun();_cprintf
#endif
这里我第一次用到了静态类对象,注意它与全局静态对象(包括静态类成员其实也是全局静态的一种变形)的区别。静态类对象在第一次使用时被构造,在以后的反复使用中并不被重新创建,也就是说,你可以这样来验证:
MyConsole()
{
m_bHaveConsole=AllocConsole();
_cprintf(“alloc\n”);
}
~MyConsole()
{
if(m_bHaveConsole)
{
_cprintf(“free\n”);
FreeConsole();
}
你会发现仅在第一次调用TRACE时,会输出“alloc”。有了静态对象的使用,我们可以把一个对象实体隐藏在.H文件中,不需要打扰用户的代码了。而且该方法还是高效的,效率和全局对象一样高。但是,当我的工程中运行两个以上线程都要用到TRACE时,我又有了新问题。你看到了,这种调用TRACE的方式并非线程安全,至少会使打印出来的信息杂乱地混在一起。你也许想到了使用线程安全地CRT版本,千万别那样做,多线程的代码大部分是不需要互斥的,如果为了一点代码的安全而导致整体代码效率降低,不值得。
于是我开始了在.H文件中添加临界区。要求所有的线程在调用TRACE中的_cprintf时进入相同的临界区。于是我必须对_cprintf进行点儿包装。我不能使用全局临界区,因为这样会导致我不得不搞出一个.CPP文件,同样我也不能使用使用静态类成员。那么使用普通的类成员做临界区吗?NO!,这样每次调用的就不是一个临界区了。于是,也许你猜到了。
——我第二次使用了静态对象!
#include <atlcore.h>
class MyTrace
{
public:
MyTrace():pTraceFun(_cprintf)
{
static CComAutoCriticalSection TheCriticalSection;
pCriticalSection=&TheCriticalSection;
pCriticalSection->Lock();
}
~MyTrace()
{
pCriticalSection->Unlock();
}
int(*pTraceFun)(const char*,...);
private:
CComAutoCriticalSection * pCriticalSection;
};
现在,不论你怎么使用,都会进入相同的临界区。
好吧,现在#define TRACE…,噢,gold!我犯了极大的错误,我简直没法写出对TRACE的重定义。怎么利用MyTrace呢?难道写成
#define TRACE MyTraceCondition::Fun();
MyTrace tracetmp;
(*tracetmp. pTraceFun)
不,我无法知道用户将怎样使用TRACE。也许他们更愿意在一对{}中使用N次,那这段代码无疑成了毒瘤。
于是,灵光一闪,我发现了成为天鹅的关键
第三版:成为漂亮的天鹅
哈哈,我将头文件内容改为这样
#ifndef MYTRACE_HPP
#define MYTRACE_HPP
#include <conio.h>
#include <atlcore.h>
class MyTrace
{
public:
MyTrace():pTraceFun(_cprintf)
{
static CComAutoCriticalSection TheCriticalSection;
pCriticalSection=&TheCriticalSection;
pCriticalSection->Lock();
}
~MyTrace()
{
pCriticalSection->Unlock();
}
int(*pTraceFun)(const char*,...);
private:
CComAutoCriticalSection * pCriticalSection;
};
class MyConsole
{
public:
MyConsole()
{
m_bHaveConsole=AllocConsole();
}
~MyConsole()
{
if(m_bHaveConsole)
FreeConsole();
}
BOOL m_bHaveConsole;
};
class MyTraceCondition
{
public:
static MyTrace Fun()
{
static MyConsole theConsole;
return MyTrace();
}
};
#undef TRACE
#define TRACE (*MyTraceCondition::Fun().pTraceFun)
#endif
哈哈,你看到了,我运用了临时对象的特性,临时对象在失去外部的引用环境时(在本例中即在TRACE执行完时)自动析构,这样以上就为我们自动生成了一个临界区。并且没有显式的对象产生。在MyTrace的构造和析构函数中打印点儿东西,你同样可以看到临时对象的构造和析构过程。
总结经验
临时对象往往是要谨慎对付的家伙,静态对象也不是所有的人都清楚的,但作为C++的FUN,总是需要创造性的思维来利用语言的强大特性,对语言的一些容易出错的细节合理利用往往可以使程序妙笔生花。希望看了这篇文章,对你有所帮助,C++万岁。
我歌月徘徊
我舞影凌乱