调用虚拟函数,持续化视图状态,POD 类型概念

2016-06-16

1.调用虚拟函数

2.持续化视图状态

3.POD 类型概念

在 C++ 中,无法从某个类的构造函数中调用派生的虚拟函数,因为虚表还没有完全建立。但是在C#中好像就可以,是这样吗?为什么会有这种差别呢?

Clifton F. Vaughn

确实如此,在这个方面 C# 与 C++ 是有差别的。在 C++ 中,如果你从构造函数或者析构函数中调用虚拟函数,编译器调用的虚拟函数是定义在这个正在被构造的类实例中的(例如,如果从 Base::Base 中调用 Base::SomeVirtFn ),不是最底层派生的实例(the most derived instance),正像你说的那样,因为在最底层派生的构造函数执行之前,虚表还没有完全被初始化。另一种说法是派生类还没有被创建。

 

Figure 2 虚拟函数 TestSimilarly

当你从析构函数中调用虚函数时,C++ 调用该基类的析构函数,因为派生类已经被销毁(其析构已经被调用)。虽然这个行为可导致异常结果(此即为什么从构造函数或析构函数中调用虚函数被认为是糟糕的编程实践的原因),它是大多数 C++ 程序员必须了然于心的基本常识。

正如你所指出的那样,在 C# 有所不同。托管对象――无论是在 C#,托管 C++ 中,还是任何其它的 .NET 兼容语言中――是作为其最终类型被创建的,也就是说,如果你从构造函数或析构函数中调用虚函数,系统调用的是最末层派生的函数。

	Figure 1 VirtFnTest
 
////////////////////////////////////////////////////////////////
// MSDN Magazine — November 2004

// To compile:
//   cl /clr VirtFnTest.cpp
//
#include < stdio.h >
#include < vcclr.h >
#using < mscorlib.dll >

using namespace System;

/////////////////
// C++ native base class. Ctor calls virtual fn: always calls Base fn.
class Base {
public:
   Base()
   {
      printf(" Base::ctor\n");
      Hello();
   }
   ~Base()
   {
      printf(" Base::dtor\n");
      Goodbye();
   }
   virtual void Hello()   { printf(" Base::Hello\n"); }
   virtual void Goodbye() { printf(" Base::Goodbye\n"); }
};

// Derived native C++ class overrides fn, but not called from ctor.
class Derived : public Base {
public:
   Derived()
   {
      printf(" Derived::ctor\n");
   }
   ~Derived()
   {
      printf(" Derived::dtor\n");
   }
   virtual void Hello()   { printf(" Derived::Hello\n"); }
   virtual void Goodbye() { printf(" Derived::Goodbye\n"); }
};

// Managed base class. Ctor calls virtual fn: always calls most derived 
// override.
public __gc class MBase {
public:
   MBase() {
      printf(" Base::ctor\n");
      Hello();
   }
   ~MBase() {
      printf(" Base::dtor\n");
      Goodbye();
   }
   virtual void Hello()   { printf(" Base::Hello\n"); }
   virtual void Goodbye() { printf(" Base::Goodbye\n"); }
};

// Managed derived class overrides virtual fn.
public __gc class MDerived : public MBase {
public:
   MDerived()
   {
      printf(" Derived::ctor\n");
   }
   ~MDerived()
   {
      printf(" Derived::dtor\n");
   }
   virtual void Hello()   { printf(" Derived::Hello\n"); }
   virtual void Goodbye() { printf(" Derived::Goodbye\n"); }
};

int main()
{
   printf("Create native object:\n");
   Derived *pd = new Derived();
   printf("Destroy native object:\n");
   delete pd;

   printf("\nCreate managed object:\n");
   MDerived *pmd = new MDerived();
   printf("Destroy managed object:\n");
   delete pmd; // force dtor call
   return 0;
}

Figure 1所示程序举例说明了这一点。如果你编译并运行这个程序,你会看到 Figure 2 所示输出。这种行为对于 C++程序员来说似乎有些奇特。它意味着在派生类被初始化之前,你可以调用某个派生类型的虚拟函数――也就是说在其构造函数运行之前。同样,如果你从基类析构函数中调用虚函数,该函数是在派生类被销毁之后运行的――也就是说在析构函数被调用之后。那么先不说这种差别存在的原因,刚才不是还说从构造函数/析构函数中调用虚函数被认为是糟糕的实践。

为什么微软的家伙们要像这样来设计 C# 呢?因为它简化了内存管理。垃圾收集器为了释放内存,它需要知道对象有多大。如果 C# 像 C++ 那样构造对象,那么你可能会碰到这样一种情况:有两个对象,Obj1 和 Obj2,下面这两条语句都为真:

typeof(Obj1)==typeof(Obj2) 
sizeof(Obj1)!= sizeof(Obj2) 

因为对象之一是被部分构造。(不要忘了垃圾收集器是异步运行的。)通过将对象构造成最终类型,垃圾收集器能从其类型决定对象的大小。如果 C# 像 C++ 那样进行部分构造,则垃圾收集器将需要更多的代码来决定部分构造对象的真实大小。这样将带来复杂性和性能下降,首先要解决这个问题很让人气馁,所以为了较快的垃圾收集利益,微软的家伙们决定像上面那样来实现 C#。有关这方面的讨论参见 Raymond Chen 的 blog:“The Old New Thing”。

在 2004 三月的专栏中,你展示了如何改变文件打开对话框的最新视图状态设置,但没有涉及到保存这个用户使用的最新视图设置。我遇到的问题是读取用户已有的打开文件对话框设置。我只找到直接读取列表框信息的方法,但当用户选择缩略图模式时,那样做不能得到正确的信息。对此你有没有解决办法?

Maarten van Dillen  

我正在用公共的 CFileDialog 类做开发,应该不是很难,但事情似乎并不是那样。我想强制文件打开对话框的视图模式为缩略图。我要用 Visual C++ 来做,你能否提供一些建议?

Elliot Leonard  

有几个读者都在问文件打开对话框中的缩略图问题。在我三月份的专栏中,我示范了如果向文件打开对话框中的 SHELLDLL_DefView 专用窗口发送 WM_COMMAND 消息以设置不同的视图模式――但你如何知道当前所处的模式是哪一个呢?你必须获取列表控件并调用 CListCtrl::GetView:

// in dialog class
HWND hlc = ::FindWindowEx(m_hWnd, 
NULL, _T("SysListView32"), NULL);
CListCtrl* plc = (CListCtrl*)CWnd::FromHandle(hlc);
DWORD dwView = plc->GetView();

CListCtrl::GetView 返回 LV_XXX 代码之一,但正像 Maarten 发现的那样,Windows 对图标模式和缩略图模式都返回 LV_VIEW_ICON。

那么如何区分到底是哪种视图模式呢?我绞尽脑汁并钻进头文件查找,最后发现一个叫 LVM_GETITEMSPACING 的消息,该消息是作什么用的呢――用来获取图标间隔。顾名思义,图标间隔是图标视图模式中图标之间的像素间隔。LVM_GETITEMSPACING 不是很好使用,以至于 MFC 都没有对之进行包装(比如说 MFC 中并没有 CListCtrl::GetIconSpacing 这样的函数)。所以在 MFC 中你得自己发送消息:

CSize sz = CSize(plc->SendMessage(LVM_GETITEMSPACING));

Windows 按照通常方式返回尺寸,在高位和低位字中编码的 cx/cy,然后CSize很礼貌地为你进行解码。一旦有了图标间隔,你便可以将它与 GetSystemMetrics(SM_CXICONSPACING) 返回的系统间隔值进行比较。如果列表视图的图标间隔与系统的一样,则视图是图标模式。如果大于系统间隔,则视图为缩略图模式:

if (sz.cx > GetSystemMetrics(SM_CXICONSPACING)) { 
   // thumbnail view
} else {
   // icon view 
}

讲了那么多缩略图,接下来的问题是如何持续化不同用户会话的视图状态?对此,当程序终止时,你需要用 Profile 函数在用户配置文件中保存最后使用的模式,并在下一次启动程序时再次恢复它。我写了一个小示范程序,DlgTest。程序使用了一个实现持续化程序行为的类 CPersistOpenDlg。这个类又借助另外一个类 CListViewShellWnd,用它来封装 SHELLDLL_DefView 窗口(参见三月份专栏)。CListViewShellWnd 包含获取和设置视图模式的函数,由这些函数来区分图标和缩略图模式:

CListViewShellWnd m_wndLVSW;
...
m_wndLVSW.SetViewMode(ODM_VIEW_THUMBS);

CListViewShellWnd 的 OnDestroy 处理器在某个数据成员 m_lastViewMode 中保存视图模式。当对话框被销毁时,CPersistOpenDlg 的析构函数调用 WriteProfileInt 将这个值写入用户配置文件。对话框启动时,CPersistOpenDlg 给自己送一个初始化消息;该消息处理例程调用 GetProfileInt 从磁盘读取存储在配置文件中的值并设置视图模式。PostMessage 是必须调用的,因为常规初始化消息 WM_INITDIALOG 和 CDN_INITDONE 在文件对话框被完全初始化之前就会到来――有关这一点的解释参见三月份专栏。

顺便说一下,任何时候你都应该使用 GetProfileXxx 和 WriteProfileXxx 来持续化应用程序的设置。MFC 用 CWinApp 包装了这些函数。如果你在应用程序启动时调用(一般都是在 InitInstance  函数中) CMyApp::SetRegistryKey("KeyName"),MFC 使用注册表来存储用户配置信息,而不是 INI 文件。下面是 DlgTest 用的 INI 文件:

[settings]
ViewMode=28717

偶尔在一些文字资料和 C++ 文档以及 Microsoft .NET 框架中看到术语“POD 类型”。这个术语是什么意思?

Shelby Nagwitz

你可以将 POD 类型看作是一种来自外太空的用绿色保护层包装的数据类型,POD 意为“Plain Old Data”(译者:如果一定要译成中文,那就叫“彻头彻尾的老数据”怎么样!)这就是 POD 类型的含义。其确切定义相当粗糙(参见 C++ ISO 标准),其基本意思是 POD 类型包含与 C 兼容的原始数据。例如,结构和整型是 POD 类型,但带有构造函数或虚拟函数的类则不是。 POD 类型没有虚拟函数,基类,用户定义的构造函数,拷贝构造,赋值操作符或析构函数。

为了将 POD 类型概念化,你可以通过拷贝其比特来拷贝它们。此外, POD 类型可以是非初始化的。例如:

struct RECT r; // value undefined
POINT *ppoints = new POINT[100]; // ditto
CString s; // calls ctor ==> not POD

非 POD 类型通常需要初始化,不论是调用缺省的构造函数(编译器提供的)还是自己写的构造函数。

过去, POD 对于编写编译器或与C 兼容的 C++ 程序的人来说很重要。现在,POD 来到 .NET 的环境中。在托管 C++ 中,托管类型(包括 __value 和 __gc 两者)能包含嵌入的原生 POD 类型。

Figure 3 Pod.cpp
 

/////////////////////////////////////////////////////////////////////
// You can embed native POD types in a managed (__gc or __value) type.
// Compile with cl /clr.

#using < mscorlib.dll >

// simple struct is a POD type
struct POINT {
   int x;
   int y;
};

// native class w/ctor: not POD!
class CPoint : public POINT {
   CPoint() { x=y=0; }
};

// Managed __gc type
__gc class Circle {
public:
   POINT center;        // ok: embedded POD type
   CPoint m_center;     // Error: not POD!
   int radius;
};

Figure 3 展示了例举说明代码。托管的 Circle 类能包含 POINT,但无法包含 CPoint 类。如果你尝试编译 pod.cpp 会报一个 C3633 错误:“Cannot define ''m_center'' as a member of managed ''Circle'' because of the presence of default constructor ''CPoint::CPoint'' on class ''CPoint''.”(译者:意思是由于类 CPoint 有缺省的构造函数‘CPoint::CPoint’,所以不能将‘m_center’定义为托管类‘Circle’的一个成员)
.NET 限定嵌入的本地对象只能为 POD 类型的理由是这样做能安全地拷贝它们,不用担心调用构造函数,初始化虚表,或任何非 POD 类型需要的其它机制。