IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 【C语言】[图文] 详解结构体,详细分析结构体对齐 -> 正文阅读

[数据结构与算法]【C语言】[图文] 详解结构体,详细分析结构体对齐

结构体

1. 结构体

? 结构体是一些值的集合,这些值成为成员变量。结构的每个成员可以是不同类型的变量。结构体属于自定义类型。

1.1 结构体声明

1.1.1 标准声明

struct User
{
    char Name[20];
    char Sex[10];
};

此结构体的类型就是 struct User
User 是结构体标签(tag)

1.1.2 特殊声明(不完全声明)

struct
{
	char Name[20];
    char Sex[10];
    int age;
}User1;
struct
{
	char Name[20];
    char Sex[10];
    int age;
}*p;

这种声明方法省略了结构体标签(tag), 直接定义的 User1p 均被称为匿名结构体变量。无法在 main 函数中再次定义此种结构体变量
并且,虽然表面上 User1*p 结构体类型 好像一样。

但是如果 p = &User1; 进行编译,将会报出警告。警告内容是: 指针 到 指针 的类型不兼容。说明 两个变量的结构体类型并不相同,编译器认为,两个成员一样的匿名结构体类型是两种不同的类型

1.2 结构体的自引用

以 链表节点 为例:

struct Node
{
    int data;
    struct Node* next;
}
// 自引用 结构体类型的指针

但是

typedef struct
{
    int data;
    Node* next;
}Node;
// 不行
// 结构体内部使用 Node* 时, typedef 还未生效

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

示例:

// 结构体声明
struct Contact
{
    char Addr[30];
    char Tele[12];
};
// 嵌套结构体的结构体声明
struct User
{
    char Name[20];
    char Sex[10];
    int age;
    struct Contact C;
};

int main()
{
    struct Contact C1 = {"CSDN", "2xxxxxxxxxx" };//结构体变量的定义和初始化
	struct User U1 = { "July3", "Male", 19, { "CSDN", "1xxxxxxxxxx" } };	//嵌套结构体的结构体变量的定义和初始化
    
	return 0;
}

1.4 结构体内存对齐 *

要弄明白什么是结构体内存对齐,我们先来看一下结构体大小

下面这段结构体的大小是多少?

struct S1
{
    char c1;
    int i;
    char c2;
};

我们用 sizeof 求出此结构体类型的大小是:12 字节

image-20220124135912996

但是 int类型大小是 4 字节,char 类型的大小是 1 字节。这个结构体大小不应该是 6 字节吗? 为什么是 12 字节呢?

这就是由于内存对齐的原因:


在正式开始讲解什么是内存对齐之前,我们先来了解一个概念:结构体成员的偏移量

什么是结构体成员的偏移量?

一个结构体的每个成员地址相对于其结构体类型的首地址的偏移量,就是结构体成员的偏移量。

怎么计算结构体成员的偏移量?

C语言中,计算结构体成员的偏移量有一个函数:offsetof

offsetof

size_t offsetof( structName, memberName);

offsetof函数的两个参数是:结构体类型名,计算偏移量的成员名

所在头文件是 stddef.h

我们先计算一下上边这段结构体类型,各成员的偏移量(%zu是输出 size_t 类型的数据的指定格式)

image-20220124141635244

第一个,c1的偏移量是 0;

其次,i 的偏移量是 4;

最后,c2的偏移量是 8.

我们做图明确出来:

image-20220124144039803

可以非常明显的看出,结构体成员c1i 之间,有三个字节的空间是空的

不过,这也才占用了 9字节的空间,但是我们计算的 结构体的大小 是 12,所以我们判断,在 c2 之后还有三个字节的空间 被这个结构体所占用。

所以,此结构体的内存空间占用情况,可能是这样的:

image-20220124144940813

那么,为什么呢?为什么会有 开辟了,但是没有用到 的空间呢?一个结构体类型的大小,到底如何计算呢?

1.4.1 结构体内存对齐规则

  1. 结构体的第一个成员变量存放在结构体变量 开始位置的 0 偏移处

  2. 从第二个成员变量开始,要对齐到 某个对齐数 的整数倍的地址处

    对齐数:编译器的默认对齐数 与 该成员自身大小 的较小值

    • VS编译器 中 对齐数默认为 8
    • 如果编译器默认没有对齐数,对齐数就是成员自身大小
  3. 结构体总大小必须是 最大对齐数的整数倍

    最大对齐数:结构体所有成员中,对齐数最大的成员的对齐数

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

了解了结构体内存对齐规则,就来详细了解一下结构体到底是如何内存对齐的

1.4.2 结构体内存对齐详解

1.4.2.1 示例1:

我们以上面举过例子的结构体为例:

struct S1
{
    char c1;
    int i;
    char c2;
};

先看一下总大小:
image-20220124160252990

然后我们具体来计算一下:

除第一个成员外的结构体成员成员自身大小成员自身大小(类型大小)编译器默认对齐数实际对齐数(取较小值)
i(int)484
c2(char)181

根据规则:

  1. c1 存放在结构体变量 开始地址的 0 偏移处

  2. i 的对齐数是 4,所以存放在偏移量是 4的整数倍 处

    至少是4

    image-20220124155055202
  3. c2 的对齐数是 1,所以存放在偏移量是 1的整数倍 处

  4. 结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为 4 的整数倍。

    c2所在空间已经是 第 9 个字节,所以此结构体总大小 最小为 12

    image-20220124155736544

    所以,结构体大小为 12 字节

1.4.2.2 示例2:

再来看下边这个例子:

struct S2
{
    char c1;
    char c2;
    int i;
};

我们将,上一个结构体成员中的,ic2换一换位置结果又是什么呢?

image-20220124160428100

我们发现只是换了一下位置,结构体大小就减少了 4 个字节

这次又是怎么对齐和计算的呢?

除第一个成员外的结构体成员成员自身大小成员自身大小(类型大小)编译器默认对齐数实际对齐数(取较小值)
c2(char)181
i(int)484
  1. c1 存放在结构体变量 开始地址的 0 偏移处

  2. c2 的对齐数是 1,所以存放在偏移量是 1的整数倍 处,c2 下面就可以

  3. i 的对齐数是 4,所以存放在偏移量是 4的整数倍 处

至少是4

image-20220124161026248
  1. 结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为 4 的整数倍。

i存放完,结构体占8个字节,正好是 4的倍数,所以不用再占用其他空间

image-20220124161322999

此结构体总大小为:8字节

1.4.2.3 示例3:

再来一个例子:

struct S3
{
	double n;
    char c1;
    int i;
};

我们先自己计算:

除第一个成员外的结构体成员成员自身大小(类型大小)编译器默认对齐数实际对齐数(取较小值)
c1(char)181
i(int)484
  1. n 存放在结构体变量 开始地址的 0 偏移处

  2. c2 的对齐数是 1,所以存放在偏移量是 1的整数倍 处

  3. i 的对齐数是 4,所以存放在偏移量是 4的整数倍 处

    至少是12

    image-20220124163205300
  4. 结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为 8 的整数倍。

    i存放完,结构体占16个字节,正好是 8的倍数,所以不用再占用其他空间

    image-20220124163407789

    此结构体总大小为:16字节

我们来验证一下:

image-20220124163820717

确实跟我们计算的一样,这个结构体大小为 16 字节

1.4.2.4 示例4(嵌套结构体的结构体)
struct S3
{
	double n;
    char c1;
    int i;
};

struct S4
{
    int n;
    struct S3 s;
    char c1;
};

那么嵌套有结构体的结构体S4,它的成员怎么去对齐,它的大小怎么计算呢?

按照内存对齐的规则:

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

被嵌套的结构体的对齐数,就是它的 对齐数最大的成员 的对齐数

所以,我们先来计算:

除第一个成员外的结构体成员成员自身大小(类型大小)对齐数最大的成员的对齐数编译器默认对齐数实际对齐数
s(struct S3)16(double)888
c1(char)181
  1. n (大小为4)存放在结构体变量 开始地址的 0 偏移处

  2. s (大小为16)的对齐数是 8,所以存放在偏移量是 8的整数倍 处

  3. c1(大小为 1)的对齐数是 1,所以存放在偏移量是 1的倍数 处

    image-20220124170223547

  4. 结构体总大小必须为 最大对齐数的整数倍,在此结构体中即为 8 的整数倍。

    c1 存放完,已经占用 25 字节,所以此结构体总大小 最小为 32

    image-20220124170531153

    此结构体总大小为:32 字节

举过 4 个例子之后,结构体的内存对齐应该就已经解释清楚了

不过,我们再来总结一下规则:

  1. 结构体的第一个成员,就存放在结构体偏移量为 0 的地址处

  2. 第二个及以后的成员,需要在 对齐数的整数倍 的地址处存放

  3. 结构体的总大小,必须是其成员的最大对齐数的整数倍

  4. 如果是结构体嵌套了结构体的情况,被嵌套的结构体的对齐数,就是 它内部成员的最大对齐数

例如:

struct S3
{
	double n;
    char c1;
    int i;
};

struct S4
{
    int n;
    struct S3 s;
    char c1;
};

在 结构体 struct S4声明中,嵌套了结构体struct S3

那么,变量s的对齐数,就是结构体struct S3内部的成员的最大对齐数(为 8),变量s的大小,就是结构体struct S3的大小(为 16

1.4.3 为什么存在内存对齐

  1. 硬件平台原因(移植的问题)

    不是所有的硬件平台都能访问 任意地址上 的 任意数据 ;

    某些硬件平台只能在 某些地址处 取 某些特定类型的数据(经过对齐的特定类型的数据),否则抛出硬件异常。

  2. 性能原因

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

    举个例子:

    struct S
    {
       char c;
       int i;
    }
    
    int main()
    {
       struct S s1;
       return 0;
    }
    

    上边的代码,创建了一个结构体变量,我们来假设两种情况

    1. 假设 不对齐内存

    2. 假设 对齐内存

    假设,我们是 32 位环境,一次可以读取 4 字节

    那么对于第一种情况:

    我们要访问完整的 i 的数据,就需要访问两次

    因为,第一次只访问到了 i的 前三个字节,第二次访问了 i 的最后一个字节

    对于第二种情况

    我们要访问完整的 i 的数据,只需要一次访问

    跳过前 4 个字节,直接访问 i 的数据,效率要更高一些

    所以,内存对齐,有一定的性能的提升

    所以,结构体的内存对齐,是一种用空间来换取时间的做法。

1.4.4 结构体声明(设计)的优化

了解了结构体内存对齐,知道了结构体变量内部的成员,在内存中可能并不是紧密排列的,是需要对齐的。是一种拿空间换取时间的做法。

那么有没有一种做法,可以在对其的同时尽量的节省空间呢?

答案是:有!

我们 举过的例子,示例 2示例 3 仅仅是将结构体内部的成员换了一个位置,就节省了 4 个字节的空间。

struct S1
{
    char c1;
    int i;
    char c2;
};
// ↓↓↓
struct S2
{
    char c1;
    char c2;
    int i;
};

看过上边的对比,我们发现 c1c2 对空间的占用都很小,都是 1 字节。两个占用内存空间小的变量挤在一起,就能减少空间的浪费。

所以呢,如果 既要满足对齐,还要尽量节省空间,就

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

1.4.5 默认对齐数的修改

上边对齐规则中,我们提到 VS编译器中,结构体的 默认对齐数 是8。而在 Linux 平台下的 gcc编译器 是没有默认对齐数的。但是,我们可以通过一个预处理指令 来设置本项目中的默认对齐数。

#pragma pack (n)

这句预处理指令是设置默认对齐数用的,n 就是 要设置的默认对齐数的值

举个栗子:

#pragma pack(1)		//设置默认对齐数为1
#include <stdio.h>

struct S1
{
    char c1;
    int i;
    char c2;
}

int main()
{
    printf("%zu", sizeof(struct S1));
    
    return 0;
}

此时,struct S1的总大小变成了 6 字节,而我们没有改变的时候是 12 字节

注意!!默认对齐数千万不要乱改,一般改为 2 的次方。

1.5 结构体传参

如果我们想要写一个比如,修改结构体变量、输出结构体变量等功能的函数,就会遇到和其他函数一样的问题——传参。

函数调用结构体变量时,怎么传参给函数呢?

  1. 结构体变量直接传参

    struct S
    {
        int data[1000];
        int num;
    };
    struct S s1 = { {2,0,0,2}, 2022 };
    
    void print1(struct S s)
    {
        printf("%d\n", s.num);
    }
    
    int main()
    {
        print1(s1);
        return 0;
    }
    
  2. 结构体变量地址传参

    struct S
    {
        int data[1000];
        int num;
    };
    struct S s1 = { {2,0,0,2}, 2022 };
    
    void print2(struct S* ptrs)
    {
        printf("%d\n", ptrs->num);
    }
    
    int main()
    {
        print2(&s1);
        return 0;
    }
    

以上两种结构体的传参方式,大家思考一下,哪一种传参方式好一些,为什么?

先来分析一下,结构体传参,传值和传地址,有什么不同:

如果传值:

给函数传入结构体变量的值,函数需要将结构体变量的值完全拷贝一份,再存储到形参。可以修改形参的值,但不能直接访问原结构体变量。

如果传地址:

给函数传入结构体变量的地址,函数需要将地址拷贝一份,存储至形参。并且可以通过地址,来直接访问原结构体变量。

所以这样来看,应该是 传结构体地址的方式好一些。具体原因:

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

如果传递一个结构体对象的时候,结构体过大,函数压栈的系统开销就会比较大,会导致项目性能的下降。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:40:34  更:2022-04-24 09:41:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 8:52:08-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码