C++/CLI基本语法和最佳实践

详细剖析C+ +/CLI中的六大核心类型,总结C+ +/CLI实现跨语言调用既有库的最佳实践。

定位

C+ +/CLI是C+ +语言的一个变种,不仅支持原生C+ +的语法特性,也支持托管代码特性,这样就达到了在C+ +中调用C#库的目的。

结合原生C+ +的性能和C#的强大生态,C+ +/CLI可以高效开发独立运行的程序(控制台程序、窗体程序等),可惜的是,这并没有成为主流(大概是因为C+ +和C#两者的应用场景过于分明,实际并不需要大规模混合使用)!C+ +/CLI作为连接原生C+ +和C#的桥梁,通常被用来实现原生C+ +和C#既有程序库的跨语言调用。

所以,本文聚焦在C+ +/CLI的核心语法特性上,不会对其侧重于独立软件开发的语法特性进行过多介绍。

基本语法

“隙中窥月”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 使用原生C+ +标准库
#include <iostream>
#include <vector>

// 通过命名空间引入.NET类型
using namespace System;
using namespace System::Collections::Generic;

/// @brief 定义一个托管类
ref class CLIPointClass {
public:
Double x;
Double y;
Double z;
CLIPointClass(Double x, Double y, Double z) : x(x), y(y), z(z) { }
void ShowPoint() {
String^ info = String::Format("x={0}, y={1}, z={2}", x, y, z);
Console::WriteLine(info);
}
};

int main() {
// C+ +动态数组
std::cout << "打印C+ +数据:" << std::endl;
std::vector<int> vec{ 1,2,3,4,5,6,7,8,9,10 };
for each (int i in vec)
std::cout << i << std::endl;

// .NET动态数组
List<Double>^ datas = gcnew List<Double>();
datas->Add(0.1);
datas->Add(0.2);
datas->Add(0.3);
datas->Add(0.4);
datas->Add(0.5);
Console::WriteLine("打印.NET数据:");
for each (Double d in datas)
{
Console::WriteLine(d);
}

// 实例化自定义类CLIPointClass
CLIPointClass^ p1 = gcnew CLIPointClass(100, 200, 300);
p1->ShowPoint();

std::system("pause");
return 0;
}

以上代码中:

(1)通过命名空间可引入C#库。

(2)定义托管类CLIPointClass,使用关键字ref class;实例化时使用gcnew表示创建托管类型,类型带上符号^。

(3)在主函数中可混合使用C+ +和C#的数据类型。

六种类和结构体类型详解

在C+ +/CLI中可以使用6种基本类和结构体类型:

  • struct / class
  • value struct / value class
  • ref struct / ref class

C#中struct / class的区别

(1)struct是值类型,class是引用类型。

(2)默认访问权限不一样,struct是public,class是private。

(3)struct不能有空参数的构造函数。

C+ +/CLI中struct / class的区别

与原生C+ +的结构体和类对应。

(1)默认访问权限不一样,struct是public,class是private。

(2)在C+ +11之后,struct也支持定义成员函数和继承。

(3)两者使用界限逐渐模糊,但是还是倾向于将struct视为一个数据容器。

C+ +/CLI中value struct / value class的区别

两者都是托管类型,基本等同;两者都不能有空参数的构造函数(与C#的struct特性对应);等同于C#中的struct。

区别:默认访问权限不一样,struct是public,class是private。

C+ +/CLI中ref struct / ref class的区别

两者都是托管类型,基本等同;等同于C#中的class。

区别:默认访问权限不一样,struct是public,class是private。

六种类型的实例化、作为函数参数传递的方式

(1)struct / class

struct和class的表现一样。同原生C+ +的struct / class,可声明到栈或堆上:

1
2
3
4
5
6
// 声明到栈上
CppPointClass p1(5, 6);

// 声明到堆上
CppPointClass* p2 = new CppPointClass(7, 8);
delete p4;

作为参数时,可以值传参、引用传参、指针传参,后两者可以对实例进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 值传参
void Test01(CppPointClass p) {
p.X = 100;
}
Test01(p1);

// 引用传参
void Test02(CppPointClass& p) {
p.X = 100;
}
Test02(p1);

// 指针传参
void Test03(CppPointClass* p) {
p->X = 1000;
}
Test03(&p1);
Test03(p2);

如果想修改指针本身的值,需要采用指针的引用或指针的指针来传参:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 指针的引用传参
void Test04(CppPointClass*& p) {
// 修改指针本身的值
p = new CppPointClass(9999, 9999);
}
Test04(p2);

// 指针的指针传参
void Test05(CppPointClass** p) {
// 修改指针本身的值
*p = new CppPointClass(9999, 9999);
}
Test05(&p2);

(2)value struct / value class

value struct和value class的表现一样。可以声明为值类型,也可以声明为托管类型:

1
2
3
4
5
// 声明为值类型
CLIValuePointClass p1(15, 16);

// 声明为托管类型
CLIValuePointClass^ p2 = gcnew CLIValuePointClass(11, 12)

同struct / class一样,可以使用值传参、引用传参、指针传参,引用传参时符号&可以写成%:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 值传参
void Test06(CLIValuePointClass p) {
p.X = 99;
}
Test06(p1);

// 引用传参1
void Test07(CLIValuePointClass& p) {
p.X = 999;
}
Test07(p1);

// 引用传参2
void Test07_1(CLIValuePointClass% p) {
p.X = 1000;
}
Test07_1(p1);

// 指针传参
void Test08(CLIValuePointClass* p) {
p->X = 9999;
}
Test08(&p1);

若接收托管类型参数,可以采用托管指针传参、托管指针引用传参,后者可以修改指针本身的值(类似C#中的ref参数):

1
2
3
4
5
6
7
8
9
10
11
12
// 托管指针传参
void Test09(CLIValuePointClass^ p) {
p->X = 99999;
}
Test09(p2);

// 托管指针引用传参
void Test10(CLIValuePointClass^% p) {
// 修改指针本身的值
p = gcnew CLIValuePointClass(9999, 9999);
}
Test10(p2);

(3)ref struct / ref class

ref struct和ref class的表现一样。只能声明为托管类型:

1
CLIRefPointClass^ p1 = gcnew CLIRefPointClass(11, 12);

可以采用托管指针传参、托管指针引用传参,后者可以修改指针本身的值(类似C#中的ref参数):

1
2
3
4
5
6
7
8
9
10
11
12
// 托管指针传参
void Test11(CLIRefPointClass^ p) {
p->X = 99999;
}
Test11(p1);

// 托管指针引用传参
void Test12(CLIRefPointClass^% p) {
// 修改指针本身的值
p = gcnew CLIRefPointClass(9999, 9999);
}
Test11(p1);

六种类型中允许使用的数据类型

(1)struct / class

  • 无法定义托管类型的字段。
  • 方法的参数和返回值可以为托管类型。

(2)托管类型(value struct / value class 和 ref struct / ref class)

  • 不能定义非托管类型的字段。
  • 可以定义非托管类型的C+ +普通指针类型。
  • 可以使用C+ +的基本数据类型,因为在这些类型可以和C#数据类型自动转换。
  • 托管类型的方法参数和返回值可以为非托管类型,但是无法传参。

C+ +/CLI的其他托管特性

C+ +/CLI的其他诸多特性,例如enum class、interface class、property、委托、事件等,都是为扩展C+ +语言以支持更多的托管特性,如果采用C+ +/CLI来开发独立程序,使用的价值会比较大。

最佳实践

六种类型在原生C+ +或C#项目中呈现什么样子?

当使用C+ +/CLI开发独立应用程序时,可以在程序逻辑中混用C+ +和C#的数据类型。

但是如果使用C+ +/CLI封装成DLL供原生C+ +或C#项目使用,这六种类型会呈现什么样子呢?

原生C+ +项目的角度

站在原生C+ +项目的角度看六种类型,可以通过简单分析得到结论:原生C+ +项目要想引用C+ +/CLI的DLL,必须包含其头文件(指明了DLL导出的数据类型),如果这个头文件中包含任何托管代码特性,那么在原生C+ +项目中就会报错(理由很明晰:原生C+ +项目不支持托管代码特性)。

所以,使用C+ +/CLI封装程序库给原生C+ +项目使用时,头文件中不应该包含任何托管代码特性;但是在实现导出类成员方法或导出函数的具体逻辑时,可以使用托管代码。

C#项目的角度

站在C#项目的角度看六种类型,可通过代码实验得到结论:

  • C+ +/CLI中的struct / class不可见,需在前面加上关键字public。在C#项目中,变成struct类型,其中的字段和方法不可访问;
  • C+ +/CLI中的value struct / value class在C#项目中是struct类型。
  • C+ +/CLI中的ref struct / ref class在C#项目中是class类型。
  • C+ +/CLI中的四种托管类型中,可以声明非托管类型的指针成员变量,也可以定义参数和返回值均为非托管类型的方法,但是这些变量和方法无法在C#项目中直接被使用。

小结

六种类型 原生C+ +项目里 C#项目里
struct struct
class class
value struct struct
value class struct
ref struct class
ref class class

C+ +/CLI实现跨语言调用既有库的最佳实践

基于以上各种结论,在使用C+ +/CLI封装程序库给原生C+ +或C#项目使用时,注意如下:

(1)封装程序库供C+ +使用:

  • 在C+ +/CLI的头文件中不能包含任何托管代码特性。
  • 封装C#代码给原生C+ +项目使用:在C+ +/CLI的导出类方法成员或导出函数内部使用C#的静态方法,或实例化C#类,从而使用既有C#的代码。

(2)封装程序库供C#使用:

  • 凡是要暴露给C#项目的接口,统一使用托管类型。
  • 托管类型的指针成员变量、参数或返回值为非托管类型的方法,只能充当函数逻辑实现的辅助,无法暴露给C#项目(避免出错,最好不要使用!)
  • 封装原生C+ +代码给C#项目使用:可以定义一个C+ +类指针成员变量,作为原生C+ +代码的统一访问入口;(推荐方法)
  • 封装原生C+ +代码给C#项目使用:可在C+ +/CLI的方法内部使用原生C+ +代码中的静态方法,或实例化原生C+ +代码里面的类,从而使用既有原生C+ +的代码。

以上就是使用C+ +/CLI实现程序库跨语言调用的最佳实践,后面两篇文章将以示例代码来展开说明。

评论