从C++到C++/CLI(1)

来源:互联网 发布:java判空 编辑:程序博客网 时间:2024/04/29 15:40

就像我们作出其它任何选择一样,在选择之前最重要的是先要清楚为什么作出这样或那样的选择——C++/CLI到底提供了哪些优势?为什么我们(标准C++程序员)要选择C++/CLI而不是C#?我们能够得到什么?CLI平台会不会束缚C++的能力?

 

这些都是来自标准C++社区的疑问。从google上面的讨论看来,更多来自标准C++社区的程序员担心的是C++/CLI会不会约束标准C++的能力,或者改变标准C++发展的方向,也有一部分人对C++/CLI的能力持怀疑态度。另外一些人则是询问C++/CLI能够带来什么。

 

这些被提出的问题在google上面一一得到了答案。好消息是:情况比乐观的人所想象的或许还要更好一些——

 

 

世界改变了吗?

 

对于谙于标准C++的程序员来说,最为关心的还是:在C++/CLI中,世界还是他们熟悉的那个世界吗?在标准C++的世界里,他们手里的各种魔棒——操作符重载|模板|多继承(语言),STL|Boost|ACE(库)——还能挥舞出五彩缤纷的火焰吗?是不是标准C++到了.NET环境下就像被拔掉了牙的老虎一样——Managed C++ Extension的阴影是不是还笼罩在他们的心头?

 

答案是:以前你所能做的,现在仍然能做,世界只是变得更广阔了——

 

 

什么是C++/CLI

 

l         C++/CLI是一集标准化的语言扩展(对标准C++进行扩展),而并非另起炉灶的另一门新语言。所以C++/CLI是标准C++的一个超集。

 

l         C++/CLI是一门ECMA标准[1](并且会被提交给ISO标准化委员会),而不是微软的专有语言。参与C++/CLI标准的修订的有很多组织(或公司),其中包括Edison Design GroupDinkumware公司等,微软在把C++/CLI标准草案提交给ECMA组织后就放弃了对其的控制权,而是将它作为一份公开发展的标准,任何使用C++/CLI的用户都可以为它的发展提出自己的建议。

 

l         C++/CLI的目的是把C++带到CLI平台上,使C++能够在CLI平台上发挥最大的能力。而并非把C++约束在CLI平台(CLI本身也是ISO标准化的)上。相反,原来标准C++的能力丝毫没有减弱,并且,通过C++/CLI中的标准扩展,C++具有了原来没有的动态编程能力以及一系列的first class.NET特性。这些扩展并非是专有的,而是以一种标准的方式呈现。

 

 

C++/CLI有什么优越性?

 

l         动态编程和refelection——标准C++是一门非常静态的语言,其原则是尽量在编译期对程序的合法性和逻辑作出检查。而在运行时的动态信息方面,标准C++是有所欠缺的。例如,标准C++在运行期能够对动态对象进行查询的就只有typeid操作符,而typeid()返回的typeinfo类虽然是个唯一标识,但是也仅仅止于“唯一”而已,首先标准C++并未规定typeinfo的底层二进制表示,所以用它作为跨平台的类唯一标识符就不可能了,其次typeinfo类几乎仅仅就表示类名字而已,这种非常“薄”的运行时类型信息阻止了标准C++在分布式领域的能力(IDL就是为了弥补标准C++在运行期类型信息的不足,但是IDL对于拥有元数据的语言如JAVAC#根本就不是必须的,同时IDL也使C++在分布式领域的使用不那么简易)。由于标准C++Native特点,所以其代码一经编译便几乎丧失所有的类型信息,从而使得运行期无法对程序本身做几乎任何的改动,换句话说,标准C++的代码一经编译就几乎变成了死的。而C++/CLI改变了这一现状,C++/CLI拥有完备的元数据,允许程序在运行期查询完整的类型信息,并可以由类型信息动态创建对象,甚至可以动态创建类型,添加方法等等,这种强大的运行期的动态特性对于现代应用领域(例如分布式WEB应用)是必须的。

 

l         GC——现在谁也不会说“往C++中加入GC就是终结了C++”这种话了。就连Bjarne Stroustrup也同意如果C++要被用于大型或超大型的软件开发中去,最好要有一个良好的可选的GC支持。GC与否,已经不再是个值得争论的问题,问题是,我们如何把它实现的更好——C++/CLI中的GC是目前最为强大的GC机制之一——分代垃圾收集。GC的好处是可以简化软件的开发模型,在效率并非极其关键的领域,GC可以很大程度上提高生产率。C++/CLI中的GC很重要的一点是:它正是可选的。这一点不同于JAVAC#,对于后者,GC无处不在,对象只能分配在托管堆上。而在C++/CLI中,如果把你的对象分配在Native Heap上,你就得到和标准C++一样的高效内存管理。如果把对象分配在Managed Heap上,那么该对象的内存就由GC来自动回收。这种混合式的内存管理环境和C++/CLI的定位有关——毕竟,C++/CLI的定位是.NET平台上的系统级编程语言,所以效率以及对底层的控制很重要,故保留了Native Heap。后面你会看到这种编程环境的优点。

 

l         BCL——.NET平台上的基础类库,BCL中丰富的类极大的方便了开发者。C++/CLI可以完全使用BCL中的任何类。

 

l         可移植性——毫无疑问,可移植性是个至关重要的问题,特别是对于标准C++社群的人们。C++/CLI对这个问题的答案是:“如果你的代码不依赖于本地二进制库,就可以“一次编译,随处运行(在.NET平台上)”(使用“/clr:pure”编译选项将代码编译成可移植的纯MSIL代码)。如果你的代码某部分依赖于本地的二进制库(C输入输出流库),那么这些部分仍然是源代码可移植的,而其它部分则可以“一次编译,随处运行”。对于标准C++来说,向来保证的只是源代码的可移植性,所以我们并没有失去什么,相反,如果遵守协定——不用本地二进制库,例如,用BCL里的输入输出流库代替C输入输出流库——你就可以得到“一次编译,随处运行”的承诺,也就是说,你的代码经过编译(/clr:pure)后可以在其它任何.NET平台上运行——UnixLinux下的Mono(移植到UnixLinux下的.NET),以及FreeBSDMac OSX下的Rotor.NET的开放源代码项目),等等。

 

习惯了标准C++输入输出流的程序员可能要抱怨了——我们为什么要使用BCL里面的输出输出流?标准的iostream已经很好了!这里其实有一个解决方案,使用 iostream的代码之所以不能“一次编译,随处运行”是因为代码要依赖于本地的二进制lib文件,而如果可以把iostream的实现重新也编译成纯MSIL代码,那么使用它的代码编译后就完全可随处运行了。目前,这是个有待商榷的方案。不过,至少当面对“总得依赖于某些平台相关的二进制代码”这种情况时,可以把平台相关的代码封装成DLL文件——对各个目标平台编译成不同的二进制版本,而程序的其它部分仍然只需一次编译即可,只要使用.NETP/Invoke就可以对不同平台调用相应的DLL了。

 

l         效率——作为.NET平台上的系统级编程语言,C++/CLI混合了NativeManaged两种环境。而不象C#那样只能进行托管编程。所以相对来说,C++/CLI可以具有更高的效率——前提是你愿意把效率敏感的代码用极具效率的Native C++来写(当然,谁不愿意呢?)另外,因为标准C++是静态语言,所以作为标准C++的一个超集的C++/CLI能够在编译期得到更多的优化(静态语言总是能够得到更多的优化,因为编译器能够知道更多的信息),从而具有更高的效率。相比之下,C#的编译期优化就弱了很多。

 

l         混合式的编程环境——这是C++/CLI独有的一种编程环境。你既可以进行高效的底层开发——对应于C++/CLI的标准C++子集,也可以在效率要求不那么严格的地方使用托管式编程以提高生产率。然后把两者平滑的联结在一起,而这一切都在你熟悉的编程语言中完成,你使用你熟悉的编程习惯,熟悉的库,熟悉的语言特性和风格不需要从头学习一门新的语言,不需要面对语言之间如何交互的问题。

 

l         习惯——谁也不能小觑习惯的力量。对于标准C++程序员,如果要在.NET平台上开发,C++/CLI是毫无疑问的首选语言,因为他们在标准C++中积累起来的任何编程技巧,惯用法,以及对库的使用经验,代码的表达方式等等全都可以“移植”到C++/CLI中。C++/CLI保持对标准C++代码的完全兼容,同时以最小最一致的语法扩展提供托管环境下编程的必要语义。

 

 

你需要改变什么?

 

      简单的答案是,几乎没有什么需要改变的。是的,你看到“几乎”两个字,总有些不安心:o)事实是:把现存的C++代码移植到C++/CLI环境下不用作任何的改变——我曾经用Native C++写了一个程序,其中用到了STLBoost里面的LambdaMPLSignal等库,然后我把编译选项“/clr”(甚至“/clr:pure”)打开,结果是程序完全通过了编译。而对于使用C++/CLI进行开发的程序员,则需要熟悉的就是.NET平台上的编程范式以及库的使用等,至于以前你所熟悉的标准C++编程的各种编程手法,技巧,各种库的使用——Just Keep Them!

 

      所以,确切的说,你需要的是学习,而不是改变。

 

 

C++/CLI——优秀的混血儿

 

      C++/CLI最大的成功在于引入了混合式编程的环境,这是一种非常自由的环境,其中NativeManaged代码可以共存,可以相互沟通,从而完全接纳了标准C++的世界,同时也为另一个世界敞开了大门...

 

    下面就是C++/CLI扩展的几大关键特性——

 

Handlegcnew——通往Managed世界的钥匙

 

       还记得在Managed C++ Extension世界里是如何访问托管类的吗?丑陋的__gc关键字无处不在——事实上,不仅是“丑陋”而已(MC++为什么会消亡?)。而在C++/CLI里则引入了一个新的语法元素,名为Handle,写作“^”——你可以把它看成Managed世界里的Pointer(不过不能进行指针算术)。

 

Handle用于持有Managed Heap上的对象,那么如何在Managed Heap上创建对象呢?原来的new显然不能用,那样会混淆其语义,所以C++/CLI引入了一个对应的gcnew关键字,这两个新的语法元素是操纵Managed世界的关键。现在,使用Handlegcnew,你就可以和任何托管类进行沟通。另外,既然有了Handle这个Managed指针,当然,基于另外一些重要原因,Managed世界里也要有一个和Native引用类似的语法元素——这就是Managed引用“%”——“^”对应“*”,“%”对应“&”,这样一来,从语法的层面上,指针、引用、以及在堆上创建对象的语法就在两个世界里面对称一致了——哦,等等,还有解引用:对Native Pointer解引用是以“*”,出于模板对形式统一性的要求,对Handle解引用也是用“*”。例如:

SomeManagedClass^ handle = gcnew SomeManagedClass( ... );
handle
->someMethod();
SomeManagedClass
% ref = *handle;

    那么,既然有gcnew,有没有gcdelete呢?答案是没有——虽然它们看起来很对称。理由是对于托管类,根本就不用回收内存。但更为重要的还是,delete的语义不仅仅是回收内存,从广义上说,delete是回收资源的意思,从这个意义上,delete托管类还是Native类的对象都是一个意思。所以,即使你需要delete你的托管类对象,以强制其释放资源,你也应该用delete,这时候托管类的析构函数会被调用——是的,托管类也有析构函数,它的语义和Dispose()一样,但是在C++/CLI里面,你不应该为你的托管类定义Dispose()函数,而总是应该用析构函数来代替它(编译器会根据析构函数自动生成Dispose()函数),因为析构函数有一个最大的优点——

原创粉丝点击