C++: 支持.NET程序设计的最强有力的语言

C++: The Most Powerful Language for .NET Framework Programming

C++: 支持.NET程序设计的最强有力的语言

Introduction

VC++小组花了大量的时间听取了使用.NET和C++工作的用户的建议,决定重新设计VC在CLR的支持能力。新的设计被称为C++/CLI,计划以更自然的语法使用和创建托管类型。这里有新语法的介绍以及和C#的对比。

Common Language Infrastructure (CLI) 是一组规范,是构成.NET的基础设施。CLR则是CLI的一个实现。C++/CLI的设计以提供自然简单的CLI支持为目标,而VC++2005编译器使C++/CLI兼容CLR。

在你了解即将来到的VC2005编译器和C++/CLI时会得到两个讯息。一是VC打算定位于作为面向CLR开发的最底层的语言,以后将没有理由选择其他.NET语言了,包括IL汇编。第二,将会可以按照尽量接近原生C++编程的方式编程。在阅读这篇文章的过程中这两个讯息会变得越来越明朗。

这篇文章是写给C++程序员的,我不打算在这里劝你从C#或者VB.NET转型。如果你喜欢C++并且希望得到所有传统C++语言强大的优势,又需要C#那样高效的生产力,那么这篇文章就是为你准备的。另外,这篇文章没有提供CLR或者.NET Framework的介绍,而是关注于介绍VC2005是如何让你编写优雅而高效的面向.NET的代码的。

Object Construction

CLR定义了两种类型 - 引用类型和值类型。值类型是为高速创建和访问而设计的,他们表现的行为很像C++的内置类型,并且你也可以创建自己的值类型。这就是为什么Bjarne Stroustrup称之为固化类型。而引用类型是为提供所有面向对象需要和特性设计,比如分级的能力,就像这些:继承的对象,虚函数,如此等等。通过CLR,引用类型还提供其他运行时特性,比如自动内存管理(就是广为所知的GC)。CLR还同时为值类型和引用类型提供更深层次的在运行时得到类型信息的能力,这种能力源自反射。

值类型被分配在堆栈上,引用类型则在托管堆上,这是受CLR的垃圾收集器(GC)管理的堆。如果你使用C++来编写程序集,你可以像以前一样把这些原生的C++编写的类型放在CRT堆上。在以后,VC++小组还会设法让你可以把原生的C++编写的类型放在托管堆上。毕竟GC这样的东西对于原生代码来讲同样极具吸引力。

原生的C++语言允许你决定创建新对象的位置。任何类型都可以选择是放在堆栈上还是CRT堆上。

// 放在栈上
std::wstring stackObject;

// 放在CRT堆上
std::wstring* heapObject = new std::wstring;

就像您看到的那样,把对象放在哪里和对象类型无关,而是完全的受程序员控制。注意,在堆上分配对象的语法,和在栈上的不太一样。

在另外一个方面,C#允许在栈上创建值类型的对象,在堆上创建引用类型的对象。System.DateTime类(在后面几个例子也要用到)被他的作者定义为值类型。

// 放在栈上
System.DateTime stackObject = new System.DateTime(2003, 1, 18);

// 放在托管堆上
System.IO.MemoryStream heapObject = new System.IO.MemoryStream();

就像你看到的,你没有任何办法控制定义好的类型的对象是创建在堆上还是在栈上,这个选择完全取决于类型的设计者和运行环境(runtime)。

C++的托管扩展,引进了简单的混合进行原生C++代码和托管代码创作的能力。顺着C++的标准,扩展使C++可以支持很大范围的CLR构件。然而不幸的是,这里实在有太多的扩展,以至于用C++编写托管代码变成了巨大的痛苦。

// 放在栈上
DateTime stackObject(2003, 1, 18);

// 放在托管堆上
IO::MemoryStream __gc* heapObject = __gc new IO::MemoryStream;

把值类型分配在栈上的代码和原来的C++代码很像。然而托管堆的那段代码,则看起来有一点奇怪。__gc是托管C++的一个关键字。就像已经被证明了的,托管C++在有些情况其实也可以推断你的意思,所以这个例子也可以这样重写,而不用__gc关键字,这就是众所周知的默认行为。

// 放在托管堆上
IO::MemoryStream* heapObject = new IO::MemoryStream;

这样子更像原生C++代码了。但是问题在于那个heapObject并不是一个真正的C++指针。C++程序员们会始终信任他们所创建的对象的存在,但是GC却可能在任何时间移除这些对象。另外一个障碍则在于这里没有任何办法显式控制对象是在原生的还是托管的堆上分配内存。你需要知道类型是如何被它的作者定义的。除此之外,这里还有大量有力的证据证明重新定义C++指针的意义实在是个糟糕的注意。

C++/CLI带来一个新的句柄的概念以区别CLR对象引用和C++指针。它取消了重载C++指针含义的方式,这样大量的含混的内容都同时从这个语言移除。除此之外,通过句柄还能提供更多更自然的CLR支持。比如,你可以在C++内直接使用引用类型上的操作符重载,因为在句柄中是支持运算符重载的。这在“托管”指针内可能没法实现,因为C++禁止在指针上作运算符重载。

// 放在栈上
DateTime stackObject(2003, 1, 18);

 // 放在托管堆上
IO::MemoryStream^ heapObject = gcnew IO::MemoryStream;

这里我们同样没有定义值类型对象时的疑惑,而引用类型却不太一样。操作符^把这个变量定义为一个CLR引用对象的句柄。句柄的轨迹,意思是句柄的内容就是他指向的对象会被GC自动的更新,在内存中将不断的移动。另外,他们可以支持重新绑定,这样可以允许他们指向多个不同的对象,就像普通的C++指针。另外一个你必须注意的事情是关键字gcnew占用了原来new的位置,这清晰的标记了这个对象将会被分配在托管堆上。而new关键字不再为托管类型重载(不再含有双重含义),而只是用于在CRT堆上分配对象 - 当然除非你自己提供自己的new操作符。你还有什么理由不喜欢C++!

这样对象的创建得到了坚实的支持:原生的C++指针和CLR对象引用被清晰的区分开来。

Memory Management vs. Resource Management

当你面对一个带有垃圾收集器(GC)的环境时,把内存管理和资源管理区分开是非常有用的。通常,GC用于释放和分配你的对象所需要的内存,它不会去关心其它对象所占用的资源,比如数据库连接或者内核对象的句柄。在下面的两节,我将分别讨论内存管理和资源管理,因为他们对于理解本文内容至关重要。

Memory Management

原生C++让程序员拥有直接操作内存的能力。把对象放在堆栈上意味着对象将在进入特定函数时创建,使用的内存将在函数返回堆栈展开时释放。动态的建立对象是使用new关键字完成的,使用的内存在CRT堆上,而内存的释放必须显式的通过对指针使用delete操作符完成。这种对内存精细的控制能力使得C++可以用作高性能程序的开发,但如果程序员不够细心的话也容易导致内存泄露。显然,你并不是必须依靠GC才能避免内存泄露,但是事实上CLR就采用了这种方法,并且是一种很有效率的办法。当然GC托管的堆还有其它好处,比如提高分配内存的性能和内存寻址相关的内容。虽然所有这些使用都可以在C++通过库支持做到,但是建立在CLR上的这种机制有利于建立统一的内存管理编程模型,对所有语言都是一样的。想想操作COM组件的情形,你也许自己就可能产生这种想法。要是有一个通用的垃圾收集器,这将极大地减少编程的工作量。

很显然的CLR为了提高操作值类型时的性能保留了堆栈的概念。然而CLR也提供了一个newobj IL指令用于在托管堆上建立对象。在C#里对一个引用类型使用new操作符时会用到这个指令。在CLR里面没有任何和delete操作符等价的功能。先前分配的内存最终总是可以回收,当程序不再存在对某个对象的引用时,GC会自动进行内存收集操作。

托管的C++同样为作用在引用类型上的new操作符产生newobj IL指令,然而使用delete操作符操作托管的(GC托管的)对象 / 指针时却是不合法的。这显然是一对恼人的矛盾。这也是证明重载C++指针内涵不合理性的另一个原因。

C++/CLI没有为内存管理带来任何新的内容,就像我们在上一节已经暗示了的一样。然而资源管理,才是C++/CLI着实优越的地方。

Resource Management

到目前为止,在资源管理上还没有任何语言能超过C++。Bjarne Stroustrup"resource acquisition is initialization"技术在根本上定义了所有的资源都应该作为类模型提供构造器和析构器(用于释放资源)。这些类型可以被用于栈上的变量,或者作为更复杂类型的成员。他们的析构器可以自动释放他们所持有的资源。Stroustrup则说,"C++ is the best language for garbage collection principally because it creates less garbage."

有些奇怪的是,对于资源管理CLR没有提供任何直接的运行时支持,CLR也不支持C++意义上的析构函数。还有,.NET Framework已经把资源管理抽象为一个核心的接口称为IDisposable,意图是所有封装了资源的类必须实现这个接口的唯一的Dispose方法,在调用者不再需要这些资源时应该调用Dispose方法。不必多说,C++程序员往往会认为这种方式是在退步,因为他们早已习惯于默认就进行资源清理的操作。

必须通过调用一个函数才能进行资源清理带来的麻烦在于更难以编写异常安全的代码。你不能简单地把Dispose方法的调用放在代码块的最后,异常在任何时候都可能弹出,这样就有可能导致资源的泄漏。C#通过提供try-finallyusing声明来解决这个问题,这样提供了一种可信赖的方式来调用Dispose方法而不用担心异常造成的问题。但是这些结构有时候会带来麻烦,甚至更糟,你必须时刻记着编写这些代码,如果你忘记了那么编译器仍然会进行编译而带上了隐藏的问题。所以非常不幸的,对于缺乏真正的析构器的语言来讲try-finally或者using声明是不可少的。

在托管C++里面也有一样的问题。你必须使用try-finally声明,这些也是微软为C++的扩展。托管C++并不带有和C#里的using声明类似的等价结构,但是我们依然可以轻松的编写出一个包装了GCHandleUsing模版类,这样可以在模版类的析构函数里面调用对象的Dispose方法。

想象一下传统的C++对资源管理的强有力的支持吧,C++/CLI已经像C++那样把资源管理变成了轻而易举的事情。我们先看一下一个管理了一个资源的类,它一般会实现CLR的Dispose模式,而不能像原生C++那样简单的建立一个析构器就行了。当你编写Dispose函数的时候,你还需要保证调用父类的Dispose函数(如果有的话)。除此之外,如果你选择使用Finalize来调用Dispose方法,你将会有并发冲突的担心,因为Finalize方法运行在另外一个独立的线程上;还有,你需要保证可能在和普通程序代码运行的同时,小心的在Finalize方法内释放资源。

C++/CLI并没有能解决所有问题,但至少提供了很大的帮助。在查看它之前,让我们再次回首现在的C#和托管C++的处理方式。这个示例架设基类Base实现了IDisposable接口(如果不是的话,这个派生的类可能不需要)。

托管C++也非常的类似,那个看起来像析构器的方法实际上就是Finalize方法。编译器会小心的添加一个try-finally块以保证调用了基类的Finalize方法,所以C#和托管C++提供了简单有效的方法来实现Finalize方法,却没有为实现Dispose方法提供任何帮助 - 更加重要的东西。程序员们只能使用Dispose作为析构器,实际上这只是提供了析构的方式,而不是强制的。

C++/CLI在引用类型把Dispose作为逻辑上的“析构器”,进一步强调了它的重要性。

这样看起来更像C++程序员所熟悉的风格了,我们可以像以前那样在析构器内释放资源了。编译器会自动的添加必要的IL代码以实现IDisposable::Dispose方法,包括阻止GC调用对象上面的Finalize方法。事实上,在C++/CLI内实现Dispose方法是不合法的,继承IDisposable会导致编译器提示错误。当然,一旦一个类型编译完成,所有使用这个类型的CLI语言都会认为它实现了Dispose模式。在C#可以直接调用Dispose方法,或者使用using语句做到,就像这是用C#编写的类;但是C++呢?你该如何调用在堆上的对象的析构器?当然使用delete操作符!在句柄上使用delete操作符将调用对象的Dispose方法。回忆一下受GC管理的对象占用的内存块,我们并没有保证释放了这块内存,但是能保证立即释放对象所占有的资源。

所以如果传给delete操作符的表达式是一个句柄,对象的Dispose方法就会被调用。如果再也没有任何路径和这个对象相连,GC就会在合适的时候清理这个对象占用的内存;如果是一个原生的C++对象,在回到堆之前析构器会被调用。

显而易见的,在对象的生命周期管理上我们和原生的C++语法又靠近了一步,但是这里还是有忘记使用delete操作符的错误倾向。C++/CLI允许在引用对象上使用堆栈的语义,这就意味着我们可以使用一些现有的语法把引用对象“放”在堆栈上。编译器将会处理好这种你可能早就想要的语义,为了满足需求而已,实际上对象还是被放在托管堆上。

d离开有效作用范围时,它的Dispose方法就会被调用以释放资源。和上面提过的一样,由于对象实际上还是在托管堆上创建的,GC会在某个时间释放对象本身占用的内存。回到我们的ADO.NET的例子,现在使用C++/CLI可以写成这样:

Types Revisited

在讨论装箱/拆箱之前,再清理一下值类型和引用类型的区别会很有帮助。

你可以想象值类型就是简单的值,引用类型的实例则是对象。先不考虑内存需要存储对象的每个字段,实现面向对象的程序中所有的对象都有个头信息,比如类的虚函数,和其他的元数据一样可以用作各种用途。然而,在对象头信息 - 这些虚方法和接口上反复的查找操作的开销经常会很大 - 而你需要的往往只是一个静态类型的简单对象,并且只需要在编译阶段就确定了的操作。可被证明的,编译器在一些情况下可以优化掉这种对象上的操作,但是不是全部。如果你非常关心这种性能问题,很显然提供值和值类型是非常有好处的。这并不是要和C++的类型系统分离。当然,C++没有强加任何编程范式,所以在C++之上建立这样不同的类型系统而不是创建库来做是可行的。

Boxing

什么是装箱?装箱是填补值和对象之间隔阂的桥梁。尽管CLR要求所有的类型都要直接或者间接的继承Object类型,而实际上值类型对象并不是。在堆栈上的像整数这样的简单类型不过是一块内存,编译器保证程序可以直接操作它。如果你确实需要把一个值当作对象来看待,那它就应该是个对象,这样你就应该可以在你的值上面调用继承自Object的方法。为了实现这种要求,CLR提供了装箱的概念。了解这种装箱的过程还是有点用处的。首先一个值被IL指令ldloc压到堆栈上,然后一组装箱的IL指令开始运行:编译器提供静态类型信息,比如Int32,然后CLR使这个值出栈并且在托管堆分配一段足够的空间来存放这个值和对象头信息,一个指向这个新创建的对象的引用被压入堆栈。所有这些构成了装箱指令。最后,要得到这个对象的引用,IL指令stloc被用于从堆栈弹出这个引用并且存储在特定的本地变量上。

那么现在的问题是对于编程语言来讲,装箱操作是应该隐式的还是显式的进行。换句话说,需要显式的进行类型转换,或者使用其他构造来实现吗?C#语言设计者决定使用隐式的转换,毕竟整数确实是间接的继承于ObjectInt32类型。

int i = 123;
object o = i;

然而正如我们已经看到的那样,装箱操作并不是一个简单的向上的类型转换,这是由值到对象的转换,一个潜在的代价昂贵的操作。正是由于这个原因,托管C++使用__box关键字使装箱操作必须是显式的。

int i = 123;
Object* o = __box(i);

当然,在托管C++里面,你在装箱的时候不需要失去静态的类型信息,这是C#所没有提供的。

int i = 123;
int __gc* o = __box(i);

强类型的装箱操作带来了转换回值类型时的方便,也就是众所周知的拆箱,我们不需要再使用动态转换的语法,只要把对象直接取消引用即可。

int c = *o;

当然在托管C++内进行显式装箱的句法已经被证明是有些冗繁的。由于这个原因,C++/CLI的设计方针有了些改变,变成和C#一致的隐式装箱。而与此同时,我们仍然可以以类型安全的方式的直接的进行强类型的装箱操作,这一点是其他.NET语言所无法做到的。

int i = 123;
int^ hi = i;
int c = *hi;
hi = nullptr;

当然,这隐含指示了不指向任何对象的句柄不应该被初始化为零值,虽然它还是可以指向装箱了的0这个数值,然而指针不是。这就是设计常数nullptr的原由。它可以赋值到任何句柄。它等价于C#的null关键字。尽管nullptr在C++/CLI的设计中只是一个新的保留字,它现在被Herb SutterBjarne Stroustrup提议加入到C++标准内。

Authoring Reference and Value Types

在下面的几节中我们将回顾CLR内类型的一些细节。//..

Accessibility
Properties
Delegates
Conclusion

关于C++/CLI可以说的还有很多很多,对于VC2005的编译器本身无需担心,但是我希望这篇文章能够让你很好的了解它能为程序员带来什么。新设计的语言为.NET程序提供了前所未有的能力和优雅的编码方式,而没有在生产力、简单性和性能上作任何牺牲。

在下面的表格中有常用的语法构造对比

描述C++/CLIC#
创建引用类型的对象ReferenceType^ h = gcnew ReferenceType;ReferenceType h = new ReferenceType();
创建值类型的对象ValueType v(3, 4);ValueType v = new ValueType(3, 4);
引用类型在堆栈上ReferenceType h;N/A
调用Dispose 方法ReferenceType^ h = gcnew ReferenceType;

delete h;

ReferenceType h = new ReferenceType();

((IDisposable)h).Dispose();

实现Dispose方法~TypeName() {}void IDisposable.Dispose() {}
实现Finalize 方法!TypeName() {}~TypeName() {}
装箱(Boxing)int^ h = 123;object h = 123;
拆箱(Unboxing)int^ hi = 123;

int c = *hi;

object h = 123;

int i = (int) h;

定义引用类型ref class ReferenceType {};

ref struct ReferenceType {};

class ReferenceType {}
定义值类型value class ValueType {};

value struct ValueType {};

struct ValueType {}
使用属性h.Prop = 123;

int v = h.Prop;

h.Prop = 123;

int v = h.Prop;

定义属性property String^ Name
{
    String^ get()
    {
        return m_value;
    }
    void set(String^ value)
    {
        m_value = value;
    }
}
string Name
{
    get
    {
        return m_name;
    }
    set
    {
        m_name = value;
    }
}


关于作者:

Kenny Kerr spends most of his time designing and building distributed applications for the Microsoft Windows platform. He also has a particular passion for C++ and security programming. Reach Kenny at http://weblogs.asp.net/kennykerr/ or visit his Web site: http://www.kennyandkarin.com/Kenny/.

关于译者:

sunmast@vip.163.com

posted on Thursday, August 19, 2004 9:20 PM
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值