这章我总结了下结构体的内容,包括结构体的声明,结构体变量的创建和初始化,结构体内存对齐,结构体实现位段等,那么话不多说 开始正文。
目录:
1.结构体的声明; 2.结构体对齐规则; 3.结构体传参; 4.结构体实现位段;
1.结构体的声明:
结构体就是一些值的集合,这些值就是结构体的成员变量,它们可以是不同类型的变量。 下面的这个代码就是结构体的声明
struct tag
{
member-list;
}variable-list;
比如描述一个学生:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
1.1.结构体变量的创建和初始化: 先声明一个结构体我们就可以创建对象了,初始化方法不唯一。 我们可以按照顺序输入,第一个输name的值,第二个输age的值...... 我们也可以随心输,这时候就需要指定输入,在成员变量前加 “ . ” ,.age=18,.name="list"......
#include <stdio.h>
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;
}
1.2结构体的不完全声明:
我们在声明结构体时还可以有种特殊的声明:匿名。顾名思义,我们可以不加结构体的标签。
如上图所示,p=&s不通过,为什么呢?这是因为p和s的数据类型不同,虽然看起来好像就是一个啊,但是没名字的话,编译器会把两个声明当作两个不同的类型,所以非法报错。由此我们也可以看出匿名结构体如果没有重命名的话,只能使用一次(声明的时候就在后面创建好要用的对象,后面可以直接用),毕竟你创建对象的时候都不知道怎么表示结构体类型。
1.3结构体的自引用:
这里感觉涉及到了链表,栈,二叉树等数据结构的一开始创建结构体的内容(我之前学他们不知道结构体为什么要这样弄,好吧是我笨)。
比如创建一个链表的节点:
struct Node{
int a;
struct Node next;
};
那么上述的代码是正确的吗?我们可以从结构体大小来判断,你能判断出它的大小吗,应该是算不出个所以然的,因为结构体里还有个结构体,结构体里还有个结构体,一直套娃下去,那就是无穷了。那我们怎么定义呢?
struct Node{
int a;
struct Node * next;
};
正确的自引用应该如上。(指针的大小是确定的,一般是4/8字节,根据机器来定)。
但在自引用中还会出现一种情况,如果我们用的是匿名结构体,你会发现编译器报错。
struct
{
int a;
struct Node * next;
}Node;
上面这种写法是肯定错误的,起码的从上到下的顺序你得有啊,编译器肯定先进到struct里面再到后面的Node,那先访问到了struct Node它就会报错,Node是谁?,不知道,根本不知道,这就是报错的原因,那么我们有什么避免这个问题出现的做法吗,其实很简单,在自引用时不使用匿名结构体了,从源头杜绝。
2.OK,我们简单的了解了结构体的声明等内容,现在聊点深入的知识,结构体的大小,涉及到的热门考点:结构体内存对齐。
2.1结构体对齐规则:
1.结构体的第一个成员变量对齐到结构体变量初始位置偏移为0的地址处。 2.结构体的其余成员变量对齐到各自的对齐数(编译器的默认对齐数与该成员变量大小的较小值)的最大整数倍。(VS里的默认数是8,如果编译器没有默认数,对齐数就是成员变量大小) 。 3.结构体的整体大小是所有成员变量的对齐数的较大数的整数倍。 4.结构体如果嵌套了别的结构体,那么嵌套的结构体成员就对齐到自己成员变量中最大的对齐数的整数倍处,结构体的整体大小是所有成员变量(包括嵌套的结构体成员的对齐数)的对齐数的较大值的整数倍。 上面的四条对齐规则要记住。 我们看一个题目,大家可以先计算一下。(VS环境) char变量大小是1,根据规则1,第一个成员变量存放在初始位置偏移量为0的地址处
再看第二条规则,其余变量如何对齐。int是4,小于8,对齐数是4,将i存放在4的整数倍的地址处
还有个c2,它的对齐数也是1,直接接在i的后面就行了,最后根据规则3整体大小是c1,i,c2之间对齐数最大的整数倍(此时不包括默认对齐数了,只包括成员变量),也就是i的对齐数,4。此时已经有9个字节了,所有整体大小是12字节。这类题大抵就是上述解体思路。还有几个练习放在这里,大家要是不熟的话可以解一下,不会的话可以问我。
2.2为什么会出现内存对齐呢?
1.平台原因:不是所有硬件平台都可以随便的访问任意地址的任意数据的,有些平台只能在某些位置访问特定的类型数据,否则抛出异常。
2.性能原因:数据结构应该尽可能的在自然边界上对齐,原因在于对于未对齐的内存,处理器需要访问两次,而对于对齐了的内存,处理器只用访问一次。假如一个处理器总是访问8个字节,那么地址就必须是八的倍数。如果我们能确保double类型的数据的地址都对齐在8的倍数处,就可以用一个内存操作来读或写了,如果不能,那么一个数据可能被分在两个8字节的内存块里,那我们就要访问两次才能得到它。
总的来说结构体内存对齐就是空间换时间(因为上面我画的那两幅图里,有几个空间就被浪费了,可以回去看下)。
那有没有既满足对齐又节约空间的方法呢?回答是有方法可以尽可能的节约空间,就是把占用空间小的成员变量集中在一起,就比如上面的c1,c2,弄完c1,直接接上c2,就可以把那个浪费的空间给用了,而且还不影响到i,最后整体大小是8,这就节省了很多空间了(相较于12字节来说)。
2.3修改默认对齐数
#pragma这个预处理指令可以改变编译器的默认对齐数 用完之后想把对齐数还原就在结构体后面加上#pragma pack()。
3.结构体传参:两种传参方式,一种是传结构体,一种是传结构体的地址。 这两种方式哪种好呢?答案是传地址,原因往深的说是因为函数传参的时候,参数需要压栈,会有时间和空间上的系统开销,而且传结构体时,如果传的结构体太大了,那传的时候时间空间开销肯定更大啊,性能也就降低了。那往浅的说,传地址可以改数据,比单传拷贝的数据肯定能干些更多的事是吧,综上所述,总结为一句话,结构传参的时候,能用传地址就用传地址。
4.结构体实现位段:位段的声明和结构的声明是类似的,有两个地方不是一样的 第一个是:位段的成员变量类型必须是int,unsigned int,signed int,在C99里位段还可以有别的类型的成员变量。 第二个点是:位段的成员变量后面要加一个冒号和数字。后面跟的数字意思就是只给a两个bit的内存,那么a的值只可能是0,1,2,3(00,01,10,11)比如:
struct s
{
int a:2;
int b:8;
int c:30;
};
s就是一个位段类型,那么我们想一下这个位段占的内存大小是多少?是2+8+30,5字节吗,这是不是太直接了一点。我们接下来探讨下位段的内存分配
4.1位段的内存分配:位段的成员可以是int,unsigned int,signed int,char(前面没有说的很绝对不会出现)。位段的空间上一般是以4字节或者1字节来开辟的。(所以不能单纯把数字都加在一起)。位段涉及很多其他因素,位段是不跨平台的,所以可移植的程序别用位段。
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
这题我先阐述一下思路,空间先开开辟一个字节用着,把a=10存入,用3bit空间(1010,所以存入010),还剩5个bit,b只用4个,还够用,把b存入,用4bit空间(1100),还剩1bit,不够存入c的数据,空间再开辟一个字节空间,然后从新的字节空间里存入c,存完还剩3bit,不足以存放d,空间开辟一字节空间,把d存入,所以最后这个位段占的内存大小是3字节。(这个例子里是按照内存从右向左分配的)。
4.2位段的跨平台问题:
1.int位段没有明确的定义为有符号或者无符号 2.位段的最大位的数目不能确定,16位机器里是16,32位机器里是32,那么27在16位机器里是错误的 3.位段里成员在内存里的分配是从左到右还是从右到左没有明确的定义 4.当结构包含两个位段成员,那么较大的成员存放后,如果剩余的空间无法容纳较小位段的成员时,是舍弃剩余的空间还是利用该空间,这也是不确定的。
位段相比与结构体,在具有相同效果的同时,还能节省空间,但是要注意位段的跨平台问题。
4.3位段的应用:
说了那么多位段的知识,肯定有朋友会想,位段能干什么,它的用处也挺大的。下面的图是网络协议里,IP数据报的格式,我们可以看出很多的小属性只用到4个bit位就能描述,这时候使用位段就很恰当了,实现了想要的效果,节省了空间,这样网络运输的数据报大小也会小些,更有利于运输
4.4位段的使用注意事项:
位段里经常有几个成员变量在一个字节里存放的情况,这样有些成员的起始位置不是某个字节的起始位置,这时候我们是得不到它们的地址的,因为在内存里,一字节对应一个地址,里面的bit是没有地址的,所以不能对位段成员使用取地址操作符,这样就无法用scanf给位段成员赋值了,这时候我们可以先创建一个带值变量,然后赋值给位段成员。
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的⽰范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}
讲到这里结构体与位段相关知识就差不多了,下一篇内容我会聊一下联合和枚举,okay。