托管类程序设计基础

Download Report

Transcript 托管类程序设计基础

第二部分 托管类程序设计
第十四章
托管类 C++ 程序设计基础
所谓托管类 C++ 程序是通过 Visual C++ 的 C++/CLI 扩展使用
.NET 框架提供的框架库 FCL 和其他功能设计和编写,并由公共
语言运行时 CLR 进行运行管理的 C++ 程序。
托管类 C++ 程序与非托管类 C++ 程序在形式上十分相似,程
序的书写语法上也基本一致,但由于程序中对 FCL 和 CLR 的使
用是通过 C++/CLI 扩展实现的,这是非托管类 C++ 程序没有的。
本章将通过比较两类程序在实现相同或可以类比的功能时,
在实现方法上的相同和不同之处,介绍设计和编写托管类 C++
程序必须要掌握的基本知识。为了便于叙述,以下称非托管类
C++ 程序为 C++ 程序;称托管类 C++ 程序为 C++/CLI 程序。
14.1 程序中常用的预编译命令
1 包含命令行 #include
C++ 程序中用于包含系统的(.h)头文件,程序项目中的
(.h)头文件或(.cpp )源文件。例如:
#include <iostream> 和 #include “stdafx.h” 等
C++/CLI 程序中一般只用于包含程序项目中的(.h)头文件或
(.cpp )源文件。例如:
#include “stdafx.h”
2 引用命令行 #using
C++ 程序中一般较少使用该命令行 。
C++/CLI 程序中经常用于引用预包装单元(如 .dll),例如:
#using <mscorlib.dll>
3 名域使用命令行 using namespace
C++ 程序和 C++/CLI 程序中都用于声明程序中要使用的在某个
指定命名空间中定义的各种成员。例如:
C++ 程序中的:
using namespace std;
C++/CLI 程序中的:
using namespace System;
C++/CLI 程序设计需要使用的 FCL 提供的类型和功能都定义在
树型层次的命名空间中。其中根命名空间为 System。
在 System 命名空间中,包含如下子命名空间:
CodeDom, Collections, ComponentModel, Configuration, Data,
Deployment, Diagnostics, DirectoryServices, Drawing,
EnterpriseServices, Globalization, IdentityModel, Internal, IO, Linq,
Management, Media, Messaging, Net, Printing, Reflection,
Resource, Runtime, Security, ServiceModel, ServiceProcess,
Speech, Text, Threading, Timers, Transaction, Transactions, Web,
Windows, Workflow 和 Xml。
第二层子命名空间中,多数包含了第三层子命名空间。
System::CodeDom 中包含了子命名空间:
compiler。
System::Collections 中包含了子命名空间:
Generic, IList, ObjectModel 和 Specialized。
System::ComponentModel 中包含了子命名空间:
Design, IBindingList 和 IEditableObject。
System::Configuration 中包含了子命名空间:
Assemblies, Install, Internal 和 Provider。
System::Data 中包含了子命名空间:
Common, Design, Linq, Odbc, OleDb, OracelClient, Sql, SqlClient
和 SqlTypes。
System::Deployment 中包含了子命名空间:
Application 和 Internal。
System::Diagnostics 中包含了子命名空间:
CodeAnalysis, Design, Eventing, PerformanceData 和
SymbolStore。
System::DirectoryServices 中包含了子命名空间:
AccountManagement, ActiveDirectory 和 Protocols。
System::Drawing 中包含了子命名空间:
Design, Drawing2D, Imaging, Printing 和 Text。
System::EnterpriseServices 中包含了子命名空间:
CompensatingResourceManager。
System::IdentiyModel 中包含了子命名空间:
Claims, Policy, Selectors 和 Tokens。
System::IO 中包含了子命名空间:
Compression, IsolatedStorage, Log, Packaging, Pipes 和 Ports。
System::Linq 中包含了子命名空间:
Expressions。
System::Management 中包含了子命名空间:
Instrumentation。
System::Messaging 中包含了子命名空间:
Design。
System::Net 中包含了子命名空间:
Cache, Configuration, Mail, Mime, NegotiateStream,
NetworkInformation, PeerToPeer, Collaboration, Security, Sockets
和 SslStream。
System::Printing 中包含了子命名空间:
IndexedProperties 和 Interop。
System::Reflection 中包含了子命名空间:
Emit。
System::Resources 中包含了子命名空间:
Tools。
System::Runtime 中包含了子命名空间:
CompilerServices, ConstrainedExecution, Hosting, InteropServices,
Remoting, Serialization 和 Versioning。
System::Security 中包含了子命名空间:
AccessControl, Authentication, Cryptography, Permissions, Policy,
Principal 和 RightsManagement。
System::ServiceModel 中包含了子命名空间:
Activation, Channels, ComIntegration, Configuration, Description,
Diagnostics, Dispatcher, Internal, MsmqIntegration, PeerResolvers,
Persistence, Security, Syndication 和 Web。
System::ServiceProcess 中包含了子命名空间:
Design。
System::Speech 中包含了子命名空间:
AudioFormat, Recognition 和 Synthesis。
System::Text 中包含了子命名空间:
RegularExceptions。
System::Trasactions 中包含了子命名空间:
Configuration。
System::Web 中包含了子命名空间:
Caching, ClientServices, Compilation, Configuration, Handlers,
Hosting, Mail, Management, Mobile, Profile, Query,
RegularExpressions, Script, Security, Services, SessionState, UI
和 Util。
System::Windows 中包含了子命名空间:
Annotation, Automation, Controls, Converters, Data, Documents,
Forms, Ink, Input, Interop, Markup, Media, Navigation, Resources,
Shapes, Threading 和 Xps。
System::Workflow 中包含了子命名空间:
Activities, ComponentModel 和 Runtime。
System::Xml 中包含了子命名空间:
Linq, Schema, Serialization, XPath 和 Xsl。
第三层子命名空间中,有些又包含有第四层子命名空间。
System::ComponentModel::Design 中包含了子命名空间:
Data。
System::Data::Linq 中包含了子命名空间:
Mapping 和 SqlClient。
System::Diagnostics::Eventing 中包含了子命名空间:
Reader。
System::Runtime::InteropServerces 中包含了子命名空间:
ComTypes, CustomMarshalers 和 Expando。
System::Runtime::Remoting 中包含了子命名空间:
Activation, Channels, Contexts, Lifetime, Messaging, Metadata,
MetadataServices, Proxies 和 Services。
System::Runtime::Serialization 中包含了子命名空间:
Configuration, Formatters 和 Json。
System::Security::Cryptography 中包含了子命名空间:
Pkcs, X509Certificates 和 Xml。
System::ServiceModel::Activation 中包含了子命名空间:
Configuration。
System::ServiceModel::Security 中包含了子命名空间:
Tokens。
System::Speech::Recognition 中包含了子命名空间:
SrgsGrammar。
System::Speech::Synthesis 中包含了子命名空间:
TtsEngine。
System::Web::ClientServices 中包含了子命名空间:
Providers。
System::Web::Configuration 中包含了子命名空间:
Internal。
System::Web::Query 中包含了子命名空间:
Dynamic。
System::Web::Script 中包含了子命名空间:
Serialization 和 Services。
System::Web::Services 中包含了子命名空间:
Configuration, Description, Discovery 和 Protocols。
System::Web::UI 中包含了子命名空间:
Adapters, Design, HtmlControls, MobileControls 和 WebControls。
System::Windows::Annotation 中包含了子命名空间:
Storage。
System::Windows::Automation 中包含了子命名空间:
Peers, Providers 和 Text。
System::Windows::Controls 中包含了子命名空间:
Primitives。
System::Windows::Documents 中包含了子命名空间:
DocumentStructures 和 Serialization。
System::Windows::Forms 中包含了子命名空间:
ComponentModel, Design, Integration, Layout,
PropertyGridInternal 和 VisualStyles。
System::Windows::Ink 中包含了子命名空间:
AnalysisCore。
System::Windows::Markup 中包含了子命名空间:
Localizer 和 Primitives。
System::Windows::Media 中包含了子命名空间:
Animation, Converters, Effects, Imaging, Media3D 和
TextFormtting。
System::Windows::Xps 中包含了子命名空间:
Packaging 和 Serialization。
System::Workflow::ComponentModel 中包含了子命名空间:
Compiler 和 Design。
System::Workflow::Runtime 中包含了子命名空间:
Configuration, DebugEngine, Hosting 和 Tracking。
System::Xml::Serialization 中包含了子命名空间:
Advanced 和 Configuration。
System::Xml::Xsl 中包含了子命名空间:
Runtime。
第四层子命名空间中,有些又包含有第五层子命名空间。
System::Runtime::Remoting::Channels 中包含了子命名空间:
Http, Ipc 和 Tcp。
System::Runtime::Remoting::Metadata 中包含了子命名空间:
W3cXsd2001。
System::Workflow::Activities::Rules 中包含了子命名空间:
Design。
System::Runtime::Serialization::Formatters 中包含了子命名空间:
Binary 和 Saop。
System::Web::UI::Design 中包含了子命名空间:
WebControls。
System::Web::UI::MobileControls 中包含了子命名空间:
Adapters。
System::Web::UI::WebControls 中包含了子命名空间:
Adapters 和 WebParts。
System::Windows::Forms::ComponentModel 中包含了子命名空间:
Com2Interop。
System::Windows::Forms::Design 中包含了子命名空间:
Behavior。
System::Windows::Media::Media3D 中包含了子命名空间:
Converters。
第五层子命名空间中,只有 WebControls 中包含第六层子命名
空间。
System::Web::UI::Design::WebControls 中包含了子命名空间:
WebParts。
4 #pragma once 命令行
在 Visual Studio.NET 开发环境中,该命令行在 C++ 程序项目和
C++/CLI 程序项目创建时都被缺省添加。该命令行指示编译器
只对一个头文件处理一次,即使该头文件在项目的多个文件
中被包含,避免了一个类被多次定义的错误。
14.2 输入与输出语句
在 C++ 程序中,控制台程序的输入与输出功能是通过 I/O 流
类库提供的输入流类和输出流类对象实现的,而 MFC 窗口程序
中输入输出功能是通过设备上下文类 CDC 和图形设备接口类
GDI 的对象实现的。
而在 C++/CLI 程序中,控制台程序的输入与输出功能是通过在
命名空间 System 中定义的控制台类 Console 提供的静态方法实
现的。典型的输入方法有 Read、ReadKey 和 ReadLine,典型的
输出方法有 Write 和 WriteLine。下面的简单代码描述了这类输入
输出方法的典型使用方法:
String ^number;
…
Console::WriteLine( L”Welcom to Visual C++ .NET Programming!” );
Console::Write( L”Please Enter a Number: “ );
number = Console::ReadLine();
而 C++/CLI 窗口程序中的输入输出功能是通过在 System::Drawing
名域空间中定义的图形上下文类 Graphics 和扩展图形设备接口
类 GDI+ 的对象实现的。
14.3 类对象的创建和回收
C++ 程序中,所有类型的创建、使用和回收是一致的。所有
的类型包括自定义类型都可以与 C++ 预定义的数据类型一样既
能使用堆栈语义在堆栈内存中直接创建类型对象;也能通过对
类型指针 * (本地指针)使用运算符 new 在堆内存中动态创建
类型对象。在堆栈内存中直接创建的类型对象会在其生存期结
束时由系统调用类型的析构函数回收类型对象占用的内存资
源;而在堆内存中动态创建的类型对象必须对指向类型对象的
类型指针 * 使用运算符 delete 以便调用类型的析构函数回收类型
对象占用的内存资源。
C++/CLI 程序中,类型分为两种:值类型(value type)和引用
类型(ref type)。两种类型的创建、使用和回收是有区别的。
系统 FCL 的内建数据类型均为值类型,同时还提供大量的引
用类型,以便使用 .NET 框架功能。用户自定义的类型既可以是
值类型,也可以引用类型。
其中引用类型对象的创建方式有两种:一种是通过类型的句
柄 ^(类对象的“跟踪指针”,^ 发音为“hat”)用 gcnew 运算
符在
托管堆内存创建类型的托管对象,也可以将句柄 ^ 指向一个已
经存在的该类型的托管对象;另一种是使用堆栈语义在堆栈内
而值类型既可以像 C++ 程序中那样使用堆栈语义在堆栈内存
直接创建或通过类型指针 * 使用 new 在堆内存中动态创建值类
型对象;也允许通过定义值类型的句柄 ^,使用 gcnew 在托管堆
内存中创建值类型托管对象。
下面的简单代码描述了如何定义类型的句柄 ^,并且通过句
柄 ^ 使用 gcnew 创建类型的托管对象的方法:
Stirng ^firstPrompt =
gcnew String( L”Please the first integer: “ );
Stirng ^secondPrompt =
gcnew String( L”Please the second integer: “ );
String ^firstNumber, ^secondNumber;
…
Console::WriteLine( firstPrompt );
firstNumber = Console::ReadLine();
Console::WriteLine( secondPrompt );
secondNumber = Console::ReadLine();
…
几点说明:
1 类型指针 *,必须用 new 在堆内存中动态创建 *指针指向的类
型对象,并必须用 delete 对类型对象进行手动撤消和回收。
注意,类型 *指针虽然可以指向在堆栈内存中的类型对象,
但绝对不能用 delete 对该类型对象进行手动撤消和回收。
2 类型句柄 ^,必须用 gcnew 在托管堆内存中动态创建类型的
托管对象或指向一个在托管堆内存中已经存在的托管对象。
系统的垃圾收集器通过句柄 ^,自动地跟踪托管对象的使用
状态,并且在托管对象不再被使用时,自动撤消和回收托管
对象所占用的托管内存的空间,程序员可以无须自己管理。
这里需要特别指出的是:C++/CLI 扩展也支持使用 delete 对
gcnew 动态创建类型的托管对象执行撤消的用法,这个概念
称为确定性销毁。这使我们可以在托管代码中通过一种与本
地 C++ 相似的语法执行手工内存管理。该语法还包括使用
delete[] 销毁一个托管数组所占用的内存。注意,delete 实际上
并没有直接销毁托管对象的内存,它只是调用类型的析构函
数对句柄 ^ 执行结束性的清理工作,而垃圾收集器将负责最
终销毁和回收托管对象的内存。C++/CLI 扩展使用垃圾收集
器,为我们自动管理内存。这意味着没有用 delete 运算符手
工销毁的托管对象都将会在垃圾收集器运行时被自动销毁。
它保证了忘记销毁的托管对象不会导致内存泄漏。事实上,
如果我们愿意,可以完全避免对托管类型的对象使用 delete,
垃圾收集器将会负责销毁这些对象的内存。垃圾收集器的工
作方式称为非确定性最后化。这意味着我们无法准确地预测
垃圾收集器什么时候将会运行。而且,当垃圾收集器运行
时,无法保证哪些对象将被销毁。在被销毁的对象中,无法
保证它们的销毁顺序。而在处理表示某种资源(例如文件和
网络连接)的对象时销毁顺序显然特别重要。因此,对于表
示这种资源的对象,最好采用手工删除的方法,以便确保内
存何时被销毁。
总之,垃圾收集器只在性能上付出一点代价,却避免了程序
员必须手工释放内存的责任,从而极大地提高了程序的可靠
性。C++/CLI 扩展同时提供了确定性和非确定性销毁托管对象
内存的功能,使我们在需要速度和确定性时可以手工删除对
象,在其他时候由垃圾收集器根据需要释放内存。
切记,垃圾收集器永远不会回收非托管对象的内存,这一点
非常重要。因为垃圾收集器并不知道那些并不是在托管堆中
分配的对象。所以我们必须仔细调用 delete 释放它们所占用
的内存,否则将会导致内存泄漏。
3 如果需要将类型的句柄 ^ 初始化为 “空”,则应该按照
如下方法为类型的句柄 ^ 赋值:
Stirng ^firstPrompt = nullptr;
// nullptr 是系统内建的
而不能按照如下方法赋值:
Stirng ^firstPrompt = null;
// 在旧版本的 MC++ 中合法
或
Stirng ^firstPrompt = 0;
// 在旧版本的 MC++ 中合法
和本地 C++ 的指针一样,在使用 delete 运算符删除托管对象
内存之后,应该养成将句柄设置为 nullptr 的好习惯。
4 注意,在旧版本的 MC++ 中,类型的句柄 ^ 是用 * 说明的,
并使用 new 创建类型的托管对象。因此,不能从形式上区分
两种不同指针,降低了可读性。例如:
String *string = String( L”Welcome to Visual C++ .NET Programming!” );
5 使用堆栈语义创建的托管类对象在其生存期结束时,系统会
自动调用类型的析构函数对堆栈内存中的句柄 ^ (在托管对
象创建时,自动隐含创建的)执行结束性的清理工作,而垃
圾收集器将负责最终销毁和回收该托管对象的内存。显然,
这实际上是由系统借助堆栈语义实现的托管对象的隐含动态
创建和托管对象的隐含确定性销毁。
14.4 字符数据类型和字符串
在 C++ 程序中, 字符数据是用 ASCII 码表示的,因此在内存
中是以 8 位字节为单位存储的。字符数据变量是多用 char 类型
定义的,每个字符数据占一个字节,表示一个 ASCII 码字符,一
个汉字字符需要用两个字符数据来表示。字符常量数据是用一
对单引号中字符表示的,例如:‘x’,‘$’,‘7’,‘*’,‘\n’
等等。
定义一个字符类型数组可以存放一个字符串。使用在 std 名
域空间中定义的 string 可以更为方便、安全地存放、管理和操
作字符串。字符串常量数据是用一对双引号中字符串表示的,
char str[] = “Welcome to Visual C++ .NET Programming!”;
在 C++/CLI 程序中,虽然也可以使用 char 类型定义字符数据
变量,但在大多数情况下则必须使用 Unicode 码表示字符数据,
所以不提倡使用 char 类型定义字符数据变量。 Unicode 码是国
际通用字符集,可以表示不同语言文字、数学符号等。
Unicode 字符在内存中是以 16 位字节为单位存储的。字符数
据变量是用 __wchar_t 或 wchar_t 类型定义的,每个字符数据占
两个字节,表示一个 Unicode 字符。字符常量数据也是用一对单
引号中字符表示的,例如: ‘x’,‘$’,‘7’,‘*’,‘\n’ 等等。
定义字符类型数组可以存放一个字符串(使用 System::Array
定义各种类型,包括字符串类型托管数组的方法将后面关于托
管数组的一节中叙述)。使用在 System 名域中定义的 String 类
型可以像在 C++ 程序中使用 string 那样,方便、安全地存放、管
理和操作字符串。
字符串常量数据是用以大写的字符 ‘L’ 为前缀的一对双引号
中
字符串表示的,例如:
wchar_t str[] = L“Welcome to Visual C++ .NET Programming!”;
注意,在 C++ 程序中也提倡使用 __wchar_t 或 wchar_t 类型定
14.5 FCL 的基元数据类型
FCL 提供一系列用结构 struct 定义的基元数据类型,这是在
.NET 框架上能够实现多语言混合编程的重要基础之一。
为了符合标准 C++ 程序员的编程习惯,在 C++/CLI 扩展中提
供了这些基元数据类型的别名,这些别名与 C++ 中提供的内建
数据类型名相同。换句话说,在 C++/CLI 程序中,使用内建数据
类型定义的变量实际上是定义了 FCL 提供基元数据类型对象。
例如,基元数据类型 Int32 的别名是 int。
基元数据类型不仅提供了相应类型数据的结构,还提供了一
系列操作数据的方法。下面的程序代码典型地描述了基元数据
类型与相应别名的关系和使用方法。
#include “stdafx.h”
#using <mscorlib.dll>
using namespace System;
int main()
{
String ^firstNumber, ^secondNumber;
int number1, number2, sum;
Console::Write( L”Please Enter the first integer: “ );
firstNumber = Console::ReadLine();
Console::Write( L”Please Enter the second integer: “ );
secondNumber = Console::ReadLine();
number1 = Int32::Parse( firstNumber );
number2 = Int32::Parse( secondNumber );
sum = number1 + number2;
Console::WriteLine( L”\nThe sum is {0}.”, sum.ToString() );
return 0;
}
其中 int 是基元数据类型 Int32 的别名,Parse 和 ToString 都是
Int32 提供的数据操作方法。
FCL 提供的主要基元数据类型与(C++/CLI 类型)别名如下:
基元数据类型
C++/CLI 类型
说明
Boolean
bool
布尔类型
Byte
char
8 位无符号整数
SByte
signed char
8 位有符号整数
Char
__wchar_t
16 位Unicode 字符类型
Int16
short
16 位有符号整数
UInt16
unsigned short
16 位无符号整数
Int32
int 或 long
32 位有符号整数
UInt32
unsigned int / long
32 位有符号整数
基元数据类型
C++/CLI 类型
说明
Int64
__int64
64 位有符号整数
UInt64
unsigned __int64
64 位有符号整数
Single
float
32 位单精度浮点数
Double
double
64 位双精度浮点数
Decimal
Decimal
96 位有符号整数
Object
Object^
类对象引用
String
String^
Unicode 字符串引用
14.6 数学函数
虽然在 C++/CLI 程序中仍然可以使用 C++ 的系统库函数。例
如,通过包含 math.h 使用数学函数。但在托管程序中更应该使
用由 FCL 提供的大量的类型和类型方法来取代使用 C++ 的系统
库函数。程序最频繁使用的数学函数在 FCL 中是由 Math 类型的
方法提供的。 Math 类型是定义在 System 命名空间中的,该类型
的静态属性 E 和 PI 为用户提供了常用的数学常量 e 和π;该类
型的静态成员函数为用户提供了常用的数学函数如下:
绝对值函数:
Abs
指数函数:
Exp
三角函数:
Sin
Cos
Tan
双曲三角函数: Sinh
Cosh
Tanh
反三角函数:
Asin
Acos
Atan
对数函数:
Log
Log10
平方函数:
Pow
开方函数:
Sqrt
Atan2
求较大数函数: Max
求较小数函数: Min
四舍五入函数: Round
取整函数:
Floor
注意使用这些静态属性和方法时必须冠以类名 Math 和名域运算
符 :: ,例如 Math::PI , Math::Sqrt( 25.0 ) 等。
调用一个函数时,如果实参与形参的类型不符,就会发生实
参类型的隐式转换或需要用户进行显式(强制)转换。隐式转
换的原则是数据长度提升。根据这一原则, FCL 内建的基元数
据类型可以进行如下隐式转换:
类型
可以安全转换成的类型
Byte
UInt16, Int16, UInt32, Int32, Uint64, Int64, Single,
Double 或 Decimal
SByte
Int16, Int32, Int64, Single, Double 或 Decimal
Int16
Int32, Int64, Single, Double 或 Decimal
UInt16
UInt32, Int32, UInt64, Int64, Single, Double 或 Decimal
Char
UInt16, UInt32, Int32, UInt64, Int64, Single, Double 或
Decimal
Int32
Int64, Single, Double 或 Decimal
UInt32
Int64, Single, Double 或 Decimal
Int64
Decimal
UInt64
Decimal
Single
Double
强制转换可以通过在 System 命名空间中定义的 Convert 类型提
供的方法实现。例如,将一个 Int32 类型的变量值强制转换为
Int64 类型的方法如下:
int number;
__int64 longNumber = Convert::ToInt64( number );
注意,强制转换允许不遵守数据长度提升的原则。
14.7 托管数组
在 C++/CLI 程序中,提倡定义 “托管数组” (Managed
Array)。
托管数组实际是 System::Array 类型的对象,因此在 C++/CLI 程序
中创建的托管数组能使用该类提供的各种方法和属性。
1 托管数组的定义和创建
托管数组定义的一般表达式:
[qualifiers] [cli::]array<[qualifiers]type[, dimension]> ^var;
qualifiers
存储方式说明(可选项)。可选择的存储方式包
括:mutable, volatile, const, extern 和 statc。
array
托管数组定义的关键字,该关键字是定义在 cli 名
域中的。托管程序项目中 using namespace cli; 是隐
type
托管数组元素的类型名。可选择的类型包括类型
的句柄 ^ 名,值类型名或本地指针名(例如值类
型的 *指针名)。
dimension
托管数组的维数。缺省维数为 1,最大维数为 32。
var
托管数组名。
例如:
array<int> ^intArray;
// 一维整型托管数组
array<String^> ^strArray1;
// 一维 String^ 托管数组
array<String^, 2> ^strArray2;
// 二维 String^ 托管数组
array<double, 2> ^doubleArray;
// 二维 double 托管数组
托管数组创建的一般表达式:
var = gcnew [cli::]array<[qualifiers]type[, dimension]>(val [, val…]);
gcnew
托管对象创建运算符。
val
托管数组指定维的尺寸。
例如:
intArray = gcnew array<int>(100);
strArray1 = gcnew array<String^>(50);
strArray2 = gcnew array<String^, 2>(5, 10);
doubleArray = gcnew array<double, 2>(10, 10);
托管数组的定义和创建可以在同一语句中,例如:
array<int> ^intArray = gcnew array<int>(100);
array<String^, 2> ^strArray2 = gcnew array<String^, 2>(5,10);
托管数组的创建也可以使用类 System::Array 提供创建托管数
组实例的方法 CreateInstance 来实现,例如:
array<int> ^intArray = Array::CreateInstance( int, 100 );
array<String^, 2> ^strArray2 = Array::CreateInstance( String^, 5, 10 );
2 托管数组的初始化
所谓托管数组的初始化就是在托管数组创建的同时为数组的
元素赋值。例如:
array<int> ^intArray =
gcnew array<int>{ 20, 15, 30, 25, 12, 10, 8, 45, 9, 22 };
array<String^, 2> strArray =
gcnew array<String^, 2>{
{ L“John”, L“male”, L“New York”},
{ L“Merry”, L“female”, L“Washington”},
{ L“Smith”, L“male”, L“Houston”} };
3 托管数组的访问
对托管数组的访问可以通过下标算符,或用类 System::Array
的 GetValue 和 SetValue 方法。例如:
int value = intArray[5];
intArray[5] = 20;
String ^str = strArray[2, 0];
strArray[2, 0] = L”Henrry”;
或者
int value = intArray->GetValue( 5 );
intArray->SetValue( 20, 5 );
String ^str = strArray->GetValue( 2, 0 );
strArray->SetValue( L”Henrry”, 2, 0 );
4 托管数组的排序和查询
类 System::Array 为托管数组提供了不少有用的方法,因此托
管数组可以方便地使用这些方法,其中静态排序方法 Sort 和静
态二分法查询方法 BinarySearch 方法为实现数组的排序和查询操
作提供了方便、安全的手段。
类 System::Array 为方法 Sort 定义多种版本,以便满足对数组
的多种排序需要,例如,对全部内建数值元素按缺省比较算法
(数值升序)排序、对部分内建数值元素按缺省比较算法(数
值升序)排序、对全部内建数值元素按降序算法排序、对全部
自定义元素按自定义比较算法排序等等。例如:
对 intArray 的全部元素按升序算法排序的语句:
Array::Sort( intArray );
对 intArray 中下标 1 到 5 的元素按升序算法排序的语句:
Array::Sort( intArray, 1, 5 );
注意,排序方法只能用于一维数组。
类 System::Array 提供了方法 BinarySearch 的多种版本,以便满
足对数组的多种查询需要,例如在全部内建数值元素中查询
指定元素、在部分内建数值元素中查询指定元素、在全部自
定义类型元素中按自定义算法查询指定元素。例如,在
intArray 的全部元素中查询指定元素的语句:
int pos = Array::BinarySearch ( intArray, (Object^)25 );
在 intArray 中下标 1 到 5 的元素中查询指定元素的语句:
int pos = Array::BinarySearch ( intArray, 1, 5, (Object^)25 );
注意,如果被查询元素存在,则返回该元素在数组中的下标
位置,否则返回 -1。排序方法也只能用于一维数组。
5 在旧版本的 MC++ 中,托管数组的定义,创建和初始化方法:
int a __gc[];
a = new int __gc[10];
int b __gc[] = { 20, 15, 30, 25, 12, 10, 8, 45, 9, 22 };
String *str1[,] = new String*[4, 2];
String *str2[,] = { { L”John”, L”male”, L”New York”},
{ L”Merry”, L”female”, L”Washington”} };
14.8 托管类型的定义与类型成员
在 C++/CLI 程序中,既可以像 C++ 程序一样定义非托管的本
地类型,但更重要的是定义托管类型。两种类型的定义从形式
上看有着很多相似之处,但也存在一些重要的差别。在已经掌
握的 C++ 类型定义和组成的基础上,本节通过比较两个类的不
同,描述托管引用类型和值类型的定义方法和类成员。
1 托管类型的定义格式:
class_access ref class name modifier
: inherit_access base_type { }; // 引用类型
class_access ref struct name modifier
: inherit_access base_type { }; // 引用结构
class_access value class name modifier
: inherit_access base_type { }; // 值类型
class_access value struct name modifier
:inherit_access base_type { }; // 值结构
class_access:(可选项)
类或结构外的可访问性,可选择值: public 或 private,缺
省值是 private。嵌套定义的类或结构不允许有此选项部
分。C++ 的类和结构定义不使用该选项。
ref class,value class,ref struct 或 value struct:
托管类或结构定义关键字。C++ 的类或结构定义关键字
是 class 或 struct。
name:
被定义类型(类或结构)名。命名规则与 C++ 相同。
modifier:(可选项)
类或结构的修饰项。可能的选择值是 abstract(只能作为
基类型的类或结构)或 sealed(不能作为基类型的类或结
构)。C++ 的类和结构定义不使用该选项。
inherit_access:(可选项)
基类型(类或结构)的可访问性。只允许选择 public,因
此缺省值为 public。 C++ 类和结构的基类型可访问性允许
选择 public、protected 或 private,缺省值为 private 。
base_type:(可选项)
基类型(类或结构)名。托管类或结构定义可以从 0 或
多个托管接口(interface)和 0 或 1 个引用类或结构继
承;值类或结构定义只能从 0 或多个值接口(interface)
继承。C++ 的类定义可以从 0 或多个 C++ 类继承。
例如:
public ref class MyRefClass { };
public ref struct MyRefStruct { };
public value class MyValClass { };
public value struct MyValStruct { };
与托管引用类型 ref class/ref struct 相比,托管值类型 value class
/value struct 存在许多限制:
① 隐含地使用了 sealed 关键字,因此它们无法作为基类。
② 托管值对象在默认情况下是按值传递的。
③ 编译器提供了不能被重写的默认构造函数。
④ 编译器提供了不能被重写的拷贝构造函数和赋值运算符。
⑤ 不能包含析构函数。
⑥ 不能包含本身是友元的成员。
⑦ 尽管可以在堆栈内存或堆内存中创建托管值类型的对象,
但不能用本地对象或本地数组作为类型的数据成员。
那么,什么时候使用托管值类型呢?当需要定义的新类型主
要用于存储其他值类型中的数据时,就应该定义托管值类
型,而不是托管引用类型。这种新类型的对象较小,不至于
因为按传递而导致性能明显降低。例如,在图形应用程序
中,定义一个用于封装、存储点坐标 X 和 Y 数据的值类型
Pixel。托管值类型也可以用于定义“智能指针”类型。
注意,在 C++/CLI 程序中声明一个枚举(托管值类型),需要
使用新的带空格的关键字 enum class ,例如:
C++ 代码中定义为:
enum Status { CONTINUE, NOW, LOST };
的枚举声明在 C++/CLI 代码中应声明为:
enum class Status { CONTINUE, WON, LOST };
C++ 代码中使用枚举值赋值操作的代码:
int gameStatus = WON;
在 C++/CLI 代码中应修改为:
int gameStatus = Status::WON;
在旧版本的 MC++ 中,托管类型或结构定义关键字是:
__gc class 或 __gc struct,值类型或结构定义关键字是:
__value class 或 __value struct。
例如:
public __gc class MyRefClass { };
public __gc struct MyRefStruct { };
public __value class MyValClass { };
public __value struct MyValStruct { };
在托管类定义中,定义在 System 命名空间中的类型 Object 总
是被作为缺省基类。所有的系统内建简单数据类型都是从
System 命名空间中的类型 ValueType 继承的。而 ValueType 是
从 Object 继承的。也就是说,托管类型总是 Object 的派生类
型,即总可通过 Object 类型句柄 ^ 访问任何托管类对象。
所有的简单数据类型的值都可以赋值给一个“Object 句柄”
类型
的变量。这个赋值过程称为装箱转换。在装箱转换中,位于
堆栈上的简单数据的值被复制到托管堆上新创建的该简单数
据类型对象上,使这个简单数据类型的值可以按照 Object 类
型进行操作。装箱转换既可以显式地进行,也可以隐式地进
int i = 5;
// create an int value on the stack
// explicitly box the int value
Object ^object1 = static_cast< Object^ >( i );
// imexplicitly box the int value
Object ^object2 = i;
拆箱转换可以把一个“Object 句柄”类型变量显式转换为一
个简
单数据类型值,如下面代码所示:
int x = static_cast< int >( object1 );
如果一个“Object 句柄”并没有引用简单数据值(未在托管
堆中
另外,如果创建一个简单数据类型的句柄,可以简单地如下
对这个句柄进行解引用:
int i = 5;
// create an int value on the stack
int ^intObject = i;
// imexplicitly box the int value in an int object
int x = *intObject;
// imexplicitly unbox the int object
这里,并没有使用 Object 句柄,而是使用一个 int 句柄。因此,
可以通过对 intObject 进行解引用隐式地执行拆箱,而不
需要显式的类型转换。
对简单数据类型值的的装箱转换和拆箱转换也同样适用于用
户自定义的值类型,因为使用 value class 定义一个值类型
时,它将自动继承于 ValueType 类型。
在旧版本的 MC++ 中,用关键字 __box 在托管堆中创建一个托
管类型对象,并将值类型值赋值到所创建的托管对象中,然
后返回该托管对象,从而实现装箱转换。例如实现 int 类型变
量 i 的值的装箱转换代码如下:
__box( i );
2 托管类型的数据成员
组成:
托管类型的数据成员类型可以包括:
托管值类型的对象、句柄 ^、*指针,
托管引用类型的对象、句柄 ^,
C++ 类型的对象、引用、*指针。
可访问性:
托管类型的数据成员与 C++ 类型的数据成员在可访问性
的种类、声明规则和缺省访问性方面完全相同。
可访问性的种类:private、public 和 protected 。
可访问性的声明规则:允许分段声明多个数据成员的可
访问性,或单独为每个数据成员声明可访问性。 只不过
在托管类型的定义中更提倡对每个数据成员都单独显式
说明可访问性。
可访问性的缺省值:类数据成员的缺省可访问性为
private;结构数据成员的缺省可访问性为 public。
静态数据成员:
与 C++ 类型相同,托管类型也使用修饰字 static 声明静态
数据成员,它们的可访问种类可以是 private、public 或
protected 。声明静态数据成员的同时可以进行初始化。
3 托管类型的方法
辅助方法:
使用 private 声明这类方法,这相当于 C++ 类型的私有成
员函数,不能在类外被调用。如果期望辅助方法在派生
类型中能被调用,应使用 protected 声明。
接口方法:
使用 public 声明这类方法,这相当于 C++ 类型的公有成
员函数,能够在类外,通过类型对象被调用。
静态方法:
加入 static 前缀说明这类方法,这相当于 C++ 类型的静态
成员函数,能够在类外,而无须通过类型对象被调用。
所有的方法既可以在类型定义中完成,也可以在类型定义中
声明,在类型定义外定义实现代码。这与 C++ 类型相同。
4 C++/CLI 的常量和友元
和本地 C++ 相比,C++/CLI 使用 const 的方式存在一个重要的
区别。在 C++/CLI 的托管代码中,我们无法把类型的函数声明
为 const(可以操作常量的只读函数)。这就是说,尽管可以
在 C++/CLI 的托管代码中把一个托管对象声明为 const。但由
于无法编写 const 函数,因此对于这种 const 对象也几乎没有
办法进行操作。这是一个令人遗憾的限制。
这个变化的原因在于,CLI 的角色是一种用于语言之间互操作
的工具。许多 .NET 语言(例如 C#)并不存在 C++ 中的 const
概念。因此,如果在 C++/CLI 中声明 const 函数,将会使这些
函数无法跨语言使用。
尽管如此,我们仍然可以通过一些实用的方式,使用 const 在
C++/CLI 中实现最小权限原则。我们可以在句柄和函数实参中
使用 const,就像对指针使用 const 一样。注意,托管数组并
不支持 const 限定符,这意味着无法在 C++/CLI 中使用 const 声
明或传递托管数组。
C++/CLI 添加了两个可以用于托管类的上下文敏感的关键字,
用于实现与在本地类中使用 const 相似的效果。在 C++/CLI 声
明一个静态常量(相当于在本地 C++ 中使用 static const)成员
应使用关键字 literal。声明为 literal 的数据成员必须在声明的
同时进行初始化,例如:
ref class X
{
literal int i = 4;
};
注意, literal 必须是类的数据成员,而不能是全局静态成员。
C++/CLI 的另一个新关键字是 initonly。被声明为 initonly 的数据
成员只能在类的构造函数中被赋值,而不能在其他地方被赋
值。例如:
ref class Y
{
initonly static int staticConst1;
initonly static int staticConst2 = 0;
static Y() {
staticConst1 = 0;
staticConst2 = 1;
}
};
注意, literal 和 initonly 必须出现在数据成员声明的开始的位
置,否则将不作为关键字,而只作为标识符。
C++/CLI 不允许在托管类中声明友元类或友元函数。只有本地
类可以使用 friend 关键字。注意,本地类中可以把托管类声明
为它的友元,这就意味着一旦一个托管类被声明为一个本地
类的友元,则该托管类对象的所有成员都可以通过参数(传
递或引用该本地类对象),无障碍地访问该本地类对象的所
有成员。
4 托管类型的属性 property
托管类型对需要类外访问数据成员,提供特定的 public 访问
“接口”,称为属性 property。这是组件设计的重要方法,它
允
许用户通过属性,方便、安全地设置或获取数据成员的值。
属性的定义:使用 property 关键字定义属性。例如,一个类型
为 type,名为 name 的属性定义格式如下:
property type name
{
type get()
{
…
void set( type value )
{
…
}
}
例如,在自定义类 Timer 中为 private 数据成员 hour 提供读写
操作的属性 Hour 的定义方法:
public ref class Timer
{
…
public:
property int Hour
{
int get()
{
return hour;
}
void set( int value )
{
hour = value;
}
}
private:
int hour;
…
};
属性的访问:
属性的可访问性是 public,所以在类外可以直接访问。例如,
通过 Timer 对象的句柄 ^ 访问属性 Hour 的方法如下:
Timer ^timer = gcnew Timer();
int hour = timer->Hour;
// int hour = timer->Hour::get();
timer->Hour = (hour + 12) % 24;
// timer->Hour::set((hour+12)%24);
索引属性的定义:
所谓索引属性就是使用属性实现对托管类型的私有数组数据
成员的赋值和获取。例如,在自定义类 Box 中为访问 private
数组数据成员 dimensions 定义的索引属性 Dimensions:
public ref class Box
{
public:
…
property double Dimensions
{
double get( int index )
{
return( index < 0 || index > dimensions->Length ) ?
-1 : dimensions[ index ];
}
void set( int index, double value )
{
if( index >= 0 || index < dimensions->Length )
dimension[ index ] = value;
}
}
private:
static array<double> ^dimensions = gcnew array<double>( 3 );
…
};
索引属性的访问:例如,通过 Box 对象的句柄 ^ 访问属性
Dimensions 的方法如下:
…
Box ^box = genew Box();
for( int i = 0.0; i < 3; i++ )
box->Dimensions[ i ] = 2.0 * ( i + 1.0 );
// box->Dimensions::set(i, 2.0*(i+1.0));
// or box->Dimensions->default::set(i, 2.0*(i+1.0));
double volume = 0.0;
for(int i = 0; i < 3; i++)
volume = 1.0 * box->Dimensions[i];
// height = box->Dimensions::get(i);
// or height = box->Dimension->default::get(i);
…
注意,属性所提供的对私有数据成员访问,并非总需要同时
提供赋值和获取操作。可以根据被访问私有数据成员应该提
供的访问需求,定义只读属性(只含有 get 操作)。
5 指向托管类型数据成员的指针
在托管类程序中,指针是需要严格限定和管理的。我们已经
知道,所有的托管类型的对象都可以用类型的句柄 ^ 指示,
而只有值类型才可以用直接指向对象的 *指针指示。但无论
是句柄 ^ 还是 *指针都不允许用来指向托管类型的数据成员,
否则将会破坏系统垃圾收集器对托管类型对象活动的跟踪、
管理。
为满足程序设计使用指针对托管类型的数据成员的操作,在
cli 命名空间中提供了 interior_ptr 类型指针,可以用于声明引用
类型的内部成员的指针。
interior_ptr 类型指针可引用的数据成员包括:句柄、值类型、
包装值类型、托管类型对象,类型的句柄 ^,和托管数组中
的元素。声明 interior_ptr 变量的通用格式如下:
[cli::]interior_ptr<[cv_qualifier] type> var [= &initializer] ;
cv_qualifier:
type:
var:
引用限定,可选值为 const 或 volatile。
var 和 initializer 的类型名。
interior_ptr 指针变量名。
例如,定义引用类数据成员和句柄 ^ 的 interior_ptr 指针:
ref class MyClass
{
public:
int data;
};
int main()
{
MyClass ^myclass = gcnew MyClass();
myclass->data = 1;
Console::WriteLine( myclass->data.ToString() );
interior_ptr<int> p = &( myclass->data );
*p = 2;
Console::WriteLine( myclass->data.ToString() );
interior_ptr<MyClass^> p1 = &myclass;
(*p1)->data = 3;
Console::WriteLine( (*p1)->data.ToString() );
}
6 销接指针
使用 interior_ptr 指针虽然可以指向托管类型的数据成员,但
却不能直接作为本地 *指针使用。例如:
void native_function( int *p )
{
for( int i = 0; i < 10; i++ )
p[i] = i;
}
…
array<int> ^arr = gcnew array<int>(10);
interior_ptr<int> p = &arr[0];
int *np = p;
native_function( np );
// 错误
为了解决这类程序设计问题,在 cli 命名空间中还提供了另一
种指针 pin_ptr。这种指针可以像一个 “销子” 一样将指向托
管
类型成员的指针与本地 *指针销接起来。换句话说,一个
interior_ptr 指针可以转换为 pin_ptr 指针,只有转换为 pin_ptr 指
针后才能与本地 *指针销接,以便被本地函数使用。上述代
码进行如下修改后就可以正确编译运行了。
…
array<int> ^arr = gcnew array<int>(10);
pin_ptr<int> p = &arr[0];
int *np = p;
14.9 托管抽象类型与托管接口类型
在 C++ 程序中提供了抽象类型定义,而没有接口类型定义。
只要在所定义的类型中包含了纯虚成员函数,则该类型就被定
义为抽象类型。例如, 抽象类 X 的定义如下:
class X
{
public:
…
virtual void fun() = 0;
…
};
抽象类型不能创建对象,而可以作为派生类型的基类型,并
可以定义抽象类型的指针或引用,用于指向或引用派生类型的
对象。抽象类型中纯虚成员函数必须在派生类中定义。
在 C++/CLI 程序中,抽象托管类型的作用与 C++ 程序中的抽
象类型相同,定义方法基本相似,但也存在着区别和限定。首
先抽象托管类型只能是引用类型,而不能是值类型;其次抽象
托管类型必须使用关键字 abstract 显式声明;另外纯虚函数既可
以使用 C++ 纯虚函数的声明方法,也可以使用关键字 abstract 说
明,并提倡使用后一种声明方法。例如,抽象托管类 X 的定义
如下:
ref class X abstract
{
public:
…
virtual void fun1() = 0;
virtual void fun2() abstract;
virtual property IsConnected
{
bool get() abstract;
}
…
};
C++/CLI 托管接口类型实际上是一种全部由纯虚方法和纯虚属
性组成,不包含数据成员的抽象托管类型。该类型为多个类型
定义一组共用的接口操作规范,而不定义具体的操作行为提供
了方便。例如,托管抽象类 X 中如果只包含纯虚方法 fun1、fun2
和纯虚属性 IsConnected,则可以定义为如下的接口类型 X:
interface class X
{
public:
void fun1();
void fun2();
property bool IsConnected { bool get(); }
};
注意,property bool IsConnected { bool get(); } 表示所声明的纯虚
属性 IsConnected 是一个只读属性。如果 IsConnected 是一个读写
属性,声明可改为: property bool IsConnected;
如果一个托管类型从一个接口类型中继承,则接口类型中声
明的纯虚方法和纯虚属性必须在该托管类型中定义具体的操作
行为。被实现的纯虚方法和属性都应添加关键字 virtual 显式说
明。例如:定义从接口类型 X 继承的托管类型 Y :
ref class Y : public X
{
public:
…
virtual void fun1()
{
…
}
virtual void fun2()
{
…
}
property bool IsConnected
{
virtual bool get()
{
return connected;
}
}
…
private: bool connected;
};
与 C++ 程序中基类的虚成员函数可以在派生类中重新定义新
操作版本一样,C++/CLI 程序中基类的虚方法在派生类中也可以
重新定义新操作版本,但两者的声明或定义格式略有差异。
C++ 程序中虚成员函数的在派生类中的新版本声明或定义中
关键字 virtual 是可选项,而 C++/CLI 程序中虚方法在派生类中的
新版本声明或定义中关键字 virtual 是必选项,并且还必须添加
后缀修饰字 override 或 new 说明被声明或定义的方法是一个虚方
法的重定义版本或新定义版本。例如,定义一个从托管类 Shape
派生的托管类 Circle,并在 Circle 中重新定义基类 Shape 的虚方
法 draw 的代码:
ref class Shape
{
public: virtual void draw() abstract;
…
};
ref class Circle : public Shape
{
public: virtual void draw() override;
…
};
void Circle::draw()
{
…
}
或
ref class Circle : public Shape
{
public: virtual void draw() override
{
…
}
…
};
或
ref class Circle : public Shape
{
public: virtual void draw() new;
…
};
void Circle::draw()
{
…
}
或
ref class Circle : public Shape
{
public: virtual void draw() new
{
…
}
…
};
C++/CLI 还支持虚函数的名字重写。这个技巧允许派生类重写
它继承的虚函数,并为这个函数提供一个新名字。新命名的
函数必须具有与基类的函数相同的参数列表和返回类型。
例如,对上面的托管类 Circle,我们可以用一个新命名的函数
display 对 draw 函数进行重写,如下所示:
ref class Circle : public Shape
{
public: virtual void display() = Shape::draw;
…
};
Shape ^s = gcnew Circle;
s->draw();
将自动调用 Circle 类的 display 函数。
C++/CLI 还允许对类成员函数使用上下文敏感的关键字 sealed
进行声明,指定该成员函数不能在派生类中被重写。例如,
如果我们像下面这样在托管类型 Shape 中声明 draw 函数:
void draw() sealed;
这样就使 Shape 的任何派生类都不允许重写 draw 函数。当
我们想保证一个成员函数在所有的派生类中都具有相同的功
能时,这个特性就非常实用。使用sealed 保证了对类层次进
行扩展的其他人不会破坏基类所提供的功能。
函数的调用是在编译时被解析的。由于编译器知道 sealed 函
数无法被重写,因此它常常对代码进行优化,在 sealed 函数
的每个调用点直接添加函数定义的展开代码,从而消除了它
们的调用开销,即代码内联。
14.10 委托
在实现面向对象的多态性设计时,我们常常可以将一个需要
产生多种操作结果的方法设计成一个具有能调用不同操作方法
的调度结构的统一方法。而那些产生不同结果的不同操作方法
的 “签名类型”(方法函数的类型和参数)都必须相同,使得
这
些方法可以作为参数传给实现方法调度的统一方法,从而实现
该统一方法能够依据所传来的参数(方法)的不同而呈现出操
作结果的多态。例如,我们可以将 “升序比较” 方法 或 “降
序比
在 C++/CLI 程序设计中,正是采用了上述设计思想来实现消
息响应的多态性。为实现这一设计思想, C++/CLI 提供了 “委
托”
(Delegates) 的概念。所谓委托,就是一种能封装一系列具有
相同签名类型的方法指针的类型。一个包含了不同方法指针的
委托对象可以传给一个方法。具有该方法的类型对象就可以依
据接收到的消息(可以从不同的类型对象发出,但具有相同的
消息名和结构),调用委托对象中所包含的相应的操作方法,
产生相应的消息处理结果;从而实现消息响应的多态性。下面
通过分析实例 DelegateTest 所实现的简单排序功能,进一步理解
首先定义一个提供委托的托管类 DelegateBubbleSort
public ref class DelegateBubbleSort
{
public:
delegate bool Comparator( int, int );
// sort array using Comparator delegate
static void SortArray( array<int>^, Comparator^ );
private:
// swap two elements
static void Swap( interior_ptr<int>, interior_ptr<int> );
};
// end class delegateBubbleSort
类的实现代码如下:
#include "DelegateBubbleSort.h"
// sort array using Comparator delegate
void DelegateBubbleSort::SortArray(
array<int> ^arr, Comparator ^Compare )
{
for( int pass = 0; pass < arr->Length; pass++ )
for( int i = 0; i < arr->Length - 1; i++ )
if( Compare( arr[i], arr[i+1] ) )
Swap( &arr[i], &arr[i+1] );
}
// end method SortArray
// swap two elements
void DelegateBubbleSort::Swap( interior_ptr<int> firstElement,
interior_ptr<int> secondElement )
{
int hold = *firstElement;
*firstElement = *secondElement;
*secondElement = hold;
}
// end method Swap
使用委托的实现排序功能的类 BubbleSort 定义如下:
#include "DelegateBubbleSort.h"
public ref class BubbleSort
{
public:
BubbleSort();
// constructor
void PopulateArray();
// sort the array
void SortArrayAscending();
void SortArrayDescending();
virtual String ^ToString() override;
private:
static array<int> ^elementArray = gcnew array<int>(10);
// delegate implementation for ascending sort
bool SortAscending( int, int );
// delegate implementation for descending sort
bool SortDescending( int, int );
};
// end class BubbleSort
类 BubbleSort 实现如下:
#include "BubbleSort.h"
BubbleSort::BubbleSort()
{
PopulateArray();
}
// end constructor
// delegate implementation for ascending sort
bool BubbleSort::SortAscending( int element1, int element2 )
{
return element1 > element2 ;
}
// end method SortAscending
// delegate implementation for descending sort
bool BubbleSort::SortDescending( int element1, int element2 )
{
return element1 < element2 ;
}
// end method SortDescending
// populate the array with random numbers
void BubbleSort::PopulateArray()
{
// create random-number generator
Random ^randomNumber = gcnew Random();
// populate elementArray with random integers
for( int i = 0; i < elementArray->Length; i++ )
elementArray[i] = randomNumber->Next( 100 );
}
// end method PopulateArray
// sort randomly generated numbers in ascending order
void BubbleSort::SortArrayAscending()
{
DelegateBubbleSort::SortArray( elementArray,
gcnew DelegateBubbleSort::Comparator( this,
&BubbleSort::SortAscending ) );
}
// end method SortArrayAscending
// sort randomly generated numbers in descending order
void BubbleSort::SortArrayDescending()
{
DelegateBubbleSort::SortArray( elementArray,
gcnew DelegateBubbleSort::Comparator( this,
&BubbleSort::SortDescending ) );
}
// end method SortArrayDescending
// return the contents of the array
String ^BubbleSort::ToString()
{
String ^contents;
for( int i = 0; i < elementArray->Length; i++ )
{
contents = String::Concat( contents,
elementArray[i].ToString(), L" " );
}
// end for
return contents;
}
// end method ToString
int main( array<System::String ^> ^args )
{
BubbleSort ^sortPtr = gcnew BubbleSort();
String ^output =
String::Concat( L"Unsorted array:\n", sortPtr->ToString() );
sortPtr->SortArrayAscending();
output = String::Concat( output, L"\n\nSorted ascending:\n",
sortPtr->ToString() );
sortPtr->SortArrayDescending();
output = String::Concat( output, L"\n\nSorted descending:\n",
sortPtr->ToString() );
MessageBox::Show( output, L"Demonstrating delegates" );
return 0;
}
// end main
14.11 C++/CLI 中的模板
在 C++/CLI 中,我们可以像本地 C++ 一样创建并使用托管类
模板或托管函数模板。例如:
template< typename >
ref class ManagedStack
{
…
};
这个托管类模板可以用于创建任何托管类型的 。托管类模板完
全支持本地类模板的所有特性,如非模板参数和显式特化。与
托管类型相同,托管类模板也不允许声明其他类或其他函数作
为自己的友元,但允许被声明本地类的友元。
14.12 C++/CLI 中的 .NET 泛型
在 C++/CLI 中,泛型提供了和本地 C++ 模板或托管模板相似
的软件复用功能。泛型是由公共语言运行时(CRL)所定义的。
定义托管泛型的语法类似于托管模板,但也存在如下区别:
① 使用泛型关键字 generic 替代模板关键字 template。
② 使用符号 % 替代模板形参引用符 & 表示传递引用语法。
③ 不支持使用泛型形参的默认值。
④ 不支持使用常量关键字 const。
1 泛型的定义、实现和使用
首先我们通过比较托管类模板 Stack 的定义代码和托管类泛
型 Stack 的定义代码,了解定义托管泛型的定义语法:
#ifndef STACK_H
#define STACK_H
template< typename T >
ref class Stack
{
public:
Stack( int = 10 );
// default constructor (Stack size 10)
// destructor
~Stack()
{
delete[] elements;
} // end ~Stack destructor
bool push( const T& );
// push an element onto the Stack
bool pop( T& );
// pop an element off the Stack
// determine whether Stack is empty
bool isEmpty() const
{
return top == -1;
} // end function isEmpty
// determine whether Stack is full
bool isFull() const
{
return top == size – 1;
} // end function isFull
private:
int size;
// # of elements in the Stack
int top;
// location of the top element (-1 means empty)
array< T > ^elements;
};
// template constructor
template< typename T >
// internal representation of the Stack
Stack< T >::Stack( int s )
: size( s > 0 ? s : 10 ),
// validate size
top( -1 ), // Stack initially empty
elements( gcnew array< T >( size )) // allocate memory for elements
{
} // end template Stack constructor
// push element onto Stack
// if successful, return true; otherwise, return false
template< typename T >
bool Stack< T >::push( T &pushValue )
{
if( !isFull() )
{
elements[ ++top ] = pushValue;
return true;
} // end if
// pop successful
// place item on Stack
return false;
// push unsuccessful
} // end template function push
// pop element off Stack
// if successful, return true; otherwise, return false
template< typename T >
bool Stack< T >::pop( T &popValue )
{
if( !isEmpty() )
{
popValue = elements[ top-- ]; // remove item from Stack
return true;
// pop successful
} // end if
return false;
// pop unsuccessful
} // end template function pop
#ifndef STACK_H
#define STACK_H
#include “stdafx.h”
generic< typename T >
ref class Stack
{
public:
Stack( int stackSize );
// constructor
// destructor
~Stack()
{
delete[] elements;
} // end ~Stack destructor
bool push( T% );
// push an element onto the Stack
bool pop( T% );
// pop an element off the Stack
// determine whether Stack is empty
bool isEmpty()
{
return top == -1;
} // end function isEmpty
// determine whether Stack is full
bool isFull()
{
return top == size – 1;
} // end function isFull
private:
int size;
// # of elements in the Stack
int top;
// location of the top element (-1 means empty)
array< T > ^elements;
};
// internal representation of the Stack
#include “stdafx.h”
#include “Stack.h”
// generic constructor
generic< typename T >
Stack< T >::Stack( int s )
: size( s > 0 ? s : 10 ),
// validate size
top( -1 ), // Stack initially empty
elements( gcnew array< T >( size )) // allocate memory for elements
{
} // end generic Stack constructor
// push element onto Stack
// if successful, return true; otherwise, return false
generic< typename T >
bool Stack< T >::push( T %pushValue )
{
if( !isFull() )
{
elements[ ++top ] = pushValue;
return true;
// pop successful
} // end if
return false;
// push unsuccessful
} // end generic function push
// place item on Stack
// pop element off Stack
// if successful, return true; otherwise, return false
generic< typename T >
bool Stack< T >::pop( T %popValue )
{
if( !isEmpty() )
{
popValue = elements[ top-- ]; // remove item from Stack
return true;
// pop successful
} // end if
return false;
// pop unsuccessful
} // end generic function pop
不难发现,模板的定义和实现代码是被写在 Stack.h 一个文件
中,而泛型的定义和实现代码是被写在 Stack.h 和 Stack.cpp 两
个文件中。这也是两者一个主要区别。这是因为模板是在编
译时进行特化的,因此编译器要求模板的完整代码出现在使
用这个模板的客户源代码文件中。所以将托管类模板的定义
和实现代码写在同一个头文件中,便于满足编译器拥有生成
模板特化所需要的所有代码。泛型是在运行时特化的,编译
器只要一次性地产生泛型类的一个运行副本,每次泛型特化
在运行时引用这个副本。由于只需要编译泛型类的一份副
本,因此我们可以把定义与实现相分离,并在编译泛型的实
现和使用的代码中包含定义文件。泛型的使用代码如下:
#include “stdafx.h”
#include “Stack.h”
using namespace System
// generic function to manipulate Stack< T >
generic< typename T >
void testStack( Stack< T > ^theStack, array< T > ^values,
String ^stackName )
{
Console::WriteLine( “Pushing elements onto {0}”, stackName );
// push elements onto Stack
for each( T value in values )
{
if( theStack->push( value ))
Console::Write( “{0} ”, value );
else
{
Console::writeLine( “\nStack is full. Cannot push {0}”, value );
break;
} // end else
} // end for each
Console::WriteLine( “\nPoping elements from {0}”, stackName );
T popedValue
// holds the value poped from the Stack
// pop elements from Stack
while( theStack->pop( popedValue ))
Console::Write( “{0} ”, popedValue );
Console::WriteLine( “\nStack is empty. Cannot pop” );
} // end function testStack
int main( array< System::String^ > ^args )
{
array< double > ^doubleValues = { 1.1, 2.2, 3.3, 4.4, 5.5, 6.6 };
array< int > ^intValues = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
Stack< double > ^doubleStack = genew Stack< double >( 5 );
Stack< int > ^intStack = gcnew Stack< int >( 10 );
testStack( doubleStack, doubleValues, “doubleStack” );
testStack( intStack, intValues, “intStack” );
return 0;
} // end main
泛型类 Stack 的测试结果如下:
Pushing elements onto doubleStack
1.1 2.2 3.3 4.4 5.5
Stack is full. Cannot push 6.6
Poping elements from doubleStack
5.5 4.4 3.3 2.2 1.1
Stack is empty. Cannot pop
Pushing elements onto intStack
1 2 3 4 5 6 7 8 9 10
Stack is full. Cannot push 11
Poping elements from intStack
10 9 8 7 6 5 4 3 2 1
Stack is empty. Cannot pop
2 泛型类型约束
由于类泛型和函数泛型都是在运行时进行特化的,编译器并
不知道什么类型将会作为运行时的特化实参。如果作为实参
的类型能够适应类泛型和函数泛型定义的操作,则特化运行
将成功;否则特化运行将失败。假如我们在定义泛型时增加
某种类型约束,就可在编译使用泛型的代码时检查特化实参
的类型是否符合类型约束,即支持泛型中的所有函数和运算
符,就可避免特化运行的失败。类模板和函数模板的特化是
在编译时进行的,编译器知道将作为特化实参的所有类型,
并可检查它们是否支持模板中的所有函数和运算符,不存在
泛型那样的限制,因此定义模板时就不需要增加类型约束。
下面我们来讨论一个函数泛型 maximum。该函数判断并返回
它的三个同一类型特化实参中最大的那个,需要使用 > 运算
符。只要一个类型实现了泛型接口 IComparable< T >(位于
System 命名空间中),就可以对这种类型的两个对象进行比
较。FCL 中对应于基本类型的结构都实现了这个接口。例
如,基本类型 double 的对应结构 Double,基本类型 int 的对应
结构 Int32 都实现了 IComparable< T > 接口。因此,这些类型
的对象就可以按如下表达式调用 IComparable< T > 接口的成员
函数 CompareTo 进行比较:
double double1, double2;
…
double1.CompareTo( double2 );
因此,可用检测特化实参类型是否实现了 IComparable< T > 接
口作为函数泛型 maximum 的类型约束。该泛型的代码如下:
#include “stdafx.h”
using namespace System;
// generic function determines the largest of there IComparable objects
generic< typename T > where T : IComparable< T >
T maximum( T x, T y, T z )
{
T max = x;
// assume x is initially the largest
// compare y with max
if( y->CompareTo( max ) > 0 )
max = y;
// compare z with max
if( z->CompareTo( max ) > 0 )
max = z;
return max;
} // end function maximum
int main( array< System::String^ > ^args )
{
Console::WriteLine( “Maximum of {0}, {1} and {2} is {3}\n”,
3, 4, 5, maximum( 3, 4, 5 ));
Console::WriteLine( “Maximum of {0}, {1} and {2} is {3}\n”,
6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ));
Console::WriteLine( “Maximum of {0}, {1} and {2} is {3}\n”,
“pear”, “apple”, “orange”, maximum( “pear”, “apple”, “orange” ));
return 0;
} // end main
上述代码的运行结果如下:
Maximum of 3, 4 and 5 is 5
Maximum of 6.6, 8.8 and 7.7 is 8.8
Maximum of pear, apple and orange is pear
3 模板和泛型的比较
从模板和泛型所提供的复用功能来看,似乎两者同时存在有
些多余,但实际上它们存在着本质的区别。下面我们对它们
的关键区别进行概括:
① 泛型是在运行时特化的,而模板是在编译时特化的。
② 泛型类型无法作为模板类型参数,但模板类型可以作为泛
型类型参数。
③ 泛型使用类型约束限制在泛型代码中可以使用的类型
④ 泛型并不支持非类型参数或默认值。
⑤ 泛型并不支持显式特化,也不支持部分特化。
⑥ 泛型类型参数必须是托管引用类型的句柄、接口类型句柄
或值类型。
泛型通常是跨程序集的。一个类泛型或函数泛型经过编译的
代码就放在项目的程序集中。由于类型参数是在运行时被替
换的,因此其他项目可以通过引用适当的程序集创建这些泛
型的特化。通过引用包含了编译的泛型代码集,泛型还可以
被由其他 .NET 语言所编写的代码使用。而模板的特化代码都
是在编译时创建的模板本身不存在于被编译的代码中。因此
无法在其他项目中创建模板的其他特化。
泛型和继承机制在以下几个方面存在联系:
① 类泛型只支持公有单继承。试图对类泛型使用私有或保护
类型的继承,或者从多个类继承都会导致编译错误。
② 类泛型可以从另一个类泛型派生。
③ 类泛型可以从一个非泛型类派生。
④ 非泛型类可以从一个类泛型派生。
和 C++/CLI 中的所有托管类一样,类泛型可以被声明为本地
类的友元,但它们本身不能声明友元。
泛型采用与模板相同的方式处理静态成员。类泛型的每个新
特化都具有这个类泛型的每个静态数据成员的一份单独副
本。这个特化的所有对象共享这份静态数据成员。