• 【结构体内功修炼】结构体内存对齐(一)


    在这里插入图片描述

    在这里插入图片描述

    🌟 前言

    本文重点讲解结构体的大小,以及在内存中如何对齐的。

    1. 结构体的声明

    🍑 结构体的基础知识

    结构是一些值的集合,这些值称为 成员变量

    结构的每个成员可以是 不同类型 的变量。

    🍑 结构的声明

    struct tag
    {
    	member-list; //成员列表
    
    }variable-list; //变量列表
    
    • 1
    • 2
    • 3
    • 4
    • 5

    假设我现在要定义一个学生的 结构体类型 ,包括:姓名、性别、年龄、身高

    struct Stu
    {
    	char name[20]; //姓名
    	char sex[5]; //性别
    	int age; //年龄
    	int hight; //身高
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么我们现在要拿刚刚定义的 学生结构体类型 去创建 结构体变量

    struct Stu
    {
    	char name[20];
    	char sex[5];
    	int age;
    	int hight;
    };
    
    
    int main()
    {
    	struct Stu s1;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    还可以用下面这种方法创建结构体变量

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    此时创建的 s2s3s4s5 都是创建的全局变量;

    s1 为局部变量;

    🍑 特殊的声明

    在声明结构的时候,可以不完全的声明。

    比如在定义一个结构体时,可以把它的标签去掉;

    struct
    {
    	char c;
    	int a;
    	double d;
    }sa;
    
    int main()
    {
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面这种方式叫 匿名结构体类型,但是 sa 只能使用一次;

    但是如果是下面这样呢?

    struct
    {
    	char c;
    	int a;
    	double d;
    }sa;
    
    struct
    {
    	char c;
    	int a;
    	double d;
    }*ps;
    
    int main()
    {
    	ps = &sa;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    sa 是一个匿名结构体,ps 是一个匿名结构体指针;

    虽然它们的成员类型是一模一样的,但是编译器会认为 = 两边是不同的结构体类型,所以这种写法完全是错误的
    在这里插入图片描述

    🍑 结构体的自引用

    我们思考一个问题:在结构中包含一个类型为该结构本身的成员是否可以呢?

    比如这样

    struct Node
    {
    	int data;
    	struct Node next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这种情况是绝对不可以的;

    如果我们在结构中包含一个类型为该结构本身的成员,那么此时我要求这个结构体类型的大小是多少?

    首先在 Node 里面有个 int 类型,然后还有个 struct Node next 类型;

    那么在 struct Node next 里面还有个 int 类型,和 struct Node next 类型…等等就会形成无线循环;

    所以我们只需要在存上一个结构体类型的地址就好了

    struct Node
    {
    	int data;
    	struct Node* next;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    此时,我们创建的每个 Node 节点里面,既可以保存一个数值,又可以保存一个地址;

    通过这个地址,就可以找到由 next 指向的下一个节点;

    此时结构体的大小就可以确定了:int 类型 4 个字节、指针类型 4 / 8 字节;

    🍑 结构体变量的定义和初始化

    有了结构体类型,那如何定义变量,其实很简单👇

    struct Point
    {
    	int x;
    	int y;
    }p1; //声明类型的同时定义变量p1
    
    struct Point p2; //定义结构体变量p2
    
    //初始化:定义变量的同时赋初值。
    struct Point p3 = { 1, 2 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如果是更复杂的一点呢?

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    上面可以看到,我们每次定义结构体的变量都要使用 struct Node,是不是感觉很麻烦?

    那么可以使用 typedef来对它进行 重命名,此时我们创建变量就可以使用 重命名 👇

    typedef struct 
    {
    	int data;
    	struct Node* next;
    }Node;
    
    int main()
    {
    	Node n = { 0 };
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编译看下
    在这里插入图片描述
    那么如果结构体的成员是 嵌套 的,那么如何来初始化结构体变量呢?

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    看一下
    在这里插入图片描述

    2. 结构体传参

    思考一下我们结构体传参的时候,传结构体还是传地址呢?

    📝 代码示例一

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    📝 代码示例二

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    上面的 print1print2 函数哪个好些呢?

    答案是:首选 print2 函数。

    原因:

    1、函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

    2、如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

    结论:结构体传参的时候,要传结构体的地址。

    3. 结构体内存对齐

    关于 结构体的内存对齐 这是一个面试的高频考点,很重要;

    📃 代码示例一

    首先下面有一段代码,分析一下这个结构体类型的大小是多少个字节呢?

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    int main()
    {
    	printf("%d\n", sizeof(struct S1));
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    第一眼,大家会想到:c1char 类型,占 1 个字节;

    iint 类型,占 4 个字节;

    c2char 类型,占 1 个字节;

    加起来就是 6 个字节,那么究竟是不是呢?运行看一下
    在这里插入图片描述
    咦?这里为什么是 12 呢?

    这里我们就需要好好探究一下,但是再这之前,要学习一个东西,叫 offsetof

    offsetof,是一个 ,它用来返回:一个成员在一个结构体起始位置的偏移量。

    也就是,它可以计算一个结构体成员,相较于起始位置的一个偏移量。

    用法:

    size_t offsetof(structName, memberName);
    
    • 1

    📝 代码示例

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    🌟 运行结果
    在这里插入图片描述
    为什么是 048 呢?

    首先,我们创建一个结构体类型 S1红色箭头 是结构体的起始位置,从起始位置开始,每个格子代表 1 个字节,第一个格子相较于 起始位置 偏移量是 0;第二个格子相较于 起始位置 偏移量是 1;第三个格子相较于 起始位置 偏移量是 2,以此类推…
    在这里插入图片描述
    根据上面代码的运行结果可以知道,c1 相较于 起始位置 偏移量是 0,并且 c1char 类型,占 1 个字节,所以 c1 成员所占内存空间如下👇
    在这里插入图片描述
    i 相较于 起始位置 偏移量是 4,并且 iint 类型,占 4 个字节,所以 i 成员所占内存空间如下👇
    在这里插入图片描述
    c2 相较于 起始位置 偏移量是 8,并且 c2 也是 char 类型,占 1 个字节,所以 c2 成员所占内存空间如下👇
    在这里插入图片描述
    但是我们计算出来的是 12 个字节呀,也就是说,前 3 个格子和后 3 个格子是没有用到👇
    在这里插入图片描述
    那么为什么呢?为什么要浪费掉这么多的空间来做这个事情呢?

    这个时候,就引入到我们今天的主题 结构体内存对齐

    首先得掌握结构体的对齐规则:

    1、结构体的第一个成员,存放在结构体变量开始位置的 0 的偏移量处。
    在这里插入图片描述
    2、从第二个成员开始,都要对齐到对齐数的整数倍的地址处。

    对齐数 :成员自身大小和默认对齐数的较小值。

    VS 环境下,默认的对齐数是 8S1i 成员自身大小是 4 字节,而 VS 编辑器提供的默认对齐数是 8,取它俩的 最小值 为对齐数,所以 i 的对齐数就是 4

    那么 i 就要对齐到 4 的整数倍的地址处,所以 i 就要放到偏移量为 4 倍数的地址处,换句话说 4 的整数倍包括 4 (这里可能有点点绕🤣)
    在这里插入图片描述
    c2 的自身大小是 1,而默认的对齐数是 8,取它俩的 最小值 为对齐数,所以 c2 的对齐数就是 1

    而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇
    在这里插入图片描述
    3、结构体的总大小,必须是最大对齐数的整数倍

    最大对齐数 :是指所有成员的对齐数中最大的那个。

    c1 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4

    c2 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    所以 S1 结构体的总大小必须是 4 的倍数,此时我们对齐完以后在 9 字节的格子处,9 不是 4 的倍数呀!

    那么就继续往下对齐,浪费 3 个空间,此时总大小为 12 个字节了(43 倍)👇
    在这里插入图片描述
    注意: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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    🌟 运行结果
    在这里插入图片描述
    那么我们再来算一下 S2 在内存中如何对齐的?

    首先结构体第一个成员永远放在 0 偏移处,那么 c1 就放在 0 偏移处👇
    在这里插入图片描述
    第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1

    而任何一个地址处都是 1 的倍数,所以就放到偏移量为 1 的格子处👇
    在这里插入图片描述
    第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4

    那么就要往下找 4 的倍数,41 倍是 4,所以 i 对齐到偏移量为 4 的地址处
    在这里插入图片描述
    结构体的总大小,必须是最大对齐数的整数倍

    c1 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    c2 :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4

    所以 S2 结构体最大对齐数是 4 ,那么总大小必须是 4 的倍数,此时我们对齐完以后在 8 字节的格子处,84 的倍数;

    所以结构体总大小为 8👇
    在这里插入图片描述

    📃 代码示例三

    我们再来看一组代码

    📝 代码示例

    #include 
    
    struct S3
    {
    	char c1;
    	char c2;
    	int i;
    };
    
    
    int main()
    {
    	struct S3 s;
    
    	printf("%d\n", sizeof(struct S3));
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这次我们先画图,画完再来验证

    首先结构体第一个成员永远放在 0 偏移处,那么 d 就放在 0 偏移处;

    ddouble 类型,占 8 个字节,所以从 0 偏移处开始,往下放 8 个字节👇
    在这里插入图片描述
    第二个成员 c2 自身大小是 1,默认对齐数是 8,取较小值,所以对齐数是 1

    而任何一个地址处都是 1 的倍数,所以就放到偏移量为 8 的格子处👇
    在这里插入图片描述
    第三个成员 i 自身大小是 4,默认对齐数是 8,取较小值,所以对齐数是 4

    那么就要往下找 4 的倍数,43 倍是 12,所以 i 放到到偏移量为 12 的地址处
    在这里插入图片描述
    结构体的总大小,必须是最大对齐数的整数倍

    d :自身大小是 8VS 默认对齐数是 8,取较小值,所以对齐数是 8

    c :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    i :自身大小是 4VS 默认对齐数是 8,取较小值,所以对齐数是 4

    所以 S3 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 16 字节的格子处,168 的倍数;

    所以结构体总大小为 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们还是先画图,画完再来验证

    首先结构体第一个成员永远放在 0 偏移处,那么 c 就放在 0 偏移处;

    cchar 类型,占 1 个字节,所以从 0 偏移处开始,往下放 1 个字节👇
    在这里插入图片描述
    此时第二个成员为结构体,那么引出我们的第四条规则:

    4、 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

    第二个成员 s3 ,它的最大对齐数是 8,那么也就是说 s3 要对齐到 8 的整数倍处;

    因为 81 倍就是 8,此时 8 地址处的格子没有被占用,所以就放到偏移量为 8 的格子处

    s3 刚刚我们计算的是 16 个字节,所以从偏移量 8 的格子开始,向下放 16 个字节👇
    在这里插入图片描述
    第三个成员 d 自身大小是 8,默认对齐数是 8,所以对齐数是 4

    那么就要往下找 8 的倍数,83 倍是 24,所以 d 放到到偏移量为 24 的地址处;

    所以从偏移量 24 的格子开始,向下放 8 个字节👇
    在这里插入图片描述
    结构体的总大小,必须是最大对齐数的整数倍

    c :自身大小是 1VS 默认对齐数是 8,取较小值,所以对齐数是 1

    s3 :是嵌套结构体,它的整体大小就是所有最大对齐数是 8VS 默认对齐数是 8,所以对齐数是 8

    d :自身大小是 8VS 默认对齐数是 8,所以对齐数是 8

    所以 S4 结构体最大对齐数是 8 ,那么总大小必须是 8 的倍数,此时我们对齐完以后在 32 字节的格子处,328 的倍数;

    所以结构体总大小为 32👇(0~31,就是32个字节)在这里插入图片描述
    🌟 运行结果
    在这里插入图片描述

    4. 为什么存在内存对齐

    • 平台原因(移植原因):

    不是所有的硬件平台都能访问任意地址上的任意数据的;
     
    某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

    • 性能原因:

    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
     
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

    总体来说:结构体的内存对齐是拿空间来换取时间的做法。

    那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到呢?

    让占用空间小的成员尽量集中在一起。

    📝 代码示例

    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    🌟 运行结果
    在这里插入图片描述
    S1S2 类型的成员一模一样,但是 S1S2 所占空间的大小有了一些区别。

    所以让占用空间小的成员尽量集中在一起。

    5. 修改默认对齐数

    之前我们见过了 #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    🌟 运行结果
    在这里插入图片描述
    结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

  • 相关阅读:
    【设计模式】Java设计模式 - 状态模式
    QWEN technical report
    Redis 哈希表操作实战(全)
    Linux目录结构与路径
    webp格式如何转成png?
    【Kafka】CDH 中配置 Kafka MirrorMaker 实现 Kafka 集群消息同步
    Linux使用wegt下载ftp内容及定时器设置
    麦芽糖-聚乙二醇-阿霉素maltose-Doxorubicin
    Android—AMS启动
    Java小树的参天成长【内部类,成员内部类,静态内部类,局部内部类,匿名内部类】
  • 原文地址:https://blog.csdn.net/m0_63325890/article/details/125662338