在 C 和 C# 编程语言中,结构体(Struct)是值类型数据结构,它使得一个单一变量可以存储多种类型的相关数据。在 C 语言中还有一种和结构体非常类似的语法,叫共用体(Union),有时也被直译为联合或者联合体。而在 C# 中并没有共用体这样一个定义,本文将介绍如何使用 C# 实现 C 语言中的共用体。

理解 C 语言的共用体

在 C 语言中,共用体是一种特殊的数据类型,允许你使用相同的一段内存空间存储不同的成员数据。光看定义有点抽象,我们来看一个 C 语言的共用体示例:

#include <stdio.h>

union data{
int n;
char ch;
short m;
}; int main(){
union data a;
printf("%d, %d\n", sizeof(a), sizeof(union data) );
a.n = 0x40;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.ch = '9';
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.m = 0x2059;
printf("%X, %c, %hX\n", a.n, a.ch, a.m);
a.n = 0x3E25AD54;
printf("%X, %c, %hX\n", a.n, a.ch, a.m); return 0;
}

运行结果:

4, 4
40, @, 40
39, 9, 39
2059, Y, 2059
3E25AD54, T, AD54

要想理解上面的输出结果,就得了解共用体各个成员在内存中的分布。此示例中的 data 各个成员在内存中的分布示意图如下:

也就是说共用体的所有成员占用的是同一段内存,所占内存等于最长的成员占用的内存,修改一个成员会影响其它所有成员。而结构体的各个成员占用的是各自不同的内存,所占内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),成员相互之间没有影响。这是共用体和结构的主要区别。

使用 C# 实现共用体

和 C 语言不同的是,C# 中没有共用体的定义。那在 C# 中如何来实现这种定义呢?

C# 不仅可以实现共用体,而且可以实现比 C 语言更强大的共用体。C 语言的共用体每个成员在共用的内存中都必须从相同的起始位置开始存储,而在 C# 中可以指定各成员的起始位置(相对偏移)。好处是,不仅可以节省内存空间,还可以实现一些自动转换操作。

以 IP 地址的存储为例,IP 地址是以 4 段数字来表示的(如 192.168.1.10),每一段是一个字节(Byte),长度是 2^8,最大值是 255。我们可以用很多类型来表示 IP 地址,比如字符串、整型、自定义类和结构等。但如果我们有时要访问或修改其中一段,怎样存储最为方便呢?

我们可以使用 C# 的显示布局结构体来实现类似 C 语言中的共用体,以方便灵活地操作 IP 地址的每一段。实现方式如下:

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
public struct IpAddress
{
// FieldOffset 表示偏移的位置(以字节为单位)
// sizeof(int) = 4, sizeof(byte) = 1
[FieldOffset(0)] public int Address;
[FieldOffset(0)] public byte Byte1;
[FieldOffset(1)] public byte Byte2;
[FieldOffset(2)] public byte Byte3;
[FieldOffset(3)] public byte Byte4; public IpAddress(int address) : this()
{
// 给 Address 赋值时,所有成员的值都会自动被修改
Address = address;
} public override string ToString() => $"{Byte1}.{Byte2}.{Byte3}.{Byte4}";
}

这里我们使用了 StructLayout 特性标注了 IpAddress,声明其内存分布是显示(Explicit)的,然后使用 FieldOffset 特性来标注成员在共用内存中相对起始位置的偏移量(以字节为单位)。

如此我们就用 C# 实现了和 C 语言一样的共用体。可能你不能马上体会这样实现的妙处,让来我们来看一个应用场景。

假设我要在 IP 段内随机生成一个 IP,比如前两段不变,后两段随机,形如:192.163.X.X。使用上面定义好的“共用体”,我们可以这样做:

var ip = new IpAddress(new Random().Next());
Console.WriteLine($"{ip} = {ip.Address}");
ip.Byte1 = 192;
ip.Byte2 = 168;
Console.WriteLine($"{ip} = {ip.Address}");

输出结果:

47.29.249.122 = 2063146287
192.168.249.122 = 2063182016

这样不仅节省内存,而且可以很灵活方便地读取和修改 IP 中的某一段。由于成员 Address 和其它成员共用内存,所以修改一个成员,其余就自动修改。

共用体作为另一个共用体的成员

既然“共用体”是值类型,那么共用体自然也可以作为作为另一个共用体的成员。让我们来看一个较为复杂的例子,使用共用体实现由协议、IP 和端口三部分组成的服务端地址的表示,形如:协议://IP:端口。

using System;
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Explicit)]
public struct IpAddress
{
[FieldOffset(0)] public int Address;
[FieldOffset(0)] public byte Byte1;
[FieldOffset(1)] public byte Byte2;
[FieldOffset(2)] public byte Byte3;
[FieldOffset(3)] public byte Byte4; public IpAddress(int address) : this()
{
Address = address;
} public override string ToString() => $"{Byte1}.{Byte2}.{Byte3}.{Byte4}";
} public enum Protocol : byte { http, https, ftp, sftp, tcp }; [StructLayout(LayoutKind.Explicit)]
public struct Server
{
[FieldOffset(0)] public IpAddress Address;
[FieldOffset(4)] public ushort Port;
[FieldOffset(6)] public Protocol Protocol;
[FieldOffset(0)] public long Payload; public Server(IpAddress addr, ushort port, Protocol prot) : this()
{
Address = addr;
Port = port;
Protocol = prot;
} public Server(long payload)
{
// 参数长度可能不足填满每个成员,所以这里先对成员设初始值
Address = new IpAddress(0);
Port = 80;
Protocol = Protocol.http; // 填值
Payload = payload;
} public Server Copy() => new Server(Payload); public override string ToString() => $"{Protocol}://{Address}:{Port}";
}

我们来用一段测试代码验证一下这个 Server 结构体的内存使用情况:

var ip = new IpAddress(new Random().Next());
Console.WriteLine($"Size: {Marshal.SizeOf(ip)} bytes. Value: {ip.Address} = {ip}"); var s1 = new Server(ip, 8080, Protocol.https);
var s2 = new Server(s1.Payload);
s2.Address.Byte1 = 100;
s2.Protocol = Protocol.ftp;
Console.WriteLine($"Size: {Marshal.SizeOf(s1)} bytes. Value: {s1.Address} = {s1}");
Console.WriteLine($"Size: {Marshal.SizeOf(s2)} bytes. Value: {s2.Address} = {s2}");

输出结果:

Size: 4 bytes. Value: 2102736192 = 64.53.85.125
Size: 8 bytes. Value: 64.53.85.125 = https://64.53.85.125:8080
Size: 8 bytes. Value: 100.53.85.125 = ftp://100.53.85.125:8080

示例中,IP 地址偏移 0 字节,长度为 4 字节;端口号偏移 4 字节,长度为 2 字节;协议偏移 6 字节,长度为 1 字节。总长度应为 4+2+1=7 字节,但实际打印出来却是 8 字节,请问是为什么?

参考:https://bit.ly/3qmH92V

[C#.NET 拾遗补漏]14:使用结构体实现共用体的更多相关文章

  1. 全国计算机等级考试二级教程-C语言程序设计_第14章_结构体、共用体和用户定义类型

    函数的返回值是结构体类型 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> struct ...

  2. __c语言__结构体、共用体、枚举__笔记

    2017-09-16 21:14:09 结构体,共用体,枚举 1.结构体 把不同的类型整合成一个有机的整体,以便于引用,这个类型就叫做结构体 1)结构体变量的定义方式(3种)和引用成员变量: 定义一个 ...

  3. 不可或缺 Windows Native (8) - C 语言: 结构体,共用体,枚举,类型定义符

    [源码下载] 不可或缺 Windows Native (8) - C 语言: 结构体,共用体,枚举,类型定义符 作者:webabcd 介绍不可或缺 Windows Native 之 C 语言 结构体 ...

  4. C++程序设计之结构体,共用体,枚举和typedef

    [1]结构体的基本功 注意结构体里面可以有很多东西,可以结构体里面包含结构体 #include<iostream> using namespace std; struct Date { i ...

  5. C++面试常见问题——13结构体与共用体的sizeof

    结构体与共用体的sizeof 结构体的sizeof 结构体变量占用的内存空间大小通常是其基本类型的大小,但是由例外(字节对齐机制) struct S1{ char c[5]; int a; doubl ...

  6. C语言程序设计(十二) 结构体和共用体

    第十二章 结构体和共用体 当需要表示复杂对象时,仅使用几个基本数据类型显然是不够的 根本的解决方法是允许用户自定义数据类型 构造数据类型(复合数据类型)允许用户根据实际需要利用已有的基本数据类型来构造 ...

  7. C语言------结构体和共用体

    仅供借鉴.仅供借鉴.仅供借鉴(整理了一下大一C语言每个章节的练习题.没得题目.只有程序了) 文章目录 1 .实训名称 2 .实训目的及要求 3.源代码及运行截图 4 .小结 1 .实训名称 实训8:结 ...

  8. C语言:结构体,共用体

    结构体: 一个变量,存储不同类型的数据项共用体:一个变量,存储不同类型的数据项,相同的内存位置,存储不同的数据类型 #include <stdio.h> #include <stri ...

  9. c的详细学习(10)结构体与共用体的学习(二)

    在c语言中,结构体数据类型与共用体数据类型都属于构造类型.共用体与结构体数据类型在定义上十分相似,但它们在存储空间的占用分配上有本质的区别.结构体变量是各种类型数据的集合,各成员占据不同的存储空间,而 ...

随机推荐

  1. 题解-CF802C Heidi and Library (hard)

    题面 CF802C Heidi and Library (hard) 有一个大小为 \(k\) 的空书架.有 \(n\) 天和 \(n\) 种书,每天要求书架中有书 \(a_i\).每天可以多次买书, ...

  2. 题解-CF101D Castle

    题面 CF101D Castle 给一棵 \(n\) 个节点的带权树,求一种遍历方案,从 \(1\) 出发,每条边走两次,走过所有点,第一次经过每个节点的平均时间最小.输出这个平均时间. 数据范围:\ ...

  3. Node第三方模块nodemon和nrm

    1.第三方模块nodemon nodemon是一个命令行工具,用以辅助项目开发. 在Node.js中,每次修改文件都要在命令行工具中重新执行该文件,非常繁琐,这时,nodemon就可以来解决这个问题. ...

  4. Java IO源码分析(二)——ByteArrayInputStream 和 ByteArrayOutputStream

    简介 ByteArrayInputStream 是字节数组输入流,它继承于InputStream. 它的内部数据存储结构就是字节数组. ByteArrayOutputStream是字节数组输出流,它继 ...

  5. STL——容器(Set & multiset)的删除 erase

    set.clear();             //清除所有元素 set.erase(pos);     //删除pos迭代器所指的元素,返回下一个元素的迭代器. set.erase(beg,end ...

  6. 手动实现Promise.all()

    Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例. Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每 ...

  7. DVWA各等级文件上传漏洞

    file upload 文件上传漏洞,攻击者可以通过上传木马获取服务器的webshell权限. 文件上传漏洞的利用是 够成功上传木马文件, 其次上传文件必须能够被执行, 最后就是上传文件的路径必须可知 ...

  8. 【Pyhton 】 装饰器

    # -*- coding:utf8 -*-# Author : Mr·Yang''' 装饰器,带参数装饰器,装饰器中带参数''' import time# 普通装饰器'''def time_decor ...

  9. pandas 学习 第14篇:索引和选择数据

    数据框和序列结构中都有轴标签,轴标签的信息存储在Index对象中,轴标签的最重要的作用是: 唯一标识数据,用于定位数据 用于数据对齐 获取和设置数据集的子集. 本文重点关注如何对序列(Series)和 ...

  10. 来体验下Linux吧

    在前面的几期中我们从树莓派开始了解Linux,大家可能已经想来试一下手了.趁热打铁,本期我将介绍两种方便体验学习Linux的方法,在线体验或者安装虚拟机. 1 在线体验Linux 如果想快速的体验下L ...