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

news2024/9/20 14:53:54

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

结构体

比如现在要保存 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赋值66
    object.a = 66;           
    // 访问b
    printf("%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() {
    // 类型名称直接写成别名,实际上本质还是int
    lbwnb 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.coloradmin.cn/o/2149187.html

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

相关文章

本地部署一个轻量化智能聊天服务Vocechat并实现异地远程交互

文章目录 前言1. 拉取Vocechat2. 运行Vocechat3. 本地局域网访问4. 群晖安装Cpolar5. 配置公网地址6. 公网访问小结 7. 固定公网地址 前言 本文主要介绍如何在本地群晖NAS搭建一个自己的聊天服务Vocechat&#xff0c;并结合内网穿透工具实现使用任意浏览器远程访问进行智能聊天…

我的AI工具箱Tauri版-VideoReapeat视频解说复述克隆

本教程基于自研的AI工具箱Tauri版进行VideoReapeat视频解说复述克隆。 VideoReapeat视频解说复述克隆 是自研的AI工具箱Tauri版中的一款专用模块&#xff0c;旨在通过AI技术对视频解说内容进行复述和克隆。该工具可自动洗稿并重新生成视频解说&#xff0c;通过简单配置即可对大…

协同编程的艺术:SIDE 让团队协作更上一层楼

一、协同编程的现状 在当前软件开发中&#xff0c;团队协作面临着诸多挑战。沟通不畅常常导致项目进度延迟&#xff0c;版本控制复杂使得代码合并困难重重。传统 IDE 在协同工作方面存在明显的局限性&#xff0c;缺乏实时协作功能&#xff0c;团队成员之间的沟通工具也不够完善…

如何上传tauri项目到csdn gitcode

如何上传tauri项目到csdn gitcode 首先保证项目目录有.gitignore&#xff0c;避免不必要的文件上传分享。 gitignore文件 # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log*node_modules dist dist-ssr *.local# Editor …

浅谈如何入门游戏漏洞挖掘,零基础入门到精通,收藏这一篇就够了

引言 每个白帽子心中&#xff0c;都有一团火 那就是对网络安全的信念 哪怕&#xff0c;是一条少有人走的路 补天希望与更多同路人携手&#xff0c;再向前一步 2021年补天平台启动“技术模块” 牛年新春特别分享单元 大年初一至初七 每天一个精选技术方向 每天一条原创…

QMT获取可转债行情数据方法介绍!支持QMT量化软件的券商平台?

获取可转债行情 为了获取转债的日线/1m/1d的k数据&#xff0c;以通过数据订阅形式获取最新行情subscribe_quote。如果您需要获取历史数据&#xff0c;可以使用download_history_data函数下载相关数据&#xff0c;然后使用get_market_data_ex函数提取所需的信息。这样&#xff…

Smartbi AIChat应用案例-某保险集团内部经营分析

案例简介 某保险企业面对大数据时代下的业务发展挑战&#xff0c;面临数据查询、分析与探索需求的激增。然而&#xff0c;当前的数据基础设施与应用方式已难以满足业务快速发展的需求。企业面临分析链路长、报表时效性低、制作效率低且灵活度不足、无效报表堆积等困局。为打破这…

探索《越南语翻译通》App:高效语言学习的利器

在当今这个全球化的世界里&#xff0c;语言学习变得越来越重要。随着科技的发展&#xff0c;我们有了更多便捷的工具来帮助我们学习新的语言。今天&#xff0c;我们来探索一款名为《越南语翻译通》的App&#xff0c;它正逐渐成为语言学习者的新宠。 《越南语翻译通》App的特点…

Linux安装(带VM激活码)

1.安装Vmare WorkStation虚拟机 VMware Workstation Pro是VMware&#xff08;威睿公司&#xff09;发布的一代虚拟机软件&#xff0c;中文名称为"VMware 工作站".它的主要功能是可以给用户在单一的桌面上同时运行不同的操作系统&#xff0c;它也是可进行开发、测试、…

笑不活了!薅走羊毛党20亿跑路?这生意我横竖没看懂……

昨天&#xff0c;各种八卦群传出一张非常离谱的截图&#xff0c;被无数吃瓜群众称为这是今年最好笑的事&#xff0c;纷纷表示能不能把我拉群里&#xff0c;让我再笑会……。 截图大概是这样的&#xff0c;博主开头先是发了几个哈哈哈&#xff0c;隔着屏幕都能感受到这事到底有…

3个月9次迭代,快手可灵AI面向全球发布1.5模型

9月19日&#xff0c;可灵AI迎来重磅升级&#xff0c;视频生成新增可灵1.5模型&#xff0c;在画质质量、动态质量、美学表现、运动合理性以及语义理解等方面均有显著提升。与此同时&#xff0c;可灵AI还引入了全新的“运动笔刷”功能&#xff0c;进一步提升视频生成的精准控制能…

电能计量,三相电基础

1. 三相交流电基础 三相交流电由三个频率相同、振幅相等、相位依次互相差120的交流电势组成。这样组织的三相电有一个特点&#xff0c;就是任何一个时刻&#xff0c;三相电的电压之和都等于零。这个特性使得本来需要六根导线来传输三组电压的减少到了三根导线&#xff0c;这样…

访问者模式:将算法与对象结构分离的设计模式

在软件开发中&#xff0c;我们常常需要对对象结构中的各个元素进行不同的操作。比如在编译器中&#xff0c;我们可能需要对抽象语法树&#xff08;AST&#xff09;的各个节点进行语法检查、代码生成、优化等操作。如果将这些操作直接嵌入到节点类中&#xff0c;会导致类的职责过…

一、桥式整流电路

桥式整流电路 1、二极管的单向导电性: 伏安特性曲线: 理想开关模型和恒压降模型 2、桥式整流电流流向过程 输入输出波形: 3、计算:Vo,lo,二极管反向电压。 学习心得

数字病理图像处理:分割、合成与数据增强研究|顶刊精析·24-09-20

小罗碎碎念 今日精析&#xff1a;Medical Image Analysis 这篇文章介绍了一种结合了先进分割模型和生成对抗网络的病理切片图像分析流程&#xff0c;用于提高癌症诊断的准确性和效率。 作者角色姓名单位名称&#xff08;中文&#xff09;第一作者Muhammad Jehanzaib博阿齐奇大学…

安卓13修改设置设备型号和设备名称分析与更改-android13设置设备型号和设备名称更改

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 用户要定制一些系统显示的设备型号和设备名称,这就需要我们分析设置里面的相关信息来找到对应的位置进行修改了。 2.问题分析 像这种信息要么是config.xml里面写死了,要…

一、编译原理(引论)

目录 【一】、引论 一、编译器 1、编译器 2、编译器与解释器 3、编译器结构 【一】、引论 一、编译器 1、编译器 &#xff08;1&#xff09;编译器&#xff1a;将人类易懂的 高级语言 翻译成 硬件可执行的目标机器语言 &#xff08;2&#xff09; 高级语言 ⚫ 直接面…

数据结构------二叉树简单介绍及实现

如果不是满二叉树或者完全二叉树&#xff0c;就要用链式存储 //搜索二叉树&#xff1a;左子树的所有值比根小&#xff0c;右子树的所有值比根大 // 实现查找&#xff0c;最多找高度次&#xff08;类似二分法&#xff09; //二分查找存在的问题&#xff1a…

【C++】——多态详解

目录 1、什么是多态&#xff1f; 2、多态的定义及实现 2.1多态的构成条件 ​2.2多态语法细节处理 2.3协变 2.4析构函数的重写 2.5C11 override 和 final关键字 2.6重载—重写—隐藏的对比分析 3、纯虚函数和抽象类 4、多态的原理分析 4.1多态是如何实现的 4.2虚函数…

TESSY创建需要高级桩的测试用例

需要打高级桩的情况如下&#xff1a; 1) 使用到桩函数的返回值&#xff1b; 2) 如果函数有形参&#xff0c;并且需要接口传参检测&#xff1b; 我们以tessy5.1 IDE为例&#xff0c;给大家展示编写一个需要高级桩的测试用例过程。 1、前期的准备工作 可以参考以下文章&…