一,结构体类型简介
在设计程序时,最重要的步骤之一就是选择表示数据的方法,下面给出七种C语言常见的的内置数据类型:
数据类型 | 类型标识符 |
---|---|
字符型 | char |
短整型 | short |
整型 | int |
长整型 | long |
长长整型 | long long |
单精度浮点型 | float |
双精度浮点型 | double |
而在许多情况下,内置的数据类型并不能满足我们的需求。在实际应用中,我们经常需要由不同类型的数据来构成一个整体:比如描述一个人,把他看成一个整体他可以由 年龄 ,身高,名字 , 等数据组成而想要表示这些数据,你可能会想到使用数组,但这是通常是行不通的,因为数组里面存放的都是相同类型的数据,而这些数据都拥有不同的类型,年龄可以是整形,身高应该是浮点型,而名字应该是字符型。
为此,C语言提供了一个自定义类型:结构体类型,它可以包含各种类型的数据,用来描述这类较复杂的问题。
二,结构体的创建和初始化
1,结构体的创建
声明方式如下:
struct tag{ //用花括号括起来的是成员列表;
member1;
member2;
...
}variable-list;
其中:
- struct:C语言中的关键字,表示结构体的声明。
- tag :结构体的名字,也就是 结构体标识符
- member1: 和member2一样,他们都是这个结构体里面的成员,可以是C语言里边的任意一种内置数据类型,当然,也可以是其他的结构体变量。
- variable-list:结构体变量名。
在声明以上结构体时,也可以不写 variable-list 而在后面的程序中写上:
struct tag variable-list;
显然,这和我们用的内置类型变量命名方式一样。
2,初始化
我们可以试着用结构体描述一个学生,这里还是给出一个例子:
struct Stu{
char name[20]; //名字
int age; //年龄
float weight; //体重
};
int main() {
//为其命名和赋值
struct Stu Student={"zhangsan",19,120};
打印方法如下(用 变量名 . 结构体成员名 来访问):
printf("名字:%s\n", Student.name);
printf("年龄:%d\n", Student.age);
printf("体重:%f", Student.weight);
输出结果:
三,结构中存在的内存对齐问题
既然结构体变量也是一个变量,那么它也就可以用操作符 sizeof 来计算所占的字节长度。
如以下代码:
struct S1 {
char c1;
char c2;
int a;
}s1;
int main() {
printf("%d\n", sizeof(s1));
return 0;
}
那么,这个输出结果是怎么的呢?
因为结构体的大小就是结构体中各个成员大小的总和,计算结构体的大小时,要将每个成员的大小相加,所以可能会想到输出的结果:6
然而,事实并非如此!!!
这就是它的输出结果:
可以看到,在VS2022,X86环境下,它输出的结果为 8 ,而不是我们所期望的 6 。
这是因为结构体的大小计算通常遵循一些规则,这取决于编译器和系统的规范,以确保内存布局是有效和紧凑的。
结构体的大小通常按照以下规则计算:
成员对齐(Member Alignment): 结构体中的每个成员都有一个对齐要求,这个要求通常是基于成员的数据类型。例如,整数通常要求4字节对齐,双精度浮点数通常要求8字节对齐。编译器会确保结构体的每个成员按照其对齐要求排列,以保证访问成员时的效率和可靠性。编译器也存在默认对齐数,VS2022中,这个对齐数的大小是 8 ,而某个成员的对齐数=编译器默认的一个对齐数与该成员大小的较小值。
填充字节(Padding Bytes): 由于对齐要求,结构体中的成员可能会导致一些填充字节的添加,以确保成员按照正确的边界对齐。这些填充字节通常不存储任何数据,只是用于对齐。填充字节的大小取决于编译器和体系结构。
计算机由低地址向高地址存放数据,于是根据上述代码,可以画出以下图:
可以看到,这个结构体所占的字节是 8 。计算机在存放整形 a 时,跳过了两个字节,放在了后面,这就是因为 成员对齐 而跳过的那两个字节,通常不存储任何数据,也就是造成了内存浪费,这就是因为 填充字节
而还有很重要的一点是,结构体中创建成员的先后顺序
比如我把上面的代码改一下,再加一个结构体 S2 :
struct S1 {
char c1;
char c2;
int a;
}s1;
struct S2 {
char c1;
int a;
char c2;
}s2;
int main() {
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
return 0;
}
其运行结果:
画出 S2 在内存存放的方式:
这里需要提到的是,除了 1,2,3块空间被浪费了,9,10,11空间也同样被浪费了!!!这是因为结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。当c2存放过后,已经有了9个字节,而还要凑够 int 大小的整数倍 12 ,于是又填充了三个空字节。
这里提一句 #pragma pack 指令: 一些编译器允许使用 #pragma pack 指令来控制结构体的对齐方式。这可以用来改变默认的对齐规则,以便更好地满足特定需求。但也需要谨慎使用,因为它可能会导致不同平台上的不一致性。
可见在创建结构体时, 不正确的对齐或没有考虑对齐规则可能导致内存浪费,因为填充字节会占用额外的内存空间。这可能会影响数据结构的大小和内存使用效率。
既然内存对齐会导致空间浪费,那为什么要内存对齐?
因为它能够提高系统的整体性能。主要原因如下:
1. 提高数据访问效率
大多数现代CPU在访问内存时,都是以字(Word)为单位进行的。如果数据没有对齐,CPU就需要做两次内存访问操作,效率会降低。通过对齐,CPU可以一次完成整个数据的读写,提高了访问效率。
2. 避免CPU错误 有些CPU对未对齐的内存访问是不被允许的,会引发硬件异常或系统崩溃。进行对齐可以避免这种情况发生。
3. 提高并行计算能力
当多个线程同时读写相邻的内存地址时,如果没有对齐,就可能产生冲突,需要加锁等机制进行同步。对齐后,不同数据占用不同的缓存行,可以减少同步开销,提高并行计算能力。
4. 优化缓存利用率
由于CPU读取数据时是以缓存行为单位,如果多个变量在同一缓存行中,则只需要加载一次,可以提高缓存命中率。对齐后,不同变量更容易落入不同的缓存行。
这就是结构中存在的内存对齐问题。