【C语言】自定义类型——结构体
目录
一、结构体的类型的声明
形式如下:
struct tag
{
member - list; // 成员列表
}variable - list; // 变量列表,属于全局变量,也可以没有
例如,定义一个学生结构体类型,并创建了全局变量s1:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1;
二、结构体变量的创建和初始化
// struct Stu 类型的定义
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
"⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
三、匿名结构体类型
省略掉结构体标签 (tag):
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
因为匿名结构体类型没有名字,所以如果没有对匿名结构体重命名的话,只能使用一次(创建一次变量,即声明结构体类型的时候就创建)。并且两个成员相同的匿名结构体类型不相同,如下:
四、结构体自引用
结构体中不能包含同类型的结构体成员。因为结构体类型还没完全声明结束就开始使用同类型是不行的(不清楚它的大小),相当于一个类型还不存在的时候就开始使用这个类型,并且仔细想想,这样声明的结构体的大小是无穷大的,如下:
如果结构体想自引用同类型,只能定义为指针类型。指针类型是内置类型,本来就存在,大小也可知(4 或 8字节),如下:
struct Node
{
int data;
struct Node* next;
};
如果是对匿名结构体重命名,就算是包含同类型的指针类型,也是不行的,因为在重命名 Node 之前都还不知道这个匿名函数叫啥,就使用 Node 的指针,如下:
因此,结构体自引用不能使用匿名结构体,改为如下就正确了:
typedef struct Node
{
int data;
struct Node* next;
}Node;
五、结构体内存对齐
是计算结构体大小的规则。
(1)对齐规则
① 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
② 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
VS 中默认的值为 8。
Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小。
③ 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的) 的
整数倍。
④ 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
(2)计算结构体大小练习
练习一:
结构体S1的大小:
结构体S2的大小:
S1 和 S2 类型的成员一模一样,但是 S2 比 S1 占的空间更小,是因为变量 c1 和 c2是放在一起的。因此,让占用空间小的成员尽量集中在一起,更节省空间。
练习二:
结构体S3的大小:
结构体S4的大小:
(3)需要内存对齐的原因
① 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,比如 int 类型数据只能在固定地址处存取,否则抛出硬件异常。为了提高代码的可移植性(对所有硬件平台都适用),需要内存对齐。
② 性能原因
访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。比如对于结构体 s:
struct s{
char c;
int i;
};
如果内存不对齐:
如果内存对齐:
32位机器上,数据总线是32根,读、写数据的时候,一次就读/写32位(4个字节)。如果不对齐,要读两个字节,才能拼凑出 i;如果对齐,发现第一个字节没有,直接跳到第二个字节,读取一次就可以得到 i 。因此,内存对齐更能节省读取时间(用空间换时间)。
(4)修改默认对齐数
使用 #pragma 预处理指令,示例:
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//还原为默认对齐数
对齐数通常是 2 的 x 次方,如 1,2,4,8,不能随意设置。
六、结构体传参
一种是传结构体本身,一种是传结构体的地址,如下:
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
因为函数传参,参数要压栈,会有空间和时间上的系统开销。如果结构体比较大,传结构体本身,开销就比较大;如果传结构体地址,指针只有 4 个字节,开销就比较小。因此,结构体传参,最好传地址。
七、结构体实现位段
(1)什么是位段
与结构体类似,但有两个不同:
- 成员类型必须是 int、unsigned int、signed int、char,C99 标准中也可以是其它类型。
- 成员名后是 冒号 + 数字。
形式如下:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
位段 A 大小是?如果按照结构体的内存对齐的方法计算,4 * 4 = 16 个字节。看看运行结果:
显然不是按结构体的方式计算内存大小,实际上位段的位表示二进制位,冒号后面的数字是该成员的大小,比如 _a 占 2 bit 。但是位段 A 的所有成员的大小加起来是 2+5+10+30 = 47,用 6 个字节(68 bit)就够了,为什么是8 字节呢?请看下节。
(2)位段的内存分配
- 位段每次开辟 4 个字节(int)或者 1 个字节(char)。
- 位段的不确定因素很多(比如每次开辟从左还是右存储;每次开辟的空间不够下一个成员使用,剩余的空间要不要接着使用),不可跨平台,注重可移植性的代码要避免用位段。
如下例子:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
printf("%d\n", sizeof(s));
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
VS中的规则:
- char 类型,每次开辟一个字节。
- 一个字节内,从右向左使用。
- 一个字节内剩余的 bit 不够下一个成员使用,浪费掉并开辟新的一个字节存放。
定义结构体变量 s 并初始化位段成员值为 0,开辟如下的空间(因为位段是 char 类型,每次开辟 1 个字节空间):
一共是3个字节。再给所有位段成员赋值(超出的截断,不够的补0):
调试验证,每 4 bit 是一个十六进制数,那么上面的值用十六进制表示就是(62 03 04):
调试结果与理论一致:
运行结果:
解决(1)中遗留的问题:
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
因为位段成员是 int 类型,所以每次开辟 4 个字节(一共开辟了 8 个字节):
(3)位段的跨平台的问题
- int 位段被当成有符号还是无符号数是不确定的。
- 位段中最大的数目不确定。(32位机器最大位是32,16位机器最大位是16。如果是16位机器,像下面这样写就是错误的,因为 30 位已经超过了最大的 16 位;但在 32 位机器上是正确的。)
struct S
{
int a : 30;
};
- 每次开辟的空间,是从左向右,还是从右向左使用是不确定的。
- 每次开辟的空间,剩余的空间不够下一个位段成员使用时,是浪费掉还是接着使用是不确定的。
总结:位段(可以设置使用的位)比结构体(固定的字节)更节省空间,但存在跨平台的问题。
(4)位段的应用
数据在网络上传输,需要遵守网络协议,网络协议中有个IP数据报的概念(相当于快递包裹上的各种邮寄信息,有发件人、收件人,才知道包裹从哪发、发给谁),下面就是IP数据报的格式:
如果不用位段,版本(4位)是整型,分配 4 个字节空间,就会浪费 28 位空间。可以发现每一行信息需要的空间加起来刚好是 32 位(4 个字节),刚好是一个整型的大小,设计成位段,将会没有一点空间浪费。
IP数据报追求节省空间,因为使用更小的空间,网络越通畅。
(5)位段不能使用取地址符&
位段中几个成员共用一个字节,而内存中是按字节编址的,所以一个字节内的 bit 没有地址,就不能对位段成员取地址,如下会报错:
因此,不能使用 scanf 直接给位段成员输入值,只能先输入值放在变量中,变量再赋值给位段成员:
原文地址:https://blog.csdn.net/2401_86272648/article/details/142149828
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!