托管C++

本文转自http://www.cnblogs.com/pursue/articles/1992324.html,如有侵权,请联系删除

原文请看(这里)

简介 
你好,欢迎光临本人关于.NET编程,特别是托管C+ +的第一篇文章。学习托管C++(简称MC++)是一次有趣并且新奇的体验,为了充分吸收相关技巧和知识,我把学到的绝大多数技巧和知识都应用在示例中,并为示例加了很多注释,用来标明应该用什么以及怎样用。

不像我的其他文章,这篇文章将以直接和体验的方式为你讲解托管C++。我不是说你会学到一切,但你会掌握要领,然后你可以按照自己的方式继续。你可以把这篇文章看作是你将在工作中需要的一些知识的快速参考。

为了最大限度吸收文中所讲,我建议你了解C / C++,一些有关.NET Framework的知识,并且最好对C#有一定了解。 
我将提交​​十二个例子,每个例子之前会有一段简短的介绍文字,并且有很多注释。你可以通过下面的命令行编译示例:

"C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\Tools\vsvars32.bat"

cl your_file.cpp /clr

请注意,示例中提到的一些能在7.0和7.1中运行的情形可能在托管C++8.0中无法运行

这些主题包括:

1.介于托管和非托管代码之间

2.托管类

3.托管字符串

4.枚举和装箱

5.Pinning指针

6.属性

7.代理

8.值类型,结构和联合

9.托管数组

10.平台调用(PInvoke)

11.多线程

12.使用Windows窗体

13.C/C++和.NET Framework之间的等效性

1.介于托管和非托管代码之间

这是我们的第一个例子。为了使用.net Framework, 我们需要把添加引用:“#using <mscorlib.dll>” 。通过”cl filename.cpp /clr”来编译托管代码。你可以像示例中那样,通过使用unmaged/managed 编译控制指令在托管程序中使用非托管代码

#using <mscorlib.dll> // to use Console::WriteLine
 
#include <stdio.h> // to printf()
using namespace System;
 
// Mark unmanaged code
#pragma unmanaged
void print(char *msg)
{
  printf("%s\n", msg);
}
 
// Switch back to managed code
#pragma managed
 
int main()
{
  // Write to the console through managed call
  Console::WriteLine(S"Hello world from managed method");
 
  // use stdio to write to console
  print("hello world from unmanaged method");
  return 0;

}

2.托管类

为了表明一个类是托管的,你必须用"__gc"修饰符来声明。这个修饰符可以和类,结构和变量一起使用,以被垃圾收集器明确标识为托管。

注意,你不能删除分配的(托管:译注)类实例,因为它由垃圾收集器负责。

你可以省略"__gc"修饰符,这样将创建一个原生C++类。或者你可以通过"__nogc"来实现同样效果

你可以创建静态构造器,然而,他们不允许有参数。静态构造器在对其某个成员的第一次访问前被调用。

#using <mscorlib.dll>
 
using namespace System;
 
// A simple class
__gc class CHelloWorld
{
  System::String *_msg;
public:
  CHelloWorld(System::String *Msg)
  {
    _msg = Msg;
  }
 
  void Print()
  {
    Console::WriteLine(_msg);
  }
};
 
// Yet another class w/ but a static constructor
__gc class CHelloWorldStatic
{
  static System::String *_static_msg = 
    S"Static message called because static constructor invoked!";
  static Int32 _instance_cnt = 0;
 
public:
 
  // constructor
  CHelloWorldStatic()
  {
    _instance_cnt++;
    Console::WriteLine(System::String::Concat("So far ", 
             _instance_cnt.ToString(), " instance(s)"));
  }
 
  // static constructor making use of the static member
  static CHelloWorldStatic()
  {
    Console::WriteLine(_static_msg);
  }
 
};
 
int main()
{
  // create an instance of the class
  CHelloWorld *c1 = __gc new CHelloWorld(S"Hello world from the class");
 
  c1->Print();
 
  // take an instance of the class that has static constructor
  // now we will notice that first, the static constructor
  // will be called, then the ordinary constructor
  CHelloWorldStatic *c2 = __gc new CHelloWorldStatic();
 
  return 0;

}

 

3.托管字符串

.NET中,字符串是托管对象,它们的功能很广泛。System::String对象是不可改变的。也就是说,如果你改变了字符串你会得到一个全新的字符串,该字符串是修改后的版本。如果你希望能够字符串缓冲,你应该使用System::Text::StringBuilder对象。

Unicode中,String对象通过Char类型来保存每个字符。为了访问某个单独的字符你可以这样:

String *str = S"hello";

Char c = str->Chars[3];

当你以"S”开始声明一个字符串的时候,这表示字符串是托管的。

你不能像这样比较两个字符串"if (s1 == s2)",因为这将比较字符串的引用,如果它们引用相同的时候会返回true。相反,你应该调用String::Equals()或String::Compare().

你也不能将托管字符串传递给C++标准库,或者CRT function.你可以调用System::Runtime::InteropServices中的Marshal::StringtoHGlobalUni方法. 你应该总是通过Marshal::FreeHGlobal()来释放返回的字符串。

VC++提供了一个辅助功能,PtrToStringChars(),它定义在”vcclr.h”中,使你可以访问托管字符串的内部wchar_t*对象。

你可以通过Marshal::PtrToStringAnsi或PtrToStringUni将ANSI或Unicode转化为托管字符串。

下面的示例可以说明这一切:

#using <mscorlib.dll>
#include <vcclr.h> // PtrToStringChars()
#include <stdio.h>
 
using namespace System;
using namespace System::Runtime::InteropServices;
 
// This is to test the system provided PtrToStringChars() function
void test_ptrtostringchars()
{
  // create a managed string
  String *s = S"Hello world\n";
 
  const Char __pin *p = PtrToStringChars(s);
 
  wprintf(p);
}
 
// Test to demonstrate string comparison and management
void test_stringcomparison()
{
  String *s1 = S"Hi";
  String *s2 = S"Hello";
 
  // grab the "H" from "Hi"
  String *temp1 = s1->Substring(0, 1);
  // grab the "H" from "Hello"
  String *temp2 = s2->Substring(0, 1);
 
  // This is a !!WRONG!! comparison,
  // because references will be checked
  if (temp1 == temp2)
  {
    Console::WriteLine("wow cool...the strings were equal");
  }
 
  // !!CORRECT!! string comparison
  if (temp1->Equals(temp2))
    Console::WriteLine("wow cool...the strings were Equal()");
}
 
// Test to demonstrate the string builder
void test_stringbuilder()
{
 
  Text::StringBuilder *sb = new Text::StringBuilder;
 
  sb->Append("This is first line\n");
 
  for (int i=2;i<=10;i++)
  {
    sb->AppendFormat("This is line #{0}\n", i.ToString());
  }
 
  Console::WriteLine("The total string built is: {0}", sb->ToString());
}
 
//
// This test demonstrates the usage of StringToHGlobal[Ansi|Uni] functions
// Allowing you to pass a managed string to an unmanaged function
//
void test_hglobal()
{
  String* s = S"This is a String!"; 
 
  // Get an ANSI pointer out of the managed string
  char *ansi = (char *) Marshal::StringToHGlobalAnsi(s).ToPointer();
 
  // Get a UNICODE pointer out of the managed string
  wchar_t *unicode = (wchar_t *) Marshal::StringToHGlobalUni(s).ToPointer();
 
  printf("printf_ansi(): %s\n", ansi);
  wprintf(L"printf_unicode(): %s\n", unicode);
 
  // Free the buffers
  Marshal::FreeHGlobal(ansi);
  Marshal::FreeHGlobal(unicode);
}
 
// Function that converts an ansi string to a managed string
String *ansi_to_managed(char *str)
{
  return Marshal::PtrToStringAnsi(str);
}
 
// Function that converts an ansi string to a managed string
String *unicode_to_managed(wchar_t *str)
{
  return Marshal::PtrToStringUni(str);
}
 
int main()
{
  // create a managed string from ANSI
  String *s1 = ansi_to_managed("Hello world! ! ! ! ! ");
 
  // create a managed string from UNICODE
  String *s2 = unicode_to_managed(L"Hello world! ! ! ! ! ");
 
  test_ptrtostringchars();
 
  test_stringcomparison();
 
  test_stringbuilder();
 
  test_hglobal();
 
  return 0;

}

 

4.枚举和装箱

枚举是值类型,并且具有类似的特点。他们继承自System::Enum->System::ValueType,所以可以被转化为其他类型。枚举值是整数类型

你可以指定[Flags]属性

当你对一个enum执行ToString()的时候将会发生两件事:

当不使用[Flags]的时候:ToString()将返回enum的数值(如果使用enum组合的话)

当不使用[Flags]的时候:ToString()将返回enum的名字,如果只用1个enum

当使用[Flags]的时候:ToString()将返回以逗号隔开的enum的名字,如果用enum组合的话

当使用[Flags]的时候:ToString()将返回enum的名字(只用1个enum)

.NET通过一个称为装箱的过程允许你将一个值类型转换为一个__gc对象。你只能将值类型的数据装箱。当装箱一个值时,运行时在heap上创建一个新对象,这个对象包含被装箱的值。

当被装箱的值发生改变的时候,修改只影响装箱后的类型,而不会对开始被装箱的值类型产生影响。

正如之前说的,当你装箱一个值类型时,你将一个值类型转换为一个__gc类型。下面演示怎样从这些类型转回去(这个过程称为拆箱)

MyInt p2(2); // create a value-type

MyInt __gc *p3 = __box(p2); // create a non value-type

MyInt p4 = *p3; // dereference and create a value-type of from the boxed object

#using <mscorlib.dll>
 
using namespace System;
using namespace System::Collections;
 
// Specify values
// If you try to ToString(RED | GREEN) you will get
// a numerical value, unlike the case
// when using [Flags] attribute
__value enum Color : unsigned int
{
  RED   = 0xFF0000,
  GREEN = 0x00FF00,
  BLUE  = 0x0000FF
};
 
 
// This example show you how to use the [Flags] attribute
// Flag allows the system to treat the items as bitfields
// The call to ToString() will try to see what
// combination of flags is the value composed of
[Flags]
__value enum Features: unsigned int
{
    feature_1 = 1,
    feature_2 = 2,
    feature_3 = 4,
    feature_all = feature_1 | feature_2 | feature_3
};
 
// Define a value-type named MyInt
__value struct MyInt
{
  Int32 val;
 
  MyInt(Int32 v)
  {
    val = v;
  }
 
};
 
int main()
{
  // Shows how the values are being displayed
  Console::WriteLine(S"RED as string: {0}\n(RED | GREEN) as string: {1:x}", 
                __box(RED)->ToString(), __box((Color)RED | GREEN)->ToString());
 
  Console::WriteLine();
 
  // Shows the effect of the flags attribute
  Console::WriteLine(S"(feature_1 | feature_2) as string: {0}", 
                __box((Features)(feature_1 | feature_2))->ToString());
 
  Console::WriteLine();
 
  // Print all the members in that enum
  Array *list = Enum::GetNames(__typeof(Color));
  for (Int32 i=0;i<list->Length;i++)
  {
    Console::WriteLine(S"Item {0} is called {1}", i.ToString(), 
                                    list->GetValue(i)->ToString());
  }
 
 
  Console::WriteLine();
 
  // Convert from an enum name to an enum value
  // This will show how to convert a name to its enum value
  Color c;
  Object *o = Enum::Parse(__typeof(Color), S"RED");
 
  c = *static_cast<__box Color *>(o);
 
  Console::WriteLine(S"{0}", __box(c)->ToString());
 
  Console::WriteLine();
 
  // Converting from value type to non-value type
  MyInt vt1(14656); // value-type
  MyInt *b = __box(vt1); // create a new boxed instance , 
      //any modification occur only to the boxed instance
 
  Console::WriteLine(S"b->val={0}", b->val.ToString());
  b->val++;
  Console::WriteLine(S"After incrementing b->val, 
                     vt1.val={0} and b->val={1}", 
                     vt1.val.ToString(), b->val.ToString());
 
  return 0;

}

 

5.固定指针(Pinning Pointer)

托管指针被垃圾收集器管理并跟踪。当你想要传递一个gc指针给一个非gc方法的时候,你需要一个方法来告诉垃圾收集器不要移动或者丢弃这个对象。

当你固定一个托管对象,你可以把固定指针传给非托管方法。

固定对象会增加对象引用计数,并且通知垃圾收集器,这样它就不会移动内存中的对象。

#using <mscorlib.dll>
#include <stdio.h>
 
using namespace System;
 
// just a simple unmanged method
#pragma unmanaged
void print_msg(char *s)
{
  printf("printf() -> %s\n", s);
}
 
#pragma managed
 
 
int main()
{
  // Construct a byte array
  Byte arr[] = new Byte[20];
 
  // fill the array with: Hello
  arr[0] = 'h'; arr[1] = 'e'; arr[2] = 'l'; 
  arr[3] = 'l'; arr[4] = 'o'; arr[5] = 0;
 
  // Pin the pointer to the first element
  unsigned char __pin *str = &arr[0];
 
  // Call the managed method
  print_msg((char *)str);
 
  return 0;

}

 

6.属性(Properties)

通常类或结构体中定义的变量称为字段。这些字段可以在未验证或检查之前被修改。属性是一种允许我们通过get/set方法验证/监测一个字段读写的手段。

为了创建属性,你需要在原型之前使用__property关键字,然后在函数名前加上"set_”或者"get_”前缀。

你也可以使用索引get属性,也就是说不是有一个类似"Name”的属性,而是形如Name[“index”]的属性。或者数字索引,比如Name[1234]。你甚至可以有二位索引,例如Name[2,3]

#using <mscorlib.dll>
 
using namespace System;
 
// Define the managed class
__gc class Student
{
private:
  String *_name;
 
public:
  // declare the GET property for Name
  __property String *get_Name()
  {
    if (_name == 0)
    {
      _name = new String(S"Noname");
    }
    return _name;
  }
  // declare the SET property for Name
  __property void set_Name(String *n)
  {
    if (n == 0)
      throw new ArgumentException(S"You must pass a name!");
    _name = n;
  }
 
  // String index property
  __property String *get_Hey(String *index)
  {
    return index;
  }
 
  // Two indexed property
  // We can access this as: Mul[1,2]
  __property int get_Mul(int x, int y)
  {
    return x * y;
  }
};
 
int main()
{
  // create an instance
  Student *s = __gc new Student();
 
  // Display the name property value
  Console::WriteLine(S"Student name={0}", s->Name);
 
  // modify the property value (internally will call the set method)
  s->Name = S"Hey!";
 
  // Display the updated value
  Console::WriteLine(S"Student name={0}", s->Name);
 
  // Call a string indexed property named "Hey"
  Console::WriteLine(S"Student name={0}", s->Hey["Hello"]);
 
  // Call a two integer indexed property named "Mul"
  Console::WriteLine(S"x*y={0}", (s->Mul[2][3]).ToString());
 
  return 0;

}

 

7.委托

委托和C/C++中的回调函数类似。委托只能被一个__gc类绑定到一个或多个方法。

当你初始化一个委托的时候,你需要传递两个参数

一个指向__gc类或NULL(如果你准备绑定到__gc类的一个静态方法的话)的指针

你准备绑定的方法的指针。

一旦委托实例被创建,你将有一个和代理具有相同签名/原型的Invoke方法。所以有两种等同的调用方式:delegate_name(param_list)或delegate_name->Invoke(param_list).

委托继承自System::MulticastDelegate, 因此你可以列出,组合或从委托链中移除委托。

可以参考MSDN(__delegate关键字的引用)了解更多关于委托的信息。

#using <mscorlib.dll>
 
using namespace System;
using namespace System::Collections;
 
/
 
// Here we define the prototype of the delegate
public __delegate int CallMethod(String *);
 
// This is a simple class that provides two callback functions
// one static and one non-static. These two members will be called
// through the delegate
__gc public class Caller
{
public:
  // A function within the class that has same
  // prototype as "CallMethod" delegate
  int CallMe(String *s)
  {
    Console::WriteLine(s); 
    return s->Length;
  }
  // A static method within the class that has
  // same prototype as "CallMethod" delegate
  static int CallMeToo(String *s)
  {
    Console::WriteLine(S"static: {0}", s);
    return s->Length;
  }
};
 
 
// This method shows how to call delegates
// (callback) within a certain class instance
void delegate_member_test()
{
  Caller *c = new Caller();
 
  CallMethod *m = new CallMethod(c, &Caller::CallMe);
 
  int i;
 
  // Call delegate w/ its name
  i = m(S"Hello");
 
  // Or through Invoke()
  i = m->Invoke(S"Hello");
}
 
// This test shows how to call delegates
// (callback) that are static member functions
void delegate_static_test()
{
  Caller *c = new Caller();
 
  CallMethod *m = new CallMethod(c, &Caller::CallMe);
 
  int i;
 
  // Call delegate w/ its name
  i = m(S"Hello");
 
  // Or through Invoke()
  i = m->Invoke(S"Hello");
}
 
/
 
// Define a delegate that is supposed to do
// an arithmetic operation on two operands
// and that returns one result
public __delegate float BinaryOperationDelegate(float x, float y);
 
// This class is supposed to perform any binary operation
// based on the BinaryOperationDelegate that is passed to it
// For instance, if we create an addition class that has
// a binary operation delegate, then we pass this delegate to our
// binary arithmetic class and have the result calculated by this class.
public __gc class BinaryArithmeticClass
{
private:
  // the two operands
  float _a, _b;
 
  // Binary operation delegate
  BinaryOperationDelegate *_op;
 
public:
  // constructor that takes parameters
  BinaryArithmeticClass(float a, float b) : _a(a), _b(b), _op(0) { }
 
  // parameterless constructor
  BinaryArithmeticClass() { }
 
  // passes the delegate address
  void SetOperation(BinaryOperationDelegate *handler)
  {
    // assign new binary operation delegate
    _op = handler;
  }
 
  // does the calculation through the delegate
  float Calculate()
  {
    // uses the delegate to perform the artihmetic operation
    return _op(a, b);
  }
 
  // demonstration of properties to set/get the two operands
  __property void set_a(float a) { _a = a; }
  __property float get_a() { return _a; }
  __property void set_b(float b) { _b = b; }
  __property float get_b() { return _b; }
 
};
 
// This class is so simple it adds two numbers
// Notice how the Execute method has similar
// prototype as the BinaryOperationDelegate
// Since delegates are related to __gc classes only and add
// is so simple, we had to create a simple class
// with this static method
__gc public class SimpleAddOperation
{
public:
  static float Execute(float a, float b)
  {
    return a + b;
  }
};
 
// This class is used to convert from a two dimensional
// coords to one dimensional coord.
// This class needs a parameter named "width"
// so that we know how to convert to one dimension
// The purpose of this class is to show how delegates
// can easily work with methods that belong to
// a specific instance of a class
__gc public class TwoDimToOneDimConverter
{
private:
  float _width;
public:
  // Simple constructor
  TwoDimToOneDimConverter(float width) : _width(width) { }
  // This is the delegate that does the operation
  // The "width" member variable is involved in the operation
  float Execute(float x, float y)
  {
    return (y*_width) + x;
  }
};
 
// This test shows
void delegate_advanced_test()
{
  // Instantiate a binary operation class. This class is generic and does any
  // binary operation as long as it is passed
  // the right delegate that will eventually
  // carry the operation
  BinaryArithmeticClass *arith = __gc new BinaryArithmeticClass();
 
  // Create a delegate that is bound to a static method in the addition class
  BinaryOperationDelegate *add = new BinaryOperationDelegate(0, 
                                 &SimpleAddOperation::Execute);
 
  // Pass the two operands to the binary arithmetic class
  arith->b = 5;
  arith->a = 10;
 
  // Tell the arithmetic class that we are using the add delegate
  arith->SetOperation(add);
  Console::WriteLine("BinaryArithmeticClass using add delegate = {0}", 
                                    arith->Calculate().ToString());
 
  // -----------------------------------------------------------------
 
  // Create an instance of the 2d-1d class
  // We pass width = 10
  TwoDimToOneDimConverter *conv1 = __gc new TwoDimToOneDimConverter(10);
 
  // Create a delegate that works with that class
  BinaryOperationDelegate *convdelegate = new BinaryOperationDelegate(conv1, 
                                     &TwoDimToOneDimConverter::Execute);
 
  arith->a = 2;
  arith->b = 1;
 
  // Switch the artihmetic class to the 2d-to-1d class
  arith->SetOperation(convdelegate);
 
  // carry the operation, we expect the result: a + width*1 = 2 + (10*1) = 12
  Console::WriteLine("BinaryArithmeticClass using 2d-to-1d delegate = {0}", 
                                         arith->Calculate().ToString());
}
 
 
/
 
 
// This delegate is used to display a message from
// a member variable of the class that it is bound to
public __delegate void MessageDisplayDelegate();
 
// This class allows you to set a string into its member,
// then display that string to the string
// when needed
__gc public class MessageDisplay
{
private:
  String *_msg;
public:
  MessageDisplay(String *msg) : _msg(msg) { }
  void Display()
  {
    Console::WriteLine(_msg);
  }
};
 
 
 
// This test will demonstrate how to:
// - Combine two or more delegates
// - Walk in the delegate chain and invoke them one by one
void delegate_juggling()
{
  // Create two classes with different instance data
  MessageDisplay *m1 = __gc new MessageDisplay(S"Msg1");
  MessageDisplay *m2 = __gc new MessageDisplay(S"Msg2");
 
  // Create the first delegate bound to the method in instance m1
  MessageDisplayDelegate *del = new MessageDisplayDelegate(m1, 
                                &MessageDisplay::Display);
  // add another delegate "m2"
  del += new MessageDisplayDelegate(m2, &MessageDisplay::Display);
 
  // Invoke the delegate. Or equally invoke as: del->Invoke()
  del();
 
  // Now let us walk in the delegate list and invoke one by one
  Delegate *d[] = del->GetInvocationList();
 
  IEnumerator *e = d->GetEnumerator();
  int idx = 1;
  while (e->MoveNext())
  {
    MessageDisplayDelegate *delegateI = 
        dynamic_cast<MessageDisplayDelegate *>(e->Current);
    Console::Write("Delegate #{0} ->", idx.ToString());
    delegateI->Invoke();
    idx++;
  }
}
 
int main()
{
  delegate_member_test();
  
  delegate_static_test();
 
  delegate_advanced_test();
 
  delegate_juggling();
 
  return 0;

}

 

8.值类型,结构体和联合

值类型是典型的短生存周期的小对象,它们通常创建在stack上。为了创建值类型,你需要在声明前加__value来标识。

通常值类型被用作数据记录,像C中结构体一样。你不能在值类型类的构造函数中使用初始化列表。相反你需要在构造函数体内部初始化变量。数据在内存中被序列存储,但他们占用的字节数有.pack元数据决定(默认的packing是8)

你可以通过指定自定义伪属性[StructLayout]来改变默认行为。

这些属性可以是一下三种枚举值:

Auto:运行时确定顺序以及成员占用的内存

Sequential:内存占用起码和成员的大小一样,然而它们是顺序排列的

Explicit:我们自己制定成员的确切layout:字节位置和每个成员的大小。

托管C++没有联合,因此你可以通过使用Explicit布局来模拟联合。

#using <mscorlib.dll>
 
using namespace System;
using namespace System::Runtime::InteropServices;
 
// Value type example 1
__value class Point
{
public:
  int _x;
  int _y;
 
  // !!! !!! NOT ALLOWED !!! !!!
  //Point(int x, int y) : _x(x), _y(y) { }
 
  // !!! !!! Correct way of doing member initialization !!! !!!
  //  Point(int x, int y)
  //  {
  //    _x = x;
  //    _y = y;
  //  }
};
 
// Emulating Unions
[StructLayout(LayoutKind::Explicit)]
__value struct LargeInteger
{
  // occupy first 4 bytes of the 8 bytes
  [FieldOffset(0)] int lowPart;
  // occupy the 2nd 4 bytes of the 8 bytes,
  // thus forming the total 8 byte structure
  [FieldOffset(4)] int highPart;
  // occupy 8 bytes starting from field offset 0.
  // Its definition collides with the two previous definitions
  [FieldOffset(0)] __int64 quadPart;
};
 
int main()
{
  // create and initialize a value-type
  Point pt1 = {1, 2};
 
  // create a union
  LargeInteger li;
 
  // assign to the union
  li.quadPart = 0x22;
 
  // Display to the screen
  Console::WriteLine("{0:X}", li.quadPart.ToString());
  return 0;

}

 

9.托管数组

数组rank:标明数组维度。一个二维数组的rank是2

下边界(dim):返回给定维度的下边界

上边界(dim):返回给定维度的上边界

默认情况下,下边界是0,上边界是数组长度-1

#using <mscorlib.dll>
 
using namespace System;
 
// Shows the contents of a one-dimensional string array
void display_string_array1(String *ar[])
{
  for (int i=0;i<ar->Length;i++)
    Console::Write(S"{0} ", ar[i]);
  Console::WriteLine();
}
 
// Shows the contents of a two-dimensional string array
void display_string_array2(String *ar[,])
{
  for (int i=ar->GetLowerBound(0);i<=ar->GetUpperBound(0);i++)
  {
    for (int j=ar->GetLowerBound(1);j<=ar->GetUpperBound(1);j++)
      Console::WriteLine(S"arr[{0},{1}] = {2}", 
                    i.ToString(), j.ToString(), ar[i,j]);
  }
}
 
// Test function to show how to create an array of strings
// Be that single dimensional or multi dimensional
void test1()
{
  // create an array of 3 managed strings
  String *names[] = __gc new String*[3];
 
  // initialize the array
  names[0] = S"Hello";
  names[1] = S"World";
  names[2] = S"of Wonders!";
 
  display_string_array1(names);
 
  // Allocate a 3 rows / 2 cols array
  String *arr2[,] = new String *[3, 2];
 
  arr2[0,0] = S"First1";
  arr2[0,1] = S"Last1";
 
  arr2[1,0] = S"First2";
  arr2[1,1] = S"Last2";
 
  arr2[2,0] = S"First3";
  arr2[2,1] = S"Last3";
 
  display_string_array2(arr2);
}
 
// Shows how to use the Array::CreateInstance to create arrays
void test2()
{
  // Create a 1-d array with 3 elements
  Array *a = Array::CreateInstance(__typeof(String), 3);
  String *names[] = dynamic_cast<String *[]>(a);
 
  names[0] = S"Hey,";
  names[1] = S"are you";
  names[2] = S"fine?";
 
  display_string_array1(names);
 
  // Create a two dimensional array such as:
  // [0 to 1][0 to 3]
  int dim __gc[] = {2, 4};
  Array *b = Array::CreateInstance(__typeof(String), dim);
  String *vals __gc[,] = dynamic_cast<String *[,]>(b);
 
  // Display the rank (or count of dimensions)
  Console::WriteLine(S"Rank is: {0}", b->Rank.ToString());
 
  // Show the contents of that array
  for (int i=vals->GetLowerBound(0);i<=vals->
                                 GetUpperBound(0);i++)
  {
    for (int j=vals->GetLowerBound(1);
         j<=vals->GetUpperBound(1);j++)
    {
      vals[i,j] = String::Format("{0},{1}", __box(i), __box(j));
      Console::WriteLine(vals[i,j]);
    }
  }
}
 
int main()
{
  test1();
 
  test2();
 
  return 0;

}

 

10.平台调用(PInvoke)

PInvoke是平台调用的简称,它让托管代码可以调用c风格的原生dll

C++托管扩展的一个重要并且独特的性质是你可以直接使用非托管API。如果你不需要自定义数据序列化(data marshaling),你就不需要使用PInvoke。

托管字符串可以通过互操作作为in参数被传递

[DllImport("kernel32", CharSet=CharSet::Auto)]

unsigned GetFileAttributes(String *file)

Thunk(形实转换程序)会根据CharSet字段把这个字符串转换成非托管buffer

如果你使用包含了windows头文件并且使用lib文件,你可以不使用PInvoke轻松的调用外部程序。

在#using<mscorlib.dll>之前确认你包含了windows头文件以避免明明冲突。或者延迟“using namespace System”到你包含了windows头文件之后。

如果碰巧包含了windows头文件,你仍然面临命名冲突。例如,调用Forms::MessageBox::Show()将和Windows头文件中的#define MessageBox入口冲突

一个解决方案是:

#ifdef MessageBox

#undef MessageBox

#endif

下面的例子将阐明很多问题:

#include <windows.h>
#using <mscorlib.dll>
 
// Link with these DLLs
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
 
using namespace System;
using namespace System::Runtime::InteropServices;
 
namespace Win32
{
  // Shows how managed strings can be used
  // to access either unicode or ansi strings
  [DllImport("kernel32", CharSet=CharSet::Auto, 
               EntryPoint="GetFileAttributes")]
  unsigned GetFileAttributesCall(String *Path);
 
  // Shows how to import from user32
  [DllImport("user32")]
  unsigned MessageBeep(unsigned uType);
 
  // Another sample. When no entrypoint is specified,
  // the imported function will have same name
  // as the internal function name
  [DllImport("kernel32")]
  unsigned GetLogicalDrives();
 
  // Yet another simple import. Notice that we have
  // specified the EntryPoint because we internally
  // named the function a different name
  [DllImport("msvcrt", EntryPoint="rand")]
  unsigned my_rand();
 
  [DllImport("msvcrt")]
  unsigned srand(unsigned seed);
 
  // This function call returns a buffer.
  // We use Text::StringBuilder to hold the returned buffer
  [DllImport("kernel32", CharSet=CharSet::Auto, 
             EntryPoint="GetWindowsDirectory")]
  unsigned GetWindowsDirectoryCall(Text::StringBuilder *, unsigned);
 
  String *getwindir()
  {
    // Call it with no params so to get the required lengths
    unsigned len = GetWindowsDirectoryCall(0, 0);
 
    // Allocate the buffer
    Text::StringBuilder *sb = new Text::StringBuilder(len);
 
    /// Call the method
    GetWindowsDirectoryCall(sb, sb->Capacity);
 
    // Return the value to caller
    return sb->ToString();
  }
};
 
 
// This function demonstrates how to call APIs through PInvoke
void test_pinvoke()
{
  // Get the file attribute
  String *filename = S"C:\autoexec.bat";
  unsigned attr = Win32::GetFileAttributesCall(filename);
 
  // Display the file's attributes
  Console::WriteLine(S"\"{0}\"'s attributes: {1:X}\n", 
                           filename, attr.ToString());
 
  // Get windows' directory
  Console::WriteLine(S"Windows directory is located at: {0}\n", 
                                           Win32::getwindir());
 
  // Randomize
  Win32::srand((unsigned)Environment::TickCount);
 
  unsigned drives = Win32::GetLogicalDrives();
 
  for (unsigned i=0;i<26;i++)
  {
    if (((1 << i) & drives) == 0)
      continue;
    Console::WriteLine(S"Drive {0}:\ present", 
                  ((Char)('A'+i)).ToString());
  }
 
  Console::WriteLine("\nA call to rand() returned: {0}", 
                           Win32::my_rand().ToString());
}
 
// Here we demonstrate how to do direct calls!
void test_direct_calls()
{
  // Call a native function directly
  ::MessageBoxA(::GetDesktopWindow(), "Info", "Hello world", MB_OK);
 
  // Here we will demonstrate how to convert TCHAR's to managed strings
  DWORD len = ::GetCurrentDirectory(0, 0);
  TCHAR *str = new TCHAR[len];
  String *s = 0;
  if (::GetCurrentDirectory(len, str) != 0)
    s = new String(str);
  delete [] str;
  
  Console::WriteLine(S"Current directory: {0}\n", 
                          s != 0 ? s : S"error");
}
 
//
// Here we should how we can still dynamic
// load functions from external libraries
void test_dynamic_load_calls()
{
  // Define the messagebox's prototype
  typedef int (__stdcall *msgboxa_proc)(int, char *, char *, int);
 
  HMODULE h = ::LoadLibrary("user32");
  if (h == 0)
    return;
 
  // Get the procedure's address
  msgboxa_proc m = (msgboxa_proc) ::GetProcAddress(h, "MessageBoxA");
 
  // did we return a correct function pointer??
  if (m != NULL)
    m(0, "Hello world", "info", MB_OK);
 
  // Free the handle
  ::FreeLibrary(h);
}
 
int main()
{
  test_pinvoke();
 
  test_direct_calls();
 
  test_dynamic_load_calls();
 
  return 0;

}

11.多线程

MC++中你可以通过System::Threading命名空间和它的线程类创建线程。线程类的访问控制符为sealed,所以你不能从它继承类。多线程中访问同一个变量时注意同步问题。例如,你可以使用Interlocked::Increment(&your_integer)来以一种安全方式使变量增加。

正如你知道的,类的所有实例共享一个静态成员。为了让每个线程使用一个静态成员,你可以使用[ThreadStatic]属性。如果从其它线程访问一个静态成员它将有不同的值

#using <mscorlib.dll>
 
using namespace System;
using namespace System::Threading;
 
__gc class MyProgress
{
private:
  int  _start, _end, _speed;
 
public:
  MyProgress(int start, int end, int speed)
  {
    _start = start;
    _end = end;
    _speed = speed;
  }
 
  void Progress()
  {
    while (_start <= _end)                 
    {
      Console::Write("{0}/{1}     \r", 
        (_start++).ToString(), _end.ToString());
      Thread::Sleep(_speed);
    }
    Console::WriteLine("finished    ");
 
  }
};
 
void display_thread_info(Thread *t)
{
  Console::WriteLine("Thread name: {0}", t->Name);
  Console::WriteLine("Thread priority: {0}", 
                     __box(t->Priority));
}
 
int main()
{
  
  display_thread_info(Thread::CurrentThread);
 
  // Create the class
  MyProgress *pb = __gc new MyProgress(0, 20, 20);
 
  // Create a thread that will carry the pb.Progress method
  Thread *t = new Thread(new ThreadStart(pb, 
                         &MyProgress::Progress));
 
  t->Name = pb->ToString();
 
  display_thread_info(t);
 
  // Start the thread
  t->Start();
 
  // Wait till the thread is finished
  t->Join();
 
 
  Console::WriteLine("--press enter to terminate application-");
  Console::ReadLine();
 
  return 0;

}

 

12.使用Windows窗体

这个实例没有关于windows窗体的解释性文字。我期望你对.net框架有所了解,这样你就能理解下面用到的大多数组件是怎样工作的。

这个实例将为你展示怎样用MC++动态创建窗体和控件。

#using <mscorlib.dll>
#using <system.dll>
#using <system.drawing.dll>
#using <system.windows.forms.dll>
 
using namespace System;
using namespace System::Drawing;
using namespace System::Windows::Forms;
using namespace System::Runtime::InteropServices;
using namespace System::Reflection;
using namespace System::IO;
 
__gc class TestForm : public Form
{
protected:
 
  // Called everytime the system wants our form to repaint itself
  void OnPaint(PaintEventArgs *args)
  {
    Graphics *g = args->Graphics;
    DrawBackground(g);
  }
 
  // Draws a cross in the form's background
  void DrawBackground(Graphics *g)
  {
    g->DrawLine(Pens::Black, 0, 0, ClientSize.Width-1, 
                                    ClientSize.Height-1);
    g->DrawLine(Pens::Black, 0, ClientSize.Height-1, 
                                  ClientSize.Width-1, 0);
  }
 
  // This allows us to control the window procedure of the form
  // A way to access low-level form messaging
  void WndProc(Message *m)
  {
    /*
    if (m->Msg == WM_NCHITTEST)
    {
      m->Result = HTCAPTION;
      return;
    }
    */
    Form::WndProc(m);
  }
private:
 
  // Loads a bitmap from the system.windows.forms.dll resources
  void SetBackGround()
  {
    String *strName = 
      String::Concat(RuntimeEnvironment::GetRuntimeDirectory(), 
      S"\system.windows.forms.dll");
    Assembly *assem = Assembly::LoadFrom(strName);
    Stream *stm = 
       assem->GetManifestResourceStream(S"System.Windows" 
                                       S".Forms.Panel.bmp");
    Bitmap *bmp = new Bitmap(stm);
    BackgroundImage = bmp;
  }
 
  Button *_buttons __gc[];
 
  void InitButtons()
  {
    int cnt = 10;
 
    // create the button array
    _buttons = __gc new Button*[cnt];
 
    for (int i=0;i<cnt;i++)
    {
      // Create a new button object
      Button *b = new Button;
 
      // store that button for later access
      _buttons[i] = b;
 
      // Assign the dimensions
      b->Width = 40;
      b->Height = 40;
 
      // Make visible and set its caption
      b->Text = String::Format("B#{0}", (i+1).ToString());
      b->Visible = true;
 
      // Set the position
      b->Left = (i*40) + 30;
      b->Top = 15;
 
      // Associate the tag with a string object
      b->Tag = String::Format(S"I am button #{0}", (i+1).ToString());
 
      // Add this control to the form
      Controls->Add(b);
 
      // Add an event handler
      b->Click += new EventHandler(this, BtnClick);
    }
  }
 
  // Generic button click handler
  void BtnClick(Object *sender, EventArgs *args)
  {
    // sender as button
    Button *btn = dynamic_cast<Button *>(sender);
    // Show the text that is associated w/ that button
    MessageBox::Show(btn->Tag->ToString());
  }
 
public:
  // Constructor
  TestForm()
  {
    // Set the form's title
    Text = S"Hello world!";
 
    // Set height / width
    Width = 470;
    Height = 100;
 
    // Set the form's background bitmap
    SetBackGround();
 
    // Create dynamic buttons
    InitButtons();
  }
};
 
int main()
{
  Application::Run(new TestForm);
 
  return 0;

}

 

13。C/C++和.net框架之间的等效性

下面的表格向你展示C/C++/Win32 API语法/函数,以及他们等价的MC++和.net框架语法

C/C++/Win32 API Syntax

MC++ / .NET Framework

printf("The string is: %s", str);

System::Console::WriteLine(S"The string is: {0}", str); // where 'str' is a String * (or else you may went to box it or ToString())

sprintf()

refer to String::Format()

strcat / strncat

refer to String::Concat, StringBuilder::Append/AppendFormat

strchr

String::IndexOf

strlen

String::Length (property)

strupr / lwr

String::ToUpper/ToLower

isalpha, isdigit, isspace

Char::IsLetter, Char::IsDigit, Char::IsWhitespace

atoi, atol, atof, strtol, strtod

refer to the object's Parse() method. Example: Int32::Parse

itoa, itol, ...

refer to the object's ToString() method. Example: Int32 i; i.ToString();

gets()

Console::ReadLine()

findclose, findfirst, findnext

Directory::GetDirectories and Directory::GetFiles

getenv

Environment::GetEnvironmentVariables

_execl, _spawnl

Process:Start

asctime, ctime, _ftime

DateTime::Now

_argc, argv[]

Environment::GetCommandLineArgs

STL containers: list, map, queue, set,                  vector, stack, ...

Array, HashTable, ArrayList, Stack, ...

 

格式化说明符表

通常情况,为了格式化一个字符串,你需要先传递格式,然后是一个顺序参数列表

printf(“%d %d %d\n”, 1, 2, 3);

在.NET中格式化字符串你要像下面这样来制定格式/顺序:

FormatFunction("Hello {0} and {1} or {1} and {0}" " in no specific order", "John", "Marie");

输出将是这样:

Hello John and Marie or Marie and John in no specific order

正如你注意到的,它的一个优势是如果你需要再次显示一个参数,你不需要传递两次,你要做的仅仅是通过数字{n}引用它。通常的格式项语法是:{index[,alignment][:formatString]}

现在你可以通过下表中定义的格式说明符来格式化输出:

Specifier

Description

{n:C}

Currency format

{n:Dm}

Integer with m digits

{n:Em}

Scientific format; m is the precision

{n:Xm}

Hex format, m is number of digits

 

可以参照".NET Framework Developer's Guide/Standard Numeric Format Strings"获得关于格式化的更多信息。

结论

我希望你通过阅读这篇文章能有所收获并且享受这个过程。这对你短时间内的起步应该够用,其它的就靠你自己了。

我希望你们能回复很多问题和评论,然而记住一点,我对.net框架只有有限的一点新经验,所以这篇文章可能并不能回答你的所有问题。下面包含了大量的文章引用,你可以从它们当中获得帮助。玩的开心!

参考文章/链接:


上面对于托管C++的介绍过时了,主要是看到托管对象要用^帽子,不用*
下面这部分转载自http://it.100xuexi.com/view/otdetail/20120715/7a13f183-cbea-4450-a163-8066078cd592.html如有侵权联系删除

托管C++存在的语法问题

一、绪论
 
  当微软推出VS.NET7实现了可扩展的托管C++后,C++程序员们反映不一。尽管大部分的程序员对于能够继续使用C++感到很欣慰,但几乎所有的人对于托管C++提供的晦涩语法感到很痛苦。微软明显从反馈中感觉到托管C++不是那么成功。
 
  2003年10月6日,ECMA(欧洲计算机制造商协会)宣布成立专家组,负责结合ISO标准C++与通用语言,开发一个可扩展语言的标准,这个新的可扩展语言被称为C++/CLI标准。这个标准将被VS.NET2005的C++编译器支持。
 
  二、老语法存在的问题
 
  1、晦涩繁琐的语法和文法--这两个"双重底线"问题加重了阅读的负担。
 
  2、二流的CLI支持--相对与C#与VB.NET,MC++使用不方便的工作区来提供CLI支持,例如,它没有一个一一对应的结构来列举.NET的集合。
 
  3、C++与.NET粗陋地结合--对于CLI类型,你不能使用C++的特色,例如模板;同样,对于C++类型,你不能使用CLI的特色,例如碎片帐集。
 
  4、令人混淆的指针--非托管的C++的指针及托管的引用指针都使用*语法,这非常令人混淆,因为-gc指针与托管指针在本质和行为上完全不同。
 
  5、MFC编译器不能产生可校验的代码。
 
  三、C++/CLI给我们提供了什么?
 
  1、优雅流畅的语法和文法--C++/CLI为C++开发人员书写托管代码提供了一种非常自然的感觉,并且它提供了非托管代码到托管代码的平滑过度。以前所谓的"双重底线"问题现在已经荡然无存。
 
  2、一流的CLI支持--CLI特色,例如属性、碎片集合和属类得到了直接支持,此外,C++/CLI还准许将这些特色用于本地非托管的类。
 
  3、一流的C++类支持--C++特色,例如模板和析构函数对于拖管和非拖管类继续有效。实际上,C++/CLI是你可以"表面上"在栈或C++本地堆上声明一个.NET类型唯一的.NET语言。
 
  4、在.NET与C++之间的沟壑上架起了一座桥梁--C++开发人员在抨击BCL时不再象离开水的鱼。
 
  5、C++/CLI编译器产生的可执行文件完全是可校验的。
 
  四、"Hello World"小程序
 
  using namespace System;
 
  void _tmain()
 
  {
 
  Console::WriteLine("Hello World");
 
  }
 
  上述代码除了不需要引用mscorlib.dll库外,与老的语法没有太大的区别,因为无论你什么时候使用/clr进行编辑,编译器都可以暗中进行引用(现在默认的是/clr:newSyntax)。
 
  五、句柄
 
  与老的语法主要的混淆是我们习惯于使用*符号来声明拖管引用或非拖管指针,在C++/CLI里微软引入了句柄的概念。
 
  void _tmain()
 
  {
 
  //The ^ punctuator represents a handle
 
  String^ str = "Hello World";
 
  Console::WriteLine(str);
 
  }
 
  ^符号代表一个托管对象(声明时看上去象个帽子),按照CLI的规定,句柄代表一个拖管对象的引用。句柄在CLI中是新的语法,相当于C++中的-gc指针。句柄与指针不再混淆,在本质上两者完全不同。
 
  六、句柄与指针是怎样区分开来的?
 
  1、指针声明时使用*符号,而句柄使用^符号。
 
  2、句柄是针对拖管堆上对象的拖管引用,而指针仅仅指向内存中的一个地址。
 
  3、指针很稳定,GC循环不会影响到它;句柄在基于GC或内存紧张的情况下,可以指向不同的内存位置。
 
  4、对于指针,程序开发人员必须"显式"地删除,否则会面临泄露的危险,而对于句柄,是否进行显式删除则完全根据程序人员的爱好了。
 
  5、句柄一定要指向一个具体的类型,即所谓的类型安全性,而指针明显不是这样,你决不可以将一个句柄指向Void^类型。
 
  6、正如new操作符返回一个指针一样,gcnew返回一个句柄。
 
  七、CLR对象示例
 
  void _tmain()
 
  {
 
  String^ str = gcnew String("Hello World");
 
  Object^ o1 = gcnew Object();
 
  Console::WriteLine(str);
 
  }
 
  关键字gcnew用来实例化一个CLI对象,而且它返回一个指向在CLR堆上的对象的句柄,gcnew的优点在于它可以方便的让我们区分拖管和非拖管的实例对象。
 
  大部分情况下,gcnew关键字和^操作符提供了你用来进行BCL的一切手段,但是很明显你需要创建和声明属于自己的拖管类和接口。
 
  八、声明类型
 
  CLR类型有一个形容词前缀用来说明类型的种类,下面是C++/CLI中的类型声明示例:
 
  1、 CLR types
 
  o Reference types
 
  § ref class RefClass{...};
 
  § ref struct RefClass{...};
 
  2、 Value types
 
  § value class ValClass{...};
 
  § value struct ValClass{...};
 
  o Interfaces
 
  § interface class IType{...};
 
  § interface struct IType{...};
 
  o Enumerations
 
  § enum class Color{...};
 
  § enum struct Color{...};
 
  3、 Native types
 
  o class Native{...};
 
  o struct Native{...};
 
  示例:
 
  using namespace System;
 
  interface class IDog
 
  {
 
  void Bark();
 
  };
 
  ref class Dog : IDog
 
  {
 
  public:
 
  void Bark()
 
  {
 
  Console::WriteLine("Bow wow wow");
 
  }
 
  };
 
  void _tmain()
 
  {
 
  Dog^ d = gcnew Dog();
 
  d->Bark();
 
  }
 
  上述程序中的代码与老的C++语言相比看上去非常简洁,在以往的C++代码中,至少要用到-gc和-interface这两个关键词。
 
  九、装箱/拆箱操作
 
  在C++/CLI中,加箱是隐含的,而且类型是安全的,一个二进制的拷贝被执行并在CLR堆上形成一个对象,去箱是显式的,仅仅需要使用reinterpret_cast操作符来解除引用。
 
  void _tmain()
 
  {
 
  int z = 44;
 
  Object^ o = z; //implicit boxing
 
  int y = *reinterpret_cast<int^>(o); //unboxing
 
  Console::WriteLine("{0} {1} {2}",o,z,y);
 
  z = 66;
 
  Console::WriteLine("{0} {1} {2}",o,z,y);
 
  }
 
  // 输出结果如下:
 
  // 44 44 44
 
  // 44 66 44
 
  在上述代码中,"o"对象是一个加箱的拷贝,从第二个语句Console::WriteLine.的输出可以很明显地看到,它并没有涉及到int类型的整数值。
 
  当你对一种数值类型进行加箱操作时,返回的对象记住了最初的数值类型。
 
  void _tmain()
 
  {
 
  int z = 44;
 
  float f = 33.567;
 
  Object^ o1 = z;
 
  Object^ o2 = f;
 
  GetType());
 
  GetType());
 
  }
 
  // Output
 
  // System.Int32
 
  // System.Single
 
  因此不能对不同类型的对象进行去箱操作。
 
  void _tmain()
 
  {
 
  int z = 44;
 
  float f = 33.567;
 
  Object^ o1 = z;
 
  Object^ o2 = f;
 
  int y = *reinterpret_cast<int^>(o2);//System.InvalidCastException
 
  float g = *reinterpret_cast<float^>(o1);//System.InvalidCastException
 
  }
 
  如果你非尝试这么做,那么你将得到一个System.InvalidCastException。让我们来探讨一下完美的类型安全性,如果你要看内部代码,你将看到微软的内部箱在实际中的运用。例如:
 
  void Box2()
 
  {
 
  float y=45;
 
  Object^ o1 = y;
 
  }
 
  编译后的代码是:
 
  .maxstack 1
 
  .locals (float32 V_0, object V_1)
 
  ldnull
 
  stloc.1
 
  ldc.r4 45.
 
  stloc.0
 
  ldloc.0
 
  box [mscorlib]System.Single
 
  stloc.1
 
  ret
 
  根据微软的内部文档,箱操作将未加工的类型转换为一个具体类型的实例,这项工作的完成通过创建一个新的对象并将数据拷贝到这个新分配的对象。
 
  十、写在后面的话
 
  为什么很多人已经可以使用C、C++、.NET来开发程序但还在积极学习C++/CLI呢,我想有四个方面的原因:
 
  1、从编译器直到内层都还在支持C++代码;
 
  2、C++/CLI对于其他标准来说无意是具有毁灭性地;
 
  3、与生俱来的内部支持胜过所有其他CLI语言
 
  4、所有在MFC中出现的下划线都已不再存在。


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值