结构体的介绍及定义
结构体是一种复合结构类型,可以将其视为类和对象的前身,一般定义在头文件中
结构体的功能在于用不同的数据集合描述一个实体
但是要描述一个东西,数据是说了不算的,数据大多数情况下无法明确的区分两种东西,比如我们想要描述一个人是小偷,仅仅描述这个人的身高、体重、穿着…这些数据是没用的,所以除了数据还要加上行为才能确实的区分一个东西,对于上面的例子,我们要加上这个人有偷东西的行为后,才可以确认他是小偷
这也就是类(数据+行为)和结构体(数据)的区别
- 当然,结构体中也可以加入函数指针,这里不做讨论
- 但类中的行为(成员函数),是不占空间描述的,匹配行为靠的是名称粉碎机制
结构体的设计难题
使用结构体的难点在于结构体的设计:一旦项目中的结构体设计出现问题,就是骨架的问题,会导致整个项目出问题,整个都需要推倒重来
浅说结构体的设计
现在有如下结构体:目的用于存储公民个人基本信息
请问:该定义是否合理?
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];
-
MemberAlig = min(/Zp,sizeof(char))
= min(8,1) = 1 -
MemberOffset % MemberAlig == 0
0 % 1 = 0szName[5]
处于结构体中的第一个元素→MemberOffset=0
-
由于 0 % 1 = 0 满足
MemberOffset % MemberAlig == 0
所以无需做额外变化所以szName[5]被装载到Offset = 0的位置
int nAge;
-
MemberAlig = min(/Zp,sizeof(int))
= min(8,4) = 4 -
MemberOffset % MemberAlig == 0
6 % 4 ≠ 0szName[5]
的MemberAlig
为 1 且MemberOffset=0
, 0+1*5 = 5 → nAge目前的的MemberOffset=6
-
由于 6 % 4 ≠ 0 不满足
MemberOffset % MemberAlig == 0
所以 ++→
7 % 4 ≠ 0 再++→
8 % 4 = 0所以
nAge
的实际MemberOffset = 8
(不是6)所以nAge被装载到Offset = 8的位置
double dblWeight;
-
MemberAlig = min(/Zp,sizeof(double))
= min(8,8) = 8 -
MemberOffset % MemberAlig == 0
12 % 8 ≠ 0nAge
的MemberAlig
为 4 且MemberOffset=8
, 4+8 = 12 → dblWeight目前的MemberOffset=12
-
由于 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;
-
MemberAlig = min(/Zp,sizeof(char))
= min(8,1) = 1 -
MemberOffset % MemberAlig == 0
24 % 8 = 0dblWeight
的MemberAlig
为 8 且MemberOffset=16
, 16+8 = 24 → cGender目前的MemberOffset=24
-
由于 24 % 8 = 0 满足
MemberOffset % MemberAlig == 0
所以无需做额外变化cGender
的MemberOffset = 24
所以dblWeight被装载到Offset = 24的位置
char *szPhoneNumber;
-
MemberAlig = min(/Zp,sizeof(char *))
= min(8,4) = 4 -
MemberOffset % MemberAlig == 0
25 % 4 ≠ 0cGender
的MemberAlig
为 1 且MemberOffset=24
, 1+24 = 25 → szPhoneNumber目前的MemberOffset=25
-
由于 25 % 4 ≠ 0 不满足
MemberOffset % MemberAlig == 0
所以 ++→
26 % 4 ≠ 0 再++→
27 % 4 ≠ 0 再++→
28 % 4 = 0所以
szPhoneNumber
的实际MemberOffset = 28
(不是25)所以szPhoneNumber被装载到Offset = 28的位置
float fHeight;
-
MemberAlig = min(/Zp,sizeof(float))
= min(8,4) = 4 -
MemberOffset % MemberAlig == 0
32 % 4 = 0szPhoneNumber
的MemberAlig
为 4 且MemberOffset=28
, 4+28 = 32 → fHeight目前的MemberOffset=32
-
由于 32 % 4 = 0 满足
MemberOffset % MemberAlig == 0
所以无需做额外变化所以
fHeight
的MemberOffset = 32
所以fHeight被装载到Offset = 32的位置
第一大步骤end
第二大步骤
设结构体变量的对齐值为StructAlig
StructAlig = max(sizeof(char),sizeof(int),sizeof(double),sizeof(char),sizeof(char),sizeof(float))
= max(1,4,8,1,1,4) = 8StructAlig = min(Zp,StructAlig)
= min(8,8) = 8StructSize % 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) // 恢复为原对齐值
在push
和pop
之间的应用自定义的对齐值,其余的全应用全局的对齐值(也就是编译选项规定的对齐值)
结构体的定义方式
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
}