C语言 14 结构体 联合体 枚举

之前认识过很多种数据类型,包括整数、小数、字符、数组等,通过使用对应的数据类型,就可以很轻松地将数据进行保存了,但是有些时候,这种简单类型很难去表示一些复杂结构。

结构体

比如现在要保存 100 个学生的信息(学号、姓名、年龄),似乎找不到一种数据类型能够同时保存这三种数据(数组虽然能保存一系列的元素,但是只能保存同种类型的)。但是如果把它们拆开单独存在,就可以使用对应的类型存放了,不过这样也太不方便了吧,这些数据应该是捆绑在一起的,而不是单独地去存放。

为了解决这种问题,C 语言提供了结构体类型,它能够将多种类型的数据集结到一起,让他们形成一个整体。

// 使用 (struct关键字 + 结构体类型名称) 来声明结构体类型,这种类型是自己创建的(同样也可以作为函数的参数、返回值之类的)
struct Student {  // 结构体中可以包含多个不同类型的数据,这些数据共同组成了整个结构体类型(当然结构体内部也能包含结构体类型的变量)int id;       int age;// 用户名可以用指针指向一个字符串,也可以用char数组来存,如果是指针的话,那么数据不会存在结构体中,只会存放字符串的地址,但是如果是数组的话,数据会存放在结构体中char* name;  
};
#include <stdio.h>int main() {// 也可以以局部形式存在struct Student {  int id;       int age;char* name;  };
}

定义好结构体后,只需要使用结构体名称作为类型就可以创建一个结构体变量了:

#include <stdio.h>struct Student {int id;int age;char * name;
};int main() {//类型需要写为struct Student,后面就是变量名称struct Student s = {1, 18, "小明"};     //结构体包含多种类型的数据(它们是一个整体),只需要把这些数据依次写好放在花括号里面就行了
}
#include <stdio.h>// 也可以直接在花括号后面写上变量名称(多个用逗号隔开),声明一个全局变量
struct Student {int id;int age;char* name;
} s;int main() {}

这样就创建好了一个结构体变量,而这个结构体表示的就是学号为 1、年龄 18、名称为小明的结构体数据了。

当然,结构体的初始化需要注意:

#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {// 如果只写一半,那么只会初始化其中一部分数据,剩余的内容相当于没有初始值,跟数组是一样的struct Student s1 = {1, 18};              // 也可以指定去初始化哪一个属性 .变量名称 = 初始值struct Student s2 = {2, .name = "小红"};  // 结构体变量.数据名称 (这里.也是一种运算符) 就可以访问结构体中存放的对应的数据了printf("id = %d, age = %d, name = %s\n", s1.id, s1.age, s1.name);printf("id = %d, age = %d, name = %s", s2.id, s2.age, s2.name);
}
id = 1, age = 18, name = (null)
id = 2, age = 0, name = 小红

当然也可以通过同样的方式对结构体中的数据进行修改:

#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {struct Student s = {1, 18, "小明"};s.name = "小红";s.age = 17;printf("id = %d, age = %d, name = %s", s.id, s.age, s.name);
}
id = 1, age = 17, name = 小红

那么结构体在内存中占据的大小是如何计算的呢?比如下面的这个结构体

struct Object {int a;short b;char c;
};

这里我们可以借助sizeof关键字来帮助计算:

#include <stdio.h>int main() {// sizeof能够计算数据在内存中所占据的空间大小(字节为单位)printf("int类型的大小是:%lu", sizeof(int));
}
int类型的大小是:4

当然也可以计算变量的值占据的大小:

#include <stdio.h>int main() {int arr[10];// 在判断非类型时,sizeof 括号可省printf("int arr[10]占据的大小是:%lu", sizeof arr); 
}
int arr[10]占据的大小是:40

同样的,它也能计算结构体类型会占用多少的空间:

#include <stdio.h>struct Object {char a;int b;short c;
};int main() {// 直接填入struct Object作为类型printf("%lu", sizeof(struct Object));  
}
12

可以看到结果是 12,那么,这个 12 字节是咋算出来的呢?

int(4字节)+ short(2字节)+ char(1字节) = 7字节

实际上结构体中的各个数据要求字节对齐,规则如下:

  • 规则一: 结构体中元素按照定义顺序依次置于内存中,但并不是紧密排列的。从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上(0默认是所有大小的整数倍)
  • 规则二: 如果结构体大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾。
  • 规则三: 基本数据类型的对齐大小为其自身的大小,结构体数据类型的对齐大小为其元素中最大对齐大小元素的对齐大小。

这里以下面的为例:

struct Object {// char占据1个字节char a;   // int占据4个字节// 因为前面存了一个char,按理说应该从第2个字节开始存放// 但是根据规则一,必须在自己的整数倍位置上存放// 因为2不是4的整数倍位置,这时离1最近的下一个整数倍地址就是4了// 所以前面空3个字节的位置出来,然后再放置int b;  // 前面存完int之后,就是从8开始了,刚好满足short(2字节)的整数倍// 但是根据规则二,整个结构体大小必须是最大对齐大小的整数倍(这里最大对齐大小是int,所以是4)// 存完short之后,只有10个字节,所以后面再补两个空字节,这样就是12个字节了short c; 
};


前面介绍了结构体,现在可以将各种类型的数据全部安排到结构体中一起存放了。

不过仅仅只是使用结构体,还不够,可能还需要保存很多个学生的信息,所以需要使用结构体类型的数组来进行保存:

#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {// 声明一个结构体类型的数组,其实和基本类型声明数组是一样的// 多个结构体数据用逗号隔开struct Student arr[3] = {{1, 18, "小明"},{2, 17, "小红"},{3, 18, "小刚"}};// 先通过arr[1]拿到第二个结构体,然后再通过同样的方式 .数据名称 就可以拿到对应的值了printf("%s", arr[1].name); 
}
小红

当然,除了数组之外,还可以创建一个指向结构体的指针。

拿到结构体类型的指针后,实际上指向的就是结构体对应的内存地址,和之前一样,也可以通过地址去访问结构体中的数据:

#include <stdio.h>struct Student {int id;int age;char* name;
};int main() {struct Student student = {1, 18, "小明"};// 同样的,类型后面加上*就是一个结构体类型的指针了struct Student* p = &student;// 由于.运算符优先级更高,所以需要先使用*p得到地址上的值,然后再去访问对应数据printf("%s\n", (*p).name);// 上面的写法写起来太累了,可以使用简便写法// 使用 -> 运算符来快速将指针所指结构体的对应数据取出printf("%s", p->name);
}
小明
小明

再来看看结构体作为参数在函数之间进行传递时会经历什么:

#include <stdio.h>struct Student {int id;int age;char* name;
};void test(struct Student student){// 对传入的结构体中的年龄进行修改student.age = 19;   
}int main() {struct Student student = {1, 18, "小明"};test(student);// 最后会是修改后的值吗?printf("%d", student.age);  
}
18

可以看到在其他函数中对结构体内容的修改并没有对外面的结构体生效,因此,实际上结构体也是值传递,修改的只是另一个函数中的局部变量而已。

所以如果需要在另一个函数中处理外部的结构体,需要传递指针:

#include <stdio.h>struct Student {int id;int age;char* name;
};// 这里使用指针,那么现在就可以指向外部的结构体了
void test(struct Student* student) {  // 别忘了指针怎么访问结构体内部数据的student->age = 19;                
}int main() {struct Student student = {1, 18, "小明"};// 传递结构体的地址过去test(&student);  printf("%d", student.age);
}
19

一般情况下推荐传递结构体的指针,而不是直接进行值传递。因为如果结构体非常大的话,光是数据拷贝就需要花费很大的精力,并且某些情况下可能根本用不到结构体中的所有数据,所以完全没必要浪费空间,使用指针反而是一种更好的方式。

联合体

联合体也可以在内部定义很多种类型的变量,但是它与结构体不同的是,它所有的变量共用同一个空间

// 定义一个联合体类型唯一不同的就是前面的union了
union Object {   int a;char b;float c;
};

来看看一个神奇的现象:

#include <stdio.h>union Object {int a;char b;float c;
};int main() {union Object object;// 先给a赋值66object.a = 66;           // 访问bprintf("%d", object.b);  
}
66

可以看到,修改的是 a,但 b 也变成 66 了。

这是因为它们共用了内存空间,实际上先将 a 修改为 66,那么就将这段内存空间上的值修改为了 66,因为内存空间共用,所以当读取 b 时,也会从这段内存空间中读取一个 char 长度的数据出来,所以得到的也是 66。

#include <stdio.h>union Object {int a;char b;float c;
};int main() {union Object object;object.a = 128;printf("%d", object.b);
}
-128

因为:128 = 1000 0000,所以用 char 读取后,由于第一位是符号位,于是就变成了 -128。

那么联合体的大小又是如何决定的呢?

#include <stdio.h>union Object {int a;char b;float c;
};int main() {printf("%lu", sizeof(union Object));
}
4

实际上,联合体的大小至少是其内部最大类型的大小,这里最大是 int 所以就是 4。当然,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

联合体的其他使用基本与结构体差不多,这里就不提了。

枚举

最后来看一下枚举类型,枚举类型一般用于表示一些预设好的整数常量,比如风扇有低、中、高三个档位,所以总是希望别人使用预设好的这三个档位,而不希望使用其他的档位,因为风扇就只设计了这三个档位。

这时就可以告诉别人,风扇有哪几个档位,这种情况使用枚举就非常适合。在程序中,只能使用基本数据类型对这三种档位进行区分,这样显然可读性不够,别人怎么知道哪个代表哪个档位呢?而使用枚举就没有这些问题了。

可以创建多个自定义名称的枚举,命名规则和变量差不多。可以每一个枚举对应一个整数值,这样的话,就不需要去记忆每个数值代表的是什么档位了,直接根据枚举的名称来进行分辨:

#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {// 直接定义即可,类型为enum + 枚举名称,后面是变量名称,值可以直接写对应的枚举enum status a = low;printf("%d", a);
}
1

进行判断也会方便很多:

#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {enum status a = high;// 判断起来就方便多了if (a == low) {printf("低档位");} else if (a == high) {printf("高档位");} else {printf("中档位");}
}

当然也可以直接加入到switch语句中:

#include <stdio.h>enum status { low = 1,middle = 2,high = 3 };int main() {enum status a = high;switch (a) {case low:printf("低档位");break;case high:printf("高档位");break;case middle:printf("中档位");break;default:printf("不存在的档位");}
}
高档位

不过在枚举变量定义时需要注意:

// 如果不给初始值的话,那么会从第一个枚举开始,默认值为0,后续依次+1
enum status {low, middle, high};   

所以这里的 low 就是 0,middle 就是 1,high 就是 2 了。

如果中途设定呢?

// 这里我们给middle设定为6
enum status {low, middle = 6, high};   

这时 low 由于是第一个,所以还是从 0 开始,不过 middle 这里已经指定为 6 了,所以紧跟着的 high 初始值就是middle 的值 +1 了,因此 low 现在是 0,middle就是 6,high就是 7 了。

typedef关键字

这里最后还要提一下 typedef 关键字,这个关键字用于给指定的类型起别名。

// typedef 类型名称 自定义类型别名
typedef int lbwnb;  

比如这里给 int 起了一个别名 lbwnb,那么现在不仅可以使用 int 来表示一个 int 整数,而且也可以使用别名作为类型名称了:

#include <stdio.h>typedef int lbwnb;int main() {// 类型名称直接写成别名,实际上本质还是intlbwnb i = 666;printf("%d", i);
}
666

再比如:

#include <stdio.h>// const char * 我们就起个名称为String表示字符串
typedef const char* String;  int main() {// 这样就很像Java了String str = "Hello World!"; printf(str);
}
Hello World!

当然除了基本类型之外,包括指针、结构体、联合体、枚举等都可以使用这个关键字来完全起别名操作:

#include <stdio.h>// 为了方便可以直接写到后面
typedef struct test {int age;char name[10];
} Student;  int main() {// 直接使用别名,甚至struct关键字都不用加了Student student = {18, "小明"};
}

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

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

相关文章

黑神话悟空mac可以玩吗

黑神话悟空mac上能不能玩对于苹果玩家来说很重要&#xff0c;那么黑神话悟空mac可以玩吗&#xff1f;目前是玩不了了&#xff0c;没有针对ios系统的版本&#xff0c;只能之后在云平台上找找了&#xff0c;大家可以再观望下看看。 黑神话悟空mac可以玩吗 ‌使用CrossOver‌&…

PyCharm 调试 Xinference 遇到问题及解决方案

本文使用的 PyCharm 2024.2.1 版本&#xff0c;如果使用低版本 PyCharm&#xff0c;那么在调试 Xinference v0.15.1 源码时可能会报错 Connection to Python debugger failed Socket closed。 一.PyCharm 调试 Xinference 源码 由于 Xinference 中的一些依赖包仅支持 Linux&a…

用友U8二次开发工具KK-FULL-*****-EFWeb使用方法

1、安装: 下一步&#xff0c;下一步即可。弹出黑框不要关闭&#xff0c;让其自动执行并关闭。 2、服务配置&#xff1a; 输入服务器IP地址&#xff0c;选择U8数据源&#xff0c;输入U8用户名及账号&#xff0c;U8登录日期勾选系统日期。测试参数有效性&#xff0c;提示测试通过…

【Obsidian】当笔记接入AI,Copilot插件推荐

当笔记接入AI&#xff0c;Copilot插件推荐 自己的知识库笔记如果增加AI功能会怎样&#xff1f;AI的回答完全基于你自己的知识库余料&#xff0c;是不是很有趣。在插件库中有Copilot插件这款插件&#xff0c;可以实现这个梦想。 一、什么是Copilot&#xff1f; 我们知道githu…

FTP管理工具 FileZilla Pro v3.66.5 中文绿色便携版

FileZilla 是一款跨平台的多线程FTP工具&#xff0c;支持SL/TLS (FTPS)协议、SFTP等多种主流的传输协议&#xff0c;软件采用了有条理、简洁的用户界面&#xff0c;支持多站点管理&#xff0c;可以管理多个FTP站点&#xff0c;可以自由新建站点&#xff0c;提供了一个简单化&am…

vue websocket 使用

基于webSocket通信的库主要有 socket.io&#xff0c;SockJS 关于SockJS的使用 先安装 sockjs-client 和 stompjs npm install sockjs-client npm install stompjs import SockJS from sockjs-client; import Stomp from stompjs; export default { data () { …

Android 内置应用裁剪

文章目录 查询目标 APK 的 Android.mk&#xff08;或 Android.bp&#xff09;文件apk裁剪方式1.注释或删除.mk/.bp文件2.将 APK 名称加入“OVERRIDES”配置项中3.自定义“PRODUCT_PACKAGES_REMOVE”配置项 查询目标 APK 的 Android.mk&#xff08;或 Android.bp&#xff09;文件…

LabVIEW机械手视觉引导系统

开发了LabVIEW软件和硬件工具开发的高精度机械手视觉引导系统。系统通过高效的视觉识别和精确的机械操作&#xff0c;提升工业自动化领域的生产效率和操作精度。 项目背景&#xff1a; 随着工业自动化的不断发展&#xff0c;对生产效率和精确度的要求也日益增高。传统的机械手…

Linux 安装JDK8和卸载

目录 一、下载JDK8的rpm包 二、安装JDK 三、设置环境变量 Linux环境下安装JDK的方式有多种&#xff0c;可以通过rpm包、yum安装或者tar.gz压缩包。本章节会教大家通过前两者方式来安装JDK&#xff0c;压缩包的形式因为下载压缩包后上传到服务器环境下&#xff0c;将压缩包解…

Unity 设计模式 之 【什么是设计模式】/ 【为什么要使用设计模式】/ 【架构和设计模式的区别】

Unity 设计模式 之 【什么是设计模式】/ 【为什么要使用设计模式】/ 【架构和设计模式的区别】 目录 Unity 设计模式 之 【什么是设计模式】/ 【为什么要使用设计模式】/ 【架构和设计模式的区别】 一、简单介绍 二、 Unity 设计模式 1、Unity 开发中使用设计模式的特点 2…

【Qt网络编程】Tcp多线程并发服务器和客户端通信

目录 一、编写思路 1、服务器 &#xff08;1&#xff09;总体思路widget.c&#xff08;主线程&#xff09; &#xff08;2&#xff09;详细流程widget.c&#xff08;主线程&#xff09; &#xff08;1&#xff09;总体思路chat_thread.c&#xff08;处理聊天逻辑线程&…

【Elasticsearch系列十四】Elasticsearch

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

MySQL:事务的ACID特性隔离级别脏读、不可重复读、幻读、Next-Key锁——场景复现

目录 1、什么是事务 2、 事务的ACID特性 2.1 事务的隔离性 3、为什么要使用事务&#xff1f; 4、查看支持事务的存储引擎 5、使用事务 5.1 控制事务 5.1.1 开启事务 5.1.2 关闭事务 5.2 开始一个事务&#xff0c;执行修改后回滚 5.3 开始一个事务&#xff0c;执行修…

使用Addressables+SpriteAtlas打包产生冗余

1&#xff09;使用AddressablesSpriteAtlas打包产生冗余 2&#xff09;使用SBP打AssetBundle脚本引用丢失 3&#xff09;Unity构建后处理&#xff08;IPostprocessBuildWithReport等接口&#xff09;抛出异常后&#xff0c;构建不会停止 4&#xff09;Unity 2022.3.0版本使用Oc…

谷歌论文提前揭示o1模型原理:AI大模型竞争或转向硬件

Open AI最强模型o1的护城河已经没有了&#xff1f;仅在OpenAI发布最新推理模型o1几日之后&#xff0c;海外社交平台 Reddit 上有网友发帖称谷歌Deepmind在 8 月发表的一篇论文内容与o1模型原理几乎一致&#xff0c;OpenAI的护城河不复存在。 谷歌DeepMind团队于今年8月6日发布…

我的AI工具箱Tauri版-VideoClipMixingCut视频批量混剪

本教程基于自研的AI工具箱Tauri版进行VideoClipMixingCut视频批量混剪。 VideoClipMixingCut视频批量混剪 是自研AI工具箱Tauri版中的一款强大工具&#xff0c;专为自动化视频批量混剪设计。该模块通过将预设的解说文稿与视频素材进行自动拼接生成混剪视频&#xff0c;适合需要…

数据结构 ——— 算法的时间复杂度

目录 时间复杂度的概念 时间复杂度函数式 大O的渐进表示法的概念 大O的渐进表示法 时间复杂度的概念 在计算机科学中&#xff0c;算法的时间复杂度是一个函数&#xff08;数学上的函数式&#xff09;&#xff0c;它定量描述了该算法的运行时间&#xff0c;一个算法执行所耗…

java工具安装教程

提示:先安装软件打开后关闭&#xff0c;在执行魔法操作 解压后会多个文件夹&#xff0c;从文件夹打开 要魔法哪款软件就打开对应的魔法脚本 比如&#xff1a;idea就运行idea魔法 点击打开 显示下面弹窗则成功&#xff0c;点击确定即可 打开IDEA查看&#xff1a;

Arthas thread(查看当前JVM的线程堆栈信息)

文章目录 二、命令列表2.1 jvm相关命令2.1.2 thread&#xff08;查看当前JVM的线程堆栈信息&#xff09;举例1&#xff1a;展示[数字]线程的运行堆栈&#xff0c;命令&#xff1a;thread 线程ID举例2&#xff1a;找出当前阻塞其他线程的线程 二、命令列表 2.1 jvm相关命令 2.…

面试题高频之token无感刷新(vue3+node.js)

无感刷新的基本原理 使用刷新令牌&#xff08;refresh token&#xff09;&#xff1a; ○ 应用程序在首次登录成功后会获得一个访问令牌&#xff08;access token&#xff09;和一个刷新令牌&#xff08;refresh token&#xff09;。 ○ 访问令牌通常有较短的有效期&#xff0…