快速学习C++/CLI - 十分钟内学好C++/CLI

简介

欢迎阅读我在.net开发的第二篇文章,它是关于C++/CLI的。就在我写完前一篇文章后不久,C++/CLI的应用变得多了起来,这就让MC++显得有点过时。如果你读过前一篇关于MC++的文章,那看这篇文章将会更容易,并且会更新你的有关知识。

这篇文章的目标是在很短的时间内向你展示开始使用C++/CLI所需的大多数信息(前提是你有C++和.NET背景)。这些信息将会以“怎样使用(how-to)”的方式展示,我会为介绍的每个主题提供一个实例。

在你开始之前,我建议你为你的神经先热热身,看看这两篇文章[1] & [8].(原文连接有误,本应为2个链接结果是指向自身页面的一个链接)

你可以通过下面的命令行编译示例(或者跟它等价的命令)

"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. 类和用户定义类型

5. 数组

6. 参数数组

7. 属性

8. 原生C++类的包装

9. C回调函数的包装

10. 反过来:从托管到C回调函数

 

那么,什么是C++/CLI

C++,正如你已经知道的,是一种高级语言。它被认为是C语言的超级,添加了许多特性,注入面向对象和模板,但什么是CLI?

CLI表示通用语言基础架构(Common Language Infrastructure)。网上到处有它的解释,但简而言之,它是一种开放的规范,它描述了允许多种高级语言在补充些特定架构的情况下在不同的计算机平台上的可执行代码和运行时环境。

现在C++/CLI是在.NET下用C++编程的方式,和C#或VB.net被使用的那样。

句柄和指针

可能你已经在C++/CLI中见过这个符号:^,并且怀疑它是干嘛用的。正如你知道的,C++中我们用*来表示一个指针,在C++/CLI中,我们用符号^来表示句柄。现在*用来指定CRT heap上的原生指针,而句柄是安全指针,它位于托管堆上,。你可以把句柄当成引用来考虑,和原生指针不同的是,他们不会不会引起内存泄漏,即便没有对它们进行适当的删除,因为GC会处理这些问题,并且他们没有一个固定的内存地址,所以在执行的时候它们会被移来移去。

为了创建一个新的类或值类型的引用,我们需要通过gcnew关键字来分配它,例如:

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

值得注意的是nullptr关键字表示一个空指针。除了符号^,我们用百分号%来表示一个跟踪的引用,引用ECMA-372来说明:

N* pn = new N;//分配在原生heap上

n& rn = *pn;//绑定一个普通引用到原生对象

R^ hr = gcnew R;//分配在CLI heap上

r% rr = *hr;//绑定跟踪的引用到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;

}

类和自定义类型

这个实例中,我们将阐述怎样创建类和用户定义类型。为了创建一个托管类,你要做的仅仅是在类的定义之前加上ref修饰符,像这样:

public ref class MyClass

{

private:

public:

MyClass()

{

}

}

而为了创建一个原生类,你只需要用你已经知道的方式来创建它。现在你可能在怀疑C++/CLI中的析构函数是不是也和之前的析构函数一样:答案是yes,确定的一点是析构函数和它们在C++中的用法一样;然而,编译器会在显式配置了IDisposable接口之后为你将析构函数的调用翻译成Dispose()调用。除了析构函数,还有一个会被GC调用的被称为finalizer的东东,它是这样定义的!MyClass().在finalizer中,你可以用它来检查析构函数是否被调用,如果没有你可以调用它。

#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继承。值类型可以存放在stack上,并且可以通过等号来赋值。

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;
  }
};
枚举

类似的,你可以用下面的语法来创建枚举

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);
}

为了创建一个多维数组,这个例子里是3维数组,例如4x5x2,会都被初始化成0:

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循环中初始化

An array of strings initialized in a for loop [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::数组的内容请参考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. 在构造函数或者其他适当的地方在原生heap上通过new构造原生类

3. 为构造函数传递必要的参数,有的类型从托管到非托管传递的时候你需要marshal

4. 为所有你想在托管类中暴露的方法创建stub

5. 确定你在托管类的析构函数中删除指向原生类的指针

下面是我们为Student类创建的托管wrapper

// 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()。代码大纲如下:

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);
  }
};

The native class created for the purpose of holding a reference to our managed class and for hosting the native callback procedure:

clip_image001Collapse | Copy Code

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;
  }
};

Putting it all together:

clip_image001[1]Collapse | Copy Code

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;
}
posted @ 2011-06-03 14:54  pursue  阅读(2151)  评论(0)    收藏  举报