Paul DiLascia
问题
我的问题是关于初始化C++类成员的。我见过许多这样的代码(包括在你的栏目中也见到过):
CSomeClass::CSomeClass()
{
x=0;
y=1;
}
而在别的什么地方则写成下面的样子:
CSomeClass::CSomeClass() : x(0), y(1)
{
}
我的一些程序员朋友说第二种方法比较好,但他们都不知道为什么是这样。你能告诉我这两种类成员初始化方法的区别吗?
回答
从技术上说,你的程序员朋友是对的,但是在大多数情况下,两者实际上没有区别。有两个原因使得我们选择第二种语法,它被称为成员初始化列表:一个原因是必须的,另一个只是出于效率考虑。
让我们先看一下第一个原因——必要性。设想你有一个类成员,它本身是一个类或者结构,而且只有一个带一个参数的构造函数。
class CMember {
public:
CMember(int x) { ... }
};
因为Cmember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。
CMember* pm = new CMember; // Error!!
CMember* pm = new CMember(2); // OK
如果Cmember是另一个类的成员,你怎样初始化它呢?你必须使用成员初始化列表。
class CMyClass {
CMember m_member;
public:
CMyClass();
};
//必须使用成员初始化列表
CMyClass::CMyClass() : m_member(2)
{
•••
}
没有其它办法将参数传递给m_member,如果成员是一个常量对象或者引用也是一样。根据C++的规则,常量对象和引用不能被赋值,它们只能被初始化。
第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时。MFC的Cstring提供了一个完美的例子。假定你有一个类CmyClass具有一个Cstring类型的成员m_str,你想把它初始化为"yada yada."。你有两种选择:
CMyClass::CMyClass() {
// 使用赋值操作符
// CString::operator=(LPCTSTR);
m_str = _T("yada yada");
}
//使用类成员列表
// and constructor CString::CString(LPCTSTR)
CMyClass::CMyClass() : m_str(_T("yada yada"))
{
}
在它们之间有什么不同吗?是的。编译器总是确保所有成员对象在构造函数体执行之前初始化,因此在第一个例子中编译的代码将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。在第二个例子中编译器产生一个对CString:: CString(LPCTSTR)的调用并将"yada yada"传递给这个函数。结果是在第一个例子中调用了两个Cstring函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。在Cstring的例子里这是无所谓的,因为缺省构造函数是内联的,Cstring只是在需要时为字符串分配内存(即,当你实际赋值时)。但是,一般而言,重复的函数调用是浪费资源的,尤其是当构造函数和赋值操作符分配内存的时候。在一些大的类里面,你可能拥有一个构造函数和一个赋值操作符都要调用同一个负责分配大量内存空间的Init函数。在这种情况下,你必须使用初始化列表,以避免不要的分配两次内存。在内部类型如ints或者longs或者其它没有构造函数的类型下,在初始化列表和在构造函数体内赋值这两种方法没有性能上的差别。不管用那一种方法,都只会有一次赋值发生。有些程序员说你应该总是用初始化列表以保持良好习惯,但我从没有发现根据需要在这两种方法之间转换有什么困难。在编程风格上,我倾向于在主体中使用赋值,因为有更多的空间用来格式化和添加注释,你可以写出这样的语句:x=y=z=0;
或者memset(this,0,sizeof(this));
注意第二个片断绝对是非面向对象的。
当我考虑初始化列表的问题时,有一个奇怪的特性我应该警告你,它是关于C++初始化类成员的,它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
class CMyClass {
CMyClass(int x, int y);
int m_x;
int m_y;
};
CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
{
}
你可能以为上面的代码将会首先做m_y=I,然后做m_x=m_y,最后它们有相同的值。但是编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。结果是m_x将有一个不可预测的值。我的例子设计来说明这一点,然而这种bug会更加自然的出现。有两种方法避免它,一个是总是按照你希望它们被初始化的顺序声明成员,第二个是,如果你决定使用初始化列表,总是按照它们声明的顺序罗列这些成员。这将有助于消除混淆。
问题
我刚刚在几台机器上安装了Windows® 2000 Release Candidate 1,不知道怎样在我的MFC应用中得到具有新的Outlook风格栏目的Open对话框(见图1)。
Figure 1 The New Open Dialog
我能否只设置一个标志,或者我是否需要一个新的头文件和一个新的公共对话框的DDL?我注意到一些旧的应用程序如Notepad好像可以得到新的Open对话框而无须重新编译,但它们不是MFC应用。理想情况,我希望在Windows 9x 和Windows NT®下得到一个使用旧对话框的应用,而在Windows 2000下使用新的对话框。
Warren Stevens
回答
这个问题恐怕没有令你高兴的答复。Windows 2000新的Open对话框是用一个新版本的commdlg.dll实现的,它在边上包含“Places”栏目。显示它的函数是GetOpenFileName,与在Windows 9x 和Windows NT®下使用的相同。然而,GetOpenFileName现在使用一个新版本的OPENFILENAME,这是一个在你的应用和对话框之间传递信息的结构。新的结构有一些额外的成员:
typedef struct tagOFN {
DWORD lStructSize; // 很重要!
•••
// 正想你总是知道并且喜欢的那样
#if (_WIN32_WINNT >= 0x0500)
void* pvReserved;
DWORD dwReserved;
DWORD FlagsEx;
#endif // (_WIN32_WINNT >= 0x0500)
} OPENFILENAME, *LPOPENFILENAME;
对,是这样。Windows 2000是Windows的第5个版本,用16进制表示是0x500。如果你用_WIN32_WINNT = 0x0500编译程序,OPENFILENAME就会得到3个新成员。前两个是保留的,第三个标志域,FlagsEx,有一个新的OFN_EX_NOPLACESBAR栏目,它屏蔽了Places栏目。Windows——或者更准确的说,commdlg.dll——使用OPENFILENAME第一个成员lStructSize来决定显示那个对话框,如果lStructSize是76(旧的大小),Windows就运行旧的对话框;如果是76+3´4=88(新的大小),它就运行新的对话框——这是友好的Redmondtonians最初告诉我的。在我的研究中间,我发现这并不是完整的图画。
但是在我详细说明之前,先让我们走马观花的看一下MFC,讨论另外一个问题。在MFC应用中,你并不经常直接调用GetOpenFileName,而是使用CfileDialog——或者,框架使用CfileDialog。当用户调用File | Open,控制稀里哗啦的一路经过CWinApp::OnFileOpen和几个其它的函数,最终到达CDocManager::DoPromptFileName,这个函数创建一个CfileDialog。CfileDialog具有一个OPENFILENAME结构的数据成员:
class CFileDialog : public CCommonDialog {
OPENFILENAME m_ofn;
•••
};
这个结构的大小是当友好的Redmondtonians 编译MFC42.DLL 时OPENFILENAME的大小;换句话说,旧的大小。而且,如果你正在进行一个静态连接,MFC代码在MFC42.DLL或NAFXCW.LIB里是冻结的,你不能仅仅设置m_ofn.lStructSize为新的大小,因为CfileDialog除m_ofn外还有其它数据成员,它们将被新的OPENFILENAME的成员覆盖。不再耽搁了,我开始使用极端的方法避开这个问题。我考虑可以做些什么,类似于MFC中使用CpropertyPage那样。PROPSHEETPAGE和PROPSHEETHEADER的大小在从Windows 95到Windows 98的过程中的某处增加了,这是为了支持wizard风格的页面。为了支持新膨胀的结构,MFC提供了CpropertyPageEx和CpropertySheetEx。最初的类(不带Ex的)仍然使用旧的结构;而新的类使用新的结构。这是一种杂凑,尤其是因为afxdlgs.h具有自己的旧的结构的定义(AFX_ OLDPROPSHEETPAGE和AFX_OLDPROPSHEETHEADER),但是这样却行得通。
我对CfileDialog做了同样的事情。首先我派生一个新的CfileDialogEx类,它带有一个新的m_ofn,包含着新的OPENFILENAMEEX结构,我模仿0x500版本加以定义。我加入这3个新的成员并且使用m_ofn.重写了CfileDialog函数。不幸的是,因为大多数的MFC代码是固定的,没有任何虚拟功能,这就意味着复制原来的整个类。但是我已经下了决心。
在我认为已经找到了m_ofn出现的所有地方以后,我重写了它,高高兴兴的编译了我的代码(在Windows 98上),然后运行——结果发现我得到的仍是旧风格的对话框。而起,有一个谜团我忘了考虑:如果Windows 2000使用lStructSize来决定运行那个Open对话框,为什么Windows 98的应用程序(象Notepad)在Windows 2000下运行时得到了新的对话框呢?啊!随Windows 98出现的NOTEPAD.EXE显然在lStructSize 上有旧的OPENFILENAME的大小,因此Windows 2000必须使用lStructSize之外的某种东西来决定运行那个对话框。
到这里,我决定回过头去重新考虑问题。我将MFC放到一边,尝试直接调用GetOpenFileName。我重写了我的应用程序的OnFileOpen:
void CMyApp::OnFileOpen()
{
OPENFILENAME ofn; // older version
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
int nResult = ::GetOpenFileName(&ofn);
}
因为贯穿本练习,我使用了旧的0x400版本的SDK文件(因为我希望应用程序既可以在Windows 2000上运行,也可以在Windows 9x上运行),ofn.lStructSize就有了旧的大小。当我编译并运行时,我在Windows 98上得到了旧的对话框,而在Windows 2000上得到了新的对话框——就象Notepad一样!因此可以说,实际上,Windows 2000足够精明的为旧的应用使用新的对话框——但不是旧的MFC应用。它毫无意义。一个MFC应用的不同之处在哪里呢?
一定是标志。为了发现真相,我在OPENFILENAME结构中手工添加了不同的标志,直到我的程序产生了不带Places栏目的旧风格的窗口。你瞧,当我为ofn.Flags加入标志OFN_ENABLEHOOK时,我的对话框回到了从前。我将此奇怪的行为报告给Redmondtonians,他们证实“这种行为是设计的”。
那么,Windows 2000判断OPENFILENAME的大小以及对话框是否使用hook过程。如果OPENFILENAME有旧的大小,Windows 2000使用OFN_ENABLEHOOK来决定运行哪个对话框。如果OPENFILENAME使用hook过程(或者设置了ORN_ENABLETEMPLATE),Windows 2000按照旧的风格显示对话框;否则,显示新的对话框。这就解释了为什么MFC应用显示了旧的对话框——因为CfileDialog,就象所有MFC的公用对话框一样,使用hook过程。这是MFC将公用对话框嵌入它的消息映射系统的方式,它用同样的方式使用AfxWndProc嵌入其它的窗口。
现在你看到了结果:你给迷惑了。在一个MFC应用中得到新风格的对话框的唯一的办法是完全绕开CfileDialog,直接调用GetOpenFileName,并且不使用hook过程。即使你用新的SDK文件和WINVER = 0x500编译你的应用,你仍不能使用MFC,因为它的库和DLLs有旧的大小。你可以使用WINVER = 0x500自行编译MFC,但是谁知道那将怎么样呢?而且如果你真的BUILD了新的MFC,你将不得不将新的DLL和你的应用一块发布,给它起个不同的名字,因为你的新的MFC DLL肯定不会与其它的希望使用CFileDialog 和其它结构的旧的大小的MFC应用兼容。或者,你可以重新生成MFC,然后静态的连接,这将极大的增加你的可执行文件的大小,如果你不重新实现Windows的话。
到截稿时间为止,我从Redmond听说在即将发布的新的Visual C++®中,将会有一个不同名字的新版本的MFC DLL。新版本的MFC将支持Windows 2000中出现的新的UI和APIs。
同时,我将图2留给你研究,它取自最新的SDK文档,"Header File Conventions",提供了Windows版本问题的钥匙。它向你说明,为了达到某个版本的Windows和Microsoft® Internet Explorer的目标你必须使用SDK头文件定义哪些宏。努力别跌倒在生活的快车道上。
Paul DiLascia是Windows++: Writing Reusable Windows Code in C++ (Addison-Wesley, 1992)的作者,还是一位自由顾问和and 知名著书者。
他的联系方法:cppqa@microsoft.com 或者 http://www.dilascia.com.
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=3230