C++/CLI 实战技术收集(我讨厌MFC)

时间:2022-09-01 09:09:33

 在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是一个比较有意思的两栖模块,它具有如下特点

  1. 既可以访问.Net类库,也可以访问C++原生类库
  2. 既可以被.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 aint 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^ get()
    {
        return m_value;
    }
    void set(String^ value)
    {
        m_value = value;
    }
}

string Name 
{
    get
    {
        return m_name;
    }
    set
    {
        m_name = value;
    }
}


C++/CLI是C++的扩展,针对.NET Framework编程进行了增强设计,可以实现类似C#的功能,同时兼容原有的C++功能。
这篇文章针对已经学习过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定义相同
}