C++/CLI 仅在 Windows 上受支持。
C++/CLI 是微软在 C++ 的基础上发明的一个语言(相当于 C++ 的超集),它扩展了 C++ 以支持 .NET;同时还可用于 C# 与 C/C++ 互操作。
微软似乎不太建议新的项目使用 C++/CLI:
”对于新项目,我们建议探索现代第三方替代方案,例如 https://github.com/dotnet/ClangSharp 或 https://www.swig.org/,它们提供更大的灵活性并更好地与当前语言和运行时功能保持一致。“
摘自:使用 C++/CLI 进行 .NET 编程 |Microsoft Learn,由 Edge 翻译
相比 C++,C++/CLI 加了一堆乱七八糟的语法,用来和 CLR 打交道。本篇会详细介绍这些语法。
先决条件
使用 C++/CLI 前需要进行额外配置:
安装 Visual Studio C++ 工作负载时,默认情况下不安装 C++/CLI。 若要在安装 Visual Studio 后安装组件,请通过选择 Windows “开始”菜单并搜索 Visual Studio 安装程序打开 Visual Studio 安装程序。 选择安装的 Visual Studio 版本旁边的“修改”按钮。 选择“单个组件”选项卡。向下滚动到“编译器、生成工具和运行时”部分,然后选择“适用于 v143 生成工具的 C++/CLI 支持(最新版本)”。 选择“修改”以下载所需的文件并更新 Visual Studio。
摘自:使用 C++/CLI 进行 .NET 编程 | Microsoft Learn
配置完后,你应该会在 Visual Studio 的 “新建项目” 发现这些多出来的项目:
“CLR” 指的是 Common Language Runtime(公共语言运行时)。总之就是我们想要的 C++/CLI。
C++/CLI 中的 CLI 指的是 Common Language Infrastructure(公共语言基础设施)。不要和 Command-Line Interface(命令行界面)搞混了。
欲详细了解,可参考:.NET学习笔记 -- 那堆名词到底是啥(CLR、CLI、CTS、CLS、IL、JIT) - 红心李 - 博客园
- 如果你只是想写一个程序,和 C# 没关系,就选 ”CLR 控制台应用”;
- 如果你想要与 C# 交互,就选 “CLR 类库”;
- 如果选择 “CLR 空项目”,你需要手动配置 “引用” 才能正常使用 .NET 功能(例如添加
mscorlib
、System
等)。
(另外还要注意 .NET 和 .NET Framework 的区别。)
C++/CLI 项目会启用编译指令 /clr
。这可以在 “配置属性 > C/C++ > 常规 > 公共语言运行时支持” 中找到。
使用 BCL
在 C++/CLI 中使用 BCL 和 C# 十分类似,除了命名空间相关语法稍有不同:
using namespace System; // C# 中是 using System;
int main() {Console::WriteLine("Hello, world!");return 0;
}
定义托管类、托管结构体
这一节要介绍的内容实际上与下一节紧密相关。C++ 的类、结构体不能在 GC 堆上分配。为了在 GC 堆上分配,必须定义托管类、托管结构体。
语法与 C# 类似,这里仅举两个例子:
ref class MyRefClass {}; // C++ 是需要分号的
value class MyValueClass {};
使用 GC 堆
C++/CLI 支持了和 C# 类似的自动内存管理功能,允许我们愉快地使用 GC(Garbage Collection,垃圾回收器)。具体来说,我们会通过特定的语法在 GC 堆(Garbage Collected Heap)上申请内存。这样申请的对象,其会在引用计数归零后由 GC 自动回收。
让我们看两个例子吧:
int^ i = gcnew int;
MyRefClass ^c = gcnew MyRefClass();
我们用 C++/CLI 关键字 gcnew
在 GC 堆上分配内存,语法和 new
类似。在 GC 堆上分配内存的类型必须为托管类型,其包括:
- 基础类型,如
int
(实际上会这里会被隐式转换为System.Int32
); ref class
、value class
;ref struct
、value struct
。
我们得到的这个 int^
,称为 “句柄(handle)”。句柄的行为和指针类似(但不支持指针算术),请看这个例子:
using namespace System;
int main() {int^ i = gcnew int;Console::WriteLine(i);Console::WriteLine(*i);i = 13;Console::WriteLine(i);Console::WriteLine(*i);*i = 17;Console::WriteLine(i);Console::WriteLine(*i);return 0;
}
其中 *i
可以看作是对句柄的解引用。此程序的输出:
0
0
13
13
17
17
从中可以发现使用 *i
和 i
赋值是等价的。使用 .NET 相关方法时也是等价的,但对于标准 C++ 函数则不是,例如使用标准输出流:
std::cout << *i;
这是因为 i
的类型是 int^
(句柄),STL 显然不支持,而 *i
就是 int
,STL显然支持。
跟踪引用(Tracking Reference)
C++/CLI 引入了跟踪引用操作符(Tracking Reference Operator),即 %
,Microsoft 给出的介绍如下:
跟踪引用有以下特征。
- 将一个对象分配至跟踪引用将导致对象的引用计数增加。
- 原生引用 (
&
) 是*
解引用的结果。跟踪引用 (%
) 是^
解引用的结果。只要你有一个对象的%
(跟踪引用),此对象就会在内存中保持活动状态(stay alive)。- 成员访问符 (
.
) 用于访问成员。- 跟踪引用是对值类型和句柄有效的类型(例如
String^
)。(注:这句话应该是说跟踪引用可以传参给值类型和句柄。)- 跟踪引用不能被分配为
nullptr
。跟踪引用可以根据需要多次重新分配给另一个有效对象。- 跟踪引用不能用作一元获取地址运算符。
摘自:Tracking Reference Operator (C++/CLI and C++/CX) | Microsoft Learn,由笔者翻译
请看例子:
int^ i = gcnew int;
int% myRef = *i; // 将 i 解引用并赋给 myRef
int^% handleRef = i; // handleRef 是句柄的引用int^ j = %myRef;
// 这个可以这么理解,C++/CLI 新定义的运算符和原有运算符的对应关系:
// *(指针) <-> ^(句柄), &(引用) >-> %(跟踪引用)
// 那 int^ j = %myRef <-> int* j = &myRef
// 相当于把 % 当 "取句柄" 运算符使用 (乱猜)
参考
- 使用 C++/CLI 进行 .NET 编程 | Microsoft Learn
- 适用于 .NET 和 UWP 的组件扩展 | Microsoft Learn
微软真坑,居然把同样主题的内容放在两个地方。这两个文档互相引用,笔者查阅的时候很麻烦。