• C语言之自定义类型------结构体


    在前面我们学习过char,short,int,long,float,double等,这些都属于内置类型,C语言本身就含有的数据类型。

    对于结构体,枚举,联合体等这种复杂的类型,我们称之为自定义类型。

    结构体

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

    结构体的定义:

    struct tag //结构体标签
    {
    	member - list;//成员变量列表:可以是同种类型,也可以是不同种类型
    }variable-list;//变量列表:注意后面的“;”,不要忘记了
    
    • 1
    • 2
    • 3
    • 4

    结构体的声明:

    #include
    struct Stu
    {
    	char name[20];
    	int age;
    	char phone[20];
    	char sex;
    }Student1, Student2;//创建全局结构体变量
    struct Stu Student3;//创建全局结构体变量
    int main()
    {
    	//创建局部结构体变量
    	struct Stu Student4;
    	struct Stu Student5;
    	struct Stu Student6;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    匿名结构体类型:

    与上面所不同的是,匿名结构体是没有名字的,它的特点是只能使用一次,并且只能在创建的时候定义结构体变量,就是下述S的这种创建方式。

    struct 
    {
    	char name[20];
    	int age;
    	char phone[20];
    	char sex;
    }S;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么省略了结构体标签,结构体指针是否能够指向两个成员变量相同的结构体呢?

    举例:

    #include
    struct 
    {
    	int a;
    	int b;
    }sa;
    struct 
    {
    	int a;
    	int b;
    }*pasa;
    int main()
    {
    	pasa = &sa;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    当你尝试运行该程序,你会发现,编译并没有通过,编译器指出,无法实现将指针变量pasa的指向改变。
    在这里插入图片描述原因是即使上述两个编译器的成员变量是完全相同的,但是编译器依然会把二者当成完全不同的两个类型,所以是无法通过编译的。

    结构体的自引用:

    在结构中包含一个类型为该结构本身的成员是否可以呢?

    举例:

    在数据结构中,我们接触过链表,如下所示:
    在这里插入图片描述那么我们想用结构体表示链表,是否可以用以下这种表示方法?

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

    data包含存储的数据,struct Node n表示下一个节点,这样有点道理,但不多,原因是:struct Node n既包含了下个节点还包含了上个节点的数据等,这样反复包含,我们根本无法确定n的大小,那么sizeof(struct Node)的值是无法确定的,在此后,如果我们想使用该结构体,根本没有办法确定应该给它创建多大的空间。

    因此,我们可以得出一个结论:结构体类型可以包含除了自己以外的结构体变量。

    对于表示上述链表的正确代码应为:

    #include
    struct Node
    {
    	int data;//4个字节
    	struct Node*next;//定义一个类型为struct Node的指针,字节大小为4/8
    };
    int main()
    {
    	sizeof(struct Node);//字节大小为8-12
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    如下图所示:

    在这里插入图片描述

    对类型名重命名:

    举例:

    typedef struct Node
    {
    	int data;
    	struct Node*next;
    }Node1;//给struct Node起别名为Node1
    //起别名后:原始名和别名都可以直接使用
    int main()
    {
    	struct Node s;//使用原始名创建结构体变量
    	Node1 s1;//使用别名创建结构体变量
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对匿名结构体进行重命名:

    当我们省略原始名之后,此时的结构体就变成了匿名结构体,那么,我们能否使用别名定义指针?

    如下所示:

    typedef struct 
    {
    	int data;
    	Node1* next;
    }Node1;
    int main()
    {
    	Node1 s1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    答案是不可以的,程序由上自下进行,当程序运行到Node1* next;,结构体别名并未被定义,所以是不能使用这种方法进行的,因此对结构体进行重命名之后不要省略原始名。

    结构体变量的初始化:

    举例:

    #include
    struct Stu 
    {
    	int age;
    	char name[20];
    	char phone[20];
    };
    int main()
    {
    	struct Stu s={19,"张三","217361"};
    	printf("%d %s %s", s.age, s.name, s.phone);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    19 张三 217361
    
    • 1

    结构体嵌套初始化:

    #include
    struct Stu1 
    {
    	char sex[10];//注意这里的sex:单个汉字,不能将类型直接定义为%c
    	//原因:汉字由两个字符组成,%c根据ascll码值只能识别数字来打印,如果我们使用%c打印单独的汉字那么就会乱码
    	char address[20];
    };
    struct Stu 
    {
    	int age;
    	char name[20];
    	char phone[20];
    	struct Stu1 s1;//结构体Stu1作为结构体Stu的成员变量
    };
    int main()
    {
    	struct Stu s = { 19,"张三","217361",{"男","北京市海淀区"}};
    	//结构体嵌套初始化时需要注意:成员变量为结构体时,初始化的数据需要用括号括起来
    	printf("%d %s %s %s %s", s.age, s.name, s.phone,s.s1.sex,s.s1.address);
    	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 Stu1 
    {
    	char c1;
    	int a;
    	char c2;
    };
    struct Stu2 
    {
    	char c1;
    	char c2;
    	int a;
    };
    int main()
    {
    	struct Stu1 s1 = { 0 };
    	printf("%d\n", sizeof(s1));
    	struct Stu2 s2 = { 0 };
    	printf("%d\n", sizeof(s2));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们以Stu1为例进行分析:

    结构体的对齐规则:

    1:第一个成员在与结构体变量偏移量为零的地址处

    分析如下:
    在这里插入图片描述

    2:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    对齐数=编译器默认的一个对齐数与该成员大小的较小值。

    vs中默认的值为8,而对于gcc编译器来说,是没有默认对齐数的,因此对齐数就是该成员大小
    在这里插入图片描述注:变量和变量之间的空间会被浪费

    3:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
    在这里插入图片描述

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

    举例:

    struct Stu3 
    {
    	double c1;
    	char a;
    	int c2;
    };
    struct Stu4 
    {
    	char c1;
    	struct Stu3 s3;
    	double d;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    分析如下:在这里插入图片描述

    为什么存在内存对齐?

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

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

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

    分析如下:
    在这里插入图片描述在设计结构体的时候,如果我们既要满足对齐还要满足节省空间,那么就需要让占用空间小的成员尽量集中在一起。

    举例:

    struct Stu3 
    {
    	char c;
    	int a;
    	char c2;
    };
    struct Stu4 
    {
    	char c1;
    	char c2;
    	int a;
    };
    //Stu4所占的空间少一些,因为浪费的空间少
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    修改默认对齐数:

    方法:#pragma pack(4)//设置默认对齐数
    #pragma pack()//取消设置的默认对齐数

    修改之后,浪费的空间数就会减少。

    举例:

    #pragma pack(4)//设置默认对齐数为4
    struct Stu3 
    {
    	char c;
    	double b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果为

    12
    
    • 1

    取消设置的默认对齐数

    输出结果为

    16
    
    • 1

    这样操作之后,空间就节省了4个字节,因此,结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。

    offsetof(structName,memberName):

    用来计算成员变量的偏移量,它的头文件是

    举例:

    #include
    #include
    struct Stu3 
    {
    	char c;
    	int i;
    	double b;
    };
    struct Stu4 
    {
    	char c1;
    	char c2;
    	int a;
    };
    int main()
    {
    	printf("%d ", offsetof(struct Stu3, c));
    	printf("%d ", offsetof(struct Stu3, i));
    	printf("%d ", offsetof(struct Stu3, b));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出如下所示:

    0 4 8
    
    • 1

    结构体传参:

    举例:

    #include
    struct Stu3 
    {
    	char c;
    	int i;
    	double b;
    };
    void Init(struct Stu3 tmp)//更改结构体Stu3的值
    {
    	tmp.c = 'b';
    	tmp.i = 0;
    	tmp.b = 13.14;
    }
    int main()
    {
    	struct Stu3 s3;
    	s3.c = 'h';
    	s3.i = 3;
    	s3.b = 23.45;
    	Init(s3);//结构体名传递
    	printf("%c ", s3.c);
    	printf("%d ", s3.i);
    	printf("%f ", s3.b);
    	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

    输出如下所示:

    为什么值没有发生改变呢?

    和我们之前在指针学习哪里是一样的,这里也是引用传递,编译器在函数Init中更改的只是Stu3的复制品,因此出Init函数之后值还是原来的值,并没有发生真正意义上的改变。

    在这里插入图片描述正确的传参方式应该是地址传递:

    代码可修改为:

    #include
    struct Stu3 
    {
    	char c;
    	int i;
    	double b;
    };
    void Init(struct Stu3* tmp)
    {
    	tmp->c = 'b';
    	tmp->i = 0;
    	tmp->b = 13.14;
    }
    void print(struct Stu3 tmp)
    {
    	printf("%c ", tmp.c);
    	printf("%d ", tmp.i);
    	printf("%f ", tmp.b);
    }
    void print1(struct Stu3* tmp)
    {
    	printf("%c ", tmp->c);
    	printf("%d ", tmp->i);
    	printf("%f ", tmp->b);
    }
    int main()
    {
    	struct Stu3 s3;
    	s3.c = 'h';
    	s3.i = 3;
    	s3.b = 23.45;
    	Init(&s3);//通过地址传递,进行结构体变量的修改
    	print(s3);//通过值传递,打印变量
    	print1(&s3);//通过地址传递,打印变量
    	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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    此时的输出结果正是我们修改后的值:
    在这里插入图片描述但是在打印修改后的变量时,我们也选用了两种方法,值传递和地址传递,那么这两种方法,那种好一些呢?

    答案是:地址传递

    原因:函数进行传参时,参数是需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致性能的下降。

    因此,结构体传参的时候,要传结构体的地址。

  • 相关阅读:
    webrtc相关介绍
    微信小程序1(代码构成和基础组件和协同开发)
    推荐一个对pytorch代码详细注释的github项目
    LC-3 汇编语言 Nim游戏
    目标检测应用场景和发展趋势
    Python经典书籍有哪些?这份书单送给你_黑马程序员
    性能测试工具有哪些?原理是什么?怎么选择适合的工具?
    老司机带带你,教你学会Java中又骚又暴力的“反射”技术
    jQuery|jQuery的基本使用
    买卖股票的最佳时机(系列)
  • 原文地址:https://blog.csdn.net/m0_64365419/article/details/126259924