定位
C++/CLI是C++语言的一个变种,不仅支持原生C++的语法特性,也支持托管代码特性,这样就达到了在C++中调用C#库的目的。
结合原生C++的性能和C#的强大生态,C++/CLI可以高效开发独立运行的程序(控制台程序、窗体程序等),可惜的是,这并没有成为主流(大概是因为C++和C#两者的应用场景过于分明,实际并不需要大规模混合使用)!C++/CLI作为连接原生C++和C#的桥梁,通常被用来实现原生C++和C#既有程序库的跨语言调用。
所以,本文聚焦在C++/CLI的核心语法特性上,不会对其侧重于独立软件开发的语法特性进行过多介绍。
基本语法
“隙中窥月”
// 使用原生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#数据类型自动转换。
- 托管类型的方法参数和返回值可以为非托管类型,但是无法传参。
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实现程序库跨语言调用的最佳实践,后面两篇文章将以示例代码来展开说明。