既然结构是一组数据的集合,那么,结构体占用内存大小究竟该如何计算呢?例如,
#include <stdio.h>
#include <string.h>
// 结构体类型的定义
struct Student
{
char name[30];
int age;
double score;
};
int main()
{
struct Student stu = {"peter", 20, 90.5};
// 计算结构体大小
// 结果是48字节
printf("结构体变量stu占用内存大小=%d\n", sizeof(stu));
return 0;
}
struct Student 结构体内存分配大小为48字节,为什么不是结构体中每个成员所占用内存之和(42字节)呢?也就是说,结构体占用的内存大小不一定是每个成员占用内存之和。
之所以这样,主要是因为结构体内存分配遵循一定的原则:
-
顺序存储原则:结构体中的元素按照定义的顺序存放到内存中。每一个元素存入内存时,都会认为内存是以自己的宽度来划分空间的,因此元素存放的位置一定会在自己大小的整数倍上开始。这意味着,如果结构体中第一个元素的大小为n字节,那么它将从内存地址的某个n字节整数倍位置开始存放。
-
内存对齐原则:为了提高内存访问的效率,编译器通常会对结构体成员进行内存对齐。这意味着,即使某个成员只需要占用较少的内存空间,编译器也可能在其后填充一些额外的字节,以确保下一个成员从合适的内存地址开始。这种对齐方式取决于目标平台或编译器的规定,可能是某个固定大小的整数倍(如4字节或8字节)。
-
最小单元补齐原则:在计算结构体总大小时,如果计算出的总大小不是结构体中最宽元素长度的整数倍,编译器会将其补齐到最宽元素长度的整数倍。这样做是为了满足内存访问的效率和一致性。
-
结构体嵌套原则:如果结构体中包含其他结构体作为成员,那么这些嵌套的结构体成员将从其内部最大元素大小的整数倍地址开始存储。这是因为嵌套的结构体本身也遵循上述的内存分配原则。
-
动态内存分配:对于使用指针指向的结构体,实际的内存分配发生在运行时,通过动态内存分配函数(如
malloc
、calloc
等)进行。这种情况下,需要确保在使用完毕后释放分配的内存,以避免内存泄漏。
一起看看结构体内存分配遵循的具体规则:
规则1:以多少个字节为单位开辟内存
结构体变量分配内存的时候,会去结构体变量中找基本类型的成员;哪个基本类型的成员占字节数多,就以它大大小为单位开辟内存。具体遵循以下原则:
-
成员中只有char型数据 ,以1字节为单位开辟内存。
-
成员中出现了short int 类型数据,没有更大字节数的基本类型数据,以2字节为单位开辟内存。
-
出现了int float ,没有更大字节的基本类型数据,以4字节为单位开辟内存。
-
出现了double类型的数据,以8字节为单位开辟内存。
算一算以下结构体占用内存大小,代码示例:
#include <stdio.h>
#include <string.h>
// 结构体类型的定义
struct Student
{
int age;
double score;
};
int main()
{
struct Student stu = {20, 90.5};
// 计算结构体大小
// 结果是48字节
printf("结构体变量stu占用内存大小=%d\n", sizeof(stu));
return 0;
}
注意,
-
如果在结构体中出现了数组,数组可以看成多个变量的集合。
-
如果出现指针的话,没有占字节数更大的类型的,在32位系统下,以4字节为单位开辟内存;在64位系统下,以8字节为单位开辟内存。
规则2:字节对齐
-
char 1字节对齐 ,即存放char型的变量,内存单元的编号是1的倍数即可。
-
short int 2字节对齐 ,即存放short int 型的变量,起始内存单元的编号是2的倍数即可。
-
int 4字节对齐 ,即存放int 型的变量,起始内存单元的编号是4的倍数即可。
-
long int 在32位平台下,4字节对齐 ,即存放long int 型的变量,起始内存单元的编号是4的倍数即可。
-
float 4字节对齐 ,即存放float 型的变量,起始内存单元的编号是4的倍数即可。
-
double
-
vc6.0和Visual Studio环境下,8字节对齐,即存放double型变量的起始地址,必须是8的倍数,double变量占8字节。
-
gcc环境下,4字节对齐,即存放double型变量的起始地址,必须是4的倍数,double变量占8字节。
-
代码示例:
#include<stdio.h>
struct Student
{
// 成员列表
char mem1;
short int mem2;
int mem3;
};
int main()
{
struct Student stu;
printf("%d\n",sizeof(stu));
printf("%p\n",&(stu.mem1));
printf("%p\n",&(stu.mem2));
printf("%p\n",&(stu.mem3));
return 0;
}
将上述代码调整一下:
#include<stdio.h>
struct Student
{
// 成员列表
char mem1;
int mem2;
short int mem3;
};
int main()
{
struct Student stu;
printf("%d\n",sizeof(stu));
printf("%p\n",&(stu.mem1));
printf("%p\n",&(stu.mem2));
printf("%p\n",&(stu.mem3));
return 0;
}
最后,回到本小节一开始的一段代码:
#include <stdio.h>
#include <string.h>
// 结构体类型的定义
struct Student
{
char name[30];
int age;
double score;
};
int main()
{
struct Student stu = {"peter", 20, 90.5};
// 计算结构体大小
// 结果是48字节
printf("结构体变量stu占用内存大小=%d\n", sizeof(stu));
return 0;
}
扩展,结构体为什么需要内存对齐?基于以下几点原因:
-
平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据。某些硬件平台只能在特定的地址处访问特定类型的数据,否则可能会抛出硬件异常。因此,内存对齐可以确保结构体中的数据能够被这些平台正确且有效地访问。
-
性能原因:为了提高数据访问的效率,数据结构(尤其是栈上的数据)应该尽可能地在自然边界上对齐。这是因为处理器访问未对齐的内存需要进行两次内存访问,而对齐的内存访问则仅需要一次。因此,内存对齐可以减少处理器的访问次数,从而提高数据访问的速度。
此外,内存对齐还有助于简化内存管理,因为对齐的内存块更容易进行分配和释放。同时,对于涉及指针运算或内存操作的代码,内存对齐也可以减少出错的可能性。