本文重点讲解结构体的大小,以及在内存中如何对齐的。
结构是一些值的集合,这些值称为 成员变量。
结构的每个成员可以是 不同类型 的变量。
struct tag
{
member-list; //成员列表
}variable-list; //变量列表
假设我现在要定义一个学生的 结构体类型 ,包括:姓名、性别、年龄、身高
struct Stu
{
char name[20]; //姓名
char sex[5]; //性别
int age; //年龄
int hight; //身高
};
那么我们现在要拿刚刚定义的 学生结构体类型 去创建 结构体变量
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
};
int main()
{
struct Stu s1;
return 0;
}
还可以用下面这种方法创建结构体变量
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
}s2,s3,s4;
struct Stu s5;
int main()
{
struct Stu s1;
return 0;
}
此时创建的 s2、s3、s4、s5 都是创建的全局变量;
s1 为局部变量;
在声明结构的时候,可以不完全的声明。
比如在定义一个结构体时,可以把它的标签去掉;
struct
{
char c;
int a;
double d;
}sa;
int main()
{
return 0;
}
上面这种方式叫 匿名结构体类型,但是 sa 只能使用一次;
但是如果是下面这样呢?
struct
{
char c;
int a;
double d;
}sa;
struct
{
char c;
int a;
double d;
}*ps;
int main()
{
ps = &sa;
return 0;
}
sa 是一个匿名结构体,ps 是一个匿名结构体指针;
虽然它们的成员类型是一模一样的,但是编译器会认为 = 两边是不同的结构体类型,所以这种写法完全是错误的
我们思考一个问题:在结构中包含一个类型为该结构本身的成员是否可以呢?
比如这样
struct Node
{
int data;
struct Node next;
};
这种情况是绝对不可以的;
如果我们在结构中包含一个类型为该结构本身的成员,那么此时我要求这个结构体类型的大小是多少?
首先在 Node 里面有个 int 类型,然后还有个 struct Node next 类型;
那么在 struct Node next 里面还有个 int 类型,和 struct Node next 类型…等等就会形成无线循环;
所以我们只需要在存上一个结构体类型的地址就好了
struct Node
{
int data;
struct Node* next;
};
此时,我们创建的每个 Node 节点里面,既可以保存一个数值,又可以保存一个地址;
通过这个地址,就可以找到由 next 指向的下一个节点;
此时结构体的大小就可以确定了:int 类型 4 个字节、指针类型 4 / 8 字节;
有了结构体类型,那如何定义变量,其实很简单👇
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { 1, 2 };
如果是更复杂的一点呢?
#include
struct Node
{
int data;
struct Node* next;
};
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
};
int main()
{
struct Node n1 = { 100, NULL };
struct Stu s1 = { "张三", "男", 20, 180 };
return 0;
}
上面可以看到,我们每次定义结构体的变量都要使用 struct Node,是不是感觉很麻烦?
那么可以使用 typedef来对它进行 重命名,此时我们创建变量就可以使用 重命名 👇
typedef struct
{
int data;
struct Node* next;
}Node;
int main()
{
Node n = { 0 };
return 0;
}
编译看下
那么如果结构体的成员是 嵌套 的,那么如何来初始化结构体变量呢?
#include
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
};
struct Data
{
struct Stu s;
char ch;
double d;
};
int main()
{
struct Data d = { { "李四", "女", 30, 170 }, "w", 3.14 }; //结构体嵌套初始化
return 0;
}
看一下
思考一下我们结构体传参的时候,传结构体还是传地址呢?
📝 代码示例一
#include
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
int main()
{
print1(s); //传结构体
return 0;
}
📝 代码示例二
#include
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print2(&s); //传地址
return 0;
}
上面的 print1
和 print2
函数哪个好些呢?
答案是:首选 print2
函数。
原因:
1、函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
2、如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
关于 结构体的内存对齐 这是一个面试的高频考点,很重要;
首先下面有一段代码,分析一下这个结构体类型的大小是多少个字节呢?
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
第一眼,大家会想到:c1 是 char 类型,占 1 个字节;
i 是 int 类型,占 4 个字节;
c2 是 char 类型,占 1 个字节;
加起来就是 6 个字节,那么究竟是不是呢?运行看一下
咦?这里为什么是 12 呢?
这里我们就需要好好探究一下,但是再这之前,要学习一个东西,叫 offsetof。
offsetof
,是一个 宏,它用来返回:一个成员在一个结构体起始位置的偏移量。
也就是,它可以计算一个结构体成员,相较于起始位置的一个偏移量。
用法:
size_t offsetof(structName, memberName);
📝 代码示例
#include
#include
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
🌟 运行结果
为什么是 0、4、8 呢?
首先,我们创建一个结构体类型 S1,红色箭头 是结构体的起始位置,从起始位置开始,每个格子代表 1 个字节,第一个格子相较于 起始位置 偏移量是 0;第二个格子相较于 起始位置 偏移量是 1;第三个格子相较于 起始位置 偏移量是 2,以此类推…
根据上面代码的运行结果可以知道,c1 相较于 起始位置 偏移量是 0,并且 c1 是 char 类型,占 1 个字节,所以 c1 成员所占内存空间如下👇
i 相较于 起始位置 偏移量是 4,并且 i 是 int 类型,占 4 个字节,所以 i 成员所占内存空间如下👇
c2 相较于 起始位置 偏移量是 8,并且 c2 也是 char 类型,占 1 个字节,所以 c2 成员所占内存空间如下👇
但是我们计算出来的是 12 个字节呀,也就是说,前 3 个格子和后 3 个格子是没有用到👇
那么为什么呢?为什么要浪费掉这么多的空间来做这个事情呢?
这个时候,就引入到我们今天的主题 结构体内存对齐 !
首先得掌握结构体的对齐规则:
1、结构体的第一个成员,存放在结构体变量开始位置的 0 的偏移量处。
2、从第二个成员开始,都要对齐到对齐数的整数倍的地址处。
对齐数 :成员自身大小和默认对齐数的较小值。
在 VS 环境下,默认的对齐数是 8,S1 中 i 成员自身大小是 4 字节,而 VS 编辑器提供的默认对齐数是 8,取它俩的 最小值 为对齐数,所以 i 的对齐数就是 4;
那么 i 就要对齐到 4 的整数倍的地址处,所以 i 就要放到偏移量为 4 倍数的地址处,换句话说 4 的整数倍包括 4 (这里可能有点点绕🤣)
c2 的自身大小是 1,而默认的对齐数是 8,取它俩的 最小值 为对齐数,所以 c2 的对齐数就是 1;
而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇
3、结构体的总大小,必须是最大对齐数的整数倍
最大对齐数 :是指所有成员的对齐数中最大的那个。
c1 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
c2 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
所以 S1 结构体的总大小必须是 4 的倍数,此时我们对齐完以后在 9 字节的格子处,9 不是 4 的倍数呀!
那么就继续往下对齐,浪费 3 个空间,此时总大小为 12 个字节了(4 的 3 倍)👇
注意:Linux环境没有默认对齐数,对齐数就是自身的大小
我们基本已经掌握了对齐的规则了,那么把上面的代码稍微调换一下位置
📝 代码示例
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S2));
return 0;
}
🌟 运行结果
那么我们再来算一下 S2 在内存中如何对齐的?
首先结构体第一个成员永远放在 0 偏移处,那么 c1 就放在 0 偏移处👇
第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1;
而任何一个地址处都是 1 的倍数,所以就放到偏移量为 1 的格子处👇
第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4;
那么就要往下找 4 的倍数,4 的 1 倍是 4,所以 i 对齐到偏移量为 4 的地址处
结构体的总大小,必须是最大对齐数的整数倍
c1 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
c2 :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
所以 S2 结构体最大对齐数是 4 ,那么总大小必须是 4 的倍数,此时我们对齐完以后在 8 字节的格子处,8 是 4 的倍数;
所以结构体总大小为 8👇
我们再来看一组代码
📝 代码示例
#include
struct S3
{
char c1;
char c2;
int i;
};
int main()
{
struct S3 s;
printf("%d\n", sizeof(struct S3));
return 0;
}
这次我们先画图,画完再来验证
首先结构体第一个成员永远放在 0 偏移处,那么 d 就放在 0 偏移处;
d 是 double 类型,占 8 个字节,所以从 0 偏移处开始,往下放 8 个字节👇
第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1;
而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇
第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4;
那么就要往下找 4 的倍数,4 的 3 倍是 12,所以 i 放到到偏移量为 12 的地址处
结构体的总大小,必须是最大对齐数的整数倍
d :自身大小是 8,VS 默认对齐数是 8,取较小值,所以对齐数是 8;
c :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
i :自身大小是 4,VS 默认对齐数是 8,取较小值,所以对齐数是 4;
所以 S3 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 16 字节的格子处,16 是 8 的倍数;
所以结构体总大小为 16👇
🌟 运行结果
既然我们学会了结构体内存对齐的方法,那么思考一下结构体嵌套如何对齐呢?
📝 代码示例
#include
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 s;
printf("%d\n", sizeof(struct S4));
return 0;
}
我们还是先画图,画完再来验证
首先结构体第一个成员永远放在 0 偏移处,那么 c 就放在 0 偏移处;
c 是 char 类型,占 1 个字节,所以从 0 偏移处开始,往下放 1 个字节👇
此时第二个成员为结构体,那么引出我们的第四条规则:
4、 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
第二个成员 s3 ,它的最大对齐数是 8,那么也就是说 s3 要对齐到 8 的整数倍处;
因为 8 的 1 倍就是 8,此时 8 地址处的格子没有被占用,所以就放到偏移量为 8 的格子处
s3 刚刚我们计算的是 16 个字节,所以从偏移量 8 的格子开始,向下放 16 个字节👇
第三个成员 d 自身大小是 8,默认对齐数是 8,所以对齐数是 4;
那么就要往下找 8 的倍数,8 的 3 倍是 24,所以 d 放到到偏移量为 24 的地址处;
所以从偏移量 24 的格子开始,向下放 8 个字节👇
结构体的总大小,必须是最大对齐数的整数倍
c :自身大小是 1,VS 默认对齐数是 8,取较小值,所以对齐数是 1;
s3 :是嵌套结构体,它的整体大小就是所有最大对齐数是 8,VS 默认对齐数是 8,所以对齐数是 8;
d :自身大小是 8,VS 默认对齐数是 8,所以对齐数是 8;
所以 S4 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 32 字节的格子处,32 是 8 的倍数;
所以结构体总大小为 32👇(0~31,就是32个字节)
🌟 运行结果
不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到呢?
让占用空间小的成员尽量集中在一起。
📝 代码示例
#include
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s1;
struct S2 s2;
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
🌟 运行结果
S1 和 S2 类型的成员一模一样,但是 S1 和 S2 所占空间的大小有了一些区别。
所以让占用空间小的成员尽量集中在一起。
之前我们见过了 #pragma
这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
📝 代码示例
#pragma pack(1) //设置默认对齐数为 1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的默认对齐数,还原为默认
struct S2 //不设置,默认就是 8
{
char c1;
int i;
char c2;
};
int main()
{
struct S1 s1;
struct S2 s2;
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
🌟 运行结果
结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。