在Windows上,除非我们必须得用C++来写界面,否则我会选择避免,避免学习和使用MFC。替代的方案是用C#来做界面,然后用C++/CLI来连接C#和Native C++。那么问题来了,C++/CLI是何方神圣?
百度上对于C++/CLI是这么说的——“C++/CLI是静态C++对象模型到CLI的动态组件对象编程模型的捆绑。简而言之就是如何用C++在·NET中编程,而不是C#或Visual Basic。像C#和CLI本身一样,C++/CLI正在ECMA(欧洲计算机制造商协会)主持下进行标准化,以最终符合ISO标准。公共语言运行时(CLR)是CLI的微软版本,它非常适用于微软的Windows操作系统,相似地,Visual C++2005是C++/CLI的实现。”
我是这么说的——C++/CLI相当于C#和Native C++的组合语言,多数情况下你可以像C#一样写代码托管代码,或者像Native C++那样写本地代码,或者在同一个文件里面同时包含这两种语言。编译器会很好的区分这两者。
某大牛是这么说——.Net的归.Net,C++的归C++。这里Net指得是托管代码,C++指得是本地代码。
让我们来点细节吧:
1、从VS里的项目类型上说,有三种:一种是本机代码并且不使用CLR支持。这种项目还是原来的C++项目,没有任何改变,直接编译出本机代码。可以被C++/CLI项目引用,但是不能被其他托管语言项目引用。第二种是原来本机代码,编译时加入CLR支持,变成能够被其他.Net语言(例如C#)调用的项目或者dll。这种项目里本身不含托管代码,但可以被其他托管代码引用(包括C++、C#等)。第三种是CLR项目,这是一个原生托管项目,其中除了默认的托管代码,还可以用#pragma unmanaged来指示将代码编译成本机代码。可以直接调用其他托管代码或者本机代码,也可以被其他托管代码调用。
2、从内存上说,C++/CLI的内存分为3种:栈内存、本机堆内存和托管堆内存。栈内存中可以包含本机对象和托管对象,退出所在范围他们的生命周期无差别地结束。本机堆内存是Native C++的动态内存,可以通过new或者malloc关键字动态动态分配。使用完毕后,由程序员手动释放,否则会内存泄露。托管堆内存是托管对象的动态内存,由gcnew关键字动态分配。不再使用的托管对象内存由公共运行时垃圾回收器(.Net GC)回收,不用程序员处理。
3、从类库上说,CLR项目原生支持.Net Framework中满足CLS的所有类型。比如C#中常见的String、List、Dictionary等,在这里的使用方式基本不变。
这里只是简介,后续会有更详细的内容。。。
C#和C++是非常相似的两种语言,然而我们却常常将其用于两种不同的地方,C#得益于其简洁的语法和丰富的类库,常用来构建业务系统。C++则具有底层API的访问能力和拔尖的执行效率,往往用于访问底层模块和构建有性能要求的算法。
这两种场景看起来有较大的差异,大多数的时候可以各行其道。但还是有很多时候会出现融合的情况。当我们构建分布式系统的时候,由于RPC机制一般都是语言无关的,我们大可以将其各尽所长,按需划分在最能发挥其长处的位置。然而,一旦我们需要构建融合两者需求的集中式系统的时候,就会头痛无比。
此时,我们可以使用C++/CLI搭建C++和.Net之间的桥梁,C++/CLI是一个比较有意思的两栖模块,它具有如下特点
-
既可以访问.Net类库,也可以访问C++原生类库
-
既可以被.Net程序引用,也可以被C++原生程序引用
使用C++/CLI,我们可以使用C++编写算法,用C#编写界面,也可以使用.Net Framework类库增强C++程序功能,各取所长。关于的优点,园子里有篇文章介绍的比较详细,值得一读:从C++到C++/CLI。
下面我们就以一个简单的例子来演示一下它的用法:
Calculator.h:
#pragma once
namespace CppCliTest
{
public ref class Calculator
{
public:
int Add(int a, int b);
};
}
Calculator.cpp
#include "stdafx.h"
#include "Calculator.h"
namespace CppCliTest
{
int Calculator::Add(int a, int b)
{
return a + b;
}
}
main.cpp
#include "stdafx.h"
#include "Calculator.h"
using namespace System;
using namespace CppCliTest;
int main(array<System::String ^> ^args)
{
Calculator^ calculator = gcnew Calculator();
int result = calculator->Add(3, 2);
Console::WriteLine(L"Result is {0}", result);
return 0;
}
从这个例子中,我们可以简单的管中窥豹的看看C++/CLI是在C++的基础上扩充了一套语法,使其具有访问.Net原始的功能,这里用到的有:
-
使用ref class声明CLI引用类型(C#中的class)
-
使用^(例如如这里的String ^)来定义CLI引用类型
-
使用gcnew创建CLI的引用类型
具体的功能我将在后面的文章中再做介绍,MSDN中也有文档详细的介绍了这些语法:https://msdn.microsoft.com/zh-cn/library/ms235289.aspx
虽然C++/CLI同时具有两者的功能,但它使得本就比较复杂的C++语法变得更加复杂了(特别是初期的版本,非常复杂,现在已经简化了不少了),并且长期没有得到VisualStudio这宇宙第一IDE的较好支持(在VS2010的时候还不支持智能提示),是无法与拥有大量语法糖的C#比开发效率的。加上大多数需求场景可以通过分布式系统解决,这些都导致了它一直没有得到太多的关注。但是,微软还是在积极的改进它的,加上C++11的支持,现在已经比之前好用多了,如果用在合适的位置,是绝对能让你的开发如鱼得水的。
文章中的对比表,十分清晰的展示了版本2语言中设计的简洁和与原生语言的接近。值得参考:
描述 |
C++/CLI |
C# |
创建引用类型的对象 |
ReferenceType^ h = gcnew ReferenceType; |
ReferenceType h = new ReferenceType(); |
创建值类型的对象 |
ValueType v(3, 4); |
ValueType v = new ValueType(3, 4); |
引用类型在堆栈上 |
ReferenceType h; |
N/A |
调用Dispose方法 |
ReferenceType^ h = gcnew ReferenceType; delete h; |
ReferenceType h = new ReferenceType(); ((IDisposable)h).Dispose(); |
实现Dispose方法 |
~TypeName() {} |
void IDisposable.Dispose() {} |
实现Finalize 方法 |
!TypeName() {} |
~TypeName() {} |
装箱(Boxing) |
int^ h = 123; |
object h = 123; |
拆箱(Unboxing) |
int^ hi = 123; int c = *hi; |
object h = 123; int i = (int) h; |
定义引用类型 |
ref class ReferenceType {}; ref struct ReferenceType {}; |
class ReferenceType {} |
定义值类型 |
value class ValueType {}; value struct ValueType {}; |
struct ValueType {} |
使用属性 |
h.Prop = 123; int v = h.Prop; |
h.Prop = 123; int v = h.Prop; |
定义属性 |
property String^ Name |
string Name |
这篇文章针对已经学习过C++和C#的读者,主要介绍C++/CLI的增强特性,方便读者快速熟悉和掌握C++/CLI这种增强编程语言。
一、访问命名空间、静态类、对象成员的方法
对于命名空间和静态类成员,C++/CLI使用范围限定操作符::来访问,例如:
using namespace System::Windows::Forms; // 访问命名空间
System::Windows::Forms::Button ^button1; // 访问命名空间中的类
Console::WriteLine("错误:文件不存在!"); // 访问静态类成员
对于对象成员,C++/CLI使用->来访问,例如:
this->Text="控制面板自定义";
listBox1->Items->Add("简明DOS教程");
对于非引用对象(基本类型、原生类型、结构等),C++/CLI使用.运算符来访问,例如:
Size.Width.ToString();
也可以混合使用:
System::Console::Error->WriteLine("错误代码:"+errno.ToString());
二、.NET对象的使用
大多数.NET对象仅允许在堆中创建,在堆中创建的方法是使用.NET对象指针^和gcnew操作符。
C++/CLI使用^修饰符修饰.NET对象指针(官方称为对象句柄),使用方法和传统的指针*差不多,但是不允许运算,例如:
ListBox ^listBox1;
String ^str1;
对.NET对象指针解引用,使用*操作符,和C++指针一样,例如:
(*label1).Text="用户名";
C++/CLI使用gcnew操作生成新的.NET对象,其作用类似其它语言的new运算符,.NET有自动垃圾回收机制,不需要手动释放,例如:
this->listBox1 = gcnew ListBox();
对于变量的引用,使用%来表示,类似于C#中的ref修饰符,以及C++中的&修饰符,如:
void increase(int% val) { ... } // 引用传递
void trimstr(String^% str) { ... } // 对.NET对象引用传递
三、数组
不同于C#的简单语法,C++/CLI使用以下方法来定义数组:
array<int> ^intarray = gcnew array<int>{1, 2, 3}; // 预定义元素
array<String^> ^strarray = gcnew array<String^>(5); // 5个空元素
数组的访问:
for (int k=0; k<intarray.Length; k++) {
MessageBox::Show(intarray[k].ToString());
}
四、字符串
C++/CLI使用以下方法来定义字符串:
String ^str1="我是字符串\n";
String ^str3=gcnew String("我是字符串\n");
String^和字符串常量均支持+运算符,并且字符串常量相加后自动转换为String^,如:
str1 = "发生错误!\n"+"错误代码:"+errno.ToString();
将数据转换为字符串的方法:
String ^str2=String::Format("错误代码:{0} 错误消息:{1}",24,"未找到文件");
String ^str4=errno.ToString();
将字符串转换为数字的方法:
int errno=Int32:: Parse(strerrno);
int errno=Convert::ToInt32(strerrno);
五、for each语法
ISO在C++11中加入了for遍历语法用以遍历集合元素,然而在C++/CLI中微软先行加入了这一语法,但是语法结构有所不同:
for each(int val in vals) { ... } // 数值传递
for each(int% val in vals) { ... } // 引用传递
for each(String^ msg in msglist) { ... }
for each(String^% str in strlist) { ... }
六、类语法
.NET类的定义如下,不同于传统C++类:
ref class MyClass [修饰符] : public BaseClass, IBaseInterface { ... };
.NET接口的定义如下:
interface class IMyInterface [修饰符] : public IBaseInterface { ... };
.NET结构定义如下:
value class MyStruct [修饰符] : public IBaseInterface { ... };
.NET枚举定义如下:
enum class MyEnum : basetype { ... };
class也可换成 struct ,区别只是默认访问权限不同,class默认是private而struct默认是public。
其中修饰符可以是 abstract 或 sealed ,分别表示抽象类和不可派生类。
泛型(generic)语法如下,不同于C++模板(template):
generic <typename T>
ref class MyClass : public BaseClass,IBaseInterface { ... };
七、成员语法
成员虚函数可在最后加上.NET修饰符,如:
ref class B{ public:
virtual void f();
virtual void g() abstract; //纯虚函数,需要派生类重写,否则派生类就是纯虚类
virtual void h() sealed; //阻止派生类重写该函数
virtual void i();
};
ref class D : B { public:
virtual void f() new; //新版本的f,虽然名字和B::f相同,但是并没有重写B::f。
virtual void h() override; //错误!sealed函数不能被重写
virtual void k() = B::i; //“命名式”重写
};
析构函数和回收函数:
析构函数~ClassName()和C++中的析构函数相同,当出作用域时被调用,相当于C#中的Dispose()。
回收函数!ClassName()则相当于C#中的Finalize(),在系统真正delete时被调用,不能确定调用时机,不建议使用。
MyClass() { ... } // 构造函数
~MyClass() { ... } // 析构函数Dispose()
!MyClass() { ... } // 回收函数Finalize()
literal表示.NET常量,在编译时展开,总是静态成员,相当于C#中的const。
literal double MyPI=3.14;
circum = diameter * MyClass::MyPI;
initonly表示.NET只读成员,最多只能在构造函数初始化一次,相当于C#中的readonly。
initonly int PortNo=42;
initonly String^ FileName; // 只能在构造函数中初始化
property表示.NET属性,如:
property String ^FileName; // 纯数值属性
property String ^Text { // 自定义set和get方法
void set(String^ value) { ... }
String^ get() { ... }
}
property int Nums[int] { // 带有索引的.NET属性
void set(int index, int value) { ... }
int get(int index) { ... }
}
八、指针
在C++/CLI中,除了传统指针(*)和.NET对象指针(^)外,还增加了两种指针类型:pin_ptr(图钉指针)和interior_ptr(托管指针)。
由于.NET堆地址有自动垃圾回收机制,因此对象地址经常变动的,不便于普通指针使用。
当传统使用指针时需要先使用pin_ptr(图钉指针)钉住对象。而interior_ptr(托管指针)则封装了可变特性,可以和普通指针一样使用。
pin_ptr(图钉指针)可以钉住堆地址以供传统指针使用(但不是万能的):
array<char>^ arr = gcnew array<char>(3);
pin_ptr<char> p = &arr[0]; // 整个arr都被定在堆上
char* pbegin=p; // 现在可以使用传统指针了
interior_ptr(托管指针)可以像传统指针一样使用:
array<char>^ arr = gcnew array<char>(3);
interior_ptr<char> begin = &arr[0]; //指向头部的指针
interior_ptr<char> end = begin + 3; //注意,不能写&arr[3],会下标越界
指针的空值为nullptr,相当于C#中的null,对应传统C++中的NULL,未初始化的任何指针默认值都是nullptr,如:
ListBox ^listbox1; // 值为nullptr,下同
int *valptr;
pin_ptr<char> pinptr;
interior_ptr<char> chrptr;
listbox1 = nullptr; // 手动设置nullptr
九、delegate和event
作为.NET的增强特性,C++/CLI当然也不会缺少delegate。delegate相当于定义一个函数指针类型,但是比函数指针强大,如下:
delegate int MyDelegate(int num1, int num2);
delegate的使用方法:
MyDelegate ^del1 = gcnew MyDelegate(&fnstatic); // 绑定静态函数
MyDelegate ^del2 = gcnew MyDelegate(this, &MyClass::fnmember); // 绑定成员函数
result = del2(2, 3);
event则是.NET事件,实质上是一种特殊的.NET属性,如:
event MyDelegate ^ OnMyEvent1;
event MyDelegate ^ OnMyEvent2 {
void add (MyDelegate ^ evt) { ... }
void remove(MyDelegate ^ evt) { ... }
int raise(int num1,int num2) { ... } // 必须和delegate定义相同
}