【C语言】自定义类型结构体详细讲解

遇到困难时不要抱怨,既然改变不了过去,那么就努力改变未来!💓💓💓

目录

•🌙知识回顾

• 🍋知识点一:结构体类型的声明

​编辑 • 🌰1.结构体的声明

​编辑• 🌰2.结构体变量的创建和初始化

​编辑• 🌰3.结构的特殊声明

​编辑 •🌰4.结构的自引用

​编辑• 🌰5.结构体成员访问操作符

• 🍋知识点二:结构体内存对齐

• 🌰1.对齐规则

 • 🌰2.为什么存在内存对齐?

 • 🌰3.修改默认对齐数

 • 🌰3.结构体传参

• 🍋知识点三:结构体实现位段

  • 🌰1.什么是位段

  • 🌰2.位段的内存分配

  • 🌰3.位段的跨平台问题 

  • 🌰4.位段的应用

  • 🌰5.位段使用的注意事项

•🌙SumUp 结语


🌙知识回顾

亲爱的友友们大家好!💖💖💖,我们又在此刻相聚于这片文字的海洋。我们接着要进入C语言自定义类型的学习,一篇文章我们详细解析了数据在内存中的存储,里面包含了大小端字节序、浮点数在内存中的存储,希望大家能够熟练应用~

    

今天给大家带来的是自定义类型-结构体的知识,包括后面的联合与枚举,都属于C语言的自定义类型,希望大家好好学习,为以后数据结构的学习打下坚实的基础。也希望可以给大家带来帮助。

  

👇👇👇
💘💘💘知识连线时刻(直接点击即可)

  🎉🎉🎉复习回顾🎉🎉🎉
     数据在内存中的存储

  

 💘💘💘学习C语言的过程离不开刷题,这里🌷给大家推荐两个很好的学习刷题网站——力扣、牛客网,各种题目应有尽有,大家可以在上面挑选合适难度的题进行训练

👇👇👇

各位友友们!🎉🎉🎉点击这里进入牛客网🎉🎉🎉

👇👇👇

🎉🎉🎉这里是力扣🎉🎉🎉

 那话不多说,赶紧进入今天的内容吧!

 

• 🍋知识点一:结构体类型的声明


 C语言已经提供了内置类型。如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述一个学生,或描述一本书,这时单一的内置类型是不行的。描述一个学生需要名字、年龄、学号、身高、体重等;描述一本书需要作者、出版社、定价等。C语言为了解决这个问题,增加了结构体这种自定义类型,让程序员可以自己创造合适的类型。

结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体。

 • 🌰1.结构体的声明

结构体应该按照以下形式进行声明:

struct tag
{
	member-list;
}; variable-list;

比如我要描述一个学生,包含性别、年龄、性别、学号等:

struct Student
{
	char name[20];//姓名
	int age;//年龄
	char gender[6];//性别
	char id[13];//学号
    //...
};

再比如说我要描述一本书,包含书名、价格、作者等: 

struct Book
{
	char book_name[20];//书名
	int price;//价格
	char author[6]//作者
    //...
};

注意:大括号外的分号不能丢

• 🌰2.结构体变量的创建和初始化

以直角坐标系中的坐标为例,我们要描述直角坐标系中的坐标,应该包含横坐标x和纵坐标y,初始化代码演示:

#include <stdio.h>
struct Point
{
	int x;
	int y;
}p5, p6;
struct Point p3;
struct Point p4;
int main()
{
	struct Point p1;
	struct Point p2;

	return 0;
}

上面的代码一共给出了三种创建结构体变量的方式:

🔥第一种,直接在main函数内部创建,如p1、p2,这样创建出的结构体变量是局部变量

🔥第二种,在main函数外部创建,如p3、p4,这样创建出的结构体变量是全局变量。

🔥第三种,在声明的同时创建,也就是直接在结构体尾部分号前的变量列表中创建,此时不用重复写一遍结构体类型struct Point,直接写上变量名就行了,这样创建出来的结构体变量是全局变量。

当然在创建的时候我们可以给他一些值也是没问题的,在初始化的时候需要把结构体的每个成员放在大括号里:

#include <stdio.h>
struct Point
{
	int x;
	int y;
}p5, p6;
struct Point p3 = { 1, 2 };
struct Point p4 = { 3,5 };
int main()
{
	struct Point p1 = { 10, 20 };
	struct Point p2 = { 0, 0 };

	return 0;
}

以之前的学生为例,初始化学生的结构体变量代码演示:

#include <stdio.h>
struct Student
{
	char name[20];
	int age;
	char gender[6];
	char id[13];
};
int main()
{
	struct Student stu1 = { "张三",20,"男","20240505116" };

	return 0;
}

结构体初始化需要按照结构体声明时成员变量的次序依次初始化,那能不能不按照顺序初始化呢?其实是可以的,代码演示:

struct Student
{
	char name[20];
	int age;
	char gender[6];
	char id[13];
};
int main()
{
	struct Student stu1 = { "张三",20,"男","20240505116" };
	struct Student stu2 = { .age = 18,.name = "李四",.id = "20250302117",.gender = "男" };

	return 0;
}

我们初始化stu2时没有按照顺序,那就必须用点操作符(.)指出初始化的成员变量是谁。但是这种初始化方式比较苛刻,在VS上后缀为.cpp的文件就没办法这样初始化。

• 🌰3.结构的特殊声明

有些结构体在声明时会省略掉结构体标签(tag),这属于不完全的声明,或特殊声明,该结构体称为匿名结构体

#include <stdio.h>

struct
{
	char c;
	int i;
	double d;
}s = { 'x',100,3.14 };

int main()
{
	printf("%c %d %f\n", s.c, s.i, s.d);

	return 0;
}

注意:匿名结构体类型不完整,只能在变量列表中创建并使用一次,第二次往后就不能再用了。

并且匿名结构体还会有以下问题:
 

#include <stdio.h>
struct
{
	char c;
	int i;
	double d;
}s;
struct
{
	char c;
	int i;
	double d;
}*ps;
int main()
{
	ps = &s;
	return 0;
}

上面的代码是不能够被执行的,匿名结构体指针不能接收匿名结构体变量的地址,编译器不认识,会报警告:

warning C4133: "=": 从"*"到"*"的类型不兼容 

🔥编译器会把上面的两个声明当成完全不同的两个类型,是非法的。

🔥匿名的结构体类型,如果没有对结构体类型重命名的话,基本只使用一次。

 •🌰4.结构的自引用

结构体中还可以包含结构体作为自己的成员变量,链表就是结构体自引用的典型代表,如定义一个链表的节点:

struct SListNode
{
	int data;
	struct SListNode next;
};

但是上面这个代码是不正确的,因为如果这样定义结构体那么sizeof(struct SListNode)的大小将会无穷大

链表的节点包含数据域data和指针域next, data 是存放在该节点处的数据,而 next 是指向下一个节点的指针,而不是直接是结构体本身。所以正确的定义方法应该是:

struct SListNode
{
	int data;
	struct SListNode* next;
};

同时,在结构体自引用的时候夹杂 typedef 重定义的时候要注意不要在结构体内部就使用重定义,这时错误的:

typedef struct SListNode
{
	int data;
	SListNode* next;
}SListNode;

原因是因为 SListNode 是对上面结构体的重命名,在还没有这个结构体类型的时候就在内部用重定义的方法定义结构体类型,那肯定是不行的,必须写完整:

typedef struct SListNode
{
	int data;
	struct SListNode* next;
}SListNode;

然后在后面在想创建这个类型的结构体变量就可以直接使用 SListNode 代替 struct SListNode 了。

注意:匿名结构体类型是不能实现结构体自引用的效果的。

• 🌰5.结构体成员访问操作符

🔥结构体成员的直接访问

结构体成员的直接访问是通过点操作符(.)访问的,点操作符接受两个操作数,还是以前面的平面直角坐标系上的点作为例子,究竟如何访问呢?代码演示:

#include <stdio.h>
struct Point
{
	int x;
	int y;
};
int main()
{
	struct Point p = { 1,2 };
	printf("x=%d y=%d\n", p.x, p.y);
    //设置坐标的值
    scanf("%d %d", &(p.x), &(p.y));
    printf("x=%d y=%d\n", p.x, p.y);
	return 0;
}

使用方式 :结构体变量.成员名

🔥结构体成员的间接访问

但是有的时候我们得到的并不是结构体变量本身,而是结构体变量的地址,通过这个结构体指针访问结构体变量的内容,称为结构体成员的间接访问,需要用这样一个形式:结构体指针->成员名 来进行间接访问 。代码演示:

#include <stdio.h>
struct Point
{
	int x;
	int y;
};
void SetPoint(struct Point* ptr)
{
	//scanf("%d %d", &((*ptr).x), &((*ptr).y));
	scanf("%d %d", &(ptr->x), &(ptr->y));
}
int main()
{
	struct Point p = { 1,2 };
	printf("x=%d y=%d\n", p.x, p.y);
	//设置坐标的值
	SetPoint(&p);
	printf("x=%d y=%d\n", p.x, p.y);
	
	return 0;
}

使用方式 :结构体变量->成员名

 SetPoint 函数接收结构体指针,用指针访问结构体成员,有上面两种方法,不过先解引用再用点操作符显然比较麻烦,我们直接用->就可以了。

• 🍋知识点二:结构体内存对齐


 结构体内存对齐探讨的其实是结构体大小的计算。上面已经详细说明了结构体的基本使用了,我们现在就来深入探讨这个问题。

举例:计算一下,下面结构类型 struct S1 的大小

#include <stdio.h>
struct S
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%zd\n", sizeof(struct S));
	
	return 0;
}

可能你会觉得,这个c1是 char 型的,占一个字节,i是 int 型的,占四个字节,c2也占一个字节,那结构体大小是6个字节吗?

输出结果:12

这是为什么呢?原因就是因为结构体它的成员在内存中存放的时候,会对齐到一些边界上,然后进行存放。那结构体到底是如何存储它的成员,到底是如何对齐的呢?

• 🌰1.对齐规则

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

🔥结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。

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

对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值,即O = min { D , M }

 VS 中默认的值是 8 

 Linux 中 gcc 没有默认对齐数,对齐数就是成员自身的大小

🔥结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的,即max {O1,O2…On })的整数倍。

🔥如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(函嵌套结构体中成员的对齐数)的整数倍。

💯图解:

💯分析:

假设结构体S创建的变量从图示从上到下第一个红线位置开始存储,根据对齐规则, char 类型的c1必须在第一个字节的位置(偏移量为0的地址处) ,而后 int 类型的i大小为4个字节,VS默认的对齐数为8,所以取较小值4,也就是从第五个字节的位置(对齐到4的整数倍地址处,偏移量为4的整数倍)开始存储4个字节,最后 char 类型的c2大小为1个字节,1和8取1为对齐数,任何地址都是1的整数倍,所以直接存储在后一个字节即可。但是这样算整体占9个字节,而结构体三个成员变量的对齐数分别是1、4、1,最大对齐数为4,那么最后结构体变量的大小必须是4的整数倍,离9最近且大于9的是4的整数倍的数为12,所以结构体的大小为12。

我们再看另外一个例子:

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

💯图解:

根据上面两个例子,我们能发现一个规律:当结构体成员变量相同时,让占用空间小的成员尽量集中在一起可以在满足对齐的情况系节省更多的空间。如上面第一种存储方式占12个字节,但第二种存储方式只占了8个字节。 

 我们再来看一个结构体嵌套的情况:

struct S3
{
 double d;
 char c;
 int i;
};
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};

如果我们要求出S2的内存大小,就要先求出S3的内存大小:

💯图解:

💯分析:

S3的分析和之前一样,先把 double 类型的d的八个四节填满,而后再 char 类型的c2的一个字节继续往后存储一个字节(1和8取1为对齐数,任何地址都是1的整数倍),最后需要将 int 类型的i对齐到4的整数倍处(偏移量为12的地址处)继续存储四个字节,这时结构体变量的大小为15个字节,结构体成员的最大对齐数为8,结构体变量的大小应该为8的整数倍,所以为16。

解下来我们分析S4,特别注意处理结构体变量s3的时候,s3的对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(函嵌套结构体中成员的对齐数)的整数倍。

💯图解:

💯分析: 

三者对齐数分别为1、8、8,分别填充1个字节、16个字节、8个字节,分析方法同上,以S4类型创建的变量大小应该为32个字节。

通过以上例题的分析,相信大家对如何计算结构体的大小已经是炉火纯青了~

 • 🌰2.为什么存在内存对齐?

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

🔥平台原因

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

🔥性能原因

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证所有的 double 类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在8个字节内存块中。

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

 • 🌰3.修改默认对齐数

#pragma 这个预处理指令,可以修改编译器的默认对齐数。

#include <stdio.h>
#pragma pack(1)//设置默认对齐数数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
	printf("%d\n", sizeof(struct S));

	return 0;
 }

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

 • 🌰3.结构体传参

我们一起来看分析以下代码:

#include <stdio.h>
struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };

//结构体传参
void Print1(struct S temp)
{
	printf("%d\n", temp.num);
}
//结构体地址传参
void Print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	Print1(s); //传结构体
	Print2(&s); //传地址

	return 0;
}

上面的 Print1 和 Print2 分别是结构体传值和传址调用的用来打印结构体成员 num 的函数。那么是Print1 好还是 Print2 好呢?

💯图解:

💯分析:  

 注意,当我们传值调用的时候,函数形参会创建一份和结构体变量大小相同的临时空间,而结构体变量的大小由于包含了一个含有1000个整型元素的数组,所以空间是非常大的,再创建一份相同大小的空间明显消耗很大;而如果我们使用传址调用,只会创建一个4或8个字节的指针变量指向结构体空间,并不会创建临时空间,节省了很大的空间,所以 Print2 更好些。

💯原因描述:

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

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

结论:结构体传参的时候,要传结构体的地址。

• 🍋知识点三:结构体实现位段


结构体可以应用于实现位段。

  • 🌰1.什么是位段

位段的声明与结构体是类似的,但是有两个不同的地方:

🔥位段的成员必须是 int 、 unsigned int 或 signed int ,在C99中位段成员的类型也可以选择其他类型。

🔥位段的成员后边有一个冒号和一个数字。

举例:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

我们可以对比一下结构体和位段,假设上面是一个结构体,那么有四个 int 型的变量,每个占四个字节,但如果变量中存储的值是1、2、3,在内存中只需要1~2个bit位就能够存储,而四个字节有32bit 位,这样就浪费了内存。所以在位段中,我们再变量的后面加上冒号,后面在加上变量占位bit 位的个数,相较于结构体,又减少了内存的占用。

  • 🌰2.位段的内存分配

那我们应该如何计算出位段的大小呢?

首先有一些是我们需要注意的:

🔥位段的成员可以是 int、 unsigned int 、 signed int 或者是 char 等类型。

🔥位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。

🔥位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

#include <stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;

	printf("%zd\n", sizeof(s));

	return 0;
}

💯图解:

💯分析:   

当我们分析这个位段式结构的内存时,一定要理解位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。上面位段结构的成员变量都是 char 型的,所以按照1个字节的方式开辟。先给上一个字节也就是 8bit 位,这个时候有个问题:空间从左向右开辟还是从右向左开辟?遗憾的是,C语言标准并没有给出规定,那么就由编译器来决定了。在VS上,规定空间是由右向左开辟的。所以从右边开始我们依次位变量开辟空间,a占3 bit 位,b占4 bit 位,此时一共7 bit 位。如果此时再加上 c 的5 bit 位,那一个字节就容纳不下了,所以我们直接再创建一个字节,重新从右向左开辟空间,同时上一个字节剩余的1个 bit 位就浪费掉了。同理,第二个字节为 c 开辟了5个 bit 位的空间后再为 d 开辟4个 bit 大小的空间,一个字节就容纳不下了,所以再创建一个字节,在下一个字节从右向左为 d 重新开辟一个 4 bit 位大小的空间。

此时,整个位段在内存中仅仅占用了3个字节。

接下来在 main 函数中对位段中的成员进行了初始化

💯图解:

 没有被占用内存的地方为0,则内存中存储的序列转化为十六进制就是 0x620304 ,我们可以调试以进行验证:

希望阅读完上面的内容你可以独立回答以下问题🎉

🔥位段申请到一块内存中,从左还是从右开始创建?

🔥剩余的空间,不足下一个成员使用的时候,是浪费呢?还是继续使用?

  • 🌰3.位段的跨平台问题 

🔥int 位段被当成有符号数的还是无符号数是不确定的。

🔥位段中最大位的数目不能确定(32位和64位的机器上 int 的长度是4个字节,而在16为机器上 int 的长度是2个字节),写成27,在16位机器会出现问题。

🔥位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。

🔥当一个结构包含两个位段,第二个位段的成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余还是利用,这时不确定的。

总结:根结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

  • 🌰4.位段的应用

例如我们在微信或qq上给对方发送消息,这个消息数据发送,需要经过一系列操作对方才能接收到。就好比你要邮寄一本书,不可能将数扔到大马路上对方就能得到,你需要对书进行封装,写上寄件人、邮件人、地址等信息,然后进行邮寄。

🔥IP数据报

当我们把数据封装打包完后,它就是IP数据报。下面网络协议中,IP数据报的格式,我们可以看到其中很多的数学只需要几个 bit 位就能描述,而且看宽度从0~31刚好一个字节的大小,这里使用位段,既可以实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是由帮助的。

+

上面IP数据包中粉色部分的源IP地址目的IP地址相当于邮寄快递时的邮寄地址和接收地址。

  • 🌰5.位段使用的注意事项

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的 bit 位是没有地址的。所以不能对位段的成员使用 & 操作符,这样就不能使用 scanf 直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值为位段的成员

💯错误代码演示: 

#include <stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//这是错误的

	return 0;
}

💯正确代码演示: 

#include <stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	int b = 0;
	scanf("%d", &b);
	sa._b = b;

	return 0;
}

•🌙SumUp 结语

C语言自定义类型结构体的学习到这里就结束啦~后面就会进入联合和枚举类型的学习。在这里希望大家能够将前面的C语言的的基础知识进行回顾复习,使整个纯C学习是连贯的,这样更加利于我们的学习和理解以及继续拓展。

 

如果大家觉得有帮助,麻烦大家点点赞,如果有错误的地方也欢迎大家指出~

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值