• 【C语言】新类型,结构体篇-----深入理解结构体,结构体成员的访问,结构体的大小和新概念偏移量!【图文详解】


    欢迎来CILMY23的博客喔,本篇为【C语言】新类型,结构体篇-----深入理解结构体,结构体成员的访问,结构体的大小和新概念偏移量!【图文详解】,感谢观看,支持的可以给个一键三连,点赞关注+收藏。

     前言

    C语言中的数据类型有内置类型,和用户自定义类型,内置类型包括char ,short,int,double,float,long long……,用户自定义类型包括,结构体,枚举,联合体,数组……本篇博客将会带大家了解用户自定义类型中的结构体。

    目录

    一、结构体的声明和使用

    结构体的声明:

    结构体的使用:

    特殊的结构体声明: 

    二、结构体的访问

     三、结构体大小

    结构体嵌套的大小

    为什么会有内存对齐?

    四、offsetof宏和#pragma预处理指令

     五、结构体的自引用(涉及数据结构)


    一、结构体的声明和使用

    在过去我们经常使用数组存储同一类型的数据,但我们不仅仅只有同一类型的数据,于是C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它被称为结构体

    结构体的声明:

    1. struct Student
    2. {
    3. int num; //学号为整型
    4. char name[20]; //姓名为字符串
    5. char sex; //性别为字符型
    6. int age; //年龄为整型
    7. float score; //成绩为浮点型
    8. };

    结构体类型的声明是用一个关键字struct +结构体名字,结构体名字是用户指定的。

     

    typedef也可以对结构体重命名:

    1. typedef struct Student
    2. {
    3. int num;
    4. }Stu;
    5. int main()
    6. {
    7. Stu n;
    8. return 0;
    9. }

    结构体的使用:

    1. int main()
    2. {
    3. struct Student s = {23,"zhangsan",'m',21,88.9};
    4. return 0;
    5. }

    这样我们就创建了一个结构体变量,这个变量的信息就如{}中所示。

    我们也可以在声明结构体类型的时候声明变量

    1. struct Student
    2. {
    3. int num; //学号为整型
    4. char name[20]; //姓名为字符串
    5. char sex; //性别为字符型
    6. int age; //年龄为整型
    7. float score; //成绩为浮点型
    8. }s1, s2;

    这两种变量还是有差别的,s1,s2是全局变量,s只是局部变量 

     我们创建变量就相当于在内存中开辟了空间,这么多数据类型就构成一个s结构体变量,其中的分布如下:

    特殊的结构体声明: 

     我们可以把结构体类型名称省去,这种情况叫做匿名结构体,用匿名结构体类型创建了一个s对象

    1. struct
    2. {
    3. int num;
    4. }s;
    5. struct
    6. {
    7. int num;
    8. }*ps;
    9. int main()
    10. {
    11. ps = &s;
    12. return 0;
    13. }

     但是同样要注意,在vs编译器看来这两个匿名结构体每个都是单独的个体,我们并不能把s的地址给到指针ps。

    二、结构体的访问

    我们有两种访问方式,一种是.,一种是->

     我们把.和->叫做结构成员访问操作符

    那如果我们得到的是s 的地址呢,就可以用到->操作符

    1. int main()
    2. {
    3. struct Student s = {23,"zhangsan",'m',21,88.9};
    4. printf("%d %s\n", s.num, s.name);
    5. struct Student* ps = &s;
    6. printf("%d %s\n", (*ps).num, (*ps).name);
    7. printf("%d %s\n", ps->num, ps->name);
    8. return 0;
    9. }

    结果如下:

     三、结构体大小

     首先我们来看以下代码:

    1. struct s1
    2. {
    3. char c1;
    4. char c2;
    5. int i1;
    6. };
    7. struct s2
    8. {
    9. char c1;
    10. int i1;
    11. char c2;
    12. };
    13. struct s3
    14. {
    15. double d;
    16. char c;
    17. int i;
    18. };
    19. int main()
    20. {
    21. struct s1 st1;
    22. struct s2 st2;
    23. struct s3 st3;
    24. printf("%d\n", sizeof(st1));
    25. printf("%d\n", sizeof(st2));
    26. printf("%d\n", sizeof(st3));
    27. return 0;
    28. }

    结果如下:

    那为什么会这样呢?这就涉及结构体的大小规则了-----内存对齐 

     内存对齐的规则:

    1.结构体的第⼀个成员对齐到相对结构体变量起始位置偏移量为0的地址处
    2.其他成员变量要对齐到对齐数的整数倍的地址处。
            对齐数 = 编译器默认的⼀个对齐数与该成员变量大小的较小值。
            VS中默认的值为8
            Linux中没有默认对齐数,对齐数就是成员自身的大小

    3.结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
    4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

    st1 

    偏移量是什么?假设我们有一个内存,偏移量为1个字节就是指向1这个地方了,所以偏移量为0的话就是指从起始位置开始.(可以理解为距离起始位置有多远)

     所以第一个成员c1就存放在第一个字节里

    其余成员要按照对齐数来对齐, 根据下面图片来看,c2只需要对齐到1的倍数即可,而i1需要对应到地址字节数4的倍数即可,也就是从第四个字节开始

    如下所示: 

    结构体总大小为最大对齐数,所有成员最大对齐数是4,st1的大小刚好是4的倍数,所以st1的结构体大小就为8 

     st2

    所以我们看st2就容易多了

    首先第一个成员要对齐偏移量为0的位置,c1的位置就确定了

    其余成员要按照对齐数来对齐, 根据下面图片来看,c2只需要对齐到1的倍数即可,而i1需要对应到地址字节数4的倍数即可,也就是从第四个字节开始,

    总的大小为9,结构体总大小为最大对齐数,所有成员最大对齐数是4,所以str2的大小为12.

     st3

    首先第一个成员要对齐偏移量为0的位置,d的位置就确定了,其余成员要按照对齐数来对齐, 根据下面图片来看,c只需要对齐到1的倍数即可,而i需要对应到地址字节数4的倍数即可,也就是从第12字节开始。

    总的大小为16,结构体总大小为最大对齐数,所有成员最大对齐数是8,所以str2的大小为16.

    结构体嵌套的大小

    1. struct S3
    2. {
    3. double d;
    4. char c;
    5. int i;
    6. };
    7. struct s4
    8. {
    9. char c1;
    10. struct S3 s3;
    11. double d;
    12. };
    13. int main()
    14. {
    15. struct s4 st4;
    16. printf("%d\n", sizeof(st4));
    17. return 0;
    18. }

    结果如下:

     st4

    如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数,也就是对齐到s3当中所有最大成员的对齐数,也就是十六的对齐数。

    总的大小为16,结构体总大小为最大对齐数,所有成员最大对齐数是16,所以str2的大小为32.

    为什么会有内存对齐?

    1.    平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
    2.    性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用⼀个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
    总体来说:结构体的内存对齐是拿空间来换取时间的做法。 

    最后,尽量将字节数小的成员安排在前面,防止空间浪费

    四、offsetof宏和#pragma预处理指令

     offsetof宏是用来计算偏移量的,在cplusplus网站可以查询

    offsetof - C++ Reference (cplusplus.com)

    offsetof的使用如下: 

    1. #include
    2. #include
    3. struct S3
    4. {
    5. double d;
    6. char c;
    7. int i;
    8. };
    9. struct s4
    10. {
    11. char c1;
    12. struct S3 s3;
    13. double d;
    14. };
    15. int main()
    16. {
    17. printf("%zd\n", offsetof(struct s4, c1));
    18. printf("%zd\n", offsetof(struct s4, s3));
    19. printf("%zd\n", offsetof(struct s4, d));
    20. return 0;
    21. }

    结果如下:

     #pragma预处理指令

    1. #pragma pack()//取消设置的默认对⻬数,还原为默认
    2. struct S3
    3. {
    4. double d;
    5. char c;
    6. int i;
    7. };
    8. #pragma pack(1)//设置默认对⻬数为1
    9. struct s4
    10. {
    11. char c1;
    12. struct S3 s3;
    13. double d;
    14. };
    15. int main()
    16. {
    17. printf("%d\n", sizeof(struct s4));
    18. return 0;
    19. }

    一般使用都会将其设置成2,4,6,8,这样的数值。 

     五、结构体的自引用(涉及数据结构)

    可以看以下这篇补充一下链表的结构知识点 ,然后再看结构体的自引用

    http://t.csdnimg.cn/BDRBY

    假设我们这里有个链表,定义⼀个链表的节点如下所示:

    1. struct Node
    2. {
    3. int num;
    4. struct Node next;
    5. };
    6. int main()
    7. {
    8. struct Node n;
    9. return 0;
    10. }

     仔细分析,其实是不行的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的,一个结构体中有一个Node,Node中又有一个Node如此无限循环下去,就会造成结构体变量的大小无穷大了。

    正确的自引用应该如下所示:

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

     同样对tepedef我们仍然要注意以下情况是不能使用的

    看以下两个代码对比例子:

    1. typedef struct Node
    2. {
    3. int data;
    4. Node* next;
    5. }Node;
    6. typedef struct
    7. {
    8. int data;
    9. Node* next;
    10. }Node;

    我们得先有结构体类型重命名后,得到的名字,然后再去命名next。 

    正确引用如下:

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

    感谢各位同伴的支持,本期结构体篇就讲解到这啦,如果你觉得写的不错的话,可以给个一键三连,点赞关注+收藏,若有不足,欢迎各位在评论区讨论。  

  • 相关阅读:
    【JVM面试】从JDK7 到 JDK8, JVM为啥用元空间替换永久代?
    SiC器件概念
    CI /CD学习
    微信小程序(基础语法)
    数据仓库dws层,DWS层搭建--商品主题宽表,md,review第1遍,220622,
    LC39.组合总和
    线性表的应用 —— 静态链表
    产品代码都给你看了,可别再说不会DDD(五):请求处理流程
    代码随想录1刷—二叉树篇(二)
    安全基础 --- 原型链污染
  • 原文地址:https://blog.csdn.net/sobercq/article/details/136107763