65.9K
CodeProject 正在变化。 阅读更多。
Home

托管 C++ 入门

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (19投票s)

2001年10月19日

CPOL

6分钟阅读

viewsIcon

335929

downloadIcon

1613

让初学者开始使用托管 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;

更多阅读

© . All rights reserved.