电脑基础 · 2023年3月19日

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

我们知道,C语言是允许我们自己来创造类型的,这些类型就叫做——自定义类型。

自定义类型又包括结构体类型,联合体类型还有枚举类型。

今天的文章,我们就着重讲解这其中的结构体类型。结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

目录

结构体的声明

1.1结构的基础知识

1.2结构的声明

1.3 匿名结构体的情况

1.4结构的自引用

 1.5重命名匿名结构体的情况

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

 1.7 结构体内存对齐

1.8为什么存在内存对齐?

1.9我们可以耍些小聪明达到节省空间的效果。

2.1修改默认对齐数

2.2 结构体传参

3.1位段

3.2 位段的内存分配

3.3 位段的跨平台问题

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

结构体的声明

1.1结构的基础知识

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

1.2结构的声明

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

我们以这种方式来描述一个结构体。下面是简单的示范,我们来描述一个学生:

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

 定义局部变量和全局变量的关系:

#define _CRT_SECURE_NO_WARNINGS
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}s1,s2,s3; //全局变量
int main()
{
	struct Stu s4;
	struct Stu s5;//局部变量
	return 0;
}

1.3 匿名结构体的情况

也可以省略不写结构体标签,不过这样会导致一个结果,结构体只能定义一次类型。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}s1; //全局变量
struct
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}*ps; //全局变量
int main()
{
	s1.age = 1;
	printf("%d", s1. age);
	return 0;
}

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)  

在上述的代码中,体现为定义结构体变量s1之后,无法再次定义诸如s2,s3等结构体类型。

不过要是你本来就准备只用一次结构体的话,定义一个匿名结构体也不错就是了。 

上面的两个结构在声明的时候省略掉了结构体标签, 那么问题来了?

//在上面代码的基础上,下面的代码合法吗? 

ps=&s1; 

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 答案是否定的,及时两个结构体里面的元素都相同,编译器也会他们当成两个完全不同的类型,所以是非法的。

1.4结构的自引用

我们想要使用结构体实现类似于链表的功能。

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

在结构中包含一个类型为该结构本身的成员是否可以呢? 
#include<stdio.h>
struct Node
{
	int data;
	struct Node n;
};
int main()
{
	return 0;
}

 我们开动小脑筋,立马就发现了错误。

struct Node这个节点它所占用的空间有多大呢?

它不仅要存放一个整形,还要存放一个n。

这就无限循环下去了,struct Node里面还有一个struct Node。

大小是无法得出的,这是一个错误示范。

我们转变战略,用指针来实现。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct Node
{
	int data;//4
	struct Node *next;//4/8
};
int main()
{
	struct Node n1;
	struct Node n2;
	n1.next = &n2;
	return 0;
}

创建两个节点n1,n2,把它们像链条一样串起来。 

 编译器没有报错,这样的写法是正确的,同时我们发现,struct Node的大小可以轻而易举地算出,我们得出一个结论:

不是在自己的类型里面包含一个自己类型的变量,而是在自己的类型里面包含一个自己类型的指针。这样的实现方式才是可行的。

 1.5重命名匿名结构体的情况

下面的代码是否可行呢?

#include<stdio.h>
typedef struct
{
	int data;
}S;
int main()
{
	return 0;
}

 可行,不过S不再是匿名结构体的变量,而是变成了匿名结构体类型。

怎么用呢?这么用:

#include<stdio.h>
typedef struct
{
	int data;
}S;
int main()
{
	S s;
	s.data = 1;
	printf("%d", s.data);
	return 0;
}

能用这种方式模拟实现上面的链表呢?

这样写行吗?

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

不行,在没有重命名出Node时就调用了Node。

在这种情况下,我们只能老老实实地写出类型名了!

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

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

有了结构体类型,那如何定义变量,其实很简单。 

 int x;
 int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量
struct Point
{
p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
 int data;
 struct Point p;
 struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

 1.7 结构体内存对齐

我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这就到了本文的重中之重:
结构体内存对齐。

 计算以下的结构体大小。

#include<stdio.h>
int main()
{
	struct S1
	{
		char c1;
		int i;
		char c2;
	};
	printf("%d\n", sizeof(struct S1));
	//练习2
	struct S2
	{
		char c1;
		char c2;
		int i;
	};
	printf("%d\n", sizeof(struct S2));
	//练习3
	struct S3
	{
		double d;
		char c;
		int i;
	};
	printf("%d\n", sizeof(struct S3));
	//练习4-结构体嵌套问题
	struct S4
	{
		char c1;
		struct S3 s3;
		double d;
	};
	printf("%d\n", sizeof(struct S4));
}
运行结果如下:
结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 是不是跟想的完全不一样?

没错,结构体的大小并不是成员大小的简单相加,而是有自己的一套规则的。

  1.  结构体的第一个成员永远是放在零偏移处。
  2. 从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
  3. 这个对齐数是成员自身大小和默认对齐数的较小值。
  4. VS中默认的值为8
  5.  结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  6.  如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如果不够,则浪费空间来对齐。

我们以s1为例子来试验一下上述规则,如图所示。 

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

因为从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。

所以1,2,3三个字节被浪费,int类型的存储从4开始到7,char类型存到8处。

最后结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

 S1中最大对齐数为4,8正好是4的整数倍,所以结构体S1的总大小为8。

再看S4的情况:

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 白色为浪费部分。

1.8为什么存在内存对齐?

1.不同硬件平台不一定支持访问任意内存地址数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。使用内存对齐可以保证每次访问都从块内存地址头部开始存取

2.提高cpu内存访问速度,内存是分块的,如两字节一块,四字节一块,考虑这种情况:一个四字节变量存在一个四字节地址的后三位和下一个四字节地址的前一位,这样cpu从内存中取数据便需要访问两个内存并将他们组合起来,降低cpu性能

用内存对齐达到了用空间换时间的效果

1.9我们可以耍些小聪明达到节省空间的效果。

让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

 S1S2类型的成员一模一样,但是S1S2所占空间的大小有了一些区别。

2.1修改默认对齐数

我们可以通过#pragma pack()指令来修改默认对齐数。

#include <stdio.h>
#pragma pack(1)
//设置默认对齐数为1
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
		return 0;
}

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解) 

 可以看到,答案不再是12,默认对齐数确实被修改了。

想要取消的话就引入一个空指令。

#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
#pragma pack()//取消设置的默认对齐数,还原为默认
struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
		return 0;
}

 结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

2.2 结构体传参

 下面print1和print2那个比较好?

struct S
{
 int data[1000];
 int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
 printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
 printf("%d\n", ps->num);
}
int main()
{
 print1(s);  //传结构体
 print2(&s); //传地址
 return 0;
}
上面的 print1 print2 函数哪个好些?
答案是:首选print2函数。
原因:
  • 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

所以结构体传参数时,要传结构体的地址。

3.1位段

结构体讲完就得讲讲结构体实现
位段
的能力。
struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

 A就是一个位段的类型,位段可以控制所给的空间大小,达到节省空间的目的。

 它所占空间是多大?

#include <stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%d\n", sizeof(struct A));
		return 0;
}

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 它占了8*8=64个比特位。

从16个字节优化到8个字节,位段的功能可以说是十分强大。

3.2 位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
#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;
//空间是如何开辟的?
return 0;
}
  • 首先做一个假设,假设内存中的比特位是由右向左使用的。
  • 一个字节内部,剩余的比特位不够使用时,直接浪费掉。

 结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 我们猜想是这个样子。

转换成16进制为:

62  03  04

我们来调试看看:

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

 我们的猜想是正确的!

3.3 位段的跨平台问题

1. int
位段被当成有符号数还是无符号数是不确定的。
2.
位段中最大位的数目不能确定。(
16
位机器最大
16

32
位机器最大
32
,写成
27
,在
16
位机
器会出问题。
3.
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4.
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

这篇博客旨在总结我自己阶段性的学习,要是能帮助到大家,那可真是三生有幸!😀如果觉得我写的不错的话还请点个赞和关注哦~我会持续输出编程的知识的!🌞🌞🌞