C++CLI(⼀)-C++CLI简介
随着C++的深⼊学习Unity与各⼤C++机器学习类库算法的编写和调⽤需求,⽤C++/CLI与Unity的C#之间建⽴关联性也越来越显得重要(怎么感觉C++是个⼤坑。。。。。)
程序集与元数据
writeline函数传统的C++编译模式包括把单独的源⽂件编译为⽬标⽂件(obj),再把⽬标⽂件与库函数链接在⼀起,以⽣成可执⾏程序。⽽CLI模式却⼤不相同,它涉及到程序集的创建与使⽤(类似C#的编译过程)。简单来说,在不计输⼊源⽂件数⽬的基础上,程序集即为单次编译的输出。如果输出带有⼀个进⼊点函数(例如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;
/*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(中间有空格)表⽰了⼀个新的关键词。
public前缀表明了类型在它的⽗类程序集之外可见——即可访问(只有两种类型的可见性,public和private,类型默认为private),另外,只有类型才能有可见性属性,⾮成员函数、全局变量及⽂件范围内的typedef都不能在它们的⽗类程序集之外访问。
与C++程序员预想的⼀样,除了默认的成员可访问性,⼀个引⽤结构(ref struct)与引⽤类基本上⼀模⼀样,在这,我们把两者都称为引⽤类。
每个引⽤类都有⼀个基类,如果没有显式指定,那么默认的基类即为System::Object,⼀个引⽤类有且只能有⼀个基类。
我们先不管Point在内部是怎么表⽰的,考虑到它有X与Y属性,我们在此使⽤了笛卡尔坐标,实现起来⾮常简单;如果它使⽤极坐标,那么就复杂多了。
作为成员的标量属性,也对实例提供了类似字段的访问性,在标记3(a)中,⽤int类型定义了⼀个X属性,property符号是⼀个上下⽂关键字,⽽不是⼀个全局保留的关键字,它的⽤法只限于在这个上下⽂中。
]对于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:
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⽬录中⽣成了,执⾏此⽂件,就可以看到相应的输出。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论