C++最强⼤的.NETFramework编程语⾔
C++ :最强⼤的 .NET Framework 编程语⾔
本⽂涉及:
最容易上手的编程语言
Microsoft Visual C++ 2005
Microsoft Visual C++ .NET
Microsoft Visual Studio 2005
通⽤语⾔运⾏时库(CLR)
⽂章概要:探索Visual C++ 2005中新语⾔C++/CLI的设计思想与基本原理,并以此 .NET编程语⾔,编写功能强⼤的 .NET应⽤程序。
内容:
简介
对象构造
内存管理 vs 资源管理
内存管理
资源管理
再论类型
装箱
编写引⽤和值类型
可访问性
属性
代理
结束语
简介
Visual C++开发⼩组花了⼤量的时间⽤于听取⽤户的意见,在对 .NET和C++经过仔细考量之后,决定在Visual C++ 2005中重新设计对通⽤语⾔运⾏时库(CLR)的⽀持,此项重新的设计被称为“C++/CLI”,它将为使⽤及编写CLR类型提供更⾃然的语法。在本⽂中,主要探讨了新的语法,并将之与C#和托管C++这两个CLR平台上极其相近的语⾔进⾏⽐较,在⽂中也会适当地以图表给出其与本地
C++的相似之处。
通⽤语⾔运⾏时库(CLR)包括了⼀组规范,其是 Microsoft .NET的基础,也是CLI的Microsoft版本实现。C++/CLI语⾔设计的⽬标是为了对CLI提供更⾃然的C++⽀持,⽽Visual C++ 2005的编译器则在CLR上实现了C++/CLI。
当在仔细研究了Visual C++ 2005编译器和C++/CLI语⾔设计之后,就会发现它们传达了两条重要的讯息;⾸先,Visual C++把⾃⼰定位于在CLR平台上的最低级编程语⾔,(看起来似乎没有必要使⽤其他语⾔了——包括MSIL);其次,.NET编程应与本地C++编程⼀样⾃然。
本⽂针对C++程序员,但并不想说服你放弃C#或者Visual Basic .NET。如果你⾮常喜欢C++,并想继续使⽤传统C++提供的全部功能,⽽⼜想要C#般的编程效率,那本⽂正适合你。另外,本⽂并不是CLR或.NET Framework的简介,⽽把重点放在Visual
C++ 2005是怎样使你可以编写.NET Framework上更优雅和⾼效的代码。
对象构造
CLR定义了两种类型:值类型和引⽤类型。值类型被设计⽤于可进⾏⾼效地分配和访问,它们与C++的内置类型⼤体相似,你也能创建属于你⾃⼰的类型,这就是Bjarne Stroustrup所称的具体类型;另⼀⽅⾯,引⽤类型被设计⽤于提供⾯向对象编程所需的特性,可⽤于创建有着层次结构的类:例如派⽣类和虚拟函数。另外在CLR中,引⽤类型⾃始⾄终都提供了额外的运⾏时特性,如⾃动内存管理——通常称为垃圾回收。同时,对引⽤类型和值类型,CLR也提供了精确的运⾏时类信息,这种特性通常被称为反射。
值类型分配在堆栈上;⽽引⽤类型通常分配在托管堆中——这是由CLR垃圾回收机制所管理的堆。如果你在C++中编写汇编代码,如平时那样,可在CRT堆中分配本地C++类型,在将来,Visual C++开发⼩组甚⾄允许你在托管堆中分配本地C++类型,毕竟,垃圾回收对本地类型来说,也是⼀个极具吸引⼒的主题。
本地C++允许选择在何处创建⼀个特定的对象,任何类型都可分配在堆栈或CRT堆中。
// 分配在堆栈上
std::wstring stackObject;
// 分配在CRT堆中
std::wstring* heapObject = new std::wstring;
如上所⽰,在何处分配对象是独⽴于类型的,主动权完全掌握在程序员的⼿中。另外,堆栈与堆的分配语法也是易于区别的。
另⼀⽅⾯,C#通常是在堆栈上创建值类型,⽽在托管堆中创建引⽤类型。下例中使⽤的System.DateTime类型,被声明为值类型。
// 分配在堆栈上
System.DateTime stackObject = new System.DateTime(2003, 1, 18);
// 分配在托管堆中
System.IO.MemoryStream heapObject = new System.IO.MemoryStream();
如上例所⽰,声明对象的⽅式并没有指出对象分配在堆栈上或托管堆中,其完全取决于程序编写者和
运⾏时库。
C++的托管扩展——简称为托管C++,可在本地C++代码中混合托管代码。为了遵循C++标准,C++被加⼊了扩展,以提供对CLR的全⾯⽀持。不幸的是,正是因为有太多的扩展,所以如果要⽤C++来编写⼤量的托管代码,就成了⼀件异常痛苦的事。
//分配在堆栈上
DateTime stackObject(2003, 1, 18);
//分配在托管堆中
IO::MemoryStream __gc* heapObject = __gc new IO::MemoryStream;
在C++程序员看来,在堆栈上分配⼀个值类型看起来⾮常正常,⽽在托管堆中的分配⽅式,看起来就有点怪:__gc是托管C++扩展中的⼀个关键字,有意思的是,在某些情况下,托管C++能推断你的意思,所以上述例⼦可重写为不带__gc关键字。
//分配在托管堆中
IO::MemoryStream* heapObject = new IO::MemoryStream;
这样看起来更像本地C++代码了——但heapObject并不是⼀个真正的C++指针。C++程序员通常倾向于在指针中保存⼀个不变的数值,但垃圾回收器会在任何时候,在内存中移动对象。另⼀个不⾜之处是,不能仅仅依靠查看代码,就能确定对象是分配在本地还是托管堆中,必须知道程序编写者是怎样定义⼀个类型的。
C++/CLI为此引⼊了句柄的概念,以便把CLR对象引⽤与C++指针区别开来。少了C++指针含义的重载,语⾔中也少了很多歧义,另外,通过句柄,对CLR也能提供更加⾃然的⽀持,例如,你能在C++中,直接对引⽤类型使⽤操作符重载,因为此时句柄已经能⽀持操作符重载了。由于C++禁⽌指针操作符重载,如果没有“托管”指针,这⼏乎不可能实现。
//分配在堆栈上
DateTime stackObject(2003, 1, 18);
//分配在托管堆中
IO::MemoryStream^ heapObject = gcnew IO::MemoryStream;
相对于值类型声明来说,和以前没什么不同,但对引⽤类型声明来说,变化却很明显,操作符^ 把变量声明为对⼀个CLR引⽤类型的句柄。当垃圾回收器在内存中移动被引⽤的对象时,同时也会⾃动更
新句柄的值。另外,它们是可重绑定的,这允许它们可像C++指针那样指向不同的对象。另外需注意的⼀件事是,操作符gcnew已经代替了操作符new,清楚地指明了对象被分配在托管堆中。对托管类型,操作符new已经不能被重载(此处并⾮语带双关),只能把对象分配在CRT堆中,除⾮你提供⾃⼰重写的new操作符。
简⽽⾔之:本地C++指针已经与CLR对象引⽤⼤不相同了。
内存管理 vs 资源管理
当运⾏环境中包含垃圾回收机制时,区别开内存管理和资源管理,就⾮常重要了。典型地来说,垃圾回收器只对包含对象的内存之分配与释放感兴趣,它可不关⼼你的对象是否拥有其他的资源,如数据库连接或核⼼对象的句柄。
内存管理
本地C++为程序员提供了超越内存管理的直接控制能⼒,在堆栈上分配⼀个对象,意味着只有在进⼊特定函数时,才会为对象分配内
存,⽽当函数返回或堆栈展开时,内存被释放。可使⽤操作符new来动态地为对象分配内存,此时内存分配在CRT堆中,并且需要程序员显存地对对象指针使⽤操作符delete,才能释放它。这种对内存
的精确控制,也是C++可⽤于编写极度⾼效的程序的原因之⼀,但如果程序员不⼩⼼,这也是内存泄漏的原因。另⼀⽅⾯,你不需要求助于垃圾回收器来避免内存泄漏——实际上这是CLR所采取的⽅法,⽽且是⼀个⾮常有效的⽅法,当然,对于垃圾回收堆,也有其他⼀些好处,如改进的分配效率及引⽤位置相关的优势。所有这⼀切,都可以在C++中通过库⽀持来实现,但除此之处,CLR还提供了⼀个单⼀的内存管理编程模型,其对所有的编程语⾔都是通⽤的,想⼀想与C++中COM⾃动化对象相交互和调度数据类型所需做的⼀切⼯作,就会发现其重要意义所在——横跨数种编程语⾔的垃圾回收器,作⽤是⾮常巨⼤的。
为了效率,CLR也保留了堆栈的概念,以便值类型可在其上分配,但CLR也提供了⼀个newobj中间语⾔指令,以在托管堆中分配⼀个对象,但此指令只在C#中对引⽤对象使⽤操作符new时提供。在CLR中,没有与C++中的delete操作符对应的函数,当应⽤程序不再引⽤某对象时,分配的内存最后将由垃圾回收器回收。
当操作符new应⽤于引⽤类型时,托管C++也会⽣成newobj指令,当然,对此使⽤delete操作符是不合法的。这确实是⼀个⽭盾,但同时也证明了为什么⽤C++指针概念来表⽰⼀个引⽤类型不是⼀个好的做法。
在内存管理⽅⾯,除了上述在对象构造⼀节讨论过的内容,C++/CLI没有提供任何新的东西;资源管理,才是C++/CLI的拿⼿好戏。
资源管理
CLR只有在资源管理⽅⾯,才能胜过本地C++。Bjarne Stroustrup的“资源获取即初始化”的技术观点,基本定义了资源类型的模式,即类的构造函数获取资源,析构函数释放资源。这些类型是被当作堆栈上的局部对象,或复杂类型中的成员,其析构函数⾃动释放先前分配的资源。⼀如Stroustrup所⾔“对垃圾回收机制来说,C++是最好的语⾔,主要是因为它⽣成很少的垃圾。”
也许有⼀点令⼈惊讶,CLR并没有对资源管理提供任何显式运⾏时⽀持,CLR不⽀持类似析构函数的C++概念,⽽是在.NET Framework中,把资源管理这种模式,提升到⼀个IDisposable核⼼接⼝类型的中⼼位置。这种想法源⾃包装资源的类型,理应实现此接⼝的单⼀Dispose⽅法,以便调⽤者在不再使⽤资源时,可调⽤该⽅法。不必说,C++程序员会认为这是时代的倒退,因为他们习惯于编写那些缺省状态下清理就是正确的代码。
因为必须要调⽤⼀个⽅法来释放资源,由此带来的问题是,现在更难编写“全⽆异常”的代码了。因为异常随时都可能发⽣,你不可能只是简单地在⼀段代码后,放置⼀个对对象的Dispose⽅法的调⽤,这样做的话,就必须要冒资源泄漏的风险。在C#中解决这个问题的办法是,使⽤try-finally块和using语句,在⾯对异常时,可提供⼀个更可靠的办法来调⽤Dispose⽅法。有时,构造函数也会使⽤这种⽅法,但⼀般的情况是,你必须要记住⼿⼯编写它们,如果忘记了,⽣成的代码可能会存在⼀个悄⽆声息的错误。对缺乏真正析构函数的语⾔来说,是否需要try-finally块和using语句,还有待论证。
using (SqlConnection connection = new SqlConnection("Database=master; Integrated Security=sspi"))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = "sp_databases";
command.CommandType = CommandType.StoredProcedure;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine(reader.GetString(0));
}
}
}
对托管C++来说,情节也⾮常类似,也需要使⽤⼀个try-finally语句,但其是Microsoft对C++的扩展。虽然很容易编写⼀个简单的Using模板类来包装GCHandle,并在模板类的析构函数中调⽤托管对象的Dispose⽅法,但托管C++中依然没有C# using语句的对等物。
Using<SqlConnection> connection(new SqlConnection(S"Database=master; Integrated Security=sspi"));
SqlCommand* command = connection->CreateCommand();
command->set_CommandText(S"sp_databases");
command->set_CommandType(CommandType::StoredProcedure);
connection->Open();
Using<SqlDataReader> reader(command->ExecuteReader());
while (reader->Read())
{
Console::WriteLine(reader->GetString(0));
}
想⼀下C++中对资源管理的传统⽀持,其对C++/CLI也是适⽤的,但C++/CLI的语⾔设计犹如为C++资源管理带来了⼀阵轻风。⾸先,在编写⼀个管理资源的类时,对⼤部分CLR平台语⾔来说,其中⼀个问题是怎样正确地实现Dispose模式,它可不像本地
C++中经典的析构函数那样容易实现。当编写Dispose⽅法时,需要确定调⽤的是基类的Dispose⽅法——若有的话,另外,如果选择通过调⽤Dispose⽅法来实现类的Finalize⽅法,还必须关注并发访问,因为Finalize⽅法很可能被不同的线程所调⽤。此外,与正常程序代码相反,如果Dispose⽅法实际上是被Finalize⽅法调⽤的,还需要⼩⼼仔细地释放托管资源。
C++/CLI并没有与上述情况脱离得太远,但它提供了许多帮助,在我们来看它提供了什么之前,先来快速回顾⼀下如今的C#和托管C++有多么接近。下例假设Base从IDisposable派⽣。
class Derived : Base
{
public override void Dispose()
{
try
{
//释放托管与⾮托管资源
}
finally
{
base.Dispose();
}
}
~Derived() //实现或重载Object.Finalize⽅法
{
//只释放⾮托管资源
}
}
托管C++也与此类似,看起来像析构函数的代码其实是⼀个Finalize⽅法,编译器实际上插⼊了⼀个try-finally块并调⽤基类的Finalize⽅法,因此,C#与托管C++相对容易编写⼀个Finalize⽅法,但在编写Dispose⽅法时,却没有提供任何帮助。程序员们经常使⽤Dispose⽅法,把它当作⼀个伪析构函数以便在代码块末执⾏⼀点其他的代码,⽽不是为了释放任何资源。
C++/CLI认识到了Dispose⽅法的重要性,并在引⽤类型中,使之成为⼀个逻辑“析构函数”。
ref class Derived : Base
{
~Derived() //实现或重载IDisposable::Dispose⽅法
{
//释放托管与⾮托管资源
}
!Derived() //实现或重载IDisposable::Dispose⽅法
{
//只释放⾮托管资源
}
};
对C++程序员来说,这让⼈感觉更⾃然了,能像以往那样,在析构函数中释放资源了。编译器会⽣成必要的IL(中间语⾔)来正确实现IDisposable::Dispose⽅法,包括抑制垃圾回收器调⽤对象的任何Finalize⽅法。事实上,在C++/CLI中,显式地实现Dispose ⽅法是不合法的,⽽从IDisposable继承只会导致⼀个编译错误。当然,⼀旦类型通过编译,所有使⽤该类型的CLI语⾔,将只会看到
Dispose模式以其每种语⾔最⾃然的⽅式得以实现。在C#中,可以直接调⽤Dispose⽅法,或使⽤⼀个using语句——如果类型定义在C#中。那么C++呢?难道要对堆中的对象正常地调⽤析构函数?此处当然是使⽤delete操作符了,对⼀个句柄使⽤delete操作符将会调⽤此对象的Dispose⽅法,⽽回收对象的内存是垃圾回收器该做的事,我们不需要关⼼释放那部分内存,只要释放对象的资源就⾏了。
Derived^ d = gcnew Derived();
d->SomeMethod()
delete d;
如果表达式中传递给delete操作符的是⼀个句柄,将会调⽤对象的Dispose⽅法,如果此时再没有其他对象链接到引⽤类型,垃圾回收器就会释放对象所占⽤的内存。如果表达式中是⼀个本地C++对象,在释放内存之前,还会调⽤对象的析构函数。
毫⽆疑问,在对象⽣命期管理上,我们越来越接近⾃然的C++语法,但要时刻记住使⽤delete操作符,却不是件易事。C++/CLI
允许对引⽤类型使⽤堆栈语义,这意味着你能⽤在堆栈上分配对象的语法来使⽤⼀个引⽤类型,编译器会提供给你所期望的C++语义,⽽在底层,实际上仍是在托管堆中分配对象,以满⾜CLR的需要。
Derived d;
d.SomeMethod();
当d超出范围时,它的Dispose将会被调⽤,以释放它所占⽤的资源。再则,因为对象实际是在托管堆中分配的,所以垃圾回收器会在它的⽣命期结束时释放它。来看⼀个ADO.NET的例⼦,它与C++/CLI中的概念⾮常相似。
SqlConnection connection("Database=master; Integrated Security=sspi");
SqlCommand^ command = connection.CreateCommand();
command->CommandText = "sp_databases";
command->CommandType = CommandType::StoredProcedure;
connection.Open();
SqlDataReader reader(command->ExecuteReader());
while (reader.Read())
{
Console::WriteLine(reader.GetString(0));
}
再论类型
在讨论装箱(boxing)之前,有必要弄清楚为什么值类型与引⽤类型之间会有所区别。
⼀个含有数值的值类型的实例,和⼀个指向对象的引⽤类型的实例,它们有什么区别呢?除了存储对象所需的内存之外,每⼀个对象都会有⼀个对象头,⽬的是为⾯向对象的编程提供基本的服务,如存在虚⽅法的类,嵌⼊其中的元数据等等。由虚⽅法和接⼝间接结合的对象头,其内存开销通常会很⼤,哪怕你所需要的只是⼀个静态类型的数值,也会带来⼀些编译器的强制操作。有趣的是,在某些
情况下,编译器能优化掉⼀些对象开销,但不总是能起作⽤。如果你⾮常在意托管代码的执⾏效率,那么使⽤数值或值类型将会有所益处,但在本地
C++的类型中,这不算⼀个很⼤的区别,当然,C++也没有强制任何编程范式,所以也有可能在C++之上,通过创建库来建⽴⼀个这样截然不同的类型系统。
装箱
什么是装箱(boxing)?装箱是⼀种⽤来桥接数值和对象的机制。尽管CLR的每种类型都是直接或间接从Object类派⽣⽽来,但数值却不是。⼀个堆栈上的数值(如整形int),只不过是⼀个编译器会进⾏某种特定操作的内存块。如果你实在想把⼀个数值当成⼀个对象,必须对数值调⽤从Object继承⽽来的⽅法,为了实现这⼀点,CLR提供了装箱的概念。知道⼀点装箱的原理还是有点⽤的,⾸先,⼀个数值通过使⽤ldloc IL指令⼊栈,接下来,装箱IL指令运⾏,把数值类型提升,CLR再把数值出栈,并分配⾜够的空间存储数值与对象头,然后⼀个对新建对象的引⽤被压⼊栈,所有这些就是装箱指令要做的事。最后,为取得对象引⽤,stloc IL指令从堆栈中弹出引⽤,并把它存储在局部变量中。
现在,问题是:在编程语⾔中,对数值的装箱操作,是应该表现为隐式还是显式呢?换句话说,是否应使⽤⼀个显式转换或其他构造函数呢?C#语⾔设计者决定做成隐式转换,毕竟,⼀个整形数是从Object间接继承来的Int32类型。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。