《C语言进阶》 第四部分 自定义类型:结构体

大家好!从这篇文章开始我们详细说一下自定义类型。自定义类型有数组,结构体,枚举,联合。前面我们已经说过数组了,这里就说后面三种,首先,我们详解一下第一种:结构体。
《C语言进阶》 第四部分 自定义类型:结构体_第1张图片

文章目录

  • 结构体
    • 1 结构体的声明
      • 1.1 结构的基础知识
      • 1.2 结构的声明
      • 1.3 特殊的声明
      • 1.4 结构的自引用
      • 1.5 结构体变量的定义和初始化
      • 1.6 结构体内存对齐
      • 1.7 修改默认对齐数
      • 1.8 结构体传参
    • 2. 位段
      • 2.1 什么是位段
      • 2.2 位段的内存分配
      • 2.3 位段的跨平台问题
      • 2.4 位段的应用

结构体

1 结构体的声明

1.1 结构的基础知识

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

1.2 结构的声明

《C语言进阶》 第四部分 自定义类型:结构体_第2张图片
例如描述一个学生:
《C语言进阶》 第四部分 自定义类型:结构体_第3张图片
这部分内容,我在前面的基础部分说过了,这里就不细说了。

1.3 特殊的声明

在声明结构的时候,可以不完全的声明。
比如:
《C语言进阶》 第四部分 自定义类型:结构体_第4张图片
这样的叫做匿名结构体类型。
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
在这里插入图片描述
警告:
编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。

1.4 结构的自引用

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

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

这样的自引用是否可行?
其实是不行的,因为我们不知道sizeof(struct Node)是多少。

正确的自引用方式:

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

然后,我们再看一个问题:

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

这样写代码,可行吗?
不行,因为在结构体里我们已经使用了Node,但是Node是后面定义的。所以就乱了。
正确的写法是:

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

这里的Node和struct Node是一样的。

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

这部分内容我在基础部分说过了,不懂的可以去看看。

1.6 结构体内存对齐

我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小。这也是一个特别热门的考点: 结构体内存对齐。
我们来看一下这个代码:

struct S1
{
 char c1;//1
 int i;//4
 char c2;//1
};

这个结构体的大小是多少呢?
估计很多人的第一反应是6个字节,其实不是。
在这里插入图片描述
结果是12个字节。为什么呢?首先,我们说一个函数offsetof
这个函数是返回数据结构或联合类型类型中成员成员的偏移值(以字节为单位)。

然后我们来看一下这三个成员的偏移量。
《C语言进阶》 第四部分 自定义类型:结构体_第5张图片
想要知道为什么,首先得掌握结构体的对齐规则:
我们先假设用struct S1类型创建一个变量s。
《C语言进阶》 第四部分 自定义类型:结构体_第6张图片
s下面的0,1,2,3…就是偏移量。
1.第一个成员在与结构体变量偏移量为0的地址处。
《C语言进阶》 第四部分 自定义类型:结构体_第7张图片
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值(VS中默认的值为8)。
我们知道i是4个字节,比8小,所以对齐数是4。i就会放到4的整数倍处。
《C语言进阶》 第四部分 自定义类型:结构体_第8张图片
然后c2是1个字节,比8小,对齐数是1。c2放到1的倍数处。
《C语言进阶》 第四部分 自定义类型:结构体_第9张图片
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
这这句话我们就知道最大对齐数是4,而结构体总大小是它整数倍。前面我们已经使用了9个字节的空间,所以我们应该浪费3个字节的空间,为12才是4的倍数。
然后我们讨论一下结构体嵌套问题。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举一个例子:

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

struct S2
{
	char c;
	struct S1 s2;
	double d;
};

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

首先,我们知道了struct S1是12个字节,它里面的最大对齐数是4。所以它应该对齐到4的倍数处。
《C语言进阶》 第四部分 自定义类型:结构体_第10张图片
struct S2结构的最大对齐数是8,然后我们已经使用了24个字节,是8的倍数。所以struct S2大小为24个字节。

为什么存在内存对齐?
大部分的参考资料都是如是说的:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
解释一下:

struct S
{
	char c;
	int i;
};

如果没有对齐:
《C语言进阶》 第四部分 自定义类型:结构体_第11张图片
假如32位机器上,我们cpu一次拿4个字节,我们要访问i要两次才能全面把i拿出来。
如果对齐了:
《C语言进阶》 第四部分 自定义类型:结构体_第12张图片
i我们只需要访问一次就全拿出来了。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
看下面的代码:

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


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

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
如果你在VS上测试过后,你会发现S1占12个字节,S2占了8个字节。

1.7 修改默认对齐数

#pragma 这个预处理指令,可以改变我们的默认对齐数。
《C语言进阶》 第四部分 自定义类型:结构体_第13张图片
在这里struct S1的对齐数被改为1了。
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数。

1.8 结构体传参

这部分内容我在前面基础篇说过了,这里就不多说。

2. 位段

2.1 什么是位段

位段的声明和结构是类似的,有两个不同:
1.位段的成员可以是 int,unsigned int,signed int 或者是 char (属于整形家族)类型。
2.位段的成员名后边有一个冒号和一个数字。

比如:

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

A就是一个位段类型。那位段A的大小是多少?
首先,要懂得A的大小是多少,我们要知道位段的位是什么?
这里的位是二进制位。
:2的意思是_a只占2个bit位,:b只占5个bit位…

2.2 位段的内存分配

1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2.位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

看下面的代码:

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

char类型是按1个字节来开辟空间的。所以我们首先来开辟1个字节(8bit),a占3个bit,还剩5bit,b占4个bit,还剩1个bit。c占5个bit,不够,该怎么办?
我们有两种情况:1.剩下的1个bit浪费,再开辟一个字节(8bit)。2.剩下的1个bit用上,再开辟一个字节(8bit)。
第一种,开辟了8个bit,c占5个,还剩3个。d占4个,不够,再开辟一个字节。
第二种,用了上面剩下的1个bit,c还剩4个,开辟1个字节,还剩4bit,正好够d使用。
如果是第一种情况,我们需要3个字节。第二种情况,我们只需要2个字节。
结果是什么,我们来看一下:
《C语言进阶》 第四部分 自定义类型:结构体_第14张图片
是3个字节,是第一种那个剩下bit不用的情况。

其实在C语言里,我们没有规定是用剩下的位,还是不用剩下的位,这是根据编译器来决定的,所以位段是不跨平台的,但位段能为我们节省空间。

估计很多人不理解为什么一个int类型或char类型,只占2个bit,或5个bit。
这是我给大家举例子,实际上,我们要根据实际情况来定,假设我们只需要01,00,10,11,那么我们就可以用2个bit来表示,如果用32位,则会浪费空间。

我们看一下下面的代码:

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

那么有一个问题:s空间是如何开辟的?
首先,我们知道struct S 是3个字节,然后我们给s里的成员赋值,是如何赋值的呢?
在这里插入图片描述
a:10---->01010,但只占3个bit,所以只取010
b:12---->01100,但只占4个bit,所以只取1100
c: 3---->00011,但只占5个bit,所以只取00011
d: 4---->00100,但只占4个bit,所以只取0100
然后,我们要将这些放到s里面,怎么放?首先,第一个问题,在每一个字节里,我们每一位是从左向右放,还是从右向左放,我们先假设从右向左放。
上面我们测试在VS编译器下,剩下的位不用。放的情况如下图所示:
《C语言进阶》 第四部分 自定义类型:结构体_第15张图片
我们将转换为为16进制是62 03 04。然后我们看一下编译器的结果:
在这里插入图片描述
结果和我们假设一样。

2.3 位段的跨平台问题

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

2.4 位段的应用

《C语言进阶》 第四部分 自定义类型:结构体_第16张图片
位段的应用一般在网络上,后面在细讲。

总结:
到这里,我们就把与结构体相关的内容说完了,内存对齐和位段都挺重要的,希望能理解它们。下一篇文章,我会讲一下自定义类型中的枚举和联合体。如果大家认为我有哪些不足之处或者知识上的错误都可以告诉我,我会在之后的文章中不断改正,也请大家多多包涵。如果大家觉得这篇文章有用的话,也希望大家可以给我关注点赞,你们的支持就是对我最大的鼓励,我们下一篇文章再见。
《C语言进阶》 第四部分 自定义类型:结构体_第17张图片

你可能感兴趣的:(C语言,c语言,开发语言,后端)