转换指南 —— 从 ISO-C++ 到 C++/CLI v1.0
3. 新的指针 —— 跟踪句柄,内部指针,图钉指针... 7
1. CLI关键字
| 关键字 | 说明 |
CLR Data Type Keywords | ref class ref struct | Defines a CLR reference class 定义一个CLR引用类型 |
| value class value struct | Defines a CLR value class 定义一个CLR值类型 |
| interface class interface struct | Defines a CLR interface 定义一个CLR接口类型 |
| enum class enum struct | Defines a CLR enumeration 定义一个CLR枚举类型 |
| property | Defines a CLR property 定义一个CLR属性 |
| delegate | Defines a CLR delegate 定义一个CLR委托 |
| event | Defines a CLR event 定义一个CLR事件 |
|
|
|
Override Specifiers | abstract | Indicates functions or classes are abstract 声明这个函数或者类是抽象的(不能实例化) |
| new | Indicates that a function is not an override of a base class version 声明该函数不是基类版本的复写 |
| override | Indicates that a method must be an override of a base-class version 声明该函数是基类版本的复写(在修饰虚函数时,override和new两者必须选择其一) |
| sealed | Prevents classes from being used as base classes 声明该类不能被继承 |
|
|
|
Keywords for Generics | generic | Defines a generic type 定义一个泛型 |
| where | Specifies the constraints of a generic typef 规定泛型的类型限制 |
|
|
|
Miscellaneous New Keywords | finally | Indicates default exception handlings behavior 声明一个缺省异常处理块 |
| for each | Enumerate elements of a collection. 枚举集合类内的元素 |
| gcnew | Allocates types on the garbage-collected heap 在托管堆内为类型分配存储空间 |
| initonly | Indicates a member can only be initialized at declaration or in a static constructor 指出一个声明同时初始化或者在类静态构造函数中初始化的类成员 |
| literal | Creates a literal variable 创建一个文字变量 |
| nullptr | Indicates that a handle or pointer does not point at an object 用来标识一个空指针或者空句柄 |
|
|
|
Non-Keyword Language Constructs | array | Type for representing CLR arrays CLR数组 |
| interior_ptr | Points to data inside reference types. 内部指针,用于获得引用类别内部的数据成员的地址 |
| pin_ptr | Points to CLR reference types to temporarily suppress the garbage collection system 图钉指针,用于锁住CLR引用类别对象的地址,防止垃圾收集器回收或者搬移 |
| safe_cast | Determines and executes the optimal casting method for CLR types 用于CLR类型的类型转换 |
| typeid | Retrieves a System.Type object describing the given type or object 获得一个CLR类型的类型信息 |
|
|
|
New C++ Operators | ^ | Indicates a handle to an object located on the garbage-collected heap 一个跟踪句柄的声明,跟踪句柄是一个指向分配在托管堆内的CLR类型对象的指针 |
| % | Indicates a tracking reference 用来标识一个跟踪引用 |
C++ 的内存布局分为两大块,堆和栈,一个C++ 类型的变量不是分配在堆上(动态内存分配)就是分配在栈上(静态内存分配),下面是一段示例代码和相关的内存分配布局图。
class NativeObject
{
public:
int _x;
};
//
NativeObject no;
no._x = 100;
NativeObject* pno = new NativeObject();
pno->_x = 101;
FIG 1 C++ 程序的内存布局
而C++/CLI编写的托管应用程序,CLR管理的内存布局除了原来的堆(有专门的术语Native Heap 原生堆来称呼它)和栈外,还另外增加了一个托管堆(CLR Heap or Managed Heap)。CLR引用类型对象会被分配在托管堆上,无论是使用动态分配方式还是静态分配方式(当使用静态分配方式的时候,CLR引用类型对象有着栈对象一样的语义行为,但是实际的内存分配是在托管堆而不是在栈上),而CLR值类型对象在使用动态分配方式的时候会被分配在托管堆上,而使用静态分配方式的时候会被分配在栈上(CLR值类型跟ISO-C++的类型,在分配方式和语义上都十分类似,而CLR引用类型则有较大的差别)。在托管堆里的对象,它们由垃圾收集器管理,被垃圾收集器回收或者搬移。
ref class ClrRefObject
{
public:
int _x;
};
value class ClrValueObject
{
public:
int _x;
};
ClrRefObject cro;
cro._x = 100;
ClrRefObject^ hcro = gcnew ClrRefObject();
hcro->_x = 101;
ClrValueObject cvo;
cvo._x = 102;
ClrValueObject^ hcvo = gcnew ClrValueObject();
hcvo->_x = 103;
FIG2 CLI程序的内存布局
CLR类型对象的内存分配方式使得CLI需要提供新的指针语法与ISO-C++指针区别开来。实际上在CLI里面,所提供的有着近似指针行为,用来操作对象的东西,正式的名称叫做跟踪句柄(Tracking Handle)。更接近ISO-C++指针的等同物是interior_ptr和pin_ptr。
3.1 跟踪句柄的语义
跟踪句柄的名称也说明它与传统的指针有着很大的区别,严格的说它是一个受控的更安全的对象标识符,它只具备访问CLR类型对象成员的指针语义,其它的传统指针语义都被摒弃了(C/C++里面的指针具备非常复杂的语义行为,使得编译器无法对它进行严格的检查,也是导致很多“内存非法访问”问题的原因):
a) 在CLI中,跟踪句柄只能指向一个CLR类型的整对象,它不能指向对象中的某个成员。
b) 跟踪句柄的值是无意义的,这意味着所有的指针算术对跟踪句柄来说都是非法的(加法,减法,大小比较等等),因为跟踪句柄的指向就只能是一个CLR类型对象,当这个在托管堆内的对象被垃圾收集器搬移的时候,CLR会自动修改跟踪句柄的值。(在传统的指针使用中,通常我们用指针加法来遍历一个数组,但是在CLI中,数组本身就是一个对象,跟踪句柄只能指向一个完整的数组对象而不能指向其中的元素)
c) 跟踪句柄除了向上转型(造型成父类类型的跟踪句柄),不能像指针一样被造型成其它类型。
3.2 跟踪句柄的声明和绑定
1 ClrRefObject^ hcro = gcnew ClrRefObject();
2 hcro->_x = 101;
3 ClrRefObject cro;
4 cro._x = 100;
5 hcro = %cro;
6 hcro = nullptr;
使用语法 T^ 声明一个类型为T的跟踪句柄。
示例代码第一句使用gcnew 关键字在托管堆上动态分配一个CLR类型对象,并把该对象绑定到一个同类型的跟踪句柄上。(这里使用的术语是绑定binding而不是赋值,当一个跟踪句柄被程序指向另外一个对象的时候,使用的术语是重绑定rebinding,这是为了强调跟踪句柄的值没有地址的语义,跟踪句柄起的是一个对象标识符的作用)
示例代码的第二句使用 ”->” 作为对象成员的访问符,这跟传统指针是一样的,这也是跟踪句柄唯一具备的指针语义。(使用”*”作为解引用dereference操作符,这跟ISO-C++也是一样的)
示例代码的第三句定义了类型ClrRefObject的一个栈对象cro 。
示例代码的第五句使用 ”%” 操作符来获得栈对象cro 的跟踪句柄,并用来重绑定hcro 。
示例代码的第六句把跟踪句柄hcro 设为一个空句柄,不指向任何对象,空句柄有了新的关键字nullptr ,而不是传统指针的 ” 0 ” 。
3.3 interior_ptr 内部指针 pin_ptr 图钉指针
在CLI里面,跟ISO-C++中指针的等同物是内部指针和图钉指针,其中图钉指针能够钉住在托管堆内的对象,防止垃圾收集器搬移对象,它是CLI托管代码和ISO-C++原生代码之间交互的重要桥梁。
// interior_ptr.cpp
// compile with: /clr
using namespace System;
ref class MyClass {
public:
int data;
};
int main() {
MyClass ^ h_MyClass = gcnew MyClass;
h_MyClass->data = 1;
Console::WriteLine(h_MyClass->data);
interior_ptr<int> p = &(h_MyClass->data);
*p = 2;
Console::WriteLine(h_MyClass->data);
// alternatively
interior_ptr<MyClass ^> p2 = &h_MyClass;
(*p2)->data = 3;
Console::WriteLine((*p2)->data);
}
上面是内部指针的一个示例程序,内部指针使用了CLI的动态泛型语法,interior_ptr<T> 用于声明一个指向类型T对象的指针。T可以是普通类型,也可以是一个跟踪句柄类型。使用”*” 操作符来对内部指针进行解引用。内部指针主要用来获得对象内部成员的地址,数组内元素的地址等等。
内部指针具备与传统指针几乎完全相同的语义,可以用来访问对象成员,进行指针算术等等。
因为托管堆内的对象可能会被垃圾收集器搬移,当原生代码通过指针访问托管堆内的某对象的数据的时候,必然要求该对象的地址是稳定的,使用图钉指针能够告诉CLR,该对象被锁定,禁止垃圾收集器对它进行搬移。
// pin_ptr_1.cpp
// compile with: /clr
using namespace System;
#define SIZE 10
#pragma unmanaged
// native function that initializes an array
void native_function(int* p) {
for(int i = 0 ; i < 10 ; i++)
p[i] = i;
}
#pragma managed
public ref class A {
private:
array<int>^ arr; // CLR integer array
public:
A() {
arr = gcnew array<int>(SIZE);
}
void load() {
pin_ptr<int> p = &arr[0]; // pin pointer to first element in arr
int* np = p; // pointer to the first element in arr
native_function(np); // pass pointer to native function
}
int sum() {
int total = 0;
for (int i = 0 ; i < SIZE ; i++)
total += arr[i];
return total;
}
};
int main() {
A^ a = gcnew A;
a->load(); // initialize managed array using the native function
Console::WriteLine(a->sum());
}
上面的示例演示了怎么用图钉指针获得数组对象里面元素的地址,再将该指针传递给原生代码,原生代码通过这个指针来遍历CLI数组。图钉指针和内部指针到传统指针之间的转型是由CLR来自动完成的,不需要任何显示转型的语法。
图钉指针的声明语法与内部指针基本一样,pin_ptr<T> 用于声明一个指向类型T对象的图钉指针。
4. CLI类型
4.1 CLI引用类型(reference type)
一个示例程序,供后面引用参考:
// stack_semantics_for_reference_types.cpp
// compile with: /clr
ref class R {
public:
int i;
R(){}
// assignment operator
void operator=(R% r) {
i = r.i;
}
// copy constructor
R(R% r) : i(r.i) {}
};
void Test(R r) {} // requires copy constructor
int main() {
01 R r1;
02 r1.i = 98;
03 R r2(r1); // requires copy constructor
04 System::Console::WriteLine(r1.i);
05 System::Console::WriteLine(r2.i);
// use % unary operator to convert instance using stack semantics
// to its underlying handle
06 R ^ r3 = %r1;
07 System::Console::WriteLine(r3->i);
08 Test(r1);
09 R r4;
10 R r5;
11 r5.i = 13;
12 r4 = r5; // requires a user-defined assignment operator
13 System::Console::WriteLine(r4.i);
// initialize tracking reference
14 R % r6 = r4;
15 System::Console::WriteLine(r6.i);
}
Output
98
98
98
13
13
4.1.1 引用类型声明
使用关键字ref class和ref struct可以声明一个CLI引用类型,struct和class之间的区别跟ISO-C++里面是一样的,struct里面的成员默认的访问权限是public而class是private,后面提到引用类型,如无明确说明都是指ref class。
示例程序声明了一个引用类型R,并且为它实现了一个拷贝构造函数和赋值操作符,跟ISO-C++不一样,编译器不会为引用类型生成缺省的拷贝构造函数和赋值操作符,如果你需要,就必须自己实现。
4.1.2 引用类型实例的栈语义
引用类型的实例是一定会被分配在托管堆上,无论是不是使用gcnew的动态分配语法,但是CLI为引用类型提供了栈语义的实现,使得使用静态分配语法分配的引用类型实例,行为表现上就像一个栈对象一样,它的可见范围和生存期都被限制在定义的范围内。示例程序[01] 为引用类型R分配了一个栈对象r1,虽然r1的实际存储空间是分配在托管堆上,但是我们可以用栈对象语法来使用它,编译器会替我们自动生成额外的代码。
对引用类型的栈对象,我们可以用 ”%” 前缀操作符来取得它的跟踪句柄(示例程序[06] ,注意这里跟ISO-C++之间的区别,用 ”&” 前缀操作符是取得它的类型的内部指针interior_ptr<T>);使用 ” . ” 操作符进行对象成员的访问(示例程序[02],这跟ISO-C++是一样的)。
请参考:
第二章和第三章
4.1.3 确定性资源清理 deterministic release of unmanaged resources
如果是一个动态分配的ISO-C++类型实例(使用new关键字),我们必须在它不再需要的时候调用delete,delete会先调用实例的析构函数用于释放它占用的资源(分配的缓冲区内存,文件句柄,网络链接,数据库链接等等),然后再回收它所占用的内存空间,如果不这么做就会造成内存泄漏或者资源泄漏。
动态分配的CLI引用类型实例(使用gcnew关键字),它位于托管堆上,由垃圾收集器管理,当它不再被引用的时候,垃圾收集器会在某个时间将它回收,在回收它的内存之前,会先调用该实例的终止器Finalizer。我们可以在引用类型的终止器中释放该实例占用的资源,来保证资源被正确释放。
但是问题是,垃圾收集器何时回收一个废弃的实例是不确定的,这就是所谓的不确定性资源清理,对于内存资源,我们并不太在意它长期被废弃实例所占用,毕竟内存资源比较充裕,而且当内存比较紧张时,垃圾收集器会自动回收废弃的实例对象。但是对于其它的资源,长期不必要的占用会导致资源紧张甚至程序死锁,不确定的资源清理对于它们来说是不可行的,它们的释放必需由程序员来完全控制,这也是CLI引入确定性资源清理的原因。
所以CLI在提供了终止器的同时仍然提供了析构函数。当一个动态分配的CLI引用类型实例被delete,或者静态分配实例(栈对象)超出定义范围的时候,该实例的析构函数会被调用,用于确定性地清理它所占用的除内存外的其它资源。至于它的终止器何时被调用,占用的内存何时被回收,这仍然由垃圾收集器来决定。
下面的示例代码声明了一个引用类型T,并且实现了一个析构函数和一个终止器,其中析构函数显式调用了终止器:
// C++ code
ref class T {
~T() { this->!T(); } // destructor calls finalizer
!T() {} // finalizer
};
4.2 CLI值类型(value type)
值类型是为了避免额外占用空间和快速数据访问而设计的,它包括像int,double这样的基本数据类型,interior_ptr、pin_ptr、枚举等CLI系统定义值类型和用户使用value class或者value struct自定义的值类型,用户自定义的值类型通常都是像Point、Rectangle、Complex Number这样拥有少量成员变量和简单接口的,强调值语义的类型。
4.2.1 CLI值类型的特性
既然值类型是为了空间和效率而设计的,那么它必然有许多自身的特性:
u 值类型不能参与继承体系,也就是说它不能继承别的类(可以实现接口),也不能被别的类继承,也没有多态的能力。因为参与继承体系,具备多态能力的类必然有额外的空间开销,如虚表等。而值类型的空间占用就只要本身定义的那些成员变量的大小总和。(由于字节对齐的原因,真正的大小可能要比变量大小总和要大,CLI编译器能够对此进行优化,保证最优的变量排列顺序来得到最小的内存占用)
u 值类型不能自定义无参的构造函数,而是由编译器自动为它生成一个,编译器生成的无参构造函数的行为是,属于值类型的成员变量,设为它的默认值(向下递归),属于跟踪句柄类型的成员变量被设为nullptr。
u this关键字在引用类型T中,是一个指向自己本身的跟踪句柄(T^),而在值类型T中,是一个指向自己本身的内部指针(interior_ptr<T>)。
u 值类型不允许拥有析构函数和终止器,因为值类型代表着简单数据对象,它不需要也不允许分配和占用外部资源。
u 如果值类型里面的成员变量不需要被垃圾收集器跟踪,那么它就是一个简单值类型(Sample Value Type),换句话说,简单值类型的成员变量是由基本类型(int,double等)、枚举、指针和其它简单值类型组成。简单值类型可以用于跟原生代码交互。
u 值类型不允许自定义拷贝构造函数,它的复制语义是由编译器实现的按位拷贝的语义。类似的,赋值操作符也是一样的。
4.2.2 值类型的装箱boxing和拆箱unboxing
单独的值类型实例分配是不在托管堆内的,它只分配在栈上(这里排除了一个引用类型包含多个值类型成员变量的情况,比如像CLI数组)。
value class ClrValueObject
{
public:
int _x;
};
1 ClrValueObject^ hcvo = gcnew ClrValueObject();
2 hcvo->_x = 100;
3 ClrValueObject cvo = *hcvo;
4 cvo._x = 101;
但是像上面这样的示例代码,一个值类型被gcnew动态分配,返回一个跟踪句柄,这个动态分配出来对象难道不是在托管堆内吗?实际上在这后面是编译器自动生成的装箱和拆箱的动作。
术语装箱的解释是:将任意一个值类型V的实例赋给一个跟踪句柄类型V^ 的变量所发生的显示或者隐式的转换,在这个转换过程中,CLR在托管堆内分配一个值类型V的箱子,并且把该实例的值拷贝到箱子里面。值类型V的箱子可以看作是V的外包裹引用类型,它继承自System::Object 。装箱使得一个值类型在需要的时候可以表现出引用类型的语义,而且这一切是由编译器自动实现的,对程序员本身是透明的。
在示例代码的第一句中,编译器在栈上生成一个值类型ClrValueObject 的匿名实例,然后装箱该匿名实例,把装箱后的箱子的跟踪句柄绑定到hcvo 上。
拆箱刚好相反,当一个箱子的跟踪句柄被解引用后,用来初始化一个栈上的值类型实例,这时候,就需要对箱子进行拆箱的动作。同样的,拆箱也是由编译器自动实现的。示例代码第三句就发生了一次拆箱的动作。
装箱和拆箱虽然对程序员是透明的,但是它们本身确实占用了一定的内存和CPU开销,频繁的进行装箱和拆箱对程序的性能是相当不利的。
4.3 接口类型 Interface
CLI里面的类继承体系结构与Java一样,只支持单继承,但是可以实现多个接口,而不像ISO-C++一样支持多重继承。一个接口定义了一个契约(contract),而实现该接口的类则履行了该契约,它实现了该契约所要求的功能,可以被需要该契约的功能的对象所使用。
比面向对象编程更进一步,在一些适当的场合中使用面向接口编程(Interface-Orient Programming)可以进一步减少对象之间的相互依赖。虽然ISO-C++没有显式支持接口类型,不过在其中一个约略的等同物是纯虚类。
4.3.1 接口的定义和继承体系
在CLI中,使用interface class或者interface struct关键字定义一个接口类型,这两者是完全一样的,因为接口里面所有成员的访问权限缺省都是public,并且不允许更改。一个接口可以从另外的接口继承,它的成员包括自身定义的成员、直接基接口(explicit base interfaces)定义的成员和直接基接口的基接口的成员(向下递归)。而实现该接口的类必须实现所以上述的接口成员(履行契约)。
4.3.2 接口成员
接口的成员可以是静态成员变量,静态或者实例成员方法,静态构造函数,静态或者实例属性(properties),静态或者实例事件(events),操作符成员方法或者其它的嵌套类型。它不能包括以下成员:实例成员变量,实例构造函数和终止器。
接口所有成员的缺省访问权限都是public,并且不允许更改。接口的所有实例成员缺省都是抽象的,并且不允许更改。
总而言之,接口不能实例化,它不拥有真正的实例成员变量,而所有的实例成员(这里包括函数,属性和事件)都是契约的组成部分,它们必须交由接口的实现类来实现。
using namespace System;
public delegate void ClickEventHandler(int, double);
// define interface with nested interface
public interface class Interface_A {
void Function_1();
};
};
// interface with a base interface
public interface class Interface_B : Interface_A {
property int Property_Block;
event ClickEventHandler^ OnClick;
static void Function_3() { Console::WriteLine("in Function_3"); }
};
// implement interface and base interface
public ref class MyClass2 : public Interface_B {
private:
int MyInt;
public:
// implement non-static function
virtual void Function_1() { Console::WriteLine("in Function_1"); }
// implement property
property int Property_Block {
virtual int get() { return MyInt; }
virtual void set(int value) { MyInt = value; }
}
// implement event
virtual event ClickEventHandler^ OnClick;
void FireEvents() {
OnClick(7, 3.14159);
}
};
在上面的示例代码中,接口Interface_A 包含一个实例方法Function_1 ,接口Interface_B 继承至接口Interface_A ,它包含一个属性Property_Block 、一个事件OnClick 和一个静态成员方法Function_3 。而接口Interface_B 的实现类MyClass2 必须实现Interface_B所有的实例成员,其中包括接口Interface_B 自身的实例成员和Interface_B 的基接口Interface_A 的实例成员。
4.4 CLI枚举类型 enum
除了ISO-C++的枚举类型外(Native enum),CLI还提供了另外的一种枚举类型(CLI enum)。两者的区别在于:Native enum在ISO-C++并不是第一级类型(first class type),它实际上是整形的一个子集,它可以由编译器隐式地转换到整型,而CLI enum在CLI类型系统中是处于一等公民的位置,编译器把它当作一个真正的类型看待,从CLI enum到整型的转换必须使用显示转换;另外一个区别是Native enum里面的项次的可见范围是在enum定义之外,这也是造成名字污染的一个原因,而CLI enum的项次的可见范围是在enum定义之内的。
使用关键字enum class或者enum struct声明一个CLI enum,两者没有任何区别。
下面的代码演示了定义一个CLI枚举类型m,在没有显式转换成int类型的时候,WriteLine方法实际上是先将类型m的变量mym装箱后再调用从System::Object继承过来的ToString方法,输出的是名称字符串,而经过显式类型转换的mym,输出的是它的值。
// managed enum
public enum class m { a, b };
int main() {
// consume managed enum
m mym = m::b;
System::Console::WriteLine("no automatic conversion to int: {0}", mym);
System::Console::WriteLine("convert to int: {0}", (int)mym);
}
Output
no automatic conversion to int: b
convert to int: 1
4.5 委托类型 delegate
委托的作用类似于ISO-C++里面的成员函数指针,用于实现回调(Callback)机制。但是在ISO-C++里面,成员函数指针有着过强的类型检查,使得它难于使用。一些解决方案包括使用union来绕过类型检查,像MFC的消息映射,或者专门实现一个有类似行为的类型。但是前者扩展性差,也有一定的危险性,而后者使用麻烦,缺乏编译器专门的支持,时间和空间上的开销都比较大。在CLI里面,由于使用delegate关键字有编译器的专门支持,不但使用上很方便,开销小,效率高,而且功能也得以增强。委托语法是.Net Framework Library 消息处理机制的基础。
4.5.1 委托的声明和使用
using namespace System;
// declare a delegate
1 public delegate void MyDel(int i);
ref class A {
public:
void func1(int i) {
Console::WriteLine("in func1 {0}", i);
}
void func2(int i) {
Console::WriteLine("in func2 {0}", i);
}
static void func3(int i) {
Console::WriteLine("in static func3 {0}", i);
}
};
int main () {
A ^ a = gcnew A;
// declare a delegate instance
2 MyDel^ DelInst;
// test if delegate is initialized
if (DelInst)
DelInst(7);
// assigning to delegate
3 DelInst = gcnew MyDel(a, &A::func1);
// invoke delegate
if (DelInst)
4 DelInst(8);
// add a function
5 DelInst += gcnew MyDel(a, &A::func2);
6 DelInst(9);
// remove a function
7 DelInst -= gcnew MyDel(a, &A::func1);
// invoke delegate with Invoke
DelInst->Invoke(10);
// make delegate to static function
8 MyDel ^ StaticDelInst = gcnew MyDel(&A::func3);
StaticDelInst(11);
}
Output
in func1 8
in func1 9
in func2 9
in func2 10
in static func3 11
delegate关键字用于定义一个委托类型:
示例代码(1)定义了一个MyDel的委托类型,它可以接受的函数的签名是void MethodName(int);
示例代码(2)定义了一个MyDel类型的跟踪句柄变量DelInst,DelInst当前没有指向任何实例对象;
示例代码(3)实例化一个MyDel实例并把它的跟踪句柄绑定到DelInst上,该实例有一个可调用实体,这个实体指向一个实例方法,所以需要两个参数 —— 被调用实例的跟踪句柄和该实例方法的地址;
示例代码(4)调用DelInst,因为DelInst指向的实例的呼叫列表只包含一个可调用实体,该实体指向对象a的func1方法,所以a->func1(8)被调用;
示例代码(5)使用 ” += ” 操作符生成一个新的MyDel类型实例,该实例包括两个调用实体,并且跟踪句柄DelInst被重绑定到这个新的实例上;
示例代码(6)再次调用DelInst,此时,对象a的func1方法和func2方法都被调用,调用参数为9;
示例代码(7)使用 ” -= ” 操作符去掉可调用实体(a->func1);
示例代码(8)实例化一个MyDel实例并绑定到跟踪句柄StaticDelInst上,因为该实例包含的可调用实体指向一个静态方法,所以不需要调用实例,只需要该静态方法的地址。
4.6 属性和事件 property and event
属性和事件,它们最简单的用法看起来就像是声明一个成员变量,使用中更是表现成一个成员变量,但实际上它们声明了一组预定义的函数,程序员可以选择不自己定义而使用编译器自动生成的缺省版本,也可以自己重新定义以提供定制的行为和受控的访问策略。
4.6.1 属性
在面向对象程序设计中,为了减少对象间的依赖关系,我们强调类设计的封装原则,类的成员变量应该是私有的。但是反之我们不得不为大多数的成员变量写一对get和set方法,它们统称为访问函数(accessor)。这样导致类的编写者耗费很多时间在重复劳动上面,而类的使用者用起来也不太方便。在CLI里面提供属性语法的支持,使得一个属性使用上像一个成员变量,但是内部连接着一对(或者一个,属性可以表现为只读,只写,可读写)编译器缺省提供或者程序员自定义的访问函数。
using namespace System;
public ref class C {
int MyInt;
public:
// property data member
1 property String ^ Simple_Property;
// property block
2 property int Property_Block {
int get();
void set(int value) {
MyInt = value;
}
}
};
int C::Property_Block::get() {
return MyInt;
}
int main() {
C ^ MyC = gcnew C();
3 MyC->Simple_Property = "test";
4 Console::WriteLine(MyC->Simple_Property);
5 MyC->Property_Block = 21;
6 Console::WriteLine(MyC->Property_Block);
}
Output
test
21
示例代码(1)声明一个名为Simple_Property 的属性成员变量,编译器会自动生成一个内部成员变量和一对访问函数。当Simple_Property 为类的客户端读写的时候,这对访问函数会被自动调用来读取或者设置该内部成员变量的值(示例代码 3,4)。
示例代码(2)声明了一个名为Property_Block的属性块,程序员自己为成员变量MyInt定义了一对访问函数。当Property_Block 为类的客户端读写的时候,这对访问函数会被自动调用来读取或者设置MyInt的值(示例代码 5,6)。
4.6.2 事件
跟属性类似,事件也是用来简化类内部委托类型变量的定义和使用。一个带有event前缀的委托类型的成员变量声明,实际上是包括了add,remove和rise这样一组的事件访问函数,它们被用来为事件添加事件处理器(event handler 实际上就是一个委托类型的实例),为事件移除事件处理器和激发事件。
同样类似的,我们可以自己使用编译器生成的缺省访问函数版本,也可以自己重新定义以实现定制的行为和访问策略。事件被外部代码使用的时候,行为表现上就像一个委托类型的成员变量。
using namespace System;
// declare delegates
delegate void ClickEventHandler(int, double);
delegate void DblClickEventHandler(String^);
// class that defines events
ref class EventSource {
public:
1 event ClickEventHandler^ OnClick; // declare the event OnClick
2 event DblClickEventHandler^ OnDblClick; // declare OnDblClick
void FireEvents() {
// raises events
OnClick(7, 3.14159);
OnDblClick("Hello");
}
};
// class that defines methods that will called when event occurs
ref class EventReceiver {
public:
void OnMyClick(int i, double d) {
Console::WriteLine("OnClick: {0}, {1}", i, d);
}
void OnMyDblClick(String^ str) {
Console::WriteLine("OnDblClick: {0}", str);
}
};
int main() {
EventSource ^ MyEventSource = gcnew EventSource();
EventReceiver^ MyEventReceiver = gcnew EventReceiver();
// hook handler to event
3 MyEventSource->OnClick += gcnew ClickEventHandler(MyEventReceiver, &EventReceiver::OnMyClick);
4 MyEventSource->OnDblClick += gcnew DblClickEventHandler(MyEventReceiver, &EventReceiver::OnMyDblClick);
// invoke events
5 MyEventSource->FireEvents();
// unhook handler to event
6 MyEventSource->OnClick -= gcnew ClickEventHandler(MyEventReceiver, &EventReceiver::OnMyClick);
7 MyEventSource->OnDblClick -= gcnew DblClickEventHandler(MyEventReceiver, &EventReceiver::OnMyDblClick);
}
Output
OnClick: 7, 3.14159
OnDblClick: Hello
示例代码(1,2)为引用类EventSource定义两个事件,实际的委托变量和访问函数有编译器自动生成;示例代码(3,4)使用“ += ”操作符为事件添加新的事件处理器(add方法被隐式调用);示例代码(5)激发事件(raise方法被调用);示例代码(6,7)使用“ -= ”操作符移除事件处理器(remove方法被调用)。
5. CLI运行时的类型操纵 —— 反射和特性
CLI跟ISO-C++之间的最大区别是在运行时的类型操纵能力上面。
ISO-C++是静态强类型的语言,它的类型检查和类型信息获取都是在编译期发生的,当程序被编译成机器码后,所有的类型信息都不复存在,只剩下可执行的机器指令。当然,通过特殊的虚函数机制和从运行时库获得的RTTI支持(运行时类型信息),ISO-C++仍然能获得有限的动态类型能力,但这是建立在程序员本身书写跟类型操纵相关的代码上(通常这是一件很烦琐,重复的而且扩展性差的劳动,一些C++的序列化库就不得不用大量的类型标记来额外记录类型信息),而不是由语言本身支持,并且这些能力也是十分有限的。
CLI跟ISO-C++不同,它是一门解释型的语言,编译后的中间代码由虚拟机来执行(在.Net里面是叫做VES虚拟执行系统),而不是编译成机器指令由CPU直接执行。CLI的代码被编译器编译的时候,不但生成中间代码,并且还生成代码的类型信息数据,这些数据被称作为元数据(Metadata)。元数据被看作是“数据的数据”,你可以想象这些元数据就像你设计程序之前画的类体系结构图,别的程序员通过看你画的类图来了解你的程序的类型结构,而VES则通过解析元数据来获得中间代码的类型结构信息。总而言之, CLI,通过由VES支持的类型操纵机制,在运行时仍然可以获得完整的类型信息。
CLR的类型操纵机制被称为反射(Reflection),它提供了以下的运行时类型操纵的能力:
l 动态发现类型(可以枚举程序集中的所有类型),查询类的类型信息,类成员方法和成员变量信息。
l 动态生成类的实例和调用类成员方法。
l 动态创建一个新类型。
l 通过对特性(Attributes)的支持,允许程序员为类型附加自定义的额外信息,然后在运行时获得,得到一种运行时环境感知的能力。
下面的内容不涉及后面两项,因为后两者相对较复杂,也较少使用。
5.1 名词解释
Assembly 程序集:
由CLI编译器编译后产生的模块,除了包括中间代码外,还包括中间代码定义的类型信息和程序资源(字符串,图标,位图等等)。另外还包括其它程序集的引用。程序集通常以DLL的形式出现,但是链接一个程序集跟链接一个普通的DLL不一样,只需要加载程序集的DLL文件即可,而隐式链接普通DLL需要头文件和导入库,显示链接也需要头文件。
Application 应用程序:
应用程序也是一个程序集,并且是一个带有一个进入点函数,可以执行的程序集。
Application domain 程序域:
当一个CLI程序被VES执行的时候,VES为它产生一个程序域,它就像该程序运行实例的一个容器,包括了程序运行状态,该程序的类型系统信息,程序的运行权限等等。一个程序的不同运行实例被视为不同的程序域,一个类型被加载到不同程序域里面也被视为是不同的类型,而该类型产生的实例对象也不能直接在不同的程序域之间共享。
5.2 动态类型发现
动态类型发现能够从程序集中搜索到你想要发现的类型。
比如说你需要获得某个模块(DLL)里面实现某个特定接口的所有类型(前面说过,接口代表着某个功能性的契约,一个实现了某接口的类型代表了它具备了履行该契约的能力),然后选择其中的一个获得它的特定的能力。
在ISO-C++里面,你可能会采用原型prototype模式,在程序初始化的时候,把这些类型的一个原型实例注册到自己定义的一张表里,而在运行时对这张原型表进行搜索,然后通过原型产生另外的实例。相当复杂的工作,程序员不得不编写大量复杂的代码来实现,而且扩展性也不佳。
而在CLI里面,因为有了类型元数据,这些事情VES能够帮你自动完成。VES通过解析程序集里面的元数据,能够建立起一张完整的类型结构体系图。你不再需要建立什么类型原型表,VES能够在运行时告诉你所需要的所有类型信息。
下面的示例代码比较简单,它演示了加载一个程序集(1);获得程序集里面的所有类型(2);选择类型集合里面的第一个类型(2);获得该类型里面一个叫Method1的方法信息(2);获得Metho1方法的参数信息(3);输出这些参数信息(4,5,6,7)。
Assembly^ SampleAssembly;
1 SampleAssembly = Assembly::LoadFrom( "c://Sample.Assembly.dll" );
// Obtain a reference to a method known to exist in assembly.
2 MethodInfo^ Method = SampleAssembly->GetTypes()[ 0 ]->GetMethod( "Method1" );
// Obtain a reference to the parameters collection of the MethodInfo instance.
3 array<ParameterInfo^>^ Params = Method->GetParameters();
// Display information about method parameters.
// Param = sParam1
// Type = System::String
// Position = 0
// Optional=False
for each ( ParameterInfo^ Param in Params )
{
4 Console::WriteLine( "Param= {0}", Param->Name );
5 Console::WriteLine( " Type= {0}", Param->ParameterType );
6 Console::WriteLine( " Position= {0}", Param->Position );
7 Console::WriteLine( " Optional= {0}", Param->IsOptional );
}
5.3 动态生成类实例对象和成员方法调用
当发现到所需的类型后,我们还可以动态产生一个该类型的实例,调用该实例的方法。毫无疑问,这些是在运行时决定的,当我们书写代码的时候,我们并没有硬编码某个特定的类型,我们只需要知道这个类型履行了某个接口的契约。ISO-C++的虚函数也提供了类似的多态的能力,但是这需要大量额外的编码,重要的是编码是在编译期完成的,当编译后,就无法再在运行时注入新的类型进入原先编码构造的一个继承体系中。
array<Type^>^ types = plugAssembly->GetTypes();
Type^ formType= Form::typeid;
for (inti=0; i<types->Length; i++)
{
if (formType->IsAssignableFrom(types[i])){
Form^ f = dynamic_cast<Form^>(Activator::CreateInstance(types[i]));
if (f){
f->Show();
}
}
}
上面的示例代码演示枚举一个plug-in的程序集里面所有的类型,找到其中属于Form类型的子类,为这样的类型产生一个实例,并向上造型成Form类型,最后调用该实例的Show方法显示出该特定的Form。
CLI是一门相当复杂的语言,比起C#,VB.Net来说,它的优势在哪里呢?都是CLS兼容的语言,编译之后都是中间代码,人们为什么不用C#,VB.Net,而要用CLI。
答案是速度。CLI的语法设定虽然相对C#复杂,但是也给了编译器更多的机会对它进行优化,产生出更优的中间代码;CLI给了程序员更多的权限,提供了更高阶的功能(当然代价是复杂的语法和更多的不确定性),帮助程序员更好地控制程序的行为,写出更高效的代码;最后也是最重要的一点,CLI与ISO-C++之间的互操作是相对比较容易的,同时使用CLI和ISO-C++,CLI编写的托管代码集中在程序中需要更多动态行为而比较不在意速度的部分,而ISO-C++编写的原生代码集中在程序中相对静态但是对速度有高要求的部分,如果能够有效的控制好托管到原生之间的接口,C++/CLI能够帮助程序员写出即高效又灵活的软件。
上面说到CLI与ISO-C++之间的互操作是相对比较容易,但是并没有能够真正做到无缝连接。由于内存模型上有着巨大的差异,当前CLI还是把自己定位在一个作为ISO-C++的托管扩展上面,它们之间的互操作还是使用一种间接的方式,需要通过指针或者引用进行调用,而不能直接在对象模型上面进行完全整合 —— 这里包括相互参与彼此的类继承体系(CLI类继承至ISO-C++类或者相反)和相互包含彼此类型的成员变量。
CLI的引用类型对象是分配在托管堆上,由垃圾收集器进行管理,ISO-C++类型对象的动态分配是在原生堆上,由程序员自己进行管理。所以,当一个CLI的引用类型里面有一个ISO-C++类型对象指针的成员变量,OK,这没有任何问题,指针的内存占用大小是确定的,它由程序员自己来管理;但是,当一个CLI的引用类型里面有一个ISO-C++类型对象的成员变量的时候,问题就产生了,这个对象是在托管堆上还是在原生堆上,如果在托管堆上,这个对象可能会在托管堆上使用ISO-C++的指针语法,最后必然导致出完全无法预测的错误行为导致系统崩溃。如果在原生堆上,CLI的引用类型本身实际上还是只包括一个间接引用,编译器可能必需在背后产生许许多多的额外代码,来使得这个引用类型中的ISO-C++对象看起来像是被直接包含但实际上还是间接的引用,这导致编译器的设计会变的十分困难,它要保证这些额外的代码在任何情况下都是正确的,还要尽量减少它们带来的时间和空间上的负担。所以,当前CLI和ISO-C++之间的整合还是处于间接的相互调用阶段,程序员可以使用一些模式来模拟在对象模型上直接整合的行为,但是这并不是编译器能够直接支持的,或许以后CLI的新版本能够做到。
虽然从ISO-C++原生代码也可以间接调用CLI托管代码,但是为了保持原生代码模块的纯粹性(可以被其它C++编译器编译和不需要.Net Runtime Library的支持),通常我们不会这么做,一般都是从CLI托管代码调用ISO-C++原生代码。所以下面的内容也只包括这部分。
6.1 CLI托管代码调用ISO-C++原生代码
在托管代码中,我们即可以在栈上静态分配一个ISO-C++类型的对象,就像一个普通的栈对象一样使用它(栈不像堆有托管和原生之分,无论是托管还是原生都是共用一个栈)。也可以显式在原生堆上动态分配一个ISO-C++类型的对象,然后通过指针间接使用它。
#include "TextQuery.h"
public ref class TextQueryCLI
{
1 TextQuery *pquery;
public:
2 TextQueryNet() : pquery( new TextQuery()){}
3 ~TextQueryNet(){ delete pquery; }
4 void query_text() { pquery->query_text(); }
5 void build_up_text() { pquery->build_up_text();}
};
上面是一个使用代理(proxy)模式的例子,TextQuery是一个ISO-C++类,而TextQueryCLI是它的CLI引用类型版本的代理类,TextQueryCLI包含一个TextQuery对象的指针(示例代码1),它在自身构造函数和析构函数中对这个指针指向的对象进行管理(分配和释放)(示例代码2,3),TextQueryCLI本身并不具备任何真正的功能,它只提供了一组接口,把TextQuery类对象的功能提供给客户(示例代码4,5)。但是客户并不知道TextQuery的存在,它所看到的只是TextQueryCLI。
使用代理模式,在C++/CLI编程中是十分常见的,由ISO-C++类型实现真正的应用逻辑,而用CLI类型进行包装提供给其它托管代码使用。这在大多数情况下可以满足性能和灵活性上双重的要求。
1:VC++ 2005从入门到精通
By 李建忠先生
https://www.microsoft.com/china/msdn/events/webcasts/shared/webcast/consyscourse/VCPP.aspx
2: 转换指南: 将程序从托管扩展 C++ 迁移到 C++/CLI
By Stanley B. Lippman
http://www.microsoft.com/china/MSDN/library/langtool/VCPP/TransGuide.mspx
3:MSDN C++参考
http://msdn2.microsoft.com/en-us/library/3bstk3k5(en-US,VS.80).aspx
4:CLI语言规范说明书
http://download.microsoft.com/download/ 9/9/c / 99c 65bcd-ac66-482e-8dc1-0e14cd1670cd/C++-CLI%20Standard.pdf