结构体内存中到底长啥样?

结构体的介绍及定义

结构体是一种复合结构类型,可以将其视为类和对象的前身,一般定义在头文件中

结构体的功能在于用不同的数据集合描述一个实体

但是要描述一个东西,数据是说了不算的,数据大多数情况下无法明确的区分两种东西,比如我们想要描述一个人是小偷,仅仅描述这个人的身高、体重、穿着…这些数据是没用的,所以除了数据还要加上行为才能确实的区分一个东西,对于上面的例子,我们要加上这个人有偷东西的行为后,才可以确认他是小偷

这也就是类(数据+行为)和结构体(数据)的区别

  • 当然,结构体中也可以加入函数指针,这里不做讨论
  • 但类中的行为(成员函数),是不占空间描述的,匹配行为靠的是名称粉碎机制

结构体的设计难题

使用结构体的难点在于结构体的设计:一旦项目中的结构体设计出现问题,就是骨架的问题,会导致整个项目出问题,整个都需要推倒重来

浅说结构体的设计

现在有如下结构体:目的用于存储公民个人基本信息

请问:该定义是否合理?

struct tagPerson
		{
			char szName[5]; //姓名
			int nAge; //年龄
			bool cGender; //性别
			char *szPhoneNumber; //电话号码
			float fHeight; //身高
			double dblWeight; //体重
		};

答:不合理!

  • 不应该定义nAge年龄年龄是会变化的,人多时总要刷新该数据

    QQ曾出现过这样的问题,就是这样定义的,造成的问题在于,若几年没有登录,
    会导致年龄不变

    后来为了解决该问题,该为出生日期,直接计算即可

  • 国际中性别有四种,无法使用bool

    • 🤨 M Male 男性——F Female 女性——U Unknow 未知——O Other 其他
  • 身高、体重,身高、体重与本人没关系与检查测量有关系,不应该加入结构体

    身高、体重也会随时变化(若想绘制一段时间内的身高、体重变化该结构体实现不了)

    • 📌 该结构体为个人基本信息,若想记录身高、体重等测量信息,需要再建立一个结构体
      用于专门存储这类信息

语法与原理

基本语法不做过多介绍,主要介绍原理部分:内存结构等

struct tagPerson
{
	char szName[5]; 
	int nAge;
	char cGender;
	char *szPhoneNumber;
	float fHeight;
 	double dblWeight;
};

struct tagTest
{

};

int main(int argc,char* argv[],char* envp[])
{
	struct tagPerson per = {
				"Jack",
				18,
				'M',
				"110",
				175.6f,
				60.5
	};

	struct tagTest test;

}

Some基本语法

接下来给出的示例,均写在上述代码的main()函数中

空结构体

struct tagTest test;
printf("%d\r\n",sizeof(test));

由上述代码可知,tagTest是一个空结构体

此时sizeof(test) = 1 ,空结构体的大小为1(实际上对齐后大小为4)

请添加图片描述

结构体的访问

主要两种方式:取成员.运算结构体指针—>运算

1:取成员 . 运算

printf("%d\r\n",per.nAge);

. 的优先级仅次于()

2:结构体指针 —> 运算

struct tagPerson *pPer = &per; //结构体指针
printf("%d\r\n",pPer->nAge); 

= (*pPer).nAge 
= (&per)->nAge

不推荐后两种写法

相同的结构体之间的赋值

相同结构体之间可直接赋值

  • 相同结构体直接赋值

    struct tagPerson per2 = per
    
  • 使用memcpy()进行结构体的复制

    🥳插播一条:有关memcpy()的介绍:(主要从与strcpy()对比中得出)

    • 复制的内容不同。strcpy()只能复制字符串,而memcpy()可以复制任意内容,例如字符数组、整型、结构体、类等
    • 复制的方法不同。strcpy()不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy()则是根据其第3个参数决定复制的长度
    • 用途不同。通常在复制字符串时用strcpy(),而需要复制其他类型数据时则一般用memcpy()

    所以上述结构体的复制还可以写为:

    memcpy(&per2,&per,sizeof(struct tagPerson));
    //将per复制给per2
    

结构体的传参

结构体传参传的就是数据,不是地址,所以若传参为结构体,就会在栈中整个开辟一块=结构体定义大小的空间

原理部分:结构体的内存结构

问题的出现

#include <stdio.h>

struct tagPerson
{
	char szName[5]; 
	int nAge;
	char cGender;
	char *szPhoneNumber;
	float fHeight;
 	double dblWeight;
};

int main(int argc)
{
	struct tagPerson per = {
		"Jack",
		18,
		'M',
		"110",
		175.6f,
		60.5
	};
	printf("%d",sizeof(per));
	return 0;
}

按照我们以前的思维,对于内存中存储对齐的这个问题,应当是每个数据分别对齐,算总长度即可,那么对于上述 sizeof(per) 的结果就应该是 8+4+4+4+4+8 = 32

请添加图片描述

那么按照上面的逻辑,若我们只调换结构体中数据类型的位置,sizeof()后大小应该是不变的,我们将顺序换一下(将dbWeight拿到cGender前面)

struct tagPerson
{
	char szName[5]; 
	int nAge;
	double dblWeight;
	char cGender;
	char *szPhoneNumber;
	float fHeight;		
};

此时奇迹发生了:仅仅换了顺序,导致sizeof()也发生了变化,也就说明了之前我们说的结构体大小的计算方式是不对的

请添加图片描述

对齐结构

结构体的对齐单位是可以设置的,通过编译选项中的 /Zp 进行设置

结构体的对齐要求不是编译器提出的,而是网络通讯提出的

网络通讯中,经常用结构体定义数据包,某些网络协议中双方需要约定对齐值,否则会影响结构体内部布局

  • 🍁 网路中没有约定对齐值的,其对齐值默认为1

结构体的对齐值

/Zp {1;2;4;8;16} 都可以,默认对齐值为 8(整体和各成员变量都有对齐值)

Project -> Settings -> C/C++ -> Code Generation -> Struct member alignment(结构体对齐单位)

使用默认对齐值时不会显示在编译选项中

请添加图片描述

若要使用其余值,会在编译选项中添加 /Zp1 /Zp2...

请添加图片描述

如何计算( ⭐⭐⭐ 吹包儿star)非常重要!!

第一大步骤:

首先我们规定这样几个值:

  • MemberAlig 成员变量__自身的对齐值
  • MemberOffset 成员变量__距结构体首地址的字节数(成员偏移量)
  • MemberType 成员变量__的数据类型

他们之间的关系必须满足:

  • MemberAlig = min(/Zp,sizeof(MemberType))
  • MemberOffset % MemberAlig == 0 (否则++)

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

第二大步骤:

设结构体变量的对齐值为StructAlig

StructAlig = max(sizeof(Member1Type),sizeof(Member2Type)...,sizeof(MemberEndType))
StructAlig = min(Zp,StructAlig)

同时必须满足

StructSize % min(Zp,StructAlig) == 0 (否则++)

  • ⚠️ 当结构体成员变量为结构体类型时,其MemberType设定为StructAlig

举例说明(VC6.0)

/Zp = 8

struct tagPerson
{
	char szName[5]; 
	int nAge;
	double dblWeight;
	char cGender;
	char *szPhoneNumber;
	float fHeight;		
};

的对齐格式:

请添加图片描述

第一大步骤:(逐行分析)

char szName[5];
  1. MemberAlig = min(/Zp,sizeof(char)) = min(8,1) = 1

  2. MemberOffset % MemberAlig == 0 0 % 1 = 0

    szName[5]处于结构体中的第一个元素→MemberOffset=0

  3. 由于 0 % 1 = 0 满足 MemberOffset % MemberAlig == 0 所以无需做额外变化

    所以szName[5]被装载到Offset = 0的位置

int nAge;
  1. MemberAlig = min(/Zp,sizeof(int)) = min(8,4) = 4

  2. MemberOffset % MemberAlig == 0 6 % 4 ≠ 0

    szName[5]MemberAlig为 1 且MemberOffset=0, 0+1*5 = 5 → nAge目前的的MemberOffset=6

  3. 由于 6 % 4 ≠ 0 不满足 MemberOffset % MemberAlig == 0 所以 ++ 7 % 4 ≠ 0 再++ 8 % 4 = 0

    所以nAge的实际MemberOffset = 8 (不是6)

    所以nAge被装载到Offset = 8的位置

double dblWeight;
  1. MemberAlig = min(/Zp,sizeof(double)) = min(8,8) = 8

  2. MemberOffset % MemberAlig == 0 12 % 8 ≠ 0

    nAgeMemberAlig为 4 且MemberOffset=8, 4+8 = 12 → dblWeight目前的MemberOffset=12

  3. 由于 12 % 8 ≠ 0 不满足 MemberOffset % MemberAlig == 0 所以 ++ 13 % 8 ≠ 0 再++ 14 % 4 ≠ 0 再++ 15 % 8 ≠ 0 再++ 16 % 8 = 0

    所以dblWeight的实际MemberOffset = 16 (不是12)

    所以dblWeight被装载到Offset = 16的位置

char cGender;
  1. MemberAlig = min(/Zp,sizeof(char)) = min(8,1) = 1

  2. MemberOffset % MemberAlig == 0 24 % 8 = 0

    dblWeightMemberAlig为 8 且MemberOffset=16, 16+8 = 24 → cGender目前的MemberOffset=24

  3. 由于 24 % 8 = 0 满足 MemberOffset % MemberAlig == 0 所以无需做额外变化

    cGenderMemberOffset = 24

    所以dblWeight被装载到Offset = 24的位置

char *szPhoneNumber;
  1. MemberAlig = min(/Zp,sizeof(char *)) = min(8,4) = 4

  2. MemberOffset % MemberAlig == 0 25 % 4 ≠ 0

    cGenderMemberAlig为 1 且MemberOffset=24, 1+24 = 25 → szPhoneNumber目前的MemberOffset=25

  3. 由于 25 % 4 ≠ 0 不满足 MemberOffset % MemberAlig == 0 所以 ++ 26 % 4 ≠ 0 再++ 27 % 4 ≠ 0 再++ 28 % 4 = 0

    所以szPhoneNumber的实际MemberOffset = 28 (不是25)

    所以szPhoneNumber被装载到Offset = 28的位置

float fHeight;	
  1. MemberAlig = min(/Zp,sizeof(float)) = min(8,4) = 4

  2. MemberOffset % MemberAlig == 0 32 % 4 = 0

    szPhoneNumberMemberAlig为 4 且MemberOffset=28, 4+28 = 32 → fHeight目前的MemberOffset=32

  3. 由于 32 % 4 = 0 满足 MemberOffset % MemberAlig == 0 所以无需做额外变化

    所以fHeightMemberOffset = 32

    所以fHeight被装载到Offset = 32的位置

第一大步骤end

第二大步骤

设结构体变量的对齐值为StructAlig

  1. StructAlig = max(sizeof(char),sizeof(int),sizeof(double),sizeof(char),sizeof(char),sizeof(float)) = max(1,4,8,1,1,4) = 8
  2. StructAlig = min(Zp,StructAlig) = min(8,8) = 8
  3. StructSize % min(Zp,StructAlig) == 0 = (32 + 4) % 8 ≠ 0 再++ → → 40 % 8 = 0

请添加图片描述

😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫 综上所述 最终大小为 40 😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫😵‍💫

  • ⚠️ 得出结论 : 第0个元素,和char型数据都是顺着排

如何定位

无论是结构体还是类的成员变量,在编译器看来都是符号化的偏移量(上面算出的Offset地址)

对于编译器,无论是 结构体 还是 类 的成员变量(成员函数不是) 都是通过首地址 + 每个成员变量不同的偏移量(MemberOffset,上述运算中的Offset) 定位的

具体做法:

struct TagType s;
  • s.member 's address is : (int)&s + MemberOffset //地址的值
  • s.member 's address type is : (MemberType *)((int)&s + MemberOffset) //地址的类型

综上所述:

s.member == *(MemberType *)((int)&s + MemberOffset)

请添加图片描述

我们可以封装一个宏来实现确认每个宏偏移地址的功能(找某个结构体s中成员变量m相对于0的偏移量)

#define GET_OFFSET(s,m) (unsigned int)(&((s *)0) -> m)

幺蛾子时刻

struct tagDataOfBirth
{

	int nYear;  +0
	unsigned char wMonth; +4
	unsigned short int cDay; +5(不可以) 需要 +6

} // sizeof(tagDataOfBirth) = 8 (6 + 2)

struct tagPerson
{
	char szName[5]; +0
	struct tagDataOfBirth dob;
	double dblWeight; +16
	char cGender; +24
	char *szPhoneNumber; +28
	float fHeight; +32

}

若在结构体中嵌套了结构体该如何计算Offset的对齐关系呢?

struct tagDataOfBirth dob;

+ 6 % min(Zp,sizeof(StructAlig))

= 6 % min(Zp,sizeof(max(sizeof(int),sizeof(unsigned char),sizeof(unsigned short int))))

= 6 % min(Zp,4)

= 6 % min(8,4)

= 6 % 4 != 0(6++直到8) 所以 Offset = +8

如何在不改变编译选项的同时改变某个结构体的对齐值

编译选项中配置的对齐值对全局生效(若结构体不配置特殊的对齐值则应用该全局生效的对齐值)

#pragma pack(push) // 保存原对齐值
#pragma pack(1)  // 此句下面的结构体的对齐值都应用pack中的对齐值:1

上述两句也可合并为一句:
#pragma pack(push,1)

#pragma pack(pop) // 恢复为原对齐值

pushpop之间的应用自定义的对齐值,其余的全应用全局的对齐值(也就是编译选项规定的对齐值)

结构体的定义方式

struct tagPerson
{
	char szName[5]; +0
	struct tagDataOfBirth
	{
		int nYear;  +0
		unsigned char wMonth; +4
		unsigned short int cDay; +5(不可以) 需要 +6
	}
		double dblWeight; +16
		char cGender; +24
		char *szPhoneNumber; +28
		float fHeight; +32

}//此时tagDataOfBirth属于私有类型的结构体

若如上述形式定义结构体则

int main()
{
	struct tagDataOfBirth dob; //错误 无法单独定义了,因为其在别的结构体中,类型为:私有

	//若想强制使用
	//就可以使用子类
	struct tagPerson::tagDataOfBirth dob
	{ 

	};
}

为了方便管理、分类管理,也可以专门定义一个上层外部的struct,这个struct中什么成员变量都不定义,其余与其相关的
struct都定义在这个大的struct中,用上述方式初始化其中的某个子struct即可

struct MyErrorInfo
{
	struct tagFileInfo
	{
	};
	struct tagDiv0Info
	{
	};
}
//struct MyErrorInfo::tagDiv0Info div0; 
//类似于C++中namespace的概念

一定要定义一个私有的,怎么也无法从外部访问,则去掉内部结构体的名字

struct tagPerson
{
	char szName[5]; +0

	struct //编译器会为其取一个粉碎后的名字,该名称不对外
	{
		int nYear;  +0
		unsigned char wMonth; +4
		unsigned short int cDay; +5(不可以) 需要 +6
	}dob;

		double dblWeight; +16
		char cGender; +24
		char *szPhoneNumber; +28
		float fHeight; +32
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值