深度剖析结构体@自定义类型1---结构体的声明,自引用,变量定义和初始化 + 结构体内存对齐 + 结构体传参 + 结构体实现位段

Posted 呀小边同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度剖析结构体@自定义类型1---结构体的声明,自引用,变量定义和初始化 + 结构体内存对齐 + 结构体传参 + 结构体实现位段相关的知识,希望对你有一定的参考价值。


🔑 引:

⭐️C语言有许多内置类型,如char,short,int, long,float,double等,
⭐️而描述(书名+作者+出版社+定价+书号)或者人(名字+年龄+身高+身份证号码)这种复杂对象,则要用可自定义的结构体类型来描述。

❄️本文介绍的结构体内存对齐,是面试考点,很重要,小伙伴们都搞起来!

正文开始@边通书

一、结构体基础知识

1.结构体声明与变量的定义

struct tag
{
 	member-list;//成员列表
}variable-list;//变量列表(可以在这里直接创建变量也可不写)

例如描述一本书:(

特殊的声明:

声明结构时,可以不完全的声明。如,匿名结构体类型

//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}s1,s2;//只能用一次,顺带着就直接创建变量s1,s2


int main()
{
	struct s3;//这样是不行的的,连名字都没有,没法创建
	return 0;
}

若两个结构在声明时都省略掉结构体标签:

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

struct
{
	int a;
	char b;
	float c;
}a[10], *p;

问:那么在上面代码基础上,这样的代码合法吗?

p = &x;


可见,尽管两结构体成员一模儿一样儿,编译器还是会把上面的两个声明当做完全不同的类型,所以是这段代码是非法的

2.结构体自引用

💚自己如何找到和自己同类型的对象呢?

引:

那么从 节点1 如何找到 节点2 ?

//思考:像这样在一个结构体内包含为结构体本身的成员可以吗?
struct Node
{
	int data;
	struct Node n;
};
//如果可以,那sizeof(struct Node);是多少?

❌不可以的,会无限递归下去,sizeof(struct Node);也会无限大。

⭐️正确的自引用方式

struct Node
{
	int data;//数据域
	struct Node* next;//指针域-->可以找到下一个节点
};

✅自己能够找到同类型的另一个对象—要存同类对象的地址

3.结构体变量的初始化和使用

💚初始化:

💚使用:

#include<stdio.h>

struct Point
{
	int x;
	int y;
};

struct S
{
	double d;
	struct Point p;
	char name[20];
};

int main()
{
	struct S s = { 3.14, { 3, 4 }, "laowang" };
	struct S* ps = &s;

	printf("%lf\\n", s.d);
	printf("%lf\\n", ps->d);
	printf("----------------\\n");

	printf("%d %d\\n", s.p.x, s.p.y);
	printf("%d %d\\n", ps->p.x, ps->p.y);
	printf("----------------\\n");

	printf("%s\\n", s.name);
	printf("%s\\n", ps->name);
	return 0;
}

运行结果:

二、结构体内存对齐

⭐️掌握了结构体的基本使用后,将深入探讨如何计算结构体大小
❄️这是特别热门的考点,大家都搞起来

思考:这个结构体类型大小是多少?

没关系,不太知道为什么小伙伴往下读就一定懂了!

1.结构体的对齐规则

❄️1. 结构体的第一个成员永远放在结构体起始位置偏移量为0的地址处。
❄️2. 第二个成员开始,总是放在偏移量为对齐数整数倍的地址处。
⛄️ 对齐数 == 编译器默认的对齐数变量自身大小的较小值 (vs的默认值为8)

❄️3. 结构体的总大小必须是各成员的对齐数中最大对齐数的整数倍

看完规则你可能还是有点蒙,不过没关系,下面

上代码边分析边学习:

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 S3
{
	double d;
	char c;
	int i;
};

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\\n", sizeof(struct S4));
	return 0;
}

这就先需要补充第四条规则:

❄️ 4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数整个结构体的大小所有成员最大对齐数(含嵌套结构体的对齐数)的整数倍。

🔑解析:

2. 结构体内存对齐的意义:

这个没有官方解释,大部分的参考资料如是说:

🐟1.平台原因
不是所有硬件平台都能访问任意地址的数据;某些硬件平台只能在某些地址处读取特定类型的数据,否则会抛出硬件异常。
🐟2.性能原因
数据结构(尤其是栈)应该尽可能在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器可能要做两次内存访问;而对齐的内存访问仅需要一次访问,提升了效率。(画图说明)


结构体的内存对齐是用时间换取空间的一种做法,那在设计结构体时,可以稍稍动一点脑筋,既满足对齐,也不要浪费太多空间。

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

例如:

struct S1
{
	char c1;
	int i;
	char c2;
};//12byte

struct S2
{
	char c1;
	char c2;
	int i;
};//8byte


尽管s1和s2类型的成员一模一样,但是占用空间的大小还是有一定区别。

3. 修改默认对齐数

之前我们见过#pragma这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数:(vs默认对齐数为8,Linux没有默认对齐数)

上代码:

#include<stdio.h>

#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置默认对齐数,恢复默认

#pragma pack(1)//相当于没有对齐(没有空间浪费,当然效率也比较低)
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置默认对齐数,恢复默认

int main()
{
	printf("%d\\n", sizeof(struct S1));//12byte
	printf("%d\\n", sizeof(struct S2));//6byte
	return 0;
}

运行结果:

在结构体对齐方式不合适时,我们可以自己修改默认对齐数,但也不能乱改,一般为
2n

4. 百度笔试题

题目:写一个宏,计算结构体中某成员相对结构体首地址的偏移量
考察offsetof宏的实现

这里小边还没有写有关宏的文章,在此只介绍offsetof函数的使用。

#include<stdio.h>
#include<stddef.h>//@offsetof

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

//offsetof是一个宏:这里居然是类型传参,后面还会聊
int main()
{
	printf("%u\\n", offsetof(struct S1, c1));
	printf("%u\\n", offsetof(struct S1, i));
	printf("%u\\n", offsetof(struct S1, c2));
	return 0;
}

运行结果:

三、结构体传参

上代码:
思考:哪种打印函数更好些?

#include<stdio.h>

struct S
{
	int data[1000];
	int num;
};

void print1(struct S tmp)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", tmp.data[i]);
	}
	printf("\\n%d\\n ", tmp.num);
}

void print2(struct S* ps)//为了使ps指向内容不被修改,可以写成(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("\\n%d\\n ", ps->num);
}

int main()
{
	struct S s = { { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 100 };
	print1(s);
	print2(&s);
	return 0;
}

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

❄️ 因此结构体传参时,最好还是穿结构体的地址。

四、结构体实现位段

1. 什么是位段

在此之前,你可能只听说过段位,没有听说过位段哈哈哈哈哈,不过呢,聊完结构体就要聊一聊结构体实现位段的能力:

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 intunsigned intsigned intchar,即整形家族。
2.位段的成员名后边有一个冒号和一个数字。

比如:

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

这是什么意思呢?那我们先来测一测它的大小:

printf("%d\\n", sizeof(struct A));

运行结果:

好像与结构体相比,位段A变小了。

其实呀,位段中的"位"代表的就是二进制位,🔑位段就是可以节省空间的!
生活中的有些值,不需要太多的存储空间,比如表示性别,男00–女01–保密11,只需要两个比特位即能表示,

struct A
{
	int _a : 2;//_a 2个bit位
	int _b : 5;//_b 5个bit位
	int _c : 10;//_c 10个bit位
	int _d : 30;//_d 30个bit位
};//47个bit位

看起来,好像6byte就足够了,那位段A的大小为什么是8byte?
那位段的内存分配究竟是怎样的呢?

2. 位段的内存分配

❄️1.位段的成员必须是intunsizgned intsigned intchar,即整型家族。
❄️2.位段的空间是按照需要以4个字节(int)或者一个字节(char)的方式来开辟。
❄️3.位段涉及了很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

那我们再来分析一下struct A的内存分配过程。

再分析一段代码:

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

在此,我们先按照如下猜想进行内存分配:

为了探寻究竟,我们调试起来监视一下:(vs2013环境测试数据)

由此可见,接过与我刚刚分析出来的一样:

⛄️开辟空间 — 一次一个字节
⛄️放数据 — 从低位到高位使用,紧挨着使用;若高位空间不够用,则浪费掉,重新开辟新的字节

但这也仅仅是在vs上成立。

2.3 位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器 – int 2byte = 16bit,32位机器最大32,假如写成27,在16位机器会出问题。
  3. 位段中的成员一个整型/字节内部在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,而第一个位段剩余的位无法容纳时,是浪费还是利用剩余的位,这是不确定的。

与结构相比,位段作用相似,结构能使用的地方,位段设计合理也能实现。可以很好的节省空间,但是也有跨平台的问题存在。

2.4 位段的应用


本文完@边通书

接下来的文章将介绍有关枚举与联合的相关内容。

敬请期待! 哈哈哈哈

以上是关于深度剖析结构体@自定义类型1---结构体的声明,自引用,变量定义和初始化 + 结构体内存对齐 + 结构体传参 + 结构体实现位段的主要内容,如果未能解决你的问题,请参考以下文章

自定义类型详解

c语言自定义类型——结构体,位段(匿名结构体,结构体的自引用,结构体的内存对齐)

C语言—自定义类型

C语言自定义数据类型之结构体

自定义类型:结构体

C语言—自定义类型