分享
 
 
 

实例解析C++/CLI之值类型

王朝c/c++·作者佚名  2008-06-01
窄屏简体版  字體: |||超大  

值类型是一种轻量级的C++/CLI类机制,非常适合于小型的数据结构,且从语义的角度来看,与数值(Value)类似。

与之相比,引用类型的实例--包括那些声明在堆栈上的,是由垃圾回收器治理的,而值类型的实例却不是。一般来说,一个值类较好的实现应只有一些数据成员,而不需要继续性,这样,在函数传递及返回值、或是赋值操作时,不会带来巨大的数据开销。

值类初印像

请看例1中的Point类,可以通过替换ref为value,来把一个引用类变为值类;与引用类(ref)相似,值类(value)也是一个包含了空格的要害字。与大家想像的一样,值类(value)与值结构(value strUCt)之间唯一的区别就是,前者默认的可访问性为private,而后者则为public。

例1:

using namespace System;

public value class Point

{

int x;

int y;

public:

//定义属性X与 Y的读写实例

property int X

{

int get() { return x; }

void set(int val) { x = val; }

}

property int Y

{

int get() { return y; }

void set(int val) { y = val; }

}

//定义实例构造函数

Point(int xor, int yor)

{

X = xor;

Y = yor;

}

void Move(int xor, int yor)

{

X = xor;

Y = yor;

}

virtual bool Equals(Object^ obj) override

{

if (obj == nullptr)

{

return false;

}

if (GetType() == obj->GetType())

{

Point^ p = static_cast<Point^>(obj);

return (X == p->X) && (Y == p->Y);

}

return false;

}

static bool operator==(Point p1, Point p2)

{

return (p1.X == p2.X) && (p1.Y == p2.Y);

}

// static bool operator==(Point% p1, Point% p2)

// {

// return (p1.X == p2.X) && (p1.Y == p2.Y);

// }

// static bool operator==(Point& p1, Point& p2)

// {

// return (p1.X == p2.X) && (p1.Y == p2.Y);

// }

virtual int GetHashCode() override

{

return X ^ (Y << 1);

}

virtual String^ ToString() override

{

return String::Concat("(", X, ",", Y, ")");

}

};

值类自动继续自System::ValueType,而System::ValueType则继续自System::Object,但是,这却不能显式地声明。值类隐式表明了为"sealed",也就是说,它不能被作为一个基类,另外,为其类成员指定一个protected是没有任何意义,并且也是不答应的。假如想显式声明一个值类(或引用类),可像如下所示:

value class X sealed {/*...*/};

请注重,此处没有默认的构造函数。对一个值类来说,CLI本身把类实例中所有字段的位都设置为零,所以,不能提供自己的默认构造函数;然而,零、false、nullptr对其他类型来说,也许并不是合适的默认值,因此,对某些特定类型来说,就要用引用类型来取代值类型了。(遵从C++/CLI的实现会将false与nullptr表示为位全部为零。)

值类的另一个限制是它们带有一个默认的拷贝构造函数和一个赋值操作符,两者都会进行逐位复制,并不可被重载。

假如要实现Point类中的Equals函数,相比引用类中的而言要简单一些。请记住,我们正在重载定义System::Object中的这个版本,而其接受一个Object^,因为这种类型的参数很可能有一个nullptr值,在此,先可以省去检查是否为自身比较这一步,而对引用类的Equals实现来说,这一步是必需的,因为可有多个句柄引用同一对象。但是话说回来,在目前的这个值类中,没有两个值的实例可表示同一个实例,两个相同的值实例,只代表两个Point有相同的坐标,但修改其中一者的x坐标,不会影响到另一者的相同值。

当一个Point的实例传递到Equals时,作为值类型(其最终也都继续自System::Object)而言,装箱就发生了--也就是说,在垃圾回收堆上分配了一个Object的实例,而其包含了传递进来Point的一份副本。因为是创建了一个新的对象,所以只有一个句柄,也不会有相同的其他Point。

之前接受Point句柄的 == 操作符函数,现在已经精简到一行,并且由接受句柄改为接受Point值,且用于选择成员的指向操作符 -> 也被替换为点操作符。因为给定的值类型为sealed,所以与值类型参数Point唯一匹配的则为同类型的值了。同样地,既无需检查nullptr来确认是否为自身比较,也无需检查传递进来的对象是否类型完全一致。

而之前用于追踪引用的 == 操作符函数基本上无需太多改动,但删除了检测同一类型这一部分。然而,这两个== 操作符函数,最好只保留一个,以免在point1 == point2调用时引发歧义。(在声明函数参数时,也可使用标准C++引用符&,而不是%,因为两者可在本地类型与值类型之间互换。但由于这种类型的实例不存在于垃圾回收堆中,所以在垃圾回收期间不会改变它们的位置,因此也不需要对它们的位置进行追踪。)

例2使用了值类中的大多数成员,最主要的是它包含了静态Point类的实例,而这在引用类中是不可能完成的。事实上,不只是不能有一个引用类的静态实例,甚至也不能有一个此类型的静态句柄。

例2:

using namespace System;

Point p1;

static Point p2(3,4);

int main()

{

static Point p3(4,7);

Console::WriteLine("p2 is {0}", p2);

Point% p4 = p3;

Point p5 = p2;

p5 = p2;

Console::WriteLine("p1 == p2 is {0}", p1 == p2);

Console::WriteLine("p1.Equals(p2) is {0}", p1.Equals(p2));

}

p2 is (3,4)

p1 == p2 is False

p1.Equals(p2) is False

在第一次调用Console::WriteLine时,用传值的方式传递进一个Point,但是,这个函数却指望着接受一个对象引用,在此,Point值被自动装箱,并把装箱后的对象引用传递给函数。

在定义中可看到,p5是由默认的拷贝构造函数初始化,而接下来的一行代码,默认的赋值操作符把p2逐位复制给p5。

引用类与值类的差异

假如我们在上述的Point引用类中加入一个ID号,用于跟踪每个不同的Point引用对象,且再添加一个布尔类型的TraceID用于指明是否进行跟踪;那么,把它改为值类之后,会有什么不同呢?

再次提醒,是不能为一个值类定义默认构造函数、拷贝构造函数及赋值操作符的,但不幸的是,这些都是我们ID解决方案中所需用到的。在引用类版本的默认构造函数中,会将X与Y两个坐标值、ID值都设置为零,并取得下一个ID赋给ID实例字段;反观值类实现的版本,对以此方式构建的每个新Point,都是由默认为零值的ID构成,但是,我们却想每个ID值为唯一。

另一个类似问题也是由缺少显式的拷贝构造函数造成的,在我们想要一个全新的对象时,值类的逐位复制却造成新对象的ID与被拷贝对象的ID一样。

另外,在赋值时,假如我们只设置即有Point的值,那么Point的ID不应改变,也就是说,虽然任一或两个坐标都可能改变,但它仍是同一Point对象,然而,逐位复制却导致目标Point的ID被源对象ID覆盖。

虽然此处没有列出包含ID的Point类,但例3中的程序显示了引用类与值类的差异所在。

例3:

using namespace System;

int main()

{

Point::TraceID = true;

Point p1, p2(3,7), p3(9,1), p4 = p2;

Console::WriteLine("p1 = {0}", p1);

Console::WriteLine("p2 = {0}", p2);

Console::WriteLine("p3 = {0}", p3);

Console::WriteLine("p4 = {0}", p4);

p2 = p1;

Console::WriteLine("p2 = {0}", p2);

}

第一次运行后,4个Point的输出如下:

Point p1, p2(3,7), p3(9,1), p4 = p2;

p1 = [0](0,0)

p2 = [0](3,7)

p3 = [1](9,1)

p4 = [0](3,7)

Point p1由默认构造函数创建,它的ID为零,但却恰好也是第一个Point的正确ID值,默认的坐标值也为零。而p2用到了自己编写的构造函数,其分配了一个可用的ID,也就是零,这样,我们有了两个一样的ID。

同样地,p3得到了ID值1,接下来,把p2逐位复制给p4,p4的ID与p2相同。在执行p2 = p1逐位复制之后,p1与p2两个对象都有了相同的p1的ID。

程序第二次运行后,输出如下:

p1 = [0](0,0)

p2 = [2](3,7)

p3 = [3](9,1)

p4 = [2](3,7)

p2 = [0](0,0)

在此可看到,p1的ID值总为零。

显而易见,引用类与值类是各有千秋,不是在每种场合,都可以调换使用的。

基本类型映射

遵照标准C++的精神,对CLI值类型的基本类型映射,都已经全部在定义中实现了,就Microsoft Visual C++而言,映射关系如表1所示。

C++/CLI类型

CLI值类型

bool

System::Boolean

wchar_t

System::Char

signed char

System::SByte

unsigned char

System::Byte

char

System::SByte或 System::Byte

short int

System::Int16

unsigned short int

System::UInt16

int

System::Int32

unsigned int

System::UInt32

long long int

System::Int64

unsigned long long int

System::UInt64

float

System::Single

double

System::Double表1:C++/CLI与CLI值类的映射关系

另外,还有一种值类型:System::Decimal,但没有对应的C++/CLI类型。

请看以下表达式,它们都涉及到访问前述CLI值类型的静态或实例成员。

Int32::MaxValue

Double::Parse("123.45e-1")

10.2f.ToString()

(10 + 5.9).ToString()

(100).ToString()

100 .ToString()

因应Visual C++的映射,10.2f的类型为float,其映射为System::Single,并调用了其ToString函数;类似地,(10 + 5.9)类型为double,因此调用了System::Double的ToString。显然,从语义的角度来看,带有圆括号的100与其后带有一个空格的100,这种写法是多余的,但是,假如忽略它们,100与其后的句点将会解析为一个带有标识符的double常量,这会导致语法错误。

复数问题

例4,演示了一个有着实部与虚部的复数的值类型。

例4:

using namespace System;

public value class Complex

{

double real;

double imag;

public:

static initonly Complex i;

static Complex()

{

i = Complex(0.0, 1.0);

}

Complex(double real)

{

this->real = real;

this->imag = 0.0;

}

Complex(double real, double imag)

{

this->real = real;

this->imag = imag;

}

property double Real

{

double get() { return real; }

void set(double value) { real = value; }

}

property double Imag

{

double get() { return imag; }

void set(double value) { imag = value; }

}

static Complex operator+(Complex z1, Complex z2)

{

return Complex(z1.real + z2.real, z1.imag + z2.imag);

}

static Complex operator-(Complex z1, Complex z2)

{

return Complex(z1.real - z2.real, z1.imag - z2.imag);

}

String^ ToString() override

{

if (imag < 0.0)

{

return String::Format("({0} - {1}i)", real, -imag);

}

else if (1.0/imag == Double::NegativeInfinity)

{

return String::Format("({0} - 0.0i)", real);

}

else

{

return String::Format("({0} + {1}i)", real, +imag);

}

}

};

CLI要求使用IEEE浮点表示法,这是一种比IEC 10559更正式的表示法,其中,零在single与double中表示为全部位为零。正因为此,所以可安全地使用CLI提供的默认构造值。

程序中,定义了一个复数i,其表示-1的平方根,这样,复数类型就可以提供具有此值的public只读常量,而这是由一个public static成员及一个static构造函数共同完成的。因为Complex在此不是一个基本类型,所以i不能成为一个只读(readonly)成员,因为无论如何,这都需要用一个常量表达式来初始化它,但这种事是不存在的。所以,我们能做的,就是让i成为initonly,并在static构造函数中初始化它。例5是测试程序及输出。

例5:

using namespace System;

int main()

{

Complex c1;

Complex c2(12.5);

Complex c3(-1.23, -4.5);

Complex c4 = c2 + c3;

Complex c5 = c2 - c3;

Console::WriteLine("c1: {0}", c1);

Console::WriteLine("c2: {0}", c2);

Console::WriteLine("c3: {0}", c3);

Console::WriteLine("c4: {0}", c4);

Console::WriteLine("c5: {0}", c5);

Console::WriteLine("i: {0}", Complex::i);

Console::WriteLine("c3.Real: {0}", c3.Real);

Console::WriteLine("c3.Imag: {0}", c3.Imag);

}

c1: (0 + 0i)

c2: (12.5 + 0i)

c3: (-1.23 - 4.5i)

c4: (11.27 - 4.5i)

c5: (13.73 + 4.5i)

i: (0 + 1i)

c3.Real: -1.23

c3.Imag: -4.5

一些其他事项

注重,一个值类型不应包含:

·类型为本地C++数组、本地类类型或位字段的数据成员

·包含局部类的成员函数

·为friend的成员

·析构函数

一个传值、传址、传引用、或追踪引用的函数,可传递进或返回一个值类。

在引用类T的实例构造函数或成员函数中,this的类型为"指向T的句柄",然而,对值类型而言,this为interior_ptr<T>。

像Point与Complex这样的简单值类型实例是完全自我包含的--但却不是必须的,举例来说,与引用类型相似,一个值类型也能包含指向本地堆的指针及垃圾回收堆中对象的句柄。在这种情况下,清理释放值类型自身所占用的内存可不是一件简单的事情,因为每种类型的数据成员在超出作用域时,都需要进行清理。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有