如何在C#中模拟C++的联合(Union)?[C#, C++]
How To Simulate C++ Union In C#?
Updated on Sunday, December 26, 2004
Written by Allen Lee
0 如何阅读本文?
如果你...
- ...希望了解联合的概念,请阅读“什么是联合?”。
- ...希望了解联合的内存使用情况,请阅读“联合的内存布局与内存使用情况。”。
- ...希望了解如何在C#中模拟联合,请阅读“第一次尝试:在C#中模拟这种布局方式。”。
- ...希望了解在C++中使用联合有哪些要注意的地方,请阅读“在实际的C++代码中,我们是如何使用联合的?”。
- ...希望了解如何在C#中更好的使用模拟的联合,请阅读“第二次尝试:改进型的联合模拟。”。
- ...希望了解在C#中使用模拟的联合有些什么注意事项,请阅读“别在模拟的联合中同时使用值类型和引用类型!”。
- ...希望了解为何我要写这篇文章,请阅读“为什么要在C#里面模拟这个用处不大的东西?”。
否则...
1 什么是联合?
联合(Union)是一种特殊的类,一个联合中的数据成员在内存中的存储是互相重叠的。每个数据成员都在相同的内存地址开始。分配给联合的存储区数量是“要包含它最大的数据成员”所需的内存数。同一时刻只有一个成员可以被赋给一个值。
下面我们来看看C++中如何表达联合:
2 联合的内存布局与内存使用情况。
下面我们来考察一下TokenValue的内存布局。
首先,我们使用sizeof运算符来获取该联合各个成员的内存占用字节数:
//Code#02
int_tmain(intargc,_TCHAR*argv[])
{
cout"sizeof(char):"sizeof(char)endl;
cout"sizeof(int):"sizeof(int)endl;
cout"sizeof(double):"sizeof(double)endl;
return0;
}
/**//*
*Output:
*sizeof(char):1
*sizeof(int):4
*sizeof(double):8
*
*/
这样,分配给该联合的内存就是8个字节。
接着,我们来看看具体使用该联合时,所分配的内存的字节占用情况如何:
//Code#03
int_tmain(intargc,_TCHAR*argv[])
{
TokenValuetv;
//[_][_][_][_][_][_][_][_]
tv._cval='K';
//[X][_][_][_][_][_][_][_]
tv._ival=1412;
//[X][X][X][X][_][_][_][_]
tv._dval=3.14159;
//[X][X][X][X][X][X][X][X]
return0;
}
3 第一次尝试:在C#中模拟这种布局方式。
在C#中,要指定成员的内存布局情况,我们需要结合使用StructLayoutAttribute特性、LayoutKind枚举和FieldOffsetAttribute特性,它们都位于System.Runtime.InteropServices命名空间中。
下面我用struct来试着模拟上面的TokenValue联合:
//Code#04
[StructLayout(LayoutKind.Explicit,Size=8)]
structTokenValue
{
[FieldOffset(0)]
publicchar_cval;
[FieldOffset(0)]
publicint_ival;
[FieldOffset(0)]
publicdouble_dval;
}
我们知道,联合的每个数据成员都在相同的内存地址开始,通过把[FieldOffset(0)]应用到TokenValue的每一个成员,我们就指定了这些成员都处于同一起始位置。当然,我们得事先告诉.NET这些成员的内存布局由我们来作主,把LayoutKind.Explicit枚举传递给StructLayoutAttribute特性的构造函数,并应用到TokenValue,.NET就不会再干涉该struct的成员在内存中的布局了。另外,我显式的把TokenValue的大小设置为8字节,当然,这样做是可选的。
4 在实际的C++代码中,我们是如何使用联合的?
在实际的C++代码中,我们应尽量避免让客户端直接使用联合,Code #03就是一个很好的反面例子了。为什么呢?熟悉C/C++的开发人员都知道,联合提供我们这样一个节省空间的储存方式,是要我们付出一定的代价的。这个代价就是代码的安全性,不恰当地使用联合可能会导致程序崩溃的。
由于每一次只有一个联合成员处于激活状态,如果我们不小心或者因为其它原因使用处于休眠状态的成员,轻则得到错误的结果,重则整个程序中止。请看下面的代码:
//Code#05
unionTokenValue
{
char_cval;
int_ival;
double_dval;
char*_sval;
};
int_tmain(intargc,_TCHAR*argv[])
{
TokenValuetv;
tv._cval='K';
couttv._cvalendl;//Line#01
couttv._ivalendl;//Line#02
couttv._dvalendl;//Line#03
couttv._svalendl;//Line#04
return0;
}
这里的TokenValue比起Code #01的仅仅多了一个_sval,它是C风格的字符串,实质上,它是指向字符串的第一个字符的指针,它占用4字节的内存空间。
当程序运行到Line #04时,就会出现Unhandled Exception,程序中止,并指出_sval的值非法(即所谓的“野指针”)。程序无法把它的值输出控制台,然而,Line #01 ~ Line #03都能输出,只是Line #02和Line #03所输出的值是错误的而已。
实际的应用中,我们一般不会看到如此低级且显而易见的错误,但复杂的实际应用中,不恰当地使用联合的确会为我们带来不少的麻烦。
5 第二次尝试:改进型的联合模拟。
一般情况下,联合作为一种内部数据的储存手段,没有必要让客户端对其有所了解,更没必要让客户端直接使用它。为了使我们的联合模拟用起来更安全,我们需要对它进行一番包装:
//Code#06
classProgram
{
staticvoidMain(string[]args)
{
Tokent=newToken();
Console.WriteLine(t);
Console.WriteLine(t.GetTokenValue());
t.SetTokenValue('K');
Console.WriteLine(t);
Console.WriteLine(t.GetTokenValue());
}
}
publicstructToken
{
privateTokenValuetv;
privateTokenKindtk;
publicvoidSetTokenValue(charc)
{
tk=TokenKind.CharValue;
tv._cval=c;
}
publicvoidSetTokenValue(inti)
{
tk=TokenKind.IntValue;
tv._ival=i;
}
publicvoidSetTokenValue(doubled)
{
tk=TokenKind.DoubleValue;
tv._dval=d;
}
publicobjectGetTokenValue()
{
switch(tk)
{
caseTokenKind.CharValue:
returntv._cval;
caseTokenKind.IntValue:
returntv._ival;
caseTokenKind.DoubleValue:
returntv._dval;
default:
return"NoValue";
}
}
publicoverridestringToString()
{
switch(tk)
{
caseTokenKind.CharValue:
returntv._cval.ToString();
caseTokenKind.IntValue:
returntv._ival.ToString();
caseTokenKind.DoubleValue:
returntv._dval.ToString();
default:
return"NoValue";
}
}
[StructLayout(LayoutKind.Explicit,Size=8)]
privatestructTokenValue
{
[FieldOffset(0)]publicchar_cval;
[FieldOffset(0)]publicint_ival;
[FieldOffset(0)]publicdouble_dval;
}
privateenumTokenKind
{
NoValue,
CharValue,
IntValue,
DoubleValue
}
}
/**//*
*Output:
*NoValue
*NoValue
*K
*K
*
*/
由于Token是值类型,实例化时,对应的成员(tv和tk)会自动被赋予与之对应的零值。此时,tv._cval为'\0'、tv._ival和tv._dval均为0(实质上它们是同一个值在不同的类型中的表现)。而tk也被自动赋予0:
tk = 0;
这里,你无需进行强类型转换,0是任何枚举的默认初始值,.NET会负责把0转换成对应的枚举类型。例如,你可以:
//Code#07
System.DayOfWeekd=0;
Console.WriteLine(d);
该代码能正确输出Sunday——一个星期的第一天(西方习惯),也是该枚举的第一个成员。
一般情况下,0对应着枚举的第一个成员(除非你在定义枚举的时候,把第一个成员指定为别的值,并为别的成员赋予0值)。这样,我们就不难看出代码的输出是合理的,而且代码本身也是安全的。
6 别在模拟的联合中同时使用值类型和引用类型!
到目前为止,我们所模拟的联合中,所有的成员都是值类型,如果我们为它加入一个引用类型,例如String呢?
//Code#08
[StructLayout(LayoutKind.Explicit,Size=8)]
structTokenValue
{
[FieldOffset(0)]
publicchar_cval;
[FieldOffset(0)]
publicint_ival;
[FieldOffset(0)]
publicdouble_dval;
[FieldOffset(0)]
publicstring_sval;
}
这样,Code #06的代码运行时就会提示出错:
Could not load type 'TokenValue' from assembly 'UnionLab, Version=1.0.1820.28531, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.
TokenValue初始化的时候,_cval、_ival和_dval都能正确的被赋予对应的零值,而这些零值也能被统一起来(别的值就不行了)。但_sval不同,它是引用类型,如果没有显示初始化为某个有意义的值,它将被赋予null值!这个null值跟之前的有意义的零值是不能被统一起来的!所以,要么你就去掉这个_sval,要么就重新定义它的起始位置(当然,你也得去掉Size=8!),但这样一来,TokenValue就不再称得上联合的模拟了。
在C++中,我们可以直接使用指针来解决这个问题,如Code #05,但C#中,问题就会变得有点辣手。如果你有兴趣的话,可以使用不安全代码(Unsafe code)来试着解决,但这样一来,你的代码又会引入一些新的问题。
7 为什么要在C#里面模拟这个用处不大的东西?[NEW]
相信很多人都有这样一个疑问:为什么要在C#里面模拟这个用处不大的东西?就我个人来说,我始终坚信事物的存在必定有它的理由,否则就不会存在。其实,联合在我们平时的编码中的确很少用到,但在某些情况下,我们必须使用它!.NET为我们提供巨大的便利的同时,也不忘让我们能够与非托管代码交互。你知道,早期的Win32 API使用C来完成的,这里面就有很多函数的参数是以联合的形式表达的,要在C#中跟这些API交互,我们就得“尊重”原函数的用法约束。
8 终点与起点的交界处。
回顾整个探索旅程,我们为了使用联合节省空间的优势,开始了这个模拟的探索,然而,为了弥补联合的不足,我们对这个模拟进行了一番包装,增加了不少额外的代码,直到后来,又发现了在这个模拟中同时使用值类型的成员和引用类型的成员所引发的问题,我们一直都没有停止过探索和思考。正如马斯洛的需要层次理论所描述的,人只要低层次的需要被满足,马上就会转向更高的需要层次,一级一级的,直到攀上最高峰为止。
关于在C#中模拟C++的联合这个话题,我并没有在本文中给予你一个完整的展示,相反,我为你展示的仅仅是一个探索的起点,希望为你带来一丝灵感,让你根据自己的实际情况来定制你的探索旅程。Have a good trip!
参考资料:
分享到:
相关推荐
2.结构体含union共同体的C++转C#示例 3.C#结构体指针的调用演示 4.C#读取USB数据设备的示例 5.C#动态调用C++dll库示例 6.C#静态调用C++dll库示例 注意:UsbLibDotNet使用时候需要先安装驱动程序才可以正常使用。
在写C#TCP通信程序时,发送数据时,只能发送byte数组,处理起来比较麻烦不说,如果是和c++等写的程序通信的话,很多的都是传送结构体,在VC6.0中可以很方便的把一个char[]数组转换为一个结构体,而在C#却不能直接把...
在这种情况下,有很多开发人员就面临了如何在C#中使用C++开发好的类的问题。下面就用一个完整的实例来详细说明怎样用托管C++封装一个C++类以提供给C#使用。 创建一个工程三个C#项目,托管C++项目和非托管C++项目。
C#可以通过P/Invoke调用C/C++写的DLL,一般在从DLL接收字符串时比较麻烦,本人在某个项目中遇到这个问题, 从DLL收读取字符串时遇到中文乱码,这里总结一下C#收取字符串时的处理。 C/C++字符串一般通过char* ...
c# 和 c++ 通用加密解密 。有c#和c++的例子代码。亲测可用 c# 和 c++ 通用加密解密 。有c#和c++的例子代码。亲测可用 c# 和 c++ 通用加密解密 。有c#和c++的例子代码。亲测可用 c# 和 c++ 通用加密解密 。有c#和c++...
便捷的C++转C#工具,用于将完整的C++项目转换成C#项目
在用C#调用C++的API,需要类型转换了,这个文档可以很好解决这个问题,需要的同学,可以下载下来看下
C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.C#调用C++.
C# To C++ Converter是一款将C#代码或者项目转换为C++的工具 转换所有版本的C#代码 许多转换和格式化选项 选项可用于C ++ 11输出 代表和事件从C#到C ++的优秀转换 操作符重载从C#到C ++的精确转换 评估所有引用...
一个Demo,描述了C#如何在C++中注册函数并回调
C#转C++ 代码转换器 , 去掉了文件个数限制, 代码行数限制
c++ to c#,把c++代码转换成c#类型的 c++ to c#,把c++代码转换成c#类型的
附件是c#调用c++托管代码,最后调用c++ DLL代码。
c#调用c++DLL,DLL里是二维数组 ,c#里如何调用二维数组
C++DLL中包含一个类,C#端要调用这个类的函数,重新封装这个类,来供C#端调用
C++ C#混合编程
C#引用C++动态库 dll 实例 项目含有C++编写DLL C#引用C++dll
C#调用C++ Api或接口转换工具,自动将C++ Api转换成C#中使用接口 开发C#调用C++库或者api的好助手。
但是,我要用在C++项目中,怎么让C++调用C#的DLL呢。今天一上午都在琢磨这个问题,耽误了很多时间,原因是编译是出现:warning C4819: 该文件包含不能在当前代码页(936)中表示的字符。请将该文件保存为 Unicode ...
资源代码演示的是c#代码调用c++ DLL 的方式。该演示为原创,绝非搬砖。解决了c# 调用 C++ Dll获取相关信息之如何传递结构体数组引用以及如何处理获取到的结构体数组数据的问题。