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

原文地址:李浩的博客 lihaohello.top


定位

C++/CLI是C++语言的一个变种,不仅支持原生C++的语法特性,也支持托管代码特性,这样就达到了在C++中调用C#库的目的。
结合原生C++的性能和C#的强大生态,C++/CLI可以高效开发独立运行的程序(控制台程序、窗体程序等),可惜的是,这并没有成为主流(大概是因为C++和C#两者的应用场景过于分明,实际并不需要大规模混合使用)!C++/CLI作为连接原生C++和C#的桥梁,通常被用来实现原生C++和C#既有程序库的跨语言调用。
所以,本文聚焦在C++/CLI的核心语法特性上,不会对其侧重于独立软件开发的语法特性进行过多介绍。

基本语法

CLI-语法深化.jpg

“隙中窥月”

// 使用原生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,可声明到栈或堆上:

// 声明到栈上
CppPointClass p1(5, 6);

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

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

// 值传参
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);

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

// 指针的引用传参
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的表现一样。可以声明为值类型,也可以声明为托管类型:

// 声明为值类型
CLIValuePointClass p1(15, 16);

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

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

// 值传参
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参数):

// 托管指针传参
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的表现一样。只能声明为托管类型:

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

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

// 托管指针传参
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#数据类型自动转换。
  • 托管类型的方法参数和返回值可以为非托管类型,但是无法传参。

image.png

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#项目里
structstruct——
classclass
value struct——struct
value classstruct
ref structclass
ref classclass

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实现程序库跨语言调用的最佳实践,后面两篇文章将以示例代码来展开说明。


原文地址:李浩的博客 lihaohello.top

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值