在清闲之余,在此与大家探讨一下,C++/CLI中的资源清理。本文将分成三部分,他们分别是引言、Destructor,Finalizer的语法表示、如何保证Destructor,Finalizer与其他语言兼容。
一、 引言资源是一个很大的范畴,先让我确定一下我们这里谈论的资源包括哪些内容。这里专指在面向对象编程中一个对象实例所使用的资源,他包括对象本身所占有的内存(对象占有内存的大小由对象字段成员来决定,字段成员越多占有的内存就越大)以及其字段成员(Field member)所使用的资源,如文件句柄,数据库链接等等。相信大家比我到清楚在一个对象不再被使用时应该释放其占有的资源,在清理对象所占有的内存之前,执行一个特定的函数,释放字段成员所使用的资源。比如一个文件对象,我们在delete之前得调用Close函数。C++/CLI中的析构器(Destructor)、终结器(Finanlizer)便扮演这个特定函数的角色。在探讨这两个函数之前我们先回忆一下与C++/CLI有着一定关系的ISO C++与.NET平台(ISO C++是他的前辈,并且C++/CLI对ISO C++是兼容的,他们的兼容性已超出本文范围,我们可以在往后再一起讨论;.NET平台是C++/CLI的运行平台),看看他们俩是如何完成资源清理的,这样能够帮助我们更好地理解C++/CLI中的资源清理。
ISO C++面对的是无虚拟机环境,直接根操作系统或是硬件打交道,资源的回收必须由程序员完成,即在某个对象不再使用时得手动地进行资源释放。如果是栈对象则在超出作用域时会自动调用析构器,同时释放对象自己所占有的内存;若是堆对象,只有程序员使用delete pointer 时才会调用pointer所指向对象的析构器,接着释放pointer所指向对象的内存。
.NET平台的一个主要特点是,托管内存,内存的回收交给垃圾回收器(GC)来管理。它会检测到哪些对象不再被使用,便回收其所占用的内存。如果该对象所属的类型实现了Finalize()函数,则会在回收内存之前调用该函数。Finalize函数的作用与ISO C++中的析构器作用类似,在对象被销毁之前释放其字段成员使用的其他资源。
比较一下ISO C++与.NET平台的资源清理,我们不难发现,ISO C++的资源清理是手动的、确定的(调用时机我们可以控制);.NET平台下的资源释放是自动的、不确定的(调用时机我们不能控制)。他们的各自缺点是, ISO C++程序员关注哪些对象不再被使用,调用delete pointer,否则会造成资源泄漏;.NET平台下的一些稀缺资源不能得及时的释放。
在C++/CLI中,析构器与终结器同时出现,解决了了ISO C++与.NET平台的不足,当然同时也没有丢失他们的优点。如果对象所使用的资源是稀缺的,必须确定性的释放,我们便可调用析构器,要是万一我们遗忘了调用析构器,最后垃圾回收器(GC)会调用帮我们调用终结器。到此我们现在可以断言,析构器与终结器所做的事情是一样的,释放其字段成员所使用的资源,只是调用者不一样,一个是程序员一个是垃圾回收器(GC)。最后再补充一下,这里所说的释放其字段成员所使用的资源,并不包括字段本身使用的内存。比如说一个对象中包含有一个文件句柄字段,他会占用4 个Byte的内存,这块内存资源在析构器或终结器中都不会被回收,他的回收将发生在最后,垃圾回收器回收托管内存时。
我们了解了析构器与终结器存在的意义自后,接下来我了解一下他们的语法表示。
二、 Destructor,Finalizer的语法表示
~ClassName(){…..} //析构器,”~”加类名称
!ClassName(){…...} //终结器,”!”加类名称
假如我们定义一个MyClass类型,他们语法表示如下:
public ref class MyClass
{
public:
~MyClass() //析构器
{…}
!MyClass() //终结器
{…}
};
从第一部分的内容我们知道析构器与终结器是一样的,为了避免代码的重复我们可以在析构器中调用终结器(MSDN推荐这样调用,见Destructors and Finalizers in Visual C++,目前还没弄明白为什么是析构器调用终结器,从函数调用上来看他们两谁调用谁都是一样的),于是我们的代码可以表现如下。
public ref class MyClass
{
public:
~MyClass() //析构器
{
this->!MyClass(); //在析构器中调用析构器
}
!MyClass() //终结器
{
//TODO,释放字段成员所使用的资源
}
};
MyClass ^myClass = gcnew MyClass();
delete myClass; //调用析构器
myClass = nullptr; //让 myClass 指向空对象, 这样便于垃圾回收器回收myClass原先所向对象所占有的内存。在此再啰唆一下delete关键字,C++/CLI保留了ISO C++ delete关键字,但是他语意有了一些变化。在ISO C++中,他会先调用指针所指向对象的析构器,然后释放对象所占用的内存。在C++/CLI中只会调用析构器,对象所占有的内存不会马上被回收,最终交给垃圾回收器来回收。这源于内存模型的变化,C++/CLI中的引用类型只能分配在托管内存中,托管内存是不能由程序员来显示回收的(当然GC.Collection()函数可以显示让垃圾回收器进行内存回收,当并不建议这样做,除非一些特殊情况)。
大家都知道.NET平台支持多种语言,为了保证不同语言编写的的类型之间能够很好地相互访问,微软定义了一个通用语言规范(Common Language Specification,简称CLS),满足CLS的类型及类型成员便可在不同语言间无缝交互。那么接下来就让我们探讨一下Destructor,Finalizer是如何做到与其他语言兼容的。
三、如何保证Destructor,Finalizer与其他语言兼容
这里我们就从C#的角度出发,分析其是如何实现Destructor,Finalizer与C#的兼容,即如何在C#中访问Destructor,Finalizer并保持语意清晰,如果我们直接使用函数名来访问(~ClassName(),!ClassName()),那看起来就不那么完美了。
查看C++/CLI Language Specification,得知C++/CLI不允许程序员显示的实现(implement)IDisposable接口,当我们给一个类型写析构器时,编译器会自动将让类型实现IDisposable接口。析构器的确定资源释放与IDisposable接口的语意一样(MSDN, IDisposable 定义一种释放分配的非托管资源的方法),在C#中调用Dispose()函数便是我们实现的析构器。终结器从名称上我们很容易想到Finalize()函数,只要编译器给我们生成Finalize()函数,并在该函数中调用!ClassName()函数即可。编译器如何处理这一问题,请看以下的代码与注释。
MyClass类型的C++/CLI代码如下:
using namespace System;
using namespace System::IO;
public ref class MyClass
{
public:
MyClass() //构造函数,初始化字段成员
{
m_stream = gcnew FileStream("C:\\test.txt",FileMode::Create,FileAccess::Write);
m_writer = gcnew StreamWriter(m_stream);
m_writer->Write("test");
}
~MyClass() //析构器
{
this->!MyClass(); //调用终结器
}
!MyClass() //终结器
{
delete m_writer; //释放字段成员所占有的资源
delete m_stream; //释放字段成员所占有的资源
}
private:
FileStream ^m_stream; //字段
StreamWriter ^m_writer; //字段
}
以下在为通过Reflector查看的编译结果:
public class MyClass : IDisposable /**//*C++/CLI源码中并没有显示实现该接口,是编译器给我们加上的*/
{
// Fields
private FileStream m_stream;
private StreamWriter m_writer;
// Methods
/**//*注意一下三个方法,从名称上,我们不难发现他们就是我们在MyClass实现时所写的三个方法。只有构造保持没变。析构器与终结器前面都加上了private访问修饰符,因此在类型之外是无法访问到析构器和终结器的。*/
private void !MyClass();
public MyClass();
private void ~MyClass();
/**//*以下三个函数是编译器给我们加上的,他们是实现与CLS兼容的关键,请注意他们的逻辑关系,逻辑关系也比较简单,看一下代码就清楚了。建议阅读时我们不妨试着在脑海里执行一下,Dispose(),Finalizer()方法。*/
// Dispose()为IDisposable成员。
public sealed override void Dispose()
{
this.Dispose(true);}
//传true调用Dispose(bool) 函数
GC.SuppressFinalize(this);
/**//*如果用户调用了Dispose(),通知垃圾回收器不再执行Finalize函数*/
/**//*这就是我们先前说的,由垃圾回收器调用的函数*/
protected override void Finalize()
{
this.Dispose(false);}
//传false调用Dispose(bool) 函数
/**//*关键是下面这个函数Dispose(bool),他确保了我们在别的语言或垃圾回收器可以调到正确的函数,C++/CLI中析构器,终结器*/
protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
{
if (flag1)
{
this.~MyClass();/**//*Dispose() 调用该函数传入true,便执行该语句块,实现了C++/CLI与CLS兼容,调用Dispose(),便是C++/CLI中的析构器,确定性资源释放*/
}
else
{
try{
this.!MyClass();/**//*Finalize() 调用该函数传入false,便执行该语句块,实现了C++/CLI与CLS兼容,调用Finalize(),便是C++/CLI中的终结器,确保对象在被销毁前释放其占有的其他资源*/
}finally{base.Finalize();}
}}