在C语言中有几种自定义的类型,结构体就是其中一种。
结构体中包含着一些成员,这些成员都被称为成员变量,成员变量可以是许多不同的类型,比如整型,浮点型,字符类型,都可以。
结构体变量的声明和构建
#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;
}
struct Stu声明了结构体的名称,可以用这个名称去创建许多相同类型的结构体变量,下面的s就是;在结构体初始化时可以按照定义结构体成员变量的顺序去初始化,也可以在{}内不按顺序初始化,但是此时要用 . 去找到你想要初始化的成员变量(在结构体中结构体变量 加上 .就可以操作结构体成员变量,当使用结构体指针的时候用 指针 加 -> 再加 成员变量就可以操作)
#include<stdio.h>
struct Stu
{
char name[30];
int age;
char sex[5];
char id[20];
};
int main()
{
struct Stu s = { "zhangsan",20,"男","20231120" };
struct Stu* p = &s;
printf("%s %d", p->name, p->age);//利用结构体指针也能操作
return 0;
}
结构体还存在特殊声明:
匿名结构体
struct
{
int a;
char b;
}s;//没有名称,直接定义了一个变量s,这种不完全声明的结果就是该结构体只能用一次
想要重新让这个结构体能够定义多个变量的时候,我们这样写:
typedef struct
{
int a;
char b;
}S;//此时这个大写的S就不是变量了,它成了结构体名称,用这个名称可以多次去定义结构体变量
int main
{
S s={0};
S h={0};
}
结构体的自引用
顾名思义,自引用就是自己用自己,相当于嵌套,可能最开始你会这样想:
struct Node
{
int data;
struct Node next;
};
这种写法是错误的,首先任何一个变量都有大小,结构体也不例外;但是这样写,当你判断这个结构体大小的时候,是套娃模式地无数个int相加,算不出大小;
正确写法:
struct Node
{
int data;
struct Node* next;
};
无论next指向的指针如何,此时我们都可以确定这个结构体有大小,因为指针的大小是固定的。
结构体大小的计算
上面提到了结构体的大小,在这里我们详细来说一下结构体的大小要怎么计算;
这里首先把有关计算结构体大小的概念列出,之后会逐一解释
相关概念:
1、结构体中第一个成员变量对齐到偏移量为0的地址处;
2、其他成员变量对齐到它自己对齐数的整数倍的偏移量处;
3、一个成员变量的对齐数是这个成员变量的大小和当前所使用的编译器默认的对齐数的较小值;
4、结构体最终的大小是所有成员变量中最大的对齐数的整数倍;
5、若是结构体中嵌套了结构体,那么这个嵌套在里面的结构体也遵循其他正常成员变量的对齐规则,它的对齐数是它自己的成员变量中的对齐数的最大值;最终要计算的结构体大小任然是最大对齐数的整数倍(嵌套在里面的结构体当成一个成员变量去看待)。
不必纠结,我们一起来看具体解释去弄懂这些概念:
首先引入一个例子:
#include<stdio.h>
struct S1
{
char i;
int j;
char l;
};
int main()
{
printf("%d", sizeof(struct S1));//打印出结构体大小的具体值
return 0;
}
为什么是12呢?
具体分析:首先第一个成员变量是char型的,放在这个结构体开辟的内存的偏移量为0的地址的看空间,那么就占了一个字节(对应概念1);
后面的int j和char k都是其他成员变量,那么它们的偏移量都应该是各自对齐数的整数倍,这里使用的是VS,系统默认对齐数是8,对于int来说它的对齐数就是4,那么对应的偏移量就是4,在图中我们可以看到就是这样;同样的,char k对齐数是1,那么它就紧接着int j的内存位置存放,因为无论是除了最开始之外的哪个地方的偏移量都是1的整数倍(对应概念2、3);
最后总的占用了9个字节,但是结构体最终的大小是其成员变量中最大对齐数的整数倍,所以是12(节省空间,最后结果取最小的)(对应概念4)
对于结构体嵌套来说运算法则一样,只不过要先把里面的那个结构体的大小算出来(这里的运算法则也一样),这个里面的结构体的对齐数是它自己成员变量中最大的对齐数,然后这个结构体对其到偏移量为对齐数的整数倍处,随后往后占用它自己大小的字节数。
这里为了方便理解,再举几个例子:
#include<stdio.h>
struct S2
{
char i;
char j;
int n;
};
struct S3
{
double i;
char j;
int n;
};
struct S4
{
char i;
struct S3 s3;
double j;
};
int main()
{
printf("%zd\n", sizeof(struct S2));
printf("%zd\n", sizeof(struct S3));
printf("%zd", sizeof(struct S4));
return 0;
}
分析:对于S2,首先char i放入占一个字节,char j紧接着再放入;之后跳过两个字节来到偏移量为4的地址处,存放int,最后两个char占用两个字节,跳过两个字节,int占4个字节,总共占8个字节,发现是成员中最大偏移量的整数倍处,那么这个结构体的大小就是8;
对于S3:double i是首个成员变量,从偏移量为0的地址处放起,这样就占了8个字节了,偏移量指向了八,char j紧接着放入,那么就是9个字节了,这时候偏移量指向9,存放int,跳过3个字节,指向偏移量为12的地址处开始存放int n,占4个字节,最后大小就是16,正好是最大偏移量8的倍数,那么最后这个结构体的大小就是16;
对于S4:首先放入一个char,接着要存放S3,我们已经知道S3中的成员变量中的最大对齐数是8,那么在这里S3作为成员变量,它的1对齐数就是8,它的大小是16,那么这时候跳过7个字节,指向偏移量为8的地址处,开始存放S3,占用16个字节;放完后,这时候占用了24个字节,偏移量指向24的地址处;最后存放double ,这时候24这个偏移量刚好是double的对齐数的整数倍处,那么直接开始存放,最后占用32个字节,刚好是S4中成员变量中最大对齐数的整数倍,那么这个结构体最终的大小就是32.
为什么会存在内存对齐
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
默认对齐数的设置和重置
#include<stdio.h>
#pragma pack(1)
struct S
{
char i;
int j;
char n;
};
//重置默认对齐数
//#pragma pack()
int main()
{
printf("%zd", sizeof (struct S));
return 0;
}
完