###什么?学会C语言就能当发明家?你也能自己创造专属的数据类型?<C语言之结构体>超丰富,一口吃成大胖子!!!“结构体的创建和初始化,结构体内存对齐,结构体传参,位段”

C语言中的自定义类型之结构体篇

引入

YY是一位制作月饼的大师,市面上提供的月饼模具已经不能满足YY大师的追求,为了做出更加诱人的月饼,YY大师决定自己制作模具
不同的数据类型就像是各种做月饼的模具。C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学的信息,这时单⼀的内置类型是不⾏的。
描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以⾃⼰创造适合的类型。

#include <stdio.h>
struct Point
{
	int x;
	int y;
};
int main()
{
	struct Point p = { 1, 2 };
	printf("%d %d", p.x, p.y);
	return 0;
}

以上代码声明了一个结构体类型Point,创建了结构体变量p,并且初始化p

结构体的创建和初始化

结构体的一般声明

我们重点关注结构体的语法形式

struct tag//结构体类型的名称
{
 	member-list;//成员变量,用来描述结构体包含的信息
}variable-list;//变量列表,用来创建结构体变量

来个具体的例子感受结构体变量的创建和初始化

#include <stdio.h>
struct Point
{
	int x;
	int y;
}p1 = { 1 ,2 };//创建并初始化结构体变量的第一种方式
struct Point p2 = { 2,3 };//创建并初始化结构体变量的第二种方式
int main()
{
	struct Point p3 = { 3,4 };//创建并初始化结构体变量的第三种方式
	return 0;
}

结构体变量可通过三种方式进行创建,代码中已体现,我们需要注意的是第一种和第二种方式创建的变量为全局变量,第三种方式为局部变量

指定顺序初始化结构体

下面我们再看一种初始化的方式:指定顺序初始化,并且与顺序初始化做了比较

struct P
{
	int x;
	int y;
};
int main()
{
	struct P p1 = { 1,2 };
	struct P p1 = { p1.x = 1, p1.y = 2 };
	struct P p1 = { p1.y = 1, p1.x = 2 };//err
	struct P p1 = { .y = 1, .x = 2 };
	printf("%d %d", p1.x, p1.y);
	return 0;
}

前两种方式为顺序初始化,程序可以正常执行
后两种方式为指定顺序初始化,只有第四种可以合法,第三种C语言是不支持的
//这里相当于提供给读者一个鄙人踩过的坑

结构体的不完全声明

最后是一种特殊的结构体声明,结构体的不完全声明

struct //没有结构体的名字
{
	int x;
	int y;
}p1;//可以在这里直接初始化

int main()
{
	p1.x = 1, p1.y = 2;//直接访问结构体成员进行初始化
	printf("%d %d", p1.x, p1.y);
	return 0;
}

结构体的自引用

该部分在数据结构中极为重要,这里了解一下即可

struct Node
{
	int Data;
	struct Node* next;//创建结构体指针,存放结构体地址
};

将每一个节点分为数据域和指针域,数据域用来存放数据,指针域用来存放下一个节点的地址,通过结构体自引用的方式来实现链表等复杂的数据结构

结构体重命名

typedef struct Node
{
	int Data;
	struct Node* next;//这里不能直接写成Node* next 因为还没有完成重命名
}Node;//给struct Node重新命名为Node

结构体变量的访问

. 操作符

语法形式: 结构体变量 . 结构体成员名

->操作符

在结构体传参时使用,语法形式:结构体指针->结构体成员名

结构体内存对齐

YY同学已经掌握了结构体的使用方式,她还想知道自己定义的结构体占用多少字节该怎么办腻?

结构体在内存的存储方式

想要探讨结构体在内存中的存储方式就要知道C语言定义的一个宏 offsetof
在这里插入图片描述
从C++官网上可以查阅到,该宏的用处是观测每个结构体成员的偏移量(后面有解释),头文件为 <stddef.h> ,参数分别为类型和成员变量,并且返回值为size_t类型

struct P
{
	char ch1;
	char ch2;
	int a;
};

int main()
{
	printf("%zd\n", offsetof(struct P, ch1));//0
	printf("%zd\n", offsetof(struct P, ch2));//1
	printf("%zd\n", offsetof(struct P, a));//4
	printf("%zd\n", sizeof(struct P));//8
	return 0;
}

YY:啥是个偏移量啊??
我们假设结构体存放时有一个起始位置,偏移量就是指:结构体变量存放时 距离起始位置的字节长度
存放时的规则(对齐规则)如下:

  1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对⻬到对⻬数的整数倍的地址处。
    对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩ 的 较⼩值。
    VS 中默认的值为 8,Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
  3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
    整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
    体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

在这里插入图片描述
来解析一下这个图片的含义:对齐规则的第一条得,ch1存放在地址为0的位置,并且占用一个字节单位,由规则第二条,其他成员变量存放在 默认的对齐数 与 该成员变量⼤⼩ 的 较⼩值也就是1,的整数倍的地址处,所以,ch2存放在地址为1的位置,接下来a同理,较小值为4,所以a存放在地址为4的位置。最后依照第三条,结构体总大小就为8个字节

再来练习一个

struct s1
{
	double d;
	char c;
	int i;
};
struct s2
{
	char ch1;
	struct s1 s1;
	double d;
};
int main()
{
	struct s2 s2;
	printf("%zd", sizeof(struct s2));
	return 0;
}

结果为32个字节,在内存中的存放形式如下
在这里插入图片描述

为什么存在内存对齐??

YY:那2,3位置的内存不就浪费掉了吗?为啥要这样存储啊???
好滴 我们下面就来解释一下YY同学的疑问

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

了解即可哈,接受C语言的设定

修改默认对齐数

YY:这么多空间都被白白浪费掉了,有啥办法咩~

  • 让占⽤空间⼩的成员尽量集中在⼀起
struct S1
{
	char ch1;
	char ch2;
	int a;
};

struct S2
{
	char ch1;
	int a;
	char ch2;
};

int main()
{
	printf("%zd\n", sizeof(struct S1));//8
	printf("%zd\n", sizeof(struct S2));//12
	return 0;
 }
  • 修改对齐数的默认值
    只要在括号里填上想要的默认对齐数即可修改
#pragma pack(1)
struct S1
{
	char ch1;
	int a;
	char ch2;
};
int main()
{
	printf("%zd", sizeof(struct S1));//6
	return 0;
}

结构体传参

和所有参数传递的情况一样,结构体传参也有两种方式,分别为传值调用和传址调用

//声明放在使用之前
struct S
{
	int arr[100];
	char n;
	char m;
};
//传值调用
void print1(struct S tmp)
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", tmp.arr[i]);
	}
	printf("\n");
	printf("%c\n", tmp.n);
	printf("%c\n", tmp.m);
}
//传址调用
void print2(struct S* p)
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
	printf("\n");
	printf("%c\n", p->n);
	printf("%c\n", p->m);
}

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

周知:传值调用是形参是实参一份临时拷贝,会再次开辟空间从而造成空间和时间上的浪费。因此,我们首选传址调用

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

结构体实现位段

YY同学现在想存储一种信息,这个信息的取值只有0,1,2,3四种情况,你是内存空间,你要给他分配多少的字节呢?
我的答案是两个比特位足矣
这就涉及到位段的相关知识,位段中的位就是指比特位,我们看以下代码

struct A
{
	char _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};
int main()
{
	printf("%zd", sizeof(struct A));
	return 0;
}
}

按照对齐规则来讲,结构体中包含四个字符型,因占四个字节,但是通过使用位段,该代码只占用了3个字节

什么是位段

位段的成员必须是 int ,unsigned int 或 char 并且位段的成员名后边有⼀个冒号和⼀个数字

位段在内存中的存储方式

假设数据存储时从右向左存储,也就是比特位在使用时先使用右边后使用左边,内存使用时由低地址到高地址
并且规定:位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的

前提:位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段,下文以VS为例

在这里插入图片描述

解释:首先存放a,开辟一个字节空间的大小,也就是八个比特位,10的二进制代码为1010,但是位段中规定只给a分配三个比特位,所以只存放了010,之后存放b,占四个比特位,此时还剩五个比特位,所以不用开辟内存空间,直接存放b的二进制代码1100,然后存放c,此时只剩下一个比特位所以开辟内存空间,申请了八个比特位,c占用五个比特位,再次开辟内存空间存放d,占用四个比特位,至此数据存储完毕,总共占用三个比特位

使用位段的注意事项

通过上述的代码我们注意到,a和b是共同占用一个字节的,那么a和b的地址就是相同的。聪明的YY同学又提问了
YY:占用同一个地址要怎么使用scanf来给a或者b赋值呢?

struct A
{
	char _a : 3;
	char _b : 4;
	char _c : 5;
	char _d : 4;
};
int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//err

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

由于位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊放在⼀个变量中,然后赋值给位段的成员。

总结

以上是C语言中结构体相关的知识,我们要像捡贝壳一样捡到自己的小篮子里哦,有不明白的地方欢迎留言,作者水平有限,文章不妥部分还请各位读者指正。

  • 21
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值