【自定义类型:结构体】

1.前言

类型分为内置类型和自定义类型。有时为了满足不同的需求,我们需要使用自定义类型。这篇文章,将深入浅出地介绍结构体的有关知识。

在这里插入图片描述

2.结构体类型的声明

2.1.结构体的声明

struct Book
{
	char name[20];
	char author[20];
	float price;
	char id[13];
	
};

2.2.结构体变量的创建和初始化

  • 创建(局部变量和全局变量已标注在注释中)
struct book
{
	char name[20];
	char author[20];
	float price;
	char id[13];
}b3,b4;//全局变量


struct book b2;//全局变量


int main()
{
	struct book b1;//局部变量
	struct book arr[5];//数组
	return 0;
}

  • 初始化
struct Book
{
	char name[20];
	char author[20];
	float price;
	char id[13];
	
	int main()//初始化
{
	struct Book b1 = {"鹏哥C语言", "鹏哥", 18.8, "PG10001"};//按顺序
	
	struct Book b2 = {.id = "PG10002", .author="鹏哥", .name="C语言程序设计", .price=88.8};
	//. 可以不按顺序

	return 0;
}

2.3.结构体的特殊声明

  • 请先记住正常的结构体声明
struct S
{
	char c;
	int i;
	double d;
};

  • 特殊声明(不完全声明)–匿名结构体类型
struct 
{      
	char c;
	int i;
	double d;
} s = {'x', 100, 3.14};//不初始化当然也可以

这个类型就是创建s的时候用了一次,之后就不能再用了。可以调用s,但是不能再创建用的struct (没名字)创建其他变量。

  • 匿名结构体的不可确定性
struct
{
	char c;
	int i;
	double d;
}s;

struct
{
	char c;
	int i;
	double d;
}* ps;

int main()
{
	ps = &s;
	return 0;
}

这是无法实现的,因为是匿名的,编译器无法确定两个结构体的类型,因此也无法进行ps = &s的操作。

  • 重命名
typedef struct
{
	char c;
	int i;
	double d;
}S;

使用typefe再重命名为S,可以多次使用。但是这个其实很没必要,先匿名又重新命名,有什么意义?匿名的意义就是只使用一次。

2.4.结构体的自引用(典型案例:链表)

  • 链表需要包含当前数据与下一个数据
struct Node
{
	int data;
	struct Node n;
};

这样可以吗?那试问sizeof(struct Node)是多少?是不是无限“递归”了?

正确写法
链表需要包含数据域和指针域

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

一个结构体中想要包括和自己一样的结构体是做不到的;但一个结构体可以包括和自己一样的结构体指针,这是一个地址,sizeof固定。

  • 简化写法
typedef struct Node//之后创建不用struct Node n1直接用Node n1即可
{
	int data;//数据
	struct Node* next;//指针
}Node;

之后创建不用struct Node n1直接用Node n1即可,
这也是很多书上表示链表的方式。
2.

struct Node//每个节点都有两部分组成,一是数据域二是指针域
{
	int data;//数据
	struct Node* next;//指针
};

typedef struct Node Node;

这个与上面那种是等价的
3.

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

这个是错的 typeof定义还没完成,依然要使用struct Node
4.

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

匿名的结构体类型是不能实现这种结构体自引用的效果的~
所以不要写这种试图省事的方式

3.结构体内存对齐

3.1.引入,一个疑惑

以下代码的运行结构是多少?

#include <stdio.h>


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

int main()
{
	struct S s0;
	printf("%zd\n", sizeof s0);
	return 0;
}

答案是12
在这里插入图片描述
为什么答案不是6呢,不是根据变量类型是1+4+1吗?
这就涉及到结构体内存对齐的相关问题了~

3.2.对齐规则

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

  1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址
    地址由0开始编号,可以认为编号就是起始位置偏移量。0处为0,7处为7。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的⼀个对齐数 与 该成员变量大小的较小值。
  • VS 中默认的值为 8
  • Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
  1. 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
  2. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

接下来我们来看几个例子
例一:

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


VS编译器默认的⼀个对齐数均为8;成员变量大小成员变量分别取其大小1,4,1;对齐数取两者较小值.
在这里插入图片描述
存下c1后,对于i要找他的对齐数(4)的整数倍位置偏移量的地址,如图在4处存入;存入i后,对于c2,要找他的对齐数(1)的整数倍位置偏移量的地址,下一地址8就满足要求(任何整数都可以是1的整数倍)
结构体的大小为结构体中每个成员变量的所有对齐数中最大的一个的整数倍,同时还应是在已存地址的基础上考虑.这里我们应取4的整数倍。
注意:这里的8不是我们要找的大小,因为我们是从0开始计算的,因此此时的大小为9个字节,再顺位到4的倍数,于是答案就是12个字节。

例二(自行分析)

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

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

例三(自行分析)

struct S3
{
	double d;//8 8 8
	char c;  //1 8 1
	int i;   //4 8 4
};

在这里插入图片描述在这里插入图片描述
例四:包含结构体

struct S3
{
	double d;//8 8 8
	char c;  //1 8 1
	int i;   //4 8 4
};

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

int main()
{
	struct S4 s4 = { 0 };
	printf("%zd\n", sizeof(s4));//size_t

	return 0;
}

在这里插入图片描述
在这里插入图片描述
这其实也没什么特殊的,在计算了struct S3的基础上把他完全看出一个16字节大小的类型,其他的照葫芦画瓢。这里struct S3类型大小为16字节,VS默认为8字节,这里就取8字节。

ps:

  • 如果其中有数组,比如char c1[5],对齐数按前面的类型char算,内存就按5个char的方式去存即可;
  • 如果嵌套三层或更多,在前者的基础上两两分析即可。

3.3.为什么存在内存对齐?

  1. 平台原因 (移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
    类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要
    作两次内存访问;而对齐的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地
    址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以
    用⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两
    个8字节内存块中。
    总体来说:结构体的内存对齐是拿空间来换取时间的做法。

举个例子来说明
在这里插入图片描述
这里的内存访问并不意味着要读完所有空间,如果是这样,那反而是不对齐的情况更有利,因为对齐是8个字节,不对齐是5个字节。
但往往实际中我们是访问某个元素,这里我们以访问i为例,假定每次都读4个字节。如果是不对齐的情况那么要完整访问完i必然需要操作2次(如图所示);如果是对齐情况,那么可以跳过之前的4个字节,可以一次就读完(如图所示)。这就是所谓的用空间换时间。

3.4.修改默认对齐数

#pragma pack(1)//设置默认对⻬数为1
#pragma pack()//取消设置的对⻬数,还原为默认

那么我们到底是使用编辑器默认的对齐数好,还是自定义的好?
这并不能一概而论,而是应该根据现实情况做进一步判断。

4.结构体传参

如果我有需求想要打印一个结构体……

struct S
{
	int arr[1000];
	int n;
	double d;
};

不妨创建一个函数。

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

void print2(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("%d ", ps->n);
	printf("%lf ", ps->d);
}

int main()
{
	struct S s = { {1,2,3,4,5}, 100, 3.14 };
	print1(s);
	print2(&s);

	return 0;
}

相比之下,printf1是直接传递,printf2是传地址,那个更好呢?
如果但从时间空间上来看,printf2传地址更好。如果是printf1还需要另外开辟一处内存去存拷贝的结构体,而这个结构体有可能非常大,多次调用非常浪费。反之printf2可以循着地址直接对原来的结构体进行操作,更加高效。
总结来说:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。

但是,如果有人想通过调用函数来修改原来的结构体呢?这不是很危险吗?这可以用一个小技巧来解决————加个const就好了。

void print2(const struct S* ps)
{
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	printf("%d ", ps->n);
	printf("%lf ", ps->d);
}

5.结构体实现位段

5.1.什么是位段

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

  1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员名后边有⼀个冒号和⼀个数字。
struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

这里的A就是⼀个位段类型。

5.2.位段的内存分配

那么这样一个位段类型有多大呢?

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

int main()
{
	printf("%zd\n", sizeof(struct S));

	return 0;
}


在这里插入图片描述
这是为什么呢?以下将介绍位段的内存分配。

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

在这里插入图片描述
这里可以认为开辟一个字节8bit后从右往左存储,如果不足再开辟一个(当然运行时是一下子开辟完的,这里为了方便说明)。根据上图,可以知道答案3的来源

对于所谓位段的不确定因素,包括有:
1.申请到的一块内存中,从左向右使用还是从右往左使用,这是不确定的
2.剩余的空间不足时,下一个成员使用时时利用还是舍弃,这也是不确定的
PS:上述案例用的时从右往左存储,并且不够的剩余空间之间舍弃。

5.3.位段的跨平台问题

  1. int 位段被当成有符号数还是⽆符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当⼀个结构包含两个位段,第⼆个位段成员比较大,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

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

5.4.位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。
比如我们想传递“呵呵”,就是按照下图传输的。需要发出的ip地址和目的的ip地址,同时用位段的方式存储信息。
在这里插入图片描述

5.5.位段使⽤的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
在这里插入图片描述
就比如这个图,就没有一个成员是从起始字节开始存储的,那我也找不到。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊
放在⼀个变量中,然后赋值给位段的成员。

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

    //正确的示范
    int b = 0;
    scanf("%d", &b);
    sa._b = b;

    return 0;
}

6.结语

这就是结构体的相关说明了,希望有些帮助~

  • 28
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值