实例解析C++/CLI之开卷有益
副标题#e#
C++/CLI可以说是尺度C++语言一种新的"方言",它是Microsoft为充实操作CLI(Common Language Infrastructure)平台而开拓出来的。那么,它在语言方面有何新颖独到之处呢,下面,就让我们一起开始奇妙的C++/CLI语言之旅(文中所有示例代码,均以Visual Studio.NET 2005 Professional编译通过,所有的讲授内容,也均以Visual Studio.NET 2005情况为基本)。
措施集与元数据
传统的C++编译模式包罗把单独的源文件编译为方针文件(obj),再把方针文件与库函数链接在一起,以生成可执行措施。而CLI模式却大不沟通,它涉及到措施集的建设与利用。
简朴来说,在不计输入源文件数目标基本上,措施集即为单次编译的输出。假如输出带有一个进入点函数(譬喻main函数),它即为一个.exe文件;假如没有,它则为一个.dll文件。任何引用外部措施集而生成的编译,必需要会见所依赖的措施集,此时也没有雷同传统链接时用到的头文件机制,而是通过编译器在所依赖的措施集内部查找,来会见所需的外部信息。
措施集包括了元数据,其描写了包括在哪里的范例与函数,尚有CIL(Common Intermediate Language)指令–Microsoft称其为"MSIL"。元数据与指令能通过独立的VES(Virtual Execution System)来执行。
CLI范例
例1是一个模仿二维点的类。此处不得不提到定名空间,所有的CLI尺度库范例都属于System定名空间,或嵌套在其内部的某个定名空间之下,譬喻System::Object和System::String,尚有System::IO、 System::Text、System::Runtime::CompilerOptions等等。标志1可制止在措施中一直利用namespace限定词。
例1:
/*1*/
using namespace System;
/*2*/
public ref class Point
{
int x;
int y;
public:
//界说用于读写X与Y实例属性
/*3a*/ property int X
{
/*3b*/ int get() { return x; }
/*3c*/ void set(int val) { x = val; }
}
/*4a*/ property int Y
{
/*4b*/ int get() { return y; }
/*4c*/ void set(int val) { y = val; }
}
//界说实例结构函数
/*5a*/ Point()
{
/*5b*/ X = 0;
/*5c*/ Y = 0;
}
/*6a*/ Point(int xor, int yor)
{
/*6b*/ X = xor;
/*6c*/ Y = yor;
}
//界说实例要领
/*7a*/ void Move(int xor, int yor)
{
/*7b*/ X = xor;
/*7c*/ Y = yor;
}
/*8a*/ virtual bool Equals(Object^ obj) override
{
/*8b*/ if (obj == nullptr)
{
return false;
}
/*8c*/ if (this == obj) //我们在测试本身吗?
{
return true;
}
/*8d*/ if (GetType() == obj->GetType())
{
/*8e*/ Point^ p = static_cast<Point^>(obj);
/*8f*/ return (X == p->X) && (Y == p->Y);
}
return false;
}
/*9*/ virtual int GetHashCode() override
{
return X ^ (Y << 1);
}
/*10a*/ virtual String^ ToString() override
{
/*10b*/ return String::Concat("(", X, ",", Y, ")");
}
};
在标志2中,我们界说了一个称为Point的引用类(ref class),一个引用类是一个CLI引用范例,当两者一起利用时,ref与class(中间有空格)暗示了一个新的要害词。
#p#副标题#e#
public前缀表白了范例在它的父类措施集之外可见–即可会见(只有两种范例的可见性,public和private,范例默认为private),别的,只有范例才气有可见性属性,非成员函数、全局变量及文件范畴内的typedef都不能在它们的父类措施集之外会见。
与C++措施员预想的一样,除了默认的成员可会见性,一个引用布局(ref struct)与引用类根基上一模一样,在这,我们把两者都称为引用类。
每个引用类都有一个基类,假如没有显式指定,那么默认的基类即为System::Object,一个引用类有且只能有一个基类。
我们先不管Point在内部是怎么暗示的,思量到它有X与Y属性,我们在此利用了笛卡尔坐标,实现起来很是简朴;假如它利用极坐标,那么就巨大多了。
作为成员的标量属性,也对实例提供了雷同字段的会见性,在标志3(a)中,用int范例界说了一个X属性,property标记是一个上下文要害字,而不是一个全局保存的要害字,它的用法只限于在这个上下文中。
#p#分页标题#e#
对付get与set存取措施,在一个属性中即可有任意一个,也可两者兼有。在标志3(b)中,get返回既定属性的值;而在标志3(c)中,set利用编程者提供的值来配置即定的属性值。这两个存取措施别离以名字get与set界说为单独的函数,必需接管或返回相应的声明范例值,在本例中,为int(留意,这两个名字不是要害字)。存取措施也能具有差异的可会见性,但大概会故障到语言间的互操纵性(interop),因为其他CLI语言大概不支持。
在标志5(b)与5(c)代表的默认结构函数中,是利用set的简朴例子–X与Y均被配置为零,留意,不能利用X=Y=0来取代,因为set为一个void返回范例,所以子表达式Y=0不能呈此刻另一个表达式中。
对一个引用类来说,相等性是通过函数Equals来实现的,而不是重载==操纵符,如标志8(a)所示。因为Point重载了System::Object::Equals,所以Point::Equals必需被声明为virtual,再次提醒的是,override标记也是一个上下文要害字,而不是一个保存要害字。而这个函数重载了Object中的一个函数,所以需要接管一个Object作为参数,而不是一个Point。
实际上,参数带有范例Object^,其暗示"Object的句柄",并指向托管堆(垃圾接纳)中的一个工具。句柄在此是一个C++/CLI术语,CLI实际上把它称为"引用",但C++已经有引用了,这是两回事。
有履历的C++类设计人员大概会寄望到,在这个类的界说中,缺乏了两个重要的对象:函数未const限定;且参数不是作为一个const句柄通报的。为什么会这样呢?因为引用类的成员函数不会用const来限定,CLI也没有观念上的const函数;把参数声明为一个const句柄将会使它成为另一种范例,这样它就不再能被System::Object::Equals重载了(const范例的句柄是答允的,但它们只能被用在一个C++/CLI上下文之内,而不能与任何CLI尺度库函数一起利用的,因为今朝CLI中还未有const这个观念,将来版本的C++/CLI有大概会全面支持const,但其他语言仍不会支持const)。
在标志8(b)中,我们把obj与nullptr作一较量。nullptr要害字暗示常量空值,当利用在一个句柄上下文中时,它暗示空句柄–没有指向任何工具的句柄;当利用在一个指针上下文中时,它暗示空指针–没有包括任何地点的指针。
为防备自身较量,在标志8(c)中,把obj与this作一比拟。在一个非引用类(指当地类)中,this是一个实例函数挪用时指向工具的指针,可带有const限定符;在一个引用类中,则是实例函数挪用时指向工具的句柄–此处要再次提醒各人,不答允带有const限定符。也可以通过雷同以指针会见成员时的指向操纵符 ->,来会见类中成员,只不外此处利用的是句柄。
Equals是为了确保其较量的两个工具有着沟通的范例,所以在标志8(d)中挪用了System::Object::GetType,其返回一个代表当前实例运行时范例的System::Type句柄,假如两个System::Type工具引用指向同一工具,则它们代表了同一范例。此处,我们较量的是两个句柄,而不是两个范例工具。
一旦你获知两个工具为同一范例,就可以安详地把Object句柄向上转换为一个Point句柄,进而执行数据较量,而不消担忧产生错误的范例匹配这样的异常,在此,利用了static_cast。
为使哈希表(散列表)数据布局事情正常,在工具中必需有一个名为GetHashCode的函数。根基上,假如一个范例界说了Equals,它也应该同时界说GetHashCode,其是重载System::Object的版本,如标志9。
与相等性较量雷同,值的名目化是通过一个重载System::Object的函数实现的,如标志10(a),而不是重载<<操纵符。这个函数称为ToString,它的成果是建设并返回一个当前实例的字符串,它挪用了System::String::Concat毗连三个字符串及两个int,实现了所需成果。
毫无疑问,不行能对任一参数及范例的搭配,Concat都能有一个适当的重载版本,那么,Concat是奈何处理惩罚这些参数的呢?本例中利用的重载版本如下: static String^ Concat(… array<Object^>^ list);
圆括号中的参数声明(其必需有一托管的数组范例),表白可接管任意数量给定元素范例的参数,即,它是一个范例安详的varargs–参数数组,参数列表为一指向工具句柄托管数组的句柄。
那么这两个int–X与Y,是奈何转换为Object^的呢?其实,在根基数据范例对Object^的表达式中,都存在着一个隐式转换,这个进程称为"装箱",也就是包括根基数据范例值的工具,在托管堆上的分派。逆进程称为"解箱",这需要显式转换。
最后提一下定名约定。CLI指定了类、函数、属性必需以PascalCase模式来编写,也就是说,每个单词的首字母必需大写,而CLI尺度库也遵循这条原则。
一个简朴的示例措施
例2是一个利用了Point类的简朴措施,下面以此为例简朴讲授各方面的寄义:
例2:
#p#分页标题#e#
using namespace System;
int main()
{
/*1*/ Point^ p1 = gcnew Point;
/*2*/ Console::WriteLine("p1 = {0}, p1's HashCode = {1}", p1, p1->GetHashCode());
/*3*/ p1->Move(5, 7);
/*4*/ Console::WriteLine("p1 = {0}, p1's HashCode = {1}", p1, p1->GetHashCode());
/*5*/ Console::WriteLine("p1 Equals Point(9, 1) = {0}",
p1->Equals(gcnew Point(9, 1)));
}
分派托管内存:在标志1中,界说了一个指向Point范例的句柄,并用gcnew操纵符返回的位置初始化它,gcnew操纵符是一个要害字,它为一个新的Point工具在托管堆中,分派了相应的空间,与各人想的一样,此处还会挪用默认的结构函数。在今朝的C++/CLI版本中,引用类的工具只能驻留于仓库或托管堆中,与其他CLI语言差异,C++/CLI可以让你编写能被通报,并通过复制结构函数或 = 操纵符赋值的引用类,还可以重载Clone函数,实现虚拟(深度)赋值。 名目化输出:CLI提供了一系列的I/O范例–利用成果性注解的函数。最简朴的例子就是System::Console Write和WriteLine(见标志2)的重载版本,其向尺度输出设备输出文本,WriteLine会跟上一个新行,而Write则不会。
这类函数有很多重载的版本,然而,最常见的形式是接管一个包括文本的名目化字符串,并带有可选的名目指定符–由花括号举办脱离,其后紧接需要名目化其值的参数。名目指定符 {0} 对应于紧接着名目化字符勾串报进来的第一个参数;而 {1} 则对应于第二个参数,以此类推。与Concat雷同,也有一些接管几个牢靠参数的重载版本,或可接管几个牢靠参数并同时接管一个可变数目标参数,在本例中,利用了如下的版本:
static void WriteLine(String^ format, Object^ arg0, Object^ arg1);
字符串在此被隐式转换为String^。因为p1是一个Point^,且Point是从Object担任而来,所以p1是is干系。GetHashCode返回一个int,因此在被通报之前,会被装箱为Object^。一旦执行到WriteLine,它会挪用第二个和第三个参数的ToString函数,并输出功效字符串。以下是措施的输出:
p1 = (0,0), p1's HashCode = 0
p1 = (5,7), p1's HashCode = 11
p1 Equals Point(9, 1) = False
垃圾接纳:由句柄p1引用的内存驻留于托管堆中,而托管堆则处于垃圾接纳器"监督"之下,当一个句柄超出浸染域时,其引用的内存就少了一个与此相联的句柄,继而当句柄计数为零时,内存就被自动接纳了。假如一个句柄在某段时间内并没有超出浸染域,但你已不需要其引用的内存了,就可以配置句柄为nullptr来淘汰其的引用计数,在此,没有步伐来显式释放一块托管内存。别的,也可以对句柄挪用delete,它会顿时运行析构函数(Dispose函数),但这块内存仍不会被接纳,直到垃圾接纳器抉择接纳它。
编译措施
假如要把Point与main措施放在两个差异的措施会合,必需建设两个项目–为Point类建设Point项目,为应用措施建设Main项目。
要建设Point项目,可在Visual Studio.NET 2005中选择"文件|新建|项目|空项目"(不要选择"类库")。在"办理方案资源打点器"中找到"源文件",鼠标右键单击选择"添加|新建项",在对话框左边的种别栏中选择"代码",接着在右边选择"C++文件",输入Point名称,并在打开的文件中粘贴例1中代码,生存文件。
在"办理方案资源打点器"中,右键单击项目名Point,首先,选择"属性|设置属性|通例",把"设置范例"改为"动态库(.dll)",选择"民众语言运行库支持"为"民众语言运行库支持(/clr)";其次,在"C/C++|代码生成"中,把"运行时库"改为多线程 DLL (/MD);最后,在"链接器|通例"栏中,把"输出文件"后缀名从.exe改为.dll。
固然在选择"类库"时,这些都是由Visual Studio.NET 2005自动完成的,但它会生成一大堆你不需要的支持文件。此时,选择"生成",就会在Point\debug目次中找到Point.dll了。
建设Main项目与建设Point项目很是雷同,除了这个项目叫做"Main",且源文件为Main.cpp外。(在此有一个小能力,你可以运行Visual Stuio.NET的两个实例,这样,你就可以同时编辑两个项目了。)默认环境下,选择"空项目"会生成一个.exe文件,这正是我们想要的。因为Main.cpp引用了Point范例,所以需要汇报编译器在哪可以找这个范例的父类措施集:首先,在"办理方案资源打点器"中,右键单击项目名Main,依次选择选择"属性|设置属性|通例",选择"民众语言运行库支持"为"民众语言运行库支持(/clr)",点击对话框的"应用"按钮;其次,在"通用属性|引用|添加新引用"对话框中,选择"欣赏"选项页,定位至Point目次的Point.dll文件,点击"确定"退出;最后,在"C/C++|代码生成"中,把"运行时库"改为多线程 DLL (/MD)。此时,选择"生成",就会在Main\debug目次中生成Main.exe了,执行此文件,就可以看到相应的输出。