Viusal Studio2005极大地丰富了它的库,可以说是科研背后的清障机和加速器,对于这一点,我想大部分人都是这么认为的。它带来的大量工具及新增的功能性函数使开发人员的生活越来越快乐、简单。但对于我来说,所有这些与Visual Studio2005在C++上做的改变相比却都显得是那么苍白无力。这篇文章中,我着重叙述即将向用户发行的Viusal Studio2005版本给C++带来的变化。
一、对下划线说再见
Visual Studio.NET 2002在C++中引入了可扩展的托管,这种扩展带来的关键词以双下划线开始,例如__gc 和 __property。这个版本发行后的这些年来,我写了大量带有双下划线的代码,我不得不承认我从来都不喜欢这一点,我完全明白真正的原因是什么:双下划线将关键词标志为特殊地扩展,以区分编译器的标准编译规则,在理论上,可以充分使用可扩展的托管,使用其他的编译器编译它,这将忽略所有带双下划线关键词。
解决方案:微软发现了一个解决方法来改变这种语言而不是替换这种语言。但是这种妥协带来了以下结果:
1、开发者发现这种语法不自然而且看上去也很不舒服。
2、不能尽其所能。
例如,下面是托管C++声明属性的例子:
public __gc class Foo
{
// hundreds of lines of code
__property String* get_Text();
// hundreds of lines of code
__property void set_Text(String*);
// hundreds of lines of code
};
我相信,有良好编程习惯的程序员会将get与set紧挨者使用,并且会紧接着又声明所有下面需要使用的变量。但是语言并不管这些,它不能提供封闭的括号来界定结构,来让你声明"这是一个作为单元的属性"。所以当它运行时显得不自然并且与其他.NET语言也格格不入。
面对这些你能做什么?唯一的方法是将C++与CLI自然地结合起来,反之也就是真正改变C++。如果你将这么作,一种自然完美的语言将给你带来巨大的自由,当你编程时就再也不会需要双下划线了。
二、生命期与作用域
我非常喜欢明确地销毁对象。实际上,我也很喜欢垃圾收集器。可能我要说的更多,事实上,虽然它们有着各地的位置,而且对于我来说都需要,但如果我正在创建的对象仅仅操作内存,如果使用后不需要我释放内容我将会非常高兴。但内存管理是如此的虚弱无力,当我的对象占用非托管的资源时,例如一个数据库连接,一个文件对象或类似的对象,我需要自己控制。我需要确认一旦不需要的时候它就消亡。Dispose模式试图处理这些情况,但它并不是自觉的行为。封闭的括号也许是一种很好地解决途径。
在普通的非托管C++中,以下的代码说明了你不得不做的工作:
//this is a code fragment
{
try
{
Foo* f=new Foo(/* params */);
//all kinds of code, some of which might throw exceptions
delete f;
}
catch (/* something */)
{
delete f;
//whatever else, or rethrow;
}
}
如果在堆上创建对象显的是那么的容易:
//this is a code fragment
{
Foo f;
//all kinds of code, some of which might throw exceptions
}
当变量f超出作用域,无论是否是因为异常,它都自动消亡,这非常自然而且令人高兴、满意。
当这个对象在托管堆上,你不需要删除它,它将被垃圾搜集器清除。但是,如果它占用了一个托管资源,你可能想通过Dispose()方法来清除它,C#为这么做提供了using构造,但是它仍然不象我们的堆例子那样简单。
在新版本的语言中(以前叫C++/CLI),你可以不依赖于对象的种类来创建它,你可以在堆上创建一个托管对象,并且它可以在超出作用域后明确地被销毁。如果愿意的话,你还可以在托管堆上创建,这完全根据你的选择而定。
这种变化带来了其他的后果。最具深远意义的结果是你可以轻松地将任意对象放入模板集或作为另外一个类的成员变量。你可以充分发挥C++的力量来管理对象的生命期,而不是仅仅在堆上分配它并等待垃圾处理器来处理。
三、析构和终结
当你书写一个可以被其他语言使用的垃圾收集对象时,将发生什么?你针对这个对象是否已经写了一个析构函数?当你正在使用C++,你可以在堆上创建对象,当超出作用域范围后,对象的析构函数将自动运行。当C#或VB应用(不能在堆上创建垃圾收集对象)使用这个对象时将发生什么?这种情况被以一种精巧的方法实时处理。它将对象的析构函数转换为Dispose()方法来使用,所以,拥有析构函数的C++/CLI对象都可以任意使用。
如果你用C#或VB写了一个带有Dispose()方法的类,你可能已经写了一个终结函数,对于终结函数C++/CLI也有一种非常简单的语法。就象foo对象的析构函数叫做~Foo()一样,foo对象的终结函数叫做!Foo(),这两种方法都提醒你他们与构造函数相反。
当一个对象在托管堆上创建后,终结函数开始运行但从不被处理(因为执行的Dispose的级别要高于终结函数)。从某种意义上说,它是一个防护网,使你确信对象能释放其占用的非托管资源。即使使用对象的开发人员忘记了处理它。
四、指针与句柄
对于扩展的托管C++来说,一个主要的限制是C++语言没有变化,两种不同的事情使用相同的符号。"*"的意思要根据代码的上下文而定,试着看看下行代码:
Foo* pf = new Foo();
Foo对象在那里创建?内存是否会被自动清除?是否可以象下面代码那样对于指针使用如下算法:
pf++;
答案依赖于foo是否使用__gc关键词声明,如果它是一个垃圾收集对象,它只能在托管堆上创建,而不能在本地堆、不能在栈上创建。另一方面,如果没有使用__gc关键词声明,这行代码将在本地堆上给对象分配内存,这时候你必须记住使用"delete"来释放它。
一旦编译器的作者拥有了修改语言的自由,正如C++/CLI已经发生的那样,你大可不必担心类对象来自那里,它在哪里存在。你完全可以通过不同的语法来告之对象它应该存在在哪里。
Foo^ hf = gcnew Foo();
这叫做一个句柄,起初绝大多数C++开发组将^这个符号称为"脱字符号或帽子",现在都叫做帽子。就象指针那样,你从处理*或-符号中解脱出来。这种变化带来的冲击绝大部分是在你的头脑里,你只要看实例的声明就能判断对象生命期的管理,而不用返回查看类的声明。
说到类的声明,__gc and __nogc已经不再存在,在它们的位置上是一些比较"Cool"的、带有空格的关键词,带空格的关键词虽然看上去是两个词,但实际上是含有空格的一个词。
例如:
ref class R
{
private:
int m_a;
public:
R(int a): m_a(a) {}
};
你可能认为"ref"是C++/CLI中的一个新关键词,但实际上它不是,"Ref class"是一个关键词,其他的一些类似关键词是:"value class",""interface class"和"enum class"。因为以前几乎写的每一个C++程序都含有一个叫"Value"的变量,我非常高兴它没有变成一个关键词。
一个ref class 是一个托管类,一个生存在托管堆上并由垃圾收集器管理的类。正如我先前说的,你可以在栈上创建实例,编译器将通过一个隐藏的灵巧的指针来管理该对象。
五、属性
C++的属性变化非常大,因为我在文章的开篇讲述了托管C++中属性的尴尬,现在就让我们以简洁的C++/CLI版本来结束吧。
ref class R
{
private:
int m_Size;
public:
property int Size
{
int get() { return m_Size; }
void set(int val){m_Size = val;}
}
};
R r;
r.Size = 42;
属性是否是关键词,从某种意义上可以这么说,它是一个定位型的关键词,所以你可以毫无冲突地将一个变量或函数称为属性,就象上面的代码那样,它只是在类的定义中有特殊的含义。现在的C++/CLI语言支持将属性定义为一个单独的单元,相对与以往的方式,我更喜欢新的方式,相信你也是这样。