//11-1 union
//联合体 / 共用体
#include <iostream>
using namespace std;
struct DataS {
int i;
double d;
char s[10];
};
//结构体每一个成员都有一个单独的内存 相互之间没有影响
//each person on the structure has an unique RAM, they wouldn’t affect each other
union DataU {
int i; //4
double d; //8
char s[10]; //10
};
//联合体每一个成员都共享同一段内存 相互影响
//所有联合体的成员占据同一个起始地址
//以上Union占据10个字节内存,但是由于字节对齐,会占16字节
/*
1.定义和使用分开
union DataU {
int i; //4
double d; //8
char s[10]; //10
};
DataU a,b,c;
2.定义和使用结合
union DataU {
int i; //4
double d; //8
char s[10]; //10
};a,b,c
//直接在后面
3.匿名:不想让别人使用
union {
int i; //4
double d; //8
char s[10]; //10
};a,b,c
*/
int main() {
DataS ds;
cout << &ds.i << “,” << &ds.d << “,” << (void *) ds.s << endl;
DataU du;
cout << &du.i << “,” << &du.d << “,” << (void*)du.s << endl;
return 0;
}
//11 – 2 memory layout of a union
//联合体的内存布局
#include <iostream>
using namespace std;
union DataU {
int i; //4
double d; //8
char s[9]; //9
};
int main() {
cout << sizeof(DataU) << endl;//字节对齐到16字节
//This place will be an alignment of byte to the 16bytes
DataU du;
du.s[0] = 255; //11111111 on the RAM
du.s[1] = 1; //00000001
du.s[2] = 0; //00000000
du.s[3] = 0; //00000000
cout << du.i << endl;//outputed 511,because finally it will displayed 00000000 00000000 00000001 11111111
//(binary)00000000 00000000 00000001 11111111 =2^9-1 =(decimal)511
du.i = 256; //1byte 一个字节最多能够表示为255
//du.i = 260; //1byte 一个字节最多能够表示为255
//i是四个字节,y影响只到s[3]
//字节序是从右到左 大小端字节序
cout << (int)du.s[0] << (int)du.s[1] << (int)du.s[2] << (int)du.s[3] << endl;//char是字符,然后这里可能有些不可见的字符,需要转换成整数再输出
//outputed 0100,超过255就进位
return 0;
}
//11 – 3 union’s application
//union 的应用
#include <iostream>
using namespace std;
struct Info {
char _name[20]; //名字 唯一标识
int _role; //角色:老师或学生
//存一个匿名的union
union {
double score; //什么分数 老师没用 学生有用
char course[20]; //教什么课 老师有用 学生没用
//放在同一个union里面可以节省一点空间
}_sc;
Info(const char name[20], int role, double s, const char c[20]) {
strcpy_s(_name, name);//目的 源 destination source
_role = role;
if (s > 0) _sc.score = s; //s有长度就赋值
if (strlen(c) > 0) strcpy_s(_sc.course, c); //c有长度就拷贝课程
}
};
int main() {
Info a[4]{
Info(“冯校长”, 0, -1, “C++”),
Info(“冯校长”, 0, -1, “Python”),
Info(“冯同学”, 1, 90, “”),
Info(“赖同学”, 1, 88, “”),
//role 0代表老师 , score -1 代表不关心成绩
};
for (int i = 0; i < 4; ++i) {
if (a[i]._role == 0) {
cout << a[i]._name << “是一位老师,他是教” << a[i]._sc.course << “的” << endl;
}
else if (a[i]._role == 1)
{
cout << a[i]._name << “是一位学生,他的成绩是” << a[i]._sc.score << endl;
}
}
return 0;
}
AI:
字节对齐(Byte Alignment):概念、原理与作用
在计算机编程(尤其是底层开发,如 C/C++)和计算机体系结构中,字节对齐(Byte Alignment) 是指计算机内存中数据的存储地址必须是其 “对齐系数”(通常与数据类型大小相关)整数倍的规则。这一机制并非由编程语言直接定义,而是源于硬件设计的限制与效率优化需求,是连接软件数据结构与硬件内存访问的关键桥梁。
一、核心概念:为什么需要字节对齐?
计算机 CPU 访问内存时,并非以 “1 字节” 为最小单位,而是以 **“内存访问粒度”**(通常为 2 字节、4 字节、8 字节,取决于 CPU 架构,如 32 位 CPU 常见 4 字节粒度,64 位 CPU 常见 8 字节粒度)批量读取 / 写入。若数据存储地址不符合对齐规则(即 “未对齐”),会触发两种结果:
- 硬件层面:CPU 需额外执行 1 次或多次内存访问,将未对齐的数据 “拆分 – 拼接” 后处理,直接降低效率;部分严苛的硬件(如早期 RISC 架构)甚至会直接抛出 “内存未对齐异常”,导致程序崩溃。
 - 软件层面:编译器会自动插入 “填充字节”(Padding Bytes),将未对齐的数据调整为符合硬件要求的地址,保证程序正常运行,但会额外占用少量内存。
 
简言之:字节对齐是 “用少量内存开销,换取硬件访问效率” 的权衡机制。
二、基础规则:对齐系数与数据类型
不同数据类型的 “默认对齐系数” 由两个因素决定:
- CPU 架构:32 位系统默认最大对齐系数为 4,64 位系统默认最大对齐系数为 8(可理解为硬件的 “最优访问单位”);
 - 数据类型大小:基本数据类型的对齐系数通常等于其自身大小(如char占 1 字节,对齐系数 1;int占 4 字节,对齐系数 4;double占 8 字节,对齐系数 8)。
 
最终数据的对齐地址需满足:数据存储的起始地址 % 对齐系数 == 0(“%” 为取余运算)。
常见基本数据类型的默认对齐(以 32 位系统为例)
| 数据类型(C/C++) | 占用字节数 | 默认对齐系数 | 合法起始地址(示例) | 非法起始地址(示例) | 
| char | 1 | 1 | 0,1,2,3,4… | 无(任何地址均合法) | 
| short(短整型) | 2 | 2 | 0,2,4,6… | 1,3,5… | 
| int(整型) | 4 | 4 | 0,4,8,12… | 1,2,3,5… | 
| float(单精度浮点) | 4 | 4 | 0,4,8,12… | 1,2,3,5… | 
| double(双精度浮点) | 8 | 8 | 0,8,16…(64 位系统) | 1-7,9-15… | 
三、复合数据类型的对齐:结构体(Struct)与共用体(Union)
实际编程中,字节对齐的核心应用场景是复合数据类型(结构体struct、共用体union),其对齐规则需同时满足 “成员对齐” 和 “整体对齐” 两层要求。
1. 结构体(Struct)的对齐规则
结构体的总大小不仅取决于成员的实际字节数,还需考虑编译器插入的 “填充字节”,规则如下:
- 成员对齐:结构体中每个成员的起始地址,必须是该成员 “对齐系数” 的整数倍;若前一个成员的结束地址不满足,则插入填充字节(Padding)。
 - 整体对齐:结构体的总大小,必须是所有成员中最大对齐系数的整数倍;若成员总字节数(含已插入的填充)不满足,则在结构体末尾插入填充字节。
 
示例:32 位系统下的结构体对齐
c
struct Example {
char a; // 1字节,对齐系数1,地址0(合法)
int b; // 4字节,对齐系数4;前一个成员结束于地址1,需填充3字节(地址1-3),b从地址4开始
short c; // 2字节,对齐系数2;前一个成员结束于地址7,c从地址8开始(合法)
};
- 成员实际字节数:1(a) + 4(b) + 2(c) = 7 字节;
 - 填充字节:成员 a 后填充 3 字节(地址 1-3);
 - 整体对齐:最大对齐系数为 4(来自int b),7 字节需补 1 字节(地址 10),总大小为 8 字节。
 
2. 共用体(Union)的对齐规则
共用体的所有成员共享同一块内存(起始地址相同),因此对齐规则更简单:
- 共用体的起始地址需满足 “所有成员中最大对齐系数” 的整数倍(保证最大成员能正常存储);
 - 共用体的总大小等于 “所有成员中最大字节数”(因共享内存),且需是 “最大对齐系数” 的整数倍(若最大成员字节数不满足,需在末尾填充)。
 
示例:32 位系统下的共用体对齐
c
union Data {
char a; // 1字节,对齐系数1
int b; // 4字节,对齐系数4
short c; // 2字节,对齐系数2
};
- 最大成员字节数:4(来自int b);
 - 最大对齐系数:4(来自int b);
 - 总大小:4 字节(无需填充,因 4 是 4 的整数倍)。
 
四、编译器控制:自定义对齐系数
默认对齐规则可能无法满足特殊需求(如硬件寄存器访问、网络数据传输需 “紧凑存储”),此时可通过编译器指令自定义对齐系数
示例:紧凑存储结构体(取消填充)
c
// GCC/G++ 示例:紧凑存储,无填充
struct PackedStruct {
char a;
int b;
short c;
} __attribute__((packed));
// 总大小:1+4+2=7字节(无填充,因1字节对齐)
五、字节对齐的实际影响
- 性能影响:未对齐的数据会导致 CPU 多轮内存访问,例如 32 位 CPU 访问地址为 1 的int(4 字节)时,需先读地址 0-3(取第 2-4 字节),再读地址 4-7(取第 1 字节),拼接后才能处理,效率降低 50% 以上。
 - 内存开销:默认对齐会产生填充字节,例如结构体成员顺序不合理(如char→int→char)会比合理顺序(char→char→int)多占用 2 字节填充(32 位系统下)。
 - 兼容性影响:跨平台(如 32 位 / 64 位系统)或跨编译器的数据交互(如文件存储、网络传输)中,若对齐规则不一致,会导致数据解析错误(如结构体大小不同,成员偏移地址错位)。
 
六、关键总结
- 字节对齐是硬件驱动的规则,核心目的是提升 CPU 内存访问效率;
 - 基本数据类型对齐系数默认等于自身大小,复合类型需满足 “成员对齐” 和 “整体对齐”;
 - 编译器可通过指令自定义对齐(如紧凑存储),但需权衡 “效率” 与 “内存开销”;
 - 跨平台 / 跨设备数据交互时,必须统一对齐规则(如强制 1 字节对齐),避免解析错误。
 
Ab1n:为什么字符数组那里不用&而是void* 强转
2025-01-19 22:25
爱吃薯条 回复 Ab1n:a.c 的实质:
字符数组 a.c 在大多数情况下会隐式退化为指向首元素的指针(类型为 char*)。
当使用 (void *)a.c 时,本质是将这个 char* 指针强制转换为 void* 类型,输出的是首元素的地址。
&a.c 的实质:
&a.c 是对整个数组 c 取地址,类型是 char (*)[10](指向长度为10的字符数组的指针)。
地址相同的原因:
数组的起始地址和其首元素的地址在内存中是同一个位置,因此两者的数值(地址值)相同。
例如:如果数组 c 从内存地址 0x1000 开始,那么 a.c(首元素地址)和 &a.c(数组地址)都是 0x1000。
2025-03-01 12:03
牵丝戏 回复 爱吃薯条:应该说在c++里面 char*,或者对char类型的a取地址,再放到cout里面,c++会解释为一个字符串,并尝试输出它直到遇到\0为止,所以直接输出地址会乱码,需要强制转换才能得到想要的地址。
2025-07-26 17:32
在 C++ 中,字符数组名本身就是一个指向数组首元素的指针(即数组首地址),而其他类型变量(如 int、double)需要通过 & 运算符才能获取地址。这是数组类型的特殊语法规则。
而使用 (void*) 强制转换的原因是:
cout 输出字符指针(char*)时,会默认将其当作字符串处理(尝试打印从该地址开始的所有字符,直到遇到 \0 终止符)。但在这里,我们的真实目的是输出数组的内存地址,而非字符串内容,因此需要通过 (void*) 强制转换为通用指针类型,让 cout 按照地址格式(如 0x7ffdabc1234)输出,而非误判为字符串。
简单说:
- ds.s 本身就是地址(无需 &),但类型是 char*
 - (void*)ds.s 是为了让 cout 输出地址而非字符串内容
 
如果不进行转换,cout 可能会打印乱码(直到遇到随机的 \0),而非我们期望的内存地址值。
在计算机存储和传输多字节数据(如 int、long 等,单字节数据无需考虑)时,字节序(Endianness) 指的是字节在内存地址或传输流中的排列顺序。核心矛盾是:数据的 “高位字节” 与内存的 “低地址” 如何对应,由此衍生出两种主流字节序 ——大端(Big-Endian) 和 小端(Little-Endian)。
一、核心概念:高位字节与内存地址
首先明确两个关键术语,以 32 位十六进制数 0x12345678 为例(4 个字节):
- 高位字节(MSB,Most Significant Byte):数据中权重最高的字节,即 0x12(对应十进制的 “千万级”);
 - 低位字节(LSB,Least Significant Byte):数据中权重最低的字节,即 0x78(对应十进制的 “个位级”);
 - 内存地址:计算机内存按 “低地址 → 高地址” 顺序排列(如地址 0x00、0x01、0x02、0x03)。
 
字节序的本质,就是决定 “高位字节(MSB)” 是否存到 “低地址”。
二、两种主流字节序:大端 vs 小端
以 0x12345678 存储到地址 0x00~0x03 为例,直观对比两种字节序:
| 字节序类型 | 核心规则 | 内存地址分配(地址从低到高) | 特点 | 
| 大端(Big-Endian) | 高位字节(MSB)存低地址 | 0x00: 0x12 → 0x01: 0x34 → 0x02: 0x56 → 0x03: 0x78 | 字节顺序与人类阅读习惯一致(从高位到低位) | 
| 小端(Little-Endian) | 低位字节(LSB)存低地址 | 0x00: 0x78 → 0x01: 0x56 → 0x02: 0x34 → 0x03: 0x12 | 字节顺序与人类阅读习惯相反,但 CPU 运算更高效(无需调整字节顺序) | 
举个通俗例子
假设你有一笔钱 12345678 元,需要分 4 个信封(对应 4 个字节)装,信封上标有地址 00(第一个信封)、01、02、03(第四个信封):
- 大端逻辑:把 “最高位的 12” 放进第一个信封(低地址),依次类推,信封里的钱是 12、34、56、78,打开信封就能按顺序读对金额;
 - 小端逻辑:把 “最低位的 78” 放进第一个信封(低地址),信封里的钱是 78、56、34、12,需要倒过来读才能得到正确金额。
 
三、字节序的起源
“大端” 和 “小端” 的命名源自英国作家 乔纳森・斯威夫特 的《格列佛游记》:
- 利立浦特国(小人国)的国民因 “吃鸡蛋先敲大头还是小头” 分裂为两派,对应计算机中 “字节存储顺序的两种对立方案”;
 - 1981 年,计算机科学家 Danny Cohen 在论文《On Holy Wars and a Plea for Peace》中首次将这两个术语引入计算机领域,用来调侃当时关于字节序的激烈争论。
 
四、应用场景:哪些系统用大端 / 小端?
字节序是 硬件架构或协议约定的底层规则,不同场景有明确选择:
1. 小端:主流 CPU 架构
- x86/x86-64 架构:Intel、AMD 的桌面 / 服务器 CPU(如 i7、Ryzen)均为小端;
 - ARM 架构(默认):手机、平板、嵌入式设备的 ARM CPU(如骁龙、麒麟)默认是小端(可通过配置切换为大端,但极少用);
 - 原因:小端让 CPU 处理多字节数据时更高效 —— 运算从低位开始,无需先 “调整字节顺序”,直接读取低地址的低位字节即可。
 
2. 大端:网络协议与部分硬件
- 网络字节序(TCP/IP 协议):所有网络传输(如 HTTP、TCP、UDP)均强制使用大端,避免不同架构的设备通信出错;
- 需通过转换函数处理:如 C 语言中的 htonl()(主机字节序→网络字节序,32 位)、ntohl()(网络字节序→主机字节序);
 
 - 部分 CPU 架构:PowerPC(旧苹果电脑)、SPARC(服务器)、MIPS(部分路由器);
 - 文件格式:JPEG、PNG、BMP(位图,注:BMP 的像素数据是小端,但文件头的 “魔数” 是大端)、TIFF(可标注字节序,默认大端)。
 
五、为什么要关注字节序?—— 跨平台 / 跨设备的 “坑”
如果忽略字节序,跨不同架构的设备传输数据会直接出错。
错误案例
- 小端电脑写文件,大端电脑读文件:
- 小端电脑将 0x12345678 写入文件,实际存储的字节是 0x78、0x56、0x34、0x12;
 - 大端电脑读取文件时,会按 “低地址→高地址” 将字节解析为 0x78563412,与原数据完全不符。
 
 - 小端手机向大端服务器发数据:
- 小端手机发送 0x1234(16 位),实际传输的字节是 0x34、0x12;
 - 大端服务器若不转换,会解析为 0x3412,数据错误。
 
 
六、如何检测当前系统的字节序?
通过代码可快速判断系统是大端还是小端,核心思路是利用联合体(Union)的内存共享特性(联合体的所有成员共用同一块内存)。
示例代码(C 语言)
c
#include <stdio.h>
union EndianTest {
int num; // 4 字节整数
char bytes[4]; // 1 字节数组(与 num 共享内存)
};
int main() {
union EndianTest test;
test.num = 0x12345678; // 给整数赋值
// 查看低地址(bytes[0] 对应地址最低)的字节值
if (test.bytes[0] == 0x78) {
printf(“当前系统是小端(Little-Endian)\n”);
} else if (test.bytes[0] == 0x12) {
printf(“当前系统是大端(Big-Endian)\n”);
} else {
printf(“混合端(极少见)\n”);
}
return 0;
}
- 若运行结果为 “小端”:对应 x86/x86-64 或 ARM 设备;
 - 若为 “大端”:对应 PowerPC 或部分嵌入式设备。
 
七、特殊情况:混合端(Middle-Endian)
极少数架构使用 “混合端”(如旧 PDP-11 计算机),字节排列既非纯大端也非纯小端。例如 0x12345678 可能存储为 0x34、0x12、0x78、0x56,目前已几乎淘汰,无需重点关注。
总结
| 维度 | 大端(Big-Endian) | 小端(Little-Endian) | 
| 核心规则 | 高位字节存低地址 | 低位字节存低地址 | 
| 人类可读性 | 高(与数据书写顺序一致) | 低(需反向读取) | 
| 硬件效率 | 低(需调整字节顺序) | 高(直接运算低位字节) | 
| 主要应用 | 网络协议(TCP/IP)、部分 CPU | x86/ARM 架构 CPU、多数嵌入式设备 | 
| 关键问题 | 跨端通信需统一为大端 | 跨端通信需转换为网络字节序 | 
理解字节序的核心是:多字节数据的 “高位 / 低位” 与内存 “低地址 / 高地址” 的对应关系,其本质是硬件架构和协议为平衡效率与兼容性的选择。
在文件格式中,“魔数(Magic Number)” 是指文件开头的几个固定字节,用于唯一标识文件类型,帮助操作系统或应用程序快速识别文件格式(即使文件扩展名被篡改,也能通过魔数判断)。
BMP 文件的魔数
BMP(Bitmap,位图)文件的魔数是固定的 2 个字节,其值为:
十六进制:0x424D
ASCII 字符:‘B’ followed by ‘M’(即 “BM”)
具体解析
- 字节构成:
魔数的第一个字节是 0x42(对应 ASCII 字符 ‘B’),第二个字节是 0x4D(对应 ASCII 字符 ‘M’)。在文件的起始地址(低地址)中,这两个字节按顺序存储为 0x42(地址 0x00)→ 0x4D(地址 0x01)。 - 含义:
“BM” 是 “Bitmap” 的缩写,是微软定义的 BMP 格式标准标识。几乎所有 Windows 系统中的 BMP 文件都以此开头。 - 与字节序的关系:
由于魔数仅由 2 个单字节组成,且顺序固定为 ‘B’ 在前、’M’ 在后,不涉及多字节数据的字节序问题(大小端差异仅影响多字节数据,单字节无需考虑)。无论系统是大端还是小端,BMP 魔数的存储和读取顺序都是固定的。 
作用
- 文件类型校验:应用程序(如图片查看器、图像处理软件)读取 BMP 文件时,首先检查前 2 个字节是否为 0x424D,若不是则判断为非 BMP 文件或损坏的 BMP 文件。
 - 跨平台兼容性:即使在不同操作系统(如 Windows、Linux)中,通过魔数也能统一识别 BMP 格式。
 
特殊情况
少数非标准 BMP 变体(如早期 OS/2 系统的位图)可能使用其他魔数,例如:
- 0x4241(“BA”):OS/2 Bitmap Array
 - 0x4349(“CI”):OS/2 Color Icon
 - 0x4350(“CP”):OS/2 Color Pointer
但这些属于小众格式,日常接触的 BMP 文件(尤其是 Windows 环境下)几乎都以 0x424D(“BM”)为魔数。 
示例
用十六进制编辑器打开任意 BMP 文件,开头两行(前 2 字节)必然显示:
42 4D
对应的 ASCII 解析即为 “BM”,这是 BMP 文件的 “身份标识”