如何:定义和使用类和结构 (C++/CLI)
本文介绍如何在 C++/CLI 中定义和使用用户定义引用类型和值类型。
对象实例化
引用 (ref) 类型只能在托管堆上进行实例化,不能在堆栈上或本机堆上进行实例化。 可以在堆栈或托管堆上实例化值类型。
// mcppv2_ref_class2.cpp
// compile with: /clr
ref class MyClass {
public:
int i;
// nested class
ref class MyClass2 {
public:
int i;
};
// nested interface
interface struct MyInterface {
void f();
};
};
ref class MyClass2 : public MyClass::MyInterface {
public:
virtual void f() {
System::Console::WriteLine("test");
}
};
public value struct MyStruct {
void f() {
System::Console::WriteLine("test");
}
};
int main() {
// instantiate ref type on garbage-collected heap
MyClass ^ p_MyClass = gcnew MyClass;
p_MyClass -> i = 4;
// instantiate value type on garbage-collected heap
MyStruct ^ p_MyStruct = gcnew MyStruct;
p_MyStruct -> f();
// instantiate value type on the stack
MyStruct p_MyStruct2;
p_MyStruct2.f();
// instantiate nested ref type on garbage-collected heap
MyClass::MyClass2 ^ p_MyClass2 = gcnew MyClass::MyClass2;
p_MyClass2 -> i = 5;
}
隐式抽象类
隐式抽象类不能进行实例化。 当一个类满足以下条件时,它就是隐式抽象类:
- 类的基类型是接口,并且
- 类不实现所有的接口成员函数。
你可能无法从派生自接口的类构造对象。 原因可能是该类是隐式抽象类。 有关抽象类的详细信息,请参阅抽象。
下面的代码示例演示了无法进行实例化的 MyClass
类,因为未实现函数 MyClass::func2
。 若要使示例能够编译,请取消注释 MyClass::func2
。
// mcppv2_ref_class5.cpp
// compile with: /clr
interface struct MyInterface {
void func1();
void func2();
};
ref class MyClass : public MyInterface {
public:
void func1(){}
// void func2(){}
};
int main() {
MyClass ^ h_MyClass = gcnew MyClass; // C2259
// To resolve, uncomment MyClass::func2.
}
类型可见性
可以控制公共语言运行时 (CLR) 类型的可见性。 当程序集被引用时,你需要控制程序集中的类型是否在程序集外部可见。
public
指示类型对于包含程序集(其中包含该类型)的 #using
指令的任何源文件可见。 private
指示类型对于包含程序集(其中包含该类型)的 #using
指令的源文件不可见。 不过,专用类型在同一程序集中是可见的。 默认情况下,类的可见性为 private
。
默认情况下,在 Visual Studio 2005 之前,本机类型在程序集外部具有公共辅助功能。 启用编译器警告(级别 1)C4692 有助于查看错误地使用了专用本机类型的位置。 使用 make_public pragma 为无法修改的源代码文件中的本机类型提供公共辅助功能。
有关详细信息,请参阅 #using 指令。
下面的示例演示了如何声明类型并指定其辅助功能,然后访问程序集中的这些类型。 如果使用 #using
引用具有专用类型的程序集,则只有程序集中的公共类型是可见的。
// type_visibility.cpp
// compile with: /clr
using namespace System;
// public type, visible inside and outside assembly
public ref struct Public_Class {
void Test(){Console::WriteLine("in Public_Class");}
};
// private type, visible inside but not outside assembly
private ref struct Private_Class {
void Test(){Console::WriteLine("in Private_Class");}
};
// default accessibility is private
ref class Private_Class_2 {
public:
void Test(){Console::WriteLine("in Private_Class_2");}
};
int main() {
Public_Class ^ a = gcnew Public_Class;
a->Test();
Private_Class ^ b = gcnew Private_Class;
b->Test();
Private_Class_2 ^ c = gcnew Private_Class_2;
c->Test();
}
输出
in Public_Class
in Private_Class
in Private_Class_2
现在来重写上一个示例,使其生成为一个 DLL。
// type_visibility_2.cpp
// compile with: /clr /LD
using namespace System;
// public type, visible inside and outside the assembly
public ref struct Public_Class {
void Test(){Console::WriteLine("in Public_Class");}
};
// private type, visible inside but not outside the assembly
private ref struct Private_Class {
void Test(){Console::WriteLine("in Private_Class");}
};
// by default, accessibility is private
ref class Private_Class_2 {
public:
void Test(){Console::WriteLine("in Private_Class_2");}
};
下一个示例演示了如何访问程序集外部的类型。 在此示例中,客户端使用在上一个示例中生成的组件。
// type_visibility_3.cpp
// compile with: /clr
#using "type_visibility_2.dll"
int main() {
Public_Class ^ a = gcnew Public_Class;
a->Test();
// private types not accessible outside the assembly
// Private_Class ^ b = gcnew Private_Class;
// Private_Class_2 ^ c = gcnew Private_Class_2;
}
输出
in Public_Class
成员可见性
通过使用成对的访问说明符 public
、protected
和 private
,可以使在同一个程序集内对一个公共类的成员的访问与在程序集外对它的访问不同
下表汇总了各种访问说明符的效果:
说明符 | 效果 |
---|---|
public |
可以在程序集内部和外部访问成员。 有关详细信息,请参阅 public 。 |
private |
在程序集内部和外部都无法访问成员。 有关详细信息,请参阅 private 。 |
protected |
可以在程序集内部和外部访问成员,但只能访问派生类型。 有关详细信息,请参阅 protected 。 |
internal |
成员在程序集内部是公共的,但在程序集外部是专用的。 internal 是上下文相关的关键字。 有关详细信息,请参阅上下文相关关键字。 |
public protected - 或 - protected public |
成员在程序集内部是公共的,但在程序集外部是受保护的。 |
private protected - 或 - protected private |
成员在程序集内部是受保护的,但在程序集外部是专用的。 |
以下示例展示了一个公共类型,该类型包含使用不同的访问说明符声明的成员。 然后,它显示了从程序集内部对这些成员的访问。
// compile with: /clr
using namespace System;
// public type, visible inside and outside the assembly
public ref class Public_Class {
public:
void Public_Function(){System::Console::WriteLine("in Public_Function");}
private:
void Private_Function(){System::Console::WriteLine("in Private_Function");}
protected:
void Protected_Function(){System::Console::WriteLine("in Protected_Function");}
internal:
void Internal_Function(){System::Console::WriteLine("in Internal_Function");}
protected public:
void Protected_Public_Function(){System::Console::WriteLine("in Protected_Public_Function");}
public protected:
void Public_Protected_Function(){System::Console::WriteLine("in Public_Protected_Function");}
private protected:
void Private_Protected_Function(){System::Console::WriteLine("in Private_Protected_Function");}
protected private:
void Protected_Private_Function(){System::Console::WriteLine("in Protected_Private_Function");}
};
// a derived type, calls protected functions
ref struct MyClass : public Public_Class {
void Test() {
Console::WriteLine("=======================");
Console::WriteLine("in function of derived class");
Protected_Function();
Protected_Private_Function();
Private_Protected_Function();
Console::WriteLine("exiting function of derived class");
Console::WriteLine("=======================");
}
};
int main() {
Public_Class ^ a = gcnew Public_Class;
MyClass ^ b = gcnew MyClass;
a->Public_Function();
a->Protected_Public_Function();
a->Public_Protected_Function();
// accessible inside but not outside the assembly
a->Internal_Function();
// call protected functions
b->Test();
// not accessible inside or outside the assembly
// a->Private_Function();
}
输出
in Public_Function
in Protected_Public_Function
in Public_Protected_Function
in Internal_Function
=======================
in function of derived class
in Protected_Function
in Protected_Private_Function
in Private_Protected_Function
exiting function of derived class
=======================
现在来生成上一个示例作为 DLL。
// compile with: /clr /LD
using namespace System;
// public type, visible inside and outside the assembly
public ref class Public_Class {
public:
void Public_Function(){System::Console::WriteLine("in Public_Function");}
private:
void Private_Function(){System::Console::WriteLine("in Private_Function");}
protected:
void Protected_Function(){System::Console::WriteLine("in Protected_Function");}
internal:
void Internal_Function(){System::Console::WriteLine("in Internal_Function");}
protected public:
void Protected_Public_Function(){System::Console::WriteLine("in Protected_Public_Function");}
public protected:
void Public_Protected_Function(){System::Console::WriteLine("in Public_Protected_Function");}
private protected:
void Private_Protected_Function(){System::Console::WriteLine("in Private_Protected_Function");}
protected private:
void Protected_Private_Function(){System::Console::WriteLine("in Protected_Private_Function");}
};
// a derived type, calls protected functions
ref struct MyClass : public Public_Class {
void Test() {
Console::WriteLine("=======================");
Console::WriteLine("in function of derived class");
Protected_Function();
Protected_Private_Function();
Private_Protected_Function();
Console::WriteLine("exiting function of derived class");
Console::WriteLine("=======================");
}
};
以下示例使用了在上一个示例中创建的组件。 它展示了如何从程序集外部访问成员。
// compile with: /clr
#using "type_member_visibility_2.dll"
using namespace System;
// a derived type, calls protected functions
ref struct MyClass : public Public_Class {
void Test() {
Console::WriteLine("=======================");
Console::WriteLine("in function of derived class");
Protected_Function();
Protected_Public_Function();
Public_Protected_Function();
Console::WriteLine("exiting function of derived class");
Console::WriteLine("=======================");
}
};
int main() {
Public_Class ^ a = gcnew Public_Class;
MyClass ^ b = gcnew MyClass;
a->Public_Function();
// call protected functions
b->Test();
// can't be called outside the assembly
// a->Private_Function();
// a->Internal_Function();
// a->Protected_Private_Function();
// a->Private_Protected_Function();
}
输出
in Public_Function
=======================
in function of derived class
in Protected_Function
in Protected_Public_Function
in Public_Protected_Function
exiting function of derived class
=======================
公共和专用本机类
可以从托管类型引用本机类型。 例如,托管类型的函数可以采用其类型为本机结构的参数。 如果托管类型和函数在程序集中是公共的,本机类型也必须是公共的。
// native type
public struct N {
N(){}
int i;
};
接下来,创建使用本机类型的源代码文件:
// compile with: /clr /LD
#include "mcppv2_ref_class3.h"
// public managed type
public ref struct R {
// public function that takes a native type
void f(N nn) {}
};
现在,编译客户端:
// compile with: /clr
#using "mcppv2_ref_class3.dll"
#include "mcppv2_ref_class3.h"
int main() {
R ^r = gcnew R;
N n;
r->f(n);
}
静态构造函数
CLR 类型(例如类或结构)可以具有可用于初始化静态数据成员的静态构造函数。 静态构造函数最多被调用一次,并且是在首次访问类型的任何静态成员之前被调用。
实例构造函数始终在静态构造函数之后运行。
如果类具有静态构造函数,编译器就无法内联对构造函数的调用。 如果类是值类型,具有静态构造函数且没有实例构造函数,则编译器无法内联对任何成员函数的调用。 CLR 可以内联调用,但编译器不可以。
将静态构造函数定义为专用成员函数,因为它只能由 CLR 调用。
有关静态构造函数的详细信息,请参阅如何:定义接口静态构造函数 (C++/CLI)。
// compile with: /clr
using namespace System;
ref class MyClass {
private:
static int i = 0;
static MyClass() {
Console::WriteLine("in static constructor");
i = 9;
}
public:
static void Test() {
i++;
Console::WriteLine(i);
}
};
int main() {
MyClass::Test();
MyClass::Test();
}
输出
in static constructor
10
11
this
指针的语义
当你使用 C++\CLI 定义类型时,引用类型中的 this
指针的类型是“句柄”。 值类型的 this
指针的类型是“内部指针”。
调用默认索引器时,this
指针的这些不同语义可能会导致意外行为。 下一个示例展示了在 ref 类型和值类型中访问默认索引器的正确方法。
有关详细信息,请参阅对象句柄运算符 (^) 和 interior_ptr (C++/CLI)
// compile with: /clr
using namespace System;
ref struct A {
property Double default[Double] {
Double get(Double data) {
return data*data;
}
}
A() {
// accessing default indexer
Console::WriteLine("{0}", this[3.3]);
}
};
value struct B {
property Double default[Double] {
Double get(Double data) {
return data*data;
}
}
void Test() {
// accessing default indexer
Console::WriteLine("{0}", this->default[3.3]);
}
};
int main() {
A ^ mya = gcnew A();
B ^ myb = gcnew B();
myb->Test();
}
输出
10.89
10.89
按签名隐藏函数
在标准 C++ 中,基类中的函数被派生类中具有相同名称的函数所隐藏,即使派生类函数没有相同类型或数量的参数也是如此。 这称为“按名称隐藏”语义。 在引用类型中,如果名称和参数列表相同,则基类中的函数仅被派生类中的函数所隐藏。 这称为“按签名隐藏”语义。
当一个类的所有函数在元数据中标记为 hidebysig
时,该类被视为按签名隐藏类。 默认情况下,在 /clr
下创建的所有类都有 hidebysig
函数。 当类具有 hidebysig
函数时,编译器不会在任何直接基类中按名称隐藏函数,但如果编译器在继承链中遇到按名称隐藏类,它将继续按名称隐藏行为。
在按签名隐藏语义下,当对对象调用函数时,编译器会标识包含可满足函数调用的函数的派生层次最高的类。 如果类中只有一个满足该调用的函数,编译器将调用该函数。 如果类中有多个可以满足该调用的函数,编译器将使用重载解析规则来确定要调用的函数。 有关重载规则的详细信息,请参阅函数重载。
对于给定的函数调用,基类中的函数可能有一个签名,使它比派生类中的函数更匹配。 但是,如果在派生类的对象上显式调用该函数,则会调用派生类中的函数。
由于返回值不被视为函数签名的一部分,因此如果基类函数具有相同的名称,并且采用与派生类函数相同类型或数量的自变量,即使它与返回值的类型不同,也会被隐藏。
以下示例展示了基类中的函数不会被派生类中的函数所隐藏。
// compile with: /clr
using namespace System;
ref struct Base {
void Test() {
Console::WriteLine("Base::Test");
}
};
ref struct Derived : public Base {
void Test(int i) {
Console::WriteLine("Derived::Test");
}
};
int main() {
Derived ^ t = gcnew Derived;
// Test() in the base class will not be hidden
t->Test();
}
输出
Base::Test
下一个示例显示,Microsoft C++ 编译器调用了派生层次最高的类中的函数(即使需要转换以匹配一个或多个参数),而不是调用更匹配函数调用的基类中的函数。
// compile with: /clr
using namespace System;
ref struct Base {
void Test2(Single d) {
Console::WriteLine("Base::Test2");
}
};
ref struct Derived : public Base {
void Test2(Double f) {
Console::WriteLine("Derived::Test2");
}
};
int main() {
Derived ^ t = gcnew Derived;
// Base::Test2 is a better match, but the compiler
// calls a function in the derived class if possible
t->Test2(3.14f);
}
输出
Derived::Test2
下面的示例显示,即使基类具有与派生类相同的签名,也可以隐藏函数。
// compile with: /clr
using namespace System;
ref struct Base {
int Test4() {
Console::WriteLine("Base::Test4");
return 9;
}
};
ref struct Derived : public Base {
char Test4() {
Console::WriteLine("Derived::Test4");
return 'a';
}
};
int main() {
Derived ^ t = gcnew Derived;
// Base::Test4 is hidden
int i = t->Test4();
Console::WriteLine(i);
}
输出
Derived::Test4
97
复制构造函数
根据 C++ 标准,移动对象时要调用复制构造函数,这样对象就在同一个地址被创建和销毁。
但是,当编译为 MSIL 的函数调用本机函数时,如果其中一个(或多个)本机类通过值传递,并且本机类具有复制构造函数或析构函数,则不会调用任何复制构造函数,对象被销毁的地址与创建的地址不同。 如果类有指向自身的指针,或者代码按地址跟踪对象,则此行为可能会导致问题。
有关详细信息,请参阅 /clr(公共语言运行时编译)。
下面的示例演示了没有生成复制构造函数时的情况。
// compile with: /clr
#include<stdio.h>
struct S {
int i;
static int n;
S() : i(n++) {
printf_s("S object %d being constructed, this=%p\n", i, this);
}
S(S const& rhs) : i(n++) {
printf_s("S object %d being copy constructed from S object "
"%d, this=%p\n", i, rhs.i, this);
}
~S() {
printf_s("S object %d being destroyed, this=%p\n", i, this);
}
};
int S::n = 0;
#pragma managed(push,off)
void f(S s1, S s2) {
printf_s("in function f\n");
}
#pragma managed(pop)
int main() {
S s;
S t;
f(s,t);
}
输出
S object 0 being constructed, this=0018F378
S object 1 being constructed, this=0018F37C
S object 2 being copy constructed from S object 1, this=0018F380
S object 3 being copy constructed from S object 0, this=0018F384
S object 4 being copy constructed from S object 2, this=0018F2E4
S object 2 being destroyed, this=0018F380
S object 5 being copy constructed from S object 3, this=0018F2E0
S object 3 being destroyed, this=0018F384
in function f
S object 5 being destroyed, this=0018F2E0
S object 4 being destroyed, this=0018F2E4
S object 1 being destroyed, this=0018F37C
S object 0 being destroyed, this=0018F378
析构函数和终结器
引用类型的析构函数会对资源执行确定性清理。 终结器清理非托管资源,可以由析构函数确定性地调用,也可以由垃圾回收器非确定性地调用。 有关标准 C++ 中的析构函数的信息,请参阅析构函数。
class classname {
~classname() {} // destructor
! classname() {} // finalizer
};
CLR 垃圾回收器删除未使用的托管对象,并在不再需要托管对象时释放其内存。 但是,类型可能会使用垃圾回收器不知道如何释放的资源。 这些资源称为非托管资源(例如,本机文件句柄)。 建议在终结器中释放所有非托管资源。 垃圾回收器非确定性地释放受管理资源,因此在终结器中引用托管资源是不安全的。 这是因为垃圾回收器可能已经将其清理干净。
Visual C++ 终结器与 Finalize 方法不同。 (CLR 文档使用终结器与 Finalize 方法是同义的)。 Finalize 方法由垃圾回收器调用,它调用类继承链中的每个终结器。 与 Visual C++ 析构函数不同,派生类终结器调用不会导致编译器在所有基类中调用终结器。
由于 Microsoft C++ 编译器支持确定性的资源释放,因此请勿尝试实现 Dispose 或 Finalize 方法。 但是,如果你熟悉这些方法,下面展示了 Visual C++ 终结器以及调用终结器的析构函数如何映射到 Dispose 模式:
// Visual C++ code
ref class T {
~T() { this->!T(); } // destructor calls finalizer
!T() {} // finalizer
};
// equivalent to the Dispose pattern
void Dispose(bool disposing) {
if (disposing) {
~T();
} else {
!T();
}
}
托管类型还可能使用你更愿意确定性地释放的受管理资源。 你可能不希望垃圾回收器在对象不再需要后的某个时刻非确定性地释放该对象。 资源的确定性释放可以显著提升性能。
Microsoft C++ 编译器使析构函数的定义能够确定性地清理对象。 使用析构函数释放你想要确定性地释放的所有资源。 如果存在终结器,请从析构函数调用它,以避免代码重复。
// compile with: /clr /c
ref struct A {
// destructor cleans up all resources
~A() {
// clean up code to release managed resource
// ...
// to avoid code duplication,
// call finalizer to release unmanaged resources
this->!A();
}
// finalizer cleans up unmanaged resources
// destructor or garbage collector will
// clean up managed resources
!A() {
// clean up code to release unmanaged resources
// ...
}
};
如果使用类型的代码不调用析构函数,垃圾回收器最终会释放所有受管理资源。
存在析构函数并不意味着存在终结器。 但是,终结器的存在意味着必须定义析构函数,并从该析构函数调用终结器。 此调用实现了非托管资源的确定性释放。
调用析构函数可以通过使用 SuppressFinalize 抑制对象的终结。 如果没有调用析构函数,则最终将由垃圾回收器调用类型的终结器。
调用析构函数以确定性地清理对象的资源,而不是让 CLR 非确定性地终结该对象,这样可以提升性能。
用 Visual C++ 编写并使用 /clr
编译的代码在以下情况下运行类型的析构函数:
使用堆栈语义创建的对象超出范围。 有关详细信息,请参阅引用类型的 C++ 堆栈语义。
在对象的构造期间引发异常。
该对象是其析构函数正在运行的某个对象中的成员。
在句柄上调用 delete 运算符(对象句柄运算符 (^))。
你显式调用了析构函数。
如果用另一种语言编写的客户端使用了你的类型,就会对析构函数进行调用,如下所示:
在对 Dispose 调用时。
在对类型上的
Dispose(void)
调用时。如果类型超出 C#
using
语句中的范围。
如果不对引用类型使用堆栈语义并在托管堆上创建引用类型的对象,请使用 try-finally 语法来确保异常不会阻止析构函数运行。
// compile with: /clr
ref struct A {
~A() {}
};
int main() {
A ^ MyA = gcnew A;
try {
// use MyA
}
finally {
delete MyA;
}
}
如果类型具有析构函数,编译器将生成可实现 IDisposable 的 Dispose
方法。 如果使用 Visual C++ 编写的类型具有从其他语言使用的析构函数,则在该类型上调用 IDisposable::Dispose
会导致调用该类型的析构函数。 当从 Visual C++ 客户端使用类型时,不能直接调用 Dispose
;而是使用 delete
运算符调用析构函数。
如果类型具有终结器,编译器将生成用来替代 Finalize 的方法 Finalize(void)
。
如果类型具有终结器或析构函数,编译器会根据设计模式生成 Dispose(bool)
方法。 (有关详细信息,请参阅释放模式)。 不能在 Visual C++ 中显式创作或调用 Dispose(bool)
。
如果类型具有符合设计模式的基类,则在调用派生类的析构函数时会调用所有基类的析构函数。 (如果你的类型是用 Visual C++ 编写的,编译器可确保你的类型实现此模式。)换句话说,引用类的析构函数会按照 C++ 标准的规定链接到它的基类和成员。 首先,运行类的析构函数。 然后,其成员的析构函数按与构造顺序相反的顺序运行。 最后,其基类的析构函数按与构造顺序相反的顺序运行。
值类型或接口中不允许使用析构函数和终结器。
终结器只能在引用类型中进行定义或声明。 与构造函数和析构函数一样,终结器没有返回类型。
对象终结器运行后,还会调用任何基类中的终结器,从最小派生类型开始。 数据成员的终结器不会被类的终结器自动链接到。
如果终结器删除了托管类型的本机指针,你必须确保对本机指针的引用或通过本机指针的引用不会被过早地收集。 在托管类型上调用析构函数,而不是使用 KeepAlive。
在编译时,可以检测类型是否具有终结器或析构函数。 有关详细信息,请参阅编译器对类型特征的支持。
下一个示例展示了两种类型:一种具有非托管资源,另一种具有受管理资源,可以被确定性地释放。
// compile with: /clr
#include <vcclr.h>
#include <stdio.h>
using namespace System;
using namespace System::IO;
ref class SystemFileWriter {
FileStream ^ file;
array<Byte> ^ arr;
int bufLen;
public:
SystemFileWriter(String ^ name) : file(File::Open(name, FileMode::Append)),
arr(gcnew array<Byte>(1024)) {}
void Flush() {
file->Write(arr, 0, bufLen);
bufLen = 0;
}
~SystemFileWriter() {
Flush();
delete file;
}
};
ref class CRTFileWriter {
FILE * file;
array<Byte> ^ arr;
int bufLen;
static FILE * getFile(String ^ n) {
pin_ptr<const wchar_t> name = PtrToStringChars(n);
FILE * ret = 0;
_wfopen_s(&ret, name, L"ab");
return ret;
}
public:
CRTFileWriter(String ^ name) : file(getFile(name)), arr(gcnew array<Byte>(1024) ) {}
void Flush() {
pin_ptr<Byte> buf = &arr[0];
fwrite(buf, 1, bufLen, file);
bufLen = 0;
}
~CRTFileWriter() {
this->!CRTFileWriter();
}
!CRTFileWriter() {
Flush();
fclose(file);
}
};
int main() {
SystemFileWriter w("systest.txt");
CRTFileWriter ^ w2 = gcnew CRTFileWriter("crttest.txt");
}