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

C++/CLI 初探

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (106投票s)

2004年4月28日

5分钟阅读

viewsIcon

591789

简要介绍 C++/CLI 的新语法,以及它相比旧 MC++ 语法有何改进。

引言

当微软在 VS.NET 7 中推出托管 C++ 扩展时,C++ 程序员的反应不一。虽然大多数人都很高兴能够继续使用 C++,但几乎所有人都对托管 C++ 所提供的丑陋而扭曲的语法感到不满。微软显然非常认真地对待收到的反馈,并决定 MC++ 语法不会取得巨大成功。

2003 年 10 月 6 日,ECMA 宣布成立一个新工作组,负责开发一套标准的语言扩展,以在 ISO 标准 C++ 编程语言和通用语言基础设施 (CLI) 之间建立绑定。同时还公布,这套新的语言扩展将被称为 C++/CLI 标准,VC++ 编译器将从 Whidbey 版本(VS.NET 2005)开始支持该标准。

旧语法的弊端

  • 丑陋扭曲的语法和文法 - 那些双下划线并不赏心悦目。
  • 二等 CLI 支持 - 与 C# 和 VB.NET 相比,MC++ 使用了 contorted 的变通方法来提供 CLI 支持,例如,它没有 for-each 构造来枚举 .NET 集合。
  • C++ 和 .NET 集成不佳 - 你无法在 CLI 类型上使用 C++ 特性(如模板),也无法在 C++ 类型上使用 CLI 特性(如垃圾回收)。
  • 令人困惑的指针用法 - 非托管 C++ 指针和托管引用指针都使用了相同的以 * 为基础的语法,这非常令人困惑,因为 __gc 指针在性质和行为上与非托管指针完全不同。
  • MC++ 编译器无法生成可验证的代码

C++/CLI 为我们带来了什么?

  • 优雅的语法和文法 - 这让 C++ 开发者在编写托管代码时感到自然,并允许从非托管编码平滑过渡到托管编码。那些丑陋的双下划线现在不见了。
  • 一流的 CLI 支持 - 属性、垃圾回收和泛型等 CLI 特性都得到直接支持。更重要的是,C++/CLI 允许我们对本地非托管类也使用这些特性。
  • 一流的 C++ 支持 - 模板和确定性析构函数等 C++ 特性可用于托管和非托管类。实际上,C++/CLI 是唯一一个你可以*似乎*在堆栈或本地 C++ 堆上声明 .NET 类型的 .NET 语言。
  • 弥合 .NET 和 C++ 之间的鸿沟 - C++ 程序员在处理 BCL 时不会感到无所适从。
  • 由 C++/CLI 编译器生成的可执行文件现在完全可验证。

Hello World

using namespace System;

void _tmain()
{
    Console::WriteLine("Hello World");
}

嗯,这看起来与旧语法差别不大,只是现在你不需要添加对 mscorlib.dll 的引用,因为 Whidbey 编译器在用 /clr(现在默认为 /clr:newSyntax)编译时会隐式引用它。

句柄

旧语法中一个主要的困惑是我们对非托管指针和托管引用都使用了 * 标点符号。在 C++/CLI 中,微软引入了句柄的概念。

void _tmain()
{
    //The ^ punctuator represents a handle
    String^ str = "Hello World";
    Console::WriteLine(str);
}

^ 标点符号(发音为 cap)表示对托管对象的句柄。根据 CLI 规范,句柄是托管对象的引用。句柄是 MC++ 语法中 __gc 指针的新语法等价物。句柄不应与指针混淆,其性质与指针完全不同。

句柄与指针有何不同?

  • 指针使用 * 标点符号表示,而句柄使用 ^ 标点符号表示。
  • 句柄是对托管堆上对象的托管引用,指针只是指向内存地址。
  • 指针是稳定的,GC 循环不会影响它们,句柄可能会根据 GC 和内存压缩指向不同的内存位置。
  • 对于指针,程序员必须显式 delete,否则就会出现内存泄漏。对于句柄,delete 是可选的。
  • 句柄是类型安全的,而指针绝对不是。你不能将句柄强制转换为 void^
  • 就像 new 返回一个指针一样,gcnew 返回一个句柄。

实例化 CLR 对象

void _tmain()
{
    String^ str = gcnew String("Hello World");
    Object^ o1 = gcnew Object();
    Console::WriteLine(str);
}

gcnew 关键字用于实例化 CLR 对象,它返回对 CLR 堆上对象的句柄。关于 gcnew 的好处是,它让我们能够轻松区分托管和非托管的实例化。

基本上,gcnew 关键字和 ^ 运算符提供了访问 BCL 所需的一切。但显然你需要创建和声明自己的托管类和接口。

声明类型

CLR 类型前面有一个形容词,描述了该类型的种类。以下是 C++/CLI 中类型声明的示例:

  • CLR 类型
    • 引用类型
      • ref class RefClass{...};
      • ref struct RefClass{...};
    • 值类型
      • value class ValClass{...};
      • value struct ValClass{...};
    • 接口
      • interface class IType{...};
      • interface struct IType{...};
    • 枚举
      • enum class Color{...};
      • enum struct Color{...};
  • 原生类型
    • class Native{...};
    • struct Native{...};
using namespace System;

interface class IDog
{
    void Bark();
};

ref class Dog : IDog
{
public:
    void Bark()
    {
        Console::WriteLine("Bow wow wow");
    }
};

void _tmain()
{
    Dog^ d = gcnew Dog();
    d->Bark();
}

这样,现在的语法就比旧语法整洁多了,旧语法中上述代码会充斥着诸如 __gc__interface 等双下划线关键字。

装箱/拆箱

装箱是隐式的(太棒了!)且类型安全的。执行位复制,并在 CLR 堆上创建一个 Object。拆箱是显式的——只需进行 reinterpret_cast 然后解引用。

void _tmain()
{
    int z = 44;
    Object^ o = z; //implicit boxing

    int y = *reinterpret_cast<int^>(o); //unboxing

    Console::WriteLine("{0} {1} {2}",o,z,y);

    z = 66; 
    Console::WriteLine("{0} {1} {2}",o,z,y);
}

// Output
// 44 44 44
// 44 66 44

Object o 是一个装箱副本,实际上并不引用值类型 int,这从第二个 Console::WriteLine 的输出可以明显看出。

当你装箱一个值类型时,返回的对象会记住原始值类型。

void _tmain()
{
    int z = 44;
    float f = 33.567;

    Object^ o1 = z; 
    Object^ o2 = f; 

    Console::WriteLine(o1->GetType());
    Console::WriteLine(o2->GetType());    
}

// Output
// System.Int32
// System.Single

因此,你不能尝试拆箱为不同的类型。

void _tmain()
{
    int z = 44;
    float f = 33.567;

    Object^ o1 = z; 
    Object^ o2 = f;

    int y = *reinterpret_cast<int^>(o2);//System.InvalidCastException
    float g = *reinterpret_cast<float^>(o1);//System.InvalidCastException
}

如果你尝试这样做,你将得到一个 System.InvalidCastException。简直是完美的类型安全!如果你查看生成的 IL,你会看到 MSIL box 指令正在起作用。例如:

void Box2()
{
    float y=45;
    Object^ o1 = y;
}

被编译为:

.maxstack  1
.locals (float32 V_0, object V_1)

  ldnull
  stloc.1
  ldc.r4     45.
  stloc.0
  ldloc.0
  box   [mscorlib]System.Single
  stloc.1
  ret

根据 MSIL 文档,“box 指令将 ‘原始’ valueType(未装箱的值类型)转换为 Object 类型(类型 O)的实例。这是通过创建一个新对象并将数据从 valueType 复制到新分配的对象来完成的。

深入阅读

结论

好吧,那么当人们可以使用 C#、J# 和那个 VB 来编写 .NET 代码时,为什么还有人想使用 C++/CLI 呢?以下是我在 2003 年 12 月 Trivandrum 的 DevCon 2003 演讲中给出的四个原因。

  • 将现有 C++ 代码编译为 IL (/clr 神奇)
  • 确定性析构
  • 任何其他 CLI 语言都无法比拟的原生互操作支持
  • MC++ 中那些下划线都消失了 ;-)
© . All rights reserved.