UML软件工程组织

Visual C++中删除托管对象、包装库及其他

 

2007-12-06 作者: Paul DiLascia 出处: msdn中国

 

本文将给大家讲解一下Visual C++中如何删除托管对象、包装库及其他,大家在实际开发中可以做个参考。

问:能否告诉我,在托管 C++ 中,使用 delete 操作符销毁托管对象是否安全?

Bernie Sanders

答:是安全的,可以在托管 C++ 中删除托管对象,不过要知道,delete 所做的就是调用对象的析构函数,所以必须显式定义它。调用 delete 并没有释放对象的存储空间。只有垃圾回收器才能够释放存储空间。图 1 所示的是一个简单的程序,它定义了一个托管类,这个类有一个析构函数,当运行这个析构函数时会显示一条消息。TESTDTOR 分配了两个 ManagedClass 实例。它显式删除第一个实例,但没有删除第二个。当您运行 TESTDTOR 时,就会得到如下所示的结果:

以下是引用片段:
  Begin main
  ManagedClass(04A712D4)::ctor
  ManagedClass(04A712D4)::dtor
  ManagedClass(04A712E0)::ctor
  End main
  ManagedClass(04A712E0)::dtor

它表明当 delete 语句执行时,第一个对象的析构函数就会立即执行,而第二个对象(位于 04A712E0)并没有被销毁,直到控制离开 main,系统终止代码调用垃圾回收器释放未完成对象时才被销毁。

testdtor 的重要部分

图 2 testdtor 的重要部分

  要是对 .NET 环境中所发生的事情不确定,随时可以编写一些代码,对其进行编译,然后检查生成的 Microsoft 中间语言 (MSIL)。如图 2 所示,定义一个析构函数会使编译器生成两个方法:一个是 Finalize 方法,它包含您的实现(在本例中为调用 printf);另一个是 __dtor 方法,它调用 System.GC::SuppressFinalize,然后调用 Finalize。当您删除一个对象时,编译器会调用这个特定的 __dtor 方法。如果您带 /FAs 参数编译 TESTDTOR,产生随源代码列出的汇编语言,您就会发现 delete 语句按照以下方式进行编译:

以下是引用片段:
  delete pmc;
  ldloc.0 ; _pmc$
  call ..1ManagedClass@@$$FQ$AAM@XZ

那些奇怪的神秘符号就是析构函数 (__dtor) 的托管名称。

经验丰富的 C++ 编程人员也可能会疑惑不解,想知道如果调用 delete 不释放对象,那调用它有什么用呢?这个问题提得好。调用 delete 的唯一目的是回收类使用的任何非托管资源。例如,如果您的对象打开文件或创建一个数据库连接,则可以编写一个析构函数,当对象使用完毕时,可以用这个析构函数关闭它的资源,然后使用 delete 来调用它。释放托管类中的资源的更好方式是通过实现 Dispose 模式、IDisposable 和 auto_dispose 模式(如果您采用托管 C++ 编写)来调用它。有关更多信息,请参考 Tomas Restrepo 在 2002 年 2 月的 MSDN?Magazine 中撰写的“Tips and Tricks to Bolster Your Managed C++ Code in Visual Studio .NET”。

如果您实现 dispose 模式,.NET 的其他用户就可以使用它。如果您在析构函数中进行清理工作,其他语言就无法显式调用您执行清理工作的代码。在 C# 和 Visual Basic 中没有 delete 操作符。

所以虽然您可以调用 delete 来调用您的析构函数,但不提倡将执行清理工作的代码放在析构函数中。较好的方法是实现 IDisposable,所有的语言都可以使用它。不过要注意,这种行为在 Visual C++? 2005 中有所变化。有关更多信息,请参考 Andy Rich 在 Deterministic Finalization IV - Benefits, part II 中关于 deterministic finalization 的讨论,以及当前的 C++/CLI Language Specification Standard。

问:我有一个非托管函数,它返回一个包含 char* 字符串的链接列表:

以下是引用片段:
  struct blah {
  int a, b;
  char *a, *b;
  struct blah *next;
  };
  struct blah *getmystruct();

因为 getmystruct() 分配了内存,所以当我使用完这个函数后,我需要调用 freemystruct(struct blah *b)。我试图生成一个包装,将它转换成托管类型的集合,但我不知道当这些指针需要释放时如何处理它们。您能指点我一下吗?

Cory Nelson

答:哦,当然可以。您不能通过简单的 dllimport 语句将本机列表转换成托管类型的集合。interop 服务不错,但不是非常 好!您需要编写一个包装,它将您的列表显式转换成托管集合(例如 ArrayLis)。我编写了一个具有三个模块的程序 (ListWrap) 来说明如何实现。第一个模块是 ListLib.cpp,它实现了一个具有两个函数(AllocateList 和 FreeList)的本机 C++ 库 (DLL),它们的作用是分配和释放本机 C++ 结构的链接列表。这两个函数模拟应用程序中的 getmystruct 和 freemystruct 函数。第二个模块是一个托管 C++ 文件 — ListWrap.cpp,它实现了一个包装本机 C++ 实现的托管类 ManagedNode(请参见图 3)。第三个模块是一个 C# 测试程序,它调用包装来显示如何工作。您可以从 MSDN Magazine Web 站点下载 ListLib.cpp 和 C# 测试程序的完整源代码。

ListLib.cpp 实现了两个本机函数 — AllocateList 和 FreeList,它们用于分配和释放 NativeNode 结构的列表:

以下是引用片段:
  // from ListLib.h
  struct NativeNode {
  int a, b;
  TCHAR *str;
  struct NativeNode *next;
  };

ListWrap.cpp 中的包装类 ManagedNode 模拟 NativeNode 的定义,不过有几个细微区别:本机 char* 被替换为托管 String,并且由于我使用 ArrayList 来实现列表结构,所以没有 next 指针。在代码中,它应该如下所示:

以下是引用片段:
  // managed equivalent of NativeNodepublic __gc class ManagedNode {public: int a, b; String* str;};

定义好 ManagedNode 之后,下一步就是编写一些代码将 NativeNode 转换成 ManagedNode。但在开始编写之前,请稍微考虑一下转换器函数应该是什么样子的,它应该有什么样的参数和返回值。一种方式是编写一个函数,将 NativeNode 的本机列表作为参数并返回 ManagedNode 的托管列表,它可能销毁进程中的本机列表。.NET 客户端应用程序会直接调用 ListLib DLL(或者您的 getmystruct)来获取本机列表,将其作为 IntPtr,然后将这个 IntPtr 传递给转换函数,如下所示:

以下是引用片段:
  // call DLL directly through interop
  IntPtr nativeList = AllocateList(7);
  // call wrapper to convert
  ArrayList amanagedList = ListWrap.Convert(nativeList);

在大多数情况下,客户端要负责调用 DLL 来释放本机列表,或者由 Convert 函数自动完成释放过程。

另一种方法是完全隐藏 DLL,其做法是:将本机函数 AllocateList 包装在一个包装器中,由它分配列表、转换并释放原始本机列表,再将托管列表作为 ArrayList 返回。哪种方法更好呢?第一种策略的优点是您只需编写一个简单的转换函数,在任何具有本机列表的地方都可以使用它。第二种策略需要包装创建列表的每个函数。如果您有多个创建列表的函数,这种方式会有些麻烦,但是它的优点是对 .NET 客户端完全隐藏了所有本机内容。客户端不需要处理 IntPtrs,甚至不需要导入 DLL;ListWrap 将这一切都隐藏了。我采取的是这种方式,我也鼓励您在自己的应用程序中使用这种方式。它要对库进行完全包装还需要做很多工作,但结果会比较可靠,而且完全封装。

有了 ManagedNode 之后,剩下的工作就是包装 AllocateList 了。这个过程十分简单。首先,调用 AllocateList 来分配本机列表,然后创建一个空的 ArrayList。接下来将所有 NativeNode 复制到 ManagedNode,并将它们添加到托管列表中,您在进行这些操作时会将它们删除。图 3 显示了完整的细节。托管 C++ 的好处之一就是所有代码看起来很简单、整洁,即使您处理的是混合对象。将本机 char* 复制到托管 String 只是一个简单的赋值,如以下的代码行所示:

以下是引用片段:
  mn->str = nn->str; // String = char*: it just works!

不需要调用转换函数,编译器知道如何去做。CreateList 在运行时会删除本机节点。这比在最后删除节省存储空间。

通过将整个列表转换成托管对象(而不是通过 interop 和 StructLayout 将它导出),您可以为托管客户端营造一种托管环境。这叫入乡随俗!毕竟,一些程序员选择 .NET 的主要原因之一就是它具有自动垃圾回收的功能。如果您直接通过 interop 导出列表,您还必须导出 FreeList,并要求使用其他基于 .NET 语言的程序员要记得调用它。

一般情况下,如果您导出到托管环境,最好是将尽可能多的数据转换成托管对象。什么情况下例外呢?您的客户端也是采用 C++ 编写的。当然,这条规则并不总是适用。有时更好的方式是直接导出结构,并要求客户端释放它们 — 例如,如果复制对性能或内存造成的影响太大而不可接受,就需要这样做。您必须使用判断来决定是进入托管环境还是进入本机环境。

问:我正在使用 C++ 托管扩展来包装现有的 C++ 库,使基于 .NET 的语言可以访问它。在托管 C++ 中,我可以写为

以下是引用片段:
  String* s = new String();
  s = _T("Hello, world");

但如何将托管 String 再次转换为本机 TCHAR*?

Matthew Brady

答:一旦您了解了神奇的 voodoo,这会变得很简单。您必须调用 PtrToStringChars 并 pin 结果。代码如下所示:

以下是引用片段:
  String __gc* s = S"Hello";
  const wchar_t __pin* p = PtrToStringChars(s);

不要忘记对从 PtrToStringChars 返回的指针进行 __pin 操作。Pin 是必须的,因为 PtrToStringChars 将一个托管 (__gc) 指针返回给托管内存中的 String 对象的第一个字符,而垃圾回收器随时都会回收托管内存,除非您显式对它进行 __pin。一般情况下,每次将 __gc 指针传递给本机(非托管)函数时都必须使用 __pin。

图 4 显示了一个小程序,它将托管 String 转换成宽字符和 ANSI 字符串。要转换成 ANSI,可以使用自己喜欢的转换函数,如 wcstombs 或 ATL W2A 宏。如果您使用 MFC Cstrings,则不必进行任何操作,因为 CString 对 char* 和 wchar_t 都有赋值运算符:

以下是引用片段:
  // both will work
  CString s1 = "hello, world";
  CString s2 = L"Hello, world";

问:在我的应用程序中,我想将选项卡控件的背景颜色从 gray 转换成 white。我试图从 CTabCtrl 派生一个类并使用所有的功能,但没有成功。能告诉我怎么办吗?

Mayur Patel

答:更改选项卡控件中选项卡的颜色十分简单,但要将属性表的颜色翻新则需要大量的工作,不下很大决心是做不到的。对于选项卡,基本思想是使控件成为所有者描述的控件,然后处理 WM_DRAWITEM。如果您使用 MFC,则可以重写虚函数 DrawItem。

在 Microsoft Systems Journal1998 年 3 月发行的那一期中,我介绍了如何实现一个选项卡控件类 — CtabCtrlWithDisable,它支持禁用选项卡。作为禁用选项卡的一部分,当选项卡禁用时,CTabCtrlWithDisable 将选项卡的文本颜色变为浅灰色。本月,我从 CTabCtrlWithDisable 借用了一些代码,实现了一个新的类 — CcolorTablCtrl,它可以使您更改选项卡的颜色(请参见图 5)。

要使用 CcolorTablCtrl,请在您的属性表中创建一个实例:

以下是引用片段:
  class CMyPropSheet : public CPropertySheet {
  protected:
  CColorTabCtrl m_tabCtrl;
  };

您必须在属性表的 OnInitDialog 处理程序中子类化选项卡控件(以便 MFC 可以使用它),然后将前景色和背景色设置成您喜欢的任何颜色:

以下是引用片段:
  // in CMyPropSheet::OnInitDialog()
  HWND hWndTab = (HWND)SendMessage(PSM_GETTABCONTROL);
  m_tabCtrl.SubclassDlgItem(::GetDlgCtrlID(hWndTab), this);
  m_tabCtrl.SetColor(WHITE, RED);

这里的 WHITE 和 RED 是标准的 COLORREF 值,即 RGB(255, 255, 255) 和 RGB(255,0,0)。一旦您实例化并初始化 CcolorTabCtrl,颜色选项卡控件就会完成剩下的工作(请参见图 6)。

颜色选项卡控件

图 6 颜色选项卡控件

CColorTabCtrl 重写了 SubclassDlgItem,它调用 ModifyStyle 来将风格更改为TCS_OWNERDRAWFIXED。进行重写的较好位置是在 PreSubclassWindow 中,因为不管控件是子类化还是通过 CreateWindow 创建都会调用这个函数(但由于杂志篇幅有限,我必须对代码进行压缩,因此我采用了这样的捷径)。注意,SubclassDlgItem 是一个简单的重写,而不是虚函数。为了设置颜色,SetColor 将颜色保存在两个成员变量 m_clrBackground 和 m_clrForeground 中。

一旦风格设置为所有者描述的,每次需要 Windows? 绘制选项卡时,它都会发送 WM_DRAWITEM 消息。MFC 捕获这个消息并调用选项卡控件的虚函数 DrawItem,CColorTabCtrl 通过用新的颜色绘制文本来实现这个函数:

以下是引用片段:
  // in CColorTabCtrl::DrawItem
  dc.FillSolidRect(rc, m_clrBackground);
  dc.SetBkColor(m_clrBackground);
  dc.SetTextColor(m_clrForeground);
  dc.DrawText(...);

这些都非常简单,所以要了解具体实现请查看源代码。由于您可能不想只更改选项卡颜色而不更改页面颜色,所以我也实现了一个 CcolorPropertyPage,它可以让您将属性页的背景颜色改成匹配的颜色,如图 6 所示。对于属性表,更改背景颜色最简单的方式就是处理 WM_ERASEBKGND:

以下是引用片段:
  BOOL CColorPropertyPage::OnEraseBkgnd(CDC* pDC)
  {
  CRect rc;
  GetClientRect(&rc);
  pDC->FillSolidRect(rc, m_clrBackground);
  return TRUE;
  }

如果您自己试运行这段代码,您会发现有各种令人苦恼的问题。首先,如果您更改页面颜色,所有控件的背景色都会是错误的,所以还必须修正。对此,您必须处理 WM_CTLCOLOR 和 WM_ERASEBKGND。有关详细信息,可以参阅 1997 年 5 月一期中我的 MSJ 专栏。

另一个问题是选项卡控件仍然使用系统 3D 颜色来绘制选项卡的边缘和圆角。唉!要修正这个问题,只能自己处理 WM_PAINT 并负责所有的绘图操作。包括画出选项卡在被选定时与其他选项卡的偏移,以便它以前景色显示。此时,您就要开始对选项卡控件进行一番彻底改造了。每个使用 Windows 的编程人员都知道,更改控件颜色无一例外都是很痛苦的事情,一旦您走上了这条路,要做的事情就似乎没有尽头了。相信很快标准颜色就会比它们原先的颜色漂亮许多,否则您会产生疑问,为什么不转为使用 .NET Framework,它要更改颜色只需简单地写成:

以下是引用片段:
  ctl.BackColor = Color.Aquamarine;