C语言——自定义类型——结构体(从零到一的跨越)

目录

前言

1.什么是结构体

2.结构体类型的声明

2.1结构体的声明

2.2结构体的创建和初始化

2.3结构成员访问操作符

2.3.1结构体成员直接访问

2.3.2结构体成员的间接访问

2.4结构体变量的重命名

2.5结构体的特殊声明

2.6结构的自引用

3.结构体内存对齐

3.1对齐规则

3.2为什么存在内存对齐

3.3修改默认对齐数

4.结构体传参

5.结构体实现位段

5.1位段的声明

5.2位段的内存分配

5.3位段的注意事项



前言

          在学习结构体之前,我们还学习了char ,int,short,float,double等内置类型,他们可以描述一些事物的某一项属性,但是如果我想描述一本书的某些属性,而不是单单的一种属性,该怎么办呢?这时候就会用到自定义类型——结构体,我们就可以创造出属于我们自己的类型。

1.什么是结构体

           结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体。也就是说结构体是由一些内置类型构成的,这些类型表示事物的某些属性。

2.结构体类型的声明

2.1结构体的声明

struct book
{char name[100];//书名int price;//价格char author[100];//作者
};//分号不能丢

struct是结构体的标志,book是我们自定义的结构体的名字,{}里面的是成员变量

2.2结构体的创建和初始化

按照顺序初始化struct book b1 = { "红心照耀中国",100,"埃德加·斯诺" };指定顺序初始化struct book b2 = { .author = "卡尔·马克思、弗里德里希·恩格斯",.name = "共产党宣言",.price = 50 };

struct book b1和struct book b2是结构体的创建,后面的{}是对这个结构体进行初始化,struct book相当于之前学习的char和int,b1和b2就是变量名。

以上两种是在声明结构体之后进行的初始化,除此之外还可以在声明结构体的时候进行初始化 ,就像这两种方法这样

	struct book
{char name[100];int price;char author[100];
}b1 = { "红心照耀中国",100,"埃德加·斯诺" };struct book
{char name[100];int price;char author[100];
}b2 = { .author = "卡尔·马克思、弗里德里希·恩格斯",.name = "共产党宣言",.price = 50 };

结构体变量的创建和初始化还可以分开进行

比如这个样子:

#include<stdio.h>
struct book
{char name[100];int price;char author[100];
}x;//创建变量int main()
{struct book x = { "红星照耀中国",100,"埃德加·斯诺" };//初始化return 0;
}

2.3结构成员访问操作符

结构体变量创建出来是为了使用的,那他会不会像int,double等类型的使用方法一样呢,关于结构体的使用,这里鱼哥给大家介绍两个操作符(.)结构体成员直接访问,(->)结构体成员简介访问

2.3.1结构体成员直接访问

#include<stdio.h>
struct book
{char name[100];int price;char author[100];
};
int main()
{struct book b1 = { "红心照耀中国",100,"埃德加·斯诺" };printf("%s\n", b1.name);printf("%d\n", b1.price);printf("%s\n", b1.author);
}

使用方法:变量名.成员名

2.3.2结构体成员的间接访问

#include<stdio.h>
struct book
{char name[100];int price;char author[100];
}b1 = { "红心照耀中国",100,"埃德加·斯诺" };
int main()
{
struct book *ptr = &b1;
ptr->price = 10;printf("%d\n", b1.price);return 0;
}

使用方法:结构体指针->成员名

2.4结构体变量的重命名

typedef struct book
{char name[100];int price;char author[100];
}Book;
使用typedef对struct book进行重命名,Book就相当于原来的struct book,也就是他们两个的效果是一样的

2.5结构体的特殊声明

struct 
{char name[100];int price;char author[100];
}x;

上面这种声明属于匿名结构体声明,也就是在结构体声明的时候省略了标签(book)

注意:匿名结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次

2.6结构的自引用

结构体出来能装内置类型,可不可以装结构体呢?

struct book2
{int x;int y;
};struct book
{char name[100];int price;char author[100];struct book2 x1;
};

这样写是OK的,我们可以算出他所占内存空间的大小

能装别的结构体不算nb,如果他能装自己才叫nb

 struct book
{char name[100];int price;char author[100];struct book x1;
};

那么这样写是正确的吗?如果正确,那么他所占内存空间的大小是多少?

经过分析,这样写结构体会无限嵌套下去,这样是无法计算出结构体的大小,他的大小就会无穷大,所以这样的写法是错误的

那么有没有正确的写法呢?答案是有的

 struct book
{char name[100];int price;char author[100];struct book *next;
};

我们可以这样写,既然结构体不能嵌套结构体,那我结构体装结构体指针,通过结构体指针找到结构体


在结构体自引用使用的过程中,掺杂着typedef对匿名结构体类型的重命名,也是容易出现问题的

typedef struct
{
int x;
Book*next;
}Book;

这样的写法也是不行的,因为Book是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Book来创建成员变量,这是不行的

3.结构体内存对齐

3.1对齐规则

1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

2.其他成员变量要对其到某个数字(对齐数)的整数倍的地址处

    对齐数=编译器默认的一个对齐数与该成员变量大小的较小值

    --VS中默认的值为8

    --Linux中gcc没有默认的对齐数,对齐数就是成员自身的大小

3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的的整数倍

4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

光看规则肯定是学不明白的,接下来鱼哥给大家举一下例子帮助大家理解

例1.

struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));

这个结构体的大小是多少?大家可以自己先算一算。

答案是12,怎么算出来的呢

有人会想,c1一个字节,i四个字节,c2两个字节,应该6个字节才对啊

数字代表偏移量

c1是一个字节,并且要对齐到偏移量为0的地址处

i是四个字节,4小于8,所以对齐数是4,所以i要存储到4的整数倍处

c2是一个字节,对齐数是1,所以c2存到偏移量为8的地址处

最大对齐数是4,所以结构体的总大小是4的倍数,而c1,i,c2占9个字节,所以结构体的大小要大于等于9并且是4的倍数,所以这个结构体的大小是12个字节

 例2.

struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));

 这个结构体的大小是8,你会想,这个结构体和例1的成员变量都一样,只是位置不同,凭什么这个的内存就要小一点呢?

c1从偏移量为0的地址处开始存储

c2的对齐数是1,偏移量1是1的倍数,所以1处存c2

i的对齐数是4,所以i从4的位置开始存储

c1,c2,i占8个字节,而最大对齐数是4,8是对齐数的整数倍,所以这个结构体的大小是8个字节

例3.

struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));

这个结构体的大小是16,有了前面两个例子,鱼哥相信你应该可以算出结果

d从0开始存储,字节大小是8

c的对齐数是1,存储在偏移量为8的位置

i的对齐数是4,存储在偏移量为12的位置

d,c,i占的字节数为16,16又是最大对齐数8的整数倍,所以结构体的大小为16个字节

例4.

struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));

s3是例3的结构体,这个s4结构体的大小是32

c1从偏移量为0开始

s3的最大对齐数是8,所以s3从偏移量为8的位置开始存储,因为例3已经算出了s3的大小,所以s3存16个字节

d的对齐数是8,24是8的倍数,所以d从24开始存储

c1,s3,d占32个字节,32又是8的整数倍,所以s4的大小是32个字节

3.2为什么存在内存对齐

1.平台原因(移植原因):

     不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.性能原因:

      数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中

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

对例1和例2分析,我们发现如果让占用空间小的成员尽量集中在一起可以起到节省空间的效果

3.3修改默认对齐数

VS的编译器的默认对齐数是8,在某些情况下,我们觉得默认对齐数不合适,想要修改,就会用到#pragma这个预处理指令

#include<stdio.h>
#pragma pack(2)
struct S
{char c1;int a2;char c2;
};
int main()
{printf("%d\n", sizeof(struct S));return 0;
}

这里将默认对齐数改为2,大家可以通过前面的学习自己算一下答案

学会修改也要学会恢复,怎么做呢?看接下来的代码

#include<stdio.h>
#pragma pack(1)
struct S
{char c1;int a2;char c2;
};
#pragma pack()
int main()
{printf("%d\n", sizeof(struct S));return 0;
}

4.结构体传参

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

上面这个代码进行了两个结构体的传参,那么哪一种传参更好一点呢?

答案是print2的传参更好一点

原因是:

函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降

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

5.结构体实现位段

5.1位段的声明

struct S
{
int a:2;
int c:3;
int f:24;
};

上述代码就是一个简单的位段声明,是不是感觉和结构体比较像

那么他和结构体有什么区别呢?

1.位段的成员必须是int,unsigned int,signed int (整形家族的),在C99中也可以是其他类型

2.位段的成员名后面是一个冒号加一个数字(这个数字代表比特位数)

5.2位段的内存分配

位段的空间上是按照4个字节或者1个字节的方式来开辟的

#include<stdio.h>struct S
{char a : 2;char b : 4;char c : 5;char d : 6;
};
struct S s = { 0 };
int main()
{s.a = 10;s.b = 12;s.c = 13;s.d = 14;printf("%d", sizeof(struct S));return 0;
}

结果为3,来看看数据是怎么在内存中存储的吧

·首先申请一个字节的空间也就是8个比特位,a:两个比特位,将a的值转换位二进制,然后存低位的两个存进去,b是4个比特位,因为申请了8个比特位还剩6个,够用,就接着存,c是5个比特位,不够了,就舍去,重新申请一个字节,存放5个比特位,剩三个,不够d的6个bit位,就有申请一个字节存放d,所以总的申请了3个字节

总结:跟结构体相比,位段可以达到同样的效果,并且可以很好的节省空间

5.3位段的注意事项

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

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

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

相关文章

10:00面试,10:06就出来了,问的问题有点变态。。。

从小厂出来&#xff0c;没想到在另一家公司又寄了。 到这家公司开始上班&#xff0c;加班是每天必不可少的&#xff0c;看在钱给的比较多的份上&#xff0c;就不太计较了。没想到8月一纸通知&#xff0c;所有人不准加班&#xff0c;加班费不仅没有了&#xff0c;薪资还要降40%…

echart trigger 为 axis 的时候不显示 tooltip 解决办法

echart trigger 为 axis 的时候不显示 tooltip 解决办法 在项目 vitetsvue3 中使用 echart 显示了一个曲线图&#xff1a; 但当把图表的 trigger 设置成 axis 的时候&#xff0c;鼠标扫过并不显示具体的数值&#xff0c;如上图所示。 但 trigger item 的时候是正常的。 解决…

【virtio-networking 和 vhost-net 简介】

文章目录 Virtio 基本构建块Virtio spec 和 vhost 协议Vhost-net/virtio-net architectureVirtio-networking and OVS总结参考链接 Virtio 是作为虚拟机 (VM)访问简化device&#xff08;如块设备和网络适配器&#xff09;的 标准化开放接口而开发的。Virtio-net是一种虚拟以太…

【Linux】多线程概念 | POSIX线程库

文章目录 一、线程的概念1. 什么是线程Linux下并不存在真正的多线程&#xff0c;而是用进程模拟的&#xff01;Linux没有真正意义上的线程相关的系统调用&#xff01;原生线程库pthread 2. 线程和进程的联系和区别3. 线程的优点4. 线程的缺点5. 线程异常6. 线程用途 二、二级页…

excel 破解 保护工作簿及保护工作表

excel 破解 保护工作簿及保护工作表 对于这种 保护工作簿及保护工作表 不知道密码时&#xff0c;可以使用以下方法破解 保护工作簿破解 打开受保存的excel 右键点击sheet名称 —> 查看代码 复制以下代码&#xff0c;粘贴到代码区域 Sub 工作簿密码破解() ActiveWorkbook.…

C++ —— 类和对象(终)

目录 1. 日期类的实现 1.1 前置 和 后置 重载 1.2 >> 和 << 的重载 2. const 成员 3. 取地址及const取地址操作符重载 4. 再谈构造函数 4.1 构造函数体赋值 4.2 初始化列表 4.3 隐式类型转换 4.4 explict 关键字 5. static 成员 5.1 概念 5.2 特性 …

【Web】浅聊Hessian异常toString姿势学习复现

目录 前言 利用关键 调用分析 如何控制第一个字节 EXP 前言 Hessian CVE-2021-43297&#xff0c;本质是字符串和对象拼接导致隐式触发了该对象的 toString 方法&#xff0c;触发toString方法便可生万物&#xff0c;而后打法无穷也&#xff01; 这个CVE针对的是Hessian2I…

Follow-Your-Click——点选图像任意区域对象使用短提示语即可生成视频

简介 “I2V”&#xff08;图像到视频生成&#xff09;旨在将静态图像转换为具有合理动作的动态视频剪辑&#xff0c;在电影制作、增强现实和自动广告等领域有广泛应用。然而&#xff0c;现有的I2V方法存在一些问题&#xff0c;例如缺乏对图像中需要移动的部分的精准控制&#…

Chain of Verification-CoVe减少LLM中的幻觉现象

Chain-Of-Verification Reduces Hallucination In Large Language Models 在大型语言模型中&#xff0c;产生看似合理但实际上错误的事实信息&#xff0c;即幻觉&#xff0c;是一个未解决的问题。我们研究了语言模型在给出回答时进行深思以纠正错误的能力。我们开发了Chain-of…

深度解析 Android 系统属性

目录 Android系统属性 1.属性在哪里&#xff1f; 2.属性长什么样&#xff1f; 3.如何读写属性&#xff1a; 4.属性的作用 属性文件生成过程 如何添加系统属性 1.添加系统属性到 /system/build.prop 2.添加系统属性到 /vendor/build.prop 3.添加系统属性到 /product/b…

未来教育趋势:AI个性化培训如何推动企业与员工共赢

AI定制学习&#xff1a;重新定义个性化员工培训的未来 随着人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;我们正目睹并亲历了AI在培训领域所引发的根本性变革。AI技术的整合不仅革新了知识传递的模式&#xff0c;而且重新塑造了个性化学习的内涵。依托于尖端算…

《深入Linux内核架构》第2章 进程管理和调度 (4)

目录 2.6 CFS调度类 2.6.1 数据结构 2.6.2 CFS操作 2.6.3 队列操作 2.6.4 选择下一个进程 2.6.5 处理周期性调度器 2.6.6 唤醒抢占 2.6 CFS调度类 即完全公平调度类&#xff0c;用于调度普通进程。 2.6.1 数据结构 struct sched_class fair_sched_class {.next &am…

Restormer: Efficient Transformer for High-Resolution Image Restoration

Abstract 由于卷积神经网络&#xff08;CNN&#xff09;在从大规模数据中学习可概括的图像先验方面表现良好&#xff0c;因此这些模型已广泛应用于图像恢复和相关任务。最近&#xff0c;另一类神经架构 Transformer 在自然语言和高级视觉任务上表现出了显着的性能提升。虽然 T…

【安全类书籍-2】Web渗透测试:使用Kali Linux

目录 内容简介 作用 下载地址 内容简介 书籍的主要内容是指导读者如何运用Kali Linux这一专业的渗透测试平台对Web应用程序进行全面的安全测试。作者们从攻击者的视角出发,详细阐述了渗透测试的基本概念和技术,以及如何配置Kali Linux以适应渗透测试需求。书中不仅教授读者…

vulnhub-----SickOS靶机

文章目录 1.信息收集2.curl命令反弹shell提权利用POC 1.信息收集 ┌──(root㉿kali)-[~/kali/vulnhub/sockos] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:10:3c:9b, IPv4: 10.10.10.10 Starting arp-scan 1.9.8 with 256…

邮件客户端 Thunderbird 简单配置

1. 基本情况介绍 原来使用的邮箱客户端是 Office 365 自带的 Outlook 365切换原因&#xff1a;新装电脑&#xff0c;发现原 Outlook 中的账号信息无法迁移&#xff0c;需要耗费大量时间手动配置邮箱使用的邮箱&#xff1a;微软 O365 邮箱、qq 邮箱、163 邮箱、公司私有邮箱 …

北斗卫星引领农机春耕新时代

北斗卫星引领农机春耕新时代 随着现代科技的快速发展&#xff0c;北斗卫星成为了农业领域不可或缺的利器。在农机自动驾驶系统的引领下&#xff0c;农机正逐渐实现自主操作&#xff0c;为农民节省了大量的时间和精力&#xff0c;并最大限度地提高了农作物的产量和质量。 北斗…

MySQL | 内置函数

目录 1. 日期函数 2. 字符串函数 3. 数学函数 4. 其他函数 4.1. USER()查询当前用户 4.2. MD5(STR)对一个字符串进行md5摘要 4.3. DATABASE()显示当前正在使用的数据库 4.4. PASSWORD()函数&#xff0c;MySQL使用该函数对用户进行加密 4.5. IFNULL(VAL1, VAL2)如果VAL1…

window下安装并使用nvm(含卸载node、卸载nvm、全局安装npm)

window下安装并使用nvm&#xff08;含卸载node、卸载nvm、全局安装npm&#xff09; 一、卸载node二、安装nvm三、配置路径和下载源四、使用nvm安装node五、nvm常用命令六、卸载nvm七、全局安装npm、cnpm八、遇到的问题 nvm 全名 node.js version management&#xff0c;顾名思义…

Vivado中的五种仿真模式比较

Vivado中的五种仿真模式 在数字电路设计过程中&#xff0c;通常涵盖三个主要阶段&#xff1a;源代码编写、综合处理以及电路的实现&#xff0c;相应地&#xff0c;电路仿真的应用也与这些阶段紧密相关。根据不同设计阶段的需求&#xff0c;仿真可以被划分为RTL行为级仿真、综合…