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






4.87/5 (44投票s)
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
目录
那么,什么是 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;
}
};
现在,要包装它,我们遵循以下简单指南:
- 创建托管类,并让它有一个指向原生类的成员变量。
- 在构造函数或其他合适的地方,在原生堆上构造原生类(使用 "
new
")。 - 根据需要传递参数给构造函数;从托管到非托管传递时,某些类型需要进行封送。
- 为你想从托管类公开的所有函数创建存根。
- 确保在托管类的析构函数中删除原生指针。
这是我们为 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。以下是代码的概要:
- 创建一个托管类,其中包含在达到原生回调时要调用的委托或函数。
- 创建一个原生类,它有一个指向我们托管类的引用。我们可以通过使用 vcclr.h 头文件中的
gcroot_auto
来实现这一点。 - 创建原生 C 回调过程,并将其作为上下文参数(在本例中为
lParam
)传递一个指向原生类的指针。 - 现在,在原生回调内部,并拥有原生类的上下文后,我们可以获取托管类的引用并调用所需的函数。
下面提供了一个简短的示例:
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 回调
现在,这个问题更容易解决了,因为我们可以在托管类中拥有一个指向原生类的指针。解决方案可以描述为:
- 创建一个托管类,其中包含应触发原生回调的所需委托。
- 创建一个托管类,它将在原生类(包含回调)和之前的托管类(事件生成器)之间进行绑定。
- 创建包含给定回调的原生类。
为了演示,我们创建了一个 "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;
}
结论
我希望你在阅读这篇文章时有所学习并享受其中。它应该足以让你很快入门,剩下的就看你了。确保你通读了本文提供的参考文献列表。