【c语言】轻松拿捏自定义类型

简介: 本文介绍了C语言中的三种自定义类型:结构体、联合体和枚举类型。结构体可以包含多个不同类型的成员,支持自引用和内存对齐。联合体的所有成员共享同一块内存,适用于判断机器的大小端。枚举类型用于列举固定值,增加代码的可读性和安全性。文中详细讲解了每种类型的声明、特点和使用方法,并提供了示例代码。

前言

       在c语言当中,除了内置的数据类型之外,还有自定义类型,它能够让我们更加方便、灵活地实现各种功能。今天我们主要来学一学三种自定义类型:结构体、联合体和枚举类型


一、结构体

1.结构体类型的定义和使用

1.1 结构体类型声明

结构体可以含有多个结构成员,成员的类型可以不同。它的声明方式是:


struct xxxx

{

       type1 x;

       type2 y;

       ......

};


这里的struct是结构体的关键字,xxxx是结构体标签,x和y是结构成员变量。例如,我们想要用结构体来描述一个学生的信息:

struct student
{
    char name[20];//姓名
    char id[20];//学号
    int age;//年龄
    char sex[5];//性别
};

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

       在声明了结构体之后,我们就可以尝试创建一个结构体变量并对其初始化。例如:

#include <stdio.h>
 
struct student
{
    char name[20];
    char id[20];
    int age;
    char sex[5];
};
 
int main()
{
    struct student s1 = { "zhangsan","000001",18,"男" };
    struct student s2 = { .age = 15,.name = "wangwu",.sex = "女",.id = "000002" };
    return 0;
}

这里需要注意:struct student是一个整体,表示的是该结构体的类型名;s1,s2是变量名。

结构体的初始化与数组类似,都是使用大括号,中间用逗号隔开。初始化的内容要按照顺序,如果不按照顺序来初始化,则在成员变量名前加一个点,再采用赋值的方法初始化


结构体变量创建也可以在声明大括号之后:

struct student
{
    char name[20];
    char id[20];
    int age;
    char sex[5];
}s1;

要注意:这里的s1是全局变量


1.3 结构体变量成员的访问

       接下来,我们在之前代码的基础上,打印学生的信息。

#include <stdio.h>
 
struct student
{
    char name[20];
    char id[20];
    int age;
    char sex[5];
};
 
int main()
{
    struct student s1 = { "zhangsan","000001",18,"男" };
    printf("%s\n", s1.name);//结构成员的访问
    printf("%s\n", s1.id);
    printf("%d\n", s1.age);
    printf("%s\n", s1.sex);
    return 0;
}

运行结果:



可以看到,结构成员的值被一一打印出来。这里使用了“ . ”操作符来访问结构体成员变量。如果是结构体指针类型,在访问成员变量时,则要解引用之后再使用“ . ”操作符或者使用“->”操作符


1.4 结构体的特殊声明(匿名结构体类型)

       在声明结构体的时候,可以不完全声明。例如:

struct
{
    int a;
    char c;
}x;

这样的结构体声明省略了结构体标签,并且一般会同时创建一个结构体变量,否则就无法使用。注意:匿名结构体类型只能使用一次,无法在主函数中创建该结构体的新变量


1.5 结构体的自引用

       首先看一个结构体的声明:

struct stu
{
    int a;
    struct stu b;
};

上述代码是否正确?


       我们可以看到,这个结构体的成员变量中,有一个变量的类型是结构体本身。这就导致这个结构体是无限嵌套的,它所占的内存大小不可知。所以这种写法是错误的。但是,我们可以使这个结构体成员变量为一个结构体本身的指针类型:

struct stu
{
    int a;
    struct stu* p;
};

由于这是一个指针类型,它的大小是确定的(4/8字节),所以这种写法是正确的。我们将这种结构体声明称为结构体的自引用


       结构体的自引用常常用于一些数据结构的定义。


2.结构体内存大小的计算(结构体内存对齐)

       我们首先看一段代码:

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

这段代码用于计算这个结构体的内存大小。按理来说,char占一个字节,int占四个字节,这个结构体总共应该占5个字节。那么真实结果是这样吗?我们看看运行结果:



为什么是8个字节而不是5个呢?这就需要我们学习一个概念:结构体内存对齐


首先介绍一下结构体内存对齐的规则:


1.结构体的第一个成员对齐到和结构体的起始地址的偏移量为0的地址处,也就是说第一个成员的偏移量记为0。

2.其他的成员要对齐到该成员的对齐数整数倍的地址处。

(对齐数:编译器默认对齐数与该成员内存大小的较小值;在VS环境中,默认对齐数是8;linux系统中,没有默认对齐数,对齐数就是该成员内存大小)

3.结构体的总大小为结构成员中最大的对齐数的整数倍。

4.嵌套结构体的情况:则内层的结构体要对齐到自己成员中最大对齐数的整数倍处;结构体的总大小为结构成员中最大对齐数的整数倍(结构成员包含内层结构体的成员)。


根据以上规则,我们来计算一下刚才结构体的内存大小:



我们可以看到,内存对齐还造成了三个字节空间的浪费。那为什么会有内存对齐呢?


       原因如下:


1.平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取到某些特定类型的数据,否则会抛出硬件异常。


2.性能原因:为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存只需要做一次内存访问。假设一个处理器总是从内存中取八个字节,则地址必须是八的倍数。如果我们能够保证将所有double类型的数据地址都对齐成八的倍数,那么就可以节省大量的内存访问时间。说白了,结构体内存对齐就是以空间换时间的做法。


所以,当我们想要在满足时间需求的情况下,尽量节省空间,我们可以在结构体声明时,将内存小的结构成员聚集在一起。例如:

#include <stdio.h>
 
struct s1
{
    char a;
    int b;
    char c;
};
 
struct s2
{
    char a;
    char c;
    int b;
};
 
int main()
{
    printf("s1的内存大小是%zd\n", sizeof(struct s1));
    printf("s2的内存大小是%zd\n", sizeof(struct s2));
    return 0;
}

运行结果:



当然,如果你想要使结构体所占的内存达到最小,也可以通过修改默认对齐数的方式实现。修改方法是:


#pragma pack(n)   n是你想要的默认对齐数值。


我们试着使用一下它:

#include <stdio.h>
 
#pragma pack(1)//调整默认对齐数为1
 
struct s
{
    char c;
    int a;
};
 
#pragma pack()//还原默认对齐数
 
int main()
{
    printf("%zd\n", sizeof(struct s));
}

运行结果:



3.结构体传参

       当我们写的程序需要对结构体进行操作的时候,常常会定义函数,然后将结构体作为参数。举一个例子:

#include <stdio.h>
 
struct s
{
    int arr[1000];
    int m;
};
 
void fun1(struct s s1)
{
    printf("%d\n", s1.arr[3]);
}
 
void fun2(struct s* s1)
{
    printf("%d\n", s1->arr[3]);
}
 
int main()
{
    struct s s1 = { {0},1 };
    fun1(s1);
    fun2(&s1);
    return 0;
}

fun1和fun2哪个更好呢?


实际上,fun2更好。原因如下:


1.函数形参是实参的一份临时拷贝,在函数中修改结构体的内容,主函数中的结构体内容不会改变。

2.如果结构体内存较大,函数就要开辟一块和结构体同样大小的内存空间,占用内存较大。而传递结构体指针时,函数只开辟了4/8个字节的内存空间。


二、联合体

       在学习了结构体之后,我们来了解一下联合体。


1.联合体类型的声明

       和结构体一样,联合体也含有多个成员,成员的类型可以不同。它的声明方法和结构体类似:


union xxxx

{

       type1 x;

       type2 y;

       ......

};


只不过它的关键字是union,结构体是struct。


2.联合体的特点

       联合体有如下特点:


1.联合体所有成员共用同一块内存空间,所以联合体也叫做共用体。


2.给其中一个成员变量赋值,其他成员变量的值也跟着变化。

#include <stdio.h>
 
union un
{
    int a;
    char c;
};
 
int main()
{
    union un x = { 0 };
    printf("%p\n", &(x.a));
    printf("%p\n", &(x.c));
    printf("%p\n", &x);
    return 0;
}

运行结果:



可以看到,三者的地址相同,说明两个成员变量确实用的是同一块内存空间。接着我们尝试修改一下成员变量的值:

#include <stdio.h>
 
union un
{
    int a;
    char c;
};
 
int main()
{
    union un x = { 0 };
    x.a = 0x11223344;
    x.c = 0x55;
    printf("%x\n", x.a);
    return 0;
}



可以看到,当修改成员c的时候,成员a的第一个字节内容也被修改了。根据它,我们就可以画图表示一下联合体的内存占用情况:



3.联合体大小的计算

       由于联合体的成员变量是共用同一块内存空间的,所以它的内存大小计算并没有结构体那般复杂:


1.联合体的大小至少是最大成员的大小。

2.当最大成员大小不是最大对齐数的整数倍的时候,它就要对齐到最大对齐数的整数倍处。


4.联合体的使用

       联合体可以用于判断当前机器的大小端。这里举个例子:

#include <stdio.h>
 
union un 
{
    int a;
    char c;
};
 
int main()
{
    union un x = { 0 };
    x.a = 1;
    printf("%d", x.c);
    return 0;
}

运行结果:



这里我们将整形成员a赋值为1。如果此时字符型的c值为1,则说明整形的最低位的值放在了最低地址上,就是小端;若是0则为大端。


三、枚举类型

       所谓枚举,就是一一列举的意思,对于某个事件,将可能的取值一一列举出来,就变成了枚举类型。比如:一个星期有七天,分别是周一、周二...可以一一列举出来。再比如:一年有十二个月,可以一一列举出来。


1.枚举类型的声明方法

       拿一周七天来举例,它的声明方法如下:

enum week//枚举类型
{
    Mon,
    Tue,
    Wed,
    Thor,
    Fri,
    Sat,
    Sun
};

这里要注意:这些成员都是有值的,如果不特定赋值,则第一个成员的值为0,之后的成员依次+1递增。

2.枚举类型的优点

       既然枚举值就像宏常量一样,那么为什么还要使用枚举呢?


1.它增加了代码的可读性和可维护性

2.与宏常量相比,枚举类型有类型检查,更加安全。

3.由于调试时#define定义的符号会被替换,而枚举不会,就便于调试。

4.使用方便,一次可以定义多个相关的变量

5.枚举类型是有作用域的,而宏常量没有,可以再某个函数体内单独使用


总结

       今天咱们学习了三种自定义类型:结构体、联合体和枚举,以及它们的定义方式、特点和使用。之后博主会和大家分享动态内存管理的相关知识,如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关文章
|
17天前
|
存储 弹性计算 人工智能
阿里云Alex Chen:普惠计算服务,助力企业创新
本文整理自阿里云弹性计算产品线、存储产品线产品负责人陈起鲲(Alex Chen)在2024云栖大会「弹性计算专场-普惠计算服务,助力企业创新」中的分享。在演讲中,他分享了阿里云弹性计算,如何帮助千行百业的客户在多样化的业务环境和不同的计算能力需求下,实现了成本降低和效率提升的实际案例。同时,基于全面升级的CIPU2.0技术,弹性计算全线产品的性能、稳定性等关键指标得到了全面升级。此外,他还宣布了弹性计算包括:通用计算、加速计算和容器计算的全新产品家族,旨在加速AI与云计算的融合,推动客户的业务创新。
|
24天前
|
存储 人工智能 弹性计算
产品技术能力飞跃,阿里云E-HPC荣获“CCF 产品创新奖”!
9月24日,在中国计算机学会举办的“2024 CCF 全国高性能计算学术年会”中,阿里云弹性高性能计算(E-HPC)荣获「 CCF HPC China 2024 产品创新奖」。这也是继 2022 年之后,阿里云E-HPC 再次荣获此奖项,代表着阿里云在云超算领域的持续创新结果,其产品能力和技术成果得到了业界的一致认可。
|
7天前
|
SQL 人工智能 安全
【灵码助力安全1】——利用通义灵码辅助快速代码审计的最佳实践
本文介绍了作者在数据安全比赛中遇到的一个开源框架的代码审计过程。作者使用了多种工具,特别是“通义灵码”,帮助发现了多个高危漏洞,包括路径遍历、文件上传、目录删除、SQL注入和XSS漏洞。文章详细描述了如何利用这些工具进行漏洞定位和验证,并分享了使用“通义灵码”的心得和体验。最后,作者总结了AI在代码审计中的优势和不足,并展望了未来的发展方向。
|
3天前
|
负载均衡 算法 网络安全
阿里云WoSign SSL证书申请指南_沃通SSL技术文档
阿里云平台WoSign品牌SSL证书是由阿里云合作伙伴沃通CA提供,上线阿里云平台以来,成为阿里云平台热销的国产品牌证书产品,用户在阿里云平台https://www.aliyun.com/product/cas 可直接下单购买WoSign SSL证书,快捷部署到阿里云产品中。
1843 6
阿里云WoSign SSL证书申请指南_沃通SSL技术文档
|
1天前
|
存储 安全 Oracle
【灵码助力安全3】——利用通义灵码辅助智能合约漏洞检测的尝试
本文探讨了智能合约的安全性问题,特别是重入攻击、预言机操纵、整数溢出和时间戳依赖性等常见漏洞。文章通过实例详细分析了重入攻击的原理和防范措施,展示了如何利用通义灵码辅助检测和修复这些漏洞。此外,文章还介绍了最新的研究成果,如GPTScan工具,该工具通过结合大模型和静态分析技术,提高了智能合约漏洞检测的准确性和效率。最后,文章总结了灵码在智能合约安全领域的应用前景,指出尽管存在一些局限性,但其在检测和预防逻辑漏洞方面仍展现出巨大潜力。
|
6天前
|
Web App开发 算法 安全
什么是阿里云WoSign SSL证书?_沃通SSL技术文档
WoSign品牌SSL证书由阿里云平台SSL证书合作伙伴沃通CA提供,上线阿里云平台以来,成为阿里云平台热销的国产品牌证书产品。
1777 2
|
15天前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
22天前
|
存储 人工智能 缓存
AI助理直击要害,从繁复中提炼精华——使用CDN加速访问OSS存储的图片
本案例介绍如何利用AI助理快速实现OSS存储的图片接入CDN,以加速图片访问。通过AI助理提炼关键操作步骤,避免在复杂文档中寻找解决方案。主要步骤包括开通CDN、添加加速域名、配置CNAME等。实测显示,接入CDN后图片加载时间显著缩短,验证了加速效果。此方法大幅提高了操作效率,降低了学习成本。
5027 15
|
9天前
|
人工智能 关系型数据库 Serverless
1024,致开发者们——希望和你一起用技术人独有的方式,庆祝你的主场
阿里云开发者社区推出“1024·云上见”程序员节专题活动,包括云上实操、开发者测评和征文三个分会场,提供14个实操活动、3个解决方案、3 个产品方案的测评及征文比赛,旨在帮助开发者提升技能、分享经验,共筑技术梦想。
1015 147
|
17天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1582 12