前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【自定义类型详解】第一篇——结构体详解

【自定义类型详解】第一篇——结构体详解

作者头像
YIN_尹
发布2024-01-23 15:44:29
1170
发布2024-01-23 15:44:29
举报
文章被收录于专栏:YIN_尹的博客YIN_尹的博客

从这篇文章开始,我们来学习C语言中的自定义类型(构造类型),今天来看第一种自定义类型——结构体,一起来学习吧!!!

1.认识结构体

前面我们已经学习过了很多的数据类型,整型、浮点型、指针类型等等。

1.1为什么要学习结构体类型

已经有这么多数据类型了,那我们为什么还要学习结构体类型呢?

因为在开发的过程中,我们有时候难免要去描述一些复杂的对象,而想要描述这些对象,我们再使用之前学过的int,double等这些类型可能就不适用了。 比如我们想要描述一本书,对于书这个类型来说,它具有的特征不止一个,我们要想去描述一本书的话,可能要给出书的书名,书的作者,书的定价等等这些信息。 这时如果我们只用一个int,double,char类型的数据好像无法描述。 这时候,我们就需要使用结构体来描述了。 因此,结构体作为一种自定义类型,使得我们有能力去描述复杂类型。

1.2什么是结构体

那既然结构体这么牛,结构体到底是什么呢?

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

2.结构体的声明

我们已经知道结构体是什么了,那如果我们想用结构体来描述一个学生该怎么做呢? 首先我们要进行结构体的声明。 如果我们想要描述一个学生,那我们就先来声明一个学生类型,怎么声明呢?

结构体声明的语法是:

代码语言:javascript
复制
struct tag
{
	member - list;
} variable - list;

每一部分都是什么意思呢?解释一下:

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

光看这个图,可能还不是特别明白,给大家举个例子就明白了。

我们就来声明一个学生类型,指定该学生类型拥有的成员变量有姓名,年龄,性别,学号。

代码语言:javascript
复制
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}; //分号不能丢

我们看到上面没有变量列表variable - list,这个是可选的。

3.结构成员的类型

结构体的成员变量可以是什么类型呢?

结构的成员可以是标量、数组、指针,甚至是其他结构体。

代码语言:javascript
复制
struct SS
{
	int a;
	char b;
	float c;
};
struct Stu
{
	char ch;
	int* p;
	double arr[10];
	struct SS s1;
}; 

4.结构体变量的定义

有了结构体类型,那如何定义变量,其实很简单。 定义结构体的方式有两种:

4.1 在声明结构体类型的同时定义结构体变量

即在声明类型的}后面直接创建结构体变量,但要注意这里创建的结构体变量是全局变量。

举个例子:

代码语言:javascript
复制
struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
4.2用声明过的结构体类型定义结构体变量
代码语言:javascript
复制
struct Point
{
	int x;
	int y;
};
struct Point p2;
int main()
{
	struct Point p3;
	return 0;
}

5.结构体变量的初始化

初始化,即在定义变量的同时赋初值。 和数组一样,初始化结构体变量也用的是{}

举个例子:

代码语言:javascript
复制
struct Point
{
	int x;
	int y;
}p1 = { 2,3 };
代码语言:javascript
复制
struct Stu 
{
	char name[15];//名字
	int age; //年龄
};
int main()
{
	struct Stu s = { "zhangsan", 20 };//初始化
	return 0;
}
5.1结构体嵌套的初始化
代码语言:javascript
复制
#include <stdio.h>
struct Point
{
	int x;
	int y;
}p1 = { 2,3 };

struct Node
{
	int data;
	struct Point p;
}n1 = { 10, {4,5},}; //结构体嵌套初始化

int main()
{
	struct Node n2 = { 20, {5, 6}};//结构体嵌套初始化
	printf("%d %d %d", n1.data, n1.p.x, n1.p.y);
	return 0;
}

再嵌套一个大括号来初始化被嵌套的那个结构体。

打印一下看看:

在这里插入图片描述
在这里插入图片描述
5.2指定成员变量初始化

在初始化结构体的时候,我们可以不按成员变量的顺序去初始化,可以指定某个成员初始化,按我们想要的顺序初始化。

举个例子:

代码语言:javascript
复制
struct Stu 
{
	char name[15];//名字
	int age; //年龄
};
int main()
{
	struct Stu s = {.age=33,.name="zhangsan"};//指定成员初始化
	printf("%d %s", s.age, s.name);
	return 0;
}

打印出来看看:

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

这样也是可以的。

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

除了上面介绍的结构体声明方式之外,还有一种特殊的结构体声明。

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

那这个不完全声明又是什么意思呢?

就是在声明一个结构体的时候的时候省略掉结构体标签(tag),或者说该结构体没有类型名。 也称为匿名结构体类型。

举个例子:

代码语言:javascript
复制
struct
{
	int a;
	char b;
	float c;
}x;

那么匿名结构体类型有什么特点呢?

因为匿名结构体类型没有类型名,所以匿名结构体类型只能在定义的时候创建结构体变量,后面再想利用这个结构体类型创建变量就做不到了。

代码语言:javascript
复制
struct
{
	int a;
	char b;
	float c;
}x;//创建匿名结构体变量x

这样是可以的。

代码语言:javascript
复制
struct
{
	int a;
	char b;
	float c;
};
int main()
{
	struct s;
	return 0;
}
在这里插入图片描述
在这里插入图片描述

这样是不行的。

然后,我们再来分析一段代码:

代码语言:javascript
复制
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}*p;
int main()
{
	p = &x;
	return 0;
}

大家思考一下,这样写,可以吗? 这样写是不行的,编译器会报警告的。

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

虽然上面两个匿名结构体类型的成员变量完全一样,但是编译器会把上面的两个声明当成完全不同的两个类型。 所以是非法的。

7.结构体成员的访问

对于结构体成员的访问,不同的情况下可以有不同的访问方式,一般可以分为两种:

7.1结构体变量访问成员

通过结构体变量访问成员是通过点操作符(.)访问的。点操作符接受两个操作数。 语法:结构体变量.成员变量

举个例子:

代码语言:javascript
复制
#include <stdio.h>
struct Stu
{
	char name[15];
	int age; 
};
int main()
{
	struct Stu s = { "zhangsan", 20 };
	printf("%s %d", s.name, s.age);
	return 0;
}
在这里插入图片描述
在这里插入图片描述

看看结果:

在这里插入图片描述
在这里插入图片描述
7.2结构体指针访问指向变量的成员

有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。 那该如何访问成员?

两种方式:

  1. 我们可以对该结构体指针解引用,这样就找到了对应的结构体变量,然后我们就可以使用(.)操作符来访问成员变量了。
  2. 那我们可不可以直接通过结构体指针访问对应结构体的成员变量呢? 当然可以。 这时候我们可以使用->操作符来实现。 语法:结构体指针->成员变量

一起来看一个例子:

代码语言:javascript
复制
struct Stu
{
	char name[15];
	int age;
};

void print(struct Stu* ps) {
	printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
	//使用结构体指针访问指向对象的成员
	printf("name = %s   age = %d\n", ps->name, ps->age);
}

int main()
{
	struct Stu s = { "zhangsan", 20 };
	print(&s);//结构体地址传参
	return 0;
}
在这里插入图片描述
在这里插入图片描述

看看结果:

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

两种方式都可以成功访问。

8.结构体的自引用

首先,我们来思考一个问题:

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

像这样:

代码语言:javascript
复制
struct Node
{
 int data;
 struct Node next;
};

这样是否可行,如果可行,那sizeof(struct Node)是多少? 如果这样写,我们去计算struct Node的大小时,需要计算成员里面一个同类型的结构体struct Node next的大小,而在计算它的大小时,发现里面还包含一个自己,这样的话就会无限套娃下去,是不是没法计算啊。

那应该怎么写,我们可以考虑这样做:

代码语言:javascript
复制
struct Node
{
 int data;
 struct Node* next;
};

换成一个同类型的结构体指针,这样它就指向了一个类型为该结构体本身的结构体变量作为成员。

通过这样一个问题,我们引出一个新的概念:

结构体的自引用:在结构体内部,包含指向自身类型结构体的指针。 数据结构中链表的结点其实就是这样搞的。

9.结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题: 计算结构体的大小。 这也是一个特别热门的考点: 结构体内存对齐

首先我们先来探讨一个问题,大家思考一下下面这两个结构体S1,S2的大小是多少?

代码语言:javascript
复制
#include <stdio.h>
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;
}

先给大家说一下我在第一次看到这个问题时是怎么算的吧。

我想的是,结构体包含的所有成员的大小之和就是结构体的总大小。 首先是S1,三个成员变量,第一个c1是char类型,1个字节,第二个i是int类型,4个字节,第三个c2也是char类型,1个字节。所以S1的大小为1+4+1=6个字节。 那S2的话成员和S1一模一样,只是顺序不同,所以大小也应该是6个字节。 是这样吗?

我们来看一下运行结果到底是多少?

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

结果是12和8,我们算错了,那说明像上面那样计算是不对的。

那结构体的大小到底要怎么计算才是正确的呢?

要解决这个问题,我们就需要掌握——结构体内存对齐

9.1内存对齐规则

我们接着讨论上面计算结构体大小的问题。

就拿S1来说,如果我们用struct S1创建一个结构体变量s(当然s的大小和结构体类型struct S1的大小肯定是一样的),那内存肯定要为s分配空间,我们简单画一个图。

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

那s的成员要怎么往内存里边存放呢?成员c1,i,c2分别应该放在那个位置呢?为什么s的最终大小是12个字节呢?

这就需要我们了解一下结构体内存对齐的规则:

规则1
  1. 结构体的第一个成员直接对齐到相对于结构体变量起始位置偏移量为0的地址处

这里解释一下什么是偏移量:

偏移量,计算机汇编语言,是指把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为“有效地址或偏移量”。

这里规则1中说的是相对于结构体起始位置的偏移量,画个图 大家就明白了:

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

那我们怎么知道结构体每个成员相对于起始位置的偏移量呢?

对于s来说,根据规则1我们知道第一个成员c1直接对齐到对于起始位置偏移量为0的地址处,那i和c2呢,它们的偏移量又如何得知呢? 这里给大家介绍一个宏——offsetof。 offsetof可以用来计算结构体中的成员相对于结构体起始位置的偏移量。

我们来简单认识一下,它在cplusplus.com也可以查询到:

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

注意需要包含头文件#include <stddef.h>

那我们现在就可以借助offsetof来看一下i和c2的偏移量:

代码语言:javascript
复制
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;
}
在这里插入图片描述
在这里插入图片描述

好的,是0,4,8,那知道了偏移量,我们就知道i和c2应该放在什么位置了。

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

好,现在我们已经知道s的成员在内存中的存放了,那s的大小是12个字节,这样的话图中红色空间的6个字节根本没有使用,不是白白被浪费掉了嘛。

那既然这样放了,就一定有它的原因,这些都是我们接下来要探讨的问题。

规则2
  1. 从第二个成员变量开始,要对齐到当前对齐数的整数倍的偏移处

那这里又出现一个新的概念——对齐数。

对齐数:结构体成员自身大小与当前环境下默认对齐数中的较小值。 这里我们使用的是vs2022。 vs环境下:默认对齐数为8。 Linux环境下:无默认对齐数,对齐数取结构体成员自身大小。

那现在根据第二条规则,我们就可以计算i和c2应该对齐到那个位置了。 先看i:

对于i来说,i的类型为int,4个字节,而当前环境(vs)下的默认对齐数是8,4<8,所以对于i来说,对齐数就是4。 而i的前面,c1放到了偏移量为0的位置,而且之占了1个字节,所有0后面,偏移量为1,2,3,4…处都可以用。 但根据规则i应该放到偏移量为4的位置,因为0之后第一个4个倍数就是4。

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

这也与我们之前看到的结果一致。

接着我们看c2:

i放到偏移量为4的位置,i为为int,占4个字节(4,5,6,7),那后面偏移量为8,9…的位置都是可用的。 对于c2来说,char类型,自身大小1个字节,默认对齐数8,1<8,所以c2的对齐数是1,那8就是1的整数倍啊,所以c2放到偏移量为8的位置就行。

这样一分析,我们就知道为什么成员c1,i,c2的偏移量是0,4,8了。

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

那现在又有一个问题,s的最后一个成员c2放在偏移量为8的位置,而且只占1个字节,那为什么结构体s的总大小为12个字节呢? 要解决这个问题,我们来看内存对齐的第3条规则:

规则3
  1. 结构体的总大小,必须是最大对齐数(即所有成员变量的对齐数中的最大值)的整数倍

s的3个成员c1,i,c2。 i,c2的对齐数我们上面计算过了,是1,4。c1大小跟c2一样,所以c1的对齐数应该也是1。 那它们之中最大的对齐数就是4,所以结构体s的大小应该是4的整数倍。 而最后一个成员c2放在了偏移量为8的位置,从0到8,已经占用了9个字节,再往后第一个4的倍数就是12,所以s的最终大小是12。 即偏移量是从0到11。

这与我们上面得到的结果是一致的。

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

s是用struct S1创建的,所以大小就是12。

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

下面我们再来看一个结构体,试着计算一下它的大小是多少:

代码语言:javascript
复制
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

大家思考一下,struct S4的大小应该是多少呢?

我们看到,struct S4里面的一个成员是用struct S3创建的一个结构体变量s3。

那我们先来计算一下s3的大小和每个成员的对齐数吧,正好练习一下: 直接上图吧:

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

那接下来我们是不是应该思考一下,被嵌套的这个结构体s3,它应该对齐到哪个位置呢?

要解决这个问题,就需要我们了解第4条规则了。

规则4
  1. 对于嵌套结构体的情况,嵌套的结构体需要对齐到自己的最大对齐数的整数倍处,结构体的总大小是最大对齐数(含被嵌套结构体的对齐数)的整数倍。

好,那知道了第4条规则,我们就来计算一下struct S4的大小:

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

是32字节吗?我们来验证一下:

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

两个大小我们算的都是正确的。

9.2为什么存在内存对齐?

现在我们已经知道了内存对齐的规则,那大家有没有想过一个问题:

为什么要规定一些这么复杂的规则去搞这个内存对齐呢? 直接让结构体的成员紧挨着在内存中存放不行吗,搞一个内存对齐,不仅计算结构体大小麻烦,而且还浪费空间,有什么用啊?

那么,既然它存在,就一定又它的道理,那到底是因为什么呢?

大部分的参考资料都是如是说的:

  1. 平台原因(移植原因):

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

  1. 性能原因:

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

总体来说:

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

9.3如何设计结构体

我们回过头来看最开始我们计算大小的两个结构体:

代码语言:javascript
复制
#include <stdio.h>
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;
}
在这里插入图片描述
在这里插入图片描述

我们发现,这两个结构体的成员变量是一样的,只是顺序不同,但是,它们的大小却相差了4个字节。 我们知道,这时内存对齐造成的。

那现在有一个问题: 如何设计结构体,可以既满足对齐,又节省空间?

对比struct S1和struct S2可以发现,S2的大小比S1小了4个字节,而S2与S1的不同之处在于S2中占用空间小的成员放在了一起。

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

所以,在设计结构体时,我们要:

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

9.4 修改默认对齐数

我们已经知道了在vs环境下,默认的对齐数是8,那如果我们想修改这个默认对齐数,能不能做到呢?

是可以的! 使用 #pragma 这个预处理指令,可以修改默认对齐数。

那怎么使用呢?

#pragma pack(n) ——设置默认对齐数为n #pragma pack()——取消设置的默认对齐数,还原为默认 末尾不需要加分号(;)

代码语言:javascript
复制
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
struct S4
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S2));
	printf("%d\n", sizeof(struct S4));
	return 0;
}

我们计算两个结构体类型的大小,它们的成员变量包括顺序都是一模一样的,唯一的区别在于: 我们用#pragma pack(1)将设置默认对齐数为1,然后声明了struct S2类型; 又用#pragma pack()取消修改的默认对齐数,还原为默认值,然后声明了struct S4类型。 这两个结构体类型声明时对应的默认对齐数是不同的,所以大小计算出来也应该时不一样的。

我们打印出来看看:

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

确实如此。

大家可以计算一下,它们的大小和修改前后的对齐数时相对应的。

结论:

结构体在对齐方式不合适的时候,我么可以自己更改默认对齐数。

10.结构体传参

我们之前在学习函数的时候,知道函数调用有两种方式——传值调用和传址调用。 那我们将结构体作为函数参数进行传参也是这样:

1.传值调用:直接将结构体变量作为实参传递给形参,形参将是实参的一份临时拷贝。 2.传址调用:将结构体变量的地址作为实参传递给形参,用一个结构体指针接收,传址调用可以通过形参改变结构体变量的值,而传值调用不能。

上代码:

代码语言:javascript
复制
#include <stdio.h>
struct S 
{
	int data[1000];
	int num;
};

//传值
void print1(struct S s) 
{
	printf("%d\n", s.num);
}

//传址
void print2(struct S* ps) 
{
	printf("%d\n", ps->num);
}

int main()
{
	struct S s = { {1,2,3,4}, 1000 };
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

函数print1为传值调用,函数print2为传址调用。

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

思考一个问题,上面的 print1 和 print2 函数哪个好些?

首选print2函数。

为什么呢?

原因是:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。 而如果我们传的是地址,地址无非就是4或8个字节,那参数压栈的开销就比较小,效率可能就会提高很多。

因此,我们得出结论:

结构体传参的时候,传结构体的地址比较好。

好了,以上内容就是对结构体的一个详细讲解,欢迎大家指正!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.认识结构体
    • 1.1为什么要学习结构体类型
      • 1.2什么是结构体
      • 2.结构体的声明
      • 3.结构成员的类型
      • 4.结构体变量的定义
        • 4.1 在声明结构体类型的同时定义结构体变量
          • 4.2用声明过的结构体类型定义结构体变量
          • 5.结构体变量的初始化
            • 5.1结构体嵌套的初始化
              • 5.2指定成员变量初始化
              • 6.特殊的声明(匿名结构体类型)
              • 7.结构体成员的访问
                • 7.1结构体变量访问成员
                  • 7.2结构体指针访问指向变量的成员
                  • 8.结构体的自引用
                  • 9.结构体内存对齐
                    • 9.1内存对齐规则
                      • 规则1
                      • 规则2
                      • 规则3
                      • 规则4
                    • 9.2为什么存在内存对齐?
                      • 9.3如何设计结构体
                        • 9.4 修改默认对齐数
                        • 10.结构体传参
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档