第二十七章 自定义数据类型之结构体进阶(1)

C语言学习之路

第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 操作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 操作符(详解及注意事项)
第十七章 指针进阶(1)
第十八章 指针进阶(2)
第十九章 指针进阶(3)
第二十章 指针进阶(4)
第二十一章 数据的存储(秒懂原、反、补码)
第二十二章 指针和数组笔试题详解(1)
第二十三章 指针和数组笔试题详解(2)
第二十四章 字符串函数(1)
第二十五章 字符串函数(2)
第二十六章 内存函数
第二十七章 自定义数据类型之结构体进阶(1)



前言

深度理解结构体的创建,匿名结构体,结构体的自引用以及以图解的方式秒懂结构体中的内存对齐难题!


1、结构的声明

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

比如我们想要创建一个学生结构体类型:

//不创建变量
struct student
{
	char name[20];
	int age;
	char phone_number[20];
};
//创建变量
struct student
{
	char name[20];
	int name;
}s1,*s2,s3;

这里需要注意的是:

  • 在中间的成员变量中,只需要创建不需要初始化。
  • 无论我们在结尾创建变量与否,都需要加上一个英文输入法下的分号。
  • 在声明的时候,我们在末尾创建的变量是全局变量。

2、特殊的声明(匿名结构体)

struct
{
	int a;
	char b;
	float c;
};

这种结构体在创建的时候是没有声明结构体的名字的。那么我们如何使用呢?
这种匿名结构体只能在结构体声明的时候创建变量,没有办法先声明然后在其余的地方创建变量。其实解释起来是很简单的,假设我们有两个匿名的结构体变量,我们在后续利用匿名结构体创建一个结构体变量的时候,编译器是无法知道这个变量的数据类型是哪一个自定义结构体。因此我们只能利用匿名结构体的声明,在其末尾处创建变量。如下图:

struct
{
	int age;
	char name[30];
}s1,s2;

那么下面这段代码是否跑得通呢?

struct 
{
	int a;
	char c;
}x;

struct 
{
	int a;
	char c;
}*p;
int main()
{
	p=&x;
	return 0;
}

我们创建了一个匿名结构体的变量a,一个匿名结构体类型的指针p。这两个结构体的声明是完全一致的。因此,我们将这个指针p指向结构体变量x的地址。但是我们发现最终这行代码是不成立的。
因为,编译器会把上面的两个声明当成完全不同的两个类型,所以这行代码是非法的。

3、结构体的自引用

struct Node
{
	int data;
	struct Node next;
};
int main()
{
	sizeof(struct Node);//能计算出来吗?
	return 0;
}

上述代码成立吗?
我们发现,我们在结构体struct Node的里面再创建一个struct Node结构体作为成员变量。
于是,我们想要计算出struct Node的大小,就需要先计算出成员变量中struct Node的大小,由此我们发现这里出现了一个死循环的逻辑。因此,这行代码并不成立。
因此我们如何实现结构体的自引用呢?
我们看下面的方式:

struct Node
{
	int data;
	struct Node*next;
};
int main
{
	sizeof(struct Node);
	return 0;
}

上面的代码中,我们发现结构体struct Node中的成员变量包括一个指针,一个整型数据,只是这个指针的数据类型是struct Node。尽管如此,这个指针的大小是确定的,因此sizeof是能够计算出这个自定义结构体的大小的。
因此,我们需要利用指针实现结构体的自引用。

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

struct point
{
	int x;
	int y;
}p1;            //声明结构体的同时定义变量p1(全局变量)。
struct point p2;//定义结构体变量p2(全局变量)。

//初始化:定义变量的同时赋值
struct point p1={1,2};

struct Node
{
	int data;
	struct point P;
	struct Node* next;
}n1={10,{2,3},NULL};         //结构体嵌套初始化

struct Node n2={20,{4,5},NULL};//结构体嵌套初始化
//我们也可以不按成员变量的顺序初始化
struct Node n3={.P={3,4},.data=12,.next=NULL};

但是我们发现,我们创建一个结构体的变量的时候,始终都要写struct ,有一些麻烦,如果我们省掉了struct,直接写结构体名,这时候会报错。为了简单一些,我们可以利用typedef关键字:

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

Node N1={1,NULL};

5、结构体内存对齐

(1)引入

我们了解了结构体的声明,初始化。接着我们了解一下如何计算结构体的大小。
我们看下面的例题:

//练习一:
struct s1
{
	char c1;
	int i;
	char c2;
};
printf("%d\n",sizeof(struct s1));
//练习二:
struct s2
{
	char c1;
	char c2;
	int a;
};
printf("%d\n",sizeof(struct s2));

我们发现上面两个结构体的成员变量仅仅是顺序不一样,那么二者最终的输出结果应该是相同的。并且二者都是两个字符型,一个整型。按常规的逻辑来看,结果就应该是(1+1+4)=6。
但是我们运行上述代码,二者的输出结果不仅不同,并且也都不是6。
在这里插入图片描述
我们发现最终的运算结果,一个是12,一个是8。那么要想解决这个问题,我们首先要了解一下内存对其的规则。

(2)内存对齐的规则

  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
  • 对齐数=编译器默认的一个对齐数与该成员内存大小之间的较小值。
  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  • VS编译器中的默认对齐数是8,Linux环境中默认不设对齐数(对齐数就是结构体成员的自身大小)。
    习题解释:
    根据上面
    在这里插入图片描述
    我们根据刚才所了解到的有关内存对其的规则,来分析一下出现12这个结果的原因。

我们画出一段内存空间,根据规则中的:“第一个成员变量要放到偏移量为0的位置”。所以我们将c1存储在0对应的空间。 然后我们考虑一下变量i的存储位置。i的自身大小是4个字节,VS编译器的默认对齐数是8。根据对齐规则:“其余的变量对齐的位置是默认对齐数的整数倍,而对齐数是VS编译器默认对齐数和变量自身大小之间的较小值。”所以,i的默认对齐数是4,那么4的整数倍第一个就是4。所以i的存储的起始位置是4,即图片中所指的空间。找到了起始位置,再向后开辟4个字节的空间来存储int类型的数据。接着我们考虑最后一个c2。c2的自身大小是1,VS的默认对齐数是8。所以c2的对齐数就是1。而下一个起始位置是标号为8的位置,8正好是1的倍数,所以c2的起始位置就在这里。然后以这个位置为起点向后开辟1个字节。存放字符型变量。此时纵观整个存储关系。我们发现,一共用了9个字节的空间。根据对齐规则:“整个结构体的大小,要是每个成员变量的对齐数中最大的对齐数的整数倍”。所以我们发现对齐数分别是1,4,1。所以,最终的结果要是4的整数倍,也就是说,我们从9的位置再向后偏移三个字节。使得结构体的内存大小变成12。

为了验证上面的结果,我们可以通过一个c语言自身设置的宏:offsetof来验证最终的结果。
在这里插入图片描述
这个宏的作用就是输出结构体变量中每一个成员变量相对于起始位置0,的存储空间位置。
但是在使用这个宏的时候,我们需要包含一个头文件:#include<stddef.h>

#include<stdio.h>
#include<stddef.h>
struct s1
{
	char c1;
	int i;
	char c2;
};
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;
}

在这里插入图片描述
我们会发现,我们最终打印出来的成员变量存储的相对位置和上面分析的结果是一致的。

同理,我们看第二个练习,这里就只画图,不做文字分析了。
在这里插入图片描述

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

在这里插入图片描述
我们再多做几组练习:

//练习三:
struct s3
{
	 double d;
	 char c;
	 int i;
};
int main()
{
	printf("%d\n",sizeof(struct s3));
	printf("%d\n",offsetof(struct s3,d));
	printf("%d\n",offsetof(struct s3,c));
	printf("%d\n",offsetof(struct s3,i));
	return 0;
}

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

#include<stdio.h>
#include<stddef.h>
//练习3
struct S3
{
	double d;
	char c;
	int i;
};

//练习4-结构体嵌套问题
struct s4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{

	printf("%d\n", sizeof(struct s4));
	printf("%d\n", offsetof(struct s4, c1));
	printf("%d\n", offsetof(struct s4, s3));
	printf("%d\n", offsetof(struct s4, d));
	return 0;
}

在这里插入图片描述
这里唯一不同的就是在这个结构体中又嵌套了一个结构体。所以我们就要提到另外一个规则:”嵌套的结构体的位置要是嵌套结构体自身所有成员变量的对齐数中的最大对齐数的整数倍“。
在这里插入图片描述

(3)设置默认对齐数

设置默认对齐数需要用到#pragma pack( )。用法如下图所示:

//设置默认对齐数
#pragma pack(1)
struct S1
{
	char c1;
	int i;
	char c2;
};
//恢复默认对齐数
#pragma pack()
int main()
{
    printf("%d\n", sizeof(struct S1));
    return 0;
}

因为改变了默认对齐数为1,所以最终的结果应该是6。
在这里插入图片描述

(4)内存对齐的意义

平台原因:

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

性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对其的内存访问仅需要一次访问。
总体来说,结构体的内存对齐是拿空间来换取时间的做法。
那我们在设计结构体的时候,如何让空间更小,时间更短呢?要想达到这个目的我们就需要:
让占用空间小的成员尽量集中在一起。

6、结构体传参

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;
}

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降。
说简单点就是,当我们直接传结构体的时候,形参创建的临时变量所占空间较大,但是我们传入一个结构体指针的时候,所占空间小。所以,结构体传参最好传入地址。


你不可能是永远的赢家,但是请不要害怕做出决定。
——阿诺德·施瓦辛格

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值