如何:定义和使用类和结构 (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 C++ 2005 之前,在程序集外本机类型具有公共可访问性。 编译器警告(等级 1)C4692 帮助你发现错误使用私有本机类型的位置。 在不能修改的源代码文件中使用 make_public 编译指示为本机类型提供公共访问。
有关详细信息,请参阅#using 指令 (C++)。
下面的示例演示如何声明类型并指定它们的可访问性,然后访问程序集中的某些类型。 当然,如果通过使用#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();
}
Output
现在,我们重写之前的示例,使其编译为 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;
}
Output
成员可见性
相同的程序集内您可以访问公有类成员,这不同于从外部程序集使用成对的访问说明符 public、protected 和 private访问它。
下表汇总了各种访问说明符的效果:
说明符 |
效果 |
---|---|
|
成员可访问内部和外部程序集。有关更多信息,请参见public (C++)。 |
|
成员不可访问程序集的内部和外部。有关更多信息,请参见private (C++)。 |
|
成员可访问程序集的内部和外部,但是,只针对派生类型。有关更多信息,请参见protected (C++)。 |
|
成员在程序集内是公有的,在程序集外是私有的。 internal是上下文相关关键字。有关详细信息,请参阅区分上下文的关键字。 |
|
成员在程序集内是公有的,但在程序集外是被保护的。 |
|
成员在程序集内是被保护的,但在程序集外是私有的。 |
下面的示例显示公有类型具有用不同可访问性声明的成员,然后显示在程序集内访问这些成员。
// type_member_visibility.cpp
// 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();
}
Output
现在我们生成前面的示例作为 DLL。
// type_member_visibility_2.cpp
// 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("=======================");
}
};
下面的示例使用上一示例中创建的组件从而显示如何在程序集访问成员。
// type_member_visibility_3.cpp
// 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();
}
Output
公共和私有本机类
本机类型可以从托管类型引用。例如,在托管类型的函数会采用类型是本机结构的参数。如果托管类型和函数在程序集中是公有的,则本机类型也必须是公有的。
// mcppv2_ref_class3.h
// native type
public struct N {
N(){}
int i;
};
接下来,创建一个使用本机类型的源代码文件:
// mcppv2_ref_class3.cpp
// 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) {}
};
现在,编译客户端:
// mcppv2_ref_class4.cpp
// 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)。
// mcppv2_ref_class6.cpp
// 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();
}
Output
此指针的语义
当使用 Visual C++ 定义类型时,引用类型中的this 指针为“图柄”类型。 在值类型中的 this 指针为“内部指针”类型。
当默认索引器调用时,这些 this 指针的语义不同可能导致意外行为。 下一个示例显示在 ref 类型和值类型中访问一个默认索引器的正确方法。
有关更多信息,请参见
// semantics_of_this_pointer.cpp
// 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();
}
Output
由签名功能隐藏
在标准 C++ 中,在基类中的函数由具有相同名称在派生类中隐藏功能,即使派生类函数没有相同数量或参数。 这称为按名称隐藏语义。 在引用类型中,如果该名称和参数列表相同,基类中的函数只能由派生类里的函数隐藏。 这称为 按签名隐藏 语义。
当其所有函数在元数据标记为 hidebysig时,一个类被视为按签名隐藏的类。 默认情况下,在**/clr** 下创建的所有类具有 hidebysig 功能。 但是,编译使用 /clr:oldSyntax 的类没有 hidebysig功能;相反,它们是按名称隐藏功能。 当类具有 hidebysig 功能时,编译器将不在直接基类中按名称隐藏功能,但是,如果编译器在继承链中遇到按名称隐藏的类,将继续按名称隐藏。
在按签名隐藏语义下,当函数调用对象时,编译器确定包含一个功能可以满足函数调用的派生类。 在类中如果只有一个可以满足调用的函数,编译器调用该函数。 类中如果存在多个可以满足调用的函数,编译器使用重载解决方案确定要调用的函数。 有关重载方案的更多信息,请参见函数重载。
对于特定函数调用,基类中的函数可能具有比在派生类函数中略微匹配的签名。 但是,如果函数显式调用该派生类的对象,派生类中的函数被调用。
由于返回值不被视为函数签名的一部分,如果作为派生类函数它具有相同名称、数量和参数种类,基类函数被隐藏,即使返回值类型不一样。
下面的示例显示,在基类中的函数不受派生类中的函数隐藏。
// hide_by_signature_1.cpp
// 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();
}
Output
下面的示例演示,Visual C++ 编译器会在派生类中调用函数(即使转换需要匹配一个或多个参数),它不会调用基类中更适合函数调用的函数。
// hide_by_signature_2.cpp
// 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);
}
Output
下面的示例演示,隐藏函数是可能的,即使该基类具有相同派生类签名。
// hide_by_signature_3.cpp
// 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);
}
Output
下面的示例定义一个使用**/clr:oldSyntax**编译的组件。 使用 Managed Extensions for C++ 定义的类具有按名称隐藏的成员函数。
// hide_by_signature_4.cpp
// compile with: /clr:oldSyntax /LD
using namespace System;
public __gc struct Base0 {
void Test() {
Console::WriteLine("in Base0::Test");
}
};
public __gc struct Base1 : public Base0 {
void Test(int i) {
Console::WriteLine("in Base1::Test");
}
};
下一个示例使用上一示例中生成的组件。 通知按签名隐藏功能如何不会应用于通过使用**/clr:oldSyntax**编译的基类类型。
// hide_by_signature_5.cpp
// compile with: /clr:oldSyntax /LD
// compile with: /clr
using namespace System;
#using "hide_by_signature_4.dll"
ref struct Derived : public Base1 {
void Test(int i, int j) {
Console::WriteLine("Derived::Test");
}
};
int main() {
Derived ^ t = gcnew Derived;
t->Test(8, 8); // OK
t->Test(8); // OK
t->Test(); // C2661
}
复制构造函数
C++ 标准表示当移动对象时调用复制构造函数,此类对象在同一地址中创建和销毁。
但是,当 /clr 用于编译时,函数编译 MSIL 调用一个本地类(或不止一个)通过值传递的本机函数,本地类有复制构造函数和(或)析构函数,复制构造函数不被调用并且对象在不同的地址被销毁在而不是在创建的位置。 如果类有指向自己的指针或者代码由地址跟踪对象,会引起错误。
有关详细信息,请参阅/clr(公共语言运行时编译)。
下面的示例演示复制构造函数何时未生成。
// breaking_change_no_copy_ctor.cpp
// 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);
}
Output
析构函数和终结器
引用类型中的析构函数执行资源的确定性清除。 终结器清理非托管资源,并可被析构函数以确定性方式调用或由垃圾回收器以不确定性方式调用。 有关标准 C++ 中的析构函数,请参阅 析构函数 (C++)。
class classname {
~classname() {} // destructor
! classname() {} // finalizer
};
在托管 Visual C++ 类中析构函数行为不同于在C++ 托管扩展中。 有关此更改的更多信息,请参见析构函数语义的更改。
当不再需要时,CLR 垃圾回收器删除未使用的托管对象并释放其内存。 但是,一种类型可以使用垃圾回收器不知道如何释放的资源。 这些资源称为非托管资源 (如,本机文件句柄)。 建议您释放终结器中所有的非托管资源。 由于垃圾回收器以非确定性的方式释放托管资源,在终结器中指定托管资源是不安全的,因为垃圾回收器可能已清理了该托管资源。
Visual C++ 终结器与 Finalize方法不一样。(CLR 文档使用终结器和 Finalize 方法) Finalize方法由垃圾回收器调用,在类继承链中调用每个终结器。 不同于 Visual C++ 析构函数,派生类的终结器调用不会导致编译器调用基类的所有终结器。
由于 Visual 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();
}
}
托管类型也可以使用您希望的以确定性释放的托管资源,不再需要对象后,不要让垃圾回收器在某些点以非确定性的方式释放。 资源的确定性释放可以显着改善性能。
Visual C++ 编译器使析构函数以确定性方式清理对象的定义可行。 使用析构函数释放所有你想要以确定性方式释放的资源。如果终结器存在,为避免代码副本,请从析构函数调用它。
// destructors_finalizers_1.cpp
// 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 语法确保异常不会妨碍析构函数运行。
// clr_destructors.cpp
// compile with: /clr
ref struct A {
~A() {}
};
int main() {
A ^ MyA = gcnew A;
try {
// use MyA
}
finally {
delete MyA;
}
}
如果您的类型具有析构函数,编译器会生成一个 Dispose 方法实现 IDisposable。 如果用 Visual C++ 编写的并具有其他语言使用的析构函数类型,对该类型的 IDisposable::Dispose 调用的原因是类型析构函数被调用。 当类型从 Visual C++ 客户端中使用,不能直接调用Dispose;相反,使用 delete 运算符调用析构函数。
如果您的类型具有析构函数,编译器会生成一个 Finalize(void) 方法实现 Finalize。
如果类型具有终结器或一个析构函数,编译器根据设计模式生成一个 Dispose(bool)方法。(有关信息,请参见Implementing Finalize and Dispose to Clean Up Unmanaged Resources。) 无法在Visual C++中显示创作或调用Dispose(bool)。
如果类型具有符合设计模式的基类,当派生类的析构函数被调用时,所有基类的析构函数被调用。(如果您的类型是在 Visual C++ 编写的,编译器可确保您的类型实现此模式。)换言之,C++ 标准为其基类和成员指定引用类链的析构函数, 首先是该类的析构函数运行,然后是其构建的相反顺序的成员析构函数,最后是其构建的相反顺序的基类析构函数。
析构函数和终结器未提供内部值类型或接口。
在引用类型中终结器只能定义或声明。 像构造函数和析构函数一样,终结器没有返回类型。
在对象上运行终结器之后,任何基类中的终结器都能被调用,从最小派生类型开始。 数据成员的终结器不会自动绑定到类的终结器。
如果终结器在托管类型中删除本机指针,必须确保通过或指向本机指针的引用不会过早地收集;调用在托管类型的析构函数而不是使用KeepAlive。
在编译时,可以检测类型是否具有终结器或析构函数。 有关详细信息,请参阅编译器使用类型特征支持。
下面的示例演示两个类型,一个具有非托管资源,一个具有以确定性的方式释放的托管资源。
// destructors_finalizers_2.cpp
// 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");
}