• C语言自定义类型详解(1)结构体知识汇总


    本篇概要

    本篇主要讲述C语言结构体的相关知识,包括结构体的基本声明,结构体的匿名结构,结构体的自引用,结构体变量的定义和初始化以及结构体的内存对齐等相关知识。


    1.结构体

    1.1结构体的基本声明

    结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

    结构体的基本用法如下:

    struct Stu
    {
    char name[20];//名字
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
    }; //分号不能丢
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当然,我在这里还有一些补充

    struct student
    {
    }s1,s2,s3;//这s1,s2,s3是三个结构体变量
    
    int main()
    {
    struct student s4,s5,s6;//s4,s5,s6是三个结构体变量
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    需要说明的是s1,s2,s3是全局变量,s4,s5,s6是局部变量。

    1.2结构体的特殊声明(匿名结构体类型)

    struct 
    {
    	char neme[20];
    	int age;
    	char sex[5];//一个汉字2个字符
    	float score;
    }s1,s2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ↑如上代码所示,可以去掉结构体的名字 匿名结构体类型,但只能用一次,后面再想定义变量不可以(只可以使用s1和s2)

    struct 
    {
    	char neme[20];
    	int age;
    	char sex[5];//一个汉字2个字符
    	float score;
    }b;
    
    struct 
    {
    	char neme[20];
    	int age;
    	char sex[5];//一个汉字2个字符
    	float score;
    }*p;
    
    int main()
    {
    p=&b;
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    ↑这里,两个结构体类型完全一样,但是p=&b这样是有问题的,虽然结构体类型,成员完全一样,但在编译器看来,这依然是两种结构体类型,编译器认为p和&b是不一样的。

    1.3结构体的自引用

    即在结构中包含一个类型为该结构本身的成员

    自引用错误用法:

    struct Node
    {
    int data;
    struct Node n;
    };
    
    int main()
    {
    printf("%d",sizeof(struct Node));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    提示:这么使用编译器会报警,运行不成功。因为struct中有一个整形为4字节,后面有一个结构体,那么是4+n,那么n中又是4+n,算不了。

    自引用的正确用法:

    struct Node
    {
    int data;
    struct Node* n;
    };
    int main()
    {
    printf("%d",sizeof(struct Node));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    此时,代码可以运行,用指针的话,指针存放下一个节点的地址,指针本身的大小也是固定的,所以可以计算出来。

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

    下面列举几种结构体的定义:

    struct Point
    {
    	int x;
    	int y;
    }p1 = {1,2};
    
    struct Point p3 = {4,5};
    
    int main()
    {
    	int a = 10;
    	int b = 20;
    	struct Point p2 = {a, b};
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    结构体可以这么定义

    struct Stu
    {
    	char name[15];//名字
    	int age;
    };
    struct Stu s = { "zhangsan", 20 };
    struct Stu s2 = { .age=18, .name="如花"};
    
    int main()
    {
    printf("%s %d\n", s.name, s.age);
    printf("%s %d\n", s2.name, s2.age);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    也可以这么定义

    struct Node
    {
    	int data;
    	struct Point p;
    	struct Node* next;
    };
    int main()
    {
    struct Node n = { 100, {20, 21}, NULL };
    printf("%d x=%d y=%d\n", n.data, n.p.x, n.p.y);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    更加复杂,也还可以这么定义,嵌套结构体。

    1.5结构体内存对齐

    我们先来看以下的代码:

    struct S1
    {
    	char c1;
    	int i;
    	char c2;
    };
    
    struct S2
    {
    	char c1;
    	char c2;
    	int i;
    };
    
    int main()
    {
    	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

    在这里插入图片描述
    提示:我们可以看到两个一摸一样的结构体,计算出来的大小,却不一样,这是什么导致的呢?

    就是结构体的内存对齐导致的。

    下来给大家介绍一个宏 offsetof,其头文件为
    它可以计算结构体成员相较于起始位置的偏移量

    我们在上面代码的main函数加上以下代码:

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

    在这里插入图片描述
    在这里插入图片描述

    我们可以看到offsetof计算出来的结果为0,4,8,上面的示意图对应的就是struct内存的结构,灰色为char c1,蓝色为int i,橙色为char c2,红色的取余浪费了。

    使用同样的操作,我们可以得出struct的结构示意图。
    在这里插入图片描述

    c1,c2,i都已在图中标注出来,红色为浪费部分。

    提示:但是为什么要这个样子呢?为什么要内存对齐?

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

    1. 第一个成员在与结构体变量偏移量为0的地址处存储。
    2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
      对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
      VS中默认的值为8
    3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的成员中最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

    例如struct s2
    char c1偏移量为0 ,直接占第一个字节
    char c2 偏移量为min{1,8}
    int i 偏移量为min{4,8}
    此结构体最大对齐数位4,根据规则三结构体总大小取4的整数倍,即8.

    再例如struct s1
    char c1偏移量位0,直接占第一个字节
    int i偏移量位min{4,8},从第四个字节开始占4个字节
    char c2,偏移量为1的倍数,直接放在i后面
    这是一共占了9个字节,最大对齐数为4,结构体大小应为4的倍数,故为12.

    接下来就来说一说为什么要对齐!

    1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
    2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

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

    对于第二点如下:
    在这里插入图片描述

    提示:在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。
    例如struct s2就比struct s1所占空间小!!

    上面讲到VS的默认对齐数为8,我们也可以更改它
    pragma pack(5) //设置默认对齐数为5
    pragma pack() //取消设置的默认对齐数,还原为默认

    1.6结构体传参

    代码使用了结构体传值调用和传址调用两种方法:

    struct S
    {
    	int data[1000];
    	int num;
    };
    
    void print1(struct S t)
    {
    	printf("%d %d %d %d\n", t.data[0], t.data[1], t.data[2], t.num);
    }
    
    void print2(const struct S * ps)
    {
    	printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
    }
    
    int main() 
    {
    	struct S s = { {1,2,3}, 100 };
    	print1(s);//传值调用
    	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
    • 22
    • 23
    • 24

    函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
    如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。所以我们优先使用传址调用。

  • 相关阅读:
    ES6中的Set集合与Map映射
    Kotlin2 进阶
    LeetCode(力扣)406. 根据身高重建队列Python
    TypeScript入门指南
    大数据在智慧城市建设中的应用
    Laravel 创建自己的 Facade 扩展 geoip 根据 IP 获取国家、地域、城市信息
    10个基于.Net开发的Windows开源软件项目
    MEV进入比特币,它将改变比特币挖矿?
    想知道图片转表格怎么转?简单实用的转换方法分享
    Word控件Spire.Doc 【文档操作】教程(二):在 C#、VB.NET 中打开 Word
  • 原文地址:https://blog.csdn.net/qq_57425280/article/details/133147453