一、创建与使用联合体
1.1 概念
联合体(union)看起来像结构体,但其成员共享同一段内存:编译器只为联合体分配一块足够大的内存——大小等于其最大成员的大小,并按成员的最大对齐要求对齐。因为所有成员从同一地址开始,所以在某一时刻只有一个成员能被“有效地”保存原始语义;如果写入了一个成员然后读取另一个成员,得到的是相同内存的不同解释。
特别之处:
"联合"与"结构"有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间,一个结构体变量的总长度大于等于各成员长度之和。而在"联合"中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。
应该说明的是,这里所谓的共享不是指把多个成员同时装入一个联合变量内,而是指该联合变量可被赋予任一成员值,但每次只能赋一种值,赋入新值则冲去旧值。
1.2 基本特性
- 所有成员共享同一块内存空间;
- 联合体的大小等于其最大成员的大小;
- 同一时间只能访问一个成员,修改一个成员会影响其他成员;
变量内存共用示例:
#include <iostream>
using namespace std;
// 定义联合体
union Data
{
int i; // 4字节
int f; // 4字节
};
int main()
{
union Data data;
data.i = 10;
// 10
cout << "整数值: " << data.i << endl;
data.f = 12;
// 12
cout << "浮点值: " << data.f << endl;
// 此时整数已经访问结果已经变了
// 12
cout << "整数值: " << data.i << endl;
return 0;
}
不同类型成员示例:
#include <iostream>
using namespace std;
// 在一个时间内中使用一个变量:
union Data
{
int i;
double f;
};
int main()
{
cout << sizeof(Data) << endl; // 8
Data data;
data.i = 12;
cout << data.i << endl; // 12
data.f = 999;
cout << data.i << endl; // 值被覆盖
cout << data.f << endl; // 999
return 0;
}
二、联合体的特点探讨
2.1 内存分析
联合体成员共用一个内存空间,那么一定要保证最大的成员要装下,所以这样的联合变量的大小,至少是最大成员的大小(因为联合体至少有能力保存最大的那个成员)。
#include <stdio.h>
#include <stdlib.h>
union Un
{
char a;
int i;
};
struct Stc
{
char a;
int i;
};
int main()
{
union Un un = { 0 };
// 4
printf("%d\n", sizeof(un));
// 8
struct Stc stc = { 0 };
printf("%d\n", sizeof(stc));
printf("%d\n", sizeof(un.a)); // 1
printf("%d\n", sizeof(un.i)); // 4
printf("%p\n", &un); // 0060FEFC
printf("%p\n", &(un.a)); // 0060FEFC
printf("%p\n", &(un.i)); // 0060FEFC
system("pause");
return 0;
}
说明:联合体及其各个成员们的地址都是一样的,说明所有成员都在首地址处存放。
2.2 内存覆盖测试
通过写入 int
再写 char
来演示低字节被覆盖的现象。要理解输出,必须知道系统的字节序(endianness):
- 小端(little-endian):低位字节放在低地址。写
i = 0x11223344
后内存从低地址到高地址是44 33 22 11
,如果再写a = 0x55
会覆盖第一个字节(低位),结果变为55 33 22 11
, 整数是0x11223355
(在打印时可能以小端理解)。 - 大端(big-endian):高位字节放在低地址,内存为
11 22 33 44
,写低地址的char
会覆盖高位字节,结果不同。
假设小端系统(x86/x86_64 通常是小端):
#include <stdio.h>
#include <stdlib.h>
union Un
{
int i;
int j;
char a;
char b;
};
int main()
{
// 测试1
union Un un1 = { 0 };
un1.i = 0x11223344;
un1.a = 0x55;
// 0x11223355
printf("%x\n", un1.i);
// 测试2
union Un un2 = { 0 };
un2.i = 0x1122;
un2.a = 0x99;
// 0x1199
printf("%x\n", un2.i);
// 测试3
union Un un3 = { 0 };
un3.a = 0x11;
un3.b = 0x88;
// 0x88
printf("%x\n", un3.i);
// 测试4
union Un un4 = { 0 };
un4.i = 0x11002200;
un4.b = 0x88;
// 0x11002288
printf("%x\n", un4.i);
// 测试5
union Un un5 = { 0 };
un5.i = 0x11002200;
un5.j = 0x88;
// 0x88
printf("%x\n", un5.i);
system("pause");
return 0;
}
说明:内存只有一块,大小为最大数据类型的大小,其它变量赋值,会从低位向高位覆盖。
2.3 大小的计算
联合体大小计算规则:
- 联合体的大小至少是最大成员的大小;
- 当最大成员大小不是最大对齐数的整数倍的时候,就要将最大成员大小对齐到最大对齐数的整数倍;
#include <stdio.h>
#include <stdlib.h>
union S1
{
char a[5]; // 5字节
int i; // 4字节
};
union S2
{
short c[7]; // 14字节
int i; // 4字节
};
union S3
{
char a[8]; // 8字节
int i; // 4字节
};
int main()
{
printf("%d\n", sizeof(union S1)); // 8
printf("%d\n", sizeof(union S2)); // 16
printf("%d\n", sizeof(union S3)); // 8
system("pause");
return 0;
}
分析:
- 在S1中,char a[5]的内存大小为5,int i内存大小为4,5不是4的整数倍,要对齐到最大对齐数的整数倍,所以是5 + 3 = 8,是4的倍数
- S2的推导结果类似S1:14不是4的整数倍,对齐到16
- S3中char a[8]是8字节,已经是4的倍数,所以大小为8
2.4 优点
联合体用于节省空间(不同类型互斥使用)非常有效。常见场景包括:协议解析、硬件寄存器映射、节省内存的变体信息存储等。
例如我们要统计三种商品数据:图书、杯子、衬衫。
每一种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
- 图书:书名、作者、页数
- 杯子:设计
- 衬衫:设计、可选颜色、可选尺寸
使用结构体的方式(浪费内存):
#include<stdio.h>
struct my_list
{
int stock_number; // 库存量
double price; // 价格
int item_type; // 商品类型
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
char design[30]; // 设计
int colors; // 颜色
int sizes; // 尺寸
};
上述结构设计简单用起来方便,但结构的设计中包含了所有商品的各种属性,这样会使得结构体的大小过大,很浪费内存。因为对于各自商品,只有部分属性信息是常用的:商品是图书,就不需要design、colors、sizes。
使用联合体的优化方式:
struct my_list
{
// 公共属性
int stock_number; // 库存量
double price; // 价格
int item_type; // 商品类型
union
{
struct // 匿名结构体 - 图书
{
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // 页数
} book;
struct // 匿名结构体 - 杯子
{
char design[30]; // 设计
} mug;
struct // 匿名结构体 - 衬衫
{
char design[30]; // 设计
int colors; // 颜色
int sizes; // 尺寸
} shirt;
} item;
};
这种方式可以显著节省内存,因为同一时间只会使用其中一种商品类型的属性。