C#抽象工厂模式的几种实现方法及比较
|
利用设计模式可以使我们的代码更灵活,更容易扩展,更容易维护。各种面向对象的程序设计语言都提供了基本相同的机制:比如类、继承、派生、多态等等。但是又有各自的特色,C# 中的反射机制便是一个很重要的工具,好好地利用就可以在实际中发挥很大的作用。 我们来看一个例子:
我的程序中有需要一系列的对象,比如apple,orange…, 要想利用他们,我们就必须在程序中根据用户要求,然后一个个调用 new
操作符来生成他们,这样客户程序就要知道相应的类的信息,生成的代码显然不够灵活。我们可以在代码中不利用具体的类,而只是说明我们需要什么,然后就能够
得到我们想要的对象吗? 哦,我们都看设计模式,听吧,很多人都在那里鼓吹他们是如何如何的棒,我们看看怎么样利用他们来解决问题。目
标明确了,那我们看看哪个能够符合我们的要求。GoF的《设计模式》都看过吧,似懂非懂的看了一些,那我们看看能够不能够“凑”上去呢?J
嗯,我们的程序考虑的是对象怎么创建的,创建型模式应该符合要求吧。然后我们浏览一下各模式的“意图”部分。呵呵,第一个好像就撞到彩了,抽象工厂,我们
看看吧,“提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类”,至少“无需指定它们具体的类”符合我们的要求。来看看它的结构吧:
我们的问题好像用不到这么复杂吧,只有orange,apple等等(应该就是product了),他们显然是一类的,都是fruit,我们只要一个生
产水果的工厂就可以,左边的继承层次不要,只有一个FruitFactroy看看行不,先别管它正统不正统,实用就行J 下面的一些东西显然是我们需要的:
Public interface IFruit { }
public class Orange:IFruit { public Orange() { Console.WriteLine("An orange is got!"); } }
public class Apple:IFruit { public Apple() { Console.WriteLine("An apple is got!"); } } |
我们的FruitFactory应该是怎么样呢?上面的结构图中它给的是CreateProductA,那好,我就MakeOrange,还有一个CreateProductB,俺MakeOrange还不行??
public class FruitFactory { public Orange MakeOrange() { return new Orange(); } public Apple MakeApple() { return new Apple(); } } |
怎么使用这个工厂呢?我们来写下面的代码:
string FruitName = Console.ReadLine(); IFruit MyFruit = null; FruitFactory MyFruitFactory = new FruitFactory();
switch (FruitName) { case "Orange": MyFruit = MyFruitFactory.MakeOrange(); break; case "Apple": MyFruit = MyFruitFactory.MakeApple(); break; default: break; } |
编译运行,然后在控制台输入想要的东西,呵呵,成功了。沉浸在幸福中的你得意忘形了吧。
|
发布日期: 六, 18 八月 2007 06:02:01 GMT
计算机与 Internet |
阅读完整项
简单工厂模式与工厂方法模式
2007-07-23 15:42
在OO设计领域,我们知道前人总结了不少的经验,许多的经验在现代软件工程过程中已经被认为是原则来遵守。下面笔者摘抄几项下文涉及到的OO原则的定义。
OCP(开闭原则,Open-Closed Principle):一个软件的实体应当对扩展开放,对修改关闭。我的理解是,对于一个已有的软件,如果需要扩展,应当在不需修改已有代码的基础上进行。
DIP(依赖倒转原则,Dependence Inversion Principle):要针对接口编程,不要针对实现编程。我的理解是,对于不同层次的编程,高层次暴露给低层次的应当只是接口,而不是它的具体类。
LoD
(迪米特法则,Law of
Demeter):只与你直接的朋友通信,而避免和陌生人通信。众所周知类(或模块)之间的通信越少,耦合度就越低,从而更有利于我们对软件的宏观管理。
老子论“圣人之治”有相同的思想,《老子》云:“是以圣人之治,虚其心,实其腹,弱其志,常使民无知无欲。”,又云:“小国寡民,邻国相望,鸡犬之声相
闻,民至老死,不相往来。”。佩服我们的老祖宗,N千年前就想到了西方N千年后才想到的东西,同时也佩服《java与模式》的作者阎宏,可以用中国传统哲
学思想这么生动的说明这一软件设计原则。
简单工厂模式及实例 简单工厂模式又叫静态工厂模式,顾名思义,它是用来实例化目标类的静态类。下面我主要通过一个简单的实例说明简单工厂及其优点。
比
如有个国家的运动员协会,他们是负责登记与注册职业运动员的(就好像我们国家的体育总局,呵呵,无论足球篮球还是乒乓球的运动员都必须在这里注册才能拿到
我们国家职业运动员牌照)。一家体育俱乐部(比如篮球的广东宏远,足球的深圳健力宝)想获得球员为自己俱乐部效力,就必须通过这个运动员协会。
根据DIP我们可以设计一个“运动员”接口,“足球运动员”和“篮球运动员”(还有其他运动员)都实现“运动员”这个接口。而“运动员协会”就是一个简单
工 厂类,它负责实例化“运动员”。我们这里的“俱乐部”就是一个客户端(Client),不同的“俱乐部”就是不同的客户端。
对于不同的俱乐部对象(无论是八一还是深圳健力宝),他们都是面向“运动员”接口编程,而不用管是“足球运动员”还是“篮球运动员”,也就是说实现了“运
动
员”接口的具体类“足球运动员”无需暴露给客户端。这也满足了DIP。但具体的俱乐部(比如足球的深圳健力宝)如何确保自己获取的是自己想要的运动员(健
力宝俱乐部需要的当然是足球运动员)呢?这就需要“运动员协会”这一工厂类了。俱乐部通过调用“运动员协会”的具体方法,返回不同的实例。这同时也满足了
LoD,也就是“深圳健力宝足球俱乐部”对象不直接与“足球运动员:李毅”对象通信,而是通过他们共同的“朋友”——“国家体育总局”通信。
下面给出各个类的程序,会有助于读者更好的了解笔者之前的介绍。
Code:
|
|
运动员.java public interface 运动员 { public void 跑(); public void 跳(); }
足球运动员.java public class 足球运动员 implements 运动员 {
public void 跑(){ //跑啊跑 } public void 跳(){ //跳啊跳 } }
篮球运动员.java public class 篮球运动员 implements 运动员 {
public void 跑(){ //do nothing } public void 跳(){ //do nothing } }
体育协会.java public class 体育协会 { public static 运动员 注册足球运动员(){ return new 足球运动员(); } public static 运动员 注册篮球运动员(){ return new 篮球运动员(); }
}
俱乐部.java public class 俱乐部 { private 运动员 守门员; private 运动员 后卫; private 运动员 前锋;
public void test() { this.前锋 = 体育协会.注册足球运动员(); this.后卫 = 体育协会.注册足球运动员(); this.守门员 = 体育协会.注册足球运动员(); 守门员.跑(); 后卫.跳(); } }
|
|
以上就是简单工厂模式的一个简单实例,读者应该想象不用接口不用工厂而把具体类暴露给客户端的那种混乱情形吧(就好像没了体育总局,各个俱乐部在市场上自己胡乱的寻找仔细需要的运动员),简单工厂就解决了这种混乱。
我
们用OCP看看简单工厂,会发现如果要对系统进行扩展的话治需要增加实现产品接口的产品类(上例表现为“足球运动员”,“篮球运动员”类,比如要增加个
“乒乓球运动员”类),而无需对原有的产品类进行修改。这咋一看好像满足OCP,但是实际上还是需要修改代码的——对,就是修改工厂类。上例中如果增加
“乒乓球运动员”产品类,就必须相应的修改“体育协会”工厂类,增加个“注册乒乓球运动员”方法。所以可以看出,简单工厂模式是不满足OCP的。
工厂方法模式及其实例 谈
了简单工厂模式,下面继续谈谈工厂方法模式。前一节的最末点明了简单工厂模式最大的缺点——不完全满足OCP。为了解决这一缺点,设计师们提出了工厂方法
模式。工厂方法模式和简单工厂模式最大的不同在于,简单工厂模式只有一个(对于一个项目或者一个独立模块而言)工厂类,而工厂方法模式有一组实现了相同接
口的工厂类。下面我们通过修改上一节的实例来介绍工厂方法模式。
我们在不改变产品类(“足球运动员”类和“篮球运动员”类)的情况下,修改下工厂类的结构,如下图所示:
相关代码如下:
Code:
|
|
运动员.java public interface 运动员 { public void 跑(); public void 跳(); }
足球运动员.java public class 足球运动员 implements 运动员 {
public void 跑(){ //跑啊跑 } public void 跳(){ //跳啊跳 } }
篮球运动员.java public class 篮球运动员 implements 运动员 {
public void 跑(){ //do nothing } public void 跳(){ //do nothing } }
体育协会.java public interface 体育协会 { public 运动员 注册(); }
足球协会.java public class 足球协会 implements 体育协会 { public 运动员 注册(){ return new 足球运动员(); } }
篮球协会.java public class 篮球协会 implements 体育协会 { public 运动员 注册(){ return new 篮球运动员(); } }
俱乐部.java public class 俱乐部 { private 运动员 守门员; private 运动员 后卫; private 运动员 前锋;
public void test() { 体育协会 中国足协 = new 足球协会(); this.前锋 = 中国足协.注册(); this.后卫 = 中国足协.注册();
守门员.跑(); 后卫.跳(); } }
|
|
很明显可以看到,“体育协会”工厂类变成了“体育协会”接口,而实现此接口的分别是“足球协会”“篮球协会”等等具体的工厂类。
这样做有什么好处呢?很明显,这样做就完全OCP了。如果需要再加入(或扩展)产品类(比如加多个“乒乓球运动员”)的话就不再需要修改工厂类了,而只需相应的再添加一个实现了工厂接口(“体育协会”接口)的具体工厂类。
从以上对两种模式的介绍可以了解到,工厂方法模式是为了克服简单工厂模式的缺点(主要是为了满足OCP)而设计出来的。但是,工厂方法模式就一定比简单工厂模式好呢?笔者的答案是不一定。下面笔者将详细比较两种模式。
1. 结构复杂度 从这个角度比较,显然简单工厂模式要占优。简单工厂模式只需一个工厂类,而工厂方法模式的工厂类随着产品类个数增加而增加,这无疑会使类的个数越来越多,从而增加了结构的复杂程度。
2.代码复杂度 代码复杂度和结构复杂度是一对矛盾,既然简单工厂模式在结构方面相对简洁,那么它在代码方面肯定是比工厂方法模式复杂的了。简单工厂模式的工厂类随着产品类的增加需要增加很多方法(或代码),而工厂方法模式每个具体工厂类只完成单一任务,代码简洁。
3.客户端编程难度 工厂方法模式虽然在工厂类结构中引入了接口从而满足了OCP,但是在客户端编码中需要对工厂类进行实例化。而简单工厂模式的工厂类是个静态类,在客户端无需实例化,这无疑是个吸引人的优点。
4.管理上的难度 这是个关键的问题。 我
们先谈扩展。众所周知,工厂方法模式完全满足OCP,即它有非常良好的扩展性。那是否就说明了简单工厂模式就没有扩展性呢?答案是否定的。简单工厂模式同
样具备良好的扩展性——扩展的时候仅需要修改少量的代码(修改工厂类的代码)就可以满足扩展性的要求了。尽管这没有完全满足OCP,但笔者认为不需要太拘
泥于设计理论,要知道,sun提供的java官方工具包中也有想到多没有满足OCP的例子啊(java.util.Calendar这个抽象类就不满足
OCP,具体原因大家可以分析下)。 然后我们从维护性的角度分析下。假如某个具体产品类需要进行一定的修改,很可能需要修改对应的工厂类。当同时
需要修改多个产品类的时候,对工厂类的修改会变得相当麻烦(对号入座已经是个问题了)。反而简单工厂没有这些麻烦,当多个产品类需要修改是,简单工厂模式
仍然仅仅需要修改唯一的工厂类(无论怎样都能改到满足要求吧?大不了把这个类重写)。
由以上的分析,笔者认为简单工厂模式更好用更方便些。当然这只是笔者的个人看法而已,毕竟公认的,工厂方法模式比简单工厂模式更“先进”。但有时过于先进的东西未必适合自己,这个见仁见智吧。
|
发布日期: 五, 17 八月 2007 09:39:37 GMT
计算机与 Internet |
阅读完整项
简话设计模式
|
作者:杨宁(来自grapecity)
原文地址:http://www.uml.org.cn/sjms/200432950.htm
|
第一章 引言
1. 本文不适合… 本文不适合想通过本文来装修房子的读者;
本文不适合面向对象编程高手,会浪费你的时间。如果你愿意抽出时间来阅读本文,并提出宝贵的建议,非常感谢!什么?你没有听说过设计模式?那你也敢称高手?
2. 本文适合… 如
果你对面向对象编程感兴趣,而又没有时间去读Gang of Four的“Design Patterns Elements of Reusable
Object-Oriented Software”(以下简称《设计模式》)。那么,本篇文章将帮助你了解23种设计模式。
我第一次读这本书是在
每次晚睡之前,几乎每次都先睡着。《设计模式》以一种严谨,系统化的风格来论述23种设计模式,原书可以说是面向对象编程的一个基础教程,但是要领会其精
髓,必须要花费一定的精力。本文的目的是为了帮助你更加方便地理解每一种设计模式,并不想成为原书的替代读物。
本文无意于介绍面向对象的基本知识。因此,假设本文的读者已经对面向对象的封装性、继承性和多态性有足够的了解和认识。并能够认识到可复用的面向对象设计的两个原则:
3. 设计模式是什么? 设
计模式概念是由建筑设计师Christopher
Alexander提出:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必
做重复劳动。”上述的定义是对设计模式的广义定义。我们将其应用到面向对象软件的领域内,就形成了对设计模式的狭义定义。
我们可以简单的认为:设计模式就是解决某个特定的面向对象软件问题的特定方法。但严格的来说上述的认识是不准确的,难道面向对象软件中只有区区23个问题?当然不是。
为了能够更加准确地理解这个概念,我们引入另外一个术语:框架(Framework)。框架这个词汇在当今有了各种各样的应用和含义。在设计模式中:框架(Framework)是构成一类特定软件可复用设计的一组相互协作的类。
框架可以认为是一个适用于某个领域的软件包。这个软件包提供了相应领域的各个问题的解决方法。那么,它和设计模式有什么区别?
|
设计模式针对面向对象的问题域;框架针对特定业务的问题域; |
|
设计模式在碰到具体问题后,才能产生代码;框架已经可以用代码表示。 |
Tips:设计模式就像是在武功中基本的招式。我们将这些招式合理地组合起来,就形成套路(框架)。
4. 为什么要用设计模式? 作
为程序员都知道良好程序的一个基本标准:高聚合,低耦合。面向对象语言比结构化语言要复杂的多,不良或者没有充分考虑的设计将会导致软件重新设计和开发。
然而实际的设计过程中,设计人员更多的考虑如何解决业务问题,对于软件内部结构考虑较少;设计模式则补充了这个缺陷,它主要考虑如何减少对象之间的依赖
性,降低耦合程度,使得系统更易于扩展,提高了对象可复用性。因此,设计人员正确的使用设计模式就可以优化系统内部的结构。
第二章 概要简介
在《设计模式》一书中,共包含23个模式。根据目的的不同,将它们分为三类:
|
● 创建型(Creational):解决如何创建对象的问题。 |
|
● 结构型(Structural):解决如何正确的组合类或对象的问题。 |
|
● 行为型(Behavioral):解决类或对象之间如何交互和如何分配职责的问题。 |
Tips:设计模式中经常会用到抽象(Abstract)和具体(Concrete)这两个词。抽象的含义是指它所描述的类(方法)是接口类(方法),具体的含义是指它所描述的类(方法)实现了相应的抽象类(方法)。
第三章 抽象工厂(Abstract factory)
1. 意图 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
2. 分类 创建型模式。
3. 问题是什么? 对于不熟悉这个模式的人都会对工厂(Factory)这个词感到奇怪,为什么会用这个词?之后,我们还会碰到另一个模式——工厂方法(Factory method),所以先解释一下工厂的意义:
房
子是由墙,门,窗户,地板,天花板,柱子组成的。如果我们为客户编写一个建造房子的软件,我们会把墙,门,窗户,地板,天花板,柱子看成不同的类:
WallClass, DoorClass, WindowClass, CeilingClass, PillarClass。
现在我们建立一个新类
A,这个类中有CreateWall(), CreateDoor(), CreateFloor(), CreateCeiling(),
CreatePillar()五个方法,每个方法的功能就是创建并返回相应的对象。如果把WallClass, DoorClass,
WindowClass, CeilingClass, FloorClass,
PillarClass的实例看成产品的话,那么类A就像是一个生产这些产品的工厂。这就是使用工厂这个词的原因。
Tips:A这个名字太糟,如果用HouseFactory会好一些。一般情况下,在你的系统使用某个模式,最好使用模式相应的关键字作为类或者方法的名字的一部分,这样,你的同伴或者系统的代码维护者就会明白你的用意。
我们的软件完成了,客户非常满意。不过,我们的客户想把这个软件出口,他发现一个问题,这个软件太本地化了,建造出来的都是中国式的房屋。因此他希望我们的软件能够建造出不同地域风格的房子。
这就是我们的问题!我们需要重新设计原系统,而且一次完成世界各地不同建筑风格是不可能的。我们会先完成部分风格(客户第一要投放软件的国家的),然后再增加其他的…
4. 解决方法 1) 建立一个抽象工厂(Abstract Factory)类HouseFactory,在这个类中声明:
2) 建立相应的抽象产品(Abstract Product)类集:
Wall, Door, Floor, Ceiling, Pillar
3) 为不同风格建立相应的具体工厂(Concrete Factory)类(不要忘了实现关系),例如:
|
ChinaHouseFactory : HouseFactory |
|
GreeceHouseFactory : HouseFactory |
4) 为不同的风格建立相应的具体产品(Concrete Product)类(实现相应的抽象产品),例如:
5. 杂谈 我想你一定明白如何灵活的创建和使用上面的一大堆类:
抽象工厂模式的重点不是声明的那个抽象工厂类,而是它声明的一系列抽象产品类,我们通过使用这些抽象产品类可以操作我们已经实现或者还未实现的具体产品类,并且保证了它们的一致性。
你
可能已经发现这个软件不能建造你的两层别墅,因为它没有楼梯。为此,我们要定义Stair抽象类,还要增加CreateStair抽象方法;最重要的是我
们要在已经完成的76种风格中增加相应的Stair类,这可是个大麻烦。确实,抽象工厂模式在适应新的产品方面的能力是比较弱的。这是它的一个缺点。
第四章 生成器(Builder)
1. 意图 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
2. 分类 创建型模式。
3. 问题是什么? 在抽象工厂一章中,我们了解到了在全球建房系统中如何支持多种房屋的风格。新的问题是:用户希望看到同样结构下,不同风格房屋的外观。比如:这个房子有一个地板,一个天花板,四面墙,一个门,一个窗户。他希望看到中国风格房屋和希腊风格房屋的相应式样。
4. 解决方法
1) 创建一个生成器(Buider)类:
Class HouseBuider { BuildHouse(){} BuildWall(int){} BuildDoor(int){} BuildWindow(int) {} BuildFloor(){} BuildCeiling() {} BuildPillar(){} GetHouse() { return Null;} }
|
在这个类中,我们对每一种房屋组成元素都定义了一个Build方法。并且定义了一个返回构造结果的方法GetHouse。
2) 对每一种风格都定义一个具体的生成器(Concrete Builder)类:
ChinaHouseBuilder: HouseBuilder GreeceHouseBuilder: HouseBuilder …
|
并且在这些类中重载父类的方法。
3) 还有,各种类型的房屋类:
5. 如何使用 我们可以用下面的方法使用上面的类来解决我们的问题。
Class HosueSystem { object Create( builder HouseBuilder) { builder.BuildHouse(); builder.BuildFloor(); builder.BuildCeiling(); builder.BuildWall(1); builder.BuildWall(2); builder.BuildWall(3); builder.BuildWall(4); builder.Door(1); // 在Wall1上建一个门 builder.Window(2); // 在Wall2上建一个窗户 return builder.GetHouse(); } }
|
只要向通过HouseSystem.Create方法传入不同的生成器就可以得到不同风格的结果。
事实上,HouseSystem也是生成器模式的一个参与者,叫做导向者(Director)。注意的问题:
|
● 生成器(HouseBuilder)类中要包含每一种构件元素的生成方法。比如:楼梯这个元素在某些建筑风格中没有,在其他风格中有,也要加上BuilderStair方法在HouseBuilder中。 |
|
● 生成器(HouseBuilder)类不是抽象类,它的每一个生成方法一般情况下什么都不做。这样,具体生成器就不必考虑和它无关的元素的生成方法。 |
6. 杂谈
1) 也许你有这样的想法:生成器模式似乎过于复杂,可以直接在具体House类中定义CreateHouse方法就可以了,例如:
Class ChinaHouse { ChinaHouse CreateHouse() { ChinaHouse house; house = new ChinaHouse(); house.Add(new Floor()); house.Add(new Ceiling()); house.Add(new Wall(1)); house.Add(new Wall(2)); house.Add(new Wall(3)); house.Add(new Wall(4)); house.Add(new Door(1)); // 在Wall1上建一个门 house.Add(new Window(2)); // 在Wall2上建一个窗户 return house; } }
|
而生成器模式至少用了两个类来解决这个问题:导向者类和具体生成器类。
那么,生成器模式好在哪里?答案就是职责分配。生成器模式将一个复杂对象的生成这一职责作了一个很好的分配。它把构造过程放到导向者的方法中,把装配过程放到具体生成器类中。我们看看下面的说明。
2)
HouseSystem类(导向者)可以非常精细的来构造House。而这个生成过程,对于产品类(ChinaHouse, GreeceHouse
…)和生成器类 (ChinaHouseBuilder, GreeceHouseBuilder)都没有必要关心。具体生成器类则考虑装配元素的问题。
7. 活用 生成器模式可以应用在以下的问题上:
● 将系统的文档格式转换到其他的格式上(每一种格式的文档都相当于一个产品)。 ● 编译问题(语法分析器是导向者,编译结果是产品)。
关于作者: 杨宁是GrapeCity公司海外应用开发部技术骨干。从事多年的程序开发,有Unix,Windows平台上的开发经验。对VB,C#, VB.Net,XML有比较丰富的认识。喜爱研究OO的编程,分析,设计,项目管理等相关技术。喜欢学习新的技术。
|
发布日期: 一, 13 八月 2007 14:15:10 GMT
计算机与 Internet |
阅读完整项
C++/CLI中实现singleton模式
2006-02-13 08:56 作者: 吴尔平 出处: 博客园 责任编辑:
方舟
双重检测锁(Double-Checked
Locking)实现的Singleton模式在多线程应用中有相当的价值。在ACE的实现中就大量使用ACE_Singleton模板类将普通类转换成
具有Singleton行为的类。这种方式很好地消除了一些重复代码臭味,而且,优化后的性能较标准互斥版本提高15倍。最近在用C++/CLI做一些工
作,Singleton不可避免地需要用到,于是我又制造了一次车轮。
1 #pragma once 2 3 /** \class sidle::Singleton 4 \brief Singleton (Double-Checked Locking) 5 \author 吴尔平 6 \version 1.0 7 \date 2005.02.08 - 8 \bug 9 \warning 10 */ 11 12 namespace sidle 13 { 14 using namespace System; 15 using namespace System::Threading; 16 17 template<typename _T> 18 ref class Singleton 19 { 20 public: 21 static _T^ Instance() 22 { 23 if (_instance == nullptr) 24 { 25 _mut->WaitOne(); 26 try 27 { 28 if (_instance == nullptr) 29 { 30 _instance = gcnew _T(); 31 } 32 } 33 finally 34 { 35 _mut->ReleaseMutex(); 36 } 37 } 38 return _instance; 39 } 40 protected: 41 Singleton(){} 42 static _T^ _instance; 43 static Mutex^ _mut = gcnew Mutex(); 44 }; // ref class Singleton 45 46 }; // namespace sidle |
发布日期: 一, 13 八月 2007 05:35:44 GMT
计算机与 Internet |
阅读完整项
C++/CLI:第一流的CLI语言
2005-08-25 11:25 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
1. 简介
本文并不是为了奉承C++/CLI的辉煌,也不是为了贬低其它如C#或者VB.NET等语言,相反,这只是一个非官方的、以一个喜欢这种语言的非微软雇员身份来论证C++/CLI有它的自己的唯一的角色,可作为第一流的.NET编程语言。
一个不断在新闻组和技术论坛上出现的问题是,当象C#和VB.NET这样的语言更适合于这种用途时,为什么要使用C++来开发.NET应用软件。通常这
样一些问题后面的评论说是,C++语法是怎样的复杂和令人费解,C++现在是怎样一种过时的语言,还有什么VS.NET设计者已不再像支持C#和
VB.NET一样继续支持C++。其中一些猜疑是完全荒谬的,但有些说法部分正确。希望本文有助于澄清所有这些围绕C++/CLI语言及其在VS.NET
语言层次中的地位的疑惑,神秘和不信任。请记住,本作者既不为微软工作也没有从微软那里取得报酬,只是想从技术上对C++/CLI作一评判。
2. 快速简洁的本机interop
除了P/Invoke机制可用在另外的象C#或VB.NET这样的语言外,C++提供了一种独有的interop机制,称作C++
interop。C++
interop比P/Invoke直观得多,因为你只是简单地#include需要的头文件,并与需要的库进行链接就能象在本机C++中一样调用任何函
数。另外,它比P/Invoke速度快--这是很容易能证明的。现在,可争辩的是在实际应用软件的开发中,经由C++
interop获得的性能好处与花在用户接口交互、数据库存取、网络数据转储、复杂数学算法等方面的时间相比可以被忽略,但是事实是在有些情况下,甚至通
过每次interop调用节省的几个纳秒也能给全局应用程序性能/响应造成巨大影响,这是绝对不能被忽视的。下面有两部分代码片断(一个是使用
P/Invoke机制的C#程序,一个是使用C++
Interop机制的C++程序),我分别记录了其各自代码重复执行消耗的时间(毫秒)。不管你如何解释这些数据,不管这会对你的应用程序产生什么影响,
全是你的事。我仅打算事实性地指出,C++代码的执行速度要比C#(其中使用了较多的本机interop调用)快。
1) C#程序(使用P/Invoke)
[SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll")] static extern uint GetTickCount(); [SuppressUnmanagedCodeSecurity] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern uint GetWindowsDirectory( [Out] StringBuilder lpBuffer, uint uSize); static void Test(int x) { StringBuilder sb = new StringBuilder(512); for (int i = 0; i < x; i++) GetWindowsDirectory(sb, 511); } static void DoTest(int x) { uint init = GetTickCount(); Test(x); uint tot = GetTickCount() - init; Console.WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } static void Main(string[] args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console.ReadKey(true); } |
2) C++程序(使用C++ Interop)
void Test(int x) { TCHAR buff[512]; for(int i=0; i<x; i++) GetWindowsDirectory(buff, 511); } void DoTest(int x) { DWORD init = GetTickCount(); Test(x); DWORD tot = GetTickCount() - init; Console::WriteLine("Took {0} milli-seconds for {1} iterations",tot, x); } int main(array<System::String ^> ^args) { DoTest(50000);DoTest(500000);DoTest(1000000);DoTest(5000000); Console::ReadKey(true); return 0; } |
3) 速度比较
重复次数
|
C# 程序
|
C++程序
|
50,000
|
61
|
10
|
500,000
|
600
|
70
|
1,000,000
|
1162
|
140
|
5,000,000
|
6369
|
721 |
其性能差别真是令人惊愕!这的确是说明为什么要使用C++/CLI的一个好理由,如果你在使用本机interop进行开发,那么性能!完全由于性能,我
就将被迫借助本机interop来实现并非基于web的.NET应用程序。当然,为什么我想要使用.NET来开发需要大量本机interop技术的应用程
序完全是另外一个问题。
如果你仍怀疑这种性能优势,有另外的理由来说明你为什么不得不使用C++/CLI而不是C#或VB.NET——源码膨胀!下面是一个C++函数的例子,它使用了IP帮助者API来枚举一台机器上的网络适配器并且列出与每个适配器相联系的所有IP地址。
4) 枚举n/w适配器的C++代码
void ShowAdapInfo() { PIP_ADAPTER_INFO pAdapterInfo = NULL; ULONG OutBufLen = 0; //得到需要的缓冲区大小 if(GetAdaptersInfo(NULL,&OutBufLen)==ERROR_BUFFER_OVERFLOW) { int divisor = sizeof IP_ADAPTER_INFO; #if _MSC_VER >= 1400 if( sizeof time_t == 8 ) divisor -= 8; #endif pAdapterInfo = new IP_ADAPTER_INFO[OutBufLen/divisor]; //取得适配器信息 if( GetAdaptersInfo(pAdapterInfo, &OutBufLen) != ERROR_SUCCESS ) {//调用失败 } else { int index = 0; while(pAdapterInfo) { Console::WriteLine(gcnew String(pAdapterInfo->Description)); Console::WriteLine("IP Address list : "); PIP_ADDR_STRING pIpStr = &pAdapterInfo->IpAddressList; while(pIpStr) { Console::WriteLine(gcnew tring(pIpStr->IpAddress.String)); pIpStr = pIpStr->Next; } pAdapterInfo = pAdapterInfo->Next; Console::WriteLine(); } } delete[] pAdapterInfo; } } |
现在让我们看一个使用P/Invoke的C#版本。
5) 使用P/Invoke技术的C#版本
const int MAX_ADAPTER_NAME_LENGTH = 256; const int MAX_ADAPTER_DESCRIPTION_LENGTH = 128; const int MAX_ADAPTER_ADDRESS_LENGTH = 8; const int ERROR_BUFFER_OVERFLOW = 111; const int ERROR_SUCCESS = 0; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDRESS_STRING { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string Address; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADDR_STRING { public IntPtr Next; public IP_ADDRESS_STRING IpAddress; public IP_ADDRESS_STRING Mask; public Int32 Context; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct IP_ADAPTER_INFO { public IntPtr Next; public Int32 ComboIndex; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_NAME_LENGTH + 4)] public string AdapterName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_ADAPTER_DESCRIPTION_LENGTH + 4)] public string AdapterDescription; public UInt32 AddressLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_ADAPTER_ADDRESS_LENGTH)] public byte[] Address; public Int32 Index; public UInt32 Type; public UInt32 DhcpEnabled; public IntPtr CurrentIpAddress; public IP_ADDR_STRING IpAddressList; public IP_ADDR_STRING GatewayList; public IP_ADDR_STRING DhcpServer; public bool HaveWins; public IP_ADDR_STRING PrimaryWinsServer; public IP_ADDR_STRING SecondaryWinsServer; public Int32 LeaseObtained; public Int32 LeaseExpires; } [DllImport("iphlpapi.dll", CharSet = CharSet.Ansi)] public static extern int GetAdaptersInfo(IntPtr pAdapterInfo, ref int pBufOutLen); static void ShowAdapInfo() { int OutBufLen = 0; //得到需要的缓冲区大小 if( GetAdaptersInfo(IntPtr.Zero, ref OutBufLen) == ERROR_BUFFER_OVERFLOW ) { IntPtr pAdapterInfo = Marshal.AllocHGlobal(OutBufLen); //取得适配器信息 if( GetAdaptersInfo(pAdapterInfo, ref OutBufLen) != ERROR_SUCCESS ) { //调用失败了 } else{ while(pAdapterInfo != IntPtr.Zero) { IP_ADAPTER_INFO adapinfo = (IP_ADAPTER_INFO)Marshal.PtrToStructure( pAdapterInfo, typeof(IP_ADAPTER_INFO)); Console.WriteLine(adapinfo.AdapterDescription); Console.WriteLine("IP Address list : "); IP_ADDR_STRING pIpStr = adapinfo.IpAddressList; while (true){ Console.WriteLine(pIpStr.IpAddress.Address); IntPtr pNext = pIpStr.Next; if (pNext == IntPtr.Zero) break; pIpStr = (IP_ADDR_STRING)Marshal.PtrToStructure( pNext, typeof(IP_ADDR_STRING)); } pAdapterInfo = adapinfo.Next; Console.WriteLine(); } } Marshal.FreeHGlobal(pAdapterInfo); } } |
3. 栈语义和确定性的析构
C++经由栈语义模仿给了我们确定性的析构。简言之,栈语义是Dispose模式的良好的语法替代品。但是它在语义上比C# using块语法更直观些。请看下列的C#和C++代码段(都做一样的事情-连接两个文件的内容并把它写到第三个文件中)。
1) C#代码--使用块语义
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamReader tr1 = new StreamReader(file1)) { using (StreamReader tr2 = new StreamReader(file2)) { using (StreamWriter sw = new StreamWriter(outfile)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } } catch (Exception e) { Console.WriteLine(e.Message); } } |
2) C++代码--栈语义
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamReader tr1(file1); StreamReader tr2(file2); StreamWriter sw(outfile); while(str = tr1.ReadLine()) sw.WriteLine(str); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^ e) { Console::WriteLine(e->Message); } } |
C#代码与相等的C++
代码相比不仅免不了冗长,而且using块语法让程序员自己明确地指定他想在哪儿调用Dispose(using块的结束处),而使用C++/CLI的栈
语义,只需让编译器使用常规的范围规则来处理它即可。事实上,这使得在C#中修改代码比在C++中更乏味-作为一实例,让我们修改这些代码以便即使仅存在
一个输入文件也能创建输出文件。请看下面修改后的C#和C++代码。
3) 修改后的C#代码
public static void ConcatFilestoFile(String file1, String file2, String outfile) { String str; try{ using (StreamWriter sw = new StreamWriter(outfile)) { try{ using (StreamReader tr1 = new StreamReader(file1)) { while ((str = tr1.ReadLine()) != null) sw.WriteLine(str); } } catch (Exception) { } using (StreamReader tr2 = new StreamReader(file2)) { while ((str = tr2.ReadLine()) != null) sw.WriteLine(str); } } } catch (Exception e){ } } |
把针对StreamWriter的using块放到顶层需要重新调整using块结构--这在上面情况下显然不是个大问题,但是对于实际开发中的修改,这可能是相当模糊的且易导致逻辑错误的。
4) 修改后的C++代码
static void ConcatFilestoFile(String^ file1, String^ file2, String^ outfile) { String^ str; try{ StreamWriter sw(outfile); try{ StreamReader tr1(file1); while(str = tr1.ReadLine()) sw.WriteLine(str); } catch(Exception^){} StreamReader tr2(file2); while(str = tr2.ReadLine()) sw.WriteLine(str); } catch(Exception^){} } |
这样不是比在C#中的做更容易些吗?我恰好把StreamWriter声明移到了顶部并增加了一个额外的try块,就这些。甚至对于象在我的示例代码片
断中的琐碎事情,如果所涉及的复杂性在C++中大大减少,那么,当你工作于更大的工程时你能想象使用栈语义对你的编码效率千万的影响。
还不确信?好,让我们看一下成员对象和它们的析构吧。Imagine CLI
GC类R1和R2,二者都实现了Idisposable接口且都有函数F(),还有一个CLI
GC类R,它有R1和R2成员和一个函数F()-它内部地调用R1和R2上的F()成员函数。让我们先看C#实现。
5) 一个disposable类继承层次的C#实现
class R1 : IDisposable{ public void Dispose() { } public void F() { } } class R2 : IDisposable{ public void Dispose() { } public void F() { } } class R : IDisposable{ R1 m_r1 = new R1(); R2 m_r2 = new R2(); public void Dispose() { m_r1.Dispose(); m_r2.Dispose(); } public void F() { m_r1.F(); m_r2.F(); } public static void CallR() { using(R r = new R()) {r.F();} } } |
这里有几件事情要做:必须为每个disposable 类手工实现IDisposable接口,对于具有成员R1和R2的类R,Dispose方法也需要调用成员类上的Dispose。现在让我们分析上面几个类的C++实现。
6) 等价的C++实现
ref class R1 { public: ~R1(){} void F(){} }; ref class R2 { public: ~R2(){} void F(){} }; ref class R { R1 m_r1; R2 m_r2; public: ~R(){} void F() { m_r1.F(); m_r2.F(); } static void CallR() { R r; r.F(); } }; |
注意,这里不再有手工的Idisposable接口实现(我们的类中仅建立了析构器)而且最好的部分--类R的析构器(Dispose方法)并没有在该类可能含有的可释放的成员上调用Dispose-它没有必要这样做,编译器自动为之生成所有的代码!
4. 混合类型
我们知道,C++支持本机类型-总是如此;C++支持CLI类型-本文正是特别强调这一点;它还支持混合类型-具有CLI成员的本机类型和具有本机成员的CLI类型!请尽管考虑所有你能的可能需求。
注意,谈到Whidbey,混合类型实现还不完整;就我从Brandon,Herb和Ronald发表的材料的理解得知,存在这种相当酷的类型--统一
模型,它将在Orcas中实现--你能够在本机C++堆上new/delete
CLI类型,而且也能够在CLI堆上gcnew/delete本机类型。但既然这是Whidbey以后的东西,本文不讨论统一模型。
在我谈论你何时使用混合类型以前,我想向你说明什么是混合类型。如果你理解混合类型,请跳过下面几段。这里引用Brandon
Bray的说法:"一种混合类型,或者是本机类ref类(需要有对象成员),或者是通过声明或继承被分配在垃圾回收堆或本机堆上的。"因此如果你有一个托
管类型或者有一个有托管成员的本机类型,你就有了一个混合类型。VC++
Whidbey不直接支持混合类型(统一类型模型是一种Whidbey之后的概念),但是它给我们划定了实现混合类型的条件。让我们开始讨论包含托管成员
的本机类型。
ref class R { public: void F(){} //假定 non-trivial ctor/dtor R(){} ~R(){} }; |
在我的例子中,设想该托管类型R有一个non-trivial构造器和一个non-trivial析构器。
class Native { private: gcroot<R^> m_ref; public: Native(): m_ref(gcnew R()){} ~Native() { delete m_ref; } void DoF() { m_ref->F(); } }; |
既然,我不能在我的类中拥有一个R成员,我使用了gcroot模板类(在gcroot.h中声明,但是你要用"#include
vcclr.h"),它包装了System::Runtime::InteropServices::GCHandle结构。它是个象类一样的灵敏指针,
它重载了运算符->以返回用作模板参数的托管类型。因此在上面类中,我可以使用m_ref,就好象我已经声明它是R^,而且你能在DoF函数中看到
这正在起作用。实际上你可以节省delete,这可以通过使用auto_gcroot(类似于std::auto_ptr,在msclr\
auto_gcroot.h文件中声明)代替gcroot来实现。下面是一个更好些的使用auto_gcroot的实现。
class NativeEx { private: msclr::auto_gcroot<R^> m_ref; public: NativeEx() : m_ref(gcnew R()){} void DoF() { m_ref->F(); } }; |
下面让我们看相反的情形:一个CLI类的本机成员。
ref class Managed { private: Native* m_nat; public: Managed():m_nat(new Native()){ } ~Managed() { delete m_nat; } !Managed() { delete m_nat; #ifdef _DEBUG throw gcnew Exception("Oh, finalizer got called!"); #endif } void DoF() { m_nat->DoF(); } }; |
我不能定义一个Native对象来作为一个ref类成员,因此需要使用一个Native*对象来代替。我在构造器中new该Native对象,然后在析
构器和finalizer中delete它。如果你运行该工程的调试版,在执行到finalizer时将抛出一个异常-
因此开发者可以马上添加一个对delete的调用或为他的CLI类型使用栈语义技术。奇怪的是,库开发小组没有建立一个gcroot的反向实现-但这不是
个大问题,我们可以自己写。
template<typename T> ref class nativeroot { T* m_t; public: nativeroot():m_t(new T){} nativeroot(T* t):m_t(t){} T* operator->() { return m_t; } protected: ~nativeroot() { delete m_t; } !nativeroot() { delete m_t; #ifdef _DEBUG throw gcnew Exception("Uh oh, finalizer got called!"); #endif } }; |
这仅是个相当简单的灵敏指针实现,就象一个负责本机对象分配/回收的ref类。不管怎样,借助nativeroot模板类,我们可以如下修改托管类:
ref class ManagedEx { private: nativeroot<Native> m_nat; public: void DoF() { m_nat->DoF(); } }; |
好,关于混合类型的最大问题是什么呢?你可能问。最大问题是,现在你能混合使用你的MFC、ATL、WTL、STL代码仓库和.NET框架,并用可能的
最直接的方式-只需写你的混合模式代码并编译实现!你可以建立在一个DLL库中建立MFC
类,然后建立一个.NET应用程序来调用这个DLL,还需要把.NET类成员添加到你的MFC类(也实现可以相反的情况)。
作为一
例,设想你有一MFC对话框--它通过一个多行的编辑框接受来自用户的数据-现在,你有一新的要求-显示一个只读编辑框,它将显示当前在该多行编辑框中文
本的md5哈希结果。你的队友正在悲叹他们将必须花费几个小时钻研crypto
API,而你的上司在担忧你们可能必须要买一个第三方加密库;那正是你在他们面前树立形象的时候,你宣布你将在15分钟内做完这项任务。下面是解决的办
法:
添加一个新的编辑框到你的对话框资源中,并且添加相应的DDX变量。选择/clr编译模式并且添加下列代码到你的对话框的头文件中:
#include <msclr\auto_gcroot.h> using namespace System::Security::Cryptography; |
使用auto_gcroot模板来声明一个MD5CryptoServiceProvider成员:
protected: msclr::auto_gcroot<MD5CryptoServiceProvider^> md5; |
在OnInitDialog过程中,gcnew MD5CryptoServiceProvider成员。
md5 = gcnew MD5CryptoServiceProvider(); |
并且为多行编辑框添加一个EN_CHANGE处理器:
void CXxxxxxDlg::OnEnChangeEdit1() { using namespace System; CString str; m_mesgedit.GetWindowText(str); array<Byte>^ data = gcnew array<Byte>(str.GetLength()); for(int i=0; i<str.GetLength(); i++) data[i] = static_cast<Byte>(str[i]); array<Byte>^ hash = md5->ComputeHash(data); CString strhash; for each(Byte b in hash) { str.Format(_T("%2X "),b); strhash += str; } m_md5edit.SetWindowText(strhash); } |
这里使用了混合类型:一个本机Cdialog派生类,该类含有一个MD5CryptoServiceProvider成员(CLI类型)。你可以轻易地
试验相反的情况(如早期的代码片断已显示的)——可以建立一个Windows表单应用程序而且可能想利用一个本机类库--这不成问题,使用上面定义的模板
nativeroot即可。
5. 托管模板
也许你对泛型的概念已很清楚了,它帮助你避免进入C++的模板梦魇,它是实现模板的最佳方式,等等。好,假设这些全部正确,C++/CLI支持泛型就象
任何其它CLI语言一样-但是它有而其它一些CLI语言还没有的是它还支持托管模板-也就是模板化的ref和value类。如果你以前从未使用过模板,你
不能一下欣赏这么多优点,但是如果你有模板使用背景而且你已发现了泛型中存在的可能限制你编码的方式,托管模板将会大大减轻你的负担。你能联合使用泛型和
模板- 事实上有可能用一个托管类型的模板参数来实例化一个泛型类型(尽管相反的情形是不可能的,因为运行时刻实例化由泛型所用)。STL.NET
(或STL/CLR)以后讨论,请很好地利用泛型和托管模板的混合编程吧。
泛型使用的子类型约束机制将防止你写出下面的代码:
generic<typename T> T Add(T t1, T t2) { return t1 + t2; } |
编译错误:
error C2676: binary ’+’ : ’T’ does not define this operator or a conversion to a type acceptable to the predefined operator |
现在请看相应的模板版本:
template<typename T> T Add(T t1, T t2) { return t1 + t2; } |
那么就可以这样做:
int x1 = 10, x2 = 20; int xsum = Add<int>(x1, x2); |
还可以这样做:
ref class R { int x; public: R(int n):x(n){} R^ operator+(R^ r) { return gcnew R(x + r->x); } }; //... R^ r1 = gcnew R(10); R^ r2 = gcnew R(20); R^ rsum = Add<R^>(r1, r2); |
这在一个象int的本机类型以及一个ref类型(只要ref类型有一个+运算符)情况下都能工作良好。这个泛型缺点不是一个调试错误或缺陷-它是设计造
成的。泛型的实例化是在运行时通过调用配件集实现的,因此编译器不能确知一特定操作能被施行于一个泛型参数,除非它匹配一个子类型约束,因此编译器在定义
泛型时解决这个问题。当你使用泛型时的另外一个妨碍是,它不会允许你使用非类型参数。下列泛型类定义不会编译:
generic<typename T, int x> ref class G{}; |
编译错:
error C2978: syntax error : expected ’typename’ or ’class’; found type ’int’; non-type parameters are not supported in generics |
与托管模板相比较:
template<typename T, int x = 0> ref class R{}; |
如果你开始感激C++向你提供了泛型和托管模板,那么请看下面这一个例子:
template<typename T> ref class R{ public: void F() { Console::WriteLine("hey"); } }; template<> ref class R<int> { public: void F() { Console::WriteLine("int"); } }; |
你不能用泛型这样编码;否则,将产生:
编译错:error C2979: explicit specializations are not supported in generics
但可以在继承链中混合使用模板和泛型:
generic<typename T> ref class Base { public: void F1(T){} }; template<typename T> ref class Derived : Base<T> { public: void F2(T){} }; //... Derived<int> d; d.F1(10); d.F2(10); |
最后,你不能从一个泛型参数类型派生一个泛型类。
下列代码不会成功编译:
generic<typename T> ref class R : T {}; |
error C3234: a generic class may not derive from a generic type parameter
模板让你这样做(好像你还不知道这些):
ref class Base{ public: void F(){} }; generic<typename T> ref class R : T {}; //... R<Base> r1; r1.F(); |
这样,当你下次遇到对泛型的贬谤时,你就知道该怎么做了。
6. STL/CLR
当大量使用STL的C++开发者转向.NET1/1.1时一定感觉非常别扭,他们中的许多可能会放弃并转回到原来的本机编码。从技术上讲,你能结合.NET类型(using gcroot)使用本机STL,但是产生的结果代码可能相当低效,更不用说是丑陋了:
std::vector< gcroot<IntPtr> >* m_vec_hglobal; //... for each(gcroot<IntPtr> ptr in *m_vec_hglobal) { Marshal::FreeHGlobal(ptr);} |
大概VC++小组考虑到了这些并决定在Whidbey以后,他们会提供STL.NET(或STL/CLR)并可以单独从网上下载。
你可能问为什么?Stan Lippman,在他的MSDN文章(STL.NET Primer)中给出了3条原因:
·可扩展性--STL设计把算法和容器隔离到自己的应用空间-也就是你可以有一组容器和一组算法,并且你能在任何一个容器上使用这些算法;同时你能在任
何一个算法中使用这些容器。因此,如果你添加一种新的算法,你能在任何一种容器中使用它;同样,一个新的容器也可以与现有算法配合使用。
·统一性--所有核心C++开发者集中在一起,汇集起他们精妙的STL专长,再使用他们的专长则轻车熟路。要较好地使用STL需要花费时间-然而一旦你掌握了它,你就有了在.NET世界中使用你的技巧的明显优势。不是吗?
·性能--STL.NET通过使用实现泛型接口的托管模板实现。并且既然它的核心已用C++和托管模板编码,可以期盼它比在BCL上使用的泛型容器更具有性能优势。
使用过STL的人不需要任何示范,所以下面代码有益于以前没有使用过STL的人。
vector<String^> vecstr; vecstr.push_back("wally"); vecstr.push_back("nish"); vecstr.push_back("smitha"); vecstr.push_back("nivi"); deque<String^> deqstr; deqstr.push_back("wally"); deqstr.push_back("nish"); deqstr.push_back("smitha"); deqstr.push_back("nivi"); |
我使用了两个STL.NET容器-vector和deque,并装满两个容器,使其看起来相同(在两个容器中都使用了push_back)。现在,我将在两个容器上使用replace算法-我们再次看到,这些代码是很相同的。
replace(vecstr.begin(), vecstr.end(), gcnew String("nish"), gcnew String("jambo")); replace(deqstr.begin(), deqstr.end(), gcnew String("nish"), gcnew String("chris")); |
这里特别要注意的是我使用了"同样"的算法--replace并在两个不同STL容器上使用相同的函数调用。这是当Stan谈及"可扩展性"时的意思。下面我用一个简单函数来证明:
template<typename ForwardIterator> void Capitalize( ForwardIterator first,ForwardIterator end) { for(ForwardIterator it = first; it < end; it++) *it = (*it)->ToUpper(); } |
它遍历一个System::String^容器并把其中的每个字符串转化为大写。
Capitalize(vecstr.begin(), vecstr.end()); Capitalize(deqstr.begin(), deqstr.end()); for(vector<String^>::iterator it = vecstr.begin(); it < vecstr.end(); it++) Console::WriteLine(*it); Console::WriteLine(); for(deque<String^>::iterator it = deqstr.begin(); it < deqstr.end(); it++) Console::WriteLine(*it); |
上面我的算法能够与vector和deque容器工作良好。至此,不再细谈;否则,guru站上的STL爱好者们会对我群起攻击,而非STL人可能感到厌烦。如果你还没使用过STL,可以参考有关资料。
7. 熟悉的语法
开发者经常迷恋他们所用的编程语言,而很少是出于实用的目的。还记得当微软宣布不再为VB6提供官方支持时,VB6人的反抗吗?非VB6人对此可能非常
震惊,而老道的VB6人早已为他们的语言作好葬礼准备了。事实上,如果VB.NET从来没被发明,多数VB6人将会离开.NET,因为C#将会对他们非常
陌生,而它的祖先就是C++。如今,许多VB.NET人可能已经转向了C#,但是他们不会从VB6直接转向C#;VB.NET起到一个桥梁作用让他们的思
想脱离开原来VB6思想。相应地,如果微软仅发行VB.NET(而没有C#),那么.NET可能成为了新的面向对象VB,且带有一个更大的类库-C++社
团的人可能对此嗤之以鼻-他们甚至不会麻烦地检验.NET基础类库。为什么任何使用一种特定语言的开发者会对另外一个团体的使用另外开发语言的开发者嗤之
以鼻?这不是我要回答的问题。--要回答该问题也许要先回答为什么有的人喜欢威士忌,有的人喜欢可口可乐,而还有人喜欢牛奶。所有我要说的是,对开发者来
说,语法家族是个大问题。
你认为对于一个具有C++背景的人,下面的代码具有怎样的直觉性?
char[] arr =new char[128]; |
他/她可能回答的第一件事是,方括号放错了位置。下面这句又如何?
"呀!"-最可能的反映。现在把下面与前面相比较:
char natarr[128]; array<char>^ refarr=gcnew array<char>(128); int y=refarr->Length; |
请注意声明一个本机数组和一个托管数组时的语法区别。这里不同的模板形式的语法可视化地告诫开发者这一事实--refarr并不是典型的C++数组而且它可能是某种CLI类的派生物(事实上确是如此),所以极有可能可以把方法和属性应用于它。
C#的finalizer语法最有可能引起转向C#的C++程序员的混淆。请看见下列C#代码:
好,这样~R看起来象一个析构器但实际是个finalizer。为什么?请与下面的C++代码比较:
ref class R { ~R(){ } !R(){ } }; |
这里~R是析构器(实际上等价于一个析构器的Dispose模式-但对C++人员来说,这它的行为象个析构器)而新的!R语法是为finalizer建立的-这样就不再有混淆而且语法看上去也与本机C++相匹配。
请看一下C#泛型语法:
再请看一下C++的语法:
generic<typename T> ref class R{}; |
曾经使用过模板的人马上就看出这种C++语法,而C#语法不能保证其没有混淆性且也不直观。我的观点是,如果你以前具有C++背景,C++/CLI语法
将最贴近于你以前所用。C#(以及J#)看上去象C++,但是还有相当多的极为使人烦火的奇怪语义差别而且如果你没有完全放弃C++,语法差别将永远不停
地带给你混乱和挫折。从这种意义上说,我认为VB.NET更好些,至少它有自己唯一的语法,所以那些共用C++和VB.NET的人不会产生语法混乱。
8. 结论
最后,至于你用什么语言编程,这可能依赖许多因素——如:你在学校学习的是什么语言,你是用什么语言开发的现有代码仓库,是否你的客户对你有具体的语言
要求等。本文的主要目的是帮助你确定使用C++/CLI的几个明确的场所--这里,它比另外CLI语言更具有明显优势。如果你开发的应用程序有90%的使
用时间是涉及本机interop,为何还要考虑使用另外的而不是C++?如果你想开发一个通用集合,为什么仅把你自己限制在泛型上,而不是结合泛型和模板
的优势呢?如果你已经用C++工作,为什么还要学习一种新的语言?我常觉得象C#和VB.NET这样的语言总是尽量向开发者隐藏CLR,而C++不仅让你
品味CLR,甚至可以让你去亲吻CLR!
发布日期: 一, 13 八月 2007 05:30:18 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之传递托管堆地址
2005-08-18 18:05 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
新的C++特点平衡了把托管堆的地址传递到非托管代码的能力。早期我们遇到的最大问题是,在托管堆中的对象的位置是非静态的。垃圾收集器以变化的时间间隔移动对象。现在新的pin_ptr(别针型指针)的引入禁止垃圾收集器改变在堆上的对象的地址。
下面代码展示了别针型指针的应用。
#pragma unmanaged //本机函数,以整型指针作参数,执行计算 void calc(int* val) { //执行计算操作 } #pragma managed //托管函数调用本机calc函数 int managedfunc() { int i=gcnew int(10); //把i的地址设置为别针型,以阻止对i的移动 pin_ptr<int> ppi=&i; int* np=ppi; calc(np);//用别针型int的地址调用本机函数 //把别针型指针的值置为nullptr,脱离了gc的限制 ppi=nullptr; //完成剩下的工作 return i; } |
基于轮廓的优化(Profile Guided Optimization)
在新版本的托管C++中有一项新增功能称为基于轮廓的优化。这一新的编译特点能够实现在编译时刻把探针注入到代码中。最后的exe文件与一个数据库一起
打包发送,由该库记录下注入代码中的探针监测到的结果数据。当用户运行该程序时,这些探针记录下应用程序的使用。当你下一次再编译时,编译器作出智能性决
策,如根据探针的记录作为相应的动作。这一特性带来了真实世界的优化。
为什么没有为使用托管类型而定义头文件?
在传统的C++中,一个头文件一般包含对象的接口,或者类与函数的声明,等等。这允许在多个翻译单元上实现某种类型的一致性声明。对于一个用托管代码编
写的且其被编译成MSIL的对象,需要在配件集中包含描述对象的元数据。因而,托管C++编译器不是通过头文件,而是通过使用配件集元数据来实现接口的读
取。这就是为什么你不必因使用托管类型而包含头文件。事实上,你可以通过使用#using指令来让编译器从配件集中读取元数据。
发布日期: 一, 13 八月 2007 03:39:32 GMT
计算机与 Internet |
阅读完整项
interior_ptr<type>
是native pointer的超集,native pointer能做的,Interior pointer也一样能做。当垃圾回收器移动对象时,Interior pointer能随之移动,并始终指向该对象。
------《Pro Visual C++_CLI and the .NET 2.0 Platform》p815
Interior_ptr可以指向引用句柄、值类型、装箱类型句柄、托管类成员、托管数组的元素。不能指向引用类型本身。
*ip 是ip所值的类型type的值
&ip 是ip指向托管堆中的地址
pin_ptr<type>
在外部调用托管堆中指针时,垃圾回收过程中该指针会发生改变,引起外部调用的错误。必须使用pin_ptr指针将该指针固定。
pinned
pointer可以指向引用句柄、值类型、托管数组的元素。不能指向引用类型,但能指向引用类型的成员(不支持钉住由 new
表达式返回的整个对象。相反,需要钉住内部成员的地址。--MSDN)。可以完成native pointer的所有的功能,如指针比较和指针运算。
都不能由跟踪句柄直接赋值。而是必须由&操作符取地址。因为interior_ptr pin_ptr是独立的类,只能由地址赋值,而不能由托管类型赋值。
int^ m_int = gcnew int(100);
interior_ptr<int> ipint = &*m_int; //ok
interior_ptr<int> ipint = m_int; //error
Value class Vtest{}
Ref class Rtest{}
Vtest ^vtest = gcnew Vtest; //值类型对象
pin_ptr< Vtest > pinp;
pinp = vtest; //error
pinp = &* vtest; //ok
interior_ptr< Vtest > ip;
ip = &* vtest; //ok
pin_ptr< Vtest ^> pinhundle;
pinhundle = vtest; //error
pinhundle = & vtest; //ok
interior_ptr< Vtest ^> iphundle;
iphundle = & vtest; //ok
RTest ^rtest = gcnew RTest; //引用类型对象
pin_ptr< Rtest > pinp; //error , 不可以钉住引用类型对象
interior_ptr < Rtest > pinp; //error , 不可以指向引用类型对象
ref class G {
public:
void incr(int* pi) { *pi += 1; }
};
ref struct H { int j; };
void f( G^ g ) {
H ^ph = gcnew H;
Console::WriteLine(ph->j);
pin_ptr<int> pj = &ph->j; //ok,可以钉住引用类型对象的成员
g->incr( pj );
Console::WriteLine(ph->j);
}
Pin_ptr指针使用例子
value class Test
{
public:
int i;
};
#pragma unmanaged
void incr (int *i)
{
(*i) += 10;
}
#pragma managed
void main ()
{
Test ^test = gcnew Test();
interior_ptr<int> ip = &test->i;
(*ip) = 5;
// incr( ip ); // invalid
pin_ptr<int> i = ip; // i is a pinned int pointer
incr( i ); // pinned pointer to interior pointer passed to a
// native function call expecting a native pointer
Console::WriteLine ( test->i );
}
非托管代码中的函数void incr(int *i)需要int指针,不能将
interior_ptr<int>的ip传递给该函数。因为内部指针会随着垃圾回收改变地址,而将其转换为pin_ptr<int>,就是将地址固定,以供外部函数incr(int* i)调用该指针。
和*指针的转换
正因为pin_ptr<>所指向的地址是固定的,因此才能和*指针转换。而interior_ptr<>无此性质。
pin_ptr<ClassValue> pValue = &value;;
ClassValue* p = pValue; //ok
interior_ptr就不可以和*指针转换。
interior_ptr<ClassValue> pValue = &value;;
ClassValue* p = pValue; //error
值类型的指针
在新语法中,值类型指针分为两种类型:V*(限于非 CLR 堆位置)和内部指针 interior_ptr<V>(允许但不要求托管堆内的地址)。
// may not address within managed heap
V *pv = 0;
// may or may not address within managed heap
interior_ptr<V> pvgc = nullptr;
托管扩展中的下列声明全部映射到新语法中的内部指针。(它们是 System命名空间内的值类型。)
Int32 *pi; // => interior_ptr<Int32> pi;
Boolean *pb; // => interior_ptr<Boolean> pb;
E *pe; // => interior_ptr<E> pe; // Enumeration
内置类型不被认为是托管类型,尽管它们确实在System命名空间内作为类型的别名。
发布日期: 一, 13 八月 2007 02:21:40 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之再谈继承
2005-08-17 17:49 作者: 朱先忠 出处: 天极网 责任编辑:
方舟
在面向对象编程领域一个关键的概念是继承。
在OO和C++中,类成员共有三种级别的继承:public,protected和private。对于基类成员的存取操作应该依赖于下面两个因素:
·派生类声明其类头(public, protected 或 private)的方式
·对类成员的存取指定标志(public, protected 或 private)
如果你在基类中声明成员为private,那么这些成员对于任何派生类都是不可存取的。
如果你在基类中声明成员为protected,而且新类是基类的私有派生,那么这些成员在派生类中变为私有的。
如果你在基类中声明成员为protected,而且新类是基类的protected 型派生,那么这些成员在派生类中变为protected 的。
如果你在基类中声明成员为protected,而且新类是基类的public 型派生,那么这些成员在派生类中变为protected的。
如果你在基类中声明成员为public,而且新类是基类的private 型派生,那么这些成员在派生类中变为private 的。
如果你在基类中声明成员为public,而且新类是基类的protected 型派生,那么这些成员在派生类中变为protected的。
如果你在基类中声明成员为public,而且新类是基类的public 型派生,那么这些成员在派生类中变为public 的。
这是一些老式的C++规则。在.Net中,情况就大大不同了-.Net仅支持public继承。但是当一个类被public继承,则基类的保护成员将对新的派生类成为private的。
考虑下面的三个C++类:
class A { protected: void fa() { printf("fa called"); } }; class B : public A { public: void fb() { fa(); } }; class C : public B { public: void fc() { fa(); } }; int main() { C c; c.fc(); return 0; } |
根据我前面介绍的规则,在C++中这些代码是能成功运行的。但是如果你把它们改变为托管类,则在类c中是不能调用fa()的。
发布日期: 五, 10 八月 2007 08:20:49 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之内部指针的两面性
2005-08-10 09:04 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
在本文中,我将解释为什么使用本地指针来管理对象。原因在于对象是以垃圾收集器移动的。所以,当对象移动时,本地指针就变成无效的。所以,如果你想取得一个托管对象的本地指针,你就会遇到一个编译错。下面的代码显示了这上点。
using namespace System; int _tmain() { int ^ hnd = gcnew int(100); int* np = &hnd; // Genarates a compile error } |
但是本地指针还是非常有用的,如在使用指针算术和指针比较时就有许多的优点。所以新的C++
CLI允许你使用一个内部指针实现同样的功能。内部指针是本地指针的一个超集。所以它能够做任何内部指针所能做的一切。
但是当垃圾收集器移动指针所指向的对象时,内部指针也帮助程序实现其指向的地址的更新。
现在让我们看一下如何用内部指针来实现指针算术运算。
#include "stdafx.h" #include <stdio.h> using namespace System; using namespace stdcli::language; int _tmain() { const int SIZE = 10; array<int>^ arr = gcnew array<int>(SIZE); for(int idx = 0; idx < SIZE; idx++) { arr[idx] = idx + 1; } // 取得指向数组第一个元素的指针 interior_ptr p = &arr[0]; // 通过增加指针值读取并输出数组元素 for(int idx = 0; idx < 10; idx++) { printf("Value of the element at %Xh ", p); Console::WriteLine(" is {0}",*(p++)); } Console::ReadLine(); } |
不管垃圾收集器执行多少次和数组元素移动多少次,上面的代码仍然能工作良好。于是C++的力量又回到了.Net运行时刻库上。但是要小心使用内部指针。
这些指针与本地指针非常相似,当试图存取不允许操作的内存区段时能给程序造成危害。例如,如果我在上面的代码中试图存取下一个p++,它将返回恰好在上面
数组元素上方4字节的内存段位置
发布日期: 五, 10 八月 2007 07:51:18 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之代理构造函数
2005-07-28 09:54 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
现在我们讨论一下新的C++/CLI环境下的一个很酷的特性,称作代理构造函数。
对一个类来说,有多个构造函数是经常的事;并且这多个构造函数有一段共同的代码也很经常。一般地,在这种情况下,我们都是为该共同代码段编写一个独立的函数,然后放在每个构造器中调用。如下例:
class Foo { private: int _mem; public: Foo() : _mem(0) { CommonConstructor(); } Foo(int mem) : _mem(mem) { CommonConstructor(); } Foo(const Foo& f) : _mem(f._mem) { CommonConstructor(); } // 我们所有的构造器都需要的代码段 void CommonConstructor() { printf("Constructing the object"); } }; |
但是现在利用C++/CLI引入的新特性-代理构造器,我们可以从一个称为基类构造器的构造器中调用另一个构造器。当你这样做时,执行控制转入到第二个构造器中,在其执行完后再返回到第一个构造器。下面代码片断中的类Foo2演示了这种方法:
class Foo2 { private: int _mem; public: // 该构造器调用第二个称为基类构造器的构造器 Foo2() : Foo2(0) { } // 下面这个构造器包含由所有构造器使用的公共代码 Foo2(int mem) : _mem(mem) { printf("Constructing the object"); } Foo2(const Foo2& f) : Foo2(f._mem) { } }; |
然而,每个类都应该包含至少一个非代理构造器,不过该构造器仍然可以有调用一个或者多个基类构造器的初始化操作。
注意,早期情况下,如果你想试用一下代理构造函数,应该会出现一个编译错误。请注意,这是由于资源问题缺乏导致的,在现在版本的C++/CLI中可以避免这一错误。
汗一个……没看懂!
发布日期: 五, 10 八月 2007 05:05:41 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之拷贝构造函数
2005-07-28 09:39 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
虽然对象复制看上去很简单,然而如果你没有对其正确理解,可能会出现一些严重问题。默认情况下,复制对象会导致相应的所有成员的复制。如果你只有实例成员,这看上去是相当不错的。但是如果你的类中含有指向在堆中分配的对象时,情况会怎样呢?考虑下面的代码片断:
#include <stdio.h> #include <string.h> class Person { private: char* _name; public: Person() { _name = new char[256]; } void SetName(const char* name) { if(strlen(name) + 1 < 256) strcpy(_name,name); } void PrintName() { printf("%s\n",_name); } }; int main() { // 创建对象的第一个实例并赋于名字为John Person p1; p1.SetName("John"); p1.PrintName(); //通过复制p1引用的对象创建另一个对象 Person p2(p1); p2.SetName("Alice"); p2.PrintName(); //现在再输出p1的名字 p1.PrintName(); scanf("q"); return 0; } |
这里的类Person有一个指向在堆上分配的字符数组的指针。当构造Person对象时,它创建该字符数组并把它的位置存放到变量_name中。
但是当你创建Person 对象 p2 时,p2的成员用p1的成员初始化。因而,p1的 _name与p2的
_name指向相同的堆对象。如在上例中看到的,调用p2.SetName将改变由这两个类共享的值。所以,当第二次调用p1.PrintName,打印
结果是"Alice"。
所以,这不是我们复制对象所期望的结果,而且还会导致堆崩溃的问题。请再考虑某个函数删除了该数组而p1又要调用该函数的情况?下面,当p2调用PrintName时,它将尽量存取实际上不是在堆上的对象。这种情况下产生的结果往往是难以预料的。
C++允许我们通过定义拷贝构造函数来克服这类问题。在我们每次通过复制另一个对象来初始化一个对象时,拷贝构造函数都被执行。你可以在拷贝构造函数中覆盖掉缺省的成员函数的复制行为。
所以,我们的类Person应该修改如下:
class Person { private: char* _name; public: Person() { _name = new char[256]; } // 这是拷贝构造函数。在此我们初始化一个新的数组,为Person的实例所用 Person(Person&) { _name = new char[256]; } void SetName(const char* name) { if(strlen(name) + 1 < 256) strcpy(_name,name); } void PrintName() { printf("%s\n",_name); } }; |
这里类Person中的拷贝构造函数保证了它初始化一个新的数组,为在复制时产生的每一个对象实例所用。这就避免了前面我们提到的问题。
希望上面所述能够帮助读者理解拷贝构造函数及其使用场所。
这些东东纯属C++的东西嘛,和CLI一点关系都没有...
发布日期: 五, 10 八月 2007 03:55:02 GMT
计算机与 Internet |
阅读完整项
浅议C++/CLI的gcnew关键字
2005-07-08 08:19 作者: LEAK 出处: C++/CLI社区 责任编辑:
方舟
C++/CLI中使用gcnew关键字表示在托管堆上分配内存,并且为了与以前的指针区分,用^来替换* ,就语义上来说他们的区别大致如下:
1. gcnew返回的是一个句柄(Handle),而new返回的是实际的内存地址.
2. gcnew创建的对象由虚拟机托管,而new创建的对象必须自己来管理和释放.
当然,从程序员的角度来说,管它是句柄还是什么其他的东西,总跑不掉是对某块内存地址的引用,实际上我们都可以理解成指针.下面我们就写一段代码来测试一下好了.
using namespace System;
ref class Foo
{
public:
Foo()
{
System::Console::WriteLine("Foo::Foo");
}
~Foo()
{
System::Console::WriteLine("Foo::~Foo");
}
public:
int m_iValue;
};
int _tmain()
{
int* pInt = new int;
int^ rInt = gcnew int;
Foo^ rFoo = gcnew Foo;
delete rFoo;
delete rInt;
delete pInt;
}
|
我把调试的时候JIT编译的汇编代码择录了部分如下显示(请注意红色部分):
int* pInt = new int;
0000004c mov ecx,4
00000051 call dword ptr ds:[03B51554h]
00000057 mov esi,eax
00000059 mov dword ptr [esp+18h],esi
int^ rInt = gcnew int;
0000005d mov ecx,788EF9D8h
00000062 call FCFAF66C
00000067 mov esi,eax
00000069 mov dword ptr [esi+4],0
00000070 mov edi,esi
Foo^ rFoo = gcnew Foo;
00000072 mov ecx,3B51768h
00000077 call FCFAF66C
0000007c mov esi,eax
0000007e mov ecx,esi
00000080 call dword ptr ds:[03B517ACh]
00000086 mov dword ptr [esp+1Ch],esi
delete rFoo;
0000008a mov ebx,dword ptr [esp+1Ch]
0000008e test ebx,ebx
00000090 je 000000A4
00000092 mov ecx,ebx
00000094 call dword ptr ds:[03FD0028h]
0000009a mov dword ptr [esp+14h],0
000000a2 jmp 000000AC
000000a4 mov dword ptr [esp+14h],0
delete rInt;
000000ac mov edx,edi
000000ae mov ecx,788F747Ch
000000b3 call FC8D20FD
000000b8 mov ebp,eax
000000ba test ebp,ebp
000000bc je 000000D0
000000be mov ecx,ebp
000000c0 call dword ptr ds:[03FD0020h]
000000c6 mov dword ptr [esp+10h],0
000000ce jmp 000000D8
000000d0 mov dword ptr [esp+10h],0
delete pInt;
000000d8 mov ecx,dword ptr [esp+18h]
000000dc call dword ptr ds:[03B51540h]
|
我们先看分配内存这部分的代码
1.调用new方式分配
int* pInt = new int;
0000004c mov ecx,4
00000051 call dword ptr ds:[03B51554h]
|
可以看到,和以前在vc6中一样,分配内存的步骤如下:
1. 首先把sizeof(int) = 4 放到ecx中
2. 调用operator new 去分配4个字节
3. 调用构造函数等等......(这里不是我们的重点)
成功分配后,会把返回地址放在eax中。
2.调用gcnew方式分配
int^ rInt = gcnew int;
0000005d mov ecx,788EF9D8h
00000062 call FCFAF66C
。。。
Foo^ rFoo = gcnew Foo;
00000072 mov ecx,3B51768h
00000077 call FCFAF66C
|
可以看到gcnew也是通过把一个参数放到ecx中,然后再调用一个函数来完成分配的操作,显然0x788EF9D8应该是一个地址,而不可能是一个数
值。我们可以看到这里gcnew创建两个不同类型的变量,调用的函数地址却都是0xFCFAF66C,而存放到ecx中的两个地址就不一样。究竟这几个地
址代表什么呢?
和new一样gcnew也是把返回地址放在eax中。我们直接从内存窗口看eax指向的内存块好了。Aha,看到了没有?
这次的eax = 0x00F73404 对应的内存块为
0x00F73404 d8 f9 8e 78 00 00 00 00 。。。
|
这个不就是 mov 到 ecx中的值么?再回忆昨天写的分析Object对象布局的文章,可以肯定这个就是
MethodTable地址了,对于这个int来说,后面的4个字节对应的就是存放它的RawData,比如如果你初始化为 4
那么内存对应的就变化为 d8 f9 8e 79 04 00 00 00
分析清楚存放到ecx中的是
MethodTable指针,我们再分析那个对应的call函数,从vm的代码可以看出,有三个全局函数用来根据MethodTable创建对象,同时
MethodTable本身也提供一个成员函数Allocate(),只不过这个成员函数也是调用的下面的函数:
OBJECTREF AllocateObject( MethodTable *pMT )
OBJECTREF AllocateObjectSpecial( MethodTable *pMT )
OBJECTREF FastAllocateObject( MethodTable *pMT )
其中AllocateObject又是调用AllocateObjectSpecial来完成工作。那么我们调用的应该就是AllocateObject或者FastAllocateObject了。
在我们的例子里面两个call的地址都一样,但是你如果写下代码 double ^ pDouble = gcnew double;这个时候的地址是多少?它和int 的一样么?
目前我还没有仔细去研究这个地址到底对应的是该类型的MethodTable::Allocate()或是上面的这三个全局函数,如果对应
MethodTable::Allocate(),那么2.0中应该有个MethodTable::FastAllocate()吧,否则应该就是对应的
全局函数AllocateObject 以及FastAllocateObject了。过几天一定要抽空再好好研究一下。
下面看对应的delete函数。
delete pInt;
000000d8 mov ecx,dword ptr [esp+18h]
000000dc call dword ptr ds:[03B51540h]
比较简单,就是传入地址,然后调用operator delete来释放类存,会调用析构函数
|
对应的,释放gcnew创建的对象的代码如下:
delete rInt;
000000ac mov edx,edi
000000ae mov ecx,788F747Ch
000000b3 call FC8D20FD
|
这个也相对简单,它对应vm里面的一个函数:
void CallFinalizer(Thread* FinalizerThread, Object* fobj)
那么也就是
fobjà edx
FinalizerThread à ecx
Call CallFinalizer
但是,请注意!!!!!!!一个类包含析构函数和不包含析构函数,它对应的delete代码是不一样的,这点可以通过汇编代码比较得到,我这里就不多说了。
发布日期: 五, 10 八月 2007 02:41:52 GMT
计算机与 Internet |
阅读完整项
C++/CLI思辨录之Object的对象布局
2005-07-28 08:35 作者: 朱先忠编译 出处: 天极网 责任编辑:
方舟
C++/CLI相对纯C++来说,支持创建托管引用对象,托管对象由虚拟机来分配内存和管理,程序员可以不再担心内存泄漏的问题。其实,说白了也就是相当于自己创建一个内存池,并且虚拟机实际上也是这样做的。
在CLI中,所有的类都从Object派生,包括int这样的值。那么Object的内部结构是怎么样的呢?通过对vm代码的研究,可以看到大致上的结构如下:
用户保存一个托管对象的句柄,其实可以看作是一个指向Object的指针,在Object里面包含一个MethodTable的指针,这个
MethodTable保存了类型的信息以及一些函数,这就好比虚函数指针一样。很多的操作都通过该指针来完成,比如Allocate(),比如Box
(),UnBox()等等。紧跟在MethodTable后面的就是真实的数据了。
这个MethodTable是vm里面很核心的一个类,通过它可以完成很多的操作。
原文地址:http://dev.yesky.com/msdn/28/2048528.shtml
这篇文章似乎没有完
发布日期: 五, 10 八月 2007 02:18:11 GMT
计算机与 Internet |
阅读完整项
C++/CLI中栈对象的设计问题
2005-06-03 11:45 作者: jzli 出处: 博客堂 责任编辑:
方舟
C++/
CLI中新推出的自动确定性资源回收(
Automatic deterministic destruction)被视为一个优秀的设计。是使用所谓C++/
CLI这个“新瓶”来装Bjarne Stroustrup提出的RAII这个“旧酒”。
这的确不错,相对而言,这个比
C#中的using
关键字(dispose模式),以及Java中的hard-coded的dispose方法都要好许多。这个特性是由C++/CLI中栈对象(局部对象)来提供的,局部对象本身没错,RAII也是局部对象应有之义。
但问题在于C++/CLI中栈对象的可用性由于许多原因会大打折扣,使用起来已经远远不如ISO-C++中那样流畅。下面列出了损伤其可用性的几大硬伤:
#1、C++/CLI的栈对象并非真的位于栈中
只要类型是ref class,C++/CLI中的栈对象就仍位于
托管堆中。仍然使用newobj
IL指令来分配。如果R没有定义析构器(~R)(注意:C++/CLI中的析构器和
C#中的析构器完全两回事),那么下面两行代码实际上将生成完全一样的IL代码:
R r;
R h=gcnew R;
好像记得Herb Sutter曾经说过他们将来可能会在真正的方法栈中分配r ——说实话恐怕只有C++背景的人敢这么“胡思乱想”:) 他们现在只是想在语法层面让程序员"感觉"就像r是从栈中分配的一样。又一个syntax sugar:)
当然为了对称和语义的完美,有时候还需要在r上应用%——虽然背后仍是什么也没做:)
#2、C++/CLI编译器默认情况下不会自动产生拷贝构造函数和拷贝赋值操作符
这一点非常令人烦恼,几乎让人“望栈对象而却步”。更糟糕的是BCL中的所有类型都没有提供拷贝构造函数和拷贝赋值操作符——因为恐怕只有C++/CLI会用到他们。
话说回来,即使C++/CLI会自动产生拷贝构造函数和拷贝赋值操作符,那么继承自BCL的类型还是会很麻烦。
#3、如果函数要被其他CLI语言调用,那么就不能将其参数设计为栈对象
a. static void add(R r){...}
编译出来有一个modopt元数据,所以可以被其他语言调用,但是如果被其他语言调用,比如C#,那么其他语言将是以传值的方式传递引用,而C++/CLI将是传递对象拷贝(要调用拷贝构造器),所以语义混乱,完全不可以这样做。
b. static void add(R% r){...}
由于编译出来都有一个modreq元数据,所以不能被其他CLI语言调用。
#4、如果函数要被其他CLI语言调用,那么也不能将其返回值设计为栈对象
a. static R add(){...}
b. static R% add(){...}
两者编译出来都有一个modreq元数据,所以都不能被其他CLI语言调用。
#5。使用BCL时,如果要传递栈对象,总要使用“莫名其妙”的%操作符
比如:
String s("abc");
ArrayList list;
list.Add(%s);
实在很不好,还是使用追踪引用比较好:
String^ s="abc";
ArrayList^ list=gcnew ArrayList();
list->Add(s);
总结一下:
#1和#5对栈对象的可用性影响不算大,毕竟从语义层面来理解,还是行得通的。
但是,#2、#3、#4的影响就很大。#3和#4使得我们必须放弃使用栈对象来进行互操作。而#2会让编写C++/CLI代码非常的不方便——除非你以后不想使用栈对象。
现在的问题是,是否C++/CLI中的栈对象只是为了获得自动确定性资源回收而存在?值得这样做吗?
原文地址:http://dev.yesky.com/msdn/29/2011029.shtml
发布日期: 四, 09 八月 2007 07:51:40 GMT
计算机与 Internet |
阅读完整项
C++/CLI基本数据类型探索
2005-05-24 09:58 作者: 李建忠 出处: CSDNBLOG 责任编辑:
方舟
导读:本文向大家揭示了在将CLI类型系统和ISO-C++语义框架集成在一起的时候,微软做了哪些调整工作,以及如何在必要的时候调整在集成过程中所出现的各个情况的优先级。同时,这也提醒大家在将一个本地类型重新构造为一个CLI类的过程中需要注意的问题。
C++/CLI所支持的基本类型,例如int、double、bool等,在某些方面可以说是沿袭了ISO-C++中的类型——同样的用法会在C++/CLI中得到同样的结果,例如加法或者赋值操作。但是C++/CLI也为这些基本类型引入了一些新的东西。
在通用类型系统(CTS)中,每一个基本类型都在System命名空间中存在一个对应的类(见表1)。例如int实际上完全等价于System::Int32。我们可以使用二者中的任何一个来声明一个整数:
int ival = 0; Int32 ival2 = 0; |
出于移植性的考虑,在使用这些基本类型时,我们推荐大家使用内建的关键词,而非System命名空间中的类名。
基本类型
|
System命名空间中对应的类
|
注释/用法
|
bool
|
System::Boolean
|
bool dirty = false;
|
char
|
System::SByte
|
char sp = ' ';
|
signed char
|
System::SByte
|
signed char ch = -1;
|
unsigned char
|
System::Byte
|
unsigned char ch = '\0';
|
wchar_t
|
System::Char
|
wchar_t wch = ch;
|
short
|
System::Int16
|
short s = ch;
|
unsigned short
|
System::UInt16
|
unsigned short s = 0xffff;
|
int
|
System::Int32
|
int ival = s;
|
unsigned int
|
System::UInt32
|
unsigned int ui = 0xffffffff;
|
long
|
System::Int32
|
long lval = ival;
|
unsigned long
|
System::UInt32
|
unsigned long ul = ui;
|
long long
|
System::Int64
|
long long etime = ui;
|
unsigned long long
|
System::UInt64
|
unsigned long long mtime = etime;
|
float
|
System::Single
|
float f = 3.14f;
|
double
|
System::Double
|
double d = 3.14159;
|
long double
|
System::Double
|
long double d = 3.14159L; |
表1 基本类型和它们在System命名空间中对应的类
对于System命名空间中类的公有静态成员,我们既可以通过内建的关键字,也可以通过System命名空间中的类名来访问。例如,为了获取一个数值类型的取值范围,我们可以直接使用内建的关键字来访问其静态属性MaxValue和MinValue。
int imaxval = int::MaxValue; int iminval = Int32::MinValue; |
每个数值类型都支持一个名为Parse的成员函数,用以将一个字符串转化为其所表示的数值。例如,给定下面的字符串:
String^ bonus = "$ 12,000.79"; |
调用Parse会将myBonus初始化为12000.79:
double myBonus = double::Parse( bonus, ns ); |
其中ns表示对一些NumberStyles枚举类型取位或(bitwise
or)运算的结果。NumberStyles是位于System::Globalization命名空间中的一个枚举类型,用于表征对空白、货币符号、小
数点或者逗号等的处理。看下面的代码:
using namespace System; using namespace System::Globalization;
double bonusString( String^ bonus ) { NumberStyles ns = NumberStyles::AllowLeadingWhite; ns |= NumberStyles::AllowCurrencySymbol; ns |= NumberStyles::AllowThousands; ns |= NumberStyles::AllowDecimalPoint;
return double::Parse( bonus, ns );
} |
我们也可以使用转型符号来在类型间进行显式的转换。
int ival = ( int ) myBonus; |
或者使用System::Convert类的一些转换方法,例如ToDouble(), ToInt32(), ToDateTime()等:
int ival2 = Convert::ToInt32( myBonus ); |
两种转换方法采用的策略有所不同:显式转型会直接对小数部分进行截断,而Convert的成员函数则采用的是舍入算法。例如上面的例子中ival赋值后的结果为12000,而ival2赋值后的结果为12001。
我们还可以直接使用字面常量(literal)来调用其对应类型的成员函数,虽然这乍看起来有些怪异。例如,我们可以编写如下代码:
Console::Write( "{0} : ", ( 5 ).ToString() ); |
其中( 5
).ToString()返回的是字面常量整数5的字符串表示。注意5外面的圆括号是必须的,因为它会使得编译器将后面的成员选择操作符点号绑定到整数5
上,而不是将'5.'解析为一个double类型的字面常量——那样的话,后面的ToString()将变得不合法。为什么我们有时候需要这样做呢?一种
可能的情况是将一个字符串传递给Console的成员函数要比传递实际的数值来的更加高效。
对于字符以及字符串这样的字面常量,我们也可以像上面的整数一样调用它们的成员函数,但是它们的行为有一点点晦涩。例如,下面的代码:
Console::WriteLine(( 'a' ).ToString() ); |
将在控制台上打印出97,而非'a'这个字符。要将字符'a'打印出来,我们需要将其首先转型为System::Char:
Console::WriteLine(((wchar_t)'a').ToString() ); |
C++/CLI对字符串字面常量采取了特殊的处理策略。从某种程度上来讲,字符串字面常量在C++/CLI中的类型更接近System::String,而非C风格的字符串指针。显然,这将对重载函数的辨析产生影响。例如:
public ref class R { public: void foo( System::String^ ); // (1) void foo( std::string ); // (2) void foo( const char* ); // (3) };
void bar( R^ r ) { // 调用哪一个foo呢? r->foo( "Pooh" ); } |
在ISO-C++中,这将被辨析为第3个foo(),因为字符串字面常量更接近const
char*,而非ISO-C++标准库中的string类型。但是,在C++/CLI中,上面的调用将被辨析为第1个foo(),因为现在字符串字面常量
被认为更接近System::String,而非字符指针。要理解其中的缘由,让我们往后退两步,先来看看ISO-C++和C++/CLI如何辨析一个重
载函数,然后再来看ISO-C++和C++/CLI如何辨析一个字符串字面常量。
一个重载函数的辨析过程通常包含以下三个步骤:
1.选择候选函数集合。候选函数是指那些从词法范畴来看与所调用函数名相匹配的函数。例如,由于我们上面是在R的一个实例上调用foo(),所以所有名
称为foo但却不是R或者其基类的成员的那些函数将不被认为是候选函数。这样看来,我们现在有三个候选函数,即R中三个名称为foo的成员函数。如果这个
阶段得到的候选函数集合为空,那么调用将告失败。
2.从候选函数集合中选择可用函数集合。可用函数是指函数声明时的参数个数和它们的类型与调用时所指定的相匹配的那些函数。在我们上面的例子中,三个候选函数都是可用函数。如果这个阶段得到的可用函数集合为空,那么调用也将失败。
3.从可用函数集合中选择最匹配的函数。这个阶段将会对实际传递的参数和可用函数所声明的参数之间的转换进行一个排名。对于只含一个参数的函数来说,这
个过程比较简单。但是对于含有多个参数的函数来说,这个过程就变得相对有些复杂。如果没有一个最佳的匹配函数胜出,那么调用将告失败。也就是说各个可用函
数的参数类型到实际参数类型之间的转换被认为一样的好,换言之多个调用之间产生了混淆。
那么现在摆在我们面前有两个问题:(1)我们实际传递的参数"Pooh"到底是什么类型?(2)在判定类型转换的优劣时采用的是什么算法?
在ISO-C++中,字符串字面常量"Pooh"的类型为const
char[5]——注意,在字符串字面常量后面有一个隐含的截断字符null。在上面的例子中显然不存在这样的精确匹配,因此必须应用某种形式的类型转
换。这样,两个ISO-C++候选函数(2)和(3)将进行竞争:
void foo( std::string ); // (2) void foo( const char* ); // (3) |
那么编译器如何从中判断可用函数呢?C++语言对类型转换按照优先顺序定义有一个层级结构,在这个结构中,如果一种转换优于另一种转换,那么它将被排在
前面。在C++/CLI中,我们将CLI类型的行为也集成到了ISO-C++的标准类型转换层级结构中。下面是对集成之后的层级结构的一个描述:
1)精确匹配是最好的。需要注意的是精确匹配并不意味着实际传递的参数类型和函数声明的形式参数类型完全匹配。它们只需要“足够接近”就可以了。我们下面将会看到,“足够接近”对于ISO-C++和C++/CLI中的字符串字面常量有着一些不同的含义。
2)在标准转换中,拓宽转换要优于非拓宽转换。例如,将short拓宽为int要优于将int转换为double。
3)标准转换优于装箱(boxing)转换。例如,将int转换为double优于将int装箱为Object。
4)装箱转换优于用户自定义的隐式转换。
5)用户自定义的隐式转换优于没有任何转换!
6)否则,用户必须使用显式的转型符号来表示期望的转换。
对于上面两个ISO-C++下的候选函数,将字符串字面常量转换为一个std::string属于上面第5条,即隐式调用string的构造器来创建一
个临时string对象。而将字符串字面常量转换为一个const char* 属于上面第1条。第1条优于第5条,因此,参数为const
char*的那个函数在这场竞争中胜出。
这种归属在第1条“精确匹配”下的trivial
conversions实际上在技术的定义上是很严格的。总共有4种这样的trivial
conversions可以被归为精确匹配。即使在这4种trivial
conversions中,为了规范语言对类型的选择,它们也有一个优先级的排序。
大多数读者和程序员可能对于这样的细节没有多大兴
趣,并且通常情况下我们也无须深入到这些细节的地方。但是如果我们要得到一个直观的语言行为、并且确保它们在不同的实现上表现相同,这些规则的存在就很有
必要。这是因为作为一门编程语言,它的行为一般要具有某种程度的“类型感知”能力,从而允许程序员忽略这些细节。 下面让我们来对这4种trivial
conversions做一简单的了解。其中3种被称为左值转换(lvalue
transformation)。左值(lvalue)是一个可寻址的,可被执行写操作的程序实体。第4种为限定性转换(qualification
conversion),例如,在一个类型声明上加一个const修饰符就属于这种转换。其中3种左值转换优于限定性转换。
在我们上面的例子中,由本地数组到指针的转换,即由const char [5]到const char *,就是一种左值转换。在大多数情况下,我们甚至不将这看作一种转换。
这种形式的左值转换在C++/CLI中仍然适用,但是在我们将System::String类引入之后,字符串字面常量到const
char*的转换就不再是最好的匹配了。实际上,在C++/CLI中,"Pooh"这样的字符串字面常量的类型既是const
char[5](C++/CLI对本地类型系统保留后的结果),同时也是System::String(C++/CLI中的托管类型)。这样,在C++
/CLI中,字符串字面常量和System::String类型之间是一个精确的匹配,它优于由字符串字面常量到const
char*的trivial conversion。
有些朋友看到这里,可能会不高兴,“为什么要这样做?难道ISO-C++对字符
串字面常量的处理不能满足C++/CLI的绑定需要吗?”C++/CLI这样做的理由在于字符串字面常量是我们程序中的一个基本元素,而ISO-C++的
行为在很多情况下显得并不直观。实际上,这些规则在我们现在看到的结果之前的一年中被来来回回改了3次之多。
这反映了ISO-C++
和C++/CLI在对待各自的类型系统时存在的一个基础性差异。在ISO-C++中,除非是显式存在于一个类库中,否则类型就是独立的。因此,在字符串字
面常量和std::string之间并没有隐含的类型关系,虽然它们共享着同一个抽象域(domain)。
但是在C++/CLI中,
我们支持统一的类型系统。每一个类型,包括字面常量值,都是一个Object的子类。这也是我们为什么可以在一个字面常量值,或者内建类型的对象上直接调
用方法的原因。整数字面常量5的类型为Int32,字符串字面常量"Pooh"的类型为String。认为字符串字面常量更接近C风格的字符串,或者就把
它看作C风格的字符串是不合适的。
集成后的类型转换层级结构使得一个正常运行的ISO-C++程序在使用/clr编译器开关重新编译
后仍能展现同样的行为,但是使用CLI类型的新的C++/CLI程序在处理字符串字面常量时将会体现新的类型优先排序规则。这段讨论的长度相对于这个主题
的重要性而言可能并不合适,但是它却向大家揭示了我们到底在将CLI类型系统和ISO-C++语义框架集成在一起的时候,做了哪些工作,以及如何在必要的
时候调整在集成过程中所出现的各个情况的优先级。同时,这也提醒大家在将一个本地类重新构造为一个CLI类的过程中需要注意的一些问题。比如有些情况下我
们最好要对那些接受字符串字面常量的成员函数进行新的设计,而不是简单地将一个参数为String的函数添加到这些重载函数集合中了事。
另外需要注意的是,String表示的是Unicode字符集。和ASCII字符集不同,这需要两个字节来表示一个字符。虽然在C++/CLI中字符串
字面常量的类型为String,但这并不意味着在C++/CLI中,一个字符串字面常量必然会被解析为双字节的字符流。在本地C++中,我们要在字符串字
面常量前加一个L来告诉编译器将其看作一个双字节的字符流。在C++/CLI中,我们仍然需要这么做。
发布日期: 四, 09 八月 2007 07:39:38 GMT
计算机与 Internet |
阅读完整项
八、声明类型
CLR类型有一个形容词前缀用来说明类型的种类,下面是C++/CLI中的类型声明示例:
1、 CLR types
o Reference types § ref class RefClass{...}; § ref struct RefClass{...}; |
2、 Value types
§ value class ValClass{...}; § value struct ValClass{...}; o Interfaces § interface class IType{...}; § interface struct IType{...}; o Enumerations § enum class Color{...}; § enum struct Color{...}; |
3、 Native types
o class Native{...}; o struct Native{...}; |
示例:
using namespace System; interface class IDog { void Bark(); }; ref class Dog : IDog { public: void Bark() { Console::WriteLine("Bow wow wow"); } }; void _tmain() { Dog^ d = gcnew Dog(); d->Bark(); } |
上述程序中的代码与老的C++语言相比看上去非常简洁,在以往的C++代码中,至少要用到-gc和-interface这两个关键词
原文地址:
|
发布日期: 二, 07 八月 2007 08:37:32 GMT
计算机与 Internet |
阅读完整项
C++/CLI语言标准草案第8章语言概述节选翻译(8.1-8.2)声明,原文版权属于微软公司,本文只是方便众多网友熟悉了解即将面世的C++/CLI技术。
译者:jiang_hgame,justlikethewind
8.语言概述
提示:
这份规范是C++标准的一个扩展。本提示说明了该归范的必要特征.以后的章节将会祥细说明规则和例外。而这一章将以牺牲完整性的代价换来简单性和明确性。我们试图提供一个介绍以促使便于读者能编写一些初步的程序并能阅读以后的章节。
8.1起始
规范的”hello world”程序可被写成如下刑式:
int main() {
System::Console::WriteLine("hello, world");
}
C++/CLI程序的源代码被存储于一个或多个以cpp为扩展名,就像hello.cpp那样的文本文件中。使用一个命
令行编译器(例如cl).程序可以被这样一个命令编译:
cl hello.cpp
之后将产生一个应用文件hello.exe,运行该文件产生输出
hello, world\n
CLI库由大量名字空间组成,其中最常用的是system,该名字空间中包含一引用类Console,它提供了一组函数用于运行控制台输入输出,
WriteLine是这些类中的一个,当输入一组字符串,控制台将输出该字符串并换行。(注意,我们假设该例的名字空间System已经被using语句
声明)
8.2Types
值类型与句柄类型的区别在于值类型直接包含它们的数据,而句柄类型的变量存储对象的句柄。对于句柄类型来说,两个变量可能引用同一个对象。这样对其中一个
变量的操作会影响到对象,从而影响到另一个变量。而对于值类型,每个变量有它们自己数据的复本,对于一个变量的操作不会影响到另一个变量。
例子
ref class Class1 {
public:
int Value;
Class1() {
Value = 0;
}
};
int main() {
int val1 = 0;
int val2 = val1;
val2 = 123;
Class1^ ref1 = gcnew Class1;
Class1^ ref2 = ref1;
ref2->Value = 123;
Console::WriteLine("Values: {0}, {1}", val1, val2);
Console::WriteLine("Refs: {0}, {1}", ref1->Value, ref2->Value);
}
从输出结果可以看出两者之间的区别
Values: 0, 123
Refs: 123, 123
对局部变量val1的赋值不会影响局部变量val2,因为两个局部变量都属于值类型(the type int ),每个值类型的局部变量都有自己的存储空间,相反的,ref2->Value = 123的赋值,影响了ref1和ref2共同引用的对象。
Console::WriteLine("Values: {0}, {1}", val1, val2);
Console::WriteLine("Refs: {0}, {1}", ref1->Value, ref2->Value);
上面几行值得做更多的注解,它们展示了某些Console::WriteLine的字符串格式化行为。实际上,它采用了可变数量的参数。第一个参量是是一
个字符串,包含一定数目的{0}和{1}这样的占位符。每个占位符引用后面跟上的参量,比如{0}引用第二个参量。{1}引用第三个参量,并以次类推。在
输出送到控制台前,每个占位符会被它所对应的已格式化的参数的值替换。
开发者可以通过enum和值类型的声明定义新的值类型。
实例
public enum class Color {
Red, Blue, Green
};
public value struct Point {
int x, y;
};
public interface class IBase {
void F();
};
public interface class IDerived : IBase {
void G();
};
public ref class A {
protected:
virtual void H() {
Console::WriteLine("A.H");
}
};
public ref class B : A, IDerived {
public:
void F() {
Console::WriteLine("B::F, implementation of IDerived::F");
}
void G() {
Console::WriteLine("B::G, implementation of IDerived::G");
}
virtual protected void H() override {
Console::WriteLine("B::H, override of A::H");
}
};
public delegate void MyDelegate();
上面展示了每种类型定义的实例。之后的章节将详细说明类型定义。
如
上面的Color,Point和Ibase等类型,并没有在其它类型中被定义,仍然可以有一种可见性的约定,它要么是公开的(public)要么是私有的
(private)。public的使用指明类型对assembly(这里并不是汇编程序的意思,在.net中指一个应用程序的概念)外是可见的。相对
的,private则说明类型对assembly外不可见。类型自动默认的可见度是private。
原文地址:
发布日期: 二, 07 八月 2007 08:20:36 GMT
计算机与 Internet |
阅读完整项
标准C++及C++/CLI发展综述
自
从Java诞生的一开始,语言以及开发平台的竞争似乎才真正开始无休无止起来,以前的那些单纯的时光似乎离我们越来越遥远。各种新名词仿佛是在一夜之间全
部涌现了出来。几乎在每个公司的招聘广告上总能看到"Java,J2EE,C#,.NET..."等字样。我们的时间似乎越来越不够,在各种新概念面前,
唯有埋头不断啃书,不断学习,我们身不由己,总担心一有懈怠就会立即落在潮流的后面...
诚
然,这些新事物总是有存在的理由,对于做高层应用开发的人们,Java以及J2EE,C#以及.NET似乎(也的确)给了人们一个很好的承诺,并且确实实
现了他们的承诺。在Web的世界里,这些语言和平台一出身就体现出了无比的优势,因为其架构本身就是为这个世界构建的。
然
而,鱼和熊掌总是不可兼得,语言的简洁性固然简化了我们的学习过程,然而却削弱了代码的表达力。托管执行的引入固然消除了平台相关性,却损害了效率(至少
在目前,这仍然很大程度上限制了托管语言的发挥空间)。GC固然简化了内存管理,固然是大势所趋,使得程序员可以从内存管理的"雷区"脱身而出,然而更一
般的却是资源管理的问题,程序员仍然没有少费多少心思,并且GC的世界也并不单纯,程序员有了依托就可以大胆的去new,用完了就丢,某种程度上GC鼓励
了垃圾的产生,更为重要的是,GC本身的速度仍然是个问题,虽然硬件和GC算法总在不断的改进,但是在某些性能要求苛刻以及内存有限的环境中(如嵌入式系
统),GC仍然还是个谨慎的选择。
然而,光芒毕竟还是掩盖了尘埃,在厂商们的大力宣传下,我们似乎也变得勇往直前,却很少有人注意,在喧嚣的背后,有一门语言仍然在冷静的发展着,这就是C++。
是的,对于C++的发展,我只能用冷静来形容,因为C++社群中集中了太多的智慧,太多来自各个领域的专家提供了他们的经验和建议。对于这样一门"公共"的语言,每一步发展必然是经过千锤百炼的,并且经受得住时间考验的。
标准C++
从
某种意义上说,C++仍然是一门很年轻的语言,从98年C++的第一次标准化到现在,虽然语言的核心并没有变化,但是库的发展却未曾有一日停止过:
STL,Boost,ACE,LOKI,MTL,Blitz++...这些熟悉的名字正在渐渐深入人心,这些库所带来的思想和效益正逐渐深入工业界,这些
库保持着C++长盛不衰的活力。
而
现在,C++社区又在酝酿一次新的变革,这就是所谓的C++
0x标准,也就是C++的下一代标准,其中“0x”可能是07~09,虽然看起来C++0x离我们还有好几年,但是从98年到04年,关于C++标准(语
言核心及库)的勘误以及提议已经达到500左右[1],其中有很大一部分思想已经成熟,只是需要反复经过时间以及实践的考验而已。当然,我们最为关心的还
是,这次变革会给我们带来多少惊喜,简洁的答案是“会有极大的惊喜”。冗长的答案就在下文——
Bjarne Stroustrup的思想——灯塔
这
位C++创始者仍然活跃在C++标准化委员会中,毫无疑问,他仍然具有举足轻重的发言权。对于C++ 0x的方向,Bjarne
Stroustrup的总体思想是:对语言核心的改动应该是谨小慎微的,而对库的发展则应该是大胆而激进的[2]。看起来非常简单平淡的两句话,却道出了
真谛——保持语言核心的简洁紧凑和一致性,仅作必要的小范围改动,可以避免增加语言的复杂度,并将对现存代码的兼容性提到最高。从另一个方面,库的发展则
大可不用顾忌这些,C++社区期待一些优秀的,可以提高生产率的库已经很久了。
语言核心的发展——精雕细琢
总的来说,C++ 0x对语言核心的调整是极其理智和谨慎的——避免大的语言扩充,进一步改善对泛型编程的支持,改善对嵌入式编程的支持。然后就是一些小范围的技术勘误和调整,改善语言的一致性等。
如果非要找出一个关于C++语言的关键性改动(它的确存在),就是加入了一个右值引用“&&”和Move语义的加入,虽然看起来这是个很小的改动,但是却对语言的效率产生了非常巨大的影响,因为它完美的解决了临时对象的效率问题。
简
单的来说,"Move"语义可以允许一个对象被"Move(搬移)"到另一处(而并非"拷贝"--这是目前的做法),从语义上说,"搬移"意味着将对象整
个的搬到目标地点,原来的对象从语义上将不复存在。对于动态分配内存的对象来说,"搬移"意味着将动态分配的内存直接搬到新家(目标对象)里面,而源对象
将不复拥有原来的动态内存。说得细节一点,这就相当于一次指针赋值,只要把源对象里的动态内存直接交给目标对象就行了,一次所有权的转交,源对象不再拥有
原来的动态内存。Move语义完美的解决了返回值的效率问题(这被认为是C++中影响效率的主要因素),所以具有非常重要的意义。在目前,大多数情况下,
即使有返回值优化,仍然会存在不可避免的对象冗余拷贝,函数返回的值至少要被一次拷贝(到目标对象),然后函数栈内的对象被析构掉,这其实是一种浪费,反
正栈内的对象总要析构的,为什么不把其中动态分配的资源直接Move到目标对象呢?但是在目前的C++里,很难在拷贝构造函数中知道源对象是否是"可搬移
的"--万一源对象并非函数返回的值,而是一个普通的"具名"对象怎么办?你可不能将别人的动态内存"偷"过来。但是,考虑到函数返回值总是右值,而"具
名"对象总是左值,所以只要有一种机制,能够区分出左值右值,就能够决定是否从源对象中"偷"内存了。这就是引入右值引用"&&"的原因
之一。只要拷贝构造函数接受的是右值,就放心的搬移吧--它必定是个临时对象(当然,也有可能是用户显式要求搬移)--这种搬移必定比“先将动态内存原版
拷贝一份过来,然后任由目标对象析构掉原先的那份内存”要快得多。考虑一下目前的std::vector在重新配置内存时候的效率吧--先要将容器里原本
存在的对象全都"拷贝"(注意,拷贝可能是相当昂贵的操作)到新配置的空间里面,然后再将原来的对象一一析构。为什么不将它们直接"搬移"(而并非"拷贝
"过来)呢?"搬移"是极具效率的动作。没有额外的动态分配(因为不是拷贝,不用照顾源对象的状态,直接吧源对象拥有的动态内存以及资源"偷"过来就可以
了,这几乎就是一次指针赋值)。此外,"Move"语义还完美的解决了函数返回值的效率问题。
另
外,右值引用(&&)也使得"perfect
forwarding"成为可能。其含义是"将一组参数完整保留原来的左右值属性以及CV修饰转发给目标函数的能力"。你可以发现,在现在的语言机制下,
这种转发是难以完美的,主要原因是无法判断左右值。右值引用使得函数可以判断参数到底是左值还是右值,从而实现完美转发。
关于右值引用的更多信息可以参见关于C++语言核心的两大提议[3]。
另外,C++ 0x还将很大程度上改善对GP的支持,如decltype,auto,varadic template等(详见C++标准的主页)。
总的来说,所有的语言核心的演化,都必须遵从零开销(zero overhead)原则,即用户不必为他们用不着的东西付出代价(特别是效率的代价),这正是C++在系统级开发领域能够保持长久活力的关键。无论如何,系统级开发是C++的立场,千变万变,立场不变。
库的发展——风起云涌
在C++库的发展的历史中,最为瞩目的当属STL,由于GP思想在其中的完美运用,STL兼得了“鱼和熊掌”——效率和优雅。看来大家都看到了GP的强大
表达力,特别是在库的构建方面,所以像Boost,ACE,LOKI,Blitz++,MTL...等库都不约而同的使用了GP,并取得了巨大的成功。可
以预见GP仍然会在库的构建方面发挥巨大的能力。
发
展最为迅疾的库是Boost[4],因为Boost库是“准”C++标准库,所以其中的很多设施被提议加入下一代C++标准库,如正则表达式库,智能指针
库,线程库,泛型函数指针库等,这些都为通用编程提供了极大的支持,另一个重要方面是,这些库的标准化意味着使用它们的代码将是完全可移植的。
另外,被程序员们长久以来所埋怨的serialize能力也在Boost库中得到了解决——boost::serialize将被加入boost库,为跨平台的serialize提供良好的支持,唯一的缺点就是我们等得太久了:-)
之
所以C++这两年来“看起来”风头减弱,主要是大家的注意力都被吸引到了J2EE和.NET所擅长的Web开发以及企业应用开发领域里去的缘故,而C++
在这方面又如何呢?CORBA太庞大,ACE还不够“傻瓜”,ICE据说是以轻量级为目的,但愿如此。但最重要的还是,C++本身对分布式的支持还不够,
所以往往要借助其它工具,比如IDL以及IDL编译器,IDL某种程度上就是类型信息(如支持哪些接口)的载体,它在客户端和服务器端传递(承载)类型信
息,但在Java或.NET中,IDL不是必要的,因为类型信息由元数据(metadata)来承载,所以在.NET Remote和Java
RMI架构下对象的远程沟通很方便,从而可以很方便的构建一个分布式计算的环境。
而
在C++中,这样的日子可能也不会太遥远了,Bjarne
Stroustrup正致力于为C++加入分布式计算的能力[5],一旦这成为现实,程序员将可以通过极为简单的几行代码就可以和远在Web另一端的对象
交流,远程调用就如同本地调用一样简单直观...这些特性可能会在C++
0x中和大家见面——当然,以库的形式。我们有理由相信,C++在分布式计算以及Web开发领域也将有一个美好的未来。
同
样,为程序员熟知的GUI编程也曾是C++的“软肋”之一。但是这也即将成为过去。一个泛型的GUI库正在悄然兴起。以前的C++
GUI库如WxWindows以及MFC由于只使用了C++的普通特性,所以表达力有限。而这个兴起的库名为Win32 GUI
Generics[6],GP与多继承的完美结合,结果是简洁而优雅的GUI框架。
可以看出来,虽然C++的主要阵地是系统级开发,但是无疑在其它更高层的领域也有不可估量的潜力,我们拭目以待。
C++/CLI——微软的又一张王牌
作
为微软的“首选”语言,C++在微软心目中占有着异常重要的地位,标准C++的发展固然是独立于平台的,但是为了让开发者们能够在windows下更具效
率的工作,标准C++仍然还不够“便利”,微软希望windows下的C++开发者既能够做高效的底层系统级开发,又能够方便的做高层的应用开发。怎么
做?.NET?当然是.NET!.NET是微软的王牌,下一代windows将完全建立在.NET的基石上,目前的windows
XP的系统API仍然是传统的C接口的WIN32API,而下一代windows(longhorn)将把.NET本身作为系统API。然而,微软最钟爱
的语言——C++——和.NET相处得又如何呢?如果把时间往前推半年,答案只能是——差强人意,Managed C++
Extension只不过是一次失败的尝试,将标准C++通过一些丑陋的双下划线为前缀的关键字强制“托管”起来已经被时间证实不是个好主意。其导致语义
的模糊以及语言表达力的退化是MC++消亡的主要原因。
为
了解决这些问题,并且稳固C++在windows(.NET)平台上的地位,微软将Stan Lippman和Herb
Sutter两位大牛人请了过来,重新设计C++在.NET下的表现形式,标准C++由于本质上是编译型语言,所以显然是不够的,然而又不能“从头发明轮
子”,怎么办呢?微软选择了最明智的办法——中庸之道——在完全支持标准C++的前提下,引入一些新的语法和语义,从而对.NET环境提供第一流
(first
class)的支持,这就是C++/CLI。之所以有这个称呼,是因为它从某种意义上说并非一门新的语言,标准C++仍然在其中完整保留着,C++
/CLI只不过建立了C++与CLI之间的联系而已,虽然说“...而已”,但是其意义是非常巨大的,C++程序员从此可以以一种非常“C++”的方式去
利用.NET中丰富的类,同时仍然可以利用标准C++的强大表达力,可谓左右逢源。C++/CLI的能力可以用下图来描述:
图一. C++/CLI的能力
语法
C++/CLI除了和标准C++相同的部分之外,还增加了一些新的语法元素,以便支持托管环境下的编程,其中最关键的就是加入了托管环境下的“指针”(称为Handle)和引用以及用于在托管堆上创建对象的gcnew,它们的声明形式分别为:
int^ handle = gcnew int(0); //托管的指针“^”,称为Handle
int% managed_ref = *handle; //托管的引用“%”
值得注意的是,对handle解引用也使用“*”,这是为了在模板函数中可以以一致的方式来解引用。
这两个语法形式的加入,确保了在C++/CLI中你可以利用.NET中所有的托管类,并且把托管的世界和native世界从语法上区别开来。
总
的来说,C++/CLI的语法完全遵循标准C++的风格(毕竟Herb
Sutter是标准C++委员会的成员嘛:-)),这种一致性可以极大的平滑程序员的学习曲线,并使得C++/CLI中的程序风格和标准C++非常接近。
另外的语法元素你可以参考微软官方网站上提供下载的C++/CLI候选标准1.5。
语义
加
入了托管语义的C++/CLI呈现出一种混合(Mixed)的语义(但并不混淆),你既可以高效的进行Native编程,也可以在必要的时候为了方便而使
用.NET中现成的类,两者互补且可以相互沟通,如果想把托管的对象传给Native接口,只要先把该对象“定”在托管堆上(通过pin_ptr<
>,不然GC在压缩堆的时候会移动对象,从而导致指向它的native指针统统失效),然后传递指向该对象的指针给Native接口就行了,反之,
在托管环境里访问非托管对象则一般不用任何辅助动作。这种混合式的编程环境提供了极大的自由度,也完全兼容了现存的C++代码。
图二. C++/CLI编程环境示意图
STL.NET——又一个诱人的条件
STL.NET是微软吸引C++开发者往.NET环境下迁移的又一个诱人的条件——用标准C++进行开发的程序员对于使用STL已经积累了大量的经验,当
然最关心的是.NET托管环境下有没有一个可以和STL相媲美的“容器——算法”库,微软的回答是“当然有”——这就是STL.NET[7]。
STL.NET中的容器针对的是托管环境,也就是说,C++/CLI的开发者可以以熟悉而优雅的方式在托管环境中使用(甚至扩展)容器类和算法,当然,原
来的“Native”STL依然“健在”,程序员仍然可以在效率要求较高的地方使用原来的STL,一切都依从他们以往的经验——看来微软对C++真是费足
了心思。
关于C++——微软的回答
对于在.NET平台上如何选择开发语言,微软的答案是:
Choosing a Language
Q: Which .NET language should you use?
Microsoft’s answer, in our Whidbey release:
• C++ is the recommended path to .NET and Longhorn.
• If you have an existing C++ code base:
Keep using C++.
• If you want to make frequent use of native code/libs:
Use C++. It’s far simpler (seamless) and faster.
• If you want to write brand-new pure-.NET apps that
rarely or never interop with native code:
Use whatever language you’re already comfortable with...
由此可见C++在.NET平台上的重要性,这种重要性反过来也会对标准C++的发展起到积极的作用——毕竟,学习C++/CLI首先意味着学习标准C++ :-),因为.NET平台的广泛性,可以预见会有更多开发者转向C++社群,这是一个良性循环。
C++&C++/CLI的未来——坐看云起时
Bjarne
Stroustrup在一次接受采访的过程中曾说:“C++拥有极其美好的未来”。他的自信当然不是盲目的。就目前标准C++的发展趋势来看,C++正在
越强大,以前主要在系统级开发领域挥洒的C++正在逐渐进入越来越多的领域,例如,对分布式计算,Web开发以及GUI编程的优秀支持。而作为标准C++
的一个超集的C++/CLI则会为托管环境下的C++编程提供优秀的支持。在标准C++发展过程中,最为难能可贵的是,标准C++始终坚持零开销
(zero overhead)原则,给系统级开发的程序员一个效率的承诺。
随着C++ 0x 标准愈来愈进的脚步声,C++正在坚定的朝着更辉煌的目标迈进。
C+
+/CLI是微软给.NET程序员的一个礼物,也是给标准C++的一个礼物。按照Bjarne
Stroustrup的思想,标准C++的发展会始终维持编译型语言的实质,而想用C++做更多事情的厂家可以自行对其进行托管环境下的扩展。C++
/CLI的出现表明了这种扩展的可行性和成功性。可以预见,完全继承了标准C++中强大的表达力的C++/CLI在.NET平台上将会有最好的表现。同
时,标准C++的发展也意味着C++/CLI的发展,C++/CLI享有标准C++发展中的一切成果!
故事才刚刚开始...
--------------------------------------------------------------------------------
[2] Bjarne Stroustrup的主页上有一篇文章描述C++0x的发展方向。
[7] 关于STL.NET,Stan Lippman有一篇精彩的阐述——“STL.NET Primer”,见MSDN
发布日期: 二, 07 八月 2007 08:16:50 GMT
计算机与 Internet |
阅读完整项
从C++到C++/CLI
看起来只是在C++后面多写了一个“/CLI”,然而其意义却远不止于此,google的c++.moderated版上为此还发起了数星期的讨论,在国内大部分人对C++/CLI还不是很了解的情况下,google上面已然硝烟四起...
就像我们作出其它任何选择一样,在选择之前最重要的是先要清楚为什么作出这样或那样的选择——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
Group,Dinkumware公司等,微软在把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对于拥有元数据的语言如JAVA或C#根本就不是必须的,同时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很重要的一点是:它正是可选的。这一点不同于JAVA或C#,对于后者,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平台上运行——Unix,
Linux下的Mono(移植到Unix,Linux下的.NET),以及FreeBSD,Mac
OSX下的Rotor(.NET的开放源代码项目),等等。
习
惯了标准C++输入输出流的程序员可能要抱怨了——我们为什么要使用BCL里面的输出输出流?标准的iostream已经很好了!这里其实有一个解决方
案,使用
iostream的代码之所以不能“一次编译,随处运行”是因为代码要依赖于本地的二进制lib文件,而如果可以把iostream的实现重新也编译成纯
MSIL代码,那么使用它的代码编译后就完全可随处运行了。目前,这是个有待商榷的方案。不过,至少当面对“总得依赖于某些平台相关的二进制代码”这种情
况时,可以把平台相关的代码封装成DLL文件——对各个目标平台编译成不同的二进制版本,而程序的其它部分仍然只需一次编译即可,只要使用.NET的
P/Invoke就可以对不同平台调用相应的DLL了。
l
效率——作为.NET平台上的系统级编程语言,C++/CLI混合了Native和Managed两种环境。而不象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++写了一个程序,其中用到了STL,Boost里面的Lambda,MPL,Signal等库,然后我把编译选项“/clr”(甚至“/clr:
pure”)打开,结果是程序完全通过了编译。而对于使用C++/CLI进行开发的程序员,则需要熟悉的就是.NET平台上的编程范式以及库的使用等,至
于以前你所熟悉的标准C++编程的各种编程手法,技巧,各种库的使用——Just Keep Them!
所以,确切的说,你需要的是学习,而不是改变。
C++/CLI——优秀的混血儿
C++/CLI最大的成功在于引入了混合式编程的环境,这是一种非常自由的环境,其中Native和Managed代码可以共存,可以相互沟通,从而完全接纳了标准C++的世界,同时也为另一个世界敞开了大门...
下面就是C++/CLI扩展的几大关键特性——
Handle和gcnew——通往Managed世界的钥匙
还记得在Managed C++
Extension世界里是如何访问托管类的吗?丑陋的__gc关键字无处不在——事实上,不仅是“丑陋”而已(MC++为什么会消亡?)。而在C++
/CLI里则引入了一个新的语法元素,名为Handle,写作“^”——你可以把它看成Managed世界里的Pointer(不过不能进行指针算术)。
Handle
用于持有Managed Heap上的对象,那么如何在Managed
Heap上创建对象呢?原来的new显然不能用,那样会混淆其语义,所以C++/CLI引入了一个对应的gcnew关键字,这两个新的语法元素是操纵
Managed世界的关键。现在,使用Handle和gcnew,你就可以和任何托管类进行沟通。另外,既然有了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()函数),因为析构函数有一个最大的优点——
Deterministic Destruction & RAII —— 资源管理的利器
正
如每一个熟悉标准C++的程序员所清楚的:由C++构造及析构函数的语义保证所支持的RAII(“资源获取即初始化”[2])技术是资源自动和安全管理的
利器,这里的资源可以包括内存,文件句柄,mutex,lock等。通过正确的使用RAII,管理资源的代码可以变得惊人的优雅和简单。相信有经验的C+
+程序员都熟悉应该类似下面的语句:
void f()
{
ofstream outf(“out.txt”);
out<<”...”;
...
} //outf在这里析构!
这里,程序员根本不用手动清理outf,在函数结束(outf超出作用域)时,outf会自动析构,并释放其所有资源。即使后续的代码抛出了异常,C++
语言也能保证析构函数会被调用。事实上,在异常抛出后,栈开解(stack
unwind)的过程中,所有已经正确构造起来的局部对象都会被析构。这就为异常环境中资源的管理提供了一种强大而优雅的方式。
而对于C#或Java,代码就没有这么优雅了(特别是java)——C#虽然有using关键字,但是代码仍然显得臃肿,而Java为了保证在异常情况下
资源能够正常释放,不得不用了丑陋冗长的try-finally块,在情况变得复杂化时,C#的和Java的代码都会变得越发臃肿。
那么,在C++/CLI中,原来的那种优雅的,靠析构函数来确保资源正确释放的手段还存在吗?答案正如你所期望和熟悉的,RAII仍然可以使用,仍然和标准C++中的能力一样强大:
ref struct D
{
D(){System::Console::WriteLine(“in D::D()\n”);}
~D(){System::Console::WriteLine(“in D::~D()\n”);}
!D(){System::Console::WriteLine(“Finalized!\n”);}
};
int main()
{
D d; // in D::D()
...
} //d在这里析构!in D::~()
ref关键字表示该类是Managed类。所有的ref类都继承自一个公共基类System::Object。至于struct和class的区别仍然和
标准C++中的一样。如你所见,对于ref类,你同样可以像在标准C++中那样定义析构函数,该析构函数会在确定的时候被调用——也就是D超出作用域时。
一切都与你以前的经验相符。
值得注意的是,对于了解Java或C#的程序员,ref类的析构函数就是Dispose(),你不必也不应该另外手动定义一个Dispose()成员函
数。那么,Finalize函数到那里去了?既然ref类创建在托管堆上,那么迟早要被GC回收,这时候,应该被调用的Finalize函数在哪儿呢?C
++/CLI为此引入了一个新的语法符号“!D()”,这就是D的Finalize函数,这个“!D”函数被调用的时机是不确定的,要看GC什么时候决定
回收该类占用的空间。
~D()析构函数和标准C++里的用法完全相同,释放以前获取的资源。而对!D()的用法则和Finalize函数一样,由于其调用时机是不确定的,所以千万不要依赖于它来释放关键资源(如文件句柄,Lock等)。
为ref类引入~D()和!D()极大的方便了资源管理,也符合了标准C++程序员所熟悉的方式。Herb Sutter[3]把这个能力看成C++/CLI在Managed环境下最为强大的能力之一。
pin_ptr —— 定身法
千
万不要小看了pin_ptr的能力,它是Native世界和Managed世界之间的桥梁。在通常情况下,任何时候,GC都会启动,一旦进行GC,托管堆
就会被压缩,对象的位置就会被移动,这时候所有指向对象的Handle都会被更新。但是,往往有时候程序员会希望能够把托管堆上的数据(的地址)传给
Native接口,比如,为了复用一个Native的高效算法,或者为了高效的做某些其它事情,这种情况下普通的Native指针显然不能胜任,因为如果
允许Native指针指向托管堆上的对象,那么一旦发生了GC,这些得不到更新的Native指针将指向错误的位置,造成严重的后果。办法是先把对象
“定”在Managed堆上,然后再把地址传给Native接口,这个“定身法”就是pin_ptr——它告诉GC:在压缩堆的时候请不要移动该对象!
array<char>^ arr = gcnew array<char>(3); //托管类
arr[0] = 'C';
arr[1] = '+';
arr[2] = '+';
pin_ptr<char> p = &arr[0]; // 整个arr都被定在堆上
char* pbegin=p;
std::sort(pbegin,pbegin+3); //复用Native的算法!
std::cout<<pbegin[0]<<pbegin[1]<<pbegin[2]; //输出 “++C”
在上面的代码中,我们复用了STL里的sort算法。事实上,既然有了pin_ptr,我们可以复用绝大部分的Native算法。这就为我们构建一个紧凑高效的程序内核提供了途径。
值得注意的是,一旦对象中的成员被定在了堆上,那么该对象整个就被定在了堆上——这很好理解,因为对象移动必然意味着其成员的移动。
还有另一个值得注意的地方就是:pin_ptr只能指向某些特定的类型如基本类型,值类型等。因为这些类型的内存布局都是特定的,所以对于Native代
码来说,通过Native指针访问它们不会引起意外的后果。但是,ref
class的内存布局是动态的,CLR可以对它的布局进行重整以做某些优化(如调整数据成员排布以更好的利用空间),从而不再是Native世界所能理解
的静态结构。然而,这里最主要的问题还是:ref
class底层的对象模型和Native世界的对象模型根本就不一致(比如vtbl的结构和vptr的位置),所以用Native指针来接受一个ref
class实例的地址并调用它的方法简直肯定是一种灾难。由于这个原因,编译器严格禁止pin_ptr指向ref class的实例。
interior_ptr —— 托管环境下的Native指针
Handle的缺憾是不能进行指针运算(由于其固有的语义要求,毕竟Handle面对的是一个要求“安全”的托管环境),所以Handle的能力较为有
限,不如标准C++程序员所熟悉的Native指针那么强大。在STL中,iterator是一种极为强大也极具效率的工具,其底层实现往往用到
Native指针。而到了托管堆上,我们还有Native指针吗?当然,原来的形如T*的指针是不能再用了,因为它不能跟踪托管堆上对象的移动。所以C+
+/CLI中引入了一种新的指针形式——interior_ptr。interior_ptr和Native指针的语义几乎完全一样,只不过
interior_ptr指向托管堆,在GC时interior_ptr能够得到更新,除此之外,interior_ptr允许你进行指针运算,允许你解
引用,一切和Native指针并无二致。interior_ptr为你操纵托管堆上的数据序列(如array)提供了强大而高效的工具,iterator
模式因此可以原版照搬到托管环境中,例如:
template<typename T>
void sort2(interior_ptr<T> begin,interior_ptr<T> end)
{
... //排序算法
for(interior_ptr<T> pn=begin;pn!=end;++pn)
{
System::Console::WriteLine(*pn);
}
}
int main()
{
array<char>^ arr = gcnew array<char>(3);
... //赋值
interior_ptr<char> begin = &arr[0]; //指向头部的指针
interior_ptr<char> end = begin + 3; //注意,不能写&arr[3],会下标越界
sort2(begin,end); //类似STL的排序方式!
}
T*,pin_ptr,interior_ptr——把它们放到一起
T*,pin_ptr,interior_ptr是C++/CLI中三种最为重要的指针形式。它们之间的关系像这样:
//很可惜,这里应该有一幅图的,可是原文显示不了,也没有搜到
强大的Override机制
在标准C++中,虚函数重写机制是隐式的,只要两个函数的签名(Signature)一样,并且基类的同名函数为虚函数,那么不管派生类的函数是否为
virtual,都会发生虚函数重写。某种程度上,这就限制了用户对它的派生类的控制能力——虚函数的版本问题就是其一。而在C++/CLI中,你拥有最
为强大的override机制,你可以更为明显的来表示你的意图,例如下面的代码:
class B
{
public:
virtual void f() ;
virtual void g() abstract; //纯虚函数,需要派生类重写,否则派生类就是纯虚类
virtual void h() sealed; //阻止派生类重写该函数
virtual void i() ;
}
class D:public B
{
virtual void f() new ; //新版本的f,虽然名字和B::f相同,但是并没有重写B::f。
virtual void h() override ; //错误!sealed函数不能被重写
virtual void k() = B::i ; //“命名式”重写!
}
通过正确的使用这些强大的override机制,你可以获得对类成员函数更强大的描述能力,避免出乎意料的隐式重写和版本错误。不过需要提醒的是,“命名式”重写是一种强大的能力,但是需要谨慎使用,如果使用不当或滥用很可能导致名字错乱。
值类型&封箱和拆箱
如果你来自C#,我几乎可以听到你的叹气声J
的确,在.NET平台上编程,你无可避免的要面对值类型和引用类型的微妙差别以及“疯狂”的隐式封箱——引用类型(对应于ref
class)的实例是第一流的对象,继承自公共基类System::Object,拥有方法表,对象头等等。但是值类型(对应于value
class)却极为简单,类似于C++中的POD[4]类型,没有方法表和对象头等,值类型应该被分配在栈上,而当你用Handle来持有值类型实例时,
它就会被隐式的封箱到托管堆上(因为Handle必须持有一个一流的对象),只有当值类型的实例被封箱到堆上的时候,它才会拥有第一流的对象特征,可以被
Object^来引用。
这些都是.NET内在的特性,所有使用.NET平台的语言都必须遵守,从这个意义上说,.NET的确是最高统治者J
幸运的是,情况或许没有你想象的那么糟糕,或许比在C#里面还要好一些——因为C++/CLI中的Handle的语法特征是如此明显,所以你几乎可以立即发现什么地方会出现封箱拆箱(尽管如此,还是要面对一些微妙的情况),我们来看一个例子:
value class V //value关键字表示这是个值类型,值类型应该分配在栈上
{ int i;};
V v; //在栈上创建V的实例
//由于V^必须引用一个“完整”的对象,也就是具有方法表,元数据以及对象头并继承自System::Object公共基类的对象,所以v被隐式封箱到托管堆上。
V^ hv1 = v; //注意,隐式封箱!
V^ hv2 =%v; //也是封箱!把”%”用到值类型上会导致一个Handle,
//所以会封箱,这种形式比较明确!
hv1->i = 10; //改变的不过是堆上封箱后的对象中的i,v的成员i的值并未改变
v = *hv1; //unbox,然后逐位拷贝到栈上,这时候v.i为10
这里你可能意识到了问题——既然用Handle来持有值类型总会导致它被封箱到托管堆上,那么万一我要写一个函数,接受一个(栈上的)值类型实例为实参并
改变其成员的值,该怎么办呢?如果使用Handle,那么你所指向的就不是原来的值而是封箱后的对象,从而看起来改变了其成员,其实只不过改变了一个“临
时”对象的值而已!所以,Handle在这里应该退居二线,这里是“%”(托管的引用,对应于Native引用——“&”)的用武之地——把一个
托管引用绑定到位于栈上的值类型不会引起封箱操作,我们看一个例子:
void adjust(V% ref_v)
{
ref_v.i = 10; //改变ref_v的成员!
}
int main()
{
V v;
adjust(v); //不会引起封箱操作
System::Console::WriteLine(v.i); //打印出10
}
原则是:要修改栈上的值类型实例,优先使用“%”,而不是“^”。这样你将获得最好的效率和程序的正确性。
STL.NET
STL是标准C++中最为优雅,使用最广泛的库之一,标准C++程序员在使用STL的过程中积累了大量的经验。当然,在C++/CLI的扩展世界里,人们
也期望能有这样的库,能够沿用他们熟悉以久的经验和技法,这就是STL.NET,为托管世界准备的STL!Stan
Lippman[5]在MSDN上的一篇文展STL.NET Primer以简明扼要的方式阐述了STL.NET的优点[6]。
代码的组织
虽然C++/CLI带来了强大的能力,但是对于从标准C++社群来的人们,则更愿意将他们的标准C++代码和使用了C++/CLI扩展特性的代码隔离开
来,以便让前者可以在不同平台上移植,而不是绑定到CLI平台。毕竟,用C++/CLI编程并不意味着你的所有代码都是和C++/CLI的扩展特性相关的
——C++/CLI的定位是系统级编程,所以可以想象会有很大一部分人会非常愿意用标准C++来写效率关键的代码部分,例如你可以用标准C++来写高效的
算法,而这些算法应该可以被复用到其它Native环境中去。那么,如何把这些标准C++代码和C++/CLI的扩展特性隔离开来呢?如何隔离?不同编译
单元之间的界限就是最好的栅栏——把你的标准C++代码放在独立的头文件和源文件中,把使用了C++/CLI扩展的代码放在另外的头文件和源文件中。并
且,尽量不要在你的Native
class中使用CLI的语法特性,如property,delegate,index等,尽量不要让你的Native Class继承自ref
Class。总之,尽量保证代码结构的清晰,你将得到最大程度上的可移植性。
小结
C++/CLI是一个创举,它把托管环境和Native环境整合在一起,使开发者同时拥有了“上天入地”的强大能力。显而易见,微软为了C++/CLI花
费了大量的心力。以使得标准C++程序员能够平滑的过渡到C++/CLI上面。所谓平滑,就是能够尽量保证原来的编程技巧,习惯,范式等,它的确做到了。
面对C++/CLI,已经不是争论该不该学习的问题,而是如何让它发挥更大的能量的问题。
原文地址:
http://blog.yesky.com/blog/cqfz/archive/2004/12/30/57339.html
发布日期: 二, 07 八月 2007 07:36:38 GMT
计算机与 Internet |
阅读完整项
面对C++/CLI,很多人的第一个问题自然是“什么是C++/CLI”,我个人喜欢将其看作是位于静态程序设计和动态程序设计之间的一座桥梁。C++/CLI这个名称本身就包含着一组术语——而其中最重要的术语却是最不明显的那一个。
首
先来看第一个术语“C++”,这当然指的是由Bjarne
Stroustrup在Bell实验室时发明的C++编程语言。它所支持的是一种为代码执行速度和执行体所占空间所高度优化的静态对象模型。除了堆内存分
配以外,它不支持在运行时对应用程序进行任何的更改。它允许我们对底层机器进行无限的访问,但对于正在运行的程序中的活动类型、以及相关的程序基础构造,
它的访问能力却非常有限、或者根本就不可能。它是一门非常成功的编程语言,但是它却不能适应目前的Web编程环境以及相关的安全问题——这已经成为目前程
序设计中一个越来越重要的考量。
再来看第三个术语“CLI”,即通用语言基础构造(Common Language
Infrastructure),这是一个支持动态组件编程模型的多层架构。在许多方面,它所表示的对象模型和C++的完全相反。在CLI中,存在一个运
行时软件层(即虚拟执行环境)运行在应用程序和底层操作系统之间,应用程序代码对底层机器的访问会受到相当严格的限制;事实上,CLI根本不允许安全环境
中的代码进行这样的访问。但另一方面,CLI却允许我们对正在运行的程序中的活动类型、以及相关的程序基础构造进行完全的访问,甚至允许我们动态构造额外
的类型和程序基础构造。这些灵活性的获得当然伴随有相当的空间(执行体所占空间)和时间(程序执行效率)代价,但是它却解决了日益增长的基于连接的计算环
境中所面临的问题和需要。
最后,再来看第二个术语,即中间的斜线“/”,它往往为人们所忽略。其表示对C++和CLI的一种绑定
(binding),它正是C++/CLI设计的焦点所在。据此,对于“什么是C++/CLI”这一问题可能的一种答案便是“它是对静态C++对象模型和
动态CLI组件模型的一种绑定”。
对于C++/CLI,一个C++程序员只需要将其添加到她[译注1]已有的编程工具箱中就可以了。要成
为一个C++/CLI程序员,你无需放弃任何已有的东西,虽然你要步入一个新的技术世界,你仍然需要学习它——但愿你能享受这一过程,至少我知道我是这样
的。由此观之,我们还可以将C++/CLI看作是一扇通往另一个世界的大门。
C++/CLI将动态的、基于组件的编程模型和ISO-C+
+集成在了一起,这种集成非常类似于我们当年在Bell实验室对使用模板的泛型编程和当时的C++所做的集成。在两种情况下,你已有的代码投资和编码经验
都将得到保留。这是我们设计C++/CLI时一个基本的需求。
通用语言基础构造(CLI)是一个多层的体系架构,它为所有CLI语言提供
了各种各样的服务。例如CLI中定义了一个通用类型系统(Common Type
System,简称CTS),而各个CLI语言都提供了自己对CTS的一个映射。该类型系统由一个根基类开始被组织为一个完整的类继承体系。实际上,每一
个CLI类型都是一个类——不仅包括像integer、double这样的数值类型,而且也包括字面常量(literal
constant)。每一个CLI类型(或者值)都表示一种Object(所有CLI类型的根基类),比如数值3.14159、比如字符串常量
"Homer Simpson"。
单一的根基类为运行时类型查询和代码生成(通常被称为反射)提供了支持机制[译注2],这是ISO-C++所缺乏的。我们将在今后一系列文章中详细讨论它们给CLI带来的动态编程特性。
除
此之外,CLI还支持一种被称作特性元数据(attribute
metadata)的构造,它允许我们定义一些特性类,然后将其关联在CLI类型和当前正在运行的程序构造上——这有效地扩展了内建于CLI中的类型和程
序构造。这些用户定义的特性也可以通过反射机制来获得,应用程序则可以根据它们的值来进行条件逻辑判断。这也是C++/CLI为C++带来的动态组件编程
的一部分。再次强调一遍,类型反射和特性将在我们的专栏中得到深入的讨论。
|
|
|
|
那么,对于大家来说怎样学习C++/CLI呢?学习C++/CLI的其中一个要点便是学习底层的通用类型系统(CTS),它包括以下三种类型:
1. 多态引用类型,其用于所有的类继承。我们将在早期的一些专栏文章中讨论它们。 2. 非多态值类型,其用于实现一些类似于数值类型那样的、对运行时效率要求比较高的类型。我们将其放在引用类型之后讨论。 3. 抽象接口类型,其用于定义一组供引用类型或者值类型实现的操作。接口为多继承提供了一种别样的设计模式。我们也将有一系列专栏文章来讨论它们。
将CTS映射为一组语言内置类型对于所有的CLI语言都适用,虽然各种语言所使用的语法各不相同。这也是一门CLI语言所要面对的第一个设计层面。例如,在C#中,我们可以用以下代码来定义一个抽象基类型Shape(一些具体的几何对象将继承自它)。 public abstract class Shape {…} 而在C++/CLI中,我们用下面的代码来定义同样的类型。 public ref class Shape abstract {…}; 除了语法差异之外,两种声明的实际表示完全相同。类似地,在C#中,我们可以用下面的代码来定义一个具体类Point2D。 public struct Point2D {…} 而在C++/CLI中,我们用下面的代码来定义同样的类型。 public value class Point2D {…}; 我们对语法的选择基于如下的出发点:以一种直观的设计视角将CLI类型和ISO-C++类型紧密地集成在一起。
因此,简单地说一种语言比另一种语言更接近底层CLI并不正确。相反,每一门CLI语言都只是表达了自己对底层CLI对象模型的一种视图。
学习C++/CLI的第二个要点是学习我们选择直接提供给程序员操作的那些底层CLI元素。例如,CLI为所有语言都提供了垃圾收集服务。一门语言不能选择是否支持垃圾收集,而只能选择如何更好地提供该服务。
在CLI
中,一个引用类型的所有对象都只能被分配在CLI托管堆上。这意味着C++/CLI支持两种动态堆——本地堆(没有任何形式的自动内存回收机制),和
CLI托管堆。对于这两种动态堆,开发人员通常要用某种形式的new操作符来分配对象;如果操作成功,对象在堆中初始位置的地址将被返回。但是两者又有所
区别,这是因为CLI托管堆中对象的位置有可能在垃圾收集器的清除以及随后的压缩中被重新调整。如果一个对象的位置被重新调整,那么CLI运行时中所含的
其中一项服务会透明地更新所有引用该对象的指代品(thingee)。
这就使得我们面临着一种困难的选择:我们是将这些指代品称为指针,并且继续用指针的语法来表示它们呢?还是引入一种新的类似的语法来表示它们需要特殊的处理?我们最后决定采用后者,看下面的代码: N *pn = new N; R ^rn = gcnew R; 这
里,N表示一个本地类型,而R表示一个CLI引用类型,帽子状的符号(^)表示相关的地址是一个托管堆上的追踪句柄(tracking
handle)——也就是说,对象位置的任何重新调整都会被CLI所追踪,相应的句柄也会被透明地更新。其中关键字gcnew在这里被用作与CLI托管堆
打交道的new表达式。
值类型事实上也可以位于托管堆上,虽然这并非必须。当它们作为一个引用类型的成员时,就会出现这种情况。如果我们
允许获取一个引用类型内部成员的地址,那么本地指针也是不合适的,因为这些成员的位置也需要被追踪。一种解决方法是简单地禁止该项功能。这样语言当然会变
得更加简单,但是同时语言也会变得更弱——例如我们将不能通过增长元素的地址值来遍历CLI数组,这是因为CLI数组是一个引用类型,其内的元素都位于托
管堆上。不提供这样的功能意味着CLI数组将不能适用于标准模板库(STL)中的iterator模式以及泛型算法。对于一个C++程序员来说,这是不可
接受的。
支持获取可能位于托管堆中的值类型的地址同样需要引入一种追踪指针,我们称之为追踪内部指针(tracking
interior pointer)。另外,我们还支持追踪引用(tracking
reference)这样的概念——它具有类似本地引用的别名语义,但是它会在必要的时候被CLI透明地更新。最后,我们还支持一种固定指针
(pinning pointer)的概念,它可以在该指针的作用范围内阻止垃圾收集器移动其所引用的对象。
这些新的符号及其表示的复杂的间接类型是在我们对托管堆反复学习和认识之后产生的。面对生存期短暂的托管堆对象,我们需要某种精巧的方式来认识和使用它们,我们相信这些额外的间接类型可以给大家很多帮助。我们将在今后的专栏文章中详细讨论它们。
我
们在此对一门CLI语言所选择的第二个设计层面表示了其对底层CLI实现模型的一层映射。选择什么样的映射取决于该编程语言定位于什么样的程序及程序员模
型。当你选择一门CLI语言进行编程的时候,你实际上也是在选择遵从一种程序员模型。我们对于C++/CLI程序员的定位是那些历练较深的系统程序员,这
些程序员通常所面对的任务是为高层的商业逻辑提供基础性的构造和关键性的应用,这时候她就必须要同时考虑系统的扩展性和性能,因此必须对底层CLI有一个
系统级的视角。
学习C++/CLI的第三个要点是学习那些非CLI本身所直接提供的功能特性。这也是每一门面向CLI的语言所要面对的设计选择,也是各种CLI语言之间相互区分的一种体现。
例
如,CLI本身并不支持多类继承(multiple class
inheritance,简称MCI),而只支持多接口继承和单类继承。但Eiffel语言在设计其面向CLI的实现时就选择了支持源代码级的多类继承。
这需要一种巧妙、甚至是复杂的设计将源代码级的多类继承映射为底层CLI的单类继承模型。Eiffel语言的设计人员认为这种映射对于CLI平台上的
Eiffel程序员是一个利好的元素。
在此C++/CLI的第三个设计层面上,我们没有采用多类继承的方案。其中一个原因是我们不能说服
自己多接口继承模型有任何不够简单或者优雅的地方。我们没有足够的经验来确定哪种方案绝对的优秀,但是我的直觉告诉我多类继承(MCI)是一个死胡同。我
们在此设计层面上的主要关注点在于为那些CLI本身所欠缺的地方提供一些额外的解决方案,我们主要集中在以下三个方面: 1. 为某些CLI要求手动干预的地方提供一种自动化的解决方案,例如确定性终止化操作(deterministic finalization)和稀有资源释放。 2. 提供一些特殊的类成员函数——例如拷贝构造器和拷贝赋值操作符,以及在CLI直接支持的操作符的基础上再为一些操作符提供一些扩展支持——例如用来支持函数对象(function object)设计模式的调用操作符“()”。 3. 提供一种静态的参数化机制来支持设计适用于CLI类型的标准模板库(STL),这是因为CLI中的泛型机制在我们来看对于当代的参数化设计是不够的——虽然我们也支持它们。
以上几点在我们的系列专栏中都将有相关的讨论。特别地,我们将会详细阐释C++/CLI中的模板和泛型机制。
C+
+/CLI的第四个设计层面在于它选择了“集成”而非“替换”的策略,这是C++以及一些语言所独有的,而其他一些语言则没有这样做,例如Visual
Basic采取的就是“替换”的策略。一个合法的C++程序是可以顺利通过C++/CLI编译,并且可以正常运行的。我们认为这对于我们的程序员是一项基
本的需求。
谈到C++/CLI的第四个设计层面,这究竟是什么意思呢?它表示我们对C++/CLI语言规范和ISO-C++所做的深入的
集成。例如,除了我们扩展支持集合使其也适用于统一的CLI类型系统,表达式评估的标准转换集合与重载函数的辨析都和ISO-C++的相同。当我们引入模
板和多继承机制时,我们也应用了同样的扩展策略。这些都是在语言中稍显抽象的部分,在某种程度上我们已经使它们的行为变得更加直观,免除了程序员深入算法
细节的需要。但我们仍会在系列专栏中花费笔墨关注一些主要的变化,例如对字面常量(literal)字符串的处理。
在C++/CLI未来
的版本中,我们希望为本地类型和CLI类型提供更为无缝的集成。在目前的实现中,仍然存在许多不能跨越的壁垒。例如,我们现在还不能直接在一个CLI类中
声明一个本地类的实例对象;相反,我们必须声明一个指向那个本地对象的指针,然后在CLI类的构造器/析构器对中处理对它的内存分配与释放。我们希望将来
能够透明地处理它们。类似地,如果可以方便地编写下面的代码就更好了: N^ n = gcnew N; R* pn = new R; 即将一个本地类透明地放在垃圾收集控制的托管堆中,以及将一个CLI引用类型透明地放在本地堆中,并使它们正常运行。这些是我们对于C++/CLI未来的一些设想和愿景。随着这些设想的实现,我们也会在我们的专栏中讨论它们。
|
|
|
|
最
后,再回答一个大家经常问到的一个问题,“我为什么要学习C++/CLI”?首要的原因是C++/CLI将会为你进入CLI所表示的动态组件编程模型领域
提供一张第一等的入口签证。如果你像我一样认为这将成为越来越重要的一种编程模型,并且如果你是一个历练较深的程序员,那么C++/CLI就是你想要的一
个语言工具。如果你不喜欢某些地方,或者发现某些东西很难表达,那么请告诉我们。我们代表着一个动态编程社区,C++/CLI也会持续不断地前进。
在C
++/CLI之前,如果我们希望或者需要在CLI所表示的动态编程领域工作,那么我们只能放弃使用C++[译注3],这意味着我们同时放弃了我们现存的代
码库和编码经验。有了C++/CLI之后,我们就拥有了一条沿着C++向上的移植路径。这是学习C++/CLI的第一个原因。
学习C++
/CLI的第二个原因在于它允许我们访问整个CLI框架类库,包括用户界面,线程,网络,XML,ADO.NET,ASP.NET,以及Web服务这个宽
广诱人的世界。另外,在即将推出的WinFX中,一个封装了整个操作系统的类库体系(包括应用程序及其执行空间[译注4])也会被收编在CLI门下。 |
http://www.bokebb.com/dev/cn/c/c++/20057185604_4148439.shtml