目录
1.结构体类型的声明
1.1结构体
C语言已经提供了内置类型,如:char, short, long, int, float, double等,但是只有这些内置类型还是不够的,假设我想描述学生,描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义的数据类型,让程序员可以自己创造合适的类型。
结构是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至其他结构体。
1.2结构的声明
1.2.1一般声明
例如描述一个学生:
struct Stu
{
char name[20];//姓名
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
1.2.2 结构的特殊声明
在声明结构的时候,可以不完全声明。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
上面两个结构体在声明的时候省略掉了结构体的标签(tag)
那么如果我们将x的地址传入p指针,即
p = &x;
在上面代码的基础上,这句代码合法吗?
警告:
- 编译器会把上面的两个声明当成完全不同类型的两个,所以是非法的。
- 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
匿名结构体可以使用typedef创建名字。
例如:
#include<stdio.h>
typedef struct
{
char c;
int i;
double d;
}S;
int main()
{
S s;
return 0;
}
不过,既然我们想用匿名结构体,我们现在又给它创建了名字,这样意义就不大了,匿名结构体在使用时基本上就是只使用一次。
2.结构体变量的创建和初始化
#include<stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三",20,"男","20231221130" };
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 = "小赵",.id = "2023122265",.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;
}
输出的结果是:
3.结构体内存对齐
3.1对齐规则
首先得掌握结构体的对齐规则:
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
(注:VS中默认对齐数是8;
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小)
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
那我们来做几道练习来试试看吧!
#include<stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
在我们不了解对齐数的规则的时候,可能得出来的答案是6。
因为我们知道char类型占1个字节,int类型占4个字节,得出整体是6个字节,不过这样计算真的对吗?我们运行起来看一下。
那为什么是12呢?
- 我们第一个成员char类型应该对齐到和结构体起始位置偏移量为0的地方,我们存下char类型的一个字节。
- 接着我们要存int类型时,发现规则中说变量要对齐到对齐数的整数倍地址处,int类型本身的大小是4,而VS中默认对齐数是8,显然4是较小值,所以int类型成员的对齐数是4,我们应该找4的倍数,所以会浪费掉3个字节的空间(标红的地方),在偏移量为4的地方开始存int类型成员。
- 当我们存完4个字节的int类型成员时,接着要存char类型,char类型的大小是1,默认对齐数是8,所以char类型成员的对齐数是1,我们在偏移量为8处存下char类型成员。
- 所有成员变量都存完后,是9个字节大小,但是最终的结果不是9而是12,这就用到了规则中所说“结构体的总大小是最大对齐数的整数倍”,我们发现结构体成员的最大对齐数是4,最终结构体总大小应该是4的倍数,所以是12。
那么此时如果我调换一下成员的顺序,对输出结果的大小又有什么影响呢?
#include<stdio.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
程序运行起来我们发现结果是8,不是12,这是为什么呢?
- 先存储两个char类型的成员,由于对齐数都是1,所以没有空间的浪费。
- 当我们在存int类型成员的时候,我们知道int类型成员对齐数是4,不可以接着从偏移量为2的地方开始存,我们需要找到4的倍数,应该是从偏移量为4处开始存放int类型的成员变量。
- 存完int类型成员,整个结构体总大小刚好为8个字节,是最大对齐数4的整数倍,所以结果输出为8。
那么当我们遇到嵌套结构体的问题又该如何解决呢?
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}
先来看看运行的结果:
首先我们先计算一下结构体S3的大小:
数一下得出的结果是16个字节(double类型占8个字节)具体过程这里就不详细说明了,可以看看规则或者再看看上面两道题,应该这个就问题不大了。
重点我们看嵌套一个结构体的S4它的大小为什么是32个字节呢?
- 我们先在偏移量为0的地址处存下char类型的成员。
- 接着重点来了,我们该如何存储S3结构体呢?由上面的计算可得S3的大小是16个字节,而默认对齐数是8,较小值为8,所以S3的对齐数是8,接着我们找到偏移量为8的倍数的地方(也就是8),将S3存在偏移量为8的地方,往下存16个字节。
- 存完S3结构体后,接着要存double类型的成员,而接下来的偏移量正好是8的倍数24,最后的结果是32,是最大对齐数8的倍数,所以输出32。
3.2为什么存在内存对齐?
大部分参考资料都是这样的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,我们可以采取:
让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
S1占了12个字节,S2占了8个字节,明显S2更节省空间。
3.3修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
#include<stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
默认对齐数改为1后,程序运行的结果就变成了:
差不多就讲到这里啦。