自定义类型:结构体类型

在学习完指针相关的知识后将进入到c语言中又一大重点——自定义类型,在之前学习操作符以及指针时我们对自定义类型中的结构体类型有了初步的了解,学习了结构体类型的创建以及如何创建结构体变量,还有结构体成员操作符的使用,现在我们将继续结构体相关知识的学习,希望在在看完本篇后你将会有新的收获,一起加油吧!!!


 1. 结构体类型的声明

1.1结构体回顾

在之前c语言常用操作符(2)中已经对结构体的声明进行了讲解,那么现在我们简单的复习一下

struct tag//tag的名字可是自定义的
{
member-list;//成员列表
}variable-list;//变量列表

例如创建一个描述学生的结构体

struct Stu//学生类型
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢

对结构体进行声明后就可以创建结构体变量并进行初始化了

struct Stu//学生类型
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id  : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
"⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id  : %s\n", s2.id);
return 0;
}

1.2结构体的特殊声明

在结构体中如果我们在创建结构体类型时不给结构体加上名字,那么这个结构体就是匿名结构体类型,例如以下代码就两个都是是匿名结构体类型

struct 
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s1,s2;struct 
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}*p1,*p2;int main()
{p1=&s1;return 0;
}

在以上代码中你认为s1=*p1这种写法是合法的吗?
在以上代码中先是创建了一个匿名结构体类型并创建了两个变量,之后又创建了一个结构体类型的指针,后将结构体变量s1的地址传给指针p1,
这时就会存在问题了原因是但我们创建两个结构体类型,虽然这两个结构体类型的成员都相同,但当创建两个匿名结构体类型时,编译器会默认这两个结构体的类型不相同,所以这时将s1的地址传给指针p1会使得等号两边的变量类型不相同 

注:匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用⼀次,
       所以使用匿名结构体类型时基本上是只打算对该结构体类型使用一次,之后不再使用

1.3结构体的自引用

数据在内存当中可能是连续存放的,也可能是像以下这样随机存放的

把1,2,3,4,5叫做节点,在节点中除了要存放数据外还要能有下一个节点的信息,这时我们就可以认为一个节点是一个结构体,那么各结构体能写成以下形式吗?

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

 这时将结构体里面的一个成员为相同的结构体类型,这时就存在一个问题,当我们用sizeof求struct Node的大小,那么使得大小为无穷大,所以以上这种写法是错误的

每各节点中我们是要存放下一个节点的信息,所以在节点中存放下一个结构体的地址也是符合要求的,因此就可以在结构体里面存放一个结构体指针类型,这时两个结构体类型不同就不会存在计算struct Node结构体时候出现无穷大的问题了

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

 我们知道使用typedef可以将变量类型简单化,如果在以下代码中将struct Node重命名为Node,那么结构体内的成员也可以简化为Node* next吗?

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

答案是不行的,原因是在对struct Node重命名前就使用简化后的名称Node会使得Node* next是未定义的,这时程序就会报错 

修改以下形式就是正确的了

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

2.结构体的内存对齐

在想要学习计算结构体在内存当中的大小,就需要学习一个重要的知识点:结构体的内存对齐

2.1内存对齐规则

#include<stdio.h>
struct s1
{char c1;char c2;int n;
};struct s2
{char c1;int n;char c2;
};
int main()
{printf("%zd", sizeof(struct s1));printf("%zd", sizeof(struct s2));return 0;
}

以上两个结构体的大小相同吗? 
运行程序发现结果不一样,这是为什么呢?

结构体的对齐规则:
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值

- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的
整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

要理解以上的对齐规则首先要了解偏移量偏移的是什么
其实偏移量就是相较于起始位置的字节偏移量


如果我们想知道s1中的各个成员变量的偏移量是多少,这时就可以用到一个宏offsetof,是c语言提供给我们的宏,作用是计算结构体成员相较结构体变量起始位置的偏移量

这时我们打开c++官网查看offsetof




 

注:使用offsetof需要引用头文件#include<stddef.h> 

#include<stdio.h>
#include<stddef.h>
struct s1
{char c1;char c2;int n;
};int main()
{printf("%zd\n", offsetof(struct s1,c1));printf("%zd\n", offsetof(struct s1,c2));printf("%zd\n", offsetof(struct s1, n));return 0;
}

​ 

 这时运行程序就可以得到c1,c2,n相较起始位置的偏移量分别为0,1,4

这时就会发现与以上计算出的struct s1的大小8字节相同
 

#include<stdio.h>
#include<stddef.h>
struct s1
{char c1;char c2;int n;
};
struct s2
{char c1;int n;char c2;};int main()
{printf("%zd\n", offsetof(struct s2,c1));printf("%zd\n", offsetof(struct s2,c2));printf("%zd\n", offsetof(struct s2, n));return 0;
}


 

这时就会发现这样计算出的struct s2大小为9个字节,这就与以上运行程序计算出的12不相同了,这是为什么呢? 

这时我们就要来了解结构体内存对齐的规则:

结构体的对齐规则:
1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值

- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

 所以在以上struct s2中一开始第一个成员c1的大小为1,比vs默认偏移数8小,所以偏移数就是1,c1大小为1

第二个成员n的大小为4,比vs默认偏移数8小,所以偏移数就是4,所以n从第五个字节开始,n大小为4字节

 第三个成员c2的大小为1,比vs默认偏移数8小,所以偏移数就是1,所以n从第九个字节开始,c2大小为1字节

 在此还未结束,因为结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍,该结构体struct s2最大对齐数为4,所以结构体大小必须是4的整数倍,以上的9字节不是4的整数倍,所以struct s2大小为12字节

 练习1

#include<stdio.h>
struct S3
{double x;char c;int n;
};
int main()
{printf("%zd\n", sizeof(struct s3));return 0;
}

以上代码的输出结果是什么呢?
 

x的大小为8个字节,偏移量为0,c的大小为1个字节,偏移量为9,n大小为4字节,偏移量为12,以上结构体大小为16字节满足结构体总大小为最大对齐数的整数倍

程序输出结果与分析结果相同

  练习2

#include<stdio.h>
struct S3
{double x;char c;int n;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{printf("%zd\n", sizeof(struct S4));return 0;
}

 以上代码的输出结果是什么呢?

在以上代码就涉及到了结构体嵌套问题,在struct S4中c1的大小为1个字节,偏移量为0,因为在以上已经求出了struct S3的大小为16字节,我们需要再了解一个规律如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍,所以 s3偏移量为8,大小为16字节,d的大小为8个字节,偏移量为24,最终结构体struct S4大小就为32个字节,符合结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

 

2.2 为什么存在内存对齐

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

例如在以上结构体struct S中如果机器每次内存读取4个字节,在未对齐情况下就要读取两次,而在对齐的情况下就只要读取一次 


总体来说:结构体的内存对齐是拿空间来换取时间的做法

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

struct s1
{char c1;char c2;int n;
};struct s2
{char c1;int n;char c2;
};

例如在以上两个结构体中成员都一样,s1将占空间小的成员集中在了一起就使得结构体所占空间小了一些

2.3 修改默认对齐数

在以上的学习中我们知道了VS的默认对齐数是8,那么有什么办法可以修改默认对齐数呢?
其实是有办法的,利用#pragma 这个预处理指令,可以改变编译器的默认对齐数。

#include<stdio.h>
#pragma pake(1)//修改默认对齐数为2
struct s1
{char c1;char c2;int n;
};
#pragma pake()//恢复默认对齐数
int main()
{printf("%zd", sizeof(struct s1));return 0;
}

 使用了#pragma后默认对齐数就被修改为了2,这时以上结构体struct s1的大小就变成6字节

3.结构体传参

#include<stdio.h>
struct S
{int data[1000];int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{for (int i = 0; i < 4; i++){printf("%d ", s.data[i]);}printf("\n");printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{for (int i = 0; i < 4; i++){printf("%d ", ps->data[i]);}printf("\n");printf("%d\n", ps->num);
}
int main()
{print1(s); //传结构体print2(&s); //传地址return 0;
}

我们知道在函数传参存在传值调用与传址调用,在结构体传参当中也是存在这两种方式的,以上代码中print1就是使用了传值调用,而print是使用了传址调用 ,两种方式都能找到结构体变量s以及其成员,但在结构体传参时一般是使用传址调用,原因是使用传址调用不用再开辟一块内存空间,新创建的指针就指向了原来的那块内存空间,而使用传值调用时,需要再创建一块新的内存空间,如果这个结构体像以上struct S一样所占内存空间很大的话,再创建新的内存就很浪费内存,还有函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

结论:
结构体传参的时候,要传结构体的地址

  

4. 结构体实现位段 

 4.1 什么是位段

位段的声明和结构是类似的,有两个不同:
1. 位段的成员必须是 int、unsigned int signed int ,在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。

例如以下结构体就是使用了位段

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

在此结构体中如果成员a知道自存0~3的整数,就可以在a后加上位段,2,这也就表示a成员只占2比特位,而用来a变量位int类型是占32个比特位,位段后就节省了许多空间,同理b,c,d也分别加上位段5,10,30

 

那么这和不使用位段的结构体大小有什么差别吗?

struct A
{
int _a;
int _b;
int _c;
int _d;
};

我们通过使用sizeof计算看看

 

这时就可以发现使用位段可以节省空间

那么为什么使用位段后以上结构体大小为8字节呢?这时我们就要学习位段的内存分配
 

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

 这时我们知道成员a所占大小为3比特位,类型为char,大小为8比特位,这时就存在一个问题在给了成员变量后在空间内部是从左向右使用还是从右向左使用?这时假设是从右向左使用,这时又存在一个问题了当剩下空间不满足存放下一个成员时候,空间是浪费还是使用呢?这里假设是浪费

 

这时我们创建一个struct S类型的变量并给各成员初始化为0,再给其赋值调试程序观察内存

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


 

存放入成员变量a的数是10二进制表示形式是1010,但成员a空间大小只有3比特位,所以存入是010, 存放入成员变量b的数是12二进制表示形式是1100,所以存入是1100, 存放入成员变量c的数是3二进制表示形式是11,所以存入是00011, 存放入成员变量d的数是4二进制表示形式是100,所以存入是0100

这时用16进制表示内存当中就为62 03 04

通过调试,查看内存与以上分析结果相同

 4.3 位段的跨平台问题

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

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

 

4.4 位段使用的注意事项 

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

 

正如以上的结构体中的a成员就没有相应的地址


所以不能对位段的成员使用&操作符,这样就不能使用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;
}

4.5位段的应用 

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这⾥使用位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报大小也会较小⼀些,对⽹络的畅通是有帮助的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/21157.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

win+mac通用的SpringBoot+H2数据库集成过程。

有小部分大学的小部分老师多毛病&#xff0c;喜欢用些晦涩难搞的数据库来折腾学生&#xff0c;我不理解&#xff0c;但大受震撼。按我的理解&#xff0c;这种数据库看着好像本地快速测试代码很舒服&#xff0c;但依赖和数据库限制的很死板&#xff0c;对不上就是用不了&#xf…

Linux基础之进程等待

目录 一、进程等待的基本概念 二、进程等待的重要性 三、进程等待的方法 四、获取子进程status 五、options选项 一、进程等待的基本概念 进程等待是指一个进程在执行过程中暂时停止&#xff0c;并等待某个条件满足后再继续执行的状态。这种等待通常是由于某些事件需要发生…

qt按钮的autoRepeat属性和default属性

autoRepeat属性&#xff1a;按住按钮不松&#xff0c;表示一直在点击按钮 default属性&#xff1a;点击Enter键表示在点击按钮

无缝接入GPT-4o:智创聚合API平台的创新与实践

在2024年5月13日&#xff0c;美国开放人工智能研究中心&#xff08;OpenAI&#xff09;发布了最新版本的ChatGPT——GPT-4o。这一更新标志着人工智能领域的又一重大进步&#xff0c;引起了全球科技界的广泛关注。GPT-4o的“o”代表“omni”&#xff08;全能&#xff09;&#x…

动态规划算法:背包问题

背包问题概述 背包问题 (Knapsack problem) 是⼀种组合优化的 NP完全问题 。 问题可以描述为&#xff1a;给定⼀组物品&#xff0c;每种物品都有⾃⼰的重量和价格&#xff0c;在限定的总重量内&#xff0c;我们如何选择&#xff0c;才能使得物品的总价格最⾼。 根据物品的个…

【刷题】初探递归算法 —— 消除恐惧

送给大家一句话&#xff1a; 有两种东西&#xff0c; 我对它们的思考越是深沉和持久&#xff0c; 它们在我心灵中唤起的惊奇和敬畏就会日新月异&#xff0c; 不断增长&#xff0c; 这就是我头上的星空和心中的道德定律。 -- 康德 《实践理性批判》 初探递归算法 1 递归算…

AI预测体彩排3采取888=3策略+和值012路一缩定乾坤测试6月2日预测第9弹

今天继续基于8883的大底进行测试&#xff0c;今天继续测试&#xff0c;好了&#xff0c;直接上结果吧~ 首先&#xff0c;888定位如下&#xff1a; 百位&#xff1a;5,4,7,3,2,9,1,0 十位&#xff1a;4,6,5,7,2,9,1,0 个位&#xff1a;3,4,2,5,…

车流量智能监测识别摄像机

车流量智能监测识别摄像机是一项革命性的技术&#xff0c;正在为城市交通管理带来巨大改变。这种摄像机利用先进的人工智能和图像识别技术&#xff0c;能够实时监测道路上的车流量&#xff0c;并对车辆进行智能识别和分类&#xff0c;从而实现对交通流量的精准监测和管理。 与传…

Day02 设计首页导航条

设计首页导航条 导航条的样式&#xff0c;主要是从Material DesignThemes UI 拷贝过来修改的,项目用了这个UI组件库。就看项目需要什么&#xff0c;就去源码拷过来使用。 直接下载源码&#xff0c;编译运行就可以看到Demo 了 下载后且正常编译成功了&#xff0c;是能正常跑起来…

iOS——类与对象底层探索

类和对象的本质 当我们使用OC创建一个testClass类并在main函数创建它的实例对象的时候&#xff0c;OC的底层到底是什么样的呢&#xff1f; 首先&#xff0c;我们要了解OC对象的底层结构&#xff0c;那么我们就得知道&#xff1a;OC本质底层实现转化其实都是C/C代码。 使用下面…

spoon工具的常用基础操作

一些常用转换工具 1、emp表输入->excel表输出 emp表输入&#xff0c;可以进行预览查看数据有没有过来excel表输出 成功执行后&#xff0c;可以到保存的excel位置进行查看。 2、excel输入->表输出 运行转换后可以在oracle进行查看是否有成功创建这个表 3、对部门最高…

【JAVA WEB实用与优化技巧】Maven自动化构建与Maven 打包技巧

文章目录 一、MavenMaven生命周期介绍maven生命周期命令解析 二、如何编写maven打包脚本maven 配置详解setting.xml主要配置元素setting.xml 详细配置 使用maven 打包springboot项目maven 引入使用package命令来打包idea打包 三、使用shell脚本自动发布四、使用maven不同环境配…

【协议开发系列】梳理关于TCP和UDP两种协议的区别和使用场景

起源 前二天项目上在核对外部对接服务的五元组列表的时候&#xff0c;有一位客户提问对于同样的服务同时支持tcp和udp二种方式&#xff0c;有什么优点和缺点&#xff0c;应该如何选择&#xff1f;这个问题突然让我愣了一下&#xff0c;确实好久没有“温故”了&#xff0c;相关…

商业新闻|当我们在讨论卖车时我们在讨论什么?

‍‍今天是2024年第22周 这是Yura「输出倒逼输入」计划的第10篇文章 全年进度&#xff1a;10/52 01 投资人为什么不断入局烧钱又亏损的新能源&#xff1f; 造车的烧钱速度超乎想象。除了最近的小米&#xff0c;这些年国内大大小小的玩家好像都在以不同的形式或直接或间接的参与…

【视频创作思维流程】教你从0培养视频创作思维

【视频创作思维流程】教你从0培养视频创作思维 1.创作认知2.培养自己的想象力2.1通过音乐辅助闭上眼睛想象2.2多看多见多模仿 3 视频脚本3.1简单的脚本3.2复杂脚本 4.拍摄预见能力4.1拍摄预见力思维用于转场4.2拍摄预见力思维给特效制作留住空间4.2拍摄预见力思维给字幕制作留住…

src挖掘-记一次付费资源的sign绕过

0x01 前言 最近都没怎么挖到过细小的漏洞&#xff0c;一直纠结于一些比较难以实现的点&#xff0c;天天在各种测试、上线服务器里面fuzz找遗漏的点(bushi) 大概上周突破了一处付费资源免费调用&#xff0c;定级了&#xff0c;故发出来水水文 0x02 绕过过程 逛着逛着主站&…

五种最新算法求解柔性作业车间调度问题(Flexible Job Shop Scheduling Problem,FJSP),提供MATLAB代码

一、WSA求解FJSP FJSP&#xff1a;波搜索算法(Wave Search Algorithm, WSA)求解柔性作业车间调度问题&#xff08;FJSP&#xff09;&#xff0c;提供MATLAB代码-CSDN博客 二、SBOA求解FJSP FJSP&#xff1a;蛇鹫优化算法&#xff08;Secretary bird optimization algorithm&a…

模糊小波神经网络(MATLAB 2018)

模糊系统是一种基于知识或规则的控制系统&#xff0c;从属于智能控制&#xff0c;通过简化系统的复杂性&#xff0c;利用控制法来描述系统变量之间的关系&#xff0c;采用语言式的模糊变量来描述系统&#xff0c;不必对被控对象建立完整的数学模型。相比较传统控制策略&#xf…

WIN系统 -> 以太网未识别的网络问题

1.方法1 2. 3. 根据诊断提示解决问题。 方法2. 右键以太网属性

GiantPandaCV | 浅谈分辨率对模型影响以及训练方法

本文来源公众号“GiantPandaCV”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;浅谈分辨率对模型影响以及训练方法 一、前言 最近几个人在讨论模型训练的时候&#xff0c;提到了一个尺度对于模型的影响以及训练方法的收益&#…