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

快速 C++/CLI - 10 分钟内学会 C++/CLI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (44投票s)

2007 年 6 月 26 日

CPOL

7分钟阅读

viewsIcon

552089

10 分钟内学会 C++/CLI。

引言

欢迎阅读我关于 .NET 编程(特别是 C++/CLI)的第二篇文章。在我写完上一篇关于 MC++ 的文章后不久,C++/CLI 就变得越来越流行,从而使得 MC++ 过时了。如果你读过上一篇 MC++ 的文章,那么阅读本文会更容易,只会更新你的知识。

本文的目的是在短时间内向你展示开始使用 C++/CLI 所需的大部分信息(前提是你具备 C++ 和 .NET 背景)。信息将以“操作指南”的方式呈现,并且你将为介绍的每一个主题提供一个示例。

在你继续之前,建议你先热身一下大脑,并参考这两篇文章 [1] & [8]

你可以通过以下命令行(或等效命令)来构建每个示例:

"C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\vsvars32.bat"

设置好环境(只需执行一次),然后使用 CL 工具进行编译,如下所示:

cl your_file.cpp /clr

目录

  1. 什么是 C++/CLI?
  2. 句柄和指针
  3. Hello World
  4. 类和 UDT
  5. 数组
  6. 参数数组
  7. 属性
  8. 包装原生 C++ 类
  9. 包装 C 回调
  10. 反过来:从托管到 C 回调

那么,什么是 C++/CLI?

如你所知,C++ 是一种高级语言,主要被认为是 C 语言的超集,它增加了许多功能,如面向对象和模板,但 CLI 是什么?

CLI 代表 Common Language Infrastructure(通用语言基础设施)。它在网络上得到了详尽的解释,但简而言之:它是一个开放的规范,描述了可执行代码和运行时环境,它允许多种高级语言在不同的计算机平台上使用,而无需为特定架构重写。[2]

现在,C++/CLI 是用 C++ 编写 .NET 程序的方式,就像 C# 或 VB.NET 一样。

句柄和指针

你可能在 C++/CLI 代码中看到过 "^" 符号,并对此感到好奇。如你所知,在 C++ 中,我们使用 "*" 来表示指针,而在 C++/CLI 中,我们使用 "^" 来表示句柄。现在,"*" 表示位于 CRT 堆上的原生指针,而句柄表示“安全指针”并位于托管堆上。句柄可以被视为引用,与原生指针不同,如果它们没有被正确删除,它们不会导致内存泄漏,因为 GC 会处理这个问题,而且它们没有固定的内存地址,因此在执行过程中会被移动。

要创建一个特定类或值类型的新的引用,我们必须使用 "gcnew" 关键字来分配它;例如:

System::Object ^x = gcnew System::Object();

值得注意的是,"nullptr" 关键字表示一个空引用。除了 "^" 符号之外,我们还有百分号 "%",它代表一个跟踪引用;我想引用 ECMA-372 的说法:

N* pn = new N; // allocate on native heap
N& rn = *pn; // bind ordinary reference to native object
R^ hr = gcnew R; // allocate on CLI heap
R% rr = *hr; // bind tracking reference to gc-lvalue

一般来说,"%" 符号相对于 "^" 就像 "&" 符号相对于 "*" 一样。

让我们开始吧:Hello World

在本节中,你将学习如何创建一个简单的 C++/CLI 程序骨架。首先,你需要知道如何定义一个正确的 "main" 函数。如你所见,C 的 main 和 C++/CLI 的 main 的原型都需要一个字符串数组传递给它们。

#using <mscorlib.dll>

using namespace System;

int main(array<System::String ^> ^args)
{
  System::Console::WriteLine("Hello world");
  return 0;
}

类和 UDT

在本例中,我们将演示如何创建类和用户定义类型。要创建托管类,你只需在类定义前加上一个访问修饰符,后跟 "ref",如下所示:

public ref class MyClass
{
private:
public:
  MyClass()
  {

  }
}

而要创建原生类,你只需像你知道的那样创建即可。现在,你可能想知道 C++/CLI 中的析构函数是否仍然像以前一样工作,答案是肯定的,析构函数(是确定性的)仍然像在 C++ 中一样使用;但是,编译器会将析构函数调用转换为 Dispose() 调用,并在为你透明地实现 IDisposable 接口后执行。此外,还有一个所谓的终结符(非确定性的),它由 GC 调用,定义如下:"!MyClass()"。在终结符中,你可能需要检查析构函数是否已被调用,如果没有,你可能需要调用它。

#using <mscorlib.dll>

using namespace System;

public ref class MyNamesSplitterClass
{
private:
  System::String ^_FName, ^_LName;
public:
  MyNamesSplitterClass(System::String ^FullName)
  {
    int pos = FullName->IndexOf(" ");
    if (pos < 0)
      throw gcnew System::Exception("Invalid full name!");
    _FName = FullName->Substring(0, pos);
    _LName = FullName->Substring(pos+1, FullName->Length - pos -1);
  }

  void Print()
  {
    Console::WriteLine("First name: {0}\nLastName: {1}", _FName, _LName);
  }
};

int main(array<System::String ^> ^args)
{
  // local copy

  MyNamesSplitterClass s("John Doe");
  s.Print();

  // managed heap

  MyNamesSplitterClass ^ms = gcnew MyNamesSplitterClass("Managed C++");
  ms->Print();

  return 0;
}

值类型

值类型是允许用户创建除基本类型之外的新类型的手段;所有值类型都派生自 System::ValueType。值类型可以存储在栈上,并可以使用等于运算符进行赋值。

public value struct MyPoint
{
  int x, y, z, time;
  MyPoint(int x, int y, int z, int t)
  {
    this->x = x;
    this->y = y;
    this->z = z;
    this->time = t;
  }
};

Enums

类似地,你可以使用以下语法创建枚举:

public enum class SomeColors { Red, Yellow, Blue};

甚至可以指定元素的类型,如:

public enum class SomeColors: char { Red, Yellow, Blue};

数组

数组创建再简单不过了,这个例子将帮助你入门:

cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};

这将创建一个包含三个整数的数组,而

array<int> ^a = gcnew array<int>(100) {1, 2, 3};

将创建一个包含 100 个元素的数组,并初始化前三个元素。要遍历数组,你可以像使用普通数组一样使用 Length 属性和索引,并且可以使用 foreach

for each (int v in a)
{
  Console::WriteLine("value={0}", v);
}

要创建多维数组,例如此处的 3D 数组,如 4x5x2,并全部初始化为零:

array<int, 3> ^threed = gcnew array<int, 3>(4,5,2);

Console::WriteLine(threed[0,0,0]);

字符串类的数组可以这样做:

array<String ^> ^strs = gcnew array<String ^> {"Hello", "World"}

在 `for` 循环中初始化字符串数组 [3]

array<String ^> ^strs = gcnew array<String ^>(5);
int cnt = 0;

// We use the tracking reference to access the references inside the array
// since normally strings are passed by value

for each (String ^%s in strs)
{
    s = gcnew String( (cnt++).ToString() );
}

有关 cli::array 的更多参考,请查看 System::Array 类,如果你想添加/删除元素,请查看 ArrayList 类。

参数数组

这相当于 C++ 中的可变参数。可变参数必须是函数中的一个参数,并且是最后一个参数。通过在类型前加上 "..." 来定义,后跟所需类型的数组:

using namespace System;

void avg(String ^msg, ... array<int> ^values)
{
  int tot = 0;
  for each (int v in values)
    tot += v;
  Console::WriteLine("{0} {1}", msg, tot / values->Length);
}

int main(array<String ^> ^args)
{
  avg("The avg is:", 1,2,3,4,5);
  return 0;
}

属性

public ref class Xyz
{
private:
  int _x, _y;
    String ^_name;
public:
  property int X
    {
      int get()
        {
          return _x;
        }
        void set(int x)
        {
          _x = x;
        }
    }
  property String ^Name
  {
    void set(String ^N)
    {
      _name = N;
    }
    String ^get()
    {
      return _name;
    }
  }
};

包装原生 C++ 类

在本节中,我们将演示如何为原生 C++ 类创建 C++/CLI 包装器。考虑以下原生类:

// native class

class Student
{
private:
  char *_fullname;
  double _gpa;
public:
  Student(char *name, double gpa)
  {
    _fullname = new char [ strlen(name+1) ];
    strcpy(_fullname, name);
    _gpa = gpa;
  }
  ~Student()
  {
    delete [] _fullname;
  }
  double getGpa()
  {
    return _gpa;
  }
  char *getName()
  {
    return _fullname;
  }
};

现在,要包装它,我们遵循以下简单指南:

  1. 创建托管类,并让它有一个指向原生类的成员变量。
  2. 在构造函数或其他合适的地方,在原生堆上构造原生类(使用 "new")。
  3. 根据需要传递参数给构造函数;从托管到非托管传递时,某些类型需要进行封送。
  4. 为你想从托管类公开的所有函数创建存根。
  5. 确保在托管类的析构函数中删除原生指针。

这是我们为 Student 类创建的托管包装器:

// Managed class

ref class StudentWrapper
{
private:
  Student *_stu;
public:
  StudentWrapper(String ^fullname, double gpa)
  {
    _stu = new Student((char *) 
           System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(
           fullname).ToPointer(), 
      gpa);
  }
  ~StudentWrapper()
  {
    delete _stu;
    _stu = 0;
  }

  property String ^Name
  {
    String ^get()
    {
      return gcnew String(_stu->getName());
    }
  }
  property double Gpa
  {
    double get()
    {
      return _stu->getGpa();
    }
  }
};

包装 C 回调

在本节中,我们将演示如何让 C 回调调用 .NET。为便于说明,我们将包装 EnumWindows() API。以下是代码的概要:

  1. 创建一个托管类,其中包含在达到原生回调时要调用的委托或函数。
  2. 创建一个原生类,它有一个指向我们托管类的引用。我们可以通过使用 vcclr.h 头文件中的 gcroot_auto 来实现这一点。
  3. 创建原生 C 回调过程,并将其作为上下文参数(在本例中为 lParam)传递一个指向原生类的指针。
  4. 现在,在原生回调内部,并拥有原生类的上下文后,我们可以获取托管类的引用并调用所需的函数。

下面提供了一个简短的示例:

using namespace System;

#include <vcclr.h>


// Managed class with the desired delegate

public ref class MyClass
{
public:
  delegate bool delOnEnum(int h);
  event delOnEnum ^OnEnum;

  bool handler(int h)
  {
    System::Console::WriteLine("Found a new window {0}", h);
    return true;
  }

  MyClass()
  {
    OnEnum = gcnew delOnEnum(this, &MyClass::handler);
  }
};

为持有我们托管类的引用和托管原生回调过程而创建的原生类:

class EnumWindowsProcThunk
{
private:
  // hold reference to the managed class

  msclr::auto_gcroot<MyClass^> m_clr;
public:

  // the native callback

    static BOOL CALLBACK fwd(
    HWND hwnd,
    LPARAM lParam)
  {
      // cast the lParam into the Thunk (native) class,
      // then get is managed class reference,
      // finally call the managed delegate

      return static_cast<EnumWindowsProcThunk *>(
            (void *)lParam)->m_clr->OnEnum((int)hwnd) ? TRUE : FALSE;
  }

    // Constructor of native class that takes a reference to the managed class

  EnumWindowsProcThunk(MyClass ^clr)
  {
    m_clr = clr;
  }
};

整合所有内容

int main(array<System::String ^> ^args)
{
  // our native class

  MyClass ^mc = gcnew MyClass();

    // create a thunk and link it to the managed class

  EnumWindowsProcThunk t(mc);

    // Call Window's EnumWindows() C API with the pointer
    // to the callback and our thunk as context parameter

  ::EnumWindows(&EnumWindowsProcThunk::fwd, (LPARAM)&t);

  return 0;
}

反过来:从托管到 C 回调

现在,这个问题更容易解决了,因为我们可以在托管类中拥有一个指向原生类的指针。解决方案可以描述为:

  1. 创建一个托管类,其中包含应触发原生回调的所需委托。
  2. 创建一个托管类,它将在原生类(包含回调)和之前的托管类(事件生成器)之间进行绑定。
  3. 创建包含给定回调的原生类。

为了演示,我们创建了一个 "TickGenerator" 托管类,它会定期生成一个 OnTick 事件,然后是一个 INativeHandler 类(接口),它应该由托管类 TickGeneratorThunk 调用。MyNativeHandler 类是 INativeHandler 的一个简单实现,用于向你展示如何设置自己的处理程序。

滴答声生成器委托

public delegate void delOnTick(int tickCount);

托管的滴答声生成器类

ref class TickGenerator
{
private:
  System::Threading::Thread ^_tickThread;
  int _tickCounts;
  int _tickFrequency;
  bool _bStop;

  void ThreadProc()
  {
    while (!_bStop)
    {
      _tickCounts++;
      OnTick(_tickCounts);
      System::Threading::Thread::Sleep(_tickFrequency);
    }
  }

public:
  event delOnTick ^OnTick;

  TickGenerator()
  {
    _tickThread = nullptr;
  }

  void Start(int tickFrequency)
  {
    // already started

    if (_tickThread != nullptr)
      return;

    // p.s: no need to check if the event was set,
    // an unset event does nothing when raised!

    _tickCounts = 0;
    _bStop = false;
    _tickFrequency = tickFrequency;

    System::Threading::ThreadStart ^ts = 
      gcnew System::Threading::ThreadStart(this, &TickGenerator::ThreadProc);
    _tickThread = gcnew System::Threading::Thread(ts);
    _tickThread->Start();
  }
  
  ~TickGenerator()
  {
    Stop();
  }

  void Stop()
  {
    // not started?

    if (_tickThread == nullptr)
      return;
    _bStop = true;

    _tickThread->Join();
    _tickThread = nullptr;
  }
};

现在,非托管的滴答声处理程序接口:

#pragma unmanaged
// Create a simple native interface for handling ticks

// Native classes implement this class to add custom OnTick handlers

class INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount) = 0;
};
一个简单的实现:
class MyNativeHandler: public INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount)
  {
    printf("MyNativeHandler: called with %d\n", tickCount);
  }
};

现在,回到托管端来创建 Thunk,在托管和非托管之间进行桥接:

#pragma managed
// Create the managed thunk for binding between the native
// tick handler and the tick generator managed class

ref class TickGeneratorThunk
{
private:
  INativeOnTickHandler *_handler;
public:
  TickGeneratorThunk(INativeOnTickHandler *handler)
  {
    _handler = handler;
  }

  void OnTick(int tickCount)
  {
    _handler->OnTick(tickCount);
  }
};

整合所有内容

int main(array<System::String ^> ^args)
{
  // Initiate the native handler

  MyNativeHandler NativeHandler;

  // Create the tick generator class

  TickGenerator ^tg = gcnew TickGenerator();

  // Create the thunk and bind it with our native handler

  TickGeneratorThunk ^thunk = gcnew TickGeneratorThunk(&NativeHandler);

  // Bind the ontick event with the thunk's onclick event

  tg->OnTick += gcnew delOnTick(thunk, &TickGeneratorThunk::OnTick);

  // Start the tick generator

  tg->Start(1000);

  // Wait for user input

  Console::ReadLine();

  // Stop the generator

  tg->Stop();

  return 0;
}

结论

我希望你在阅读这篇文章时有所学习并享受其中。它应该足以让你很快入门,剩下的就看你了。确保你通读了本文提供的参考文献列表。

参考

  1. 纯 C++:你好,C++/CLI
  2. Common Language Infrastructure - 维基百科
  3. C++/CLI - 维基百科
  4. Pro Visual C++/CLI
  5. Applied Microsoft .NET Framework Programming
  6. ECMA 372 - C++/CLI 规范
  7. C++/CLI 初探
  8. 托管 C++ - 示例学习 - 第一部分
  9. MSDN
© . All rights reserved.