结构体是一种重要的数据结构,它就像一个数据容器,可以包含各种类型的成员,比如整型、浮点型、字符串、数组等,是日后学习顺序表链表等重要知识的基础。
目录
结构体的创建和声明
创建结构体
假设我们需要使用一种数据存储学生信息,包括名字、学号、年龄、学号,但是这些数据类型既包括了字符串,又包括了整型,无法使用一个单纯的数组进行存储,这个时候就需要用到结构体。
struct stu
{
char name[20];//名字
int age;//年龄
char sex[5];//学号
char id[20];//学号
};
上述代码就是对这个数据类型的创建 ,stu称为结构体的标签,在花括号内的变量name、age、sex、id称为结构体的成员
结构体初始化
结构体成员的初始化需要按照顺序进行
struct Stu s = { "张三", 20, "男", "20230818001" };
也可以指定成员初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
结构体声明
普通声明
如果需要声明该结构体,可以在第二个花括号后加上结构体名称
struct stu
{
char name[20];//名字
int age;//年龄
char sex[5];//学号
char id[20];//学号
}stu_infor;//结构体名称
stu_infor = { "张三", 20, "男", "20230818001" };
除此之外,可以将该结构体类型使用typedef进行重定义,效果就是可以像int一样使用
typedef struct stu
{
char name[20];//名字
int age;//年龄
char sex[5];//学号
char id[20];//学号
}student;//数据类型的名称
student s2 = { "张三", 20, "男", "20230818001" };
特殊声明
在声明结构体时,可以不完全声明
struct
{
int a;
char b;
float c;
}x;
这个结构体声明时就省略了结构体标签。需要注意的是,这样的结构体如果没有对结构体类型进行重命名的话,只能使用一次,例子如下
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
p = &x;
在上面这个代码中的两个结构体声明会被编译器当成两个完全不同的类型,因此 p = &x 是非法的
结构体的自引用
当定义一个链表的节点时,除了包括链表的数据外,还需要包含节点的指针,此时我们定义的结构体就自引用了
struct Node
{
int date;
struct Node* next;
};
但是下面这个自引用是错误的
struct Node
{
int data;
struct Node next;
};
因为结构体中包含了一个同类型的结构体,此时结构体的大小就会变为无穷大,显然是不合理的
再比如下面这个结构体
typedef struct
{
int data;
Node* next;
}Node;
但这个结构体重定义时 ,在匿名结构体内部提前使用了Node来创建成员变量,显然也是非法的。
所以我们定义结构体时就不用匿名结构体。
typedef struct Node
{
int data;
struct Node* next;
}Node;
结构体内存对齐
对齐规则
1. 结构体第一个成员对齐到结构体起始位置偏移量为0的位置
2. 其它成员变量对齐到某个数字(对齐数)的整数倍的地址处
对齐数等于编译器默认的一个对齐数与该成员变量大小的较小值
VS编译器中默认是0,Linux中gcc没有默认对齐数,最小对齐数就是变量大小
3. 结构体总大小等于最大对齐数(每个成员都有一个对齐数,最大对齐数就是这些对齐数中最大的)的整数倍
4. 如果嵌套了结构体成员,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包括嵌套的结构体)的整数倍
补充:偏移量就是相对于某一个地址偏移的字节数,可以是负数
struct s1//假设s1的地址是0x0012ff30
{
char c1;
int i;
char c2;
};
int main()
{
struct s1 s1 = {'a',100,'b'};
printf("%d",sizeof(s1));
return 0;
}
创建的结构体s1地址为0x0012ff30,结构体第一个成员c1的地址对齐到结构体起始地址偏移量为0的位置,就是0x0012ff30。
成员i的大小为4个字节,vs的默认对齐数为8,4<8,所以i的对齐到结构体起始位置偏移量为4的整数倍的位置,在这个结构体中由于成员c1只占了一个字节,后面的三个字节都是空闲的,所以i对齐到0x0012ff34。int大小为4个字节,所以0x0012ff34后面4个字节都是i的空间。
同理,成员c2最小对齐数为1,对齐到0x0012ff38
在结构体s1中,成员的对齐数的最大值就是i的对齐数4,所以结构体s1的大小就是4的整数倍,因为三个成员变量占据了9个字节,8个字节无法包含全部成员变量,所以s1的大小是12个字节。
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
struct s2 s2 = {'a',100,'b'};
printf("%d",sizeof(s1));
return 0;
}
在s2中c1同样对齐s2的起始地址,c2的最小对齐数为1,应该对齐到0x0012ab41,i最小对齐数为4,那么c2后的两个字节的空间就会空置,i对齐到0x0012ab44。
s2的最大对齐数为4,s2的大小应该是4的整数倍,这里8个字节已经可以包含全部成员,所以s2的大小为8
再看一下嵌套的结构体
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct s3 s3 = {0.2,'a',4};
struct s4 s4 = {'b',s3,0.5};
printf("%d %d",sizeof(s3),sizeof(s4));
return 0;
}
在结构体s3中,最大对齐数为8,大小就是16个字节
s4的第一个成员c1对齐起始地址;第二个成员嵌套了s3,大小为16个字节,s3的成员中最小对齐数是8,因此s3的最小对齐数为8。所以c1后面7个字节的空间都空置,s3对齐到0x0034ab68;第三个成员d的最小对齐数为8,对齐到0x0034ab84。
s4的大小是s4成员中最大对齐数的整数倍(包括s3中成员的最大对齐数),这里最大对齐数就是8,而大小就刚好是32.
需要注意的是,上面初始化的s3和s4中的s3成员不是占用同一空间,可以视为相同数据的两个变量,既将s3的值拷贝到s4的第二个成员中。
为什么存在内存对齐
1. 平台原因
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
比如说只能在4的整数倍的地址处读取整型数据,只能在8的整数倍地址处读取double型数据
2. 性能原因
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
以上面的s2为例, 上图左侧是没有内存对齐的s2,右侧是内存对齐的s2。现在假设计算机一次读取4个字节,那么读取第三个成员i的时候就需要读取两次才能完全读取数据。而在对齐的s2中就可以直接读取i的数据。
总的来说,结构体就是以空间换时间的做法。
那么在设计结构体过程中,能否既满足结构体对齐,又节省空间呢?答案是可以的,只需要将占用空间小的成员尽量放在一起
struct S1
{
char c1;
int i;
char c2;
};
//优化后
struct S2
{
char c1;
char c2;
int i;
};
优化前的结构体大小为12个字节,优化后是8个字节
修改默认对齐数
使用#pragma这个预处理指令,可以改变编译器的默认对齐数。
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认