托管 C++ 入门






4.78/5 (19投票s)
让初学者开始使用托管 C++ 的尝试
引言
本文旨在作为托管 C++ 编程的快速入门教程。它并不涵盖托管扩展的方方面面,但确实涵盖了一些对于来自 C# 或原生 C++ 背景的读者来说常常感到困惑的领域。文章假设读者熟悉 .NET 技术的基础知识,并且能够看懂 C++ 代码。
简单的程序
#using <mscorlib.dll>
using namespace System;
int _tmain(void)
{
return 0;
}
#using
是一个预处理器指令,用于导入 .NET 模块或库的元数据。在我们的小程序中,我们对 mscorlib.dll 使用了 #using
,这是所有 .NET 程序的基本库。仅使用 #using
就能让我们访问该模块或库中的所有命名空间和类。但我们需要为使用的每个类键入完全限定名称,包括任何命名空间。我们可以使用 using namespace
指令,它允许我们使用类名而不必显式使用命名空间名称。
例如,我可以这样写:
System::Console::WriteLine("Hello Earth");
但是如果我使用了 using namespace
指令,那么我就可以使用:
using namespace System;
...
Console::WriteLine("Hello Earth");
这主要是一个可读性的问题。有些人可能更喜欢使用完全限定名称来提高代码的清晰度。有时你被迫这样做,例如当两个命名空间中有同名类时,编译器会感到困惑。
托管类
托管类是一个被垃圾回收的类,意味着你使用完它后不必 delete
它。所有这些都由公共语言运行时为你管理。在 MC++ 中,我们使用 __gc
关键字创建托管类,如下所示。
__gc class First
{
public:
First()
{
m_name = "Anonymous";
}
First(String* s)
{
m_name = s;
}
String* GetName()
{
return m_name;
}
void SetName(String* s)
{
m_name = s;
}
private:
String* m_name;
};
嗯,它看起来与普通 C++ 类差不多,只是我们在声明它时使用了 __gc
关键字。这将其标记为托管。你还会注意到我使用了 String*
而不是 String
。这是因为 String
类是一个托管类,只能在堆上声明。
First* f1 = new First();
First* f2 = new First("Andrew");
Console::WriteLine(f1->GetName());
Console::WriteLine(f2->GetName());
f2->SetName("Peace");
Console::WriteLine(f2->GetName());
使用托管类与使用普通的旧式 C++ 类没有太大区别。只不过我不能声明 First
类型的对象,我必须使用 First*
和 new
在堆上声明它们。你可能会注意到,我没有调用 delete
。嘿,别用那种闪烁着不赞同光芒的眼神看着我,好像在说我不应该如此粗心。我没有调用 delete
,因为没有必要。我们正在谈论的是托管 C++,还记得吗?因为 First
是一个托管类,公共语言运行时会自动 delete
不再使用的对象,使用其垃圾收集器。非常方便,我说非常方便!
使用属性
请看下面的两个列表:
//Listing A
f2->SetName("Peace");
Console::WriteLine(f2->GetName());
//Listing B
f2->Name = "Colin";
Console::WriteLine(f2->Name);
显然第二个列表更易读。当然,使用公共成员变量是一个非常鲁莽的想法,并且会招致大多数程序员的鄙视。但在 .NET 中,我们有属性,事实上,我有一些相当模糊的想法,原生 C++ 编译器也支持属性,但由于这种模糊性很强,我最好还是对此保持沉默。
__gc class First
{
public:
...
__property String* get_Name()
{
return m_name;
}
__property void set_Name(String* s)
{
m_name = s;
}
...
};
如果你只有一个 get_
函数,那么你的属性就是只读的。如果你只有一个 set_
函数,那么你有一个只写属性。在我们的例子中,因为我们同时拥有 get_
和 set_
函数,我们拥有一个读写属性。get_
函数的返回类型必须与 set_
函数的参数类型相同。MSDN 还提到,你不能定义一个与包含类同名的属性。
装箱
考虑下面的函数:
void Show(Object* o)
{
Console::WriteLine(o);
}
相当简洁,不是吗?它接受一个 Object
类型的对象并在控制台上显示它。现在看看这段代码。
String* s1 = "Hello World";
Show(s1);
编译并运行良好。现在看看下面的代码片段。
int i = 100;
Show(i); //This won't compile
糟糕!那甚至都无法编译。你将收到编译器错误 C2664: 'Show
' : cannot convert parameter 1 from 'int
' to 'System::Object __gc *
'。这时我们就需要使用 __box
关键字。
int i = 100;
Show(__box(i));
__box
的作用很简单。它会在堆上创建一个新的托管对象,并将值类型对象复制到该托管对象中。它返回的是一个托管对象,该对象实际上是原始值类型对象的副本。这意味着,如果你修改了 __box
返回的托管对象,原始对象将不会有任何改变。
拆箱
显然,如果你可以装箱,你也应该可以拆箱,对吧?假设我们要将一个值对象装箱到一个 Object
中,然后再将其拆箱回一个值对象。下面的代码片段向你展示了如何完成这项工作。
Object* o1 = __box(i);
int j = *static_cast<__box int*>(o1);
j *= 3;
Show(__box(j));
老天!这比你预期的要复杂得多,我敢打赌。我们所做的是将托管对象强制转换为 CLR 堆上的 __gc
指针,然后对其进行解引用。为了增加类型安全性,你可以使用 dynamic_cast
而不是 static_cast
。
原生代码块
考虑下面的函数。
void NativeCall()
{
puts("This is printed from a native function");
}
如你所见,它完全使用了普通的非 .NET 代码。显然,如果我们可以将这个函数编译为原生代码,执行速度会更快。MC++ 为我们提供了 #pragma managed
和 #pragma unmanaged
预处理器指令。我们只需在函数前面加上 #pragma unmanaged
,然后在函数后面加上 #pragma managed
。
#pragma unmanaged
void NativeCall()
{
...
}
#pragma managed
现在,在程序执行过程中遇到此函数时,公共语言运行时会将控制权交给原生平台。显然,每当我们有使用完全非托管函数的函数时,都必须使用它。真正巧妙之处在于,我们可以在托管代码块中调用标记为 #pragma unmanaged
的非托管函数。
__value 类
之前我说过托管类不能在栈上分配。有时,我们可能需要一个非常简单的类,通常只是为了存储一些值。在这种情况下,拥有一个可以在栈上分配的值类型类可能是可取的。这时 __value
关键字就派上用场了。下面的代码片段向你展示了如何声明一个值类型类。
__value class Second
{
public:
void Abc()
{
Console::WriteLine("Second::Abc");
}
};
现在我们可以声明 Second
类型的对象在栈上。
Second sec;
sec.Abc();
实际上,我们甚至可以在堆上声明它们。但请记住,这不是 CLR 托管堆,因此我们需要自己 delete
我们的对象。
Second* psec = __nogc new Second();
psec->Abc();
delete psec;