【C语言】自定义类型:结构体

本文详细介绍了结构体的声明、变量创建、内存对齐规则、结构体传参、位段使用以及VS编译器中的结构体布局。作者强调了内存对齐的重要性,并提供了代码示例和内存布局分析。

今天我来分享我学到的关于结构体的知识:

一.结构体类型的声明

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

三.结构成员访问操作符

四.结构体内存对齐

五.结构体传参

六.结构体实现位段

1.结构体类型声明

我们不妨看一下声明:

struct tag

{

     member-list;

   }variable-list;

假如我们描述一个人:

struct Stu

{

     size_t age;

    char name;

    int marks;

};(注意:不要漏掉了这个分号)

特殊的声明

我们也可以不要名称,直接匿名声明结构体:

这里虽然两个结构体类型相同,但是由于是匿名声明,是只能使用一次的,我们在声明结构体的时候要带上结构体名称,不然就没什么意义了。

结构体的自引用

我们之前学过什么叫递归,递归就是函数自己调用自己,而结构体其实也是可以自引用的,有点想递归但又不完全相同,下面我们一起来看看错误示例和正确示例吧:

错误示例:

struct Node

{

   int data;

   struct Node next;

}

这个是模拟链表,虽然数据是分散的,但通过这种手段成功的将这些数据连接在一起,挨个访问,但这里我们打个比方:我们不能在自己的车子里在塞进去一辆一模一样大小的车子吧,所以这个是不行的。(求这个结构体的大小将会是无穷大)

正确示例:

struct Node

{

int data;

struct Node *next;

}

我们传一个指针过去就可以了,达到链式访问的效果。

但是我们有些人喜欢摆弄:

#include<stdio.h>
typedef struct
{
	int data;
	struct Node* next;
}Node;
//我们用这样的方式来重命名,是不行的,因为我们在把这个结构体设置全局变量的名称的时候
//在结构体里面就使用了这个名称。

括号外面这个Node是因为上面的匿名结构体而产生的,但是里面的成员变量优先使用了Node来创建变量,这是不行的。

正确的思路应该是

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

就不要用匿名结构体了。

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

这个看我写一段代码就很好理解了:

#include<stdio.h>
struct Stu
{
int age;
char name;
float grade;
};//这个是声明;
struct Tea
{
int age;
char name;
float grade;
}p1;//边声明边创造一个结构体变量p;
struct Fam
{
int age;
struct Stu p7;
float grade;
}p2={20,"zhangsan",20.00};//边声明边创建一个全局结构体变量p2;
int main()
{
struct Stu s1={10,"wangwu",70.00};
struct Stu s2={30,"lisi",80.00};//一个名字Stu可以创建多个结构体变量,这样“赋值”算初始化;
struct Fam s9={100,{80,"lanlan",150.00},76.00};//嵌套结构体初始化;
return 0;
}

我们初始化也不一定要按照顺序来,如图:

3.结构体成员访问操作符

我们访问结构体成员的操作符有两个,一个是".",一个是“->”

结构体名称.成员变量名或结构体指针->成员变量名得到相关元素。

我们一起来看一下怎么弄吧:

看图片:

优化:如果我们不想指针对应的内容被改变,用const修饰;另外我们尽量传指针过去,以免造成内存过于占用,效率降低。

4.结构体内存对齐

结构体有以下4点对齐规则:

1 结构体中第一个元素默认在偏移起始点地址0的位置存放;

2 往后的元素存放的位置存放在偏移起始点地址为对齐数整数倍的位置上;

对齐数=编译器默认对齐数与该数据所占字节数的较小值;

3.这个结构体的大小必须等于这个结构体中铺开所有元素中的最大对齐数的整数倍;

4.如果嵌套了结构体,那么里面这个结构体的位置的可能对齐数是里面这个小结构体里面的成员最大对齐数的整数倍,同时考虑编译器的默认对齐数(VS默认为8,linux的对齐数是元素的对齐数),再做打算;整个大结构体的内存大小是全部铺开后的元素的最大对齐数的整数倍最大整数倍。

我们来几道题先实验一下:

//第一题
struct S1

{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));
//第二题
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));
//第三题
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
//第四题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));

第一题:char   int   char

第二题:char char int

第三题:double char int

第四题:嵌套结构体类型char struct double

我们挑第四题来证明一下:使用offsetof宏

但是第二个就错了,因为我没有考虑s3里面虽然那个double类型是从起始点开始的,但是s3的最大对齐数其实是8,s3的内存是8的倍数,也是24没毛病,然后画s4的内存布局要更改一下:

s3的布局图:

s4的布局图:

这里我再介绍一下怎么修改编译器的默认值:

#include <stdio.h>

#pragma pack(1)//设置默认对⻬数为1 

struct S

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

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

在默认对齐数不合理的时候,我们可以考虑把它改一下。

结构体为什么要内存对齐?简而言之就是牺牲空间换取效率!

5.结构体传参

我们结构体传参其实和数组传参是差不多的,声明函数的时候,要加上类型名,形参,而在main函数里面使用函数时传递实参就行了。老规矩:分为传值调用和传址调用。

#include<stdio.h>
struct Stu {
	int age;
	char name[20];
};//仅仅是声明,还没有初始化及定义变量
void print1(struct Stu s1)
{
	printf("%d %s\n", s1.age, s1.name);
}
void print2(struct Stu* s2)
{
	printf("%d %s\n", s2->age, s2->name);
}
int main()
{
	struct Stu s = { 20,"lihua" };//这个结构体定义了结构体变量s,
	//并给它初始化内容(我们也可以叫"赋值")。
	//定义一个print1函数,一个print2函数,前者用来传值调用,后者用来传址调用。
	print1(s);
	print2(&s);
	return 0;
}

代码结果是

但是我这里更推荐使用传址调用,因为如果我选择传值调用,参数是要压栈的,会消耗系统大量的内存和时间,这是不提倡的,而使用传址调用则好的多。

6.结构体实现位段

位段其实是基于结构体的,它其实和结构体的声明等十分相似,但又有一些不同,比如:

1.位段的成员需要是int,unsigned int,signed int或者char类型;

2.位段里面多了一个冒号;

我们现在来写代码举例一下:

这里的下划线可有可无,这里的数字表示为a,b,c,d分别开放2个bit位,3个bit位,4个bit位,5个bit位(注意:我这里讲的位同样也是正儿八经的二进制位),以达到节约空间的目的。

但是,我们这里先提醒一些内容:

1.由于大家使用的编译器不一样,编译器没办法对int类型默认为unsigned int或者是signed int,这样我们在使用的时候就可能会出bug了;

2.有一些编译器是16位的,当我想给一个数据开辟17个bit位上,编译器会报错;

3.我们无法确定从上而下的这些数据是从左往右还是从右往左分配空间的;

4.这些数据占用内存后,剩下的空间我们也不知道会怎么利用;

5.位段的内存是以int(4字节)或char(1字节)开辟空间的;

6.我们无法对这些内容进行取地址观察,因为取地址至少是以一个字节为单位,在这个字节的开头开始读取的,我们不能保证刚好我们可以通过取地址再访问的方式得到我们想要的结果;

现在我通过画图和内存监视的方式讲一下在vs2022下,这些经过位段“处理”的数据可能在内存里这样放:(顺便讲一下在这基础上一个位段的大小如何)

先上代码:

#include<stdio.h>
struct B
{
	char a : 2;
    char _b : 3;
	char _c : 4;
	char _d : 5;
};
int main()
{
struct B s={0};
s.a=2;
s.b=5;
s.c=8;
s.d=20;
	printf("%zd", sizeof(struct B));//答案是3
	return 0;
}

我们来画一下图:

转换成16进制后,为1 6    0 8     1 4

我们看一下内存布局图:

这个是仅仅在vs系列下的结果,其他的我们还有待探究。

本系统旨在构建一套面向高等院校的综合性教务管理平台,涵盖学生、教师及教务处三个核心角色的业务需求。系统设计着重于实现教学流程的规范化与数据处理的自动化,以提升日常教学管理工作的效率与准确性。 在面向学生的功能模块中,系统提供了课程选修服务,学生可依据培养方案选择相应课程,并生成个人专属的课表。成绩查询功能支持学生查阅个人各科目成绩,同时系统可自动计算并展示该课程的全班最高分、平均分、最低分以及学生在班级内的成绩排名。 教师端功能主要围绕课程与成绩管理展开。教师可发起课程设置申请,提交包括课程编码、课程名称、学分学时、课程概述在内的新课程信息,亦可对已开设课程的信息进行更新或撤销。在课程管理方面,教师具备录入所授课程期末考试成绩的权限,并可导出选修该课程的学生名单。 教务处作为管理中枢,拥有课程审批与教学统筹两大核心职能。课程设置审批模块负责处理教师提交的课程申请,管理员可根据教学计划与资源情况进行审核批复。教学安排模块则负责全局管控,包括管理所有学生的选课最终结果、生成包含学号、姓名、课程及成绩的正式成绩单,并能基于选课与成绩数据,统计各门课程的实际选课人数、最高分、最低分、平均分以及成绩合格的学生数量。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值